From 17958fa7a9a757c2b0abcdc3f931a010584234f1 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Thu, 30 Apr 2009 13:47:39 +0000 Subject: [PATCH] Fixed #10134 -- Added unique_for_[date|day|month|year] validation to ModelForm handling. Thanks to Alex Gaynor for the patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@10646 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/forms/models.py | 123 +++++++++++++++++++------ tests/modeltests/model_forms/models.py | 41 +++++++++ 2 files changed, 135 insertions(+), 29 deletions(-) diff --git a/django/forms/models.py b/django/forms/models.py index 41380f21e9..86eeceedd3 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -225,6 +225,7 @@ class BaseModelForm(BaseForm): object_data.update(initial) super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted) + def clean(self): self.validate_unique() return self.cleaned_data @@ -239,16 +240,16 @@ class BaseModelForm(BaseForm): # not make sense to check data that didn't validate, and since NULL does not # equal NULL in SQL we should not do any unique checking for NULL values. unique_checks = [] + # these are checks for the unique_for_ + date_checks = [] for check in self.instance._meta.unique_together[:]: fields_on_form = [field for field in check if self.cleaned_data.get(field) is not None] if len(fields_on_form) == len(check): unique_checks.append(check) - form_errors = [] - # Gather a list of checks for fields declared as unique and add them to # the list of checks. Again, skip empty fields and any that did not validate. - for name, field in self.fields.items(): + for name in self.fields: try: f = self.instance._meta.get_field_by_name(name)[0] except FieldDoesNotExist: @@ -260,10 +261,39 @@ class BaseModelForm(BaseForm): # get_field_by_name found it, but it is not a Field so do not proceed # to use it as if it were. continue - if f.unique and self.cleaned_data.get(name) is not None: + if self.cleaned_data.get(name) is None: + continue + if f.unique: unique_checks.append((name,)) + if f.unique_for_date and self.cleaned_data.get(f.unique_for_date) is not None: + date_checks.append(('date', name, f.unique_for_date)) + if f.unique_for_year and self.cleaned_data.get(f.unique_for_year) is not None: + date_checks.append(('year', name, f.unique_for_year)) + if f.unique_for_month and self.cleaned_data.get(f.unique_for_month) is not None: + date_checks.append(('month', name, f.unique_for_month)) + form_errors = [] bad_fields = set() + + field_errors, global_errors = self._perform_unique_checks(unique_checks) + bad_fields.union(field_errors) + form_errors.extend(global_errors) + + field_errors, global_errors = self._perform_date_checks(date_checks) + bad_fields.union(field_errors) + form_errors.extend(global_errors) + + for field_name in bad_fields: + del self.cleaned_data[field_name] + if form_errors: + # Raise the unique together errors since they are considered + # form-wide. + raise ValidationError(form_errors) + + def _perform_unique_checks(self, unique_checks): + bad_fields = set() + form_errors = [] + for unique_check in unique_checks: # Try to look up an existing object with the same values as this # object's values for all the unique field. @@ -288,39 +318,74 @@ class BaseModelForm(BaseForm): # This cute trick with extra/values is the most efficient way to # tell if a particular query returns any results. if qs.extra(select={'a': 1}).values('a').order_by(): - model_name = capfirst(self.instance._meta.verbose_name) - - # A unique field if len(unique_check) == 1: - field_name = unique_check[0] - field_label = self.fields[field_name].label - # Insert the error into the error dict, very sneaky - self._errors[field_name] = ErrorList([ - _(u"%(model_name)s with this %(field_label)s already exists.") % \ - {'model_name': unicode(model_name), - 'field_label': unicode(field_label)} - ]) - # unique_together + self._errors[unique_check[0]] = ErrorList([self.unique_error_message(unique_check)]) else: - field_labels = [self.fields[field_name].label for field_name in unique_check] - field_labels = get_text_list(field_labels, _('and')) - form_errors.append( - _(u"%(model_name)s with this %(field_label)s already exists.") % \ - {'model_name': unicode(model_name), - 'field_label': unicode(field_labels)} - ) + form_errors.append(self.unique_error_message(unique_check)) # Mark these fields as needing to be removed from cleaned data # later. for field_name in unique_check: bad_fields.add(field_name) + return bad_fields, form_errors - for field_name in bad_fields: - del self.cleaned_data[field_name] - if form_errors: - # Raise the unique together errors since they are considered - # form-wide. - raise ValidationError(form_errors) + def _perform_date_checks(self, date_checks): + bad_fields = set() + for lookup_type, field, unique_for in date_checks: + lookup_kwargs = {} + # there's a ticket to add a date lookup, we can remove this special + # case if that makes it's way in + if lookup_type == 'date': + date = self.cleaned_data[unique_for] + lookup_kwargs['%s__day' % unique_for] = date.day + lookup_kwargs['%s__month' % unique_for] = date.month + lookup_kwargs['%s__year' % unique_for] = date.year + else: + lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(self.cleaned_data[unique_for], lookup_type) + lookup_kwargs[field] = self.cleaned_data[field] + + qs = self.instance.__class__._default_manager.filter(**lookup_kwargs) + # Exclude the current object from the query if we are editing an + # instance (as opposed to creating a new one) + if self.instance.pk is not None: + qs = qs.exclude(pk=self.instance.pk) + + # This cute trick with extra/values is the most efficient way to + # tell if a particular query returns any results. + if qs.extra(select={'a': 1}).values('a').order_by(): + self._errors[field] = ErrorList([ + self.date_error_message(lookup_type, field, unique_for) + ]) + bad_fields.add(field) + return bad_fields, [] + + def date_error_message(self, lookup_type, field, unique_for): + return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % { + 'field_name': unicode(self.fields[field].label), + 'date_field': unicode(self.fields[unique_for].label), + 'lookup': lookup_type, + } + + def unique_error_message(self, unique_check): + model_name = capfirst(self.instance._meta.verbose_name) + + # A unique field + if len(unique_check) == 1: + field_name = unique_check[0] + field_label = self.fields[field_name].label + # Insert the error into the error dict, very sneaky + return _(u"%(model_name)s with this %(field_label)s already exists.") % { + 'model_name': unicode(model_name), + 'field_label': unicode(field_label) + } + # unique_together + else: + field_labels = [self.fields[field_name].label for field_name in unique_check] + field_labels = get_text_list(field_labels, _('and')) + return _(u"%(model_name)s with this %(field_label)s already exists.") % { + 'model_name': unicode(model_name), + 'field_label': unicode(field_labels) + } def save(self, commit=True): """ diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index 992bb903ba..16b50145d7 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -189,6 +189,15 @@ class ExplicitPK(models.Model): def __unicode__(self): return self.key +class Post(models.Model): + title = models.CharField(max_length=50, unique_for_date='posted', blank=True) + slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) + subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) + posted = models.DateField() + + def __unicode__(self): + return self.name + __test__ = {'API_TESTS': """ >>> from django import forms >>> from django.forms.models import ModelForm, model_to_dict @@ -1472,6 +1481,38 @@ ValidationError: [u'Select a valid choice. z is not one of the available choices +### Validation on unique_for_date + +>>> p = Post.objects.create(title="Django 1.0 is released", slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) +>>> class PostForm(ModelForm): +... class Meta: +... model = Post + +>>> f = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'}) +>>> f.is_valid() +False +>>> f.errors +{'title': [u'Title must be unique for Posted date.']} +>>> f = PostForm({'title': "Work on Django 1.1 begins", 'posted': '2008-09-03'}) +>>> f.is_valid() +True +>>> f = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-04'}) +>>> f.is_valid() +True +>>> f = PostForm({'slug': "Django 1.0", 'posted': '2008-01-01'}) +>>> f.is_valid() +False +>>> f.errors +{'slug': [u'Slug must be unique for Posted year.']} +>>> f = PostForm({'subtitle': "Finally", 'posted': '2008-09-30'}) +>>> f.is_valid() +False +>>> f.errors +{'subtitle': [u'Subtitle must be unique for Posted month.']} +>>> f = PostForm({'subtitle': "Finally", "title": "Django 1.0 is released", "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p) +>>> f.is_valid() +True + # Clean up >>> import shutil >>> shutil.rmtree(temp_storage_dir)