Ticket #12521: 12521.diff

File 12521.diff, 18.9 KB (added by jkocherhans, 14 years ago)

This combines my fixes, Honza fixes, and Ivan's tests. Almost there.

  • django/contrib/auth/forms.py

    diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
    index dbc55ca..e9a7fb2 100644
    a b  
    1 from django.contrib.auth.models import User, UNUSABLE_PASSWORD
     1from django.contrib.auth.models import User
    22from django.contrib.auth import authenticate
    33from django.contrib.auth.tokens import default_token_generator
    44from django.contrib.sites.models import Site
    class UserCreationForm(forms.ModelForm):  
    2121        model = User
    2222        fields = ("username",)
    2323
    24     def clean(self):
    25         # Fill the password field so model validation won't complain about it
    26         # being blank. We'll set it with the real value below.
    27         self.instance.password = UNUSABLE_PASSWORD
    28         super(UserCreationForm, self).clean()
    29 
    3024    def clean_username(self):
    3125        username = self.cleaned_data["username"]
    3226        try:
    class UserCreationForm(forms.ModelForm):  
    4337        self.instance.set_password(password1)
    4438        return password2
    4539
     40    def save(self, *args, **kwargs):
     41        self.instance.set_password(self.cleaned_data["password1"])
     42        return super(UserCreationForm, self).save(*args, **kwargs)
     43
    4644class UserChangeForm(forms.ModelForm):
    4745    username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$',
    4846        help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."),
  • django/core/exceptions.py

    diff --git a/django/core/exceptions.py b/django/core/exceptions.py
    index fee7db4..29da67c 100644
    a b class FieldError(Exception):  
    3333    pass
    3434
    3535NON_FIELD_ERRORS = '__all__'
    36 class BaseValidationError(Exception):
     36class ValidationError(Exception):
    3737    """An error while validating data."""
    3838    def __init__(self, message, code=None, params=None):
    3939        import operator
    class BaseValidationError(Exception):  
    6363        if hasattr(self, 'message_dict'):
    6464            return repr(self.message_dict)
    6565        return repr(self.messages)
    66 
    67 class ValidationError(BaseValidationError):
    68     pass
    69 
    70 class UnresolvableValidationError(BaseValidationError):
    71     """Validation error that cannot be resolved by the user."""
    72     pass
    73 
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    index 06db7cc..cfe7fec 100644
    a b class Model(object):  
    649649        not be associated with a particular field; it will have a special-case
    650650        association with the field defined by NON_FIELD_ERRORS.
    651651        """
    652         self.validate_unique()
     652        pass
    653653
    654     def validate_unique(self):
    655         unique_checks, date_checks = self._get_unique_checks()
     654    def validate_unique(self, exclude=None):
     655        if exclude is None:
     656            exclude = []
     657        unique_checks, date_checks = self._get_unique_checks(exclude)
    656658
    657659        errors = self._perform_unique_checks(unique_checks)
    658660        date_errors = self._perform_date_checks(date_checks)
    class Model(object):  
    663665        if errors:
    664666            raise ValidationError(errors)
    665667
    666     def _get_unique_checks(self):
    667         from django.db.models.fields import FieldDoesNotExist, Field as ModelField
     668    def _get_unique_checks(self, exclude=None):
     669        if exclude is None:
     670            exclude = []
     671        unique_checks = []
     672        # Gather a list of checks to perform. Since validate_unique could be
     673        # called from a ModelForm, some fields may have been excluded; we can't
     674        # perform a unique check on a model that is missing fields involved
     675        # in that check. It also does not make sense to check data that didn't
     676        # validate, and since NULL does not equal NULL in SQL we should not do
     677        # any unique checking for NULL values.
     678        for check in self._meta.unique_together:
     679            for name in check:
     680                # If this is an excluded field, short circuit and don't a unique_check.
     681                if name in exclude:
     682                    break
     683            # Also, Skip fields that don't have a value, we can't check for
     684            # their uniqueness.
     685            if getattr(self, name, None) is None:
     686                break
     687            else:
     688                unique_checks.append(check)
    668689
    669         unique_checks = list(self._meta.unique_together)
    670         # these are checks for the unique_for_<date/year/month>
     690        # These are checks for the unique_for_<date/year/month>.
    671691        date_checks = []
    672692
    673693        # Gather a list of checks for fields declared as unique and add them to
    674694        # the list of checks. Again, skip empty fields and any that did not validate.
    675695        for f in self._meta.fields:
    676696            name = f.name
     697            # Don't add excluded fields to unique for date checks.
     698            if name in exclude:
     699                continue
    677700            if f.unique:
    678701                unique_checks.append((name,))
    679702            if f.unique_for_date:
    class Model(object):  
    684707                date_checks.append(('month', name, f.unique_for_month))
    685708        return unique_checks, date_checks
    686709
    687 
    688710    def _perform_unique_checks(self, unique_checks):
    689711        errors = {}
    690712
    class Model(object):  
    781803                'field_label': unicode(field_labels)
    782804            }
    783805
    784     def full_validate(self, exclude=[]):
     806    def full_validate(self, exclude=None):
    785807        """
    786808        Cleans all fields and raises ValidationError containing message_dict
    787809        of all validation errors if any occur.
    788810        """
     811        if exclude is None:
     812            exclude = []
    789813        errors = {}
    790814        for f in self._meta.fields:
    791815            if f.name in exclude:
    class Model(object):  
    795819            except ValidationError, e:
    796820                errors[f.name] = e.messages
    797821
    798         # Form.clean() is run even if other validation fails, so do the
    799         # same with Model.validate() for consistency.
    800822        try:
    801             self.validate()
     823            self.validate_unique(exclude=exclude.extend(errors.keys()))
    802824        except ValidationError, e:
    803825            if hasattr(e, 'message_dict'):
    804826                if errors:
    805827                    for k, v in e.message_dict.items():
    806                         errors.set_default(k, []).extend(v)
     828                        errors.setdefault(k, []).extend(v)
    807829                else:
    808830                    errors = e.message_dict
    809831            else:
    810832                errors[NON_FIELD_ERRORS] = e.messages
    811833
     834        # Form.clean() is run even if other validation fails, so do the
     835        # same with Model.validate() for consistency.
     836        # However, do not run Model.validate() on incomplete forms when
     837        # creating new instance.
     838        if not (exclude and getattr(self, '_adding', True)):
     839            try:
     840                self.validate()
     841            except ValidationError, e:
     842                if hasattr(e, 'message_dict'):
     843                    if errors:
     844                        for k, v in e.message_dict.items():
     845                            errors.setdefault(k, []).extend(v)
     846                    else:
     847                        errors = e.message_dict
     848                else:
     849                    errors[NON_FIELD_ERRORS] = e.messages
     850
    812851        if errors:
    813852            raise ValidationError(errors)
    814853
  • django/forms/models.py

    diff --git a/django/forms/models.py b/django/forms/models.py
    index ff20c93..5435f2a 100644
    a b from django.utils.datastructures import SortedDict  
    99from django.utils.text import get_text_list, capfirst
    1010from django.utils.translation import ugettext_lazy as _, ugettext
    1111
    12 from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, UnresolvableValidationError
     12from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
    1313from django.core.validators import EMPTY_VALUES
    1414from util import ErrorList
    1515from forms import BaseForm, get_declared_fields
    class BaseModelForm(BaseForm):  
    249249        opts = self._meta
    250250        self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
    251251        try:
    252             self.instance.full_validate(exclude=self._errors.keys())
     252            # For backwards-compatibility, several types of fields need to be
     253            # excluded from model validation. See the following tickets for
     254            # details: #12507, #12521, #12553
     255            exclude = []
     256            for f in self.instance._meta.fields:
     257                field = f.name
     258                # Exclude fields that aren't on the form. The developer may be
     259                # adding these values to the model after form validation.
     260                if field not in self.fields:
     261                    exclude.append(field)
     262                # Exclude fields that failed form validation.
     263                elif field in self._errors.keys():
     264                    exclude.append(field)
     265                # Exclude empty fields that are not required by the form. Model
     266                # validation might complain that they are null, but the
     267                # developer is reponsible for setting their value after calling
     268                # form validation..
     269                elif ((not self.fields[field].required) and
     270                       self.cleaned_data.get(field, None) in EMPTY_VALUES):
     271                    exclude.append(field)
     272            self.instance.full_validate(exclude=exclude)
    253273        except ValidationError, e:
    254274            for k, v in e.message_dict.items():
    255275                if k != NON_FIELD_ERRORS:
    256276                    self._errors.setdefault(k, ErrorList()).extend(v)
    257 
    258277                    # Remove the data from the cleaned_data dict since it was invalid
    259278                    if k in self.cleaned_data:
    260279                        del self.cleaned_data[k]
    261 
    262280            if NON_FIELD_ERRORS in e.message_dict:
    263281                raise ValidationError(e.message_dict[NON_FIELD_ERRORS])
    264 
    265             # If model validation threw errors for fields that aren't on the
    266             # form, the the errors cannot be corrected by the user. Displaying
    267             # those errors would be pointless, so raise another type of
    268             # exception that *won't* be caught and displayed by the form.
    269             if set(e.message_dict.keys()) - set(self.fields.keys() + [NON_FIELD_ERRORS]):
    270                 raise UnresolvableValidationError(e.message_dict)
    271 
    272 
    273282        return self.cleaned_data
    274283
    275284    def save(self, commit=True):
  • tests/modeltests/model_forms/models.py

    diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
    index ba59f9a..cb2aa14 100644
    a b True  
    11141114
    11151115>>> instance.delete()
    11161116
     1117# Test the non-required FileField
     1118>>> f = TextFileForm(data={'description': u'Assistance'})
     1119>>> f.fields['file'].required = False
     1120>>> f.is_valid()
     1121True
     1122>>> instance = f.save()
     1123>>> instance.file
     1124<FieldFile: None>
     1125
    11171126>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance)
    11181127>>> f.is_valid()
    11191128True
    True  
    11521161>>> class BigIntForm(forms.ModelForm):
    11531162...     class Meta:
    11541163...         model = BigInt
    1155 ... 
     1164...
    11561165>>> bif = BigIntForm({'biggie': '-9223372036854775808'})
    11571166>>> bif.is_valid()
    11581167True
    False  
    14251434>>> form._errors
    14261435{'__all__': [u'Price with this Price and Quantity already exists.']}
    14271436
    1428 # This form is never valid because quantity is blank=False.
     1437This Price instance generated by this form is not valid because the quantity
     1438field is required, but the form is valid because the field is excluded from
     1439the form. This is for backwards compatibility.
     1440
    14291441>>> class PriceForm(ModelForm):
    14301442...     class Meta:
    14311443...         model = Price
    14321444...         exclude = ('quantity',)
    14331445>>> form = PriceForm({'price': '6.00'})
    14341446>>> form.is_valid()
     1447True
     1448>>> price = form.save(commit=False)
     1449>>> price.full_validate()
    14351450Traceback (most recent call last):
    14361451  ...
    1437 UnresolvableValidationError: {'quantity': [u'This field cannot be null.']}
     1452ValidationError: {'quantity': [u'This field cannot be null.']}
     1453
     1454The form should not validate fields that it doesn't contain even if they are
     1455specified using 'fields', not 'exclude'.
     1456...     class Meta:
     1457...         model = Price
     1458...         fields = ('price',)
     1459>>> form = PriceForm({'price': '6.00'})
     1460>>> form.is_valid()
     1461True
     1462
     1463The form should still have an instance of a model that is not complete and
     1464not saved into a DB yet.
     1465
     1466>>> form.instance.price
     1467Decimal('6.00')
     1468>>> form.instance.quantity is None
     1469True
     1470>>> form.instance.pk is None
     1471True
    14381472
    14391473# Unique & unique together with null values
    14401474>>> class BookForm(ModelForm):
  • tests/modeltests/model_formsets/models.py

    diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
    index 5eab202..f41908b 100644
    a b False  
    10311031>>> formset._non_form_errors
    10321032[u'Please correct the duplicate data for price and quantity, which must be unique.']
    10331033
     1034# Only the price field is specified, this should skip any unique checks since
     1035# the unique_together is not fulfilled. This will fail with a KeyError if broken.
     1036>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2)
     1037>>> data = {
     1038...     'form-TOTAL_FORMS': '2',
     1039...     'form-INITIAL_FORMS': '0',
     1040...     'form-0-price': '24',
     1041...     'form-1-price': '24',
     1042... }
     1043>>> formset = FormSet(data)
     1044>>> formset.is_valid()
     1045True
     1046
    10341047>>> FormSet = inlineformset_factory(Author, Book, extra=0)
    10351048>>> author = Author.objects.order_by('id')[0]
    10361049>>> book_ids = author.book_set.values_list('id', flat=True)
  • tests/modeltests/validation/test_unique.py

    diff --git a/tests/modeltests/validation/test_unique.py b/tests/modeltests/validation/test_unique.py
    index cbb56aa..3aea99d 100644
    a b  
    11import unittest
     2import datetime
    23from django.conf import settings
    34from django.db import connection
    45from models import CustomPKModel, UniqueTogetherModel, UniqueFieldsModel, UniqueForDateModel, ModelToValidate
    class GetUniqueCheckTests(unittest.TestCase):  
    1314        )
    1415
    1516    def test_unique_together_gets_picked_up(self):
    16         m = UniqueTogetherModel()
     17        # Give the fields values, otherwise they won't be picked up in
     18        # unique checks.
     19        m = UniqueTogetherModel(
     20            cfield='test',
     21            ifield=1,
     22            efield='test@example.com'
     23        )
    1724        self.assertEqual(
    1825            ([('ifield', 'cfield',),('ifield', 'efield'), ('id',), ], []),
    1926            m._get_unique_checks()
    class GetUniqueCheckTests(unittest.TestCase):  
    2431        self.assertEqual(([('my_pk_field',)], []), m._get_unique_checks())
    2532
    2633    def test_unique_for_date_gets_picked_up(self):
    27         m = UniqueForDateModel()
     34        # Give the fields values, otherwise they won't be picked up in
     35        # unique checks.
     36        m = UniqueForDateModel(
     37            start_date=datetime.date(2010, 1, 9),
     38            end_date=datetime.datetime(2010, 1, 9, 9, 0, 0),
     39            count=1,
     40            order=1,
     41            name='test'
     42        )
    2843        self.assertEqual((
    29                 [('id',)],
    30                 [('date', 'count', 'start_date'), ('year', 'count', 'end_date'), ('month', 'order', 'end_date')]
     44            [('id',)],
     45            [('date', 'count', 'start_date'), ('year', 'count', 'end_date'), ('month', 'order', 'end_date')]
    3146            ), m._get_unique_checks()
    3247        )
    3348
    class PerformUniqueChecksTest(unittest.TestCase):  
    5671        mtv = ModelToValidate(number=10, name='Some Name')
    5772        mtv.full_validate()
    5873        self.assertEqual(l, len(connection.queries))
     74
  • tests/modeltests/validation/tests.py

    diff --git a/tests/modeltests/validation/tests.py b/tests/modeltests/validation/tests.py
    index c00070b..7bf3994 100644
    a b class BaseModelValidationTests(ValidationTestCase):  
    1919        mtv = ModelToValidate(number=10, name='Some Name')
    2020        self.assertEqual(None, mtv.full_validate())
    2121
    22     def test_custom_validate_method_is_called(self):
     22    def test_custom_validate_method(self):
    2323        mtv = ModelToValidate(number=11)
    24         self.assertFailsValidation(mtv.full_validate, [NON_FIELD_ERRORS, 'name'])
     24        # model.validate() will not be called on unsaved models that otherwise
     25        # fail validation.
     26        self.assertFailsValidation(mtv.full_validate, ['name'])
     27        mtv.name = 'test'
     28        # model.validate() *should* be called if the model otherwise *does* validate
     29        self.assertFailsValidation(mtv.full_validate, [NON_FIELD_ERRORS])
    2530
    2631    def test_wrong_FK_value_raises_error(self):
    2732        mtv=ModelToValidate(number=10, name='Some Name', parent_id=3)
  • tests/regressiontests/admin_views/tests.py

    diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
    index 899fff8..351974b 100644
    a b import re  
    44import datetime
    55from django.core.files import temp as tempfile
    66from django.test import TestCase
    7 from django.contrib.auth.models import User, Permission
     7from django.contrib.auth import admin # Register auth models with the admin.
     8from django.contrib.auth.models import User, Permission, UNUSABLE_PASSWORD
    89from django.contrib.contenttypes.models import ContentType
    910from django.contrib.admin.models import LogEntry, DELETION
    1011from django.contrib.admin.sites import LOGIN_FORM_KEY
    class ReadonlyTest(TestCase):  
    17531754        self.assertEqual(Post.objects.count(), 2)
    17541755        p = Post.objects.order_by('-id')[0]
    17551756        self.assertEqual(p.posted, datetime.date.today())
     1757
     1758class IncompleteFormTest(TestCase):
     1759    """
     1760    Tests validation of a ModelForm that doesn't explicitly have all data
     1761    corresponding to model fields. Model validation shouldn't fail
     1762    such a forms.
     1763    """
     1764    fixtures = ['admin-views-users.xml']
     1765
     1766    def setUp(self):
     1767       self.client.login(username='super', password='secret')
     1768
     1769    def tearDown(self):
     1770       self.client.logout()
     1771
     1772    def test_user_creation(self):
     1773       response = self.client.post('/test_admin/admin/auth/user/add/', {
     1774           'username': 'newuser',
     1775           'password1': 'newpassword',
     1776           'password2': 'newpassword',
     1777       })
     1778       new_user = User.objects.order_by('-id')[0]
     1779       self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk)
     1780       self.assertNotEquals(new_user.password, UNUSABLE_PASSWORD)
     1781
     1782    def test_password_mismatch(self):
     1783       response = self.client.post('/test_admin/admin/auth/user/add/', {
     1784           'username': 'newuser',
     1785           'password1': 'newpassword',
     1786           'password2': 'mismatch',
     1787       })
     1788       self.assertEquals(response.status_code, 200)
     1789       self.assert_('password' not in response.context['form'].errors)
     1790       self.assertFormError(response, 'form', 'password2', ["The two password fields didn't match."])
Back to Top