Search code examples
pythondjangodatetimetimezonepytz

Django saving datetime based on per object timezone


For the majority of my application saving datetimes with the TIME_ZONE and USE_TZ settings are fine. My question is, how can I accomplish saving a model with a datetime that is in UTC, but the datetime is set so that converting back to the users inputted timezone is correct? Model, view, form and html given below. This code will work if USE_TZ = False in the settings.py file, but I would like to keep the timezone for everything else in the project.

Model:

class TZTestModel(models.Model):
    timezone = TimeZoneField()
    dt = models.DateTimeField()

View:

class TZTestView(LoginRequiredMixin, TemplateView):
    template_name = "tz_test.html"

    def get_context_data(self, **kwargs):
        return {
            'form': self.form
        }

    def dispatch(self, request, *args, **kwargs):
        self.form = TZTestForm(self.request.POST or None)
        return super(TZTestView, self).dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        if self.form.is_valid():
            self.form.save()
        return self.render_to_response(self.get_context_data())

Form:

class TZTestForm(forms.ModelForm):
    class Meta:
        model = TZTestModel

    def clean(self):
        timezone = self.cleaned_data['timezone']
        dt = self.cleaned_data['dt']
        dt = timezone.localize(dt)
        self.cleaned_data['dt'] = pytz.UTC.normalize(dt.astimezone(pytz.UTC))
        return self.cleaned_data

Template:

<html>
    <body>
        <form method="post">
            {% csrf_token %}
            {{ form }}
            <input type="submit">
        </form>
    </body>
</html>

Example:

I would like to be able to enter a timezone of 'US/Alaska' and a datetime of today at 13:00, save that as it's UTC value, then be able to convert back to 'US/Alaska' and get the correct value.

Essentially I am trying to save one model's datetime in a different timezone than my application, where the timezone is specified by the user in the same form that the datetime is specified in.


Solution

  • I have had the same issue with object-level timezones.

    I found this blog entry. It's not perfect but it works! and is not too complex. Plus handling the Admin is dealt with.

    Pasting the snippets here:

    Making sure the form is processed in the wanted timezone:

    def view(request):
    
        if request.method == 'POST':
            tz_form = TimeZoneForm(request.POST)
            if tz_form.is_valid():
                tz = tz_form.cleaned_data['event_time_zone']
                timezone.activate(tz)
                # Process the full form now
        else:
            # assuming we have an event object already
            timezone.activate(event.event_time_zone)
            # Continue to create form for display on the web page
    

    Displaying correctly in the Admin list view

    class EventAdmin(admin.ModelAdmin):
        list_display = [..., 'event_datetime_in_timezone', ...]
    
        def event_datetime_in_timezone(self, event):
            """Display each event time on the changelist in its own timezone"""
            fmt = '%Y-%m-%d %H:%M:%S %Z'
            dt = event.event_datetime.astimezone(pytz_timezone(event.event_time_zone))
            return dt.strftime(fmt)
        event_datetime_in_timezone.short_description = _('Event time')
    

    Interpret correctly the datetimne in Admin Add view

    class EventAdmin(admin.ModelAdmin):
        # ...
    
        # Override add view so we can peek at the timezone they've entered and
        # set the current time zone accordingly before the form is processed
        def add_view(self, request, form_url='', extra_context=None):
            if request.method == 'POST':
                tz_form = TimeZoneForm(request.POST)
                if tz_form.is_valid():
                    timezone.activate(tz_form.cleaned_data['event_time_zone'])
            return super(EventAdmin, self).add_view(request, form_url, extra_context)
    

    Handling correctly timezones in the Admin edit view

    class EventAdmin(admin.ModelAdmin):
        # ...
    
        # Override change view so we can peek at the timezone they've entered and
        # set the current time zone accordingly before the form is processed
        def change_view(self, request, object_id, form_url='', extra_context=None):
            if request.method == 'POST':
                tz_form = TimeZoneForm(request.POST)
                if tz_form.is_valid():
                    timezone.activate(tz_form.cleaned_data['event_time_zone'])
            else:
                obj = self.get_object(request, unquote(object_id))
                timezone.activate(obj.event_time_zone)
            return super(EventAdmin, self).change_view(request, object_id, form_url, extra_context)