Django model save method, updating m2m does not work

1.5k views Asked by At

I need to prevent user from self-killing from administrators list in the model:

class Organization(models.Model):       
    administrators = models.ManyToManyField(User, blank=True, null=True, help_text=_('Administrators are people that manage the organization'))

    def save(self, *args, **kwargs):
        # --- some specific code here ---
        super(Organization, self).save(*args, **kwargs)
        if self.user_id not in self.administrators.values_list('id', flat=True):
            self.administrators.add(self.user)
            # super(Organization, self).save(*args, **kwargs)
            self.save()                
            # assert False, self.administrators.all() # <- it works, if assert goes here

Ok, it might be some black magic casted here, let's try a post_save signal:

def organization_post_save(sender, instance, created, **kwargs):
    if instance.user_id not in instance.administrators.all().values_list('id', flat=True):
        instance.administrators.add(instance.user)
        instance.save()
        # assert False, '?'

It adds a user in the administrators list only if assertion occures. Ok, may be black magic occured again, let's try:

def organization_m2m_changed(sender, instance, action, reverse, model, pk_set, using, **kwargs):
    if instance.user_id not in instance.administrators.all().values_list('id', flat=True):
        instance.administrators.add(instance.user_id)

m2m_changed.connect(organization_m2m_changed, sender=Organization.administrators.through)

Of course, maximum recursion depth exceeded. What's wrong? This pain is unstoppable :(

UPD1

It seems, that post_save method called BEFORE m2m relations saved, so it ran into a race condition, and a new data was replaced with form data, that was empty. Here is a bad solution:

def organization_m2m_changed(sender, instance, action, reverse, model, pk_set, using, **kwargs):

    if not instance.administrators.filter(id=instance.user_id).exists():
        if action.startswith('post_'):
            instance.administrators.add(instance.user)

Now i'd like to know what signal Django emits after ALL work about a model done?

P.S. No magic. :(

UPD2 lie-ryan

forms.py

class OrganizationEditForm(forms.ModelForm):    
    class Meta:
        model = Organization
        exclude = ['user']
        widgets = floppyforms_widgets(Organization)

views.py

@login_required
def edit_organization(request, organization_id=None):
    user = request.user
    c = Context({'user': user})

    instance = get_object_or_404(Organization, id=organization_id) if organization_id else Organization(user=user)
    form = OrganizationEditForm(request.POST or None, request.FILES or None, instance=instance)

    if form.is_valid():
        form.save()
        messages.success(request, _('Organization saved successfully'))
        return HttpResponseRedirect(reverse('organizations'))

    c['form'] = form
    c['instance'] = instance

    return render_to_response('cat/edit_organization.html', c, context_instance=RequestContext(request))
1

There are 1 answers

7
Lie Ryan On

You're getting recursion error because you're calling self.save() inside of Organization.save(). Don't do this, if you want to save while overriding the save(), call the superclass' version of save() instead of your own save().

Before adding an instance to an m2m relationship, both sides of the relationship must already have a primary key (i.e. already inserted to the db). This means you can't add an unsaved, uncreated object to an m2m relationship.

Do this:

organisation.save()
user.save()
organisation.administrators.add(user)

I'd recommend doing this inside the View or Form since it is quite unexpected if a model's save method saved the user's model as well (though it should work inside the model's save() as well)