ManyToMany field not saved when using Django admin

2019-04-21 06:09发布

问题:

I'm experiencing a weird problem which I hope someone in here may be able to shed some light on.

I'm overriding the save() method of a model to add some values to a ManyToMany-field after running super(). My problem is that when I'm saving in Django admin the values seems to get added to the relationship but is then empty again.

If however I do it from manage.py shell it works without problem.

I've put two print statements in there and they produce the exact same output regardless of if I'm running it via Django admin or via shell.

class Store(models.Model):
    holidays = models.ManyToManyField(StoreHoliday, blank=True)
    copy_holidays_from = models.ForeignKey('Store', blank=True, null=True)

    def save(self):
        print '====  BEFORE SAVE:', self.holidays.all()
        super(Store, self).save()
        self.copy_holidays()
        print '====  AFTER SAVE:', self.holidays.all()

    def copy_holidays(self):
        if self.pk and self.copy_holidays_from:
            self.holidays.clear()
            for h in self.copy_holidays_from.holidays.all():
                self.holidays.add( h )

This is the output of the print statements:

====  BEFORE SAVE: []
====  AFTER SAVE: [<StoreHoliday: 10 Mar 2010, Chuck Norris birthday (Closed)>]

Does anyone have any suggestions on what might be causing this?

Edit: All manual changes to the m2m relationship in save() seems to be discarded by Django when saving through the admin interface. Is this related to how it processes the form?

回答1:

So it turns out the above was not the correct way to implement it. The code belonged in StoreAdmin, by overriding model_save().

This is how I solved it:

class StoreAdmin(admin.ModelAdmin):
    def save_model(self, request, obj, form, change):
        if obj.copy_holidays_from:
            form.cleaned_data['holidays'] = obj.copy_holidays_from.holidays.all()

        super(StoreAdmin, self).save_model(request, obj, form, change)


回答2:

I probably ran into this same behaviour just today and yes, you are correct in assuming it's related to how django handles the data.

The django admin makes the changes to a ManyToMany field separately from changing the actual object. (Remember that the m2m is saved in a different database table).

In my case if I didn't select anything in the ManyToMany field in the admin site, this would translate into a clear()-operation on the ManyToMany relation. Everything you do in the save()-method is immediately removed by this clear. Same thing with stuff I did in the post_save signal handler.

The solution (for me) was to separate the ManyToMany-field into an inline so it doesn't automatically get saved as empty when modifying the object.



回答3:

In django 2,1,4 my solution was to use save_related()

def save_related(self, request, form, formsets, change):
    super().save_related(request, form, formsets, change)
    form.instance.permissions.add(request.user)


回答4:

For me the problem that the admin was only saving the last selected instance of the many fields (last 'holiday' selected). So I had to override the save_model method such as this:

@admin.register(Store)
class StoreAdmin(admin.ModelAdmin):

    def save_model(self, request, obj, form, change):
        form.cleaned_data['holidays'] = StoreHoliday.objects.filter(pk__in=dict(request.POST).get('holidays'))
        super(StoreAdmin, self).save_model(request, obj, form, change)

I spent a lot of time on it and other solutions were not working, so I hope it will help.



回答5:

One of the solutions to update m2m, along with updating one of your models.

Django 1.11 and higher

The behavior which you can observe during updating, when changes which you made with m2m records were not saved, even after you made them in a save method one of your models or in a signal, happens only because m2m form rewrites all records after the main object is updated.

This is why, step by step:

  1. The main object is updated.

  2. Your code(in a save method or in a signal) made changes (you can look at them, just put a breakpoint in ModelAdmin):

 def save_related(self, request, form, formsets, change):
     breakpoint()
     form.save_m2m()
     for formset in formsets:
         self.save_formset(request, form, formset, change=change)
  1. form.save_m2m() takes all m2m values which were placed on a page(roughly speaking) and replace all m2m records via a related manager. That's why you can't see your changes at the end of a transaction.

There is a solution: make your changes with m2m via transaction.on_commit. transaction.on_commit will make your changes after form.save_m2m() when the transaction is committed.

Unfortunately, the downside of this solution - your changes with m2m will be executed in a separate transaction.