Time zone middleware broken on Django admin site

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.

database save datetime without timezone. it is not bug.
and, when rending page in datetime, it just convert by django.

in django, convert datetime by settings.TIME_ZONE.

if you change this setting:

  • you can change settings.py
  • you can change datetime in views.py before rendering page
  • you can change datetime in admin.py before rendering page

not middleware.

It appears that you have misunderstood my question/issue.

I am aware that when USE_TZ = True Django saves datetimes to the database as UTC and converts them to the ‘local’ time set via TIME_ZONE = '<timezone>'.

The issue here is not that.

I have a TimezoneMiddleware so that I can have users with multiple ‘local’ times rather than a single hardcoded ‘local’ time via TIME_ZONE = 'US/Eastern'. For some reason this TimezoneMiddleware does not work correctly when inputting datetimes via the admin site and uses LMT rather than EDT (and I didn’t mention it initially, but yes I have used the timezone module to verify that the active time zone is set to US/Eastern on these admin views).

As far as I know, DB data interference is difficult with middleware.
Are you sure the middleware can convert the data retrieved from the DB?
How about customizing the model manager and changing the time value when retrieving data?

This middleware works fine for inputting and displaying datetimes while in my custom frontend; it only has this issue on the admin site.

I probably could use a manager to do this, however I already have a workaround. Ideally, I’m looking for a complete solution so that I don’t have to have this weird exception in my TimezoneMiddleware for the admin site. I also would prefer not having to add this functionality to a custom manager for every single model in the event that I would like to use the admin site for managing pieces of their data.

Ultimately (if I can find the time), I might end up making an actual view in my frontend, outside of the admin site where I know things work correctly, but I would still like to know if there’s someone out there who can diagnose the root of this issue, because it makes no sense to me.

Have you tried something like def view_birth_date(self, obj):?
I think it exposes utc+0 time on the admin page.

I think I’ve found the source.

Because of my job I’m forced to use Windows due to having to develop desktop applications (otherwise I would daily-drive linux). However, occasionally with web projects, I have to work with pieces of code that are non-compatible with Windows, and in this case that is Celery.

Just last week I moved development into WSL so I could develop Celery tasks, prior to this all development was performed on Windows OS. This includes the development of the frontend of my site that I mentioned before. Well, I was just testing a fix for django_bootstrap_datepicker_plus on my site and noticed that my datetimes were saving using LMT rather than EST on the frontend I knew was working perfectly previously (which I took care to mention).

Well, switching back over to my host machine and running app, datetimes work perfectly. It appears to be some weird issue with WSL because it should piggyback off the host OS for timezone, plus I set it to US/Eastern manually, but for some reason it causes this weird LMT bug. In any case, I am aware of it now and know that it won’t affect production which doesn’t run on WSL (who would do such a thing to themselves?), I’ll just need to find a fix or keep an eye on it so that it doesn’t trip me up when I’m debugging some other issues related to datetimes.

Regardless, thanks for taking some time to attempt to assist me.

If I find a permanent solution I’ll post it here as the solution.

1 Like