Ticket #13091: 13091_partial_unique_together.3.diff

File 13091_partial_unique_together.3.diff, 14.2 KB (added by legutierr, 4 years ago)

This patch keeps the tests added to admin_views by the prior patch, but it makes a more precise change to the behavior (plus more tests and documantaion), as discussed here: http://groups.google.com/group/django-developers/browse_thread/thread/deb2645bf5157211

  • docs/ref/models/instances.txt

     
    113113
    114114Finally, ``full_clean()`` will check any unique constraints on your model.
    115115
     116.. _model-validate-unique
     117
    116118.. method:: Model.validate_unique(exclude=None)
    117119
     120.. versionadded:: 1.4
     121   Prior to Django 1.4, a ``unique_together`` constraint would be ignored if any of the fields were listed in ``exclude``
     122
    118123This method is similar to ``clean_fields``, but validates all uniqueness
    119124constraints on your model instead of individual field values. The optional
    120125``exclude`` argument allows you to provide a list of field names to exclude
     
    122127validation.
    123128
    124129Note that if you provide an ``exclude`` argument to ``validate_unique``, any
    125 ``unique_together`` constraint that contains one of the fields you provided
    126 will not be checked.
     130``unique_together`` constraint that has all of its fields listed in ``exclude``
     131will be ignored. An exception to this is the case of a field with its ``default``
     132parameter defined. If such a default-enabled field is listed in ``exclude``, all
     133``unique_together`` constraints that reference that field will also be ignored.
    127134
    128135
     136
    129137Saving objects
    130138==============
    131139
  • docs/ref/models/options.txt

     
    231231    This is a list of lists of fields that must be unique when considered together.
    232232    It's used in the Django admin and is enforced at the database level (i.e., the
    233233    appropriate ``UNIQUE`` statements are included in the ``CREATE TABLE``
    234     statement).
     234    statement).  As of Django 1.2, it is also used in ModelForm's 
     235    :ref:`model validation <model-validate-unique>`.
    235236
    236237    For convenience, unique_together can be a single list when dealing with a single
    237238    set of fields::
    238239
    239240        unique_together = ("driver", "restaurant")
    240241
     242
    241243``verbose_name``
    242244----------------
    243245
  • django/db/models/base.py

     
    77import django.db.models.manager     # Imported to register signal handler.
    88from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS
    99from django.core import validators
    10 from django.db.models.fields import AutoField, FieldDoesNotExist
     10from django.db.models.fields import AutoField, FieldDoesNotExist, NOT_PROVIDED
    1111from django.db.models.fields.related import (OneToOneRel, ManyToOneRel,
    1212    OneToOneField, add_lazy_relation)
    1313from django.db.models.query import Q
     
    667667
    668668        for model_class, unique_together in unique_togethers:
    669669            for check in unique_together:
     670                # if all fields are excluded, do not add to unique_checks
     671                check_count = len(check)
    670672                for name in check:
    671                     # If this is an excluded field, don't add this check.
    672673                    if name in exclude:
    673                         break
     674                        check_count -= 1
     675                        if check_count > 0:
     676                            # if the default value is specified, do not add to unique_checks
     677                            default = self._meta.fields_dict[name].default
     678                            if default is not NOT_PROVIDED:
     679                                break
     680                        else:
     681                            break
    674682                else:
    675683                    unique_checks.append((model_class, tuple(check)))
    676684
  • django/db/models/options.py

     
    219219        return self._field_name_cache
    220220    fields = property(_fields)
    221221
     222    def _fields_dict(self):
     223        try:
     224            return self._fields_dict_cache
     225        except AttributeError:
     226            self._fields_dict_cache = dict([(field.name, field) for field in self.fields])
     227            return self._fields_dict_cache
     228    fields_dict = property(_fields_dict)
     229
    222230    def get_fields_with_model(self):
    223231        """
    224232        Returns a sequence of (field, model) pairs for all fields. The "model"
  • tests/modeltests/model_forms/tests.py

     
    44from models import Category, Writer, Book, DerivedBook, Post, FlexibleDatePost
    55from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm,
    66                   ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm,
    7                    FlexDatePostForm)
     7                   FlexDatePostForm, BirthdayPresentForm, DefaultBirthdayPresentForm,
     8                   ExcludeAllBirthdayPresentForm)
    89
    910
    1011class IncompleteCategoryFormWithFields(forms.ModelForm):
     
    6869        self.assertEqual(len(form.errors), 1)
    6970        self.assertEqual(form.errors['__all__'], [u'Price with this Price and Quantity already exists.'])
    7071
     72    def test_excluded_unique_together(self):
     73        """
     74        ModelForm test of unique_together where a unique_together
     75        field is excluded from the form
     76        """
     77        form = BirthdayPresentForm({'year': '1998', 'description':'a blue bicycle'})
     78        form.instance.username = 'joe.smith'
     79        self.assertTrue(form.is_valid())
     80        form.save()
     81        form = BirthdayPresentForm({'year': '1998', 'description':'a sweater'})
     82        form.instance.username = 'joe.smith'
     83        self.assertFalse(form.is_valid())
     84        self.assertEqual(len(form.errors), 1)
     85        self.assertEqual(form.errors['__all__'], [u'Birthday present with this Username and Year already exists.'])
     86
     87    def test_excluded_unique_together_default(self):
     88        """
     89        ModelForm test of unique_together where a unique_together
     90        field with a default value is excluded from the form
     91        """
     92        form = DefaultBirthdayPresentForm({'year': '2000', 'description':'a blue bicycle'})
     93        self.assertTrue(form.is_valid())
     94        form.save()
     95        form = DefaultBirthdayPresentForm({'year': '2000', 'description':'a sweater'})
     96        self.assertTrue(form.is_valid())
     97
     98    def test_excluded_unique_together_all(self):
     99        """
     100        ModelForm test of unique_together where all specified
     101        unique_together fields are excluded from the form
     102        """
     103        form = ExcludeAllBirthdayPresentForm({'description':'a rock'})
     104        form.instance.year = '2010'
     105        form.instance.username = 'fred.wilson'
     106        self.assertTrue(form.is_valid())
     107        form.save()
     108        form = ExcludeAllBirthdayPresentForm({'description':'an empty box'})
     109        form.instance.year = '2010'
     110        form.instance.username = 'fred.wilson'
     111        self.assertTrue(form.is_valid())
     112
    71113    def test_unique_null(self):
    72114        title = 'I May Be Wrong But I Doubt It'
    73115        form = BookForm({'title': title, 'author': self.writer.pk})
  • tests/modeltests/model_forms/mforms.py

     
    22from django.forms import ModelForm
    33
    44from models import (Product, Price, Book, DerivedBook, ExplicitPK, Post,
    5         DerivedPost, Writer, FlexibleDatePost)
     5        DerivedPost, Writer, FlexibleDatePost, BirthdayPresent,
     6        DefaultBirthdayPresent)
    67
    78class ProductForm(ModelForm):
    89    class Meta:
     
    4243class FlexDatePostForm(ModelForm):
    4344    class Meta:
    4445        model = FlexibleDatePost
     46
     47class BirthdayPresentForm(ModelForm):
     48    class Meta:
     49        model = BirthdayPresent
     50        exclude = ('username',)
     51
     52class DefaultBirthdayPresentForm(ModelForm):
     53    class Meta:
     54        model = DefaultBirthdayPresent
     55        exclude = ('username',)
     56
     57class ExcludeAllBirthdayPresentForm(ModelForm):
     58    class Meta:
     59        model = BirthdayPresent
     60        exclude = ('username', 'year',)
     61
  • tests/modeltests/model_forms/models.py

     
    248248    subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True)
    249249    posted = models.DateField(blank=True, null=True)
    250250
     251class BirthdayPresent(models.Model):
     252    """used to test unique_together validation"""
     253    username = models.CharField(max_length=30)
     254    year = models.IntegerField()
     255    description = models.CharField(max_length=200)
     256
     257    class Meta:
     258        unique_together = ('username', 'year')
     259
     260class DefaultBirthdayPresent(models.Model):
     261    """used to test unique_together validation"""
     262    username = models.CharField(max_length=30, default='jeremy.jones')
     263    year = models.IntegerField()
     264    description = models.CharField(max_length=200)
     265
     266    class Meta:
     267        unique_together = (('username', 'year'),)
     268
     269
    251270__test__ = {'API_TESTS': """
    252271>>> from django import forms
    253272>>> from django.forms.models import ModelForm, model_to_dict
  • tests/modeltests/model_formsets/tests.py

     
    10661066        self.assertEqual(formset._non_form_errors,
    10671067            [u'Please correct the duplicate data for price and quantity, which must be unique.'])
    10681068
    1069         # Only the price field is specified, this should skip any unique checks since
    1070         # the unique_together is not fulfilled. This will fail with a KeyError if broken.
     1069        # Only the price field is specified, this will fail in the unique checks since
     1070        # the unique_together is partially included.
    10711071        FormSet = modelformset_factory(Price, fields=("price",), extra=2)
    10721072        data = {
    10731073            'form-TOTAL_FORMS': '2',
     
    10771077            'form-1-price': '24',
    10781078        }
    10791079        formset = FormSet(data)
    1080         self.assertTrue(formset.is_valid())
     1080        self.assertFalse(formset.is_valid())
     1081        self.assertEqual(formset._non_form_errors,
     1082            [u'Please correct the duplicate data for price and quantity, which must be unique.'])
    10811083
    10821084        FormSet = inlineformset_factory(Author, Book, extra=0)
    10831085        author = Author.objects.create(pk=1, name='Charles Baudelaire')
  • tests/regressiontests/admin_views/tests.py

     
    3636    Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast,
    3737    Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit,
    3838    Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
    39     Question, Answer, Inquisition, Actor, FoodDelivery,
     39    Question, Answer, Inquisition, Actor, FoodDelivery, UniqueTogether,
    4040    RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory)
    4141
    4242
     
    14031403        # 1 select per object = 3 selects
    14041404        self.assertEqual(response.content.count("<select"), 4)
    14051405
     1406    def test_partial_unique_together(self):
     1407        """ Ensure that no IntegrityError is raised when editing some (but
     1408            not all) of the values specified as unique_together. Refs #13091.
     1409        """
     1410        UniqueTogether.objects.create(t1='a', t2='b', t3='c')
     1411        UniqueTogether.objects.create(t1='b', t2='b', t3='a')
     1412        UniqueTogether.objects.create(t1='c', t2='a', t3='c')
     1413        data = {
     1414            "form-TOTAL_FORMS": "3",
     1415            "form-INITIAL_FORMS": "3",
     1416            "form-MAX_NUM_FORMS": "0",
     1417 
     1418            "form-0-t1": "a",
     1419            "form-0-t2": "b",
     1420            "form-0-t3": "c",
     1421            "form-0-id": "1",
     1422 
     1423            "form-1-t1": "a",
     1424            "form-1-t2": "b",
     1425            "form-1-t3": "c",
     1426            "form-1-id": "2",
     1427 
     1428            "form-2-t1": "a",
     1429            "form-2-t2": "b",
     1430            "form-2-t3": "c",
     1431            "form-2-id": "3",
     1432 
     1433            "_save": "Save",
     1434        }
     1435        response = self.client.post('/test_admin/admin/admin_views/uniquetogether/', data)
     1436        self.assertContains(response, 'Please correct the errors below.')
     1437        self.assertContains(response, 'Unique together with this T1, T2 and T3 already exists.')
     1438         
    14061439    def test_post_messages(self):
    14071440        # Ticket 12707: Saving inline editable should not show admin
    14081441        # action warnings
  • tests/regressiontests/admin_views/models.py

     
    803803    list_display_links = ('title', 'id') # 'id' in list_display_links
    804804    list_editable = ('content', )
    805805
     806class UniqueTogether(models.Model): 
     807    t1 = models.CharField(max_length=255, blank=True, null=True) 
     808    t2 = models.CharField(max_length=255, blank=True, null=True) 
     809    t3 = models.CharField(max_length=255, blank=True, null=True)       
     810     
     811    class Meta: 
     812        unique_together = ['t1','t2','t3'] 
     813                 
     814class UniqueTogetherAdmin(admin.ModelAdmin): 
     815    list_display = ['t3', 't1', 't2'] 
     816    list_editable = ['t1', 't2',]
     817
     818
    806819admin.site.register(Article, ArticleAdmin)
    807820admin.site.register(CustomArticle, CustomArticleAdmin)
    808821admin.site.register(Section, save_as=True, inlines=[ArticleInline])
     
    845858admin.site.register(CoverLetter, CoverLetterAdmin)
    846859admin.site.register(Story, StoryAdmin)
    847860admin.site.register(OtherStory, OtherStoryAdmin)
     861admin.site.register(UniqueTogether, UniqueTogetherAdmin)
    848862
    849863# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    850864# That way we cover all four cases:
Back to Top