Commit 23c12c9c authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

[1.0.X] Fixed #10134 -- Added unique_for_[date|day|month|year] validation to...

[1.0.X] Fixed #10134 -- Added unique_for_[date|day|month|year] validation to ModelForm handling. Thanks to Alex Gaynor for the patch.

Merge of r10646 from trunk.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.0.X@10647 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent 655b6020
Loading
Loading
Loading
Loading
+94 −29
Original line number Diff line number Diff line
@@ -219,6 +219,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
@@ -233,16 +234,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/year/month>
        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:
@@ -254,10 +255,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.
@@ -276,6 +306,55 @@ 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():
                if len(unique_check) == 1:
                    self._errors[unique_check[0]] = ErrorList([self.unique_error_message(unique_check)])
                else:
                    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

    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
@@ -283,32 +362,18 @@ class BaseModelForm(BaseForm):
            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)}
                    ])
            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'))
                    form_errors.append(
                        _(u"%(model_name)s with this %(field_label)s already exists.") % \
                        {'model_name': unicode(model_name),
                         'field_label': unicode(field_labels)}
                    )

                # Mark these fields as needing to be removed from cleaned data
                # later.
                for field_name in unique_check:
                    bad_fields.add(field_name)

        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)
            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):
        """
+47 −6
Original line number Diff line number Diff line
@@ -168,6 +168,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
@@ -1349,6 +1358,38 @@ ValidationError: [u'Select a valid choice. z is not one of the available choices
>>> core.parent
<Inventory: Pear>

### 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)