What is the use of writing custom QuerySet classes in Django - python

I was going through Django documentation for model managers. I saw that in this section i saw that that model.QuerySet class is overriden by a custom QuerySet class which has two methods which returns the queryset by filtering role='A' or role='E'.
class PersonQuerySet(models.QuerySet):
def authors(self):
return self.filter(role='A')
def editors(self):
return self.filter(role='E')
class PersonManager(models.Manager):
def get_queryset(self):
return PersonQuerySet(self.model, using=self._db)
def authors(self):
return self.get_queryset().authors()
def editors(self):
return self.get_queryset().editors()
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
role = models.CharField(max_length=1, choices=[('A', _('Author')), ('E', _('Editor'))])
people = PersonManager()
But I don't understand what was the need of filtering in the QuerySet class when we could have done that in model manager itself like this.
class PersonManager(models.manager):
def authors(self):
return self.filter(role='A')
def editors(self):
return self.filter(role='E')
Please help to understand the difference. Thank you in advance.

While making queries using Django's ORM it is quite common to see people chaining methods on a queryset, example:
queryset.filter(...).annotate(...).order_by(...)
Each of the method here returns an instance of QuerySet upon which we can apply more methods.
Now let us assume we were to use your solution and implement our methods in the model manager. Let us consider the below snippet in that case:
print(type(Person.objects)) # prints the manager class
print(type(Person.objects.all())) # prints the queryset class
Person.objects.authors() # Succeeds
Person.objects.all().authors() # likely (might be some other) raises an AttributeError
Whereas if we had done this with a custom queryset the second queryset too would have worked. This is because if we declared the method on the manager the queryset will not really have it, and hence our chaining of the method would fail.

Related

Make the filter from class method or get the queryset from class method

What I want to do finally is like this using is_special
MyObjectForm = forms.ModelChoiceField(
queryset=MyObj.objects.filter(is_special=False),required=False)
However is_special is not the member of the model but method.
class MyObj(models.Model):
key = m.CharField(max_length=20,null=False,unique=False)
class Meta:
db_table = 'myobj'
def is_special(self):
return SpecialMember.is_key_exist(self.key)
So, I come to two ideas.
Using method as filter variable
Geting the queryset by object method.
Is it possible?

DRF This field is required [duplicate]

With Django REST Framework, a standard ModelSerializer will allow ForeignKey model relationships to be assigned or changed by POSTing an ID as an Integer.
What's the simplest way to get this behavior out of a nested serializer?
Note, I am only talking about assigning existing database objects, not nested creation.
I have hacked away around this in the past with additional 'id' fields in the serializer and with custom create and update methods, but this is such a seemingly simple and frequent issue for me that I'm curious to know the best way.
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# phone_number relation is automatic and will accept ID integers
children = ChildSerializer() # this one will not
class Meta:
model = Parent
Updated on July 05 2020
This post is getting more attention and it indicates more people have a similar situation. So I decided to add a generic way to handle this problem. This generic way is best suitable for you if you have more serializers that need to change to this format
Since DRF doesn't provide this functionality out of the box, we need to create a serializer field first.
from rest_framework import serializers
class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.serializer = kwargs.pop('serializer', None)
if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
raise TypeError('"serializer" is not a valid serializer class')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return False if self.serializer else True
def to_representation(self, instance):
if self.serializer:
return self.serializer(instance, context=self.context).data
return super().to_representation(instance)
I am not well impressed with this class name, RelatedFieldAlternative, you can use anything you want.
Then use this new serializer field in your parent serializer as,
class ParentSerializer(ModelSerializer):
child = RelatedFieldAlternative(queryset=Child.objects.all(), serializer=ChildSerializer)
class Meta:
model = Parent
fields = '__all__'
Original Post
Using two different fields would be ok (as #Kevin Brown and #joslarson mentioned), but I think it's not perfect (to me). Because getting data from one key (child) and sending data to another key (child_id) might be a little bit ambiguous for front-end developers. (no offense at all)
So, what I suggest here is, override the to_representation() method of ParentSerializer will do the job.
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Complete representation of Serializer
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
fields = '__all__'
class ParentSerializer(ModelSerializer):
class Meta:
model = Parent
fields = '__all__'
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Advantage of this method?
By using this method, we don't need two separate fields for creation and reading. Here both creation and reading can be done by using child key.
Sample payload to create parent instance
{
"name": "TestPOSTMAN_name",
"phone_number": 1,
"child": 1
}
Screenshot
The best solution here is to use two different fields: one for reading and the other for writing. Without doing some heavy lifting, it is difficult to get what you are looking for in a single field.
The read-only field would be your nested serializer (ChildSerializer in this case) and it will allow you to get the same nested representation that you are expecting. Most people define this as just child, because they already have their front-end written by this point and changing it would cause problems.
The write-only field would be a PrimaryKeyRelatedField, which is what you would typically use for assigning objects based on their primary key. This does not have to be write-only, especially if you are trying to go for symmetry between what is received and what is sent, but it sounds like that might suit you best. This field should have a source set to the foreign key field (child in this example) so it assigns it properly on creation and updating.
This has been brought up on the discussion group a few times, and I think this is still the best solution. Thanks to Sven Maurer for pointing it out.
Here's an example of what Kevin's answer is talking about, if you want to take that approach and use 2 separate fields.
In your models.py...
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
then serializers.py...
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# if child is required
child = ChildSerializer(read_only=True)
# if child is a required field and you want write to child properties through parent
# child = ChildSerializer(required=False)
# otherwise the following should work (untested)
# child = ChildSerializer()
child_id = serializers.PrimaryKeyRelatedField(
queryset=Child.objects.all(), source='child', write_only=True)
class Meta:
model = Parent
Setting source=child lets child_id act as child would by default had it not be overridden (our desired behavior). write_only=True makes child_id available to write to, but keeps it from showing up in the response since the id already shows up in the ChildSerializer.
There is a way to substitute a field on create/update operation:
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
child = ChildSerializer()
# called on create/update operations
def to_internal_value(self, data):
self.fields['child'] = serializers.PrimaryKeyRelatedField(
queryset=Child.objects.all())
return super(ParentSerializer, self).to_internal_value(data)
class Meta:
model = Parent
A few people here have placed a way to keep one field but still be able to get the details when retrieving the object and create it with only the ID. I made a little more generic implementation if people are interested:
First off the tests:
from rest_framework.relations import PrimaryKeyRelatedField
from django.test import TestCase
from .serializers import ModelRepresentationPrimaryKeyRelatedField, ProductSerializer
from .factories import SomethingElseFactory
from .models import SomethingElse
class TestModelRepresentationPrimaryKeyRelatedField(TestCase):
def setUp(self):
self.serializer = ModelRepresentationPrimaryKeyRelatedField(
model_serializer_class=SomethingElseSerializer,
queryset=SomethingElse.objects.all(),
)
def test_inherits_from_primary_key_related_field(self):
assert issubclass(ModelRepresentationPrimaryKeyRelatedField, PrimaryKeyRelatedField)
def test_use_pk_only_optimization_returns_false(self):
self.assertFalse(self.serializer.use_pk_only_optimization())
def test_to_representation_returns_serialized_object(self):
obj = SomethingElseFactory()
ret = self.serializer.to_representation(obj)
self.assertEqual(ret, SomethingElseSerializer(instance=obj).data)
Then the class itself:
from rest_framework.relations import PrimaryKeyRelatedField
class ModelRepresentationPrimaryKeyRelatedField(PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.model_serializer_class = kwargs.pop('model_serializer_class')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return False
def to_representation(self, value):
return self.model_serializer_class(instance=value).data
The usage is like so, if you have a serializer somewhere:
class YourSerializer(ModelSerializer):
something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer)
This will allow you to create an object with a foreign key still only with the PK, but will return the full serialized nested model when retrieving the object you created (or whenever really).
There is a package for that! Check out PresentablePrimaryKeyRelatedField in Drf Extra Fields package.
https://github.com/Hipo/drf-extra-fields
I think the approach outlined by Kevin probably would be the best solution, but I couldn't ever get it to work. DRF kept throwing errors when I had both a nested serializer and a primary key field set. Removing one or the other would function, but obviously didn't give me the result I needed. The best I could come up with is creating two different serializers for reading and writing, Like so...
serializers.py:
class ChildSerializer(serializers.ModelSerializer):
class Meta:
model = Child
class ParentSerializer(serializers.ModelSerializer):
class Meta:
abstract = True
model = Parent
fields = ('id', 'child', 'foo', 'bar', 'etc')
class ParentReadSerializer(ParentSerializer):
child = ChildSerializer()
views.py
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
queryset = Parent.objects.all()
def get_serializer_class(self):
if self.request.method == 'GET':
return ParentReadSerializer
else:
return self.serializer_class
Here's how I've solved this problem.
serializers.py
class ChildSerializer(ModelSerializer):
def to_internal_value(self, data):
if data.get('id'):
return get_object_or_404(Child.objects.all(), pk=data.get('id'))
return super(ChildSerializer, self).to_internal_value(data)
You'll just pass your nested child serializer just as you get it from the serializer ie child as a json/dictionary. in to_internal_value we instantiate the child object if it has a valid ID so that DRF can further work with the object.
I started by implementing something similar to JPG's solution before I found this answer, and noticed that it breaks the built-in Django Rest Framework's templates. Now, that isn't such a big deal (as their solution works wonderfully via requests/postman/AJAX/curl/etc.), but if someone's new (like me) and wants the built-in DRF form to help them along the way, here's my solution (after cleaning it up and integrating some of JPG's ideas):
class NestedKeyField(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.serializer = kwargs.pop('serializer', None)
if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
raise TypeError('You need to pass a instance of serialzers.Serializer or atleast something that inherits from it.')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return not self.serializer
def to_representation(self, value):
if self.serializer:
return dict(self.serializer(value, context=self.context).data)
else:
return super().to_representation(value)
def get_choices(self, cutoff=None):
queryset = self.get_queryset()
if queryset is None:
return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([
(
self.to_representation(item)['id'] if self.serializer else self.to_representation(item), # If you end up using another column-name for your primary key, you'll have to change this extraction-key here so it maps the select-element properly.
self.display_value(item)
)
for item in queryset
])
and an example below,
Child Serializer class:
class ChildSerializer(serializers.ModelSerializer):
class Meta:
model = ChildModel
fields = '__all__'
Parent Serializer Class:
class ParentSerializer(serializers.ModelSerializer):
same_field_name_as_model_foreign_key = NestedKeyField(queryset=ChildModel.objects.all(), serializer=ChildSerializer)
class Meta:
model = ParentModel
fields = '__all__'
Based on the answers of both JPG and Bono, I came up with a solution that handles the OpenAPI Schema generator of DRF as well.
The actual field class is:
from rest_framework import serializers
class ModelRepresentationPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.response_serializer_class = kwargs.pop('response_serializer_class', None)
if self.response_serializer_class is not None \
and not issubclass(self.response_serializer_class, serializers.Serializer):
raise TypeError('"serializer" is not a valid serializer class')
super(ModelRepresentationPrimaryKeyRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return False if self.response_serializer_class else True
def to_representation(self, instance):
if self.response_serializer_class is not None:
return self.response_serializer_class(instance, context=self.context).data
return super(ModelRepresentationPrimaryKeyRelatedField, self).to_representation(instance)
The extended AutoSchema class is:
import inspect
from rest_framework.schemas.openapi import AutoSchema
from .fields import ModelRepresentationPrimaryKeyRelatedField
class CustomSchema(AutoSchema):
def _map_field(self, field):
if isinstance(field, ModelRepresentationPrimaryKeyRelatedField) \
and hasattr(field, 'response_serializer_class'):
frame = inspect.currentframe().f_back
while frame is not None:
method_name = frame.f_code.co_name
if method_name == '_get_request_body':
break
elif method_name == '_get_responses':
field = field.response_serializer_class()
return super(CustomSchema, self)._map_field(field)
frame = frame.f_back
return super(CustomSchema, self)._map_field(field)
Then on your Dganjo's project settings you can define this new Schema class to be used globally like:
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': '<path_to_custom_schema>.CustomSchema',
}
Lastly from within your models you can use the new field type like:
class ExampleSerializer(serializers.ModelSerializer):
test_field = ModelRepresentationPrimaryKeyRelatedField(queryset=Test.objects.all(), response_serializer_class=TestListSerializer)
I have been also stuck in the same situation. But what i have done that i have created two serializers for the following models as follow:
class Base_Location(models.Model):
Base_Location_id = models.AutoField(primary_key = True)
Base_Location_Name = models.CharField(max_length=50, db_column="Base_Location_Name")
class Location(models.Model):
Location_id = models.AutoField(primary_key = True)
Location_Name = models.CharField(max_length=50, db_column="Location_Name")
Base_Location_id = models.ForeignKey(Base_Location, db_column="Base_Location_id", related_name="Location_Base_Location", on_delete=models.CASCADE)
This is my parent serializer
class BaseLocationSerializer(serializers.ModelSerializer):
class Meta:
model = Base_Location
fields = "__all__"
I'm using this serializer only for get request so in response i got data with foreign key also because of nested serializer
class LocationSerializerList(serializers.ModelSerializer): <-- using for get request
Base_Location_id = BaseLocationSerializer()
class Meta:
model = Location
fields = "__all__"
Screenshot of get method request and response in postman
I'm using this serializer only for post request so while sending post request i do not need to include any additional information rather than primary key field value
class LocationSerializerInsert(serializers.ModelSerializer): <-- using for post request
class Meta:
model = Location
fields = "__all__"
Screenshot of post method request and response in postman
Here's what I'm using all over. This may be the simplest, most straight forward method which needs no hacks etc, and is directly using DRF without jumping thru hoops. Happy to hear disagreements with this approach.
In the view's perform_create (or equivalent), fetch the FK model database object corresponding to the field sent in the POST request, and then send that into the Serializer. The field in the POST request can be anything that can be used to filter and locate the DB object, need not be an ID.
This is documented here: https://www.django-rest-framework.org/api-guide/generic-views/#genericapiview
These hooks are particularly useful for setting attributes that are
implicit in the request, but are not part of the request data. For
instance, you might set an attribute on the object based on the
request user, or based on a URL keyword argument.
def perform_create(self, serializer):
serializer.save(user=self.request.user)
This method also has the advantage of maintaining parity between the read and write side, by not sending a nested representation for child in the response to the GET or POST.
Given the example posted by the OP:
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# Note this is different from the OP's example. This will send the
# child name in the response
child = serializers.ReadOnlyField(source='child.name')
class Meta:
model = Parent
fields = ('name', 'phone_number', 'child')
In the View's perform_create:
class SomethingView(generics.ListCreateAPIView):
serializer_class = ParentSerializer
def perform_create(self, serializer):
child_name = self.request.data.get('child_name', None)
child_obj = get_object_or_404(Child.objects, name=child_name)
serializer.save(child=child_obj)
PS: Please note that I've not tested this above snippet, however its based on a pattern I'm using in many places so it should work as is.

Python programmatically define variables in class body

I have two django models, model Person and model PersonTemplate that have the exact same field names & types, with the exception being that some of the fields are required in Person, whereas they are not in PersonTemplate. It's easy enough to just copy paste the models into both of these classes, but that involves a lot of hard-coding and if I edit one model and forget the other, stuff might break.
The solution I have in mind is to define the django model fields in a function that takes a boolean argument, whether or not the fields are required. Something like
def get_fields(is_required=True):
first_name = models.CharField(max_length=128, verbose_name=_("First Name"),
blank=is_required, null=is_required)
last_name = models.CharField(max_length=128, verbose_name=_("Last Name"),
blank=is_required, null=is_required)
return locals()
class Person(models.Model):
vars = get_fields(True)
class PersonTemplate(models.Model):
vars = get_fields(False)
What I don't know how to do is get the local variables from get_fields into the class body of the models. Anybody have any ideas, or suggestions for a better way to implement these models?
Django has Abstract models which you can use do define repetitive fields and methods and then inherit then to an actual model of your choice.
And for dynamic required field, you can set their values using model methods and override those methods in child model. See this answer.
Something like this should work:
class AbstractPerson(models.Model):
#staticmethod
def get_first_name_requirement():
return True
#staticmethod
def get_last_name_requirement():
return True
first_name = models.CharField(max_length=128, verbose_name=_("First Name"),
blank=get_first_name_required.__func__(), null=get_first_name_required.__func__())
last_name = models.CharField(max_length=128, verbose_name=_("Last Name"),
blank=get_last_name_required.__func__(), null=get_last_name_required.__func__())
class Meta(object):
abstract = True
class Person(AbstractPerson):
#staticmethod
def get_first_name_requirement():
return True
#staticmethod
def get_last_name_requirement():
return True
class PersonTemplate(AbstractPerson):
#staticmethod
def get_first_name_requirement():
return False
#staticmethod
def get_last_name_requirement():
return False
I am sure there must be more elegant way for dynamically assigning null values. But this too will get the job done.
To achieve you idea you need to use metaclasses. But as usual this is bad idea.
Why do not you look at Django Abstract models and inheritance? This will solve your question in much more readable and simple way.

Tastypie resource not showing newly created objects (date filtering issue)

I have a model with a custom manager with the purpose of filtering "active" objects, i.e. objects which have a start_date lower than the current time and an end_date greater than the current time.
This is the relevant part of my models.py:
from django.utils.timezone import now
class ActiveObjectManager(models.Manager):
def get_query_set(self):
return super(ActiveObjectManager, self).get_query_set().\
filter(start_date__lt=now(), end_date__gt=now())
class Object(models.Model):
start_date = models.DateTimeField(_('Service start date'), \
auto_now_add=False, null=False, blank=False)
end_date = models.DateTimeField(_('Service end date'), auto_now_add=False, \
null=False, blank=False)
...
objects = models.Manager()
objects_active = ActiveObjectManager()
This manager works great across the application and in a Django shell. However, if I create an object in the admin interface, and set the start_date to the "now" selector, the API provided by tastypie isn't showing this newly created object (though it does show older objects). The admin list correctly shows the new object as active.
This is the relevant part of my api.py:
from app.models import Object
class ActiveObjectResource(ModelResource):
modified = fields.BooleanField(readonly=True)
class Meta:
resource_name = 'activeobjects'
queryset = Object.objects_active.all()
My strong suspicion is that, as the class ActiveObjectResource is being interpreted once, the couple of now() calls are only being executed once, i.e., the API subsystem is always calling filter() with the same values for the start_date__lt and end_date__gt parameters (the value returned by now() immediately after I run manage.py runserver).
This problem persists even when I do the filtering right in the resource class like this:
class ActiveObjectResource(ModelResource):
...
class Meta:
queryset = Object.objects.\
filter(start_date__lt=now(), end_date__gt=now())
Also, the problem persists if I pass callables like this:
class ActiveObjectResource(ModelResource):
...
class Meta:
queryset = Object.objects.filter(start_date__lt=now, end_date__gt=now)
Is there a way I can rewrite ActiveObjectManager or ActiveObjectResource to overcome this?
Update:
OK, it seems I need to override get_object_list to achieve per-request alterations to the queryset, like:
class ActiveObjectResource(ModelResource):
class Meta:
queryset = Object.objects.all()
def get_object_list(self, request):
return super(MyResource, self).get_object_list(request).\
filter(start_date__lt=now, end_date__gt=now)
But I hate to duplicate this logic when I already have a custom manager at the model level to do this work for me.
So my question is: how can I use my custom model manager from within my ModelResource?
Well, about queryset in ModelResource.Meta. Here's the excerpt from the tastypie documentation:
If you place any callables in this, they’ll only be evaluated once (when the Meta class is instantiated). This especially affects things that are date/time related. Please see the :ref:cookbook for a way around this.
Here it goes:
A common pattern is needing to limit a queryset by something that changes per-request, for instance the date/time. You can accomplish this by lightly modifying get_object_list
So, yeah, seems like the only way to achieve what you are trying to do is to declare get_object_list.
New Update: since get_object_list is just a return self._meta.queryset._clone(), try something like that:
class ActiveObjectResource(ModelResource):
class Meta:
queryset = Object.objects_active.all()
def get_object_list(self, request):
return Object.objects_active.all()

Can "list_display" in a Django ModelAdmin display attributes of ForeignKey fields?

I have a Person model that has a foreign key relationship to Book, which has a number of fields, but I'm most concerned about author (a standard CharField).
With that being said, in my PersonAdmin model, I'd like to display book.author using list_display:
class PersonAdmin(admin.ModelAdmin):
list_display = ['book.author',]
I've tried all of the obvious methods for doing so, but nothing seems to work.
Any suggestions?
As another option, you can do lookups like:
#models.py
class UserAdmin(admin.ModelAdmin):
list_display = (..., 'get_author')
def get_author(self, obj):
return obj.book.author
get_author.short_description = 'Author'
get_author.admin_order_field = 'book__author'
Since Django 3.2 you can use display() decorator:
#models.py
class UserAdmin(admin.ModelAdmin):
list_display = (..., 'get_author')
#admin.display(ordering='book__author', description='Author')
def get_author(self, obj):
return obj.book.author
Despite all the great answers above and due to me being new to Django, I was still stuck. Here's my explanation from a very newbie perspective.
models.py
class Author(models.Model):
name = models.CharField(max_length=255)
class Book(models.Model):
author = models.ForeignKey(Author)
title = models.CharField(max_length=255)
admin.py (Incorrect Way) - you think it would work by using 'model__field' to reference, but it doesn't
class BookAdmin(admin.ModelAdmin):
model = Book
list_display = ['title', 'author__name', ]
admin.site.register(Book, BookAdmin)
admin.py (Correct Way) - this is how you reference a foreign key name the Django way
class BookAdmin(admin.ModelAdmin):
model = Book
list_display = ['title', 'get_name', ]
def get_name(self, obj):
return obj.author.name
get_name.admin_order_field = 'author' #Allows column order sorting
get_name.short_description = 'Author Name' #Renames column head
#Filtering on side - for some reason, this works
#list_filter = ['title', 'author__name']
admin.site.register(Book, BookAdmin)
For additional reference, see the Django model link here
Like the rest, I went with callables too. But they have one downside: by default, you can't order on them. Fortunately, there is a solution for that:
Django >= 1.8
def author(self, obj):
return obj.book.author
author.admin_order_field = 'book__author'
Django < 1.8
def author(self):
return self.book.author
author.admin_order_field = 'book__author'
Please note that adding the get_author function would slow the list_display in the admin, because showing each person would make a SQL query.
To avoid this, you need to modify get_queryset method in PersonAdmin, for example:
def get_queryset(self, request):
return super(PersonAdmin,self).get_queryset(request).select_related('book')
Before: 73 queries in 36.02ms (67 duplicated queries in admin)
After: 6 queries in 10.81ms
For Django >= 3.2
The proper way to do it with Django 3.2 or higher is by using the display decorator
class BookAdmin(admin.ModelAdmin):
model = Book
list_display = ['title', 'get_author_name']
#admin.display(description='Author Name', ordering='author__name')
def get_author_name(self, obj):
return obj.author.name
According to the documentation, you can only display the __unicode__ representation of a ForeignKey:
http://docs.djangoproject.com/en/dev/ref/contrib/admin/#list-display
Seems odd that it doesn't support the 'book__author' style format which is used everywhere else in the DB API.
Turns out there's a ticket for this feature, which is marked as Won't Fix.
I just posted a snippet that makes admin.ModelAdmin support '__' syntax:
http://djangosnippets.org/snippets/2887/
So you can do:
class PersonAdmin(RelatedFieldAdmin):
list_display = ['book__author',]
This is basically just doing the same thing described in the other answers, but it automatically takes care of (1) setting admin_order_field (2) setting short_description and (3) modifying the queryset to avoid a database hit for each row.
There is a very easy to use package available in PyPI that handles exactly that: django-related-admin. You can also see the code in GitHub.
Using this, what you want to achieve is as simple as:
class PersonAdmin(RelatedFieldAdmin):
list_display = ['book__author',]
Both links contain full details of installation and usage so I won't paste them here in case they change.
Just as a side note, if you're already using something other than model.Admin (e.g. I was using SimpleHistoryAdmin instead), you can do this: class MyAdmin(SimpleHistoryAdmin, RelatedFieldAdmin).
You can show whatever you want in list display by using a callable. It would look like this:
def book_author(object):
return object.book.author
class PersonAdmin(admin.ModelAdmin):
list_display = [book_author,]
This one's already accepted, but if there are any other dummies out there (like me) that didn't immediately get it from the presently accepted answer, here's a bit more detail.
The model class referenced by the ForeignKey needs to have a __unicode__ method within it, like here:
class Category(models.Model):
name = models.CharField(max_length=50)
def __unicode__(self):
return self.name
That made the difference for me, and should apply to the above scenario. This works on Django 1.0.2.
If you have a lot of relation attribute fields to use in list_display and do not want create a function (and it's attributes) for each one, a dirt but simple solution would be override the ModelAdmin instace __getattr__ method, creating the callables on the fly:
class DynamicLookupMixin(object):
'''
a mixin to add dynamic callable attributes like 'book__author' which
return a function that return the instance.book.author value
'''
def __getattr__(self, attr):
if ('__' in attr
and not attr.startswith('_')
and not attr.endswith('_boolean')
and not attr.endswith('_short_description')):
def dyn_lookup(instance):
# traverse all __ lookups
return reduce(lambda parent, child: getattr(parent, child),
attr.split('__'),
instance)
# get admin_order_field, boolean and short_description
dyn_lookup.admin_order_field = attr
dyn_lookup.boolean = getattr(self, '{}_boolean'.format(attr), False)
dyn_lookup.short_description = getattr(
self, '{}_short_description'.format(attr),
attr.replace('_', ' ').capitalize())
return dyn_lookup
# not dynamic lookup, default behaviour
return self.__getattribute__(attr)
# use examples
#admin.register(models.Person)
class PersonAdmin(admin.ModelAdmin, DynamicLookupMixin):
list_display = ['book__author', 'book__publisher__name',
'book__publisher__country']
# custom short description
book__publisher__country_short_description = 'Publisher Country'
#admin.register(models.Product)
class ProductAdmin(admin.ModelAdmin, DynamicLookupMixin):
list_display = ('name', 'category__is_new')
# to show as boolean field
category__is_new_boolean = True
As gist here
Callable especial attributes like boolean and short_description must be defined as ModelAdmin attributes, eg book__author_verbose_name = 'Author name' and category__is_new_boolean = True.
The callable admin_order_field attribute is defined automatically.
Don't forget to use the list_select_related attribute in your ModelAdmin to make Django avoid aditional queries.
if you try it in Inline, you wont succeed unless:
in your inline:
class AddInline(admin.TabularInline):
readonly_fields = ['localname',]
model = MyModel
fields = ('localname',)
in your model (MyModel):
class MyModel(models.Model):
localization = models.ForeignKey(Localizations)
def localname(self):
return self.localization.name
I may be late, but this is another way to do it. You can simply define a method in your model and access it via the list_display as below:
models.py
class Person(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
def get_book_author(self):
return self.book.author
admin.py
class PersonAdmin(admin.ModelAdmin):
list_display = ('get_book_author',)
But this and the other approaches mentioned above add two extra queries per row in your listview page. To optimize this, we can override the get_queryset to annotate the required field, then use the annotated field in our ModelAdmin method
admin.py
from django.db.models.expressions import F
#admin.register(models.Person)
class PersonAdmin(admin.ModelAdmin):
list_display = ('get_author',)
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.annotate(
_author = F('book__author')
)
return queryset
#admin.display(ordering='_author', description='Author')
def get_author(self, obj):
return obj._author
AlexRobbins' answer worked for me, except that the first two lines need to be in the model (perhaps this was assumed?), and should reference self:
def book_author(self):
return self.book.author
Then the admin part works nicely.
I prefer this:
class CoolAdmin(admin.ModelAdmin):
list_display = ('pk', 'submodel__field')
#staticmethod
def submodel__field(obj):
return obj.submodel.field

Categories

Resources