Trying to get a schedule time to reflect the users time zone automatically.

I’m working on a school scheduling app using using django 4.1.6 and python 3.10.5. I have the app working but I need the schedules to reflect the users time zone.

Basically, if a teacher from Seattle creates a schedule for 7am, then a student in Tokyo should see that schedule as 11pm. I’d like the schedules to reflect the time zone automatically so if a student or teacher leaves town and are in a different time zone than their residence (lets say their traveling for some reason), they won’t need to manually change their time zone.

I have the pytz installed and the settings.py show:

TIME_ZONE = ‘UTC’
USE_I18N = True
USE_TZ = True.

If I’m correct I should be ready to use time zones in my app. I just don’t know how to get a saved time to reflect the individual users time zone.

I’ll include the relevant code here.

models.py:

class Student(models.Model):
user = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
englishName = models.CharField(max_length=200)
studentName = models.CharField(max_length=200)
studentId = models.CharField(max_length=200)
birthday = models.DateField()
gender = models.CharField(max_length=6)
gradeLevel = models.CharField(max_length=8)
since = models.DateField()
duration = models.CharField(max_length=3)
contactNumber = models.CharField(max_length=13, default=00000000)
email = models.EmailField(max_length=200, default=‘address@email.com’)
schedules = models.ManyToManyField(‘Schedule’, blank=True)
profile_picture = models.ImageField(upload_to=‘profile_pictures/’, default=‘profile_pictures/default.jpg’)
timezone = models.CharField(max_length=50, default=timezone.get_default_timezone_name)

def get_first_note_or_none(self):
try:
return self.schedule.notes.filter(student=self).first()
except AttributeError:
return None

def __str__(self):
    return self.studentName

def student_index(request):
data=Student
context = {‘form’: data}
return render(request, ‘student-information.html’, context)

class Teacher(models.Model):
user = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
teacherName = models.CharField(max_length=200)
teacherId = models.CharField(max_length=200)
birthday = models.DateField()
gender = models.CharField(max_length=6)
since = models.DateField()
contactNumber = models.CharField(max_length=13, default=00000000)
email = models.EmailField(max_length=200, default=‘address@email.com’)
profile_picture = models.ImageField(upload_to=‘profile_pictures/’, default=‘profile_pictures/default.jpg’)

def __str__(self):
    return self.teacherName

def teacher_index(request):
data=Teacher
context = {‘form’: data}
return render(request, ‘teacher-information.html’, context)

views.py:

@login_required
@allowed_users(allowed_roles=‘student’)
def selectSchedule(request, schedule_id):
schedule = Schedule.objects.get(pk=schedule_id)
student = request.user.student
schedule_datetime = datetime.combine(schedule.date, schedule.time)
if schedule_datetime > datetime.now() + timedelta(hours=1):
if schedule.capacity > 0:
student.schedules.add(schedule)
schedule.capacity -= 1
schedule.save()
messages.success(request, ‘Schedule added’)
else:
messages.warning(request, ‘Schedule is full’)
else:
messages.warning(request, ‘You can only select a schedule more than 1 hour from the start time’)
return redirect(‘view-schedule’)

template (student to select a schedule):

{% extends ‘main.html’ %}

{% block content %}

<div class="student-schedule-title">
    <h3 class="student-schedule-header">Select Your Schedule</h3>
</div>
<div class="student-schedule-area">
    <div class="student-schedule-date-field">
        
    </div>
    <table class="student-schedule-table">
        <tr>
            <th></th>
            <th>Date</th>
            <th>Time</th>
            <th>Teacher</th>
            <th>Class Duration</th>
        </tr>
        {% for schedule in schedules %}
        {% if schedule.capacity > 0 %}
        <tr>
           <td>
               
               <form action="{% url 'select-schedule' schedule.id %}" method="POST">
                   {% csrf_token %}
                   <input type="hidden" name="schedule_id" value="{{ schedule.id }}">
                   <input type="Submit" value="Select">
               </form>
               
           </td>
           <td>{{ schedule.date }}</td>
           <td>{{ schedule.time }}</td>
           <td>{{ schedule.user.username }}</td>
           <td>{{ schedule.duration }}</td>
       </tr>
       {% endif %}
       {% endfor %}
   </table>
Save
{% endblock content %}

If anyone can guide me through how to get this done I’d appreciate the help.
Thanks.

Manage time zone is a few complex. You have to understand that there is two types of datetime objects, naive and aware. In your case you should manage all datetimes as aware, django timezone has some helps, but you should be aware :slight_smile: that all datetimes has the correct format.

In some very high level you need:

  • When someone inputs a datetime it should be from its timezone and then recorded with the timezone of the server.

  • When you output this date it should be displayed as the timezone of the user.

All that implies that in all your views, models, etc. You should keep datetime with timezones, AKA aware datime objects.

You should carefully read the Python docs about it , and try to go in every step of your code making sure that all datetimes are aware and the cycle I wrote above is Ok.

I read through the docs and I had it working for about a day. Then it stopped working again. I’ve made a number of changes to the code and I verified that the students timezone is being read and their current time is being presented correctly. But, when I try to get the schedules date and time to convert to the students timezone, it simply shows up blank.

I’ll show my new code below, maybe someone can point out the problem.

views.py

@login_required
@allowed_users(allowed_roles=[‘student’])
def makeSchedule(request):
student = request.user.student
current_datetime = datetime.now()

student_timezone = pytz.timezone(student.timezone)
current_datetime = current_datetime.astimezone(student_timezone)

schedules = Schedule.objects.filter(date__gte=current_datetime.date()).order_by(‘date’, ‘time’)
for schedule in schedules:
# Convert date object to datetime object
dt = datetime.combine(schedule.date, datetime.min.time())
dt_aware = timezone.make_aware(dt, timezone=student_timezone)
schedule.date = dt_aware.date()

   schedule.time = schedule.time.astimezone(student_timezone).time()

context = {
‘schedules’: schedules,
‘student’: student,
‘current_datetime’: current_datetime,
}

return render(request, ‘eslbeeph/make-schedule.html’, context)

models.py

class Student(models.Model):
all_timezones = pytz.all_timezones
TIMEZONE_CHOICES = [(timezone, timezone) for timezone in all_timezones]

GENDER = (
    ('Male', 'Male'),
    ('Female', 'Female'),
)

user = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
englishName = models.CharField(max_length=200)
studentName = models.CharField(max_length=200)
studentId = models.CharField(max_length=200)
birthday = models.DateField()
gender = models.CharField(max_length=6, choices=GENDER, default='Male')
gradeLevel = models.CharField(max_length=8)
since = models.DateField()
duration = models.IntegerField()
contactNumber = models.CharField(max_length=13, default=00000000)
email = models.EmailField(max_length=200, default='address@email.com')
schedules = models.ManyToManyField('Schedule', blank=True)
profile_picture = models.ImageField(upload_to='profile_pictures/', default='profile_pictures/default.jpg')
timezone = models.CharField(max_length=50, choices=TIMEZONE_CHOICES, default='UTC')

def get_first_note_or_none(self):
    try:
        return self.schedule.notes.filter(student=self).first()
    except AttributeError:
        return None

def __str__(self):
    return self.studentName

def get_finished_hours(self):
    total_completed_duration = Note.objects.filter(student=self, status='Complete').aggregate(total_duration=Sum('schedule__duration'))
    if total_completed_duration['total_duration']:
        return total_completed_duration['total_duration'] / 60
    else:
        return 0

def get_remaining_hours(self):
    total_completed_duration = Note.objects.filter(student=self, status='Complete').aggregate(total_duration=Sum('schedule__duration'))
    if total_completed_duration['total_duration']:
        total_completed_hours = total_completed_duration['total_duration'] / 60
    else:
        total_completed_hours = 0

    remaining_hours = self.duration - total_completed_hours
    return remaining_hours

def student_index(request):
data=Student
context = {‘form’: data}
return render(request, ‘student-information.html’, context)

def get_default_date():
return timezone.now().date()

def get_default_time():
return (datetime.now() + timedelta(minutes=30)).time()

class Schedule(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
date = models.DateField(null=True, blank=True, default=get_default_date)
time = models.TimeField(null=True, blank=True, default=get_default_time)
duration = models.PositiveSmallIntegerField(default=30)
capacity = models.PositiveSmallIntegerField(default=1)
notes = models.ManyToManyField(‘Note’, blank=True, related_name=‘schedules’)

def get_schedule_datetime(self, student_timezone):
    try:
        tz = pytz.timezone(student_timezone)
    except pytz.UnknownTimeZoneError:
        # Handle the case when the student's timezone is not found in pytz
        tz = pytz.utc

    dt = datetime.combine(self.date, self.time)
    dt_aware = timezone.make_aware(dt, timezone=tz)
    print(f"DEBUG: get_schedule_datetime - Input TimeZone: {student_timezone}, DT in UTC: {dt_aware}")
    return dt_aware

template:

{% extends ‘main.html’ %}
{% load static %}
{% load tz %}

{% block content %}

Select Your Schedule

Current Datetime in Your Timezone: {{ current_datetime|timezone:student.timezone|date:'M d, Y H:i:s' }}

{% for schedule in schedules %} {% if schedule.capacity > 0 %}
Date Time Teacher Class Duration
                <form action="{% url 'select-schedule' schedule.id %}" method="POST">
                    {% csrf_token %}
                    <input type="hidden" name="schedule_id" value="{{ schedule.id }}">
                    <input type="Submit" value="Select">
                </form>
                
            </td>
            <!--show schedule date for student timezone-->
            <td>{{ schedule.date|timezone:student.timezone|date:'M d' }}</td>
            <td>{{ schedule.time|timezone:student.timezone|time:'H:i A' }}</td>
            <td>{{ schedule.user.username }}</td>
            <td>{{ schedule.duration }}</td>
        </tr>
        {% endif %}
        {% endfor %}
    </table>
</div>
<div class="student-schedule-save">
    <!-- <button class="student-schedule-button">Save</button> -->
</div>
{% endblock content %}

Side note: When posting code, templates, tracebacks, or error messages here, enclose that code between lines of three backtick - ` characters. This means you’ll have a line of ```, then the code, then another line of ```. This forces the forum software to keep your code properly formatted and prevents you from having to do things like trying to mark each line with a >.

First step is identifying where it goes wrong.

Is the issue with the time zone conversion or the query filter?

If it’s the conversion then test that bit of code separately and if it doesn’t work paste that bit of standalone code here.

One likely issue I see is that you have defined two separate fields in your schedule, one each for date and time. A Python datetime.date object does not carry timezone information! (datetime — Basic date and time types — Python 3.11.4 documentation)

This is not how you want to store this data in your Schedule object. If you’re going to reliably work with local times, you want all such values to be stored as a DateTime field. Otherwise, you’re likely going to encounter any number of odd edge cases when using those dates.

Python also makes two constants available, time.min and time.max to allow you to have a reference to the beginning and end of each date.

Duely noted. :slightly_smiling_face: I’ll rework my model. Thanks.

After changing the model and fixing up the views and templates I have the timezones working perfectly. Thanks for helping me out with this.