Django

Code

Ticket #9493: formset-unique.9.diff

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

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

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