Adding aggregate over filtered self-join field to Admin list_display

北慕城南 提交于 2019-12-11 13:44:41

问题


I would like to augment one of my model admins with an interesting value. Given a model like this:

class Participant(models.Model):
    pass

class Registration(models.Model):
    participant = models.ForeignKey(Participant)
    is_going = models.BooleanField(verbose_name='Is going')

Now, I would like to show the number of other Registrations for this Participant where is_going is False. So, something akin to this SQL query:

SELECT reg.*, COUNT(past.id) AS not_going_num
FROM   registrations AS reg, registrations AS past
WHERE  past.participant_id = reg.participant_id AND
       past.is_going = False

I think I can extend the Admin's queryset() method according to Django Admin, Show Aggregate Values From Related Model, by annotating it with the extra Count, but I still cannot figure out how to work the self-join and filter into this.

I looked at Self join with django ORM and Django self join , How to convert this query to ORM query, but the former is doing SELECT * AND the latter seems to have data model problems.

Any suggestions on how to solve this?


回答1:


See edit history for previous version of the answer.

The admin implementation below will display "Not Going Count" for each Registration model. The "Not Going Count" is the count of is_going=False for the registration's participant.

@admin.register(Registration)
class RegistrationAdmin(admin.ModelAdmin):

    list_display = ['id', 'participant', 'is_going', 'ng_count']

    def ng_count(self, obj):
        return obj.not_going_count
    ng_count.short_description = 'Not Going Count'

    def get_queryset(self, request):
        qs = super(RegistrationAdmin, self).get_queryset(request)
        qs = qs.filter(participant__registration__isnull=False)
        qs = qs.annotate(not_going_count=Sum(
            Case(
                When(participant__registration__is_going=False, then=1),
                default=0,
                output_field=models.IntegerField())
            ))
        return qs

Below is a more thorough explanation of the QuerySet:

qs = qs.filter(participant__registration__isnull=False)

The filter causes Django to perform two joins - an INNER JOIN to participant table, and a LEFT OUTER JOIN to registration table.

qs = qs.annotate(not_going_count=Sum(
    Case(
        When(participant__registration__is_going=False, then=1),
        default=0,
        output_field=models.IntegerField())
    )    
))

This is a standard aggregate, which will be used to SUM up the count of is_going=False. This translates into the SQL

SUM(CASE WHEN past."is_going" = False THEN 1 ELSE 0 END)

The sum is generated for each registration model, and the sum belongs to the registration's participant.




回答2:


I might misunderstood, but you can do for single participant:

participant = Participant.objects.get(id=1)
not_going_count = Registration.objects.filter(participant=participant,
                                              is_going=False).count()

For all participants,

from django.db.models import Count
Registration.objects.filter(is_going=False).values('participant') \
                                           .annotate(not_going_num=Count('participant'))

Django doc about aggregating for each item in a queryset.



来源:https://stackoverflow.com/questions/35325643/adding-aggregate-over-filtered-self-join-field-to-admin-list-display

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!