I have a Django model:
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.PROTECT)
#property
def slug(self):
return slugify(self.author.name)
Now if I add slug field to admin list_display, there will be a separated query for each instance.
How to make just one query for all instances?
I tried to use select_related in the ModelAdmin class, but I did not get it working.
You can override get_queryset() of your ModelAdmin to add your select_related.
def get_queryset(self, request):
return super().get_queryset(request).select_related('author')
Related
Hello Django Programmers,
I have an issue which I don't understand.
Here is my model class:
class Profile(models.Model):
name = models.CharField(max_length=50)
employeeView = models.BooleanField(default=True)
owner = models.OneToOneField(User, related_name="profile", on_delete=models.CASCADE, null=True)
This class extends my User Model. Please notice that I'm using here OneToOneField in relation to User Model.
This model is serialized with that Serializer:
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Profile
fields = (
'url',
'pk',
'name',
'employeeView')
And finally I have view which I use to process GET (list) and POST requests to the database:
below view works fine (for GET request), but it gives me all Profiles related with all users
class ProfileList(generics.ListCreateAPIView):
permission_classes = [
permissions.IsAuthenticated
]
serializer_class = ProfileSerializer
name = 'profile-list'
queryset = Profile.objects.all()
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
Because I would like to create view which could give me only profile for My user, I modifiet above view like so:
class ProfileList(generics.ListCreateAPIView):
permission_classes = [
permissions.IsAuthenticated
]
serializer_class = ProfileSerializer
name = 'profile-list'
##queryset = Profile.objects.all()
def get_queryset(self):
return self.request.user.profile.all()
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
But when I send the GET request I get this error:
{code}
'Profile' object has no attribute 'all'
{code}
So my question here is more general. Above view will work when I will use in my Profile model ForeignKey field instead of OneToOneField. In such case when in my view I will request for all objects which belongs to my user like so:
def get_queryset(self):
return self.request.user.profile.all()
I will get necessary data. But how to ask for objects when we have OneToOneField? It seems that the procedure is different, but I can not find it anywhere.
all() implies that there are many related objects, so this is applicable for reverse one-to-many (ForeignKey) and many-to-many (ManyToManyField) calls.
In case of one-to-one (OneToOneField), as the name suggests, one object can be related to only one of the object of the related model, hence there's no point of all-like methods here.
Now, the get_queryset method is supposed to return a queryset of objects, not a single one. So you can not use self.request.user.profile as that refers to only one instance.
You need to return a queryset with the only instance of Profile that is related to the requesting User:
def get_queryset(self):
return Profile.objects.filter(owner=self.request.user)
The documentation https://docs.djangoproject.com/en/2.2/topics/db/examples/one_to_one/ says that you would access the user profile by using user.profile. This makes sense, since it’s a OneToOneField and so we shouldn’t expect a QuerySet, just a single object.
I have a class based ListView of which I would like to filter the objects by the logged in user_id since Item model has a foreign key to settings.AUTH_USER_MODEL
class ItemListView(LoginRequiredMixin, StaffRequiredMixin, ListView):
model = Item
template_name = "items/list_items.html"
In function based views I can do this using request.user but not in generic Class based views. Any ideas of how to best do this?
Here is the Item model
class Item(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
description = models.CharField(max_length=300)
You can override get_queryset function in ListView and filter on self.request.user
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
I have the following class in my admin site:
class MyClassAdmin(admin.ModelAdmin):
options = forms.ModelMultipleChoiceField(queryset=MyClass.objects.filter(is_default=True), widget=FilteredSelectMultiple("verbose name", is_stacked=False))
In my understanding, this should return only objects for which MyClass.is_default is True.
EDIT
The MyClass model only has:
class MyClass(models.Model):
is_default = models.BooleanField(default=False, help_text="is default")
name = models.CharField(help_text="the name of this", max_length=50)
def __unicode__(self):
return self.name
However, what happens is I get all objects of MyClass. Only 2 are set to True! That's really weird to me....
django 1.7.7
If you want to only show objects where is_default=True, you can override the queryset method in your model admin.
class MyClassAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyClassAdmin, self).queryset(request)
return qs.filter(is_default=True)
Update:
As #pawel commented, the queryset() method was renamed to get_queryset() from Django 1.6.
I have two models - Contract and Supplier. Each supplier supplies a type of commodity. These are defined as follows:
class CommodityType(models.Model):
name = models.CharField(max_length=64)
def __unicode__(self):
return self.name
class Supplier(models.Model):
name = models.CharField(max_length=64)
type = models.ForeignKey(CommodityType)
def __unicode__(self):
return self.name
class Meta:
ordering = ['type', 'name']
class Contract(models.Model):
supplier = models.ForeignKey(Supplier)
clientNumber = models.CharField(max_length=32)
def __unicode__(self):
return u'%s, %s' % (self.supplier, self.clientNumber)
I want to have a listing of the Contracts in the Django Admin site. For each of the Contracts, I want to have the type from the referenced Supplier displayed. So, for example, if the associated supplier supplies Electricity, then I want to have that displayed in the listing of Contracts.
However, I cannot seem to find how this is done. I found this answer, but trying that gives me an ImproperlyConfigured error.
How can this be done?
What you probably need is the list_display
class ContractAdmin(admin.ModelAdmin):
list_display('clientNumber', 'supplier')
admin.register(Contract, ContractAdmin)
To allow __ in Admin for foreign key, You can use this snippet
From the snippet:
from django.contrib import admin
from django.db import models
def getter_for_related_field(name, admin_order_field=None, short_description=None):
"""
Create a function that can be attached to a ModelAdmin to use as a list_display field, e.g:
client__name = getter_for_related_field('client__name', short_description='Client')
"""
related_names = name.split('__')
def getter(self, obj):
for related_name in related_names:
obj = getattr(obj, related_name)
return obj
getter.admin_order_field = admin_order_field or name
getter.short_description = short_description or related_names[-1].title().replace('_',' ')
return getter
class RelatedFieldAdminMetaclass(admin.ModelAdmin.__metaclass__):
"""
Metaclass used by RelatedFieldAdmin to handle fetching of related field values.
We have to do this as a metaclass because Django checks that list_display fields are supported by the class.
"""
def __getattr__(self, name):
if '__' in name:
getter = getter_for_related_field(name)
setattr(self, name, getter) # cache so we don't have to do this again
return getter
raise AttributeError # let missing attribute be handled normally
class RelatedFieldAdmin(admin.ModelAdmin):
"""
Version of ModelAdmin that can use related fields in list_display, e.g.:
list_display = ('address__city', 'address__country__country_code')
"""
__metaclass__ = RelatedFieldAdminMetaclass
def queryset(self, request):
qs = super(RelatedFieldAdmin, self).queryset(request)
# include all related fields in queryset
select_related = [field.rsplit('__',1)[0] for field in self.list_display if '__' in field]
# Include all foreign key fields in queryset.
# This is based on ChangeList.get_query_set().
# We have to duplicate it here because select_related() only works once.
# Can't just use list_select_related because we might have multiple__depth__fields it won't follow.
model = qs.model
for field_name in self.list_display:
try:
field = model._meta.get_field(field_name)
except models.FieldDoesNotExist:
continue
if isinstance(field.rel, models.ManyToOneRel):
select_related.append(field_name)
return qs.select_related(*select_related)
#### USAGE ####
class FooAdmin(RelatedFieldAdmin):
# these fields will work automatically:
list_display = ('address__phone','address__country__country_code','address__foo')
# ... but you can also define them manually if you need to override short_description:
address__foo = getter_for_related_field('address__foo', short_description='Custom Name')
Recently, a library called django-related-admin released, which allows you to use foreign key attributes in Django admin change list list_display with '__' so easily, Specifically for this question, how to use this library in admin.py module is as follows:
admin.py
from related_admin import RelatedFieldAdmin
from related_admin import getter_for_related_field
class ContractAdmin(RelatedFieldAdmin):
# these fields will work automatically (and boolean fields will display an icon):
list_display = ('clientNumber','supplier__type__name')
# or you can also define them manually if you need to override short_description or boolean parameter:
supplierType = getter_for_related_field('supplier__type__name', short_description='supplier type', boolean=False)
# admin.py
class CustomerAdmin(admin.ModelAdmin):
list_display = ('foo', 'number_of_orders')
# models.py
class Order(models.Model):
bar = models.CharField[...]
customer = models.ForeignKey(Customer)
class Customer(models.Model):
foo = models.CharField[...]
def number_of_orders(self):
return u'%s' % Order.objects.filter(customer=self).count()
How could I sort Customers, depending on number_of_orders they have?
admin_order_field property can't be used here, as it requires a database field to sort on. Is it possible at all, as Django relies on the underlying DB to perform sorting? Creating an aggregate field to contain the number of orders seems like an overkill here.
The fun thing: if you change url by hand in the browser to sort on this column - it works as expected!
I loved Greg's solution to this problem, but I'd like to point that you can do the same thing directly in the admin:
from django.db import models
class CustomerAdmin(admin.ModelAdmin):
list_display = ('number_of_orders',)
def get_queryset(self, request):
# def queryset(self, request): # For Django <1.6
qs = super(CustomerAdmin, self).get_queryset(request)
# qs = super(CustomerAdmin, self).queryset(request) # For Django <1.6
qs = qs.annotate(models.Count('order'))
return qs
def number_of_orders(self, obj):
return obj.order__count
number_of_orders.admin_order_field = 'order__count'
This way you only annotate inside the admin interface. Not with every query that you do.
I haven't tested this out (I'd be interested to know if it works) but what about defining a custom manager for Customer which includes the number of orders aggregated, and then setting admin_order_field to that aggregate, ie
from django.db import models
class CustomerManager(models.Manager):
def get_query_set(self):
return super(CustomerManager, self).get_query_set().annotate(models.Count('order'))
class Customer(models.Model):
foo = models.CharField[...]
objects = CustomerManager()
def number_of_orders(self):
return u'%s' % Order.objects.filter(customer=self).count()
number_of_orders.admin_order_field = 'order__count'
EDIT: I've just tested this idea and it works perfectly - no django admin subclassing required!
The only way I can think of is to denormalize the field. That is - create a real field that get's updated to stay in sync with the fields it is derived from. I usually do this by overriding save on eith the model with the denormalized fields or the model it derives from:
# models.py
class Order(models.Model):
bar = models.CharField[...]
customer = models.ForeignKey(Customer)
def save(self):
super(Order, self).save()
self.customer.number_of_orders = Order.objects.filter(customer=self.customer).count()
self.customer.save()
class Customer(models.Model):
foo = models.CharField[...]
number_of_orders = models.IntegerField[...]