Django

Code

Ticket #9493: formset-unique.8.diff

File formset-unique.8.diff, 7.8 kB (added by Alex, 1 year ago)
  • a/django/forms/models.py

    old new  
    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 = [] 
     
    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. 
  • a/docs/topics/forms/modelforms.txt

    old new  
    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 
     
    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 
  • a/tests/modeltests/model_formsets/models.py

    old new  
    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 
     
    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"""}