Ticket #12521: 12521.2.diff

File 12521.2.diff, 24.4 KB (added by jkocherhans, 14 years ago)

Similar to the last patch, but splits full_validate into validate_fields, validate_unique, validate.

  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index 6dc707e..8f68eb1 100644
    a b class ModelAdmin(BaseModelAdmin):  
    579579        """
    580580        messages.info(request, message)
    581581
    582     def save_form(self, request, form, change, commit=False):
     582    def save_form(self, request, form, change):
    583583        """
    584584        Given a ModelForm return an unsaved instance. ``change`` is True if
    585585        the object is being changed, and False if it's being added.
    586586        """
    587         return form.save(commit=commit)
     587        return form.save(commit=False)
    588588
    589589    def save_model(self, request, obj, form, change):
    590590        """
    class ModelAdmin(BaseModelAdmin):  
    758758        if request.method == 'POST':
    759759            form = ModelForm(request.POST, request.FILES)
    760760            if form.is_valid():
    761                 # Save the object, even if inline formsets haven't been
    762                 # validated yet. We need to pass the valid model to the
    763                 # formsets for validation. If the formsets do not validate, we
    764                 # will delete the object.
    765                 new_object = self.save_form(request, form, change=False, commit=True)
     761                new_object = self.save_form(request, form, change=False)
    766762                form_validated = True
    767763            else:
    768764                form_validated = False
    class ModelAdmin(BaseModelAdmin):  
    779775                                  prefix=prefix, queryset=inline.queryset(request))
    780776                formsets.append(formset)
    781777            if all_valid(formsets) and form_validated:
     778                self.save_model(request, new_object, form, change=False)
     779                form.save_m2m()
    782780                for formset in formsets:
    783781                    self.save_formset(request, form, formset, change=False)
    784782
    785783                self.log_addition(request, new_object)
    786784                return self.response_add(request, new_object)
    787             elif form_validated:
    788                 # The form was valid, but formsets were not, so delete the
    789                 # object we saved above.
    790                 new_object.delete()
    791785        else:
    792786            # Prepare the dict of initial data from the request.
    793787            # We have to special-case M2Ms as a list of comma-separated PKs.
  • 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..bc599df 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):  
    6464            return repr(self.message_dict)
    6565        return repr(self.messages)
    6666
    67 class ValidationError(BaseValidationError):
    68     pass
    69 
    70 class UnresolvableValidationError(BaseValidationError):
    71     """Validation error that cannot be resolved by the user."""
    72     pass
     67def update_errors(errors, e):
     68    if hasattr(e, 'message_dict'):
     69        if errors:
     70            for k, v in e.message_dict.items():
     71                errors.setdefault(k, []).extend(v)
     72        else:
     73            errors = e.message_dict
     74    else:
     75        errors[NON_FIELD_ERRORS] = e.messages
     76    return errors
    7377
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    index 06db7cc..6a1960b 100644
    a b import sys  
    33import os
    44from itertools import izip
    55import django.db.models.manager     # Imported to register signal handler.
    6 from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS
     6from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS, update_errors
    77from django.core import validators
    88from django.db.models.fields import AutoField, FieldDoesNotExist
    99from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField
    class Model(object):  
    642642    def prepare_database_save(self, unused):
    643643        return self.pk
    644644
    645     def validate(self):
     645    def full_validate(self):
    646646        """
    647         Hook for doing any extra model-wide validation after clean() has been
    648         called on every field. Any ValidationError raised by this method will
    649         not be associated with a particular field; it will have a special-case
    650         association with the field defined by NON_FIELD_ERRORS.
     647        Calls validate_fields, validate_unique, and validate on the model,
     648        and combines and reraises any ``ValidationError``s that occur.
    651649        """
    652         self.validate_unique()
     650        errors = {}
     651
     652        try:
     653            self.validate_fields()
     654        except ValidationError, e:
     655            errors = update_errors(errors, e)
    653656
    654     def validate_unique(self):
    655         unique_checks, date_checks = self._get_unique_checks()
     657        try:
     658            self.validate_unique()
     659        except ValidationError, e:
     660            errors = update_errors(errors, e)
     661
     662        # Form.clean() is run even if other validation fails, so do the
     663        # same with Model.validate() for consistency.
     664        try:
     665            self.validate()
     666        except ValidationError, e:
     667            errors = update_errors(errors, e)
     668
     669        if errors:
     670            raise ValidationError(errors)
     671
     672    def validate_fields(self, fields=None):
     673        """
     674        Cleans all fields and raises a ValidationError containing message_dict
     675        of all validation errors if any occur.
     676
     677        If ``fields`` is supplied, only validate the specified fields.
     678        """
     679        errors = {}
     680        if fields is None:
     681            fields = self._meta.fields
     682        else:
     683            fields = [self._meta.get_field_by_name(f)[0] for f in fields]
     684        for f in fields:
     685            try:
     686                setattr(self, f.attname, f.clean(getattr(self, f.attname), self))
     687            except ValidationError, e:
     688                errors[f.name] = e.messages
     689        if errors:
     690            raise ValidationError(errors)
     691
     692    def validate_unique(self, fields=None):
     693        unique_checks, date_checks = self._get_unique_checks(fields=fields)
    656694
    657695        errors = self._perform_unique_checks(unique_checks)
    658696        date_errors = self._perform_date_checks(date_checks)
    class Model(object):  
    663701        if errors:
    664702            raise ValidationError(errors)
    665703
    666     def _get_unique_checks(self):
    667         from django.db.models.fields import FieldDoesNotExist, Field as ModelField
     704    def _get_unique_checks(self, fields=None):
     705        if fields is None:
     706            fields = [f.name for f in self._meta.fields]
     707        unique_checks = []
     708        # Gather a list of checks to perform. Since validate_unique could be
     709        # called from a ModelForm, some fields may have been excluded; we can't
     710        # perform a unique check on a model that is missing fields involved
     711        # in that check. It also does not make sense to check data that didn't
     712        # validate, and since NULL does not equal NULL in SQL we should not do
     713        # any unique checking for NULL values.
     714        for check in self._meta.unique_together:
     715            for name in check:
     716                # If this is an excluded field, short circuit and don't a unique_check.
     717                if name not in fields:
     718                    break
     719            # Also, Skip fields that don't have a value, we can't check for
     720            # their uniqueness.
     721            if getattr(self, name, None) is None:
     722                break
     723            else:
     724                unique_checks.append(check)
    668725
    669         unique_checks = list(self._meta.unique_together)
    670         # these are checks for the unique_for_<date/year/month>
     726        # These are checks for the unique_for_<date/year/month>.
    671727        date_checks = []
    672728
    673729        # Gather a list of checks for fields declared as unique and add them to
    674730        # the list of checks. Again, skip empty fields and any that did not validate.
    675731        for f in self._meta.fields:
    676732            name = f.name
     733            # Don't add excluded fields to unique for date checks.
     734            if name not in fields:
     735                continue
    677736            if f.unique:
    678737                unique_checks.append((name,))
    679738            if f.unique_for_date:
    class Model(object):  
    684743                date_checks.append(('month', name, f.unique_for_month))
    685744        return unique_checks, date_checks
    686745
    687 
    688746    def _perform_unique_checks(self, unique_checks):
    689747        errors = {}
    690748
    class Model(object):  
    781839                'field_label': unicode(field_labels)
    782840            }
    783841
    784     def full_validate(self, exclude=[]):
     842    def validate(self):
    785843        """
    786         Cleans all fields and raises ValidationError containing message_dict
    787         of all validation errors if any occur.
     844        Hook for doing any extra model-wide validation after clean() has been
     845        called on every field. Any ValidationError raised by this method will
     846        not be associated with a particular field; it will have a special-case
     847        association with the field defined by NON_FIELD_ERRORS.
    788848        """
    789         errors = {}
    790         for f in self._meta.fields:
    791             if f.name in exclude:
    792                 continue
    793             try:
    794                 setattr(self, f.attname, f.clean(getattr(self, f.attname), self))
    795             except ValidationError, e:
    796                 errors[f.name] = e.messages
    797 
    798         # Form.clean() is run even if other validation fails, so do the
    799         # same with Model.validate() for consistency.
    800         try:
    801             self.validate()
    802         except ValidationError, e:
    803             if hasattr(e, 'message_dict'):
    804                 if errors:
    805                     for k, v in e.message_dict.items():
    806                         errors.set_default(k, []).extend(v)
    807                 else:
    808                     errors = e.message_dict
    809             else:
    810                 errors[NON_FIELD_ERRORS] = e.messages
    811 
    812         if errors:
    813             raise ValidationError(errors)
     849        pass
    814850
    815851
    816852############################################
  • django/db/models/fields/related.py

    diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
    index 8fec836..ae6a2f7 100644
    a b class ForeignKey(RelatedField, Field):  
    740740    def validate(self, value, model_instance):
    741741        if self.rel.parent_link:
    742742            return
     743        # Don't validate the field if a value wasn't supplied. This is
     744        # generally the case when saving new inlines in the admin.
     745        # See #12507.
     746        if value is None:
     747            return
    743748        super(ForeignKey, self).validate(value, model_instance)
    744749        if not value:
    745750            return
  • django/forms/models.py

    diff --git a/django/forms/models.py b/django/forms/models.py
    index ff20c93..58d6a10 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, update_errors, NON_FIELD_ERRORS
    1313from django.core.validators import EMPTY_VALUES
    1414from util import ErrorList
    1515from forms import BaseForm, get_declared_fields
    class BaseModelForm(BaseForm):  
    248248    def clean(self):
    249249        opts = self._meta
    250250        self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
     251        # For backwards-compatibility, several types of fields need to be
     252        # excluded from model validation. See the following tickets for
     253        # details: #12507, #12521, #12553
     254        errors = {}
     255        # Build up a list of fields that should be included in model validation
     256        # and unique checks.
     257        fields = []
     258        for f in self.instance._meta.fields:
     259            field = f.name
     260            # Exclude fields that aren't on the form. The developer may be
     261            # adding these values to the model after form validation.
     262            if field not in self.fields:
     263                continue
     264            # Exclude fields that failed form validation. There's no need for
     265            # the model to validate them as well.
     266            elif field in self._errors.keys():
     267                continue
     268            # Exclude empty fields that are not required by the form. Model
     269            # validation might complain that they are empty, but the
     270            # developer is reponsible for setting their value after calling
     271            # form validation.
     272            elif (not self.fields[field].required) and self.cleaned_data.get(field, None) is None:
     273                continue
     274            else:
     275                fields.append(field)
     276        try:
     277            self.instance.validate_fields(fields=list(fields))
     278        except ValidationError, e:
     279            errors = update_errors(errors, e)
    251280        try:
    252             self.instance.full_validate(exclude=self._errors.keys())
     281            self.instance.validate_unique(fields=fields)
    253282        except ValidationError, e:
    254             for k, v in e.message_dict.items():
     283            errors = update_errors(errors, e)
     284        if errors:
     285            for k, v in errors.items():
    255286                if k != NON_FIELD_ERRORS:
    256287                    self._errors.setdefault(k, ErrorList()).extend(v)
    257 
    258288                    # Remove the data from the cleaned_data dict since it was invalid
    259289                    if k in self.cleaned_data:
    260290                        del self.cleaned_data[k]
    261 
    262             if NON_FIELD_ERRORS in e.message_dict:
    263                 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 
     291            if NON_FIELD_ERRORS in errors:
     292                raise ValidationError(errors[NON_FIELD_ERRORS])
    273293        return self.cleaned_data
    274294
    275295    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..702523e 100644
    a b This is used in the admin for save_as functionality.  
    543543...     'book_set-2-title': '',
    544544... }
    545545
     546>>> formset = AuthorBooksFormSet(data, instance=Author(), save_as_new=True)
     547>>> formset.is_valid()
     548True
     549
    546550>>> new_author = Author.objects.create(name='Charles Baudelaire')
    547551>>> formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True)
    548552>>> [book for book in formset.save() if book.author.pk == new_author.pk]
    False  
    10311035>>> formset._non_form_errors
    10321036[u'Please correct the duplicate data for price and quantity, which must be unique.']
    10331037
     1038# Only the price field is specified, this should skip any unique checks since
     1039# the unique_together is not fulfilled. This will fail with a KeyError if broken.
     1040>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2)
     1041>>> data = {
     1042...     'form-TOTAL_FORMS': '2',
     1043...     'form-INITIAL_FORMS': '0',
     1044...     'form-0-price': '24',
     1045...     'form-1-price': '24',
     1046... }
     1047>>> formset = FormSet(data)
     1048>>> formset.is_valid()
     1049True
     1050
    10341051>>> FormSet = inlineformset_factory(Author, Book, extra=0)
    10351052>>> author = Author.objects.order_by('id')[0]
    10361053>>> 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..002e136 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)
    2424        self.assertFailsValidation(mtv.full_validate, [NON_FIELD_ERRORS, 'name'])
    2525
  • 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