Ultimately, my goal is to extend Django's ModelAdmin to provide field-level permissions—that is, given properties of the request object and values of the fields of the object being edited, I would like to control whether or not the fields/inlines are visible to the user. I ultimately accomplished this by adding a
can_view_field() method to the ModelAdmin and modifying the built-in
get_fieldset() methods to remove/exclude fields+inlines that the user does not have permissions (as determined by
can_view_field()) to see. If you'd like to see the code, I placed it <a href="http://pastebin.com/pHgizEQL" rel="nofollow">in a pastebin</a>, since it's long and only somewhat relevant.
It works great...almost. I appear to have run into some sort of thread-safety or caching issue, where the state of the ModelAdmin object is being leaked from one request to another in a reproducible manner.
I'll illustrate the problem with a simple example. Suppose that I have a model whose ModelAdmin I have extended with the field-level permissions code. This model has two fields:
public_field, which can be seen/edited by any staff member
secret_field, which can only be seen/edited by superusers
In this case, the
can_view_field() method would look like this:
def can_view_field(self, request, obj, field_name): """ Returns boolean indicating whether the user has necessary permissions to view the passed field. """ if obj is None: return request.user.has_perm('%s.%s_%s' % ( self.opts.app_label, action, obj.__class__.__name__.lower() )) else: if field_name == "public_field": return True if field_name == "secret_field" and request.is_superuser: return True return False
Test case 1: with a fresh server restart, if you first view the changelist form as a superuser, you see the form as should happen, with both
secret_field visible. If you log out and view it as a staff member (but not superuser), you only see
Test case 2: with a fresh server restart, if you log in as a staff member first, you still only see
public_field. However, if you then log out and view as a superuser, you do <em>not</em> see
secret_field. This is 100% reproducible.
I've done some basic thread-safety diagnostics:<ol><li>At the end of
get_form(), I've printed out the memory address of the ModelForm object. As it should be, it is unique with each request. Therefore, the ModelForm object is not the problem.</li> <li>Immediately before the admin registration, I tried printing the memory address of the ModelAdmin object. In test case 1, it is unique with both requests. However with test case 2, it does not print at all on the second request.</li> </ol>
At this point, I'm clueless. My next point of research will be the admin registration system (which I admittedly know nothing about). The state resets with a server restart, so it seems that the ModelAdmin must be cached? Or is it a thread-safety issue? If I turn it into a factory and return a
deepcopy() of the ModelAdmin, would it serve a fresh ModelAdmin with each request? I'm clueless and would appreciate any thoughts. Thanks!
I'm confused about why you think ModelAdmin should be a new instance on each request. The admin objects are instantiated by the
admin.site.register(Model) calls in each admin.py, which in turn is called from
admin.autodiscover() in urls.py. In other words, this happens on process startup. Given the dynamic multi-process nature of most web serving environments, you may or may not get a new process with any particular request - certainly you won't get one every single time.
Because of this, it's not wise to store or alter state on a global object like ModelAdmin. I haven't looked through your linked code properly, but there was at least one case where you were altering an attribute on
self as a result of a method call. Don't do that - you'll need to find some other way of passing dynamic values between methods.