Ticket #9493: formset-unique.11.diff

File formset-unique.11.diff, 13.4 KB (added by Alex Gaynor, 10 years ago)
  • django/forms/models.py

    diff --git a/django/forms/models.py b/django/forms/models.py
    index 86eecee..695c5e2 100644
    a b class BaseModelForm(BaseForm): 
    231231        return self.cleaned_data
    232232
    233233    def validate_unique(self):
     234        unique_checks, date_checks = self._get_unique_checks()
     235        form_errors = []
     236        bad_fields = set()
     237
     238        field_errors, global_errors = self._perform_unique_checks(unique_checks)
     239        bad_fields.union(field_errors)
     240        form_errors.extend(global_errors)
     241
     242        field_errors, global_errors = self._perform_date_checks(date_checks)
     243        bad_fields.union(field_errors)
     244        form_errors.extend(global_errors)
     245
     246        for field_name in bad_fields:
     247            del self.cleaned_data[field_name]
     248        if form_errors:
     249            # Raise the unique together errors since they are considered
     250            # form-wide.
     251            raise ValidationError(form_errors)
     252
     253    def _get_unique_checks(self):
    234254        from django.db.models.fields import FieldDoesNotExist, Field as ModelField
    235255
    236256        # Gather a list of checks to perform. We only perform unique checks
    class BaseModelForm(BaseForm): 
    271291                date_checks.append(('year', name, f.unique_for_year))
    272292            if f.unique_for_month and self.cleaned_data.get(f.unique_for_month) is not None:
    273293                date_checks.append(('month', name, f.unique_for_month))
     294        return unique_checks, date_checks
    274295
    275         form_errors = []
    276         bad_fields = set()
    277 
    278         field_errors, global_errors = self._perform_unique_checks(unique_checks)
    279         bad_fields.union(field_errors)
    280         form_errors.extend(global_errors)
    281 
    282         field_errors, global_errors = self._perform_date_checks(date_checks)
    283         bad_fields.union(field_errors)
    284         form_errors.extend(global_errors)
    285 
    286         for field_name in bad_fields:
    287             del self.cleaned_data[field_name]
    288         if form_errors:
    289             # Raise the unique together errors since they are considered
    290             # form-wide.
    291             raise ValidationError(form_errors)
    292296
    293297    def _perform_unique_checks(self, unique_checks):
    294298        bad_fields = set()
    class BaseModelFormSet(BaseFormSet): 
    504508            self.save_m2m = save_m2m
    505509        return self.save_existing_objects(commit) + self.save_new_objects(commit)
    506510
     511    def clean(self):
     512        self.validate_unique()
     513
     514    def validate_unique(self):
     515        for form in self.forms:
     516            if hasattr(form, 'cleaned_data'):
     517                break
     518        else:
     519            return
     520        unique_checks, date_checks = form._get_unique_checks()
     521        errors = []
     522        for unique_check in unique_checks:
     523            seen_data = set()
     524            for form in self.forms:
     525                if not hasattr(form, "cleaned_data"):
     526                    continue
     527                if [f for f in unique_check if f in form.cleaned_data and form.cleaned_data[f] is not None]:
     528                    row_data = tuple([form.cleaned_data[field] for field in unique_check])
     529                    if row_data in seen_data:
     530                        errors.append(self.get_unique_error_message(unique_check))
     531                        break
     532                    seen_data.add(row_data)
     533        for date_check in date_checks:
     534            seen_data = set()
     535            lookup, field, unique_for = date_check
     536            for form in self.forms:
     537                if not hasattr(self, 'cleaned_data'):
     538                    continue
     539                if (form.cleaned_data and form.cleaned_data[field] is not None
     540                    and form.cleaned_data[unique_for] is not None):
     541                    if lookup == 'date':
     542                        date = form.cleaned_data[unique_for]
     543                        date_data = (date.year, date.month, date.day)
     544                    else:
     545                        date_data = (getattr(form.cleaned_data[unique_for], lookup),)
     546                    data = (form.cleaned_data[field],) + date_data
     547                    if data in seen_data:
     548                        errors.append(self.get_date_error_message(date_check))
     549                        break
     550                    seen_data.add(data)
     551        if errors:
     552            raise ValidationError(errors)
     553
     554    def get_unique_error_message(self, unique_check):
     555        if len(unique_check) == 1:
     556            return _("You have entered duplicate data for %(field)s. It "
     557                "should be unique.") % {
     558                    "field": unique_check[0],
     559                }
     560        else:
     561            return _("You have entered duplicate data for %(field)s. They "
     562                "should be unique together.") % {
     563                    "field": get_text_list(unique_check, _("and")),
     564                }
     565
     566    def get_date_error_message(self, date_check):
     567        return _("%(field_name)s data must be unique for %(date_field)s %(lookup)s") % {
     568            'field_name': self.forms[0][date_check[1]].label,
     569            'date_field': date_check[2],
     570            'lookup': unicode(date_check[0]),
     571        }
     572
    507573    def save_existing_objects(self, commit=True):
    508574        self.changed_objects = []
    509575        self.deleted_objects = []
    class BaseInlineFormSet(BaseModelFormSet): 
    657723                label=getattr(form.fields.get(self.fk.name), 'label', capfirst(self.fk.verbose_name))
    658724            )
    659725
     726    def get_unique_error_message(self, unique_check):
     727        unique_check = [field for field in unique_check if field != self.fk.name]
     728        return super(BaseInlineFormSet, self).get_unique_error_message(unique_check)
     729
    660730def _get_foreign_key(parent_model, model, fk_name=None):
    661731    """
    662732    Finds and returns the ForeignKey from model to parent if there is one.
  • docs/topics/forms/modelforms.txt

    diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
    index be67a38..81d4642 100644
    a b exclude:: 
    515515
    516516.. _saving-objects-in-the-formset:
    517517
     518Overriding clean() method
     519-------------------------
     520
     521You can override the ``clean()`` method to provide custom validation to
     522the whole formset at once. By default, the ``clean()`` method will validate
     523that none of the data in the formsets violate the unique constraints on your
     524model (both field ``unique`` and model ``unique_together``). To maintain this
     525default behavior be sure you call the parent's ``clean()`` method::
     526
     527    class MyModelFormSet(BaseModelFormSet):
     528        def clean(self):
     529            super(MyModelFormSet, self).clean()
     530            # example custom validation across forms in the formset:
     531            for form in self.forms:
     532                # your custom formset validation
     533
    518534Saving objects in the formset
    519535-----------------------------
    520536
    than that of a "normal" formset. The only difference is that we call 
    599615``formset.save()`` to save the data into the database. (This was described
    600616above, in :ref:`saving-objects-in-the-formset`.)
    601617
     618
     619Overiding ``clean()`` on a ``model_formset``
     620--------------------------------------------
     621
     622Just like with ``ModelForms``, by default the ``clean()`` method of a
     623``model_formset`` will validate that none of the items in the formset validate
     624the unique constraints on your model(either unique or unique_together).  If you
     625want to overide the ``clean()`` method on a ``model_formset`` and maintain this
     626validation, you must call the parent classes ``clean`` method.
     627
     628
    602629Using a custom queryset
    603630~~~~~~~~~~~~~~~~~~~~~~~
    604631
  • tests/modeltests/model_formsets/models.py

    diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
    index f30b212..482cd1d 100644
    a b class Book(models.Model): 
    2323    author = models.ForeignKey(Author)
    2424    title = models.CharField(max_length=100)
    2525
     26    class Meta:
     27        unique_together = (
     28            ('author', 'title'),
     29        )
     30        ordering = ['id']
     31
    2632    def __unicode__(self):
    2733        return self.title
    2834
    class CustomPrimaryKey(models.Model): 
    5864class Place(models.Model):
    5965    name = models.CharField(max_length=50)
    6066    city = models.CharField(max_length=50)
    61    
     67
    6268    def __unicode__(self):
    6369        return self.name
    6470
    class OwnerProfile(models.Model): 
    8591
    8692class Restaurant(Place):
    8793    serves_pizza = models.BooleanField()
    88    
     94
    8995    def __unicode__(self):
    9096        return self.name
    9197
    class Poem(models.Model): 
    166172    def __unicode__(self):
    167173        return self.name
    168174
     175class Post(models.Model):
     176    title = models.CharField(max_length=50, unique_for_date='posted', blank=True)
     177    slug = models.CharField(max_length=50, unique_for_year='posted', blank=True)
     178    subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True)
     179    posted = models.DateField()
     180
     181    def __unicode__(self):
     182        return self.name
     183
    169184__test__ = {'API_TESTS': """
    170185
    171186>>> from datetime import date
    True 
    573588...     print book.title
    574589Les Fleurs du Mal
    575590
    576 Test inline formsets where the inline-edited object uses multi-table inheritance, thus 
     591Test inline formsets where the inline-edited object uses multi-table inheritance, thus
    577592has a non AutoField yet auto-created primary key.
    578593
    579594>>> AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1)
    True 
    740755>>> formset.save()
    741756[<OwnerProfile: Joe Perry is 55>]
    742757
    743 # ForeignKey with unique=True should enforce max_num=1 
     758# ForeignKey with unique=True should enforce max_num=1
    744759
    745760>>> FormSet = inlineformset_factory(Place, Location, can_delete=False)
    746761>>> formset = FormSet(instance=place)
    True 
    943958>>> FormSet = modelformset_factory(ClassyMexicanRestaurant, fields=["tacos_are_yummy"])
    944959>>> sorted(FormSet().forms[0].fields.keys())
    945960['restaurant', 'tacos_are_yummy']
     961
     962# Prevent duplicates from within the same formset
     963>>> FormSet = modelformset_factory(Product, extra=2)
     964>>> data = {
     965...     'form-TOTAL_FORMS': 2,
     966...     'form-INITIAL_FORMS': 0,
     967...     'form-0-slug': 'red_car',
     968...     'form-1-slug': 'red_car',
     969... }
     970>>> formset = FormSet(data)
     971>>> formset.is_valid()
     972False
     973>>> formset._non_form_errors
     974[u'You have entered duplicate data for slug. It should be unique.']
     975
     976>>> FormSet = modelformset_factory(Price, extra=2)
     977>>> data = {
     978...     'form-TOTAL_FORMS': 2,
     979...     'form-INITIAL_FORMS': 0,
     980...     'form-0-price': '25',
     981...     'form-0-quantity': '7',
     982...     'form-1-price': '25',
     983...     'form-1-quantity': '7',
     984... }
     985>>> formset = FormSet(data)
     986>>> formset.is_valid()
     987False
     988>>> formset._non_form_errors
     989[u'You have entered duplicate data for price and quantity. They should be unique together.']
     990
     991# only the price field is specified, this should skip any unique checks since the unique_together is not fulfilled.
     992# this will fail with a KeyError if broken.
     993>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2)
     994>>> data = {
     995...     'form-TOTAL_FORMS': '2',
     996...     'form-INITIAL_FORMS': '0',
     997...     'form-0-price': '24',
     998...     'form-1-price': '24',
     999... }
     1000>>> formset = FormSet(data)
     1001>>> formset.is_valid()
     1002True
     1003
     1004>>> FormSet = inlineformset_factory(Author, Book, extra=0)
     1005>>> author = Author.objects.order_by('id')[0]
     1006>>> book_ids = author.book_set.values_list('id', flat=True)
     1007>>> data = {
     1008...     'book_set-TOTAL_FORMS': '2',
     1009...     'book_set-INITIAL_FORMS': '2',
     1010...
     1011...     'book_set-0-title': 'The 2008 Election',
     1012...     'book_set-0-author': str(author.id),
     1013...     'book_set-0-id': str(book_ids[0]),
     1014...
     1015...     'book_set-1-title': 'The 2008 Election',
     1016...     'book_set-1-author': str(author.id),
     1017...     'book_set-1-id': str(book_ids[1]),
     1018... }
     1019>>> formset = FormSet(data=data, instance=author)
     1020>>> formset.is_valid()
     1021False
     1022>>> formset._non_form_errors
     1023[u'You have entered duplicate data for title. It should be unique.']
     1024
     1025>>> FormSet = modelformset_factory(Post, extra=2)
     1026>>> data = {
     1027...     'form-TOTAL_FORMS': '2',
     1028...     'form-INITIAL_FORMS': '0',
     1029...
     1030...     'form-0-title': 'blah',
     1031...     'form-0-slug': 'Morning',
     1032...     'form-0-subtitle': 'foo',
     1033...     'form-0-posted': '2009-01-01',
     1034...     'form-1-title': 'blah',
     1035...     'form-1-slug': 'Morning in Prague',
     1036...     'form-1-subtitle': 'rawr',
     1037...     'form-1-posted': '2009-01-01'
     1038... }
     1039>>> formset = FormSet(data)
     1040>>> formset.is_valid()
     1041False
     1042>>> formset._non_form_errors
     1043[u'Title data must be unique for posted date']
     1044
     1045>>> data = {
     1046...     'form-TOTAL_FORMS': '2',
     1047...     'form-INITIAL_FORMS': '0',
     1048...
     1049...     'form-0-title': 'foo',
     1050...     'form-0-slug': 'Morning in Prague',
     1051...     'form-0-subtitle': 'foo',
     1052...     'form-0-posted': '2009-01-01',
     1053...     'form-1-title': 'blah',
     1054...     'form-1-slug': 'Morning in Prague',
     1055...     'form-1-subtitle': 'rawr',
     1056...     'form-1-posted': '2009-08-02'
     1057... }
     1058>>> formset = FormSet(data)
     1059>>> formset.is_valid()
     1060False
     1061>>> formset._non_form_errors
     1062[u'Slug data must be unique for posted year']
     1063
     1064>>> data = {
     1065...     'form-TOTAL_FORMS': '2',
     1066...     'form-INITIAL_FORMS': '0',
     1067...
     1068...     'form-0-title': 'foo',
     1069...     'form-0-slug': 'Morning in Prague',
     1070...     'form-0-subtitle': 'rawr',
     1071...     'form-0-posted': '2008-08-01',
     1072...     'form-1-title': 'blah',
     1073...     'form-1-slug': 'Prague',
     1074...     'form-1-subtitle': 'rawr',
     1075...     'form-1-posted': '2009-08-02'
     1076... }
     1077>>> formset = FormSet(data)
     1078>>> formset.is_valid()
     1079False
     1080>>> formset._non_form_errors
     1081[u'Subtitle data must be unique for posted month']
    9461082"""}
Back to Top