Fixed #14234 -- Re-validating a model instance added via ModelForm no longer throws spurious PK uniqueness errors. Thanks to David Reynolds and Jeremy Dunck.

Also moved Model._adding to Model._state.adding to reduce instance namespace footprint.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14612 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Carl Meyer 2010-11-18 22:43:46 +00:00
parent 9cebb526f2
commit 38ba3775ec
4 changed files with 46 additions and 5 deletions

View File

@ -262,6 +262,10 @@ class ModelState(object):
""" """
def __init__(self, db=None): def __init__(self, db=None):
self.db = db self.db = db
# If true, uniqueness validation checks will consider this a new, as-yet-unsaved object.
# Necessary for correct validation of new instances of objects with explicit (non-auto) PKs.
# This impacts validation only; it has no effect on the actual save.
self.adding = False
class Model(object): class Model(object):
__metaclass__ = ModelBase __metaclass__ = ModelBase
@ -555,12 +559,15 @@ class Model(object):
# Store the database on which the object was saved # Store the database on which the object was saved
self._state.db = using self._state.db = using
# Once saved, this is no longer a to-be-added instance.
self._state.adding = False
# Signal that the save is complete # Signal that the save is complete
if origin and not meta.auto_created: if origin and not meta.auto_created:
signals.post_save.send(sender=origin, instance=self, signals.post_save.send(sender=origin, instance=self,
created=(not record_exists), raw=raw, using=using) created=(not record_exists), raw=raw, using=using)
save_base.alters_data = True save_base.alters_data = True
def delete(self, using=None): def delete(self, using=None):
@ -701,7 +708,7 @@ class Model(object):
if lookup_value is None: if lookup_value is None:
# no value, skip the lookup # no value, skip the lookup
continue continue
if f.primary_key and not getattr(self, '_adding', False): if f.primary_key and not self._state.adding:
# no need to check for unique primary key when editing # no need to check for unique primary key when editing
continue continue
lookup_kwargs[str(field_name)] = lookup_value lookup_kwargs[str(field_name)] = lookup_value
@ -714,7 +721,7 @@ class Model(object):
# Exclude the current object from the query if we are editing an # Exclude the current object from the query if we are editing an
# instance (as opposed to creating a new one) # instance (as opposed to creating a new one)
if not getattr(self, '_adding', False) and self.pk is not None: if not self._state.adding and self.pk is not None:
qs = qs.exclude(pk=self.pk) qs = qs.exclude(pk=self.pk)
if qs.exists(): if qs.exists():
@ -744,7 +751,7 @@ class Model(object):
qs = model_class._default_manager.filter(**lookup_kwargs) qs = model_class._default_manager.filter(**lookup_kwargs)
# Exclude the current object from the query if we are editing an # Exclude the current object from the query if we are editing an
# instance (as opposed to creating a new one) # instance (as opposed to creating a new one)
if not getattr(self, '_adding', False) and self.pk is not None: if not self._state.adding and self.pk is not None:
qs = qs.exclude(pk=self.pk) qs = qs.exclude(pk=self.pk)
if qs.exists(): if qs.exists():

View File

@ -254,10 +254,10 @@ class BaseModelForm(BaseForm):
# if we didn't get an instance, instantiate a new one # if we didn't get an instance, instantiate a new one
self.instance = opts.model() self.instance = opts.model()
object_data = {} object_data = {}
self.instance._adding = True self.instance._state.adding = True
else: else:
self.instance = instance self.instance = instance
self.instance._adding = False self.instance._state.adding = False
object_data = model_to_dict(instance, opts.fields, opts.exclude) object_data = model_to_dict(instance, opts.fields, opts.exclude)
# if initial was provided, it should override the values from instance # if initial was provided, it should override the values from instance
if initial is not None: if initial is not None:

View File

@ -5,28 +5,34 @@ import tempfile
from django.db import models from django.db import models
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
temp_storage_location = tempfile.mkdtemp() temp_storage_location = tempfile.mkdtemp()
temp_storage = FileSystemStorage(location=temp_storage_location) temp_storage = FileSystemStorage(location=temp_storage_location)
class BoundaryModel(models.Model): class BoundaryModel(models.Model):
positive_integer = models.PositiveIntegerField(null=True, blank=True) positive_integer = models.PositiveIntegerField(null=True, blank=True)
callable_default_value = 0 callable_default_value = 0
def callable_default(): def callable_default():
global callable_default_value global callable_default_value
callable_default_value = callable_default_value + 1 callable_default_value = callable_default_value + 1
return callable_default_value return callable_default_value
class Defaults(models.Model): class Defaults(models.Model):
name = models.CharField(max_length=255, default='class default value') name = models.CharField(max_length=255, default='class default value')
def_date = models.DateField(default = datetime.date(1980, 1, 1)) def_date = models.DateField(default = datetime.date(1980, 1, 1))
value = models.IntegerField(default=42) value = models.IntegerField(default=42)
callable_default = models.IntegerField(default=callable_default) callable_default = models.IntegerField(default=callable_default)
class ChoiceModel(models.Model): class ChoiceModel(models.Model):
"""For ModelChoiceField and ModelMultipleChoiceField tests.""" """For ModelChoiceField and ModelMultipleChoiceField tests."""
name = models.CharField(max_length=10) name = models.CharField(max_length=10)
class ChoiceOptionModel(models.Model): class ChoiceOptionModel(models.Model):
"""Destination for ChoiceFieldModel's ForeignKey. """Destination for ChoiceFieldModel's ForeignKey.
Can't reuse ChoiceModel because error_message tests require that it have no instances.""" Can't reuse ChoiceModel because error_message tests require that it have no instances."""
@ -38,6 +44,7 @@ class ChoiceOptionModel(models.Model):
def __unicode__(self): def __unicode__(self):
return u'ChoiceOption %d' % self.pk return u'ChoiceOption %d' % self.pk
class ChoiceFieldModel(models.Model): class ChoiceFieldModel(models.Model):
"""Model with ForeignKey to another model, for testing ModelForm """Model with ForeignKey to another model, for testing ModelForm
generation with ModelChoiceField.""" generation with ModelChoiceField."""
@ -51,11 +58,17 @@ class ChoiceFieldModel(models.Model):
multi_choice_int = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice_int', multi_choice_int = models.ManyToManyField(ChoiceOptionModel, blank=False, related_name='multi_choice_int',
default=lambda: [1]) default=lambda: [1])
class FileModel(models.Model): class FileModel(models.Model):
file = models.FileField(storage=temp_storage, upload_to='tests') file = models.FileField(storage=temp_storage, upload_to='tests')
class Group(models.Model): class Group(models.Model):
name = models.CharField(max_length=10) name = models.CharField(max_length=10)
def __unicode__(self): def __unicode__(self):
return u'%s' % self.name return u'%s' % self.name
class Cheese(models.Model):
name = models.CharField(max_length=100)

View File

@ -3,6 +3,9 @@ from django.forms import *
from django.utils.unittest import TestCase from django.utils.unittest import TestCase
from django.utils.translation import ugettext_lazy, activate, deactivate from django.utils.translation import ugettext_lazy, activate, deactivate
from regressiontests.forms.models import Cheese
class FormsRegressionsTestCase(TestCase): class FormsRegressionsTestCase(TestCase):
def test_class(self): def test_class(self):
# Tests to prevent against recurrences of earlier bugs. # Tests to prevent against recurrences of earlier bugs.
@ -120,3 +123,21 @@ class FormsRegressionsTestCase(TestCase):
f = SomeForm({'field': ['<script>']}) f = SomeForm({'field': ['<script>']})
self.assertEqual(t.render(Context({'form': f})), u'<ul class="errorlist"><li>field<ul class="errorlist"><li>&quot;&lt;script&gt;&quot; is not a valid value for a primary key.</li></ul></li></ul>') self.assertEqual(t.render(Context({'form': f})), u'<ul class="errorlist"><li>field<ul class="errorlist"><li>&quot;&lt;script&gt;&quot; is not a valid value for a primary key.</li></ul></li></ul>')
def test_regression_14234(self):
"""
Re-cleaning an instance that was added via a ModelForm should not raise
a pk uniqueness error.
"""
class CheeseForm(ModelForm):
class Meta:
model = Cheese
form = CheeseForm({
'name': 'Brie',
})
if form.is_valid():
obj = form.save()
obj.name = 'Camembert'
obj.full_clean()