Ticket #13091: 13091_partial_unique_together.4.diff

File 13091_partial_unique_together.4.diff, 15.2 KB (added by legutierr, 4 years ago)
  • 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
    118120This method is similar to ``clean_fields``, but validates all uniqueness
     
    122124validation.
    123125
    124126Note 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.
     127``unique_together`` constraint that has all of its fields listed in ``exclude``
     128will be ignored.  An exception to this is the case of a field with its ``default``
     129parameter defined, or ``blank`` set to True (which is equivalent to ``default=""``).
     130If such a default-enabled field is listed in ``exclude``, all ``unique_together``
     131constraints that reference that field will also be ignored.
    127132
     133.. versionadded:: 1.4
     134   Prior to Django 1.4, a ``unique_together`` constraint would be ignored if any of the fields were listed in ``exclude``
    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). 
    235235
     236.. versionadded:: 1.2
     237   As of Django 1.2, ``unique_together`` is also used in ModelForm's :ref:`model validation <model-validate-unique>`.
     238
    236239    For convenience, unique_together can be a single list when dealing with a single
    237240    set of fields::
    238241
    239242        unique_together = ("driver", "restaurant")
    240243
     244
    241245``verbose_name``
    242246----------------
    243247
  • 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
     
    666666                unique_togethers.append((parent_class, parent_class._meta.unique_together))
    667667
    668668        for model_class, unique_together in unique_togethers:
    669             for check in unique_together:
    670                 for name in check:
    671                     # If this is an excluded field, don't add this check.
    672                     if name in exclude:
    673                         break
    674                 else:
    675                     unique_checks.append((model_class, tuple(check)))
     669            for unique_tuple in unique_together:
     670                perform_check = False
     671                for name in unique_tuple:
     672                    # if any of the fields are NOT excluded, then validate unique
     673                    if name not in exclude:
     674                        perform_check = True
     675                    # however, if there is a default value specified among excluded, don't
     676                    else:
     677                        field = self._meta.get_field_by_name(name)[0]
     678                        if field.default is not NOT_PROVIDED or field.blank:
     679                            perform_check = False
     680                            break
     681                       
     682                if perform_check:
     683                    unique_checks.append((model_class, tuple(unique_tuple)))
    676684
    677685        # These are checks for the unique_for_<date/year/month>.
    678686        date_checks = []
  • tests/modeltests/model_forms/tests.py

     
    11import datetime
    22from django.test import TestCase
    33from django import forms
    4 from models import Category, Writer, Book, DerivedBook, Post, FlexibleDatePost
     4from models import (Category, Writer, Book, DerivedBook, Post, FlexibleDatePost,
     5                   BirthdayPresent)
    56from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm,
    67                   ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm,
    7                    FlexDatePostForm)
     8                   FlexDatePostForm, BirthdayPresentForm, DefaultBirthdayPresentForm,
     9                   BlankBirthdayPresentForm, ExcludeAllBirthdayPresentForm)
    810
    911
    1012class IncompleteCategoryFormWithFields(forms.ModelForm):
     
    6870        self.assertEqual(len(form.errors), 1)
    6971        self.assertEqual(form.errors['__all__'], [u'Price with this Price and Quantity already exists.'])
    7072
     73    def test_excluded_unique_together(self):
     74        """
     75        ModelForm test of unique_together where a unique_together
     76        field is excluded from the form
     77        """
     78        new_instance = BirthdayPresent(username='joe.smith')
     79        form = BirthdayPresentForm({'year': '1998', 'description':'a blue bicycle'},
     80                                   instance=new_instance)
     81        self.assertTrue(form.is_valid())
     82        form.save()
     83        new_instance = BirthdayPresent(username='joe.smith')
     84        form = BirthdayPresentForm({'year': '1998', 'description':'a sweater'},
     85                                   instance=new_instance)
     86        self.assertFalse(form.is_valid())
     87        self.assertEqual(len(form.errors), 1)
     88        self.assertEqual(form.errors['__all__'], [u'Birthday present with this Username and Year already exists.'])
     89
     90    def test_excluded_unique_together_default(self):
     91        """
     92        ModelForm test of unique_together where a unique_together
     93        field with a default value is excluded from the form
     94        """
     95        form = DefaultBirthdayPresentForm({'year': '2000', 'description':'a blue bicycle'})
     96        self.assertTrue(form.is_valid())
     97        form.save()
     98        form = DefaultBirthdayPresentForm({'year': '2000', 'description':'a sweater'})
     99        self.assertTrue(form.is_valid())
     100
     101    def test_excluded_unique_together_blank(self):
     102        """
     103        ModelForm test of unique_together where a unique_together
     104        field with blank set to True is excluded from the form
     105        """
     106        form = BlankBirthdayPresentForm({'year': '2000', 'description':'a blue bicycle'})
     107        self.assertTrue(form.is_valid())
     108        form.save()
     109        form = BlankBirthdayPresentForm({'year': '2000', 'description':'a sweater'})
     110        self.assertTrue(form.is_valid())
     111
     112    def test_excluded_unique_together_all(self):
     113        """
     114        ModelForm test of unique_together where all specified
     115        unique_together fields are excluded from the form
     116        """
     117        new_instance = BirthdayPresent(username='fred.wilson', year='2010')
     118        form = ExcludeAllBirthdayPresentForm({'description':'a rock'},
     119                                             instance=new_instance)
     120        self.assertTrue(form.is_valid())
     121        form.save()
     122        new_instance = BirthdayPresent(username='fred.wilson', year='2010')
     123        form = ExcludeAllBirthdayPresentForm({'description':'an empty box'},
     124                                             instance=new_instance)
     125        self.assertTrue(form.is_valid())
     126
    71127    def test_unique_null(self):
    72128        title = 'I May Be Wrong But I Doubt It'
    73129        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, BlankBirthdayPresent)
    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 BlankBirthdayPresentForm(ModelForm):
     58    class Meta:
     59        model = BlankBirthdayPresent
     60        exclude = ('username',)
     61
     62class ExcludeAllBirthdayPresentForm(ModelForm):
     63    class Meta:
     64        model = BirthdayPresent
     65        exclude = ('username', 'year',)
     66
  • 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
     269class BlankBirthdayPresent(models.Model):
     270    """used to test unique_together validation"""
     271    username = models.CharField(max_length=30, blank=True)
     272    year = models.IntegerField()
     273    description = models.CharField(max_length=200)
     274
     275    class Meta:
     276        unique_together = (('username', 'year'),)
     277
     278
    251279__test__ = {'API_TESTS': """
    252280>>> from django import forms
    253281>>> 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-id": "1",
     1421 
     1422            "form-1-t1": "a",
     1423            "form-1-t2": "b",
     1424            "form-1-id": "2",
     1425 
     1426            "form-2-t1": "a",
     1427            "form-2-t2": "b",
     1428            "form-2-id": "3",
     1429 
     1430            "_save": "Save",
     1431        }
     1432        response = self.client.post('/test_admin/admin/admin_views/uniquetogether/', data)
     1433        self.assertContains(response, 'Please correct the errors below.')
     1434        self.assertContains(response, 'Unique together with this T1, T2 and T3 already exists.')
     1435         
    14061436    def test_post_messages(self):
    14071437        # Ticket 12707: Saving inline editable should not show admin
    14081438        # 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)       
     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