I have Node and User models which both belong to an Organisation. I want to ensure that a User will only ever see Node instances belonging to their Organisation.
For this I want to override the Node objects Manager with one that returns a query_set of User owned filtered results.
Based on https://docs.djangoproject.com/en/2.1/topics/db/managers/#modifying-a-manager-s-initial-queryset
the relevant models.py code I have is below:
class Organisation(models.Model):
users = models.ManyToManyField(User, related_name='organisation')
...
class UserNodeManager(models.Manager):
def get_queryset(self, request):
return super().get_queryset().filter(organisation=self.request.user.organisation.first())
class Node(models.Model):
organisation = models.ForeignKey(
Organisation, related_name='nodes', on_delete=models.CASCADE)
uuid = models.UUIDField(primary_key=True, verbose_name="UUID")
...
objects = UserNodeManager
views.py
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
EDIT
I can add custom query_set to individual views and this does work as below:
views.py
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
def get_queryset(self):
return Node.objects.filter(organisation__users__id=self.request.user.pk)
However, my intention is to be DRY and override a 'master' query_set method at a single point so that any view (e.g. form dropdown list, API endpoint) will perform the user restricted query without additional code.
For example, I am using django's generic list views have a form for adding a Scan object which requires a user to select a Node the Scan belongs to. The form currently shows Nodes from other Organisations, which is against the permissions logic I need.
Unfortunately, the overridden Node.objects property does not seem to have any effect and any User can see all Nodes. Am I taking the right approach?
I think the problem is here:
objects = UserNodeManager
You need to initiate UserNodeManager instance like this:
objects = UserNodeManager()
Also, it should throw error when you calling YourModel.objects.all() method(which is called from get_queryset method in view), because when it calls get_queryset() method, it does not pass request. So I think it would be a better approach:
class UserNodeManager(models.Manager):
def all(self, request=None):
qs = super(UserNodeManager, self).all()
if request:
return qs.filter(...)
return qs
Or you can create a new manager method like this(optional):
class UserNodeManager(models.Manager):
def user_specific_nodes(self, request):
return self.get_queryset().filter(...)
Also update in the view:
class NodeListView(LoginRequiredMixin, generic.ListView):
model = Node
def get_queryset(self):
return Node.objects.all(self.request) # where you can obviously use filter(...) or Model.objects.user_specific_nodes(self.request)
Update
from comments
Thing is that, you need to pass request with filter() or all(). In Generic views, the get_queryset method does not pass that information to all(). So you need to pass that either way. There is another way, to use a middleware like this django-crequest. You can use it like this:
from crequest.middleware import CrequestMiddleware
class UserNodeManager(models.Manager):
def all(self):
qs = super(UserNodeManager, self).all()
request = CrequestMiddleware.get_request()
return qs.filter(...)
The best way of achieving this is by using groups and custom permissions. You might add a group for every organization and set the correct permissions for those groups over your Nodes.
Take a look to this article, it might help: User Groups with Custom Permissions in Django
#ruddra thanks again for your guidance.
While your middleware example did not have effect for me (as user could still see others' objects), I was able to use that with the django documentation to finally implement the Manager similar to:
class UserDeviceManager(models.Manager):
def get_queryset(self):
request = CrequestMiddleware.get_request()
return super().get_queryset().filter(organisation=request.user.organisation)
Related
I have the following custom model manager in Django that is meant to count the number of related comments and add them to the objects query set:
class PublicationManager(models.Manager):
def with_counts(self):
return self.annotate(
count_comments=Coalesce(models.Count('comment'), 0)
)
Adding this manager to the model does not automatically add the extra field in DRF. In my API view, I found a way to retrieve the count_comments field by overriding the get function such as:
class PublicationDetails(generics.RetrieveUpdateAPIView):
queryset = Publication.objects.with_counts()
...
def get(self, request, pk):
queryset = self.get_queryset()
serializer = self.serializer_class(queryset.get(id=pk))
data = {**serializer.data}
data['count_comments'] = queryset.get(id=pk).count_comments
return Response(data)
This works for a single instance, but when I try to apply this to a paginated list view using pagination_class, overriding the get method seems to remove pagination functionality (i.e. I get a list of results instead of the usual page object with previous, next, etc.). This leads me to believe I'm doing something wrong: should I be adding the custom manager's extra field to the serializer instead? I'm not sure how to proceed given that I'm using a model serializer. Should I be using a basic serializer?
Update
As it turns out, I was using the model manager all wrong. I didn't understand the idea of table-level functionality when what I really wanted was row-level functionality to count the number of comments related to a single instance. I am now using a custom get_paginated_response method with Comment.objects.filter(publication=publication).count().
Original answer
I ended up solving this problem by creating a custom pagination class and overriding the get_paginated_response method.
class PaginationPublication(pagination.PageNumberPagination):
def get_paginated_response(self, data):
for item in data:
publication = Publication.objects.with_counts().get(id=item['id'])
item['count_comments'] = publication.count_comments
return super().get_paginated_response(data)
Not sure it's the most efficient solution, but it works!
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.
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>]
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).
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.
If your CMS calls Account.groups directly I would investigate how to override a ManyRelatedManager since the account.groups is actually a django ManyRelatedManager object.
It can probably be achieved using django Proxy model for the Group.
One strategy to investigate would then be:
Add the virtual group to the groups in the database.
Override the ManyRelatedManager by changing get_queryset() (The base queryset)
Something like (pseudo code):
def get_queryset():
account = self.instance # Guess this exist in a RelatedManager
if account.has_active_subscribtion():
return self.get_all_relevant_groups_including_the_virtual_one()
return self.get_all_relevant_groups_excluding_the_virtual_one()
The key here is to get access to the current instance in the custom RelatedManager
This may be a useful discussion around custom related managers.
Subclass a Django ManyRelatedManager a.k.a. ManyToManyField
I do not recommend to try fiddle/monkey patch with the queryset itself since it very easy to break the django admin etc...
This is my model:
class Delivery(models.Model):
name = models.CharField(_(u"Name"), max_length=50)
permissions = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True)
I'm creating a method to return True or False t check if a certain user can view a certain row:
#classmethod
def can_view(self, user):
permission = self.permissions.filter(permissions = user);
return permission is not None;
And when trying to use this method I get this error: 'ReverseManyRelatedObjectsDescriptor' object has no attribute 'filter'
As you can see I don't know how I can check if the user has permission: permission = self.permissions.filter(permissions = user);. How can I get the related within the model?
Thanks for any help!
You've declared this to be a classmethod, instead of a normal instance method. So the first parameter, which you've named self, is actually the class, not an instance of it; and the class itself doesn't have any related objects.
There's no reason for this to be a classmethod; remove that decorator.
Firstly you should fix your can_view method so it is not a class method, remove #classmethod
The line self.permissions.filter(permissions = user) probably won't work if you're trying to filter on the User. The filter() method is expecting a list of keyword arguments, with the models field name as the key and the value you're filtering on as the, well, value. Here you are filtering on your User model, so you probably want to filter on a field like id (or pk), email, username, or some other unique field. Give the
docs for .filter(**kwargs) a read.
For your query, I'd recommend using return self.permissions.filter(pk=user.pk).exists(). I'm filtering on pk because this is simple and guaranteed to work.
i.e.
def can_view(self, user):
return self.permissions.filter(pk=user.pk).exists()
You have a few issues here:
As others mentioned, self and #classmethod don't go together. self refers to the instance, so its for instance methods.
Your ORM query is also not correct as permissions is a ManyToMany field, and you need to access it accordingly.
Try this method:
def can_view(self, user):
return self.permissions.filter(pk=user.pk).exists()
However, you really don't need this method as all User objects will have a delivery_set model (see the docs), so you can simply do:
def check_if_delivery_belongs_to_user(request, delivery_pk=None):
delivery = get_object_or_404(Delivery, pk=delivery_pk)
if request.user.delivery_set.filter(pk=delivery.pk).exists():
print('You can access this')
else:
print('Nope, you cannot')
When I get a list of objects I only ever want to the objects that belong to the logged in user.
I'm thinking, maybe this can be done in the model manager by overriding the default manager, but I'm unsure how to get the logged in use. This is what I have so far...
class GroupsManager(models.Manager):
def get_query_set(self):
return super(GroupsManager, self).get_query_set().filter(user=???????)
I'm I on the right track? How can I achieve my goal? Or is there a better way to do this?
Thanks.
You don't have access to the request (and therefore the currently logged in user) in the model or model manager. So you could write a generic manager method that accepts a user instance and only returns objects belonging to that user:
class GroupsManager(models.Manager):
def for_user(self, user):
return self.get_query_set().filter(user=user)
and in your model:
class Group(models.Model):
objects = GroupsManager()
and in the view:
def my_view(request):
groups = Group.objects.for_user(request.user)
There must be a problem with super(InviteManager, self).get_query_set() here but I don't know what to use. When I look through the RelatedManager of a user instance,
len(Invite.objects.by_email()) == len(user.invite_set.by_email())
Even if the user does not have any invites. However, user.invite_set.all() correctly returns all of the Invite objects that are keyed to the User object.
class InviteManager(models.Manager):
"""with this we can get the honed querysets like user.invite_set.rejected"""
use_for_related_fields = True
def by_email(self):
return super(InviteManager, self).get_query_set().exclude(email='')
class Invite(models.Model):
"""an invitation from a user to an email address"""
user = models.ForeignKey('auth.User', related_name='invite_set')
email = models.TextField(blank=True)
objects = InviteManager()
'''
u.invite_set.by_email() returns everything that Invite.objects.by_email() does
u.invite_set.all() properly filters Invites and returns only those where user=u
'''
You may want a custom QuerySet that implements a by_email filter. See examples on Subclassing Django QuerySets.
class InviteQuerySet(models.query.QuerySet):
def by_email(self):
return self.exclude(email='')
class InviteManager(models.Manager):
def get_query_set(self):
model = models.get_model('invite', 'Invite')
return InviteQuerySet(model)
Try:
def by_email(self):
return super(InviteManager, self).exclude(email='')
If nothing else, the .get_query_set() is redundant. In this case, it may be returning a whole new queryset rather than refining the current one.
The documentation specifies that you should not filter the queryset using get_query_set() when you replace the default manager for related sets.
Do not filter away any results in this type of manager subclass
One reason an automatic manager is used is to access objects that are related to from some other model. In those situations, Django has to be able to see all the objects for the model it is fetching, so that anything which is referred to can be retrieved.
If you override the get_query_set() method and filter out any rows, Django will return incorrect results. Don’t do that. A manager that filters results in get_query_set() is not appropriate for use as an automatic manager.
Try using .all() in place of .get_query_set(). That seemed to do the trick for a similar problem I was having.
def by_email(self):
return super(InviteManager, self).all().exclude(email='')