I'm reporting this to the developers list as I feel this shows a
shortfall in (to me) expected behavior. I'm not sure this is exactly a
bug or simply a use case the unique validation wasn't designed for.
Basically, sometimes I want to create model formsets that use a
limited number of model fields. These model fields may have a unique
constraint using unique_together. The other field (or fields) of
unique together may not be included in the formset. Upon validating
the form, unique_together fields are only checked if all fields are
contained in the form. This means, my unique fields will not be
validated automatically. To achieve the validation I usually have to
copy parts of Django form validation, missing out on DRY.
I think one solution would be for model formsets to have a "static" or
"constant" fields argument. This could be a single value per field
that all forms in the formset would share for a particular set of
fields. These fields would not be a part of the rendered form, but
could be used for validating the forms and creating new models
instances. Thoughts?
An example where I might do this: suppose I have a "container" model.
This model has many "item" models created through a FK relationship.
Perhaps a field is unique together with the container. This example
could apply to any container/item relationship. I might create a
formset for all items of just one container for a bulk (unique)
rename. I have created a simple small example that illustrates my
point:
models.py
----
class Container(models.Model):
pass
class Item(models.Model):
container = models.ForeignKey(Container)
name = models.CharField(max_length=100)
class Meta:
unique_together = ('container', 'name')
ItemFormSet = modelformset_factory(model=Item, fields=['name'])
----
tests.py
----
class ItemFormSetTestCase(TestCase):
def test_unique_item_name(self):
container = Container()
container.save()
item1 = Item(container=container, name='item1')
item1.save()
item2 = Item(container=container, name='item2')
item2.save()
data = {
'form-TOTAL_FORMS': 2,
'form-INITIAL_FORMS': 2,
'form-MAX_NUM_FORMS': 2,
'form-0-id': str(
item1.pk),
'form-0-name': 'newname',
'form-1-id': str(
item2.pk),
'form-1-name': 'newname',
}
formset = ItemFormSet(
data,
queryset=Item.objects.filter(container=container))
self.assertFalse(formset.is_valid())
---
This test fails because the uniqueness of name is not actually
validated. If I were to go ahead an save this "valid" form, I receive
the following error:
---
Traceback (most recent call last):
File "/home/jon/djtest/djtest/myapp/tests.py", line 27, in
test_unique_item_name
formset.save()
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/forms/models.py",
line 636, in save
return self.save_existing_objects(commit) + self.save_new_objects(commit)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/forms/models.py",
line 753, in save_existing_objects
saved_instances.append(self.save_existing(form, obj, commit=commit))
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/forms/models.py",
line 623, in save_existing
return form.save(commit=commit)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/forms/models.py",
line 457, in save
construct=False)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/forms/models.py",
line 103, in save_instance
instance.save()
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/models/base.py",
line 590, in save
force_update=force_update, update_fields=update_fields)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/models/base.py",
line 618, in save_base
updated = self._save_table(raw, cls, force_insert, force_update,
using, update_fields)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/models/base.py",
line 680, in _save_table
forced_update)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/models/base.py",
line 724, in _do_update
return filtered._update(values) > 0
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/models/query.py",
line 598, in _update
return query.get_compiler(self.db).execute_sql(CURSOR)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py",
line 1003, in execute_sql
cursor = super(SQLUpdateCompiler, self).execute_sql(result_type)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/models/sql/compiler.py",
line 785, in execute_sql
cursor.execute(sql, params)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/backends/utils.py",
line 65, in execute
return self.cursor.execute(sql, params)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/utils.py",
line 94, in __exit__
six.reraise(dj_exc_type, dj_exc_value, traceback)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/backends/utils.py",
line 65, in execute
return self.cursor.execute(sql, params)
File "/home/jon/djtest/venv/lib/python2.7/site-packages/django/db/backends/sqlite3/base.py",
line 485, in execute
return Database.Cursor.execute(self, query, params)
IntegrityError: UNIQUE constraint failed: myapp_item.container_id,
myapp_item.name
---