Use case:
In English:
I have many policies, each of which has a status from a table of statuses
and is not unique to that policy. I associate these policies to my many
domains through the many_to_many table. Domains can have many policies, but
only one of each type of policy. Likewise, the same policy can apply to
multiple domains. (In practice, there are a few thousand domains and a
dozen or so policies.) In the following code, I have successfully
implemented this relationship at the database level.
In pseudocode:
Domain(models.Model) <fields> PolicyStatus(models.Model): status =
models.CharField(<>) Policy(models.Model) status =
models.ForeignKey(PolicyStatus, <>) domains =
models.ManyToManyField(Domain, through="Domain_Policy_m2m", <>) <fields>
Domain_Policy_m2m(models.Model): class Meta: constraints = [
models.UniqueConstraint( fields=["domain", "status", ],
name="unique_constraint" ) ] domain = models.ForeignKey(Domain, <>) policy
= models.ForeignKey(Policy, <>) status = models.ForeignKey(PolicyStatus,
<>)
Scenario:
I go to associate a new policy with a domain:
domain.policy_set.add(policy, through_defaults={"status": policy.status})
However, the domain already has a policy with this status and raises
IntegrityError <https://code.djangoproject.com/wiki/IntegrityError>.
Instead, I would like to replace the existing policy with the new one. I
can do this in my code:
new_policy = Policy(<>) existing_policy =
domain.policy_set.filter(status=new_policy.status).first() if
existing_policy: domain.policy_set.remove(existing_policy)
domain.policy_set.add(policy, through_defaults={"status": policy.status})
Instead, I wanted to take care of this in a more permanent way, so I
created a m2m_change signal receiver, per the documentation
<https://docs.djangoproject.com/en/4.0/ref/models/relations/#django.db.models.fields.related.RelatedManager.add>
:
from <> import Policy @receiver(m2m_changed, sender=Domain_Policy_m2m) def
delete_old_m2m(action, instance, pk_set, **kwargs): if action == "pre_add":
pk = min(pk_set) policy = Policy.objects.get(pk=pk) status = policy.status
existing_policy = Domain_Policy_m2m.objects.filter(status=status,
domain=instance) if existing_policy.exists(): existing_policy.delete()
This works, but it's not pretty. It would be excellent if I could have the
through_defaults as an argument, so I could do something like:
@receiver(m2m_changed, sender=Domain_Policy_m2m) def delete_old_m2m(action,
instance, through_defaults, **kwargs): if action == "pre_add": status =
through_defaults["status"] existing_policy =
Domain_Policy_m2m.objects.filter(status=status, domain=instance) if
existing_policy.exists(): existing_policy.delete()
Even the object(s) being added would be so much more convenient than the pk:
@receiver(m2m_changed, sender=Domain_Policy_m2m) def delete_old_m2m(action,
instance, model_set, **kwargs): if action == "pre_add": model =
min(model_set) existing_policy =
Domain_Policy_m2m.objects.filter(status=model.status, domain=instance) if
existing_policy.exists(): existing_policy.delete()
I could alternatively override Domain_Policy_m2m.save(<>) and use
Domain_Policy_m2m.objects.create(<>) but that's just seems less easily
maintainable for future developers.
I'll also note that the Django documentation (linked above and here
<https://docs.djangoproject.com/en/4.0/ref/models/relations/#django.db.models.fields.related.RelatedManager.add>)
indicates that this would be incorrect implementation:
Using add() with a many-to-many relationship, however, will not call any
save() methods (the bulk argument doesn’t exist), but rather create the
relationships using QuerySet.bulk_create(). If you need to execute some
custom logic when a relationship is created, listen to the m2m_changed
signal, which will trigger pre_add and post_add actions.
--
This email may contain confidential material; unintended recipients must
not disseminate, use, or act upon any information in it. If you received
this email in error, please contact the sender and permanently delete the
email.
--
You received this message because you are subscribed to the Google Groups
"Django developers (Contributions to Django itself)" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-developers/812b7b81-14f8-4840-8756-e58c972d769fn%40googlegroups.com.