Brainstorm's snippets (1/234)

  Many-to-many example using "through" to augment M2M relationships

On one side of the relation, we do the following:

file models.py

class Device(models.Model):

    ...
    users = models.ManyToManyField(
        User,
        through='DeviceUsers',
        related_name='devices'
    )


class DeviceUsers(models.Model):

    class Meta:
        verbose_name = _("Device/User")
        verbose_name_plural = _("Device/Users")

    device = models.ForeignKey(Device, verbose_name=_('Device'), on_delete=models.CASCADE)
    user = models.ForeignKey(User, verbose_name=_('User'), on_delete=models.CASCADE)
    enabled = models.BooleanField(_('Enabled'), null=False, default=True, blank=True)
    expiration_data = models.DateField(_('Expiration date'), null=True, blank=True)

    def __str__(self):
        return ''

while nothing is required in the related Model.

In the admin, we can:

  • add inlines to edit the relation (in both ModelAdmins),
  • (optionally) list the related models in the listing
  • note that we also add a search_fields attribute to have autocomplete_fields working (Django 2.x)

file admin.py

class UserForDeviceTabularInline(admin.TabularInline):
    model = Device.users.through
    extra = 0
    autocomplete_fields = ['user', ]


@admin.register(Device)
class DeviceAdmin(BaseModelAdmin):

    list_display = [..., 'list_users', ]
    search_fields = ['=id', 'description', 'code', ]
    inlines = [UserForDeviceTabularInline, ]

    def list_users(self, obj):
        return mark_safe(', '.join([
            '<a href="%s">%s</a>' % (user.get_admin_url(), str(user))
            for user in obj.users.all()  # .order_by('panelregisters__position')
        ]))
    list_users.short_description = _('users')


class DeviceTabularInline(admin.TabularInline):
    from backend.models import Device
    model = Device.users.through
    #exclude = ['position', ]
    extra = 0
    autocomplete_fields = ['device', ]


@admin.register(User)
class UserAdmin(AuthUserAdmin, HijackUserAdminMixin):

    list_display = [..., 'list_devices', ]
    inlines = [DeviceTabularInline, ]

    def list_devices(self, obj):
        return mark_safe(', '.join([
            '<a href="%s">%s</a>' % (device.get_admin_url(), str(device))
            for device in obj.devices.all()  # .order_by('panelregisters__position')
        ]))
    list_devices.short_description = _('devices')

TODO: add select_related() as required