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:
parent
9cebb526f2
commit
38ba3775ec
|
@ -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():
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>"<script>" 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>"<script>" 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()
|
||||||
|
|
Loading…
Reference in New Issue