Counter not updating accurately, and then not at all

I’ve tried to accomplish what I want both by overriding the save method in my model, and by trying various iterations of signals, and nothing seems to work.

I have a Member model with a many2many relationship to a Link model. I add Links to a Member record using a TabularInline in the admin form for a Member.

This is an abbreviated view of my code:

# models.py

from django.db.models.signals import m2m_changed
from django.dispatch import receiver

class Link(models.Model):
    link = models.URLField(max_length=200, unique=True)
    ...

class Member(models.Model):
    links = models.ManyToManyField('Link', blank=True)
    link_count = models.SmallIntegerField(default=0)
    ...

@receiver(m2m_changed, sender=Member.links.through)
def update_counter(sender, instance, action, reverse, model, pk_set, **kwargs):
    if action == 'post_add':
        instance.link_count = instance.links.count()
        instance.save()


# admin.py

admin.site.register(Link)

class LinkInline(admin.TabularInline):
    model = Member.links.through
    ...

@admin.register(Member)
class MemberAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
    exclude = ['links', ]
    inlines = [LinkInline,]
    ...

After I defined my most recent iteration of a m2m_changed signal, then added another Link to a sample Member record, the link_count in the relevant Member record was updated one time, yet it was erroneously updated to a count that would’ve only been correct prior to the record edit that added additional Links. Moreover, each subsequent edit of that same Member record is now leaving the link_count unchanged from the first erroneous number.

I don’t know if there is a proper way to achieve what I want to do, which is to update the link_count field on an affected Member record after any update to that Member record, using an accurate count of all related Link records. Maybe the correct solution lies beyond signals, or overriding the save method (which itself also yielded an erroneous, pre-edit link_count).

Or are many-to-many relationships, specifically, something that is mostly impossible to base any sort of auto-updating scheme upon?

I do get an accurate count using a method in the admin, and I can sort on this calculated column in the admin, but I cannot do a search along the lines of link_count > 3 in the admin for example, so if I want to do such a search, it seems I do need to have a persisted field in the Member model. I just seem to have no good way of updating that persisted field automatically.

One thing to keep in mind here is that changing membership of a “many-to-many” relationship does not affect either model.

In other words, doing an add() on Member.links does not result in a change to either the Link or the Member model. The only change is to the “through” table that exists to join those two models.

<opinion>
I would definitely look for an alternative to tracking a value. My first inclination would be to define a property on the model. (See the section on list_display for doing this.)
There’s certainly no way that I would get signals involved with something like this.
</opinion>

If this: djangoql · PyPI is the package you are using, it looks like it can handle it, or be made to work with it.

1 Like

Thank you, I will abandon the idea of a persisted link_count field in the database.

I tried a property in the model, but anytime I sort on that column in the admin change list, a Member record having a relation to six Link records went from being one row in the admin UI to being six rows for some reason.

In addition, I don’t know how I missed this before but you are right it does seem that with some extra tweaking DjangoQL can at least access annotations, and I was already using some annotations in the ModelAdmin’s get_queryset. On the other hand, I couldn’t get DjangoQL to see the property in the model.

Now that I’ve realize DjangoQL will work with annotations I’ll just stick with that I suppose.

# admin.py
...
from djangoql.admin import DjangoQLSearchMixin
from djangoql.schema import DjangoQLSchema, IntField
...

class MemberQLSchema(DjangoQLSchema):
    def get_fields(self, model):
        fields = super(MemberQLSchema, self).get_fields(model)
        if model == Member:
            fields += [
                IntField(name='link_count'),  # from annotation
            ]
        return fields

@admin.register(Member)
class MemberAdmin(DjangoQLSearchMixin, admin.ModelAdmin):
    djangoql_schema = MemberQLSchema
    list_display = [
        'ct_linx',
        ...
    ]
    ...

    def get_queryset(self, request):
        qs = super().get_queryset(request)
        return qs.annotate(
            link_count=Count('links'),
            ...
        )

    @admin.display(description='lc')
    def ct_linx(self, inst):
        return inst.link_count
    ct_linx.admin_order_field = 'link_count'

    ...