#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.