Creating an Admin Mixin for a BaseModel

I have a BaseModel that I’m using across all my applications. BaseModel provides common auditing fields:
“created”, “created_by”, “modified”, “modified_by”

created and modified are DateTimeField using django’s built-in auto_now_add and auto_now functionality.

created_by and modified_by are CharFields as the values can be a username, system action identifier, or API action authenticated outside of the auth framework (for example a webhook).

Within the admin I’m setting these fields to readonly_fields:

readonly_fields = ("created", "created_by", "modified", "modified_by")

And I’m overriding the admin save_model to capture the admin user to set the create_by or modified_by as documented in this example
https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#django.contrib.admin.ModelAdmin.save_model

In the spirit of DRY I’m researching how to create a Mixin that will provide the base readonly_fields and save_model to each app’s ModelAdmin. This is what I have come up with for the BaseAdminMixin:

class BaseAdminMixin:
    base_readonly_fields = ("created", "created_by", "modified", "modified_by")

    def get_readonly_fields(self, request, obj=None):
        if self.readonly_fields:
            return self.readonly_fields + self.base_readonly_fields
        else:
            return self.base_readonly_fields

    def save_model(self, request, obj, form, change):
        if not obj.pk:
            obj.created_by = request.user.get_username()
        if change:
            obj.modified_by = request.user.get_username()
        super().save_model(request, obj, form, change)

This approach is passing all the tests I wrote when I was repeating the readonly_fields and save_model code in every ModelAdmin class.

This is a new area of using django for me so I’m looking for feedback on this approach.

Since posting this I’ve looked through some popular projects. Another pattern I’m seeing it to create a Admin class and inherit that within apps:

class BaseAdmin(admin.ModelAdmin):
    # some base functionality/configuration

and then sub class the BaseAdmin:

class ArticleAdmin(BaseAdmin):
    # Artice admin configuration

Is one approach better than the other? Any recommended reading, examples/tutorials so I that can understand these conventions and how to best approach for my use case?

I don’t think there’s necessarily a “best” way here. Mechanically as it relates to Python, I’m not even sure there is an effective difference in the case of a single Mixin compared to a Parent class.

Our general perception is that Mixin classes work better if you have multiple mixins needing to be added. (For example, let’s say that in addition to the BaseAdminMixin that you’ve provided, you now want to add another mixin - let’s call it StatusMixin that adds a status field along with a status_date.) With this type of situation, it tends to be easier to set them up as Mixins in case you have situations needing one or the other but not both.
(We’ve got a system with 4 different mixins on the Model classes, any combination of which can be applied to any model. Straight inheritence wouldn’t work in that case.)

On the other hand, the straight inheritence model is more appealing to us if it’s associated with behavior and attributes applied consistently across a majority of Models.

1 Like

@KenWhitesell thanks for your insight on this topic.

Are there any considerations with the save_model method being called in multiple places?

For example if my BaseMixin sets the user creating/modifying via a save_model call and then a StatusMixin introduces a save_model to update status_date will Python sort out the multiple save_model calls? Is there potential for unexpected results when calling save_model in multiple mixins and potentially also in the app’s ModelAdmin class?

A method is only ever called once. The Python MRO (Method resolution order) determines which method is going to be called. (Basically, it’s a depth-first, left to right order.)
If any class needs to do some work and then ensure that other classes do their work, that’s when they’d call super().

For example, referring to your save_model method at the top, notice that it’s calling super as the last step. That call is going to dispach to the next save_model method in the MRO. If you then had a second Mixin with a save_model method, it would be called. Assuming that save_model then ended with a super(), then it would call the save_model method of ModelAdmin.

As long as every method limits itself to the work it needs to do and then calls super(), the methods should work as you would expect. The order in which they are called is determinable, and you can always inspect the base class’ code to ensure that there are no unintended side effects.

1 Like