Ticket #9493: formset-unique.10.diff
File formset-unique.10.diff, 11.3 KB (added by , 16 years ago) |
---|
-
django/forms/models.py
diff --git a/django/forms/models.py b/django/forms/models.py index 86eecee..0a32ffe 100644
a b 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 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 break 532 seen_data.add(row_data) 533 for date_check in date_checks: 534 seen_data = set() 535 lookup, field, unique_for = date_check 536 for form in self.forms: 537 if not hasattr(self, 'cleaned_data'): 538 continue 539 if (form.cleaned_data and form.cleaned_data[field] is not None 540 and form.cleaned_data[unique_for] is not None): 541 if lookup == 'date': 542 date = form.cleaned_data[unique_for] 543 date_data = (date.year, date.month, date.day) 544 else: 545 date_data = (getattr(form.cleaned_data[unique_for], lookup),) 546 data = (form.cleaned_data[field],) + date_data 547 if data in seen_data: 548 errors.append(self.get_date_error_message(date_check)) 549 break 550 seen_data.add(data) 551 if errors: 552 raise ValidationError(errors) 553 554 def get_unique_error_message(self, unique_check): 555 if len(unique_check) == 1: 556 return _("You have entered duplicate data for %(field)s. It " 557 "should be unique.") % { 558 "field": unique_check[0], 559 } 560 else: 561 return _("You have entered duplicate data for %(field)s. They " 562 "should be unique together.") % { 563 "field": get_text_list(unique_check, _("and")), 564 } 565 566 def get_date_error_message(self, date_check): 567 return _("%(field_name) data must be unique for %(date_field) %(lookup)s") % { 568 'field_name': unicode(date_check[1]), 569 'date_field': unicode(date_check[2]), 570 'lookup': unicode(date_check[0]), 571 } 572 507 573 def save_existing_objects(self, commit=True): 508 574 self.changed_objects = [] 509 575 self.deleted_objects = [] … … class BaseInlineFormSet(BaseModelFormSet): 657 723 label=getattr(form.fields.get(self.fk.name), 'label', capfirst(self.fk.verbose_name)) 658 724 ) 659 725 726 def get_unique_error_message(self, unique_check): 727 unique_check = [field for field in unique_check if field != self.fk.name] 728 return super(BaseInlineFormSet, self).get_unique_error_message(unique_check) 729 660 730 def _get_foreign_key(parent_model, model, fk_name=None): 661 731 """ 662 732 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..3455cee 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 … … True 573 579 ... print book.title 574 580 Les Fleurs du Mal 575 581 576 Test inline formsets where the inline-edited object uses multi-table inheritance, thus 582 Test inline formsets where the inline-edited object uses multi-table inheritance, thus 577 583 has a non AutoField yet auto-created primary key. 578 584 579 585 >>> AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1) … … True 740 746 >>> formset.save() 741 747 [<OwnerProfile: Joe Perry is 55>] 742 748 743 # ForeignKey with unique=True should enforce max_num=1 749 # ForeignKey with unique=True should enforce max_num=1 744 750 745 751 >>> FormSet = inlineformset_factory(Place, Location, can_delete=False) 746 752 >>> formset = FormSet(instance=place) … … True 943 949 >>> FormSet = modelformset_factory(ClassyMexicanRestaurant, fields=["tacos_are_yummy"]) 944 950 >>> sorted(FormSet().forms[0].fields.keys()) 945 951 ['restaurant', 'tacos_are_yummy'] 952 953 # Prevent duplicates from within the same formset 954 >>> FormSet = modelformset_factory(Product, extra=2) 955 >>> data = { 956 ... 'form-TOTAL_FORMS': 2, 957 ... 'form-INITIAL_FORMS': 0, 958 ... 'form-0-slug': 'red_car', 959 ... 'form-1-slug': 'red_car', 960 ... } 961 >>> formset = FormSet(data) 962 >>> formset.is_valid() 963 False 964 >>> formset._non_form_errors 965 [u'You have entered duplicate data for slug. It should be unique.'] 966 967 >>> FormSet = modelformset_factory(Price, extra=2) 968 >>> data = { 969 ... 'form-TOTAL_FORMS': 2, 970 ... 'form-INITIAL_FORMS': 0, 971 ... 'form-0-price': '25', 972 ... 'form-0-quantity': '7', 973 ... 'form-1-price': '25', 974 ... 'form-1-quantity': '7', 975 ... } 976 >>> formset = FormSet(data) 977 >>> formset.is_valid() 978 False 979 >>> formset._non_form_errors 980 [u'You have entered duplicate data for price and quantity. They should be unique together.'] 981 982 # only the price field is specified, this should skip any unique checks since the unique_together is not fulfilled. 983 # this will fail with a KeyError if broken. 984 >>> FormSet = modelformset_factory(Price, fields=("price",), extra=2) 985 >>> data = { 986 ... 'form-TOTAL_FORMS': '2', 987 ... 'form-INITIAL_FORMS': '0', 988 ... 'form-0-price': '24', 989 ... 'form-1-price': '24', 990 ... } 991 >>> formset = FormSet(data) 992 >>> formset.is_valid() 993 True 994 995 >>> FormSet = inlineformset_factory(Author, Book, extra=0) 996 >>> author = Author.objects.order_by('id')[0] 997 >>> book_ids = author.book_set.values_list('id', flat=True) 998 >>> data = { 999 ... 'book_set-TOTAL_FORMS': '2', 1000 ... 'book_set-INITIAL_FORMS': '2', 1001 ... 1002 ... 'book_set-0-title': 'The 2008 Election', 1003 ... 'book_set-0-author': str(author.id), 1004 ... 'book_set-0-id': str(book_ids[0]), 1005 ... 1006 ... 'book_set-1-title': 'The 2008 Election', 1007 ... 'book_set-1-author': str(author.id), 1008 ... 'book_set-1-id': str(book_ids[1]), 1009 ... } 1010 >>> formset = FormSet(data=data, instance=author) 1011 >>> formset.is_valid() 1012 False 1013 >>> formset._non_form_errors 1014 [u'You have entered duplicate data for title. It should be unique.'] 1015 946 1016 """}