Ticket #9493: formset-unique.12.diff

File formset-unique.12.diff, 14.6 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..5ab3d7d 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        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 = []
    class BaseInlineFormSet(BaseModelFormSet): 
    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.
  • 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..b99d9d4 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'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"""}
Back to Top