diff --git a/django/db/models/base.py b/django/db/models/base.py index 18a684d715c..84259f1a6d5 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -648,7 +648,7 @@ class Model(object): called from a ModelForm, some fields may have been excluded; we can't perform a unique check on a model that is missing fields involved in that check. - Fields that did not validate should also be exluded, but they need + Fields that did not validate should also be excluded, but they need to be passed in via the exclude argument. """ if exclude is None: @@ -740,6 +740,8 @@ class Model(object): # there's a ticket to add a date lookup, we can remove this special # case if that makes it's way in date = getattr(self, unique_for) + if date is None: + continue if lookup_type == 'date': lookup_kwargs['%s__day' % unique_for] = date.day lookup_kwargs['%s__month' % unique_for] = date.month diff --git a/tests/modeltests/model_forms/mforms.py b/tests/modeltests/model_forms/mforms.py index aef763ef620..140e8073e79 100644 --- a/tests/modeltests/model_forms/mforms.py +++ b/tests/modeltests/model_forms/mforms.py @@ -1,7 +1,8 @@ from django import forms from django.forms import ModelForm -from models import Product, Price, Book, DerivedBook, ExplicitPK, Post, DerivedPost, Writer +from models import (Product, Price, Book, DerivedBook, ExplicitPK, Post, + DerivedPost, Writer, FlexibleDatePost) class ProductForm(ModelForm): class Meta: @@ -37,3 +38,7 @@ class CustomWriterForm(ModelForm): class Meta: model = Writer + +class FlexDatePostForm(ModelForm): + class Meta: + model = FlexibleDatePost diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index 7ded82bb7c0..b9b8d16c071 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -236,6 +236,12 @@ class CustomFieldForExclusionModel(models.Model): name = models.CharField(max_length=10) markup = MarkupField() +class FlexibleDatePost(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(blank=True, null=True) + __test__ = {'API_TESTS': """ >>> from django import forms >>> from django.forms.models import ModelForm, model_to_dict diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py index c5647c714f4..33918ee88cd 100644 --- a/tests/modeltests/model_forms/tests.py +++ b/tests/modeltests/model_forms/tests.py @@ -1,9 +1,10 @@ import datetime from django.test import TestCase from django import forms -from models import Category, Writer, Book, DerivedBook, Post -from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm, - ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm) +from models import Category, Writer, Book, DerivedBook, Post, FlexibleDatePost +from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm, + ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm, + FlexDatePostForm) class IncompleteCategoryFormWithFields(forms.ModelForm): @@ -183,3 +184,16 @@ class UniqueTest(TestCase): "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p) self.assertTrue(form.is_valid()) + def test_unique_for_date_with_nullable_date(self): + p = FlexibleDatePost.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) + + form = FlexDatePostForm({'title': "Django 1.0 is released"}) + self.assertTrue(form.is_valid()) + form = FlexDatePostForm({'slug': "Django 1.0"}) + self.assertTrue(form.is_valid()) + form = FlexDatePostForm({'subtitle': "Finally"}) + self.assertTrue(form.is_valid()) + form = FlexDatePostForm({'subtitle': "Finally", "title": "Django 1.0 is released", + "slug": "Django 1.0"}, instance=p) + self.assertTrue(form.is_valid()) diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py index dd429368857..861d1440fe7 100644 --- a/tests/modeltests/validation/models.py +++ b/tests/modeltests/validation/models.py @@ -63,3 +63,18 @@ class Article(models.Model): def clean(self): if self.pub_date is None: self.pub_date = datetime.now() + +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 + +class FlexibleDatePost(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(blank=True, null=True) diff --git a/tests/modeltests/validation/test_unique.py b/tests/modeltests/validation/test_unique.py index d239b8480cf..223aa021e4f 100644 --- a/tests/modeltests/validation/test_unique.py +++ b/tests/modeltests/validation/test_unique.py @@ -1,12 +1,13 @@ import datetime from django.conf import settings +from django.core.exceptions import ValidationError from django.db import connection from django.test import TestCase from django.utils import unittest from models import (CustomPKModel, UniqueTogetherModel, UniqueFieldsModel, - UniqueForDateModel, ModelToValidate) + UniqueForDateModel, ModelToValidate, Post, FlexibleDatePost) class GetUniqueCheckTests(unittest.TestCase): @@ -76,3 +77,75 @@ class PerformUniqueChecksTest(TestCase): mtv = ModelToValidate(number=10, name='Some Name') mtv.full_clean() self.assertNumQueries(0, test) + + def test_unique_for_date(self): + p1 = Post.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) + + p = Post(title="Django 1.0 is released", posted=datetime.date(2008, 9, 3)) + try: + p.full_clean() + except ValidationError, e: + self.assertEqual(e.message_dict, {'title': [u'Title must be unique for Posted date.']}) + else: + self.fail('unique_for_date checks should catch this.') + + # Should work without errors + p = Post(title="Work on Django 1.1 begins", posted=datetime.date(2008, 9, 3)) + p.full_clean() + + # Should work without errors + p = Post(title="Django 1.0 is released", posted=datetime.datetime(2008, 9,4)) + p.full_clean() + + p = Post(slug="Django 1.0", posted=datetime.datetime(2008, 1, 1)) + try: + p.full_clean() + except ValidationError, e: + self.assertEqual(e.message_dict, {'slug': [u'Slug must be unique for Posted year.']}) + else: + self.fail('unique_for_year checks should catch this.') + + p = Post(subtitle="Finally", posted=datetime.datetime(2008, 9, 30)) + try: + p.full_clean() + except ValidationError, e: + self.assertEqual(e.message_dict, {'subtitle': [u'Subtitle must be unique for Posted month.']}) + else: + self.fail('unique_for_month checks should catch this.') + + p = Post(title="Django 1.0 is released") + try: + p.full_clean() + except ValidationError, e: + self.assertEqual(e.message_dict, {'posted': [u'This field cannot be null.']}) + else: + self.fail("Model validation shouldn't allow an absent value for a DateField without null=True.") + + def test_unique_for_date_with_nullable_date(self): + p1 = FlexibleDatePost.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) + + p = FlexibleDatePost(title="Django 1.0 is released") + try: + p.full_clean() + except ValidationError, e: + self.fail("unique_for_date checks shouldn't trigger when the associated DateField is None.") + except: + self.fail("unique_for_date checks shouldn't explode when the associated DateField is None.") + + p = FlexibleDatePost(slug="Django 1.0") + try: + p.full_clean() + except ValidationError, e: + self.fail("unique_for_year checks shouldn't trigger when the associated DateField is None.") + except: + self.fail("unique_for_year checks shouldn't explode when the associated DateField is None.") + + p = FlexibleDatePost(subtitle="Finally") + try: + p.full_clean() + except ValidationError, e: + self.fail("unique_for_month checks shouldn't trigger when the associated DateField is None.") + except: + self.fail("unique_for_month checks shouldn't explode when the associated DateField is None.")