If you have used Django to build a web application before, then you likely have interacted with its robust permissions system.
While first and foremost used in the Django admin site, the built-in permissions system allows a developer to limit access to certain parts of an application only to users with the necessary permissions.
By using the permissions system, one gets access to such handy methods as user.groups.add(group_obj, other_group_obj)
or group.permissions.set([permissions])
, which can really speed up development by abstracting functionality that would be tedious to write and maintain.
There’s just one problem: the Group
model that powers one aspect of the permissions system is extremely basic, containing two fields: id
(not even included in the model definition) and name
.
The model also contains a ManyToManyField
that tracks the permissions assigned to a role, but this creates a “through”, or junction, table.
Let’s suppose you want to use the Group model but wish to add a few more fields to match your needs. How would you go about that?
Hat tip to this Stack Overflow answer, which provided the foundation for this answer.
The obvious answer would be to add fields to the model class. But since Group
is built in, that’s not an option. So let’s strike this off the list.
The next option would be to subclass the Group
model and add the fields to your new class.
Doing so would create an entirely new table in the database and the fields would not reflect in the Group
model. Again, let’s strike this option off the list.
This brings us to the third option: Inject your fields into the Group
model.
(The complete code is available on GitHub.)
Add the fields to your
models.py
file.
Supposing you wish to add thecreated_at
field to the model:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# example/models.py from django.contrib.auth.models import Group from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ if not hasattr(Group, "created_at"): created_at_field = models.DateTimeField( _("Created At"), blank=True, null=True, editable=False, default=timezone.now, ) created_at_field.contribute_to_class(Group, "created_at")
Create your migration files.
1
python manage.py makemigrations
You will notice, though, that the migration file is created in the
site-packages
directory, i.e., where your Django installation is. This means that it won’t be included in your source control commits. It also wouldn’t be present should you choose to re-install your dependencies. Hardly ideal.
Let’s correct that.Copy the migrations of Django’s built-in auth app to a different directory. The output from the
makemigrations
command above should show you the location of the migration files. Copy all files from that directory to, for example,auth_migrations
.
Your file structure should look something like:1 2 3 4 5
└── application_root/ └── auth_migrations/ └── # All contents of django.contrib.auth.migrations excluding __pycache__ └── # Other app directories └── manage.py
In
settings.py
, add theMIGRATION_MODULES
dictionary, a setting which specifies on a per-app basis where the migration files are found.1 2 3
MIGRATION_MODULES = { "auth": "auth_migrations" }
Run your migrations:
1
python manage.py migrate
Having done all of that, you will find that the auth_group
table now has the created_at
column that you defined.
This means that you can work with the field in queries like you would with any other field. The one downside to this approach is that you’re not provided with any Intellisense or auto-completion options; not that I know of, at the very least.
Should you wish to add other fields in the future, any new migration file(s) will be added to auth_migrations
.
And that’s it for this post. Until the next one.