Ticket #9493: formset-unique.8.diff

File formset-unique.8.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 4b697a8..ad97116 100644
    a b class BaseModelFormSet(BaseFormSet): 
    413413            self.save_m2m = save_m2m
    414414        return self.save_existing_objects(commit) + self.save_new_objects(commit)
    415415
     416    def clean(self):
     417        self.validate_unique()
     418
     419    def validate_unique(self):
     420        from django.db.models.fields import FieldDoesNotExist, Field as ModelField
     421        unique_checks = []
     422        first_form = self.forms[0]
     423        for name, field in first_form.fields.iteritems():
     424            try:
     425                f = first_form.instance._meta.get_field_by_name(name)[0]
     426            except FieldDoesNotExist:
     427                continue
     428            if not isinstance(f, ModelField):
     429                # This is an extra field that happens to have a name that matches,
     430                # for example, a related object accessor for this model.  So
     431                # get_field_by_name found it, but it is not a Field so do not proceed
     432                # to use it as if it were.
     433                continue
     434            if f.unique:
     435                unique_checks.append((name,))
     436        unique_together = []
     437        for unique_together_check in first_form.instance._meta.unique_together:
     438            fields_on_form = [f for f in unique_together_check if f in first_form.fields]
     439            if len(fields_on_form) == len(unique_together_check):
     440                unique_together.append(unique_together_check)
     441        unique_checks.extend(unique_together)
     442        errors = []
     443        for unique_check in unique_checks:
     444            seen_data = set()
     445            for form in self.forms:
     446                if not hasattr(form, "cleaned_data"):
     447                    continue
     448                if [f for f in unique_check if f in form.cleaned_data and form.cleaned_data[f] is not None]:
     449                    row_data = tuple([form.cleaned_data[field] for field in unique_check])
     450                    if row_data in seen_data:
     451                        errors.append(self.get_unique_error_message(unique_check))
     452                        break
     453                    else:
     454                        seen_data.add(row_data)
     455        if errors:
     456            raise ValidationError(errors)
     457
     458    def get_unique_error_message(self, unique_check):
     459        if len(unique_check) == 1:
     460            return _("You have entered duplicate data for %(field)s. It "
     461                "should be unique.") % {
     462                    "field": unique_check[0],
     463                }
     464        else:
     465            return _("You have entered duplicate data for %(field)s. They "
     466                "should be unique together.") % {
     467                    "field": get_text_list(unique_check, _("and")),
     468                }
     469
    416470    def save_existing_objects(self, commit=True):
    417471        self.changed_objects = []
    418472        self.deleted_objects = []
    class BaseInlineFormSet(BaseModelFormSet): 
    547601        else:
    548602            form.fields[self.fk.name] = InlineForeignKeyField(self.instance, label=form.fields[self.fk.name].label)
    549603
     604    def get_unique_error_message(self, unique_check):
     605        unique_check = [field for field in unique_check if field != self.fk.name]
     606        return super(BaseInlineFormSet, self).get_unique_error_message(unique_check)
     607
    550608def _get_foreign_key(parent_model, model, fk_name=None):
    551609    """
    552610    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 1e87957..4462f79 100644
    a b exclude:: 
    487487
    488488.. _saving-objects-in-the-formset:
    489489
     490Overriding clean() method
     491-------------------------
     492
     493You can override the ``clean()`` method to provide custom validation to
     494the whole formset at once. By default, the ``clean()`` method will validate
     495that none of the data in the formsets violate the unique constraints on your
     496model (both field ``unique`` and model ``unique_together``). To maintain this
     497default behavior be sure you call the parent's ``clean()`` method::
     498
     499    class MyModelFormSet(BaseModelFormSet):
     500        def clean(self):
     501            super(MyModelFormSet, self).clean()
     502            # example custom validation across forms in the formset:
     503            for form in self.forms:
     504                # your custom formset validation
     505
    490506Saving objects in the formset
    491507-----------------------------
    492508
    than that of a "normal" formset. The only difference is that we call 
    571587``formset.save()`` to save the data into the database. (This was described
    572588above, in :ref:`saving-objects-in-the-formset`.)
    573589
     590
     591Overiding ``clean()`` on a ``model_formset``
     592--------------------------------------------
     593
     594Just like with ``ModelForms``, by default the ``clean()`` method of a
     595``model_formset`` will validate that none of the items in the formset validate
     596the unique constraints on your model(either unique or unique_together).  If you
     597want to overide the ``clean()`` method on a ``model_formset`` and maintain this
     598validation, you must call the parent classes ``clean`` method.
     599
     600
    574601Using a custom queryset
    575602~~~~~~~~~~~~~~~~~~~~~~~
    576603
  • 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