MattG
MattG

Reputation: 1932

How to use timezones in Django Forms

Timezones in Django...

I am not sure why this is so difficult, but I am stumped. I have a form that is overwriting the UTC dateTime in the database with the localtime of the user. I can't seem to figure out what is causing this.

my settings.py timezone settings look like:

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Toronto'
USE_I18N = True
USE_L10N = False
USE_TZ = True

I am in Winnipeg, my server is hosted in Toronto. My users can be anywhere.

I have a modelfield for each user that is t_zone = models.CharField(max_length=50, default = "America/Winnipeg",) which users can change themselves.

with respect to this model:

class Build(models.Model):
    PSScustomer = models.ForeignKey(Customer, on_delete=models.CASCADE)
    buildStart = models.DateTimeField(null=True, blank=True)
    ...

I create a new entry in the DB using view logic like:

...
now = timezone.now()
newBuild = Build(author=machine,
                PSScustomer = userCustomer,
                buildStart = now,
                status = "building",
                addedBy = (request.user.first_name + ' ' +request.user.last_name),
                ...
                )
newBuild.save()

buildStart is saved to the database in UTC, and everything is working as expected. When I change a user's timezone in a view with timezone.activate(pytz.timezone(self.request.user.t_zone)) it will display the UTC time in their respective timezone.

All is good (I think) so far.

Here is where things go sideways: When I want a user to change buildStart in a form, I can't seem to get the form to save the date to the DB in UTC. It will save to the DB in whatever timezone the user has selected as their own.

Using this form:

class EditBuild_building(forms.ModelForm):
    buildStart = forms.DateTimeField(input_formats = ['%Y-%m-%dT%H:%M'],widget = forms.DateTimeInput(attrs={'type': 'datetime-local','class': 'form-control'},format='%Y-%m-%dT%H:%M'), label = "Build Start Time")
    def __init__(self, *args, **kwargs):# for ensuring fields are not left empty
        super(EditBuild_building, self).__init__(*args, **kwargs)
        self.fields['buildDescrip'].required = True

    class Meta:
        model = Build
        fields = ['buildDescrip', 'buildStart','buildLength'...]

        labels = {
            'buildDescrip': ('Build Description'),
            'buildStart': ('Build Start Time'),
            ...
        }

        widgets = {'buildDescrip': forms.TextInput(attrs={'class': 'required'}),

and this view:

class BuildUpdateView_Building(LoginRequiredMixin,UpdateView):
    model = Build
    form_class = EditBuild_building
    template_name = 'build_edit_building.html'
    login_url = 'login'

    def get(self, request, *args, **kwargs):
        proceed = True
        try:
            instance = Build.objects.get(id = (self.kwargs['pk']))
        except:
            return HttpResponse("<h2 style = 'margin:2em;'>This build is no longer available it has been deleted, please please return to dashboard</h2>")
        if instance.buildActive == False:
            proceed = False
        if instance.deleted == True:
            proceed = False
        #all appears to be well, process request
        if proceed == True:
            form = self.form_class(instance=instance)
            timezone.activate(pytz.timezone(self.request.user.t_zone))
            customer = self.request.user.PSScustomer
            choices = [(item.id, (str(item.first_name) + ' ' + str(item.last_name)))  for item in CustomUser.objects.filter(isDevice=False, PSScustomer = customer)]
            choices.insert(0, ('', 'Unconfirmed'))
            form.fields['buildStrategyBy'].choices = choices
            form.fields['buildProgrammedBy'].choices = choices
            form.fields['operator'].choices = choices
            form.fields['powder'].queryset = Powder.objects.filter(PSScustomer = customer)
            context = {}
            context['buildID'] = self.kwargs['pk']
            context['build'] = Build.objects.get(id = (self.kwargs['pk']))
            return render(request, self.template_name, {'form': form, 'context': context})
        else:
            return HttpResponse("<h2 style = 'margin:2em;'>This build is no longer editable here, or has been deleted, please return to dashboard</h2>")


    def form_valid(self, form):
        timezone.activate(pytz.timezone(self.request.user.t_zone))
        proceed = True
        try:
            instance = Build.objects.get(id = (self.kwargs['pk']))
        except:
            return HttpResponse("<h2 style = 'margin:2em;'>This build is no longer available it has been deleted, please please return to dashboard</h2>")
        if instance.buildActive == False:
            proceed = False
        if instance.deleted == True:
            proceed = False
        #all appears to be well, process request
        if proceed == True:
            form.instance.editedBy = (self.request.user.first_name)+ " " +(self.request.user.last_name)
            form.instance.editedDate = timezone.now()
            print('edited date ' + str(form.instance.editedDate))
            form.instance.reviewed = True
            next = self.request.POST['next'] #grabs prev url from form template
            form.save()
            build = Build.objects.get(id = self.kwargs['pk'])
            if build.buildLength >0:
                anticipated_end = build.buildStart + (timedelta(hours = float(build.buildLength)))
                print(anticipated_end)
            else:
                anticipated_end = None
            build.anticipatedEnd = anticipated_end
            build.save()
            build_thres_updater(self.kwargs['pk'])#this is function above, it updates threshold alarm counts on the build
            return HttpResponseRedirect(next) #returns to this page after valid form submission
        else:
            return HttpResponse("<h2 style = 'margin:2em;'>This build is no longer available it has been deleted, please please return to dashboard</h2>")

When I open this form, the date and time of buildStart are displayed in my Winnipeg timezone, so Django converted from UTC to my timezone, perfect, but when I submit this form, the date in the DB has been altered from UTC to Winnipeg Time. Why is this?

I have tried to convert the submitted time to UTC in the form_valid function, but this does not seem like the right approach. What am I missing here? I simply want to store all times as UTC, but display them in the user's timezone in forms/pages.

EDIT

When I remove timezone.activate(pytz.timezone(self.request.user.t_zone)) from both get and form_valid, UTC is preserved in the DB which is great. But the time displayed on the form is now in the default TIME_ZONE in settings.py. I just need this to be in the user's timezone....

EDIT 2

I also tried to add:

{% load tz %}

{% timezone "America/Winnipeg" %}
    {{form}}
{% endtimezone %}

Which displayed the time on the form correctly, but then when the form submits, it will again remove 1 hour from the UTC time in the DB.

If I change template to:

{% load tz %}

{% timezone "Europe/Paris" %}
    {{form}}
{% endtimezone %}

The time will be displayed in local Paris time. When I submit the form, it will write this Paris time to the DB in UTC+2. So, in summary:

What is happening here!?

Upvotes: 0

Views: 1973

Answers (1)

Kevin Christopher Henry
Kevin Christopher Henry

Reputation: 48902

Put simply: your activate() call in form_valid() comes too late to affect the form field, so the incoming datetime gets interpreted in the default timezone—which in your case is America/Toronto—before being converted to UTC and saved to the database. Hence the apparent time shift.

The documentation doesn't really specify when you need to call activate(). Presumably, though, it has to come before Django converts the string value in the request to the aware Python datetime in the form dictionary (or vice versa when sending a datetime). By the time form_valid() is called, the dictionary of field values is already populated with the Python datetime object.

The most common place to put activate() is in middleware (as in this example from the documentation), since that ensures that it comes before any view processing. Alternatively, if using generic class-based views like you are, you could put it in dispatch().

Upvotes: 3

Related Questions