Google Groups

modelformset_factory and unique_together don't always validate unique fields


Jon Dufresne Jul 2, 2014 8:07 PM
Posted in group: Django developers (Contributions to Django itself)
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
---