Django admin: using inlines through a non-adjacent ManyToMany relationship

260 views Asked by At

Consider the following models.py, where a Group contains multiple Persons who each have zero or more Phone numbers. In this particular case, Persons who share a Group will often share at least one Phone number, so a many-to-many relationship is used.

class Group(models.Model):
    name = models.CharField(max_length=30)

class Person(models.Model):
    group = models.ForeignKey(Group)
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

class Phone(models.Model):
    persons = models.ManyToManyField(Person)
    number = models.CharField(max_length=30)

I would like to show these models in the Django admin, in a single view, as shown below.

class PersonInline(admin.StackedInline):
    model = Person

class PhoneInline(admin.StackedInline):
    model = Phone # also tried: Phone.persons.through

@admin.register(Group)
class GroupAdmin(admin.ModelAdmin):
    inlines = [PersonInline, PhoneInline]

However, there is no foreign key between Group and Phone, so this raises a SystemCheckError (one of the following):
<class 'myapp.admin.PhoneInline'>: (admin.E202) 'myapp.Phone' has no ForeignKey to 'myapp.Group'.
<class 'myapp.admin.PhoneInline'>: (admin.E202) 'myapp.Phone_persons' has no ForeignKey to 'myapp.Group'.

Is it possible to make this work through the Person model? The goal is for the Phone inline to show phone number records for all Persons in the Group (bonus: when adding a new Phone, the Person SelectMultiple widget will need to only show other Persons in the Group). I would prefer to avoid modifying any templates. A third-party app could be integrated if necessary. I can use Django 1.10 or 1.11.

Thanks!

1

There are 1 answers

0
Owen T. Heisler On BEST ANSWER

I solved this by slightly modifying my requirements. Rather than requiring a relationship between Phone and Person only, I added another many-to-one relationship: between Phone and Group. For my particular case, it actually works out better this way; and a Group should cascade to both related Persons and related Phones on delete.

There is no change to the admin.py shown in the question. The models.py has one more line in the Phone class, a ForeignKey field:

class Phone(models.Model):
    group = models.ForeignKey(Group)
    persons = models.ManyToManyField(Person)
    number = models.CharField(max_length=30)

The "bonus" above requires that the ManyToManyField form widget in the Person inline show only those Persons that are in the same Group. For this, two functions can be added to admin.py:

class PersonInline(admin.StackedInline):
    model = Person

class PhoneInline(admin.StackedInline):
    model = Phone

    def formfield_for_manytomany(self, db_field, request=None, **kwargs):
        field = super(PhoneInline,
                      self).formfield_for_manytomany(db_field, request,
                                                     **kwargs)
        if db_field.name == 'persons':
            if request._obj_ is None:
                field.queryset = field.queryset.none()
            else:
                qs = Person.objects.filter(group=request._obj_.id)
                field.queryset = qs
                field.initial = qs
        return field

@admin.register(Group)
class GroupAdmin(admin.ModelAdmin):
    inlines = [PersonInline, PhoneInline]

    def get_form(self, request, obj=None, **kwargs):
        # Save obj reference for future processing in Phone inline
        request._obj_ = obj
        return super(GroupAdmin, self).get_form(request, obj, **kwargs)