Django

Code

Changeset 8805

Show
Ignore:
Timestamp:
09/01/08 14:08:08 (3 months ago)
Author:
jacob
Message:

Fixed #8209: ModelForms now validate unique constraints. Alex Gaynor did much of this work, and Brian Rosner helped as well.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/forms/models.py

    r8775 r8805  
    44""" 
    55 
    6 from django.utils.translation import ugettext_lazy as _ 
    76from django.utils.encoding import smart_unicode 
    87from django.utils.datastructures import SortedDict 
     8from django.utils.text import get_text_list 
     9from django.utils.translation import ugettext_lazy as _ 
    910 
    1011from util import ValidationError, ErrorList 
     
    2021    'ModelMultipleChoiceField', 
    2122) 
     23 
    2224 
    2325def save_instance(form, instance, fields=None, fail_message='saved', 
     
    203205        super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data, 
    204206                                            error_class, label_suffix, empty_permitted) 
     207    def clean(self): 
     208        self.validate_unique() 
     209        return self.cleaned_data 
     210 
     211    def validate_unique(self): 
     212        from django.db.models.fields import FieldDoesNotExist 
     213        unique_checks = list(self.instance._meta.unique_together[:]) 
     214        form_errors = [] 
     215         
     216        # Make sure the unique checks apply to actual fields on the ModelForm 
     217        for name, field in self.fields.items(): 
     218            try: 
     219                f = self.instance._meta.get_field_by_name(name)[0] 
     220            except FieldDoesNotExist: 
     221                # This is an extra field that's not on the ModelForm, ignore it 
     222                continue 
     223            # MySQL can't handle ... WHERE pk IS NULL, so make sure we don't  
     224            # don't generate queries of that form. 
     225            is_null_pk = f.primary_key and self.cleaned_data[name] is None 
     226            if name in self.cleaned_data and f.unique and not is_null_pk: 
     227                unique_checks.append((name,)) 
     228                 
     229        # Don't run unique checks on fields that already have an error. 
     230        unique_checks = [check for check in unique_checks if not [x in self._errors for x in check if x in self._errors]] 
     231         
     232        for unique_check in unique_checks: 
     233            # Try to look up an existing object with the same values as this 
     234            # object's values for all the unique field. 
     235             
     236            lookup_kwargs = {} 
     237            for field_name in unique_check: 
     238                lookup_kwargs[field_name] = self.cleaned_data[field_name] 
     239             
     240            qs = self.instance.__class__._default_manager.filter(**lookup_kwargs) 
     241 
     242            # Exclude the current object from the query if we are editing an  
     243            # instance (as opposed to creating a new one) 
     244            if self.instance.pk is not None: 
     245                qs = qs.exclude(pk=self.instance.pk) 
     246                 
     247            # This cute trick with extra/values is the most efficiant way to 
     248            # tell if a particular query returns any results. 
     249            if qs.extra(select={'a': 1}).values('a').order_by(): 
     250                model_name = self.instance._meta.verbose_name.title() 
     251                 
     252                # A unique field 
     253                if len(unique_check) == 1: 
     254                    field_name = unique_check[0] 
     255                    field_label = self.fields[field_name].label 
     256                    # Insert the error into the error dict, very sneaky 
     257                    self._errors[field_name] = ErrorList([ 
     258                        _("%(model_name)s with this %(field_label)s already exists.") % \ 
     259                        {'model_name': model_name, 'field_label': field_label} 
     260                    ]) 
     261                # unique_together 
     262                else: 
     263                    field_labels = [self.fields[field_name].label for field_name in unique_check] 
     264                    field_labels = get_text_list(field_labels, _('and')) 
     265                    form_errors.append( 
     266                        _("%(model_name)s with this %(field_label)s already exists.") % \ 
     267                        {'model_name': model_name, 'field_label': field_labels} 
     268                    ) 
     269                 
     270                # Remove the data from the cleaned_data dict since it was invalid 
     271                for field_name in unique_check: 
     272                    del self.cleaned_data[field_name] 
     273         
     274        if form_errors: 
     275            # Raise the unique together errors since they are considered form-wide. 
     276            raise ValidationError(form_errors) 
    205277 
    206278    def save(self, commit=True): 
     
    247319        self.queryset = queryset 
    248320        defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix} 
    249         if self.max_num > 0: 
    250             qs = self.get_queryset()[:self.max_num] 
    251         else: 
    252             qs = self.get_queryset() 
    253         defaults['initial'] = [model_to_dict(obj) for obj in qs] 
     321        defaults['initial'] = [model_to_dict(obj) for obj in self.get_queryset()] 
    254322        defaults.update(kwargs) 
    255323        super(BaseModelFormSet, self).__init__(**defaults) 
    256324 
     325    def _construct_form(self, i, **kwargs): 
     326        if i < self._initial_form_count: 
     327            kwargs['instance'] = self.get_queryset()[i] 
     328        return super(BaseModelFormSet, self)._construct_form(i, **kwargs) 
     329 
    257330    def get_queryset(self): 
    258         if self.queryset is not None: 
    259             return self.queryset 
    260         return self.model._default_manager.get_query_set() 
     331        if not hasattr(self, '_queryset'): 
     332            if self.queryset is not None: 
     333                qs = self.queryset 
     334            else: 
     335                qs = self.model._default_manager.get_query_set() 
     336            if self.max_num > 0: 
     337                self._queryset = qs[:self.max_num] 
     338            else: 
     339                self._queryset = qs 
     340        return self._queryset 
    261341 
    262342    def save_new(self, form, commit=True): 
     
    359439            self._initial_form_count = 0 
    360440        super(BaseInlineFormSet, self)._construct_forms() 
     441 
     442    def _construct_form(self, i, **kwargs): 
     443        form = super(BaseInlineFormSet, self)._construct_form(i, **kwargs) 
     444        if self.save_as_new: 
     445            # Remove the primary key from the form's data, we are only 
     446            # creating new instances 
     447            form.data[form.add_prefix(self._pk_field.name)] = None 
     448        return form 
    361449 
    362450    def get_queryset(self): 
  • django/trunk/django/utils/itercompat.py

    r7914 r8805  
    7373    out_value.sort() 
    7474    return out_value 
     75 
  • django/trunk/docs/topics/forms/modelforms.txt

    r8616 r8805  
    339339   ...         model = Article 
    340340 
     341Overriding the clean() method 
     342----------------------------- 
     343 
     344You can overide the ``clean()`` method on a model form to provide additional 
     345validation in the same way you can on a normal form.  However, by default the 
     346``clean()`` method validates the uniqueness of fields that are marked as unique 
     347on the model, and those marked as unque_together, if you would like to overide 
     348the ``clean()`` method and maintain the default validation you must call the 
     349parent class's ``clean()`` method. 
     350 
    341351Form inheritance 
    342352---------------- 
  • django/trunk/tests/modeltests/model_formsets/models.py

    r8775 r8805  
    7373    def __unicode__(self): 
    7474        return self.name 
     75 
     76class Product(models.Model): 
     77    slug = models.SlugField(unique=True) 
     78 
     79    def __unicode__(self): 
     80        return self.slug 
     81 
     82class Price(models.Model): 
     83    price = models.DecimalField(max_digits=10, decimal_places=2) 
     84    quantity = models.PositiveIntegerField() 
     85 
     86    def __unicode__(self): 
     87        return u"%s for %s" % (self.quantity, self.price) 
     88 
     89    class Meta: 
     90        unique_together = (('price', 'quantity'),) 
    7591 
    7692class MexicanRestaurant(Restaurant): 
     
    554570<class 'django.db.models.fields.related.ForeignKey'> 
    555571 
     572# unique/unique_together validation ########################################### 
     573 
     574>>> FormSet = modelformset_factory(Product, extra=1) 
     575>>> data = { 
     576...     'form-TOTAL_FORMS': '1', 
     577...     'form-INITIAL_FORMS': '0', 
     578...     'form-0-slug': 'car-red', 
     579... } 
     580>>> formset = FormSet(data) 
     581>>> formset.is_valid() 
     582True 
     583>>> formset.save() 
     584[<Product: car-red>] 
     585 
     586>>> data = { 
     587...     'form-TOTAL_FORMS': '1', 
     588...     'form-INITIAL_FORMS': '0', 
     589...     'form-0-slug': 'car-red', 
     590... } 
     591>>> formset = FormSet(data) 
     592>>> formset.is_valid() 
     593False 
     594>>> formset.errors 
     595[{'slug': [u'Product with this Slug already exists.']}] 
     596 
     597# unique_together 
     598 
     599>>> FormSet = modelformset_factory(Price, extra=1) 
     600>>> data = { 
     601...     'form-TOTAL_FORMS': '1', 
     602...     'form-INITIAL_FORMS': '0', 
     603...     'form-0-price': u'12.00', 
     604...     'form-0-quantity': '1', 
     605... } 
     606>>> formset = FormSet(data) 
     607>>> formset.is_valid() 
     608True 
     609>>> formset.save() 
     610[<Price: 1 for 12.00>] 
     611 
     612>>> data = { 
     613...     'form-TOTAL_FORMS': '1', 
     614...     'form-INITIAL_FORMS': '0', 
     615...     'form-0-price': u'12.00', 
     616...     'form-0-quantity': '1', 
     617... } 
     618>>> formset = FormSet(data) 
     619>>> formset.is_valid() 
     620False 
     621>>> formset.errors 
     622[{'__all__': [u'Price with this Price and Quantity already exists.']}] 
     623 
    556624"""} 
  • django/trunk/tests/modeltests/model_forms/models.py

    r8772 r8805  
    118118        return self.field 
    119119 
     120class Product(models.Model): 
     121    slug = models.SlugField(unique=True) 
     122 
     123    def __unicode__(self): 
     124        return self.slug 
     125 
     126class Price(models.Model): 
     127    price = models.DecimalField(max_digits=10, decimal_places=2) 
     128    quantity = models.PositiveIntegerField() 
     129 
     130    def __unicode__(self): 
     131        return u"%s for %s" % (self.quantity, self.price) 
     132 
     133    class Meta: 
     134        unique_together = (('price', 'quantity'),) 
     135 
    120136class ArticleStatus(models.Model): 
    121137    status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True) 
     138 
    122139 
    123140__test__ = {'API_TESTS': """ 
     
    11331150u'1' 
    11341151 
     1152# unique/unique_together validation 
     1153 
     1154>>> class ProductForm(ModelForm): 
     1155...     class Meta: 
     1156...         model = Product 
     1157>>> form = ProductForm({'slug': 'teddy-bear-blue'}) 
     1158>>> form.is_valid() 
     1159True 
     1160>>> obj = form.save() 
     1161>>> obj 
     1162<Product: teddy-bear-blue> 
     1163>>> form = ProductForm({'slug': 'teddy-bear-blue'}) 
     1164>>> form.is_valid() 
     1165False 
     1166>>> form._errors 
     1167{'slug': [u'Product with this Slug already exists.']} 
     1168>>> form = ProductForm({'slug': 'teddy-bear-blue'}, instance=obj) 
     1169>>> form.is_valid() 
     1170True 
     1171 
     1172# ModelForm test of unique_together constraint 
     1173>>> class PriceForm(ModelForm): 
     1174...     class Meta: 
     1175...         model = Price 
     1176>>> form = PriceForm({'price': '6.00', 'quantity': '1'}) 
     1177>>> form.is_valid() 
     1178True 
     1179>>> form.save() 
     1180<Price: 1 for 6.00> 
     1181>>> form = PriceForm({'price': '6.00', 'quantity': '1'}) 
     1182>>> form.is_valid() 
     1183False 
     1184>>> form._errors 
     1185{'__all__': [u'Price with this Price and Quantity already exists.']} 
     1186 
    11351187# Choices on CharField and IntegerField 
    1136  
    11371188>>> class ArticleForm(ModelForm): 
    11381189...     class Meta: