Accessing parent model instance within model admin to provide custom queryset - django

I want to provide a custom queryset within a model admin class that inherits from TabluarInline, but I want to provide this queryset by calling a method of current instance of the model object.
I have two models. One for tracks belonging to an album, and one for the Album itself. Some tracks can be hidden and I have a method in Album to return only the visible tracks.
class Track(models.Model):
name = models.CharField()
length = models.IntegerField()
album = ForeignKey(Album)
hidden = BooleanField()
class Album(models.Model):
name = models.CharField()
def get_visible_tracks_queryset(self):
return self.track_set.filter(hidden=False)
And I have a tracks inline admin which is included on the django admin page for an album. I want to re-use the get_visible_tracks_queryset to define the queryset for this inline admin, I don't want to repeat the logic again. I can't figure out how to do it. I could do something like the following, however I'm using a simplified example here, I actually have more complex logic and I don't want to be repeating the logic in multiple places.
class TracksInlineAdmin(admin.TabularInline):
fields = ("name", "length")
model = Track
def get_queryset(self, request):
qs = super(TracksInlineAdmin, self).get_queryset(request)
return qs.filter(hidden=False)
Ideally I could do something like:
class TracksInlineAdmin(admin.TabularInline):
fields = ("name", "length")
model = Track
def get_queryset(self, request, parent_model_instance):
return parent_model_instance.get_visible_tracks_queryset()
Any thoughts on how to achieve this?

The cleanest way is to define a custom QuerySet class for your model in which you can define any complex filters for re-use in various places:
class Track(models.Model):
# fields defined here
objects = TrackManager()
class TrackManager(models.Manager):
def get_queryset(self):
return TrackQuerySet(self.model, using=self._db)
class TrackQuerySet(models.QuerySet):
def visible(self):
return self.filter(hidden=False)
Now, anywhere in code, when you have a queryset of tracks (e.g. Track.objects.filter(name="my movie")) you can add .visible() to filter further. Also on a related set:
album.track_set.all().visible()

Related

Prevent need for the same select_related clause on multiple views in DRF

Given the following models...
class Player(models.Model):
user = models.ForeignKey(User)
class Activity(models.Model):
player = models.ForeignKey(Player)
and these serializers...
class PlayerSerializer(serializers.ModelSerializer):
class Meta:
model = Player
fields = ['user']
class ActivitySerializer(serializers.ModelSerializer):
player = PlayerSerializer()
class Meta:
model = Activity
fields = ['player']
if I want to use django-rest-framework to list all the activities, I need to do something like this...
class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Activity.objects.select_related("player__user") <--- this is needed because the activity serializer is going to serialize the player which has a user
serializer_class = ActivitySerializer
That's all fine. But then, every time I write a new view which, at some level, uses PlayerSerializer, I'm going to have to remember to do the proper select_related clause in order to keep the number of DB lookups low.
I'm going to end up writing a lot of views which have the same select_related clauses (mine are actually a LOT more complicated than this example) and, if the PlayerSerializer ever changes, I'm going to need to remember to change all the view lookups.
This doesn't seem very DRY to me and I feel like there must be a better way to do it. Have I missed something obvious?
Maybe having a base parent class like:
class BaseViewset(viewsets.ReadOnlyModelViewSet):
SELECT_RELATED_FIELD = None # for providing select related field in your queryset or maybe have a default value for this one (based on your use case)
def get_queryset(self):
if not self.SELECT_RELATED_FIELD or not isinstance(self.SELECT_RELATED_FIELD, str):
raise NotImplementedError("Ensure 'SELECT_RELATED_FIELD' is not empty/ Is instance of string")
return super().get_queryset().select_related(self.SELECT_RELATED_FIELD)
And then use this parent class in all of your views instead of ReadOnlyModelViewSet:
class ActivityViewSet(BaseViewset):
SELECT_RELATED_FIELD = "player__user"
queryset = Activity.objects.all()
serializer_class = ActivitySerializer
would be a good start (in all views that you have select_related part).

Why does a queryset applied in a ModelForm not inherit a queryset from a ModelManager?

I have a custom queryset on a model manager:
class TenantManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(myfield=myvalue)
class TenantModel(TenantModelMixin, models.Model):
objects = TenantManager()
class Meta:
abstract = True
I use the abstract TenantModel as a mixin with another model to apply the TenantManager. E.g.
class MyModel(TenantModel):
This works as expected, applying the TenantManager filter every time MyModel.objects.all() is called when inside a view.
However, when I create a ModelForm with the model, the filter is not applied and all results (without the filter are returned. For example:
class AddPersonForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('person', )
Why is this and how to I ensure the ModelManager is applied to the queryset in ModelForm?
Edit
#Willem suggests the reason is forms use ._base_manager and not .objects (although I can not find this in the Django source code), however the docs say not to filter this kind of manager, so how does one filter form queries?
Don’t filter away any results in this type of manager subclass
This
manager is used 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_queryset() method and filter out any rows,
Django will return incorrect results. Don’t do that. A manager that
filters results in get_queryset() is not appropriate for use as a base
manager.
You can do it in two ways:
First: When creating the form instance, add the queryset for the desired field.
person_form = AddPersonForm()
person_form.fields["myfield"].queryset = TenantModel.objects.filter(myfield="myvalue")
Second: Override the field's queryset in the AddPersonForm itself.
class AddPersonForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('person', )
def __init__(self, *args, **kwargs):
super(AddPersonForm, self).__init__(*args, **kwargs)
self.fields['myfield'].queryset = TenantModel.objects.filter(myfield="myvalue")
I'm not sure why your code doesn't properly works. Probably you haven't reload django app. You could load queryset in __init__ of your form class
class AddPersonForm(forms.ModelForm):
person = forms.ModelMultipleChoiceField(queryset=None)
class Meta:
model = MyOtherModel
fields = ('person', )
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['person'].queryset = MyModel.objects.all()

Filter by custom QuerySet of a related model in Django

Let's say I have two models: Book and Author
class Author(models.Model):
name = models.CharField()
country = models.CharField()
approved = models.BooleanField()
class Book(models.Model):
title = models.CharField()
approved = models.BooleanField()
author = models.ForeignKey(Author)
Each of the two models has an approved attribute, which shows or hides the object from the website. If the Book is not approved, it is hidden. If the Author is not approved, all his books are hidden.
In order to define these criteria in a DRY manner, making a custom QuerySet looks like a perfect solution:
class AuthorQuerySet(models.query.QuerySet):
def for_site():
return self.filter(approved=True)
class BookQuerySet(models.query.QuerySet):
def for_site():
reuturn self.filter(approved=True).filter(author__approved=True)
After hooking up these QuerysSets to the corresponding models, they can be queried like this: Book.objects.for_site(), without the need to hardcode all the filtering every time.
Nevertheless, this solution is still not perfect. Later I can decide to add another filter to authors:
class AuthorQuerySet(models.query.QuerySet):
def for_site():
return self.filter(approved=True).exclude(country='Problematic Country')
but this new filter will only work in Author.objects.for_site(), but not in Book.objects.for_site(), since there it is hardcoded.
So my questions is: is it possible to apply a custom queryset of a related model when filtering on a different model, so that it looks similar to this:
class BookQuerySet(models.query.QuerySet):
def for_site():
reuturn self.filter(approved=True).filter(author__for_site=True)
where for_site is a custom QuerySet of the Author model.
I think, I've come up with a solution based on Q objects, which are described in the official documentation. This is definitely not the most elegant solution one can invent, but it works. See the code below.
from django.db import models
from django.db.models import Q
######## Custom querysets
class QuerySetRelated(models.query.QuerySet):
"""Queryset that can be applied in filters on related models"""
#classmethod
def _qq(cls, q, related_name):
"""Returns a Q object or a QuerySet filtered with the Q object, prepending fields with the related_name if specified"""
if not related_name:
# Returning Q object without changes
return q
# Recursively updating keywords in this and nested Q objects
for i_child in range(len(q.children)):
child = q.children[i_child]
if isinstance(child, Q):
q.children[i_child] = cls._qq(child, related_name)
else:
q.children[i_child] = ('__'.join([related_name, child[0]]), child[1])
return q
class AuthorQuerySet(QuerySetRelated):
#classmethod
def for_site_q(cls, q_prefix=None):
q = Q(approved=True)
q = q & ~Q(country='Problematic Country')
return cls._qq(q, q_prefix)
def for_site(self):
return self.filter(self.for_site_q())
class BookQuerySet(QuerySetRelated):
#classmethod
def for_site_q(cls, q_prefix=None):
q = Q(approved=True) & AuthorQuerySet.for_site_q('author')
return cls._qq(q, q_prefix)
def for_site(self):
return self.filter(self.for_site_q())
######## Models
class Author(models.Model):
name = models.CharField(max_length=255)
country = models.CharField(max_length=255)
approved = models.BooleanField()
objects = AuthorQuerySet.as_manager()
class Book(models.Model):
title = models.CharField(max_length=255)
approved = models.BooleanField()
author = models.ForeignKey(Author)
objects = BookQuerySet.as_manager()
This way, whenever the AuthorQuerySet.for_site_q() method is changed, it will be automatically reflected in the BookQuerySet.for_site() method.
Here the custom QuerySet classes perform selection at the class level by combining different Q objects, instead of using filter() or exclude() methods at the object level. Having a Q object allows 3 different ways of using it:
put it inside a filter() call, to filter a queryset in place;
combine it with other Q objects using & (AND) or | (OR) operators;
dynamically change names of keywords used in the Q objects by accessing its children attribute, which is defined in the superclass django.utils.tree.Node
The _qq() method defined in every custom QuerySet class takes care of prepending the specified related_name to all filter keys.
If we have a q = Q(approved=True) object, then we can have the following outputs:
self._qq(q) – is equivalent to self.filter(approved=True);
self._qq(q, 'author') – is equivalent to self.filter(author__approved=True)
This solution still has serious drawbacks:
one has to import and call custom QuerySet class of the related model explicitly;
for each filter method one has to define two methods filter_q (class method) and filter (instance method);
UPDATE: The drawback 2. can be partially reduced by creating filter methods dynamically:
# in class QuerySetRelated
#classmethod
def add_filters(cls, names):
for name in names:
method_q = getattr(cls, '{0:s}_q'.format(name))
def function(self, *args, **kwargs):
return self.filter(method_q(*args, **kwargs))
setattr(cls, name, function)
AuthorQuerySet.add_filters(['for_site'])
BookQuerySet.add_filters(['for_site'])
Therefore, if someone comes up with a more elegant solution, please suggest it. It would be very appreciated.

"list_editable" doesn't work in admin changelist view, using custom get_queryset() method

I use custom abstract model with manager throughout my project.
class BaseQueryset(models.QuerySet):
pass
class BaseManager(models.Manager):
queryset_class = BaseQueryset
def get_queryset(self, exclude_no_published=True):
""" exclude all objects with is_published=False by default """
q = self.queryset_class(self.model)
if exclude_no_published:
q = q.exclude(is_published=False)
return q
def all_objects(self):
""" allows geting all objects in admin """
return self.get_queryset(exclude_no_published=False)
class BaseAbstractModel(models.Model):
is_published = models.BooleanField(default=True)
objects = BaseManager()
class Meta:
abstract = True
All models inherit from this abstract model and I need a way to represent all objects in admin. So I wrote my own mixin for admin classes with get_queryset method
class AdminFullQuerysetMixin(object):
def get_queryset(self, request):
"""
Allows showing all objects despite on is_public=False
"""
qs = self.model.objects.all_objects()
ordering = self.get_ordering(request)
if ordering:
qs = qs.order_by(*ordering)
return qs
There is my typical admin class:
#admin.register(SomeModel)
class SomeModelAdmin(AdminFullQuerysetMixin, admin.ModelAdmin):
list_display = ('name', 'slug', 'is_published')
list_filter = ('is_published',)
list_editable = ('is_published',)
All works fine, I can see all objects in admin whether with is_published False or True. But such attributes like list_filter or list_editable don't work, when I use it in admin objects list page. There is no exception provided, just text at the top of the list: "Please correct the error below".
What methods except get_queryset should I override for solving my problem?
You may want to read this :
if you use custom Manager objects, 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.
I strongly suspect you fell upon one of those cases...
The solution would then be to change your model to:
class BaseAbstractModel(models.Model):
is_published = models.BooleanField(default=True)
# this one will be the default manager
all_objects = models.Manager()
# and this one will be known as 'objects'
objects = BaseManager()
class Meta:
abstract = True
Then you can remove your AdminFullQuerysetMixin (or rewrite it's get_queryset() method to use self.model._default_manager instead)
NB : I may of course be wrong and the problem be totally unrelated ;)

How to order a Django Rest Framework ManyToMany related field?

I have a Django Rest Framework application with the following (simplified) models.py:
class Photo(models.Model):
...
class Album(models.Model):
...
photos = models.ManyToManyField(Photo, through='PhotoInAlbum', related_name='albums')
class PhotoInAlbum(models.Model):
photo = models.ForeignKey(Photo)
album = models.ForeignKey(Album)
order = models.IntegerField()
class Meta:
ordering = ['album', 'order']
And in my serializers.py, I have the following:
class AlbumSerializer(serializers.ModelSerializer):
...
photos = serializers.PrimaryKeyRelatedField('photos', many=True)
My question is, how can I have AlbumSerializer return the photos ordered by the field order?
The best solution to customise the queryset is using serializers.SerializerMethodField, but what shezi's reply is not exactly right. You need to return serializer.data from SerializerMethodField. So the solution should be like this:
class PhotoInAlbumSerializer(serialisers.ModelSerializer):
class Meta:
model = PhotoInAlbum
class AlbumSerializer(serializers.ModelSerializer):
# ...
photos = serializers.SerializerMethodField('get_photos_list')
def get_photos_list(self, instance):
photos = PhotoInAlbum.objects\
.filter(album_id=instance.id)\
.order_by('order')\
.values_list('photo_id', flat=True)
return PhotoInAlbumSerializer(photos, many=True, context=self.context).data
It looks as if the RelatedManager that handles the relationship for ManyToManyFields does not respect ordering on the through model.
Since you cannot easily add an ordering parameter to the serializer field, the easiest way to achieve ordering is by using a serializer method:
class AlbumSerializer(serializers.modelSerializer):
# ...
photos = serializers.SerializerMethodField('get_photos_list')
def get_photos_list(self, instance):
return PhotoInAlbum.objects\
.filter(album_id=instance.id)\
.order_by('order')\
.values_list('photo_id', flat=True)
Generally, the easiest way is to do this in your AlbumView or AlbumViewSet.
You can do this by filtering - in this case you should define a get_queryset method in your AlbumViewSet.
Anyway, this is a good solution as long as you only GET the data. If you want to have POST and PUT methods working with ordering the photos, you can do it in two ways:
stay with ManyToMany relation - patch the create method in AlbumViewSet and __create_items and restore_object method in AlbumSerializer
or
replace it with something more sophisticated - use django-sortedm2m field.
Note that the second solution does not mess with AlbumViewSet (and even AlbumSerializer!) - ordering logic stays in the relation field code.