Ticket #9493: formset-unique.13.diff
File formset-unique.13.diff, 16.0 KB (added by , 16 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. 6 6 from django.utils.encoding import smart_unicode, force_unicode 7 7 from django.utils.datastructures import SortedDict 8 8 from django.utils.text import get_text_list, capfirst 9 from django.utils.translation import ugettext_lazy as _ 9 from django.utils.translation import ugettext_lazy as _, ugettext 10 10 11 11 from util import ValidationError, ErrorList 12 from forms import BaseForm, get_declared_fields 12 from forms import BaseForm, get_declared_fields, NON_FIELD_ERRORS 13 13 from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES 14 14 from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput 15 15 from widgets import media_property … … class BaseModelForm(BaseForm): 231 231 return self.cleaned_data 232 232 233 233 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): 234 254 from django.db.models.fields import FieldDoesNotExist, Field as ModelField 235 255 236 256 # Gather a list of checks to perform. We only perform unique checks … … class BaseModelForm(BaseForm): 271 291 date_checks.append(('year', name, f.unique_for_year)) 272 292 if f.unique_for_month and self.cleaned_data.get(f.unique_for_month) is not None: 273 293 date_checks.append(('month', name, f.unique_for_month)) 294 return unique_checks, date_checks 274 295 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 considered290 # form-wide.291 raise ValidationError(form_errors)292 296 293 297 def _perform_unique_checks(self, unique_checks): 294 298 bad_fields = set() … … class BaseModelFormSet(BaseFormSet): 504 508 self.save_m2m = save_m2m 505 509 return self.save_existing_objects(commit) + self.save_new_objects(commit) 506 510 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 507 601 def save_existing_objects(self, commit=True): 508 602 self.changed_objects = [] 509 603 self.deleted_objects = [] … … class BaseInlineFormSet(BaseModelFormSet): 657 751 label=getattr(form.fields.get(self.fk.name), 'label', capfirst(self.fk.verbose_name)) 658 752 ) 659 753 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 660 758 def _get_foreign_key(parent_model, model, fk_name=None): 661 759 """ 662 760 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:: 515 515 516 516 .. _saving-objects-in-the-formset: 517 517 518 Overriding clean() method 519 ------------------------- 520 521 You can override the ``clean()`` method to provide custom validation to 522 the whole formset at once. By default, the ``clean()`` method will validate 523 that none of the data in the formsets violate the unique constraints on your 524 model (both field ``unique`` and model ``unique_together``). To maintain this 525 default 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 518 534 Saving objects in the formset 519 535 ----------------------------- 520 536 … … than that of a "normal" formset. The only difference is that we call 599 615 ``formset.save()`` to save the data into the database. (This was described 600 616 above, in :ref:`saving-objects-in-the-formset`.) 601 617 618 619 Overiding ``clean()`` on a ``model_formset`` 620 -------------------------------------------- 621 622 Just like with ``ModelForms``, by default the ``clean()`` method of a 623 ``model_formset`` will validate that none of the items in the formset validate 624 the unique constraints on your model(either unique or unique_together). If you 625 want to overide the ``clean()`` method on a ``model_formset`` and maintain this 626 validation, you must call the parent classes ``clean`` method. 627 628 602 629 Using a custom queryset 603 630 ~~~~~~~~~~~~~~~~~~~~~~~ 604 631 -
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): 23 23 author = models.ForeignKey(Author) 24 24 title = models.CharField(max_length=100) 25 25 26 class Meta: 27 unique_together = ( 28 ('author', 'title'), 29 ) 30 ordering = ['id'] 31 26 32 def __unicode__(self): 27 33 return self.title 28 34 … … class CustomPrimaryKey(models.Model): 58 64 class Place(models.Model): 59 65 name = models.CharField(max_length=50) 60 66 city = models.CharField(max_length=50) 61 67 62 68 def __unicode__(self): 63 69 return self.name 64 70 … … class OwnerProfile(models.Model): 85 91 86 92 class Restaurant(Place): 87 93 serves_pizza = models.BooleanField() 88 94 89 95 def __unicode__(self): 90 96 return self.name 91 97 … … class Poem(models.Model): 166 172 def __unicode__(self): 167 173 return self.name 168 174 175 class 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 169 184 __test__ = {'API_TESTS': """ 170 185 171 186 >>> from datetime import date … … True 573 588 ... print book.title 574 589 Les Fleurs du Mal 575 590 576 Test inline formsets where the inline-edited object uses multi-table inheritance, thus 591 Test inline formsets where the inline-edited object uses multi-table inheritance, thus 577 592 has a non AutoField yet auto-created primary key. 578 593 579 594 >>> AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1) … … True 740 755 >>> formset.save() 741 756 [<OwnerProfile: Joe Perry is 55>] 742 757 743 # ForeignKey with unique=True should enforce max_num=1 758 # ForeignKey with unique=True should enforce max_num=1 744 759 745 760 >>> FormSet = inlineformset_factory(Place, Location, can_delete=False) 746 761 >>> formset = FormSet(instance=place) … … True 943 958 >>> FormSet = modelformset_factory(ClassyMexicanRestaurant, fields=["tacos_are_yummy"]) 944 959 >>> sorted(FormSet().forms[0].fields.keys()) 945 960 ['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() 972 False 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() 987 False 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() 1002 True 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() 1021 False 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() 1043 False 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() 1064 False 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() 1083 False 1084 >>> formset._non_form_errors 1085 [u'Please correct the duplicate data for subtitle which must be unique for the month in posted.'] 946 1086 """}