Django ordering in admin [duplicate] - python

I'm trying to sort a Django Admin list page by a specific value in the objects' related foreign key set.
Specifically, in the below code, I want the ContentAdmin view to show a list of all content objects sorted by the "Twitter Score" (The Score object with name "Twitter").
In the django app I have the following models:
class Content(models.Model):
body = models.CharField(max_length=564)
title = models.CharField(max_length=64)
class Score(models.Model):
name = models.CharField(max_length=64)
score = models.IntegerField()
content = models.ForeignKey('Content')
And in the admin.py I have the following:
class ContentAdmin(admin.ModelAdmin):
list_display = ('title', 'show_twitter_score',)
def show_twitter_score(self, obj):
twitter_score = obj.score_set.get(name='Twitter')
return 'Twitter: ' + str(twitter_score.score)
GOAL: The admin panel for ContentAdmin displays the content objects ordered by "Twitter" scores
Thanks everyone!

I solved this by extending the get_queryset method of the ContentAdmin class. After that, it was just a matter of getting the right ORM query
def get_queryset(self, request):
qs = super(ContentAdmin, self).get_queryset(request)
return qs.filter(score__name='Twitter').order_by('-score__score')
For Django 1.5 and earlier, the method was queryset.
def queryset(self, request):
qs = super(ContentAdmin, self).queryset(request)
return qs.filter(score__name='Twitter').order_by('-score__score')

If I understand correctly, you can try this from ModelAdmin.list_display in Django's documentation:
Usually, elements of list_display that aren't actual database fields can't be used in sorting (because Django does all the sorting at the database level).
However, if an element of list_display represents a certain database field, you can indicate this fact by setting the admin_order_field attribute of the item.
For example:
class Person(models.Model):
first_name = models.CharField(max_length=50)
color_code = models.CharField(max_length=6)
def colored_first_name(self):
return '<span style="color: #%s;">%s</span>' % (self.color_code, self.first_name)
colored_first_name.allow_tags = True
colored_first_name.admin_order_field = 'first_name'
class PersonAdmin(admin.ModelAdmin):
list_display = ('first_name', 'colored_first_name')
The above will tell Django to order by the first_name field when trying to sort by colored_first_name in the admin.
You can try this workaround in your code for the sorting.

Since django admin uses the db to sort you cant sort on the function you are showing in the list.
What you can do is to add the column you want to show to the queryset that django admin is using to list your models, this way you can have sorting.
To add the column you need you have to use the queryset extra method.
This should do the trick :)
Content.objects.all().extra(select={'twitter_score': 'SELECT score from content_score WHERE content_score.id = content_content.id'})
BONUS ROUND:
Content.objects.all().extra(select={'twitter_score': 'SELECT 'Twitter score:' || score from content_score WHERE content_score.id = content_content.id'})

Related

Django form not populating with POST data

SOLUTION AT THE BOTTOM
Problem: Django form populating with list of objects rather than values
Summary: I have 2 models Entities and Breaks. Breaks has a FK relationship to the entity_id (not the PK) on the Entities model.
I want to generate an empty form for all the fields of Breaks. Generating a basic form populates all the empty fields, but for the FK it generates a dropdown list of all objects of the Entities table. This is not helpful so I have excluded this in the ModelForm below and tried to replace with a list of all the entity_ids of the Entities table. This form renders as expected.
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
exclude = ('entity',)
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all().values_list('entity_id', flat=True))
The below FormView is the cbv called by the URL. As the below stands if I populate the form, and for the FK column entity_id choose one of the values, the form will not submit. By that field on the form template the following message appears Select a valid choice. That choice is not one of the available choices.
class ContactFormView(FormView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
My initial thoughts were either that the datatype of this field (string/integer) was wrong or that Django needed the PK of the row in the Entities table (for whatever reason).
So I added a post function to the FormView and could see that the request.body was populating correctly. However I can't work out how to populate this into the ModelForm and save to the database, or overcome the issue mentioned above.
Addendum:
Models added below:
class Entity(models.Model):
pk_securities = models.AutoField(primary_key=True)
entity_id = models.CharField(unique=True)
entity_description = models.CharField(blank=True, null=True)
class Meta:
managed = False
db_table = 'entities'
class Breaks(models.Model):
pk_break = models.AutoField(primary_key=True)
date = models.DateField(blank=True, null=True)
entity = models.ForeignKey(Entity, on_delete= models.CASCADE, to_field='entity_id')
commentary = models.CharField(blank=True, null=True)
active = models.BooleanField()
def get_absolute_url(self):
return reverse(
"item-update", args=[str(self.pk_break)]
)
def __str__(self):
return f"{self.pk_break}"
class Meta:
managed = False
db_table = 'breaks'
SOLUTION
Firstly I got this working by adding the following to the Entity Model class. However I didn't like this as it would have consequences elsewhere.
def __str__(self):
return f"{self.entity_id}"
I found this SO thread on the topic. The accepted answer is fantastic and the comments to it are helpful.
The solution is to subclass ModelChoiceField and override the label_from_instance
class EntityChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return obj.entity_id
I think your problem is two fold, first is not rendering the dropdown correctly and second is form is not saving. For first problem, you do not need to do any changes in ModelChoiceField queryset, instead, add to_field_name:
class BreakForm(ModelForm):
class Meta:
model = Breaks
#fields = '__all__'
def __init__(self, *args, **kwargs):
super(BreakForm, self).__init__(*args, **kwargs)
self.fields['entity_id'] = ModelChoiceField(queryset=Entities.objects.all(), to_field_name='entity_id')
Secondly, if you want to save the form, instead of FormView, use CreateView:
class ContactFormView(CreateView):
template_name = "breaks/test/breaks_form.html"
form_class = BreakForm
model = Breaks
In Django, the request object passed as parameter to your view has an attribute called "method" where the type of the request is set, and all data passed via POST can be accessed via the request. POST dictionary. The view will display the result of the login form posted through the loggedin. html.

How to save the created object with a many-to-many relationship and map table?

Using the accepted answer in Class-based views for M2M relationship with intermediate model, I am able to save the Membership if I hard code an ID for its foreignkey to Group, despite not having any information for date_joined and invite_reason, but the Group for which the form is filled out never gets saved, and therefore I cannot supply Membership with the correct ID. In fact, any time I try to save thegroup, before or after the code in the accepted answer, I get the same AttributeError as the OP:
Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
Is there a way to make this code actually work? Am I being led astray? What would be the proper way to save something with a many-to-many field that goes through a mapping table? The answers I've found have confused me more than helped me.
For reference, should the question ever be deleted:
models.py
class Person(models.Model):
name = models.CharField(max_length=128)
def __unicode__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
def __unicode__(self):
return self.name
class Membership(models.Model):
person = models.ForeignKey(Person)
group = models.ForeignKey(Group)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
Accepted Answer (With an additional line to hard code an ID)
from django.views.generic.edit import ModelFormMixin
from django.views.generic import CreateView
class GroupCreate(CreateView):
model = Group
def form_valid(self, form):
self.object = form.save(commit=False)
for person in form.cleaned_data['members']:
membership = Membership()
membership.group = self.object
membership.group_id = 108 # Added line
membership.person = person
membership.save()
return super(ModelFormMixin, self).form_valid(form)
Note: I had to add the id line because I would get the following IntegrityError without it:
Column 'group_id' cannot be null
In case it's pertinent, I am trying to save this to a MySQL database.

Why does Django admin list_select_related not work in this case?

I've got a ModelAdmin class that includes a foreign key field in its list_display. But the admin list page for that model is doing hundreds of queries, one query per row to get the data from the other table instead of a join (select_related()).
The Django docs indicate you can add list_select_related = True as an attribute to your ModelAdmin to make this go away, but it doesn't seem to work at all for me. This SO question seems to give a similar problem, but his resolution is unclear, and it doesn't work in my case.
Here's a cut-down version of my model and model admin:
class Device(models.Model):
serial_number = models.CharField(max_length=80, blank=True, unique=True)
label = models.CharField(max_length=80, blank=True)
def __str__(self):
s = str(self.serial_number)
if self.label:
s += ': {0}'.format(self.label)
return s
class Event(models.Model):
device = models.ForeignKey(Device, null=True)
type = models.CharField(max_length=40, null=False, blank=True, default='')
class EventAdmin(admin.ModelAdmin):
list_display = ('__str__', 'device')
list_select_related = True
However, adding that list_selected_related = True didn't change anything. I still get lots of queries like this instead of an SQL join:
Any ideas why the Django admin seems to be ignoring my list_select_related and doing N queries? I'm using Python 2.7 and Django 1.3.3.
The issue here is that setting list_select_related = True just adds a basic select_related() onto the query, but that call does not by default follow ForeignKeys with null=True. So the answer is to define the queryset the changelist uses yourself, and specify the FK to follow:
class EventAdmin(admin.ModelAdmin):
list_display = ('__str__', 'device')
def queryset(self, request):
return super(EventAdmin, self).queryset(request).select_related('device')
Since Django 1.6, list_select_related accepts a boolean, list or tuple with the names of the fields to include in the select_related() call.
Hence you can now use:
class EventAdmin(admin.ModelAdmin):
list_display = ('__str__', 'device')
list_select_related = ['device']
Although select_related is generally the way to go, there are time when one requires more control, then overiding the get_queryset becomes more applicable, this is a more modern version of Daniel Roseman's answer:
Where foo and bar are foreign key fields:
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.select_related('foo', 'foo__bar').only('foo__field1', 'foo__bar__field2')

Django admin List Display + ForeignKey = Empty Change List

I've got a weird problem in django admin list_display. Whenever I add a foreign key to a list_display the whole change list view goes blank showing only the total no of entries.
models.py:
class Organization(models.Model):
org_id = models.AutoField(primary_key=True)
org_name = models.CharField(max_length=288)
def __unicode__(self):
return self.org_name
class Meta:
db_table = u'organization'
class Server(models.Model):
server_id = models.AutoField(primary_key=True)
server_name = models.CharField(max_length=135,verbose_name="Server Name")
org = models.ForeignKey(Organization,verbose_name="Organization")
def __unicode__(self):
return self.server_name
class Meta:
db_table = u'server'
admin.py:
class ServerAdmin(admin.ModelAdmin):
list_display = ('server_name','org')
admin.site.register(Server,ServerAdmin)
Now I'd expect this code to show me the organization name in the ChangeList View, But instead I get this:
If I remove the org in the list_display of ServerAdmin class, I get this:
I didn't modify the template or override any ModelAdmin methods. I'm using Mysql(5.1.58) as my database that comes with ubuntu 11.10 repository.
I'll be really glad if I could a get a sloution for this problem guys. Thanks in advance.
I second Stefano on the fact that null=True, blank=True is to be added. But, I think you only need to add it to the org_name field of the Organization model. That should make your way through. It has to be done because you have run inspectdb to create models from your legacy DB. And probably the organization table in the DB has an empty string stored. So, adding the above would allow the Admin to have a blank field/column displayed.
Moreover, you can also try using callbacks in situations where you don't want to make changes to your model definition like the above.
Try adding null=True, blank=True to all your model fields.
Usually django admin will silenty fail (thus show no records in the list) if the row does not validate the model constraints.
See: https://stackoverflow.com/a/163968/1104941
Does the following work for you?
admin.py:
class ServerAdmin(admin.ModelAdmin):
list_display = ('server_name','org__org_name')
admin.site.register(Server,ServerAdmin)
I had a similar problem and solved it like this (using your example):
class ServerAdmin(admin.ModelAdmin):
list_display = ('server_name', 'get_org')
def get_org(self, obj):
return obj.org.org_name
get_org.short_description = 'Org'
admin.site.register(Server,ServerAdmin)

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