Skip to content

Custom Permissions

Volunteers shouldn't change an animal's status or an adopted animal's record. Employees need to change both. Django's built-in permissions can't tell those apart, so we'll add a custom permission and check it in the admin.

Adding a Custom Permission

We can add custom permissions to a model. Open animals/models.py and add a Meta class to Animal, below its fields:

animals/models.py
    intake_date = models.DateField(default=date.today)

    class Meta:
        permissions = [
            (
                "manage_animal",
                "Can change an animal's status, and change adopted animals",
            ),
        ]

Each permission is a row in the auth_permission table. The migration records our new Meta option, and migrate then creates a row for each permission it finds:

shell
uv run manage.py makemigrations
uv run manage.py migrate

Employees should have it:

  1. As admin, open Groups, then Employees.
  2. Add Animals | animal | Can change an animal's status, and change adopted animals, and click Save.

A permission does nothing until code checks it. The admin checks the built-in ones, and our code checks ours.

Checking a Permission

The admin's methods are called with the request, and request.user is the logged-in user.

request.user.has_perm("animals.manage_animal") checks our permission, named by the app's label and the codename. It returns True for a superuser, and for a user who has the permission directly or through a group.

A Read-Only Status

Most admin options have a get_ method that's called with the request, such as get_readonly_fields(), so a setting can depend on who's asking. Add one to AnimalAdmin:

animals/admin.py
@admin.register(Animal)
class AnimalAdmin(admin.ModelAdmin):
    list_display = ["name", "species", "breed", "status", "intake_date"]
    list_filter = ["species", "status"]
    search_fields = ["name", "breed"]
    inlines = [AdoptionApplicationInline]

    def get_readonly_fields(self, request, obj=None):
        fields = list(super().get_readonly_fields(request, obj))
        if not request.user.has_perm("animals.manage_animal"):
            fields.append("status")
        return fields

super() returns readonly_fields as the class sets them. Without the permission, we add status, and the admin shows it as text. A read-only field isn't in the form, so an animal a volunteer adds gets the model's default status, Available.

An adopted animal should be closed to volunteers too. That rule goes in has_change_permission(), and we'll write it in the exercise.

Volunteers and Employees in the Admin

  1. Log out, and log in as volunteer / rescue-volunteer. The admin shows only Animals.
  2. Open Pepper. The page has no Delete button, and no table of applications, since the group can't view them. The status is text.
  3. Go back to the list of animals, and click Add animal. The status is text, Available, and a new animal is saved with it.
  4. Log out, and log in as employee / rescue-employee. The status is a menu again, and Pepper's page has the table of applications.