Almost anyone who has worked with Django’s ModelAdmin for a while has run into a situation where it would be useful to have access to the “request” object in a ModelAdmin method that isn’t supplied with it by default.

For example, having access to the request might help us

  • filter a queryset based on the logged-in user (request.user)
  • construct a fully qualified url  (request.build_absolute_url())
  • get access to the session (request.session)

Whatever our use-case, it comes in handy to know how to get access to this when we need it.

Finding request

Our first challenge is finding out where ModelAdmin gets request from, so we can get a better idea of how to stash it away for later.

The ideal place would be as early as possible in the lifecycle of the request, so any later operations can safely reference it.

ModelAdmin is defined in django.contrib.admin.options, and we can see lots of methods which receive request as an argument:

# in class ModelAdmin, line ~934
def get_list_display(self, request):
    """
    Return a sequence containing the fields to be displayed on the
    changelist.
    """
    return self.list_display

…however these are unsuitable because they’re all being called very late in the lifecycle of the request — they don’t seem to be the first place where request is passed in by Django.

The First Request

Now that we’ve found request, let’s look a bit deeper to find out where Django initially passed it in.

Given that Django’s ModelAdmin is just a complicated, introspective view, could there be view-like methods where everything starts?

There are!

In fact, there are several:

  • def changeform_view(self, request, object_id=None, form_url='', extra_context=None)
  • def add_view(self, request, form_url='', extra_context=None)
  • def change_view(self, request, object_id, form_url='', extra_context=None)
  • def changelist_view(self, request, extra_context=None)
  • def delete_view(self, request, object_id, extra_context=None)
  • def history_view(self, request, object_id, extra_context=None)

Right, so these look like they’ll be the earliest reasonable place for us to make contact with the request object.

How do we grab it?

Super View

One fast and clean way is to

  1. override the view method in our ModelAdmin
  2. grab the request
  3. stash it somewhere on our instance of ModelAdmin
  4. use super()to call the original/parent view method

We’ll also define a getter method for the request.

Here’s how it looks, using changeform_view as an example:

from django.contrib import admin

class PollAdmin(admin.ModelAdmin):
    def __init__(self, *args, **kwargs):
        # let's define this so there's no chance of AttributeErrors
        self._request = None
        super(PollAdmin, self).__init__(*args, **kwargs)

    def get_request(self):
        return self._request
 
    def changeform_view(self, request, *args, **kwargs):
        # stash the request
        self._request = request
 
        # call the parent view method with all the original args
        return super(PollAdmin, self).changeform_view(request, *args, **kwargs)

Great!

But should we have to do this every time, for every ModelAdmin?

Mixin it up

There doesn’t seem to be anything Model-specific in the ModelAdmin we just defined, so that means we should be able to create a reusable mixin.

This time, we’ll define overrides for __init__ plus all 6 view methods

class ModelAdminRequestMixin(object):
    def __init__(self, *args, **kwargs):
        # let's define this so there's no chance of AttributeErrors
        self._request = None
        super(ModelAdminRequestMixin, self).__init__(*args, **kwargs)
    
    def get_request(self):
        return self._request

    def changeform_view(self, request, *args, **kwargs):
        # stash the request
        self._request = request        

        # call the parent view method with all the original args
        return super(ModelAdminRequestMixin, self).changeform_view(request, *args, **kwargs)
    
    def add_view(self, request, *args, **kwargs):
        self._request = request        
        return super(ModelAdminRequestMixin, self).add_view(request, *args, **kwargs)
    
    def change_view(self, request, *args, **kwargs):
        self._request = request        
        return super(ModelAdminRequestMixin, self).change_view(request, *args, **kwargs)
    
    def changelist_view(self, request, *args, **kwargs):
        self._request = request        
        return super(ModelAdminRequestMixin, self).changelist_view(request, *args, **kwargs)
    
    def delete_view(self, request, *args, **kwargs):
        self._request = request        
        return super(ModelAdminRequestMixin, self).delete_view(request, *args, **kwargs)
    
    def history_view(self, request, *args, **kwargs):
        self._request = request        
        return super(ModelAdminRequestMixin, self).history_view(request, *args, **kwargs)

Finally, we’ll include it in our ModelAdmin definition:

from core.mixins import ModelAdminRequestMixin
from django.contrib import admin
class PollAdmin(ModelAdminRequestMixin, admin.ModelAdmin):
    readonly_fields = ('is_owned_by_me',)

    def is_owned_by_me(self, obj):
        request = self.get_request()
        
        return request and obj.owner == request.user or 'Unknown'

We’re done, right?

Thread-ly Danger

Sadly, we have to consider how threads factor into this solution. In a multi-threaded environment, our instance of ModelAdmin might be shared, so storing request directly on the instance is unwise (and probably unsafe!).

To solve this we’ll dip into python’s threading module, to use “thread locals”:

In Python, everything is shared, except for function-local variables. […] Attributes of threading.local are not shared between threads; each thread sees only the attributes it itself placed in there.

(Credit: StackOverflow)

Let’s modify our Mixin just a bit now. We’ll also add in a setter function just to clean things up.

import threading

class ModelAdminRequestMixin(object):
    def __init__(self, *args, **kwargs):
        # let's define this so there's no chance of AttributeErrors
        self._request_local = threading.local()
        self._request_local.request = None
        super(ModelAdminRequestMixin, self).__init__(*args, **kwargs)

    def get_request(self):
        return self._request_local.request
    
    def set_request(self, request):
        self._request_local.request = request

    def changeform_view(self, request, *args, **kwargs):
        # stash the request
        self.set_request(request)

        # call the parent view method with all the original args
        return super(ModelAdminRequestMixin, self).changeform_view(request, *args, **kwargs)

    def add_view(self, request, *args, **kwargs):
        self.set_request(request)
        return super(ModelAdminRequestMixin, self).add_view(request, *args, **kwargs)

    def change_view(self, request, *args, **kwargs):
        self.set_request(request)
        return super(ModelAdminRequestMixin, self).change_view(request, *args, **kwargs)

    def changelist_view(self, request, *args, **kwargs):
        self.set_request(request)
        return super(ModelAdminRequestMixin, self).changelist_view(request, *args, **kwargs)

    def delete_view(self, request, *args, **kwargs):
        self.set_request(request)
        return super(ModelAdminRequestMixin, self).delete_view(request, *args, **kwargs)

    def history_view(self, request, *args, **kwargs):
        self.set_request(request)
        return super(ModelAdminRequestMixin, self).history_view(request, *args, **kwargs)

Recap

In this post we explored a simple way to grab the a request from ModelAdmin early in the request lifecycle, stash it safely, and get access to it anytime we need.

We refactored everything out into a Mixin to keep the DRY gods happy, and we even made it thread-safe.

Not bad!