I'm setting up a new Django app and need to customize the Admin for a given table by restricting the records where a field is NULL. Basically a built-in, permanent filter.
Seems like changelist_view needs to be overridden, but I'm uncertain what that change would look like.
There's no code to be included as I'm not overriding changelist_view right now.
You can override default manager, but it has a drawback that you'll have to explicitly specify original manager in all your queries:
class MyManager(models.Manager):
def get_query_set(self):
return super(MyManager, self).get_query_set().filter(my_field__isnull=False)
class MyModel(models.Model):
objects = MyManager()
all_objects = models.Manager()
MyModel.all_objects.all() # all objects including those having my_field=None
There's not really a good way to do this at present - there is in fact an open ticket on Django requesting the ability to customise what QuerySet gets used for the admin views - see ticket #10761. Antony's solution will work in the short term, but you may have to wait until that ticket is resolved for a proper solution.
I've decided to use limited queryset manager as objects. For ModelAdmin I've copied queryset() from django/contrib/admin/options.py and changed _default_manager by mine unlimited queryset manager. Simple!
Related
I am struggling to understand how permissioning in DRF is meant to work. Particularly when/why should a permission be used versus when the queryset should be filtered and the difference between has_object_permission() & has_permission() and finally, where does the serializer come in.
For example, with models:
class Patient(models.Model):
user = models.OneToOneField(User, related_name='patient')
class Appointment(models.Model):
patient = models.ForeignKey(Patient, related_name='appointment')
To ensure that patients can only see/change their own appointments, you might check in a permission:
class IsRelevantPatient(BasePermission):
def has_object_permission(self, request, view, obj):
if self.request.user.patient == obj.appointment.patient:
return True
else:
return False
But, modifying the queryset also makes sense:
class AppointmentViewSet(ModelViewSet):
...
def get_queryset(self):
if self.request.user.is_authenticated:
return Appointment.objects.filter(patient=self.request.user.patient)
What's confusing me is, why have both? Filtering the queryset does the job - a GET (retrieve and list) only returns that patient's appointments and, a POST or PATCH (create or update) only works for that patient's appointments.
Aside from this seemingly redundant permission - what is the difference between has_object_permission() & has_permission(), from my research, it sounds like has_permission() is for get:list and post:create whereas has_object_permission() is for get:retrieve and patch:update. But, I feel like that is probably an oversimplification.
Lastly - where does validation in the serializer come in? For example, rather than a permission to check if the user is allowed to patch:update an object, You can effectively check permissions by overriding the update() method of the serializer and checking there.
Apologies for the rambling post but I have read the docs and a few other question threads and am at the point where I am probably just confusing myself more. Would really appreciate a clear explanation.
Thanks very much.
First, difference between has_object_permission() and has_permission() :
has_permission() tells if the user has the permission to use the view or the viewset without dealing with any object in the database
has_object_permission() tells if the user has the permission to use the view or the viewset based on a specific object in the database.
The important note thaw is that DRF wont perform the test itself in the case of object level permission, but you have to do it explicitly by calling check_object_permission() somewhere in your view (doc here).
The second important note is that DRF will not filter the result of the query based on object permission. If you want the query to be filtered, then you have to do it yourself (by overriding get_queryset() like you did or using a filter backend), that's the difference.
The serializer has nothing to do with permission neither with filtering. It handles objects one by one, applying validation (not permission) on each field of each objects.
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...
I have a custom model manager and a custom queryset defined specifically for related obj which means I have defined Meta.base_manager_name in the model.
I would like to use a all() manager method which fetches related obj on a OneToOneFeild.
Now I know this does not make sense since OneToOneFeild will always return one obj there is no need for a all() method. I am working on django-oscar project and am extending its "Partner" model. It originally has a field "users" with ManyToManyField and now changed to a OneToOneFeild.
The users field is called in code several times using relation user.partners.all(). I don't want to extend/modify all these places (am I being lazy here?) since I want to keep the code as upgrade friendly as possible and so instead I wanted to have all() model manager defined which will work. Not sure if it is a good idea?
the all() method takes user arg to return queryset of the user instance
class PartnerQuerySet(models.QuerySet):
def all(self, user):
return self.filter(user=user)
class PartnerManager(models.Manager):
def get_queryset(self):
return PartnerQuerySet(self.model, using=self._db)
def all(self, user):
return self.get_queryset().all(users)
class Partner(models.Model):
objects = PartnerManager()
class Meta:
base_manager_name = 'objects'
The problem is when it is used with related obj it asks for user arg which makes sense but since I am using it with a related obj I wanted to use the related obj as arg so,
user.partner.all() - should use user as arg and fetch the results
user.partner.all(user) - and I should not have to do the below
2 related questions:
1) Does this make sense - should I be doing this?
2) how I can achieve user.partner.all() without adding user in arg
PS: I know i can work with middleware to get_current_user but this function is not reliable as per some of the responses on a different question on SO.
I don't think what you are trying to do will work. Your new situation with a OneToOneField gives you the partner instance.
>>>> user.partner
<Partner xxx>
While in the old situation with the ManyToManyField, the PartnerQuerySet would've been returned.
>>>> user.partner
<PartnerQuerySet []>
A solution would be to create a custom OneToOneField, but this would most probably violate the "simple is better than complex" rule and in the end may even be more work than changing all existing .all()'s.
I'm developing a legacy Django 1.7 system for a client. The programmers before me overrode the Member model (basically the User model) "objects" property with a filter query that removes anything with "is_deleted" set to "True". I've listed the snippets below:
Member class snippet:
class Member(AbstractUser):
objects = MemberManager()
all_objects = models.Manager()
MemberManager class snippet:
class MemberManager(BaseUserManager):
def get_queryset(self):
return super(MemberManager, self).get_queryset().filter(is_deleted=False)
Now when I try to update a user that has the is_deleted flag set to "True" it fails. Below is an example code snippet. Notice how I use "all_objects" which is the default models.Manager() that returns all records.
user = Member.all_objects.get(pk=id) # id of an is_deleted = True record
user.is_deleted = False
user.save()
This code causes this Django query to run which unfortunately has "is_deleted = 0" included in the WHERE clause, which causes it to not find the record. Below is what shows up in the logs:
UPDATE Member [[snip...]] WHERE (Member.is_deleted = 0 AND Member.id = 6)
Is there any way to call "save()" that will not use the MemberManager.objects get_queryset filter?
I think the problem stems from having MemberManager listed first. As the documentation says:
Take note that the first Manager Django encounters (in the order in which they’re defined in the model) has a special status. Django interprets the first Manager defined in a class as the "default" Manager, and several parts of Django will use that Manager exclusively for that model. As a result, it’s a good idea to be careful in your choice of default manager in order to avoid a situation where overriding get_queryset() results in an inability to retrieve objects you’d like to work with.
Reversing the order of objects and all_objects should fix the problem.
I tried Kevin Christopher Henry's answer and unfortunately changing the default manager messed up the authentication code, which needed to inherit from the "BaseUserManager" class. The solution was to create an "undelete" function in the Member model class that uses the "all_objects" property to undelete the user before it's saved.
class Member(AbstractUser):
objects = MemberManager() # default manager
all_objects = models.Manager()
def undelete(self):
if self.is_deleted:
Member.all_objects.filter(id=self.id).update(is_deleted=False)
Then in my code I did this:
user = Member.all_objects.get(pk=id)
user.undelete()
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='')