diff --git a/django/forms/models.py b/django/forms/models.py index 561ee7a44b8..65434a6f6ea 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -85,6 +85,8 @@ def save_instance(form, instance, fields=None, fail_message='saved', for f in opts.many_to_many: if fields and f.name not in fields: continue + if exclude and f.name in exclude: + continue if f.name in cleaned_data: f.save_form_data(instance, cleaned_data[f.name]) if commit: @@ -405,7 +407,8 @@ class BaseModelForm(BaseForm): else: fail_message = 'changed' return save_instance(self, self.instance, self._meta.fields, - fail_message, commit, construct=False) + fail_message, commit, self._meta.exclude, + construct=False) save.alters_data = True diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py index 847cd6ebdb5..2616ddaf7d0 100644 --- a/tests/forms_tests/tests/tests.py +++ b/tests/forms_tests/tests/tests.py @@ -5,7 +5,7 @@ import datetime from django.core.files.uploadedfile import SimpleUploadedFile from django.db import models -from django.forms import Form, ModelForm, FileField, ModelChoiceField +from django.forms import Form, ModelForm, FileField, ModelChoiceField, CharField from django.forms.models import ModelFormMetaclass from django.test import TestCase from django.utils import six @@ -26,6 +26,14 @@ class OptionalMultiChoiceModelForm(ModelForm): fields = '__all__' +class ChoiceFieldExclusionForm(ModelForm): + multi_choice = CharField(max_length=50) + + class Meta: + exclude = ['multi_choice'] + model = ChoiceFieldModel + + class FileForm(Form): file1 = FileField() @@ -221,3 +229,31 @@ class RelatedModelFormTests(TestCase): model=A self.assertTrue(issubclass(ModelFormMetaclass(str('Form'), (ModelForm,), {'Meta': Meta}), ModelForm)) + + +class ManyToManyExclusionTestCase(TestCase): + def test_m2m_field_exclusion(self): + # Issue 12337. save_instance should honor the passed-in exclude keyword. + opt1 = ChoiceOptionModel.objects.create(id=1, name='default') + opt2 = ChoiceOptionModel.objects.create(id=2, name='option 2') + opt3 = ChoiceOptionModel.objects.create(id=3, name='option 3') + initial = { + 'choice': opt1, + 'choice_int': opt1, + } + data = { + 'choice': opt2.pk, + 'choice_int': opt2.pk, + 'multi_choice': 'string data!', + 'multi_choice_int': [opt1.pk], + } + instance = ChoiceFieldModel.objects.create(**initial) + instance.multi_choice = instance.multi_choice_int = [opt2, opt3] + form = ChoiceFieldExclusionForm(data=data, instance=instance) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['multi_choice'], data['multi_choice']) + form.save() + self.assertEqual(form.instance.choice.pk, data['choice']) + self.assertEqual(form.instance.choice_int.pk, data['choice_int']) + self.assertEqual(list(form.instance.multi_choice.all()), [opt2, opt3]) + self.assertEqual([obj.pk for obj in form.instance.multi_choice_int.all()], data['multi_choice_int'])