Permissions based on foreign key?

Hi,

I’m trying to figure out what the best path is to follow in order to implement additional permissions on an existing app or something with similar results.

I’ve been looking at various options like django-rules or django-guardian as well as potentially rolling my own but, right now, I’m still quite confused. I’m hoping maybe someone here has had to deal with a similar issue and might have some pointers in regard to what I should be looking at (or if I’m looking in a completely wrong direction, which I have also been known to do :slight_smile: ).

This is an app that lets people come to a website and book a session in a fitness-type venue. At the moment there are 3 venues and it all works fine. There’s a venue model, a reservation model and a bunch of other elements that don’t impact the reservation or the current problem itself.

The issue is that up until now, all 3 venues were managed by the same team, but they’ve now decided to expand and these new venues will be managed by third parties. This means that people from the venue X team should only be able to view/edit/add reservations for venue X in the admin and be unable to see reservations for venues Y or Z, for example.

In an ideal world, when a new venue instance is added via the admin, let’s say “venue w”, a series of permissions linked to this venue would be created. Something like “can_add_reservations_for_venue_w”, “can_delete_reservations_for_venue_w” etc.
If an admin user is in a group with these permissions, they can add, view or delete reservations linked to this venue but none of the other venues they don’t have permissions for.

Here are my models, they’re pretty standard:

Venue:

class Venue(models.Model):
    name = models.CharField(max_length=255, verbose_name=_("name"))
    street = models.CharField(
        max_length=255, blank=True, null=True, verbose_name=_("street & number")
    )
    post_code = models.CharField(
        max_length=20, blank=True, null=True, verbose_name=_("post/zip code")
    )
    city = models.CharField(
        max_length=255, blank=True, null=True, verbose_name=_("city")
    )
    
    [etc]

and Reservation:

class Reservation(models.Model):

    venue = models.ForeignKey(
        to=Venue, on_delete=models.CASCADE, verbose_name=_("venue"), default=1
    )
    customer = models.ForeignKey(
        to=Customer, on_delete=models.CASCADE, verbose_name=_("customer")
    )
    date = models.DateField(default=timezone.now, verbose_name=_("activity date"))
    time = models.IntegerField(default=14, verbose_name=_("time"))
    duration = models.IntegerField(
        default=1, choices=RESERVATION_DURATION, verbose_name=_("duration")
    )
    participants = models.IntegerField(default=1, verbose_name=_("number of people"))
    	   
    [etc]

Any tips would be appreciated, or indications that I’m missing something horribly obvious :slight_smile:

Thanks!

Override the get_queryset method in your view (assuming you are using Class Based Views), check if the user is a superuser by calling request.user.is_superuser and filter the queryset.

For the admin:
Create a ReservationAdmin class in your admin.py file, override the get_queryset method and filter by user.

For selecting all relevant objects take a look at this https://docs.djangoproject.com/en/3.0/ref/models/querysets/#select-related

Hi! I’d like to ask a follow-up question, I hope that’s okay. It’s about how to base admin form field values on user information.

I tried to recreate your project with the components relevant to the task. I’ll describe this as briefly as I can:

# venues/models.py
from django.db import models
from django.contrib.auth.models import Group
from django.utils import timezone

RESERVATION_DURATION = (
    (1, 'One day'),
    (2, 'Two days'),
    (3, 'Three days'),
)

class Venue(models.Model):
    name = models.CharField(max_length=255, verbose_name=("name"))
    street = models.CharField(
        max_length=255, blank=True, null=True, verbose_name=("street & number")
    )
    post_code = models.CharField(
        max_length=20, blank=True, null=True, verbose_name=("post/zip code")
    )
    city = models.CharField(
        max_length=255, blank=True, null=True, verbose_name=("city")
    )
    admin_group = models.ForeignKey(
        to=Group, on_delete=models.SET_NULL,
        verbose_name=("responsible admin group"),
        null=True
    )

    def __str__(self):
        return self.name


class Reservation(models.Model):
    venue = models.ForeignKey(
        to='Venue', on_delete=models.CASCADE, verbose_name=("venue"), default=1
    )
    customer = models.ForeignKey(
        to='Customer', on_delete=models.CASCADE, verbose_name=("customer")
    )
    date = models.DateField(default=timezone.now, verbose_name=("activity date"))
    time = models.IntegerField(default=14, verbose_name=("time"))
    duration = models.IntegerField(
        default=1, choices=RESERVATION_DURATION, verbose_name=("duration")
    )
    participants = models.IntegerField(default=1, verbose_name=("number of people"))

    def __str__(self):
        return f"reservation on {str(self.date)}, at {str(self.time)} o'clock"


class Customer(models.Model):
    name = models.CharField(max_length=255)

The main addition is the admin_group ForeignKey field for Venue.

I’m including tests I wrote since I think it helps to understand what it is you’re working on

# venues/tests.py
from django.test import TestCase
from django.contrib.auth.models import Group, User
from .models import Venue, Reservation, Customer


class VenueTestCase(TestCase):
    def setUp(self):
        self.g1 = Group.objects.create(name='gymgroup')
        self.g2 = Group.objects.create(name='taegroup')
        self.u1 = User.objects.create_user(username='gymwoman', email='test@test.com',
                                           password='testpass')
        self.u1.groups.set([self.g1])
        self.v1 = Venue.objects.create(name='Foo gym',
                                       admin_group=self.g1)
        self.v2 = Venue.objects.create(name='Tae Kwon Dojo',
                                       admin_group=self.g2)
        self.v3 = Venue.objects.create(name='Qux gym',
                                       admin_group=self.g1)
        self.c1 = Customer.objects.create(name='Jane')
        self.c2 = Customer.objects.create(name='John')
        self.r1 = Reservation.objects.create(venue=self.v1, customer=self.c1)
        self.r2 = Reservation.objects.create(venue=self.v1, customer=self.c2)
        self.r3 = Reservation.objects.create(venue=self.v2, customer=self.c2)

    def test_venue_query_by_admingroup(self):
        gym_count = Venue.objects.filter(admin_group__in=self.u1.groups.all()).count()
        self.assertEqual(gym_count, 2)

    def test_reservation_query_by_admingroup(self):
        reservation_count = Reservation.objects.filter(venue__admin_group__in=self.u1.groups.all()).count()
        self.assertEqual(reservation_count, 2)

Here’s the admin setup, just like @KKP4 suggested I’m overriding the get_queryset method.

# venues/admin.py
from django.contrib import admin
from .models import Reservation

class ReservationAdmin(admin.ModelAdmin):
    model = Reservation
    fields = ('venue', 'customer', 'date', 'time', 'duration', 'participants')

    def get_queryset(self, request):
        if request.user.is_superuser:
            queryset = Reservation.objects.all()
        else:
            queryset = Reservation.objects.filter(venue__admin_group__in=request.user.groups.all())
        return queryset

    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        form.base_fields['venue'].initial = request.user.groups.get(id=1).venue_set.get(id=1)
        return form

admin.site.register(Reservation, ReservationAdmin)

I managed to set the default value of the ‘venue’ field to be the user’s group’s related venue. But I think there should be a better way to do that, and to also stop the user from changing the venue to another. Alternatively, it would be nice to not show the user the venue at all, since it will always be the same for them, and setting the value ‘quietly’/in the background. So:

  1. How can one set the admin form field value without showing it to the user?
  2. Is it better at this point to create a proper Form and use that with the admin, instead of trying to force the ModelAdmin to do what’s required?