Ticket #9493: formset-unique.10.diff

File formset-unique.10.diff, 11.3 KB (added by Alex Gaynor, 11 years ago)

added the date stuff and removed code duplication. Not sure what to do to make the formset errors more user friendly. Also needs tests in the worst way (I'll do this during the EDC sprint in all likelyhood)

  • django/forms/models.py

    diff --git a/django/forms/models.py b/django/forms/models.py
    index 86eecee..0a32ffe 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) data must be unique for %(date_field) %(lookup)s") % {
     568            'field_name': unicode(date_check[1]),
     569            'date_field': unicode(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..3455cee 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
    True 
    573579...     print book.title
    574580Les Fleurs du Mal
    575581
    576 Test inline formsets where the inline-edited object uses multi-table inheritance, thus 
     582Test inline formsets where the inline-edited object uses multi-table inheritance, thus
    577583has a non AutoField yet auto-created primary key.
    578584
    579585>>> AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1)
    True 
    740746>>> formset.save()
    741747[<OwnerProfile: Joe Perry is 55>]
    742748
    743 # ForeignKey with unique=True should enforce max_num=1 
     749# ForeignKey with unique=True should enforce max_num=1
    744750
    745751>>> FormSet = inlineformset_factory(Place, Location, can_delete=False)
    746752>>> formset = FormSet(instance=place)
    True 
    943949>>> FormSet = modelformset_factory(ClassyMexicanRestaurant, fields=["tacos_are_yummy"])
    944950>>> sorted(FormSet().forms[0].fields.keys())
    945951['restaurant', 'tacos_are_yummy']
     952
     953# Prevent duplicates from within the same formset
     954>>> FormSet = modelformset_factory(Product, extra=2)
     955>>> data = {
     956...     'form-TOTAL_FORMS': 2,
     957...     'form-INITIAL_FORMS': 0,
     958...     'form-0-slug': 'red_car',
     959...     'form-1-slug': 'red_car',
     960... }
     961>>> formset = FormSet(data)
     962>>> formset.is_valid()
     963False
     964>>> formset._non_form_errors
     965[u'You have entered duplicate data for slug. It should be unique.']
     966
     967>>> FormSet = modelformset_factory(Price, extra=2)
     968>>> data = {
     969...     'form-TOTAL_FORMS': 2,
     970...     'form-INITIAL_FORMS': 0,
     971...     'form-0-price': '25',
     972...     'form-0-quantity': '7',
     973...     'form-1-price': '25',
     974...     'form-1-quantity': '7',
     975... }
     976>>> formset = FormSet(data)
     977>>> formset.is_valid()
     978False
     979>>> formset._non_form_errors
     980[u'You have entered duplicate data for price and quantity. They should be unique together.']
     981
     982# only the price field is specified, this should skip any unique checks since the unique_together is not fulfilled.
     983# this will fail with a KeyError if broken.
     984>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2)
     985>>> data = {
     986...     'form-TOTAL_FORMS': '2',
     987...     'form-INITIAL_FORMS': '0',
     988...     'form-0-price': '24',
     989...     'form-1-price': '24',
     990... }
     991>>> formset = FormSet(data)
     992>>> formset.is_valid()
     993True
     994
     995>>> FormSet = inlineformset_factory(Author, Book, extra=0)
     996>>> author = Author.objects.order_by('id')[0]
     997>>> book_ids = author.book_set.values_list('id', flat=True)
     998>>> data = {
     999...     'book_set-TOTAL_FORMS': '2',
     1000...     'book_set-INITIAL_FORMS': '2',
     1001...
     1002...     'book_set-0-title': 'The 2008 Election',
     1003...     'book_set-0-author': str(author.id),
     1004...     'book_set-0-id': str(book_ids[0]),
     1005...
     1006...     'book_set-1-title': 'The 2008 Election',
     1007...     'book_set-1-author': str(author.id),
     1008...     'book_set-1-id': str(book_ids[1]),
     1009... }
     1010>>> formset = FormSet(data=data, instance=author)
     1011>>> formset.is_valid()
     1012False
     1013>>> formset._non_form_errors
     1014[u'You have entered duplicate data for title. It should be unique.']
     1015
    9461016"""}
Back to Top