Django

Code

Ticket #9493: formset-unique.12.diff

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

    old new  
    66from django.utils.encoding import smart_unicode, force_unicode 
    77from django.utils.datastructures import SortedDict 
    88from django.utils.text import get_text_list, capfirst 
    9 from django.utils.translation import ugettext_lazy as _ 
     9from django.utils.translation import ugettext_lazy as _, ugettext 
    1010 
    1111from util import ValidationError, ErrorList 
    12 from forms import BaseForm, get_declared_fields 
     12from forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS 
    1313from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 
    1414from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 
    1515from widgets import media_property 
     
    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                        form._errors[NON_FIELD_ERRORS] = self.get_form_error() 
     532                        del form.cleaned_data 
     533                        break 
     534                    seen_data.add(row_data) 
     535        for date_check in date_checks: 
     536            seen_data = set() 
     537            lookup, field, unique_for = date_check 
     538            for form in self.forms: 
     539                if not hasattr(self, 'cleaned_data'): 
     540                    continue 
     541                if (form.cleaned_data and form.cleaned_data[field] is not None 
     542                    and form.cleaned_data[unique_for] is not None): 
     543                    if lookup == 'date': 
     544                        date = form.cleaned_data[unique_for] 
     545                        date_data = (date.year, date.month, date.day) 
     546                    else: 
     547                        date_data = (getattr(form.cleaned_data[unique_for], lookup),) 
     548                    data = (form.cleaned_data[field],) + date_data 
     549                    if data in seen_data: 
     550                        errors.append(self.get_date_error_message(date_check)) 
     551                        form._errors[NON_FIELD_ERRORS] = self.get_form_error() 
     552                        del form.cleaned_data 
     553                        break 
     554                    seen_data.add(data) 
     555        if errors: 
     556            raise ValidationError(errors) 
     557 
     558    def get_unique_error_message(self, unique_check): 
     559        if len(unique_check) == 1: 
     560            return ugettext("You have entered duplicate data for %(field)s. It " 
     561                "should be unique.") % { 
     562                    "field": unique_check[0], 
     563                } 
     564        else: 
     565            return ugettext("You have entered duplicate data for %(field)s. They " 
     566                "should be unique together.") % { 
     567                    "field": get_text_list(unique_check, _("and")), 
     568                } 
     569 
     570    def get_date_error_message(self, date_check): 
     571        return ugettext("%(field_name)s data must be unique for %(date_field)s %(lookup)s") % { 
     572            'field_name': self.forms[0][date_check[1]].label, 
     573            'date_field': date_check[2], 
     574            'lookup': unicode(date_check[0]), 
     575        } 
     576 
     577    def get_form_error(self): 
     578        return ugettext("This form contains duplicate data.") 
     579 
    507580    def save_existing_objects(self, commit=True): 
    508581        self.changed_objects = [] 
    509582        self.deleted_objects = [] 
     
    657730                label=getattr(form.fields.get(self.fk.name), 'label', capfirst(self.fk.verbose_name)) 
    658731            ) 
    659732 
     733    def get_unique_error_message(self, unique_check): 
     734        unique_check = [field for field in unique_check if field != self.fk.name] 
     735        return super(BaseInlineFormSet, self).get_unique_error_message(unique_check) 
     736 
    660737def _get_foreign_key(parent_model, model, fk_name=None): 
    661738    """ 
    662739    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>>> formset.errors 
     1025[{}, {'__all__': u'This form contains duplicate data.'}] 
     1026 
     1027>>> FormSet = modelformset_factory(Post, extra=2) 
     1028>>> data = { 
     1029...     'form-TOTAL_FORMS': '2', 
     1030...     'form-INITIAL_FORMS': '0', 
     1031... 
     1032...     'form-0-title': 'blah', 
     1033...     'form-0-slug': 'Morning', 
     1034...     'form-0-subtitle': 'foo', 
     1035...     'form-0-posted': '2009-01-01', 
     1036...     'form-1-title': 'blah', 
     1037...     'form-1-slug': 'Morning in Prague', 
     1038...     'form-1-subtitle': 'rawr', 
     1039...     'form-1-posted': '2009-01-01' 
     1040... } 
     1041>>> formset = FormSet(data) 
     1042>>> formset.is_valid() 
     1043False 
     1044>>> formset._non_form_errors 
     1045[u'Title data must be unique for posted date'] 
     1046>>> formset.errors 
     1047[{}, {'__all__': u'This form contains duplicate data.'}] 
     1048 
     1049>>> data = { 
     1050...     'form-TOTAL_FORMS': '2', 
     1051...     'form-INITIAL_FORMS': '0', 
     1052... 
     1053...     'form-0-title': 'foo', 
     1054...     'form-0-slug': 'Morning in Prague', 
     1055...     'form-0-subtitle': 'foo', 
     1056...     'form-0-posted': '2009-01-01', 
     1057...     'form-1-title': 'blah', 
     1058...     'form-1-slug': 'Morning in Prague', 
     1059...     'form-1-subtitle': 'rawr', 
     1060...     'form-1-posted': '2009-08-02' 
     1061... } 
     1062>>> formset = FormSet(data) 
     1063>>> formset.is_valid() 
     1064False 
     1065>>> formset._non_form_errors 
     1066[u'Slug data must be unique for posted year'] 
     1067 
     1068>>> data = { 
     1069...     'form-TOTAL_FORMS': '2', 
     1070...     'form-INITIAL_FORMS': '0', 
     1071... 
     1072...     'form-0-title': 'foo', 
     1073...     'form-0-slug': 'Morning in Prague', 
     1074...     'form-0-subtitle': 'rawr', 
     1075...     'form-0-posted': '2008-08-01', 
     1076...     'form-1-title': 'blah', 
     1077...     'form-1-slug': 'Prague', 
     1078...     'form-1-subtitle': 'rawr', 
     1079...     'form-1-posted': '2009-08-02' 
     1080... } 
     1081>>> formset = FormSet(data) 
     1082>>> formset.is_valid() 
     1083False 
     1084>>> formset._non_form_errors 
     1085[u'Subtitle data must be unique for posted month'] 
    9461086"""}