﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
33369	Add through_defaults as argument for m2m_changed signal	ShmuelTreiger	nobody	"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 receive:

{{{
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()
}}}"	New feature	closed	Database layer (models, ORM)	dev	Normal	wontfix	m2m_changed		Unreviewed	0	0	0	0	0	0
