#23964: MySQL: BaseModelFormSet.validate_unique() fails for mixed case;
----------------------------+--------------------
     Reporter:  jdufresne   |      Owner:  nobody
         Type:  Bug         |     Status:  new
    Component:  Forms       |    Version:  1.7
     Severity:  Normal      |   Keywords:
 Triage Stage:  Unreviewed  |  Has patch:  0
Easy pickings:  0           |      UI/UX:  0
----------------------------+--------------------
 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'")

 ----------------------------------------------------------------------
 }}}

--
Ticket URL: <https://code.djangoproject.com/ticket/23964>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-updates+unsubscr...@googlegroups.com.
To post to this group, send email to django-updates@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/052.a4d9a3b2a05012c8e25314ddbf60ba82%40djangoproject.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to