Django

Code

Ticket #9493: formset-unique.13.diff

File formset-unique.13.diff, 16.0 kB (added by Alex, 11 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        # Iterate over the forms so that we can find one with potentially valid 
     516        # data from which to extract the error checks 
     517        for form in self.forms: 
     518            if hasattr(form, 'cleaned_data'): 
     519                break 
     520        else: 
     521            return 
     522        unique_checks, date_checks = form._get_unique_checks() 
     523        errors = [] 
     524        # Do each of the unique checks (unique and unique_together) 
     525        for unique_check in unique_checks: 
     526            seen_data = set() 
     527            for form in self.forms: 
     528                # if the form doesn't have cleaned_data then we ignore it, 
     529                # it's already invalid 
     530                if not hasattr(form, "cleaned_data"): 
     531                    continue 
     532                # get each of the fields for which we have data on this form 
     533                if [f for f in unique_check if f in form.cleaned_data and form.cleaned_data[f] is not None]: 
     534                    # get the data itself 
     535                    row_data = tuple([form.cleaned_data[field] for field in unique_check]) 
     536                    # if we've aready seen it then we have a uniqueness failure 
     537                    if row_data in seen_data: 
     538                        # poke error messages into the right places and mark 
     539                        # the form as invalid 
     540                        errors.append(self.get_unique_error_message(unique_check)) 
     541                        form._errors[NON_FIELD_ERRORS] = self.get_form_error() 
     542                        del form.cleaned_data 
     543                        break 
     544                    # mark the data as seen 
     545                    seen_data.add(row_data) 
     546        # iterate over each of the date checks now 
     547        for date_check in date_checks: 
     548            seen_data = set() 
     549            lookup, field, unique_for = date_check 
     550            for form in self.forms: 
     551                # if the form doesn't have cleaned_data then we ignore it, 
     552                # it's already invalid 
     553                if not hasattr(self, 'cleaned_data'): 
     554                    continue 
     555                # see if we have data for both fields 
     556                if (form.cleaned_data and form.cleaned_data[field] is not None 
     557                    and form.cleaned_data[unique_for] is not None): 
     558                    # if it's a date lookup we need to get the data for all the fields 
     559                    if lookup == 'date': 
     560                        date = form.cleaned_data[unique_for] 
     561                        date_data = (date.year, date.month, date.day) 
     562                    # otherwise it's just the attribute on the date/datetime 
     563                    # object 
     564                    else: 
     565                        date_data = (getattr(form.cleaned_data[unique_for], lookup),) 
     566                    data = (form.cleaned_data[field],) + date_data 
     567                    # if we've aready seen it then we have a uniqueness failure 
     568                    if data in seen_data: 
     569                        # poke error messages into the right places and mark 
     570                        # the form as invalid 
     571                        errors.append(self.get_date_error_message(date_check)) 
     572                        form._errors[NON_FIELD_ERRORS] = self.get_form_error() 
     573                        del form.cleaned_data 
     574                        break 
     575                    seen_data.add(data) 
     576        if errors: 
     577            raise ValidationError(errors) 
     578 
     579    def get_unique_error_message(self, unique_check): 
     580        if len(unique_check) == 1: 
     581            return ugettext("Please correct the duplicate data for %(field)s.") % { 
     582                "field": unique_check[0], 
     583            } 
     584        else: 
     585            return ugettext("Please correct the duplicate data for %(field)s, " 
     586                "which must be unique.") % { 
     587                    "field": get_text_list(unique_check, _("and")), 
     588                } 
     589 
     590    def get_date_error_message(self, date_check): 
     591        return ugettext("Please correct the duplicate data for %(field_name)s " 
     592            "which must be unique for the %(lookup)s in %(date_field)s.") % { 
     593            'field_name': date_check[1], 
     594            'date_field': date_check[2], 
     595            'lookup': unicode(date_check[0]), 
     596        } 
     597 
     598    def get_form_error(self): 
     599        return ugettext("Please correct the duplicate values below.") 
     600 
    507601    def save_existing_objects(self, commit=True): 
    508602        self.changed_objects = [] 
    509603        self.deleted_objects = [] 
     
    657751                label=getattr(form.fields.get(self.fk.name), 'label', capfirst(self.fk.verbose_name)) 
    658752            ) 
    659753 
     754    def get_unique_error_message(self, unique_check): 
     755        unique_check = [field for field in unique_check if field != self.fk.name] 
     756        return super(BaseInlineFormSet, self).get_unique_error_message(unique_check) 
     757 
    660758def _get_foreign_key(parent_model, model, fk_name=None): 
    661759    """ 
    662760    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'Please correct the duplicate data for slug.'] 
     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'Please correct the duplicate data for price and quantity, which must be unique.'] 
     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'Please correct the duplicate data for title.'] 
     1024>>> formset.errors 
     1025[{}, {'__all__': u'Please correct the duplicate values below.'}] 
     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'Please correct the duplicate data for title which must be unique for the date in posted.'] 
     1046>>> formset.errors 
     1047[{}, {'__all__': u'Please correct the duplicate values below.'}] 
     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'Please correct the duplicate data for slug which must be unique for the year in posted.'] 
     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'Please correct the duplicate data for subtitle which must be unique for the month in posted.'] 
    9461086"""}