Recently, I was adding a model into my site to allow me to create an alert for scheduled maintenance that would display to all users.
Because I was going to be the only one actually managing this alert I decided not to create dedicated CRUD views and to instead just use the built-in admin to manage it. However, I got some really weird issue related to an incorrect time zone being used for the scheduled maintenance, that has only appeared for this one model that I manage via the admin site.
Currently, I manage site time zones using the following:
proj/settings.py:
Time zone support enabled
...
# Middleware settings
MIDDLEWARE = [
...,
"main_app.middleware.TimezoneMiddleware",
]
...
# Internationalization settings
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
...
main_app/middleware.py
Middleware to use user’s saved timezone
...
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request: HttpRequest) -> HttpResponse:
if request.user.is_anonymous:
return self.get_response(request)
user_preferences: UserPreferences = request.user.preferences
try:
timezone = pytz.timezone(user_preferences.timezone)
except pytz.UnknownTimeZoneError:
timezone = pytz.timezone("UTC")
tz.activate(timezone)
return self.get_response(request)
main_app/models.py
Saved time zone model
...
class UserPreferences(models.Model):
TIMEZONE_CHOICES = [(tz, tz) for tz in pytz.all_timezones]
user = AutoOneToOneField(settings.AUTH_USER_MODEL, primary_key=True, on_delete=models.CASCADE, related_name="preferences")
...
# A bunch of email-related settings
...
timezone = models.CharField(max_length=100, default="US/Eastern")
objects = UserPreferencesQuerySet.as_manager()
class Meta:
db_table = "user_preferences"
verbose_name_plural = "User Preferences"
def __str__(self) -> str:
return str(self.user)
...
Then here is the model and admin site code used for the maintenance alert:
maintenance/models.py
...
class MaintenanceAlert(models.Model):
id = models.IntegerField(primary_key=True, default=1)
message = models.TextField(verbose_name="Alert Message", max_length=500)
scheduled_start = models.DateTimeField(verbose_name="Scheduled Start")
is_active = models.BooleanField(default=True, verbose_name="Active")
last_changed = models.DateTimeField(auto_now=True, verbose_name="Last Changed")
class Meta:
verbose_name = "Maintenance Alert"
verbose_name_plural = "Maintenance Alert"
def __str__(self):
return self.scheduled_start.strftime("%Y-%m-%d %I:%M %p %Z")
...
maintenance/admin.py
...
@admin.action(description="Mark Active")
def mark_active(modeladmin: MaintenanceAlertAdmin, request: HttpRequest, queryset: QuerySet) -> None:
queryset.update(is_active=True)
@admin.action(description="Mark Inactive")
def mark_inactive(modeladmin: MaintenanceAlertAdmin, request: HttpRequest, queryset: QuerySet) -> None:
queryset.update(is_active=False)
class MaintenanceAlertAdminForm(forms.ModelForm):
class Meta:
model = MaintenanceAlert
fields = ("scheduled_start", "message", "is_active",)
@admin.register(MaintenanceAlert)
class MaintenanceAlertAdmin(admin.ModelAdmin):
form = MaintenanceAlertAdminForm
actions = (mark_active, mark_inactive)
list_display = ("scheduled_start", "message", "alert_level", "is_active")
readonly_fields = ("last_changed",)
sortable_by = tuple()
def alert_level(self, obj: MaintenanceAlert) -> str:
return MaintenanceAlertLevel(obj.alert_level).label
def has_add_permission(self, request: HttpRequest) -> bool:
return not MaintenanceAlert.objects.exists()
This should work fine. I’ve used this same middleware strategy since my first Django project that used timezones and I’ve never had any issues. However, I’ve never actually used the admin site for managing models before, and doing it with this model causes the following to occur:
Note: Apparently as a ‘new user’ I cannot add more than one image, so I have had to cut the less important images from this post.
Creating the alert:
Creating an alert with the following data:
Scheduled Start: "2024-07-30 20:30" # 8:30 PM
Alert Message: "This is an alert message."
Active: True
Alert saved:
The success message says the following:
The Maintenance Alert "2024-07-30 08:30 PM LMT" was added successfully.
Notice the LMT time zone?
Then just below in the table it shows the following in the Scheduled Start
column:
July 30, 2024, 9:26 p.m.
Viewing alert:
Viewing the data shows the same value
In the admin site:
I do want to note that the
LastChanged
field has the correct datetime to when I created this example alert.In the database:
This remains true in the database as well:
id | message | scheduled_start | is_active | last_changed |
---|---|---|---|---|
1 | This is an alert message. | 2024-07-30 21:26:00.000 -0400 | true | 2024-07-30 18:48:18.353 -0400 |
This is the first time I have ever encountered this issue and it just doesn’t make sense to me. My user account has US/Eastern set as the time zone, all other functionality (that I made) saves datetimes correctly, the admin page displays datetimes correctly; its just inputting datetimes that breaks it.
After struggling to find a fix for a while (even trying to switch the admin datepicker out at one point), I eventually tried the following:
There is no reason that this should work, yet it does and it baffles me to no end.
main_app/middleware.py
...
class TimezoneMiddleware:
...
def __call__(self, request: HttpRequest) -> HttpResponse:
# Added the following check at the beginning of `__call__`
# Ignores setting timezone for the admin site
if request.path.startswith(reverse("admin:index")):
return self.get_response(request)
...
maintenance/admin.py
...
@admin.register(MaintenanceAlert)
class MaintenanceAlertAdmin(admin.ModelAdmin):
...
# Added the following overrides
# Each one activates the user's timezone (same method as the middleware) before returning the response.
def add_view(self, request: HttpRequest, form_url: str = "", extra_context: dict[str, bool] | None = None) -> HttpResponse:
tz.activate(request.user.preferences.timezone)
return super().add_view(request, form_url, extra_context)
def changelist_view(self, request: HttpRequest, extra_context: dict[str, str] | None = None) -> TemplateResponse:
tz.activate(request.user.preferences.timezone)
return super().changelist_view(request, extra_context)
def change_view(self, request: HttpRequest, object_id: str, form_url: str = "", extra_context: dict[str, bool] | None = None) -> HttpResponse:
tz.activate(request.user.preferences.timezone)
return super().change_view(request, object_id, form_url, extra_context)
Creating the exact same alert:
Scheduled Start: "2024-07-30 20:30" # 8:30 PM
Alert Message: "This is an alert message."
Active: True
Alert saved:
Now the success message says the following:
The Maintenance Alert "2024-07-30 08:30 PM EDT" was added successfully.
Then just below in the table it now shows the following in the Scheduled Start
column:
July 30, 2024, 8:30 p.m.
So, I do have a workaround for this issue, however it doesn’t make any sense to me. Is there anyone out there who knows enough to be able to understand why this might be happening and why my workaround works.
I would love to get this working normally so that I don’t have the entire admin site excluded from the time zone middleware, because, while I can always limit it to just this model, I would still like to have the option of editing models via the admin site if, for whatever reason, my frontend broke and allowed bad data to enter the site, and that’s just not possible if its going to make any datetime fields change to 56 minutes ahead of their correct time.
Thanks in advance for any assistance.