Django

Code

Ticket #9493: formset-unique.11.diff

File formset-unique.11.diff, 13.4 kB (added by Alex, 11 months ago)
  • a/django/forms/models.py

    old new  
    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 
     
    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() 
     
    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)s data must be unique for %(date_field)s %(lookup)s") % { 
     568            'field_name': self.forms[0][date_check[1]].label, 
     569            'date_field': 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 = [] 
     
    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. 
  • 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  
    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 
     
    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 
     
    8591 
    8692class Restaurant(Place): 
    8793    serves_pizza = models.BooleanField() 
    88      
     94 
    8995    def __unicode__(self): 
    9096        return self.name 
    9197 
     
    166172    def __unicode__(self): 
    167173        return self.name 
    168174 
     175class Post(models.Model): 
     176    title = models.CharField(max_length=50, unique_for_date='posted', blank=True) 
     177    slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) 
     178    subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) 
     179    posted = models.DateField() 
     180 
     181    def __unicode__(self): 
     182        return self.name 
     183 
    169184__test__ = {'API_TESTS': """ 
    170185 
    171186>>> from datetime import date 
     
    573588...     print book.title 
    574589Les Fleurs du Mal 
    575590 
    576 Test inline formsets where the inline-edited object uses multi-table inheritance, thus  
     591Test inline formsets where the inline-edited object uses multi-table inheritance, thus 
    577592has a non AutoField yet auto-created primary key. 
    578593 
    579594>>> AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1) 
     
    740755>>> formset.save() 
    741756[<OwnerProfile: Joe Perry is 55>] 
    742757 
    743 # ForeignKey with unique=True should enforce max_num=1  
     758# ForeignKey with unique=True should enforce max_num=1 
    744759 
    745760>>> FormSet = inlineformset_factory(Place, Location, can_delete=False) 
    746761>>> formset = FormSet(instance=place) 
     
    943958>>> FormSet = modelformset_factory(ClassyMexicanRestaurant, fields=["tacos_are_yummy"]) 
    944959>>> sorted(FormSet().forms[0].fields.keys()) 
    945960['restaurant', 'tacos_are_yummy'] 
     961 
     962# Prevent duplicates from within the same formset 
     963>>> FormSet = modelformset_factory(Product, extra=2) 
     964>>> data = { 
     965...     'form-TOTAL_FORMS': 2, 
     966...     'form-INITIAL_FORMS': 0, 
     967...     'form-0-slug': 'red_car', 
     968...     'form-1-slug': 'red_car', 
     969... } 
     970>>> formset = FormSet(data) 
     971>>> formset.is_valid() 
     972False 
     973>>> formset._non_form_errors 
     974[u'You have entered duplicate data for slug. It should be unique.'] 
     975 
     976>>> FormSet = modelformset_factory(Price, extra=2) 
     977>>> data = { 
     978...     'form-TOTAL_FORMS': 2, 
     979...     'form-INITIAL_FORMS': 0, 
     980...     'form-0-price': '25', 
     981...     'form-0-quantity': '7', 
     982...     'form-1-price': '25', 
     983...     'form-1-quantity': '7', 
     984... } 
     985>>> formset = FormSet(data) 
     986>>> formset.is_valid() 
     987False 
     988>>> formset._non_form_errors 
     989[u'You have entered duplicate data for price and quantity. They should be unique together.'] 
     990 
     991# only the price field is specified, this should skip any unique checks since the unique_together is not fulfilled. 
     992# this will fail with a KeyError if broken. 
     993>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2) 
     994>>> data = { 
     995...     'form-TOTAL_FORMS': '2', 
     996...     'form-INITIAL_FORMS': '0', 
     997...     'form-0-price': '24', 
     998...     'form-1-price': '24', 
     999... } 
     1000>>> formset = FormSet(data) 
     1001>>> formset.is_valid() 
     1002True 
     1003 
     1004>>> FormSet = inlineformset_factory(Author, Book, extra=0) 
     1005>>> author = Author.objects.order_by('id')[0] 
     1006>>> book_ids = author.book_set.values_list('id', flat=True) 
     1007>>> data = { 
     1008...     'book_set-TOTAL_FORMS': '2', 
     1009...     'book_set-INITIAL_FORMS': '2', 
     1010... 
     1011...     'book_set-0-title': 'The 2008 Election', 
     1012...     'book_set-0-author': str(author.id), 
     1013...     'book_set-0-id': str(book_ids[0]), 
     1014... 
     1015...     'book_set-1-title': 'The 2008 Election', 
     1016...     'book_set-1-author': str(author.id), 
     1017...     'book_set-1-id': str(book_ids[1]), 
     1018... } 
     1019>>> formset = FormSet(data=data, instance=author) 
     1020>>> formset.is_valid() 
     1021False 
     1022>>> formset._non_form_errors 
     1023[u'You have entered duplicate data for title. It should be unique.'] 
     1024 
     1025>>> FormSet = modelformset_factory(Post, extra=2) 
     1026>>> data = { 
     1027...     'form-TOTAL_FORMS': '2', 
     1028...     'form-INITIAL_FORMS': '0', 
     1029... 
     1030...     'form-0-title': 'blah', 
     1031...     'form-0-slug': 'Morning', 
     1032...     'form-0-subtitle': 'foo', 
     1033...     'form-0-posted': '2009-01-01', 
     1034...     'form-1-title': 'blah', 
     1035...     'form-1-slug': 'Morning in Prague', 
     1036...     'form-1-subtitle': 'rawr', 
     1037...     'form-1-posted': '2009-01-01' 
     1038... } 
     1039>>> formset = FormSet(data) 
     1040>>> formset.is_valid() 
     1041False 
     1042>>> formset._non_form_errors 
     1043[u'Title data must be unique for posted date'] 
     1044 
     1045>>> data = { 
     1046...     'form-TOTAL_FORMS': '2', 
     1047...     'form-INITIAL_FORMS': '0', 
     1048... 
     1049...     'form-0-title': 'foo', 
     1050...     'form-0-slug': 'Morning in Prague', 
     1051...     'form-0-subtitle': 'foo', 
     1052...     'form-0-posted': '2009-01-01', 
     1053...     'form-1-title': 'blah', 
     1054...     'form-1-slug': 'Morning in Prague', 
     1055...     'form-1-subtitle': 'rawr', 
     1056...     'form-1-posted': '2009-08-02' 
     1057... } 
     1058>>> formset = FormSet(data) 
     1059>>> formset.is_valid() 
     1060False 
     1061>>> formset._non_form_errors 
     1062[u'Slug data must be unique for posted year'] 
     1063 
     1064>>> data = { 
     1065...     'form-TOTAL_FORMS': '2', 
     1066...     'form-INITIAL_FORMS': '0', 
     1067... 
     1068...     'form-0-title': 'foo', 
     1069...     'form-0-slug': 'Morning in Prague', 
     1070...     'form-0-subtitle': 'rawr', 
     1071...     'form-0-posted': '2008-08-01', 
     1072...     'form-1-title': 'blah', 
     1073...     'form-1-slug': 'Prague', 
     1074...     'form-1-subtitle': 'rawr', 
     1075...     'form-1-posted': '2009-08-02' 
     1076... } 
     1077>>> formset = FormSet(data) 
     1078>>> formset.is_valid() 
     1079False 
     1080>>> formset._non_form_errors 
     1081[u'Subtitle data must be unique for posted month'] 
    9461082"""}