Skip to content

Make MonitorField support M2M fields #647

@browser-bug

Description

@browser-bug

Setting the monitor value on the MonitorField pointing to a M2M field currently throws this exception:

"<Model: Model object (None)>" needs to have a value for field "id" before this many-to-many relationship can be used.

This is due to:

    def get_monitored_value(self, instance: models.Model) -> Any:
        # Trying to access the related model instance before the instance is actually saved
        return getattr(instance, self.monitor)

Moreover there isn't a way to get a real difference between the old and new value on a M2M field.

Since the only way I though of was to leverage the m2m_changed Django signal, I extended the field this way.

class M2MMonitorField(MonitorField):
    """Extend MonitorField to also support M2M fields.

    Always set the initial value with the default.
    When the `m2m_changed` signal is triggered update the monitor field.
    """

    def contribute_to_class(
        self,
        cls: type[models.Model],
        name: str,
        *args,
        **kwargs,
    ) -> None:
        super().contribute_to_class(cls, name, *args, **kwargs)
        models.signals.post_init.connect(self._setup_m2m_signal, sender=cls)

    def get_monitored_field(self, instance: models.Model):
        return instance._meta.get_field(self.monitor)  # noqa: SLF001

    def get_monitored_value(self, instance: models.Model):
        monitored_field = self.get_monitored_field(instance)
        if monitored_field.many_to_many:
            return self.default
        return getattr(instance, self.monitor)

    def _setup_m2m_signal(
        self,
        sender: type[models.Model],
        instance: models.Model,
        **kwargs,
    ) -> None:
        monitored_field = self.get_monitored_field(instance)
        if monitored_field.many_to_many:
            sender = monitored_field.remote_field.through
            models.signals.m2m_changed.connect(self._save_m2m_field, sender=sender)

    def _save_m2m_field(
        self,
        sender: type[models.Model],
        instance: models.Model,
        **kwargs,
    ) -> None:
        value = timezone.now()
        setattr(instance, self.attname, value)
        instance.save_base(update_fields=self.attname, raw=True)

This is working as expected but I'd like to hear if there are any possible flaws or room for improvements.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions