﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
23964	Support for Meta.constraints validation across forms in a model formset.	Jon Dufresne	nobody	"Due to MySQL collation rules, two strings differing only by case are considered equal (`""foo""` is equal to `""FOO""`). If a database field is unique, upon inserting two rows where the unique field differs only by case, there will be a unique constraint violation.

If a `modelformset_factory()` is used to create a form set for a model with a unique field, the user can enter two strings differing only by case. The `BaseModelFormSet.validate_unique()` does not consider MySQL's collation rules and considers these two strings different. Upon insertion in the database, there is a constraint violation. The following ''new'' unit test demonstrates how this could happen. Code: <https://github.com/jdufresne/django/tree/mysql-formset-duplicates>.

Not sure the correct approach to fix this as other database backends will happily accept strings differing only by case. So the formset needs to have some sense of the database backend or whether or not unique fields should be case sensitive.

{{{
diff --git a/tests/forms_tests/models.py b/tests/forms_tests/models.py
index 82fa005..45d9680 100644
--- a/tests/forms_tests/models.py
+++ b/tests/forms_tests/models.py
@@ -110,3 +110,7 @@ class Cheese(models.Model):
 
 class Article(models.Model):
     content = models.TextField()
+
+
+class Tag(models.Model):
+    name = models.CharField(max_length=100, unique=True)
diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py
index 94e2704..2e77afd 100644
--- a/tests/forms_tests/tests/test_formsets.py
+++ b/tests/forms_tests/tests/test_formsets.py
@@ -6,8 +6,10 @@ import datetime
 from django.forms import (CharField, DateField, FileField, Form, IntegerField,
     SplitDateTimeField, ValidationError, formsets)
 from django.forms.formsets import BaseFormSet, formset_factory
+from django.forms.models import modelformset_factory
 from django.forms.utils import ErrorList
 from django.test import TestCase
+from ..models import Tag
 
 
 class Choice(Form):
@@ -1213,3 +1215,22 @@ class TestEmptyFormSet(TestCase):
         class FileForm(Form):
             file = FileField()
         self.assertTrue(formset_factory(FileForm, extra=0)().is_multipart())
+
+
+TagFormSet = modelformset_factory(Tag, fields=['name'])
+
+
+class ModelFormSetTestCase(TestCase):
+    def test_duplicates_mixed_case(self):
+        formset = TagFormSet({
+            'form-TOTAL_FORMS': 2,
+            'form-INITIAL_FORMS': 0,
+            'form-0-name': 'TAG',
+            'form-1-name': 'tag',
+        })
+        self.assertTrue(formset.is_valid())
+        formset.save()
+        self.assertQuerysetEqual(
+            Tag.objects.all(),
+            ['TAG'],
+            lambda o: o.name)
}}}

When running this test with a MySQL database the test fails with:

{{{
======================================================================
ERROR: test_duplicates_mixed_case (forms_tests.tests.test_formsets.ModelFormSetTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File ""/home/jon/devel/django/tests/forms_tests/tests/test_formsets.py"", line 1232, in test_duplicates_mixed_case
    formset.save()
  File ""/home/jon/devel/django/django/forms/models.py"", line 638, in save
    return self.save_existing_objects(commit) + self.save_new_objects(commit)
  File ""/home/jon/devel/django/django/forms/models.py"", line 769, in save_new_objects
    self.new_objects.append(self.save_new(form, commit=commit))
  File ""/home/jon/devel/django/django/forms/models.py"", line 621, in save_new
    return form.save(commit=commit)
  File ""/home/jon/devel/django/django/forms/models.py"", line 461, in save
    construct=False)
  File ""/home/jon/devel/django/django/forms/models.py"", line 103, in save_instance
    instance.save()
  File ""/home/jon/devel/django/django/db/models/base.py"", line 694, in save
    force_update=force_update, update_fields=update_fields)
  File ""/home/jon/devel/django/django/db/models/base.py"", line 722, in save_base
    updated = self._save_table(raw, cls, force_insert, force_update, using, update_fields)
  File ""/home/jon/devel/django/django/db/models/base.py"", line 803, in _save_table
    result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
  File ""/home/jon/devel/django/django/db/models/base.py"", line 842, in _do_insert
    using=using, raw=raw)
  File ""/home/jon/devel/django/django/db/models/manager.py"", line 86, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File ""/home/jon/devel/django/django/db/models/query.py"", line 952, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File ""/home/jon/devel/django/django/db/models/sql/compiler.py"", line 930, in execute_sql
    cursor.execute(sql, params)
  File ""/home/jon/devel/django/django/db/backends/utils.py"", line 65, in execute
    return self.cursor.execute(sql, params)
  File ""/home/jon/devel/django/django/db/utils.py"", line 95, in __exit__
    six.reraise(dj_exc_type, dj_exc_value, traceback)
  File ""/home/jon/devel/django/django/db/backends/utils.py"", line 65, in execute
    return self.cursor.execute(sql, params)
  File ""/home/jon/devel/django/django/db/backends/mysql/base.py"", line 126, in execute
    return self.cursor.execute(query, args)
  File ""/usr/lib64/python2.7/site-packages/MySQLdb/cursors.py"", line 174, in execute
    self.errorhandler(self, exc, value)
  File ""/usr/lib64/python2.7/site-packages/MySQLdb/connections.py"", line 36, in defaulterrorhandler
    raise errorclass, errorvalue
IntegrityError: (1062, ""Duplicate entry 'tag' for key 'name'"")

----------------------------------------------------------------------
}}}"	Bug	new	Forms	1.7	Normal			Adam Johnson Hannes Ljungberg Serl	Accepted	0	0	0	0	0	0
