Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I override user.groups in a custom account model in Django to implement a "virtual" group?

Tags:

django

I have a custom account class in a Django app using PermissionsMixin:

class Account(AbstractBaseUser, PermissionsMixin):

Our CMS calls various .groups methods on this class in order to ascertain permissions.

We essentially want to override the queryset that is returned from .groups in this custom Account class and to inject an additional group under specific conditions. (I.e. the user has an active subscription and we then want to return "member" as one of the groups for that user, despite them not actually being in the group.)

How should we handle this override? We need to get the original groups, so that basic group functionality isn't broken, then inject our "virtual" group into the queryset.

like image 524
Martin Eve Avatar asked Feb 10 '19 10:02

Martin Eve


3 Answers

Override the get_queryset method ManyRelatedManager. An object of ManyRelatedManager class has access to the parent instance.

Code Sample:

def add_custom_queryset_to_many_related_manager(many_related_manage_cls):
    class ExtendedManyRelatedManager(many_related_manage_cls):
        def get_queryset(self):
            qs = super(ExtendedManyRelatedManager, self).get_queryset()
            # some condition based on the instance
            if self.instance.is_staff:
                return qs.union(Group.objects.filter(name='Gold Subscription'))
            return qs

    return ExtendedManyRelatedManager

ManyRelatedManager class is obtained from the ManyToManyDescriptor.

class ExtendedManyToManyDescriptor(ManyToManyDescriptor):

    @cached_property
    def related_manager_cls(self):
        model = self.rel.related_model if self.reverse else self.rel.model
        return add_custom_queryset_to_many_related_manager(create_forward_many_to_many_manager(
            model._default_manager.__class__,
            self.rel,
            reverse=self.reverse,
        ))

Associated the ExtendedManyToManyDescriptor with groups field when the Account class is initialized.

class ExtendedManyToManyField(ManyToManyField):

    def contribute_to_class(self, cls, name, **kwargs):
        super(ExtendedManyToManyField, self).contribute_to_class(cls, name, **kwargs)
        setattr(cls, self.name, ExtendedManyToManyDescriptor(self.remote_field, reverse=False))

Override PermissionsMixin to use ExtendedManyToManyField for groups field instead of ManyToManyField.

class ExtendedPermissionsMixin(PermissionsMixin):
    groups = ExtendedManyToManyField(
        Group,
        verbose_name=_('groups'),
        blank=True,
        help_text=_(
            'The groups this user belongs to. A user will get all permissions '
            'granted to each of their groups.'
        ),
        related_name="user_set",
        related_query_name="user",
    )

    class Meta:
        abstract = True

Reference: django.db.models.fields.related_descriptors.create_forward_many_to_many_manager

Testing:

account = Account.objects.get(id=1)
account.is_staff = True
account.save()

account.groups.all() 

# output
[<Group: Gold Subscription>]
like image 109
sun_jara Avatar answered Oct 21 '22 02:10

sun_jara


The groups related manager is added by the PermissionMixin, you could actually remove the mixin and add only the parts of it that you need and redefine groups:

class Account(AbstractBaseUser):
    # add the fields like is_superuser etc...
    # as defined in https://github.com/django/django/blob/master/django/contrib/auth/models.py#L200

    default_groups = models.ManyToManyField(Group)

    @property
    def groups(self):
        if self.is_subscribed:
            return Group.objects.filter(name="subscribers")
        return default_groups.all()

Then you can add your custom groups using the Group model. This approach should work fine as long it is ok for all parts that groups returns a queryset instead of a manager (which probably mostly should be fine as managers mostly offer the same methods - but you probably need to find out yourself).

like image 42
Bernhard Vallant Avatar answered Oct 21 '22 03:10

Bernhard Vallant


Update

After reading carefully the docs related to Managers and think about your requirement, I've to say there is no way to achieve the magic you want (I need to override the original, not to add a new ext_groups set - I need to alter third party library behavior that is calling groups.) Without touch the Django core itself (monkey patching would mess up admin, the same with properties).

In the solution I'm proposing, you have the necessary to add a new manager to Group, perhaps, you should start thinking in override that third-party library you're using, and make it use the Group's Manager you're interested in.

If the third-party library is at least medium quality it will have implemented tests that will help you to keep it working after the changes.

Proposed solution

Well, the good news is you can fulfill your business requirements, the bad news is you will have code a little more than you surely expect.

How should we handle this override?

You could use a proxy model to the Group class in order to add a custom manager that returns the desired QuerySet.

A proxy manager won't add an extra table for groups and will keep all the Group functionality besides, you can set custom managers on proxy models too, so, its perfect for this case use.

class ExtendedGroupManager(models.Manager):
    def get_queryset(self):
        qs = super().get_queryset()
        # Do work with qs.
        return qs


class ExtendedGroup(Group):
    objects = ExtendedGroupManager()

    class Meta:
        proxy = True

Then your Account class should then have a ManyToMany relationship to ExtendedGroup that can be called ... ext_groups?

Till now you can:

acc = Account(...)
acc.groups.all()      # All groups for this account (Django default).
acc.ext_groups.all()  # Same as above, plus any modification you have done in get_queryset  method. 

Then, in views, you can decide if you call one or another depending on a condition of your own selection (Eg. user is subscribed).

Is worth mention you can add a custom manager to an existeing model using the method contribute_to_class

em = ExtendGroupManager()
em.contribute_to_class(Group, 'ext_group')

# Group.ext_group  is now available.
like image 40
Raydel Miranda Avatar answered Oct 21 '22 02:10

Raydel Miranda