How to display list_filter with count of related objects in django admin? - django-admin

How can I display the count of related objects after each filter in list_filter in django admin?
class Application(TimeStampModel):
name = models.CharField(verbose_name='CI Name', max_length=100, unique=True)
description = models.TextField(blank=True, help_text="Business application")
class Server(TimeStampModel):
name = models.CharField(max_length=100, verbose_name='Server Name', unique=True)
company = models.CharField(max_length=3, choices=constants.COMPANIES.items())
online = models.BooleanField(default=True, blank=True, verbose_name='OnLine')
application_members = models.ManyToManyField('Application',through='Rolemembership',
through_fields = ('server', 'application'),
)
class Rolemembership(TimeStampModel):
server = models.ForeignKey(Server, on_delete = models.CASCADE)
application = models.ForeignKey(Application, on_delete = models.CASCADE)
name = models.CharField(verbose_name='Server Role', max_length=50, choices=constants.SERVER_ROLE.items())
roleversion = models.CharField(max_length=100, verbose_name='Version', blank=True)
Admin.py
#admin.register(Server)
class ServerAdmin(admin.ModelAdmin):
save_on_top = True
list_per_page = 30
list_max_show_all = 500
inlines = [ServerInLine]
list_filter = (
'region',
'rolemembership__name',
'online',
'company',
'location',
'updated_on',
)
i.e After each filter in list filter, I want to show the count of related objects.
Now it only shows the list of filter
i.e location filter list
Toronto
NY
Chicago
I want the filter to show the count like below:
Toronto(5)
NY(3)
Chicago(2)
And if the filter has 0 related objects, don't display the filter.

This is possible with a custom list filter by combining two ideas.
One: the lookups method lets you control the value used in the query string and the text displayed as filter text.
Two: you can inspect the data set when you build the list of filters. The docs at https://docs.djangoproject.com/en/1.11/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter shows examples for a start decade list filter (always shows «80s» and «90s») and a dynamic filter (shows «80s» if there are matching records, same for «90s»).
Also as a convenience, the ModelAdmin object is passed to the lookups
method, for example if you want to base the lookups on the available
data
This is a filter I wrote to filter data by language:
class BaseLanguageFilter(admin.SimpleListFilter):
title = _('language')
parameter_name = 'lang'
def lookups(self, request, model_admin):
# Inspect the existing data to return e.g. ('fr', 'français (11)')
# Note: the values and count are computed from the full data set,
# ignoring currently applied filters.
qs = model_admin.get_queryset(request)
for lang, name in settings.LANGUAGES:
count = qs.filter(language=lang).count()
if count:
yield (lang, f'{name} ({count})')
def queryset(self, request, queryset):
# Apply the filter selected, if any
lang = self.value()
if lang:
return queryset.filter(language=lang)
You can start from that and adapt it for your cities by replacing the part with settings.LANGUAGES with a queryset aggregation + values_list that will return the distinct values and counts for cities.

Éric's code got me 80% of what I needed. To address the comment he left in his code (about ignoring currently applied filters), I ended up using the following for my use case:
from django.db.models import Count
class CountAnnotatedFeedFilter(admin.SimpleListFilter):
title = 'feed'
parameter_name = 'feed'
def lookups(self, request, model_admin):
qs = model_admin.get_queryset(request).filter(**request.GET.dict())
for pk, name, count in qs.values_list('feed__feed_id', 'feed__feed_name').annotate(total=Count('feed')).order_by('-total'):
if count:
yield pk, f'{name} ({count})'
def queryset(self, request, queryset):
feed_id = self.value()
if feed_id:
return queryset.filter(feed_id=feed_id)
And then, in the admin model:
class FeedEntryAdmin(admin.ModelAdmin):
list_filter = (CountAnnotatedFeedFilter,)
Note: As Éric also mentioned, this can impact the speed of the admin panel quite heavily, as it may have to perform expensive queries.

Related

Criteria in relation between entities - Grails

I have an app with the following entities:
Topic:
class Topic {
UUID id
String description
String name
boolean visibility = true
// Relation
static hasMany = [tests:Test]
...
}
Test:
class Test {
UUID id
boolean active = true
String description
...
static hasMany = [evaluationsTest: Evaluation]
static belongsTo = [topic: Topic, catalog: Catalog]
}
When I show all visible topics to the user I request the query:
def visibleTopics = Topic.findAllByVisibility(true, [sort:"name", order:"asc"])
This query returns me for example: [['English'], ['Spanish']]. Then, I can show the full information about each topic to the user.
But I also want to indicate to the user the number of active test in each visible topic.
For example:
English topic has 2 active test.
Spanish topic has a total of 2 test. One is active and the other is not.
German topic has not any active test.
Then I need a query whose result is: def activeTotalEachTopic = [[2],[1],[0]] and I can pass the activeTotalEachTopic variable to the view (.gsp).
Solution:
From the first query where I can obtain all visible topics, I get the number of active test.
def visibleTopics = Topic.findAllByVisibility(true, [sort:"name", order:"asc"])
def numberActiveTest = []
activeTopics.each { topic ->
def result = Test.findAllByTopicAndActive(topic, true).size()
numberActiveTest.push(result)
}
And I pass to the view both variables.
render view: 'home', model: [activeTopics: activeTopics, numberActiveTest: numberActiveTest]
What you are missing is grouping so that you get the count per group, rather than a total count.
You also need to change the join type from the default inner join to an outer join in order for topics without an active test to return 0. However, a side-effect of this is that it changes how association properties are referenced due to the alias that's created by the join. Something like this:
import static org.hibernate.sql.JoinType.*
def activeTotalEachTopic = Topic.createCriteria().list() {
createAlias('tests', 't', LEFT_OUTER_JOIN)
eq 'visibility', true
or {
eq 't.active', true
isNull 't.id'
}
projections {
groupProperty 'name'
count()
}
order ("name", "asc")
}
Now, another issue to address is that the output would be something like this due to the grouping: [['English', 2],['Spanish', 1],['German', 0]]. So what you can do is collect the second item in each sub-list:
activeTotalEachTopic*.getAt(1)
// Which is the equivalent of...
activeTotalEachTopic.collect { it[1] }

Django Admin shows "(nothing)"

I want to show in the app/model admin the columns of the model but with some customization. These 3 'tar', 'per_hor','per_tar' have choices in their modelfield.
First I wrote this code:
class ValoresAdmin(admin.ModelAdmin):
list_display = ('fecha', 'tar', 'per_hor','per_tar')
list_filter = ('fecha','tar','per_tar','per_hor')
date_hierarchy = 'fecha'
fieldsets = (
(None, {
'fields': ('fecha',('tar', 'per_hor', 'per_tar'))
}),
(None, {
'fields': ('feu', 'coef_perf','sah', 'pmh','carg_cap')
}),
)
I shows the verbose name of the column but the values is always "(nothing)" on the filter page but if I enter the change form they display correctly their value (their choide).
I read some and decided to create methods like these ones and call them in the list_display:
def get_tar(Self):
return self.get_tar_display()
def get_per_hor(Self):
return self.get_per_hor_display()
def get_per_tar(Self):
return self.get_per_tar_display()
get_tar_display.short_description = 'Tarifa'
get_per_hor_display.short_description = 'Periodo horario'
get_per_tar_display.short_description = 'Periodo tarifario'
Now the filter page will display columns named as the short description BUT with the real value of the field instead of theirs "choice value".
Addiotionally if I mark 'per_tar' as non editable it will show also "(nothing)" in the change from instead of it´s stored value.
What am I doing wrong?
Diving in internet I found the answer here:
http://thinkingnectar.com/2009/django-get_foo_display-behaviour-with-characters-and-integer-fields/
The post comment that when the field is char type, the choice key should be a string but when it´s an integer field it should be an interger!
Here was my problem! Changing strings to integer made it work.

Index out of bound when use data binding to update many-to-many relation

I have problem for updating domain that has many-to-many relation. For instance, consider these 2 simple domains.
class Student {
String name
static hasMany = [courses: Course]
}
class Course {
String name
static hasMany = [students: Student]
static belongsTo = [Student]
}
To update student's name along with his/her courses' name, I use data binding like this:
def params = [
'courses[0].id': c2.id,
'courses[0].name': 'c11',
'courses[1].id': c1.id,
'courses[1].name': 'c22'
]
s1.properties = params
s1.save(flush: true)
However, this will cause error:
org.springframework.beans.InvalidPropertyException: Invalid property 'courses[1]' of bean class [tb.Student]:
Invalid list index in property path 'courses[1]'; nested exception is java.lang.IndexOutOfBoundsException:
Index: 1, Size: 1
After some searching, I found that all answers suggest to use List for relation instead of Set. However, I still prefer to use Set.
Environment
Grails: 2.2.3
Java: 1.6.0_45
OS: Ubuntu 13.04
My solution is to clear the children list before data binding. This is the full code to test above domains. The line s1.courses.clear() will prevent above error.
def s1 = new Student(name: 's1').save(flush: true)
def s2 = new Student(name: 's2').save(flush: true)
def c1 = new Course(name: 'c1').save(flush: true)
def c2 = new Course(name: 'c2').save(flush: true)
s1.addToCourses(c1)
s1.addToCourses(c2)
s1.save(flush: true)
def params = [
'courses[0].id': c2.id,
'courses[0].name': 'c11',
'courses[1].id': c1.id,
'courses[1].name': 'c22'
]
s1.courses.clear()
s1.properties = params
s1.save(flush: true)
However, I still think this problem is a bug. And my solution is a work around.
A Set isn't ordered hence it will fail if you specify an index.
If you don't want to use List, try to use SortedSet instead.
You can find more info # (http://grails.org/doc/latest/guide/single.html#ormdsl) 6.5.3 Default Sort Order

Grails: Use results of Criteria + projections as a filter on the original table

I have the following table/model:
class Post {
int id;
String comment;
static belongsTo = [category_id:Category];
}
I wish to create a query that can filter out the last Post (highest id) per Category. I want the results in List<Post> form.
In other words (I believe) in SQL the query would look as follows:
SELECT *
FROM
Post AS source
JOIN (
SELECT MAX(id) AS id, category_id
FROM Post
GROUP BY category_id
) AS filter
ON source.id = filter.id;
If I understand correctly, the first step is to use a HibernateCriteriaBuilder:
def c = Post.createCriteria();
def results = c.list {
projections {
groupProperty("category_id", "myid")
max("id", "version")
}
}
So my question is a two part:
Am I on the right track?
How can I use the results object to obtain a List<Post> array?
(Something like: def latest = Post.FindAllByXXX(result); )
Yes, you are on the right track. I would also add the id property for the Post to my projections:
projections {
property('id')
}
and then collect all Posts using the id to get a list of posts, something like:
def latestPosts = results?.collect{Post.read(it[0])}

Binding multiple objects in Grails

I have there domain classes:
Person. (Person.ID, Name,Address)
Designation.(Designation.ID, Title, Band)
SalarySlip (Person.ID, Designation.ID, totalIncome, Tax etc etc.)
In the update method the person controller when I assign a person a designation from a list of designation values I want to insert a new record inside SalarySlip.
Something like:
def update = {
def SalarySlipInstance = new SalarySlip()
SalarySlipInstance.Person.ID = Params.ID //is this correct?
SalarySlipInstance.Designation.ID = ?? //since the value is coming from a list. How can I bind this field?
}
You need to load the Person and Designation objects first:
salarySlipInstance.Person = Person.get(params.person.id)
salarySlipInstance.Designation = Designation.get(params.designation.id)
If in your form you prefix the person and designation id's with person. and designation. it makes it easier to load.

Resources