Ticket #9493: formset-unique.9.diff

File formset-unique.9.diff, 7.8 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 de5f1ab..4cd518f 100644
    a b class BaseModelFormSet(BaseFormSet): 
    414414            self.save_m2m = save_m2m
    415415        return self.save_existing_objects(commit) + self.save_new_objects(commit)
    416416
     417    def clean(self):
     418        self.validate_unique()
     419
     420    def validate_unique(self):
     421        from django.db.models.fields import FieldDoesNotExist, Field as ModelField
     422        unique_checks = []
     423        first_form = self.forms[0]
     424        for name, field in first_form.fields.iteritems():
     425            try:
     426                f = first_form.instance._meta.get_field_by_name(name)[0]
     427            except FieldDoesNotExist:
     428                continue
     429            if not isinstance(f, ModelField):
     430                # This is an extra field that happens to have a name that matches,
     431                # for example, a related object accessor for this model.  So
     432                # get_field_by_name found it, but it is not a Field so do not proceed
     433                # to use it as if it were.
     434                continue
     435            if f.unique:
     436                unique_checks.append((name,))
     437        unique_together = []
     438        for unique_together_check in first_form.instance._meta.unique_together:
     439            fields_on_form = [f for f in unique_together_check if f in first_form.fields]
     440            if len(fields_on_form) == len(unique_together_check):
     441                unique_together.append(unique_together_check)
     442        unique_checks.extend(unique_together)
     443        errors = []
     444        for unique_check in unique_checks:
     445            seen_data = set()
     446            for form in self.forms:
     447                if not hasattr(form, "cleaned_data"):
     448                    continue
     449                if [f for f in unique_check if f in form.cleaned_data and form.cleaned_data[f] is not None]:
     450                    row_data = tuple([form.cleaned_data[field] for field in unique_check])
     451                    if row_data in seen_data:
     452                        errors.append(self.get_unique_error_message(unique_check))
     453                        break
     454                    else:
     455                        seen_data.add(row_data)
     456        if errors:
     457            raise ValidationError(errors)
     458
     459    def get_unique_error_message(self, unique_check):
     460        if len(unique_check) == 1:
     461            return _("You have entered duplicate data for %(field)s. It "
     462                "should be unique.") % {
     463                    "field": unique_check[0],
     464                }
     465        else:
     466            return _("You have entered duplicate data for %(field)s. They "
     467                "should be unique together.") % {
     468                    "field": get_text_list(unique_check, _("and")),
     469                }
     470
    417471    def save_existing_objects(self, commit=True):
    418472        self.changed_objects = []
    419473        self.deleted_objects = []
    class BaseInlineFormSet(BaseModelFormSet): 
    564618                label=getattr(form.fields.get(self.fk.name), 'label', capfirst(self.fk.verbose_name))
    565619            )
    566620
     621    def get_unique_error_message(self, unique_check):
     622        unique_check = [field for field in unique_check if field != self.fk.name]
     623        return super(BaseInlineFormSet, self).get_unique_error_message(unique_check)
     624
    567625def _get_foreign_key(parent_model, model, fk_name=None):
    568626    """
    569627    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 d8cbe34..6906e59 100644
    a b class BetterAuthor(Author): 
    2222class Book(models.Model):
    2323    author = models.ForeignKey(Author)
    2424    title = models.CharField(max_length=100)
     25   
     26    class Meta:
     27        unique_together = (
     28            ('author', 'title'),
     29        )
     30        ordering = ['id']
    2531
    2632    def __unicode__(self):
    2733        return self.title
    True 
    934940>>> formset.get_queryset()
    935941[<Player: Bobby>]
    936942
     943# Prevent duplicates from within the same formset
     944>>> FormSet = modelformset_factory(Product, extra=2)
     945>>> data = {
     946...     'form-TOTAL_FORMS': 2,
     947...     'form-INITIAL_FORMS': 0,
     948...     'form-0-slug': 'red_car',
     949...     'form-1-slug': 'red_car',
     950... }
     951>>> formset = FormSet(data)
     952>>> formset.is_valid()
     953False
     954>>> formset._non_form_errors
     955[u'You have entered duplicate data for slug. It should be unique.']
     956
     957>>> FormSet = modelformset_factory(Price, extra=2)
     958>>> data = {
     959...     'form-TOTAL_FORMS': 2,
     960...     'form-INITIAL_FORMS': 0,
     961...     'form-0-price': '25',
     962...     'form-0-quantity': '7',
     963...     'form-1-price': '25',
     964...     'form-1-quantity': '7',
     965... }
     966>>> formset = FormSet(data)
     967>>> formset.is_valid()
     968False
     969>>> formset._non_form_errors
     970[u'You have entered duplicate data for price and quantity. They should be unique together.']
     971
     972# only the price field is specified, this should skip any unique checks since the unique_together is not fulfilled.
     973# this will fail with a KeyError if broken.
     974>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2)
     975>>> data = {
     976...     'form-TOTAL_FORMS': '2',
     977...     'form-INITIAL_FORMS': '0',
     978...     'form-0-price': '24',
     979...     'form-1-price': '24',
     980... }
     981>>> formset = FormSet(data)
     982>>> formset.is_valid()
     983True
     984
     985>>> FormSet = inlineformset_factory(Author, Book, extra=0)
     986>>> author = Author.objects.order_by('id')[0]
     987>>> book_ids = author.book_set.values_list('id', flat=True)
     988>>> data = {
     989...     'book_set-TOTAL_FORMS': '2',
     990...     'book_set-INITIAL_FORMS': '2',
     991...     
     992...     'book_set-0-title': 'The 2008 Election',
     993...     'book_set-0-author': str(author.id),
     994...     'book_set-0-id': str(book_ids[0]),
     995...     
     996...     'book_set-1-title': 'The 2008 Election',
     997...     'book_set-1-author': str(author.id),
     998...     'book_set-1-id': str(book_ids[1]),
     999... }
     1000>>> formset = FormSet(data=data, instance=author)
     1001>>> formset.is_valid()
     1002False
     1003>>> formset._non_form_errors
     1004[u'You have entered duplicate data for title. It should be unique.']
    9371005"""}
Back to Top