Ticket #9493: formset-unique.13.diff

File formset-unique.13.diff, 16.0 KB (added by Alex Gaynor, 10 years ago)
  • django/forms/models.py

    diff --git a/django/forms/models.py b/django/forms/models.py
    index 86eecee..4c9e15d 100644
    a b and database field objects. 
    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
    class BaseModelForm(BaseForm): 
    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
    class BaseModelForm(BaseForm): 
    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()
    class BaseModelFormSet(BaseFormSet): 
    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 = []
    class BaseInlineFormSet(BaseModelFormSet): 
    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.
  • docs/topics/forms/modelforms.txt

    diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
    index be67a38..81d4642 100644
    a b exclude:: 
    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
    than that of a "normal" formset. The only difference is that we call 
    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
  • tests/modeltests/model_formsets/models.py

    diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
    index f30b212..cda3aa9 100644
    a b class Book(models.Model): 
    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
    class CustomPrimaryKey(models.Model): 
    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
    class OwnerProfile(models.Model): 
    8591
    8692class Restaurant(Place):
    8793    serves_pizza = models.BooleanField()
    88    
     94
    8995    def __unicode__(self):
    9096        return self.name
    9197
    class Poem(models.Model): 
    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
    True 
    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)
    True 
    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)
    True 
    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"""}
Back to Top