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. 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:
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) 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.