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.
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)
Edit: pastebin source for form field: http://pastebin.com/j4TnnHTS
further discussion: https://code.djangoproject.com/ticket/21300
It appears that the way to do this is to create a custom form field that returns a naive datetime, then convert that to the timezone the user specifies, then convert that to UTC.
Custom field:
class DateTimeNoTimeZoneField(forms.DateTimeField):
def to_python(self, value):
"""
Validates that the input can be converted to a datetime. Returns a
Python datetime.datetime object.
"""
if value in validators.EMPTY_VALUES:
return None
if isinstance(value, datetime.datetime):
return value
if isinstance(value, datetime.date):
return datetime.datetime(value.year, value.month, value.day)
if isinstance(value, list):
# Input comes from a SplitDateTimeWidget, for example. So, it's two
# components: date and time.
if len(value) != 2:
raise ValidationError(self.error_messages['invalid'])
if value[0] in validators.EMPTY_VALUES and value[1] in validators.EMPTY_VALUES:
return None
value = '%s %s' % tuple(value)
# Try to coerce the value to unicode.
unicode_value = force_text(value, strings_only=True)
if isinstance(unicode_value, six.text_type):
value = unicode_value.strip()
# If unicode, try to strptime against each input format.
if isinstance(value, six.text_type):
for format in self.input_formats:
try:
return self.strptime(value, format)
except (ValueError, TypeError):
continue
raise ValidationError(self.error_messages['invalid'])
Form:
class TZTestForm(forms.ModelForm):
dt = DateTimeNoTimeZoneField()
class Meta:
model = TZTestModel
def clean(self):
tz = self.cleaned_data['timezone']
dt = self.cleaned_data['dt']
dt = pytz.UTC.normalize(tz.localize(dt).astimezone(pytz.UTC))
self.cleaned_data['dt'] = dt
return self.cleaned_data