Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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

…tion 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
  • Loading branch information...
commit 23c12c9c2b75fa703ca99485c6da8246f57fa32b 1 parent 655b602
Russell Keith-Magee authored April 30, 2009
123  django/forms/models.py
@@ -219,6 +219,7 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
219 219
             object_data.update(initial)
220 220
         super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
221 221
                                             error_class, label_suffix, empty_permitted)
  222
+
222 223
     def clean(self):
223 224
         self.validate_unique()
224 225
         return self.cleaned_data
@@ -233,16 +234,16 @@ def validate_unique(self):
233 234
         # not make sense to check data that didn't validate, and since NULL does not
234 235
         # equal NULL in SQL we should not do any unique checking for NULL values.
235 236
         unique_checks = []
  237
+        # these are checks for the unique_for_<date/year/month>
  238
+        date_checks = []
236 239
         for check in self.instance._meta.unique_together[:]:
237 240
             fields_on_form = [field for field in check if self.cleaned_data.get(field) is not None]
238 241
             if len(fields_on_form) == len(check):
239 242
                 unique_checks.append(check)
240 243
 
241  
-        form_errors = []
242  
-
243 244
         # Gather a list of checks for fields declared as unique and add them to
244 245
         # the list of checks. Again, skip empty fields and any that did not validate.
245  
-        for name, field in self.fields.items():
  246
+        for name in self.fields:
246 247
             try:
247 248
                 f = self.instance._meta.get_field_by_name(name)[0]
248 249
             except FieldDoesNotExist:
@@ -254,10 +255,39 @@ def validate_unique(self):
254 255
                 # get_field_by_name found it, but it is not a Field so do not proceed
255 256
                 # to use it as if it were.
256 257
                 continue
257  
-            if f.unique and self.cleaned_data.get(name) is not None:
  258
+            if self.cleaned_data.get(name) is None:
  259
+                continue
  260
+            if f.unique:
258 261
                 unique_checks.append((name,))
  262
+            if f.unique_for_date and self.cleaned_data.get(f.unique_for_date) is not None:
  263
+                date_checks.append(('date', name, f.unique_for_date))
  264
+            if f.unique_for_year and self.cleaned_data.get(f.unique_for_year) is not None:
  265
+                date_checks.append(('year', name, f.unique_for_year))
  266
+            if f.unique_for_month and self.cleaned_data.get(f.unique_for_month) is not None:
  267
+                date_checks.append(('month', name, f.unique_for_month))
  268
+
  269
+        form_errors = []
  270
+        bad_fields = set()
  271
+
  272
+        field_errors, global_errors = self._perform_unique_checks(unique_checks)
  273
+        bad_fields.union(field_errors)
  274
+        form_errors.extend(global_errors)
  275
+
  276
+        field_errors, global_errors = self._perform_date_checks(date_checks)
  277
+        bad_fields.union(field_errors)
  278
+        form_errors.extend(global_errors)
  279
+
  280
+        for field_name in bad_fields:
  281
+            del self.cleaned_data[field_name]
  282
+        if form_errors:
  283
+            # Raise the unique together errors since they are considered
  284
+            # form-wide.
  285
+            raise ValidationError(form_errors)
259 286
 
  287
+    def _perform_unique_checks(self, unique_checks):
260 288
         bad_fields = set()
  289
+        form_errors = []
  290
+
261 291
         for unique_check in unique_checks:
262 292
             # Try to look up an existing object with the same values as this
263 293
             # object's values for all the unique field.
@@ -276,39 +306,74 @@ def validate_unique(self):
276 306
             # This cute trick with extra/values is the most efficient way to
277 307
             # tell if a particular query returns any results.
278 308
             if qs.extra(select={'a': 1}).values('a').order_by():
279  
-                model_name = capfirst(self.instance._meta.verbose_name)
280  
-
281  
-                # A unique field
282 309
                 if len(unique_check) == 1:
283  
-                    field_name = unique_check[0]
284  
-                    field_label = self.fields[field_name].label
285  
-                    # Insert the error into the error dict, very sneaky
286  
-                    self._errors[field_name] = ErrorList([
287  
-                        _(u"%(model_name)s with this %(field_label)s already exists.") % \
288  
-                        {'model_name': unicode(model_name),
289  
-                         'field_label': unicode(field_label)}
290  
-                    ])
291  
-                # unique_together
  310
+                    self._errors[unique_check[0]] = ErrorList([self.unique_error_message(unique_check)])
292 311
                 else:
293  
-                    field_labels = [self.fields[field_name].label for field_name in unique_check]
294  
-                    field_labels = get_text_list(field_labels, _('and'))
295  
-                    form_errors.append(
296  
-                        _(u"%(model_name)s with this %(field_label)s already exists.") % \
297  
-                        {'model_name': unicode(model_name),
298  
-                         'field_label': unicode(field_labels)}
299  
-                    )
  312
+                    form_errors.append(self.unique_error_message(unique_check))
300 313
 
301 314
                 # Mark these fields as needing to be removed from cleaned data
302 315
                 # later.
303 316
                 for field_name in unique_check:
304 317
                     bad_fields.add(field_name)
  318
+        return bad_fields, form_errors
305 319
 
306  
-        for field_name in bad_fields:
307  
-            del self.cleaned_data[field_name]
308  
-        if form_errors:
309  
-            # Raise the unique together errors since they are considered
310  
-            # form-wide.
311  
-            raise ValidationError(form_errors)
  320
+    def _perform_date_checks(self, date_checks):
  321
+        bad_fields = set()
  322
+        for lookup_type, field, unique_for in date_checks:
  323
+            lookup_kwargs = {}
  324
+            # there's a ticket to add a date lookup, we can remove this special
  325
+            # case if that makes it's way in
  326
+            if lookup_type == 'date':
  327
+                date = self.cleaned_data[unique_for]
  328
+                lookup_kwargs['%s__day' % unique_for] = date.day
  329
+                lookup_kwargs['%s__month' % unique_for] = date.month
  330
+                lookup_kwargs['%s__year' % unique_for] = date.year
  331
+            else:
  332
+                lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(self.cleaned_data[unique_for], lookup_type)
  333
+            lookup_kwargs[field] = self.cleaned_data[field]
  334
+
  335
+            qs = self.instance.__class__._default_manager.filter(**lookup_kwargs)
  336
+            # Exclude the current object from the query if we are editing an
  337
+            # instance (as opposed to creating a new one)
  338
+            if self.instance.pk is not None:
  339
+                qs = qs.exclude(pk=self.instance.pk)
  340
+
  341
+            # This cute trick with extra/values is the most efficient way to
  342
+            # tell if a particular query returns any results.
  343
+            if qs.extra(select={'a': 1}).values('a').order_by():
  344
+                self._errors[field] = ErrorList([
  345
+                    self.date_error_message(lookup_type, field, unique_for)
  346
+                ])
  347
+                bad_fields.add(field)
  348
+        return bad_fields, []
  349
+
  350
+    def date_error_message(self, lookup_type, field, unique_for):
  351
+        return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % {
  352
+            'field_name': unicode(self.fields[field].label),
  353
+            'date_field': unicode(self.fields[unique_for].label),
  354
+            'lookup': lookup_type,
  355
+        }
  356
+
  357
+    def unique_error_message(self, unique_check):
  358
+        model_name = capfirst(self.instance._meta.verbose_name)
  359
+
  360
+        # A unique field
  361
+        if len(unique_check) == 1:
  362
+            field_name = unique_check[0]
  363
+            field_label = self.fields[field_name].label
  364
+            # Insert the error into the error dict, very sneaky
  365
+            return _(u"%(model_name)s with this %(field_label)s already exists.") %  {
  366
+                'model_name': unicode(model_name),
  367
+                'field_label': unicode(field_label)
  368
+            }
  369
+        # unique_together
  370
+        else:
  371
+            field_labels = [self.fields[field_name].label for field_name in unique_check]
  372
+            field_labels = get_text_list(field_labels, _('and'))
  373
+            return _(u"%(model_name)s with this %(field_label)s already exists.") %  {
  374
+                'model_name': unicode(model_name),
  375
+                'field_label': unicode(field_labels)
  376
+            }
312 377
 
313 378
     def save(self, commit=True):
314 379
         """
53  tests/modeltests/model_forms/models.py
@@ -102,7 +102,7 @@ class ImageFile(models.Model):
102 102
     def custom_upload_path(self, filename):
103 103
         path = self.path or 'tests'
104 104
         return '%s/%s' % (path, filename)
105  
-    
  105
+
106 106
     description = models.CharField(max_length=20)
107 107
     try:
108 108
         # If PIL is available, try testing PIL.
@@ -155,19 +155,28 @@ class Book(models.Model):
155 155
     title = models.CharField(max_length=40)
156 156
     author = models.ForeignKey(Writer, blank=True, null=True)
157 157
     special_id = models.IntegerField(blank=True, null=True, unique=True)
158  
-    
  158
+
159 159
     class Meta:
160 160
         unique_together = ('title', 'author')
161  
-        
  161
+
162 162
 class ExplicitPK(models.Model):
163 163
     key = models.CharField(max_length=20, primary_key=True)
164 164
     desc = models.CharField(max_length=20, blank=True, unique=True)
165 165
     class Meta:
166 166
         unique_together = ('key', 'desc')
167  
-    
  167
+
168 168
     def __unicode__(self):
169 169
         return self.key
170 170
 
  171
+class Post(models.Model):
  172
+    title = models.CharField(max_length=50, unique_for_date='posted', blank=True)
  173
+    slug = models.CharField(max_length=50, unique_for_year='posted', blank=True)
  174
+    subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True)
  175
+    posted = models.DateField()
  176
+
  177
+    def __unicode__(self):
  178
+        return self.name
  179
+
171 180
 __test__ = {'API_TESTS': """
172 181
 >>> from django import forms
173 182
 >>> from django.forms.models import ModelForm, model_to_dict
@@ -1253,8 +1262,8 @@ def __unicode__(self):
1253 1262
 True
1254 1263
 
1255 1264
 # Unique & unique together with null values
1256  
->>> class BookForm(ModelForm): 
1257  
-...     class Meta: 
  1265
+>>> class BookForm(ModelForm):
  1266
+...     class Meta:
1258 1267
 ...        model = Book
1259 1268
 >>> w = Writer.objects.get(name='Mike Royko')
1260 1269
 >>> form = BookForm({'title': 'I May Be Wrong But I Doubt It', 'author' : w.pk})
@@ -1349,6 +1358,38 @@ def __unicode__(self):
1349 1358
 >>> core.parent
1350 1359
 <Inventory: Pear>
1351 1360
 
  1361
+### Validation on unique_for_date
  1362
+
  1363
+>>> p = Post.objects.create(title="Django 1.0 is released", slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3))
  1364
+>>> class PostForm(ModelForm):
  1365
+...     class Meta:
  1366
+...         model = Post
  1367
+
  1368
+>>> f = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'})
  1369
+>>> f.is_valid()
  1370
+False
  1371
+>>> f.errors
  1372
+{'title': [u'Title must be unique for Posted date.']}
  1373
+>>> f = PostForm({'title': "Work on Django 1.1 begins", 'posted': '2008-09-03'})
  1374
+>>> f.is_valid()
  1375
+True
  1376
+>>> f = PostForm({'title': "Django 1.0 is released", 'posted': '2008-09-04'})
  1377
+>>> f.is_valid()
  1378
+True
  1379
+>>> f = PostForm({'slug': "Django 1.0", 'posted': '2008-01-01'})
  1380
+>>> f.is_valid()
  1381
+False
  1382
+>>> f.errors
  1383
+{'slug': [u'Slug must be unique for Posted year.']}
  1384
+>>> f = PostForm({'subtitle': "Finally", 'posted': '2008-09-30'})
  1385
+>>> f.is_valid()
  1386
+False
  1387
+>>> f.errors
  1388
+{'subtitle': [u'Subtitle must be unique for Posted month.']}
  1389
+>>> f = PostForm({'subtitle': "Finally", "title": "Django 1.0 is released", "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p)
  1390
+>>> f.is_valid()
  1391
+True
  1392
+
1352 1393
 # Clean up
1353 1394
 >>> import shutil
1354 1395
 >>> shutil.rmtree(temp_storage_dir)

0 notes on commit 23c12c9

Please sign in to comment.
Something went wrong with that request. Please try again.