Fixed #12512. Changed ModelForm to stop performing model validation on fields that are not part of the form. Thanks, Honza Kral and Ivan Sagalaev.
This reverts some admin and test changes from [12098] and also fixes #12507, #12520, #12552 and #12553. git-svn-id: http://code.djangoproject.com/svn/django/trunk@12206 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
26279c5721
commit
2f9853b2dc
|
@ -579,12 +579,12 @@ class ModelAdmin(BaseModelAdmin):
|
|||
"""
|
||||
messages.info(request, message)
|
||||
|
||||
def save_form(self, request, form, change, commit=False):
|
||||
def save_form(self, request, form, change):
|
||||
"""
|
||||
Given a ModelForm return an unsaved instance. ``change`` is True if
|
||||
the object is being changed, and False if it's being added.
|
||||
"""
|
||||
return form.save(commit=commit)
|
||||
return form.save(commit=False)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
|
@ -758,11 +758,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||
if request.method == 'POST':
|
||||
form = ModelForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
# Save the object, even if inline formsets haven't been
|
||||
# validated yet. We need to pass the valid model to the
|
||||
# formsets for validation. If the formsets do not validate, we
|
||||
# will delete the object.
|
||||
new_object = self.save_form(request, form, change=False, commit=True)
|
||||
new_object = self.save_form(request, form, change=False)
|
||||
form_validated = True
|
||||
else:
|
||||
form_validated = False
|
||||
|
@ -779,15 +775,13 @@ class ModelAdmin(BaseModelAdmin):
|
|||
prefix=prefix, queryset=inline.queryset(request))
|
||||
formsets.append(formset)
|
||||
if all_valid(formsets) and form_validated:
|
||||
self.save_model(request, new_object, form, change=False)
|
||||
form.save_m2m()
|
||||
for formset in formsets:
|
||||
self.save_formset(request, form, formset, change=False)
|
||||
|
||||
self.log_addition(request, new_object)
|
||||
return self.response_add(request, new_object)
|
||||
elif form_validated:
|
||||
# The form was valid, but formsets were not, so delete the
|
||||
# object we saved above.
|
||||
new_object.delete()
|
||||
else:
|
||||
# Prepare the dict of initial data from the request.
|
||||
# We have to special-case M2Ms as a list of comma-separated PKs.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.contrib.auth.models import User, UNUSABLE_PASSWORD
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.sites.models import Site
|
||||
|
@ -21,12 +21,6 @@ class UserCreationForm(forms.ModelForm):
|
|||
model = User
|
||||
fields = ("username",)
|
||||
|
||||
def clean(self):
|
||||
# Fill the password field so model validation won't complain about it
|
||||
# being blank. We'll set it with the real value below.
|
||||
self.instance.password = UNUSABLE_PASSWORD
|
||||
super(UserCreationForm, self).clean()
|
||||
|
||||
def clean_username(self):
|
||||
username = self.cleaned_data["username"]
|
||||
try:
|
||||
|
@ -40,9 +34,15 @@ class UserCreationForm(forms.ModelForm):
|
|||
password2 = self.cleaned_data["password2"]
|
||||
if password1 != password2:
|
||||
raise forms.ValidationError(_("The two password fields didn't match."))
|
||||
self.instance.set_password(password1)
|
||||
return password2
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super(UserCreationForm, self).save(commit=False)
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
class UserChangeForm(forms.ModelForm):
|
||||
username = forms.RegexField(label=_("Username"), max_length=30, regex=r'^\w+$',
|
||||
help_text = _("Required. 30 characters or fewer. Alphanumeric characters only (letters, digits and underscores)."),
|
||||
|
|
|
@ -33,7 +33,7 @@ class FieldError(Exception):
|
|||
pass
|
||||
|
||||
NON_FIELD_ERRORS = '__all__'
|
||||
class BaseValidationError(Exception):
|
||||
class ValidationError(Exception):
|
||||
"""An error while validating data."""
|
||||
def __init__(self, message, code=None, params=None):
|
||||
import operator
|
||||
|
@ -64,10 +64,14 @@ class BaseValidationError(Exception):
|
|||
return repr(self.message_dict)
|
||||
return repr(self.messages)
|
||||
|
||||
class ValidationError(BaseValidationError):
|
||||
pass
|
||||
|
||||
class UnresolvableValidationError(BaseValidationError):
|
||||
"""Validation error that cannot be resolved by the user."""
|
||||
pass
|
||||
def update_error_dict(self, error_dict):
|
||||
if hasattr(self, 'message_dict'):
|
||||
if error_dict:
|
||||
for k, v in self.message_dict.items():
|
||||
error_dict.setdefault(k, []).extend(v)
|
||||
else:
|
||||
error_dict = self.message_dict
|
||||
else:
|
||||
error_dict[NON_FIELD_ERRORS] = self.messages
|
||||
return error_dict
|
||||
|
||||
|
|
|
@ -640,17 +640,21 @@ class Model(object):
|
|||
def prepare_database_save(self, unused):
|
||||
return self.pk
|
||||
|
||||
def validate(self):
|
||||
def clean(self):
|
||||
"""
|
||||
Hook for doing any extra model-wide validation after clean() has been
|
||||
called on every field. Any ValidationError raised by this method will
|
||||
not be associated with a particular field; it will have a special-case
|
||||
association with the field defined by NON_FIELD_ERRORS.
|
||||
called on every field by self.clean_fields. Any ValidationError raised
|
||||
by this method will not be associated with a particular field; it will
|
||||
have a special-case association with the field defined by NON_FIELD_ERRORS.
|
||||
"""
|
||||
self.validate_unique()
|
||||
pass
|
||||
|
||||
def validate_unique(self):
|
||||
unique_checks, date_checks = self._get_unique_checks()
|
||||
def validate_unique(self, exclude=None):
|
||||
"""
|
||||
Checks unique constraints on the model and raises ``ValidationError``
|
||||
if any failed.
|
||||
"""
|
||||
unique_checks, date_checks = self._get_unique_checks(exclude=exclude)
|
||||
|
||||
errors = self._perform_unique_checks(unique_checks)
|
||||
date_errors = self._perform_date_checks(date_checks)
|
||||
|
@ -661,17 +665,35 @@ class Model(object):
|
|||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
def _get_unique_checks(self):
|
||||
from django.db.models.fields import FieldDoesNotExist, Field as ModelField
|
||||
def _get_unique_checks(self, exclude=None):
|
||||
"""
|
||||
Gather a list of checks to perform. Since validate_unique could be
|
||||
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
|
||||
to be passed in via the exclude argument.
|
||||
"""
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
unique_checks = []
|
||||
for check in self._meta.unique_together:
|
||||
for name in check:
|
||||
# If this is an excluded field, don't add this check.
|
||||
if name in exclude:
|
||||
break
|
||||
else:
|
||||
unique_checks.append(check)
|
||||
|
||||
unique_checks = list(self._meta.unique_together)
|
||||
# these are checks for the unique_for_<date/year/month>
|
||||
# These are checks for the unique_for_<date/year/month>.
|
||||
date_checks = []
|
||||
|
||||
# Gather a list of checks for fields declared as unique and add them to
|
||||
# the list of checks. Again, skip empty fields and any that did not validate.
|
||||
# the list of checks.
|
||||
for f in self._meta.fields:
|
||||
name = f.name
|
||||
if name in exclude:
|
||||
continue
|
||||
if f.unique:
|
||||
unique_checks.append((name,))
|
||||
if f.unique_for_date:
|
||||
|
@ -682,7 +704,6 @@ class Model(object):
|
|||
date_checks.append(('month', name, f.unique_for_month))
|
||||
return unique_checks, date_checks
|
||||
|
||||
|
||||
def _perform_unique_checks(self, unique_checks):
|
||||
errors = {}
|
||||
|
||||
|
@ -779,34 +800,61 @@ class Model(object):
|
|||
'field_label': unicode(field_labels)
|
||||
}
|
||||
|
||||
def full_validate(self, exclude=[]):
|
||||
def full_clean(self, exclude=None):
|
||||
"""
|
||||
Cleans all fields and raises ValidationError containing message_dict
|
||||
Calls clean_fields, clean, and validate_unique, on the model,
|
||||
and raises a ``ValidationError`` for any errors that occured.
|
||||
"""
|
||||
errors = {}
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
|
||||
try:
|
||||
self.clean_fields(exclude=exclude)
|
||||
except ValidationError, e:
|
||||
errors = e.update_error_dict(errors)
|
||||
|
||||
# Form.clean() is run even if other validation fails, so do the
|
||||
# same with Model.clean() for consistency.
|
||||
try:
|
||||
self.clean()
|
||||
except ValidationError, e:
|
||||
errors = e.update_error_dict(errors)
|
||||
|
||||
# Run unique checks, but only for fields that passed validation.
|
||||
for name in errors.keys():
|
||||
if name != NON_FIELD_ERRORS and name not in exclude:
|
||||
exclude.append(name)
|
||||
try:
|
||||
self.validate_unique(exclude=exclude)
|
||||
except ValidationError, e:
|
||||
errors = e.update_error_dict(errors)
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
def clean_fields(self, exclude=None):
|
||||
"""
|
||||
Cleans all fields and raises a ValidationError containing message_dict
|
||||
of all validation errors if any occur.
|
||||
"""
|
||||
if exclude is None:
|
||||
exclude = []
|
||||
|
||||
errors = {}
|
||||
for f in self._meta.fields:
|
||||
if f.name in exclude:
|
||||
continue
|
||||
# Skip validation for empty fields with blank=True. The developer
|
||||
# is responsible for making sure they have a valid value.
|
||||
raw_value = getattr(self, f.attname)
|
||||
if f.blank and raw_value in validators.EMPTY_VALUES:
|
||||
continue
|
||||
try:
|
||||
setattr(self, f.attname, f.clean(getattr(self, f.attname), self))
|
||||
setattr(self, f.attname, f.clean(raw_value, self))
|
||||
except ValidationError, e:
|
||||
errors[f.name] = e.messages
|
||||
|
||||
# Form.clean() is run even if other validation fails, so do the
|
||||
# same with Model.validate() for consistency.
|
||||
try:
|
||||
self.validate()
|
||||
except ValidationError, e:
|
||||
if hasattr(e, 'message_dict'):
|
||||
if errors:
|
||||
for k, v in e.message_dict.items():
|
||||
errors.setdefault(k, []).extend(v)
|
||||
else:
|
||||
errors = e.message_dict
|
||||
else:
|
||||
errors[NON_FIELD_ERRORS] = e.messages
|
||||
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
|
|
@ -740,6 +740,11 @@ class ForeignKey(RelatedField, Field):
|
|||
def validate(self, value, model_instance):
|
||||
if self.rel.parent_link:
|
||||
return
|
||||
# Don't validate the field if a value wasn't supplied. This is
|
||||
# generally the case when saving new inlines in the admin.
|
||||
# See #12507.
|
||||
if value is None:
|
||||
return
|
||||
super(ForeignKey, self).validate(value, model_instance)
|
||||
if not value:
|
||||
return
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.utils.datastructures import SortedDict
|
|||
from django.utils.text import get_text_list, capfirst
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext
|
||||
|
||||
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS, UnresolvableValidationError
|
||||
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
from util import ErrorList
|
||||
from forms import BaseForm, get_declared_fields
|
||||
|
@ -250,31 +250,51 @@ class BaseModelForm(BaseForm):
|
|||
super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
|
||||
error_class, label_suffix, empty_permitted)
|
||||
|
||||
|
||||
def _get_validation_exclusions(self):
|
||||
"""
|
||||
For backwards-compatibility, several types of fields need to be
|
||||
excluded from model validation. See the following tickets for
|
||||
details: #12507, #12521, #12553
|
||||
"""
|
||||
exclude = []
|
||||
# Build up a list of fields that should be excluded from model field
|
||||
# validation and unique checks.
|
||||
for f in self.instance._meta.fields:
|
||||
field = f.name
|
||||
# Exclude fields that aren't on the form. The developer may be
|
||||
# adding these values to the model after form validation.
|
||||
if field not in self.fields:
|
||||
exclude.append(f.name)
|
||||
# Exclude fields that failed form validation. There's no need for
|
||||
# the model fields to validate them as well.
|
||||
elif field in self._errors.keys():
|
||||
exclude.append(f.name)
|
||||
# Exclude empty fields that are not required by the form. The
|
||||
# underlying model field may be required, so this keeps the model
|
||||
# field from raising that error.
|
||||
else:
|
||||
form_field = self.fields[field]
|
||||
field_value = self.cleaned_data.get(field, None)
|
||||
if field_value is None and not form_field.required:
|
||||
exclude.append(f.name)
|
||||
return exclude
|
||||
|
||||
def clean(self):
|
||||
opts = self._meta
|
||||
self.instance = construct_instance(self, self.instance, opts.fields, opts.exclude)
|
||||
exclude = self._get_validation_exclusions()
|
||||
try:
|
||||
self.instance.full_validate(exclude=self._errors.keys())
|
||||
self.instance.full_clean(exclude=exclude)
|
||||
except ValidationError, e:
|
||||
for k, v in e.message_dict.items():
|
||||
if k != NON_FIELD_ERRORS:
|
||||
self._errors.setdefault(k, ErrorList()).extend(v)
|
||||
|
||||
# Remove the data from the cleaned_data dict since it was invalid
|
||||
if k in self.cleaned_data:
|
||||
del self.cleaned_data[k]
|
||||
|
||||
if NON_FIELD_ERRORS in e.message_dict:
|
||||
raise ValidationError(e.message_dict[NON_FIELD_ERRORS])
|
||||
|
||||
# If model validation threw errors for fields that aren't on the
|
||||
# form, the the errors cannot be corrected by the user. Displaying
|
||||
# those errors would be pointless, so raise another type of
|
||||
# exception that *won't* be caught and displayed by the form.
|
||||
if set(e.message_dict.keys()) - set(self.fields.keys() + [NON_FIELD_ERRORS]):
|
||||
raise UnresolvableValidationError(e.message_dict)
|
||||
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
|
@ -412,17 +432,20 @@ class BaseModelFormSet(BaseFormSet):
|
|||
self.validate_unique()
|
||||
|
||||
def validate_unique(self):
|
||||
# Iterate over the forms so that we can find one with potentially valid
|
||||
# data from which to extract the error checks
|
||||
# Collect unique_checks and date_checks to run from all the forms.
|
||||
all_unique_checks = set()
|
||||
all_date_checks = set()
|
||||
for form in self.forms:
|
||||
if hasattr(form, 'cleaned_data'):
|
||||
break
|
||||
else:
|
||||
return
|
||||
unique_checks, date_checks = form.instance._get_unique_checks()
|
||||
if not hasattr(form, 'cleaned_data'):
|
||||
continue
|
||||
exclude = form._get_validation_exclusions()
|
||||
unique_checks, date_checks = form.instance._get_unique_checks(exclude=exclude)
|
||||
all_unique_checks = all_unique_checks.union(set(unique_checks))
|
||||
all_date_checks = all_date_checks.union(set(date_checks))
|
||||
|
||||
errors = []
|
||||
# Do each of the unique checks (unique and unique_together)
|
||||
for unique_check in unique_checks:
|
||||
for unique_check in all_unique_checks:
|
||||
seen_data = set()
|
||||
for form in self.forms:
|
||||
# if the form doesn't have cleaned_data then we ignore it,
|
||||
|
@ -444,7 +467,7 @@ class BaseModelFormSet(BaseFormSet):
|
|||
# mark the data as seen
|
||||
seen_data.add(row_data)
|
||||
# iterate over each of the date checks now
|
||||
for date_check in date_checks:
|
||||
for date_check in all_date_checks:
|
||||
seen_data = set()
|
||||
lookup, field, unique_for = date_check
|
||||
for form in self.forms:
|
||||
|
|
|
@ -34,31 +34,88 @@ Validating objects
|
|||
|
||||
.. versionadded:: 1.2
|
||||
|
||||
To validate your model, call its ``full_validate()`` method:
|
||||
There are three steps in validating a model, and all three are called by a
|
||||
model's ``full_clean()`` method. Most of the time, this method will be called
|
||||
automatically by a ``ModelForm``. (See the :ref:`ModelForm documentation
|
||||
<topics-forms-modelforms>` for more information.) You should only need to call
|
||||
``full_clean()`` if you plan to handle validation errors yourself.
|
||||
|
||||
.. method:: Model.full_validate([exclude=[]])
|
||||
.. method:: Model.full_clean(exclude=None)
|
||||
|
||||
The optional ``exclude`` argument can contain a list of field names to omit
|
||||
when validating. This method raises ``ValidationError`` containing a
|
||||
message dictionary with errors from all fields.
|
||||
This method calls ``Model.clean_fields()``, ``Model.clean()``, and
|
||||
``Model.validate_unique()``, in that order and raises a ``ValidationError``
|
||||
that has a ``message_dict`` attribute containing errors from all three stages.
|
||||
|
||||
To add your own validation logic, override the supplied ``validate()`` method:
|
||||
The optional ``exclude`` argument can be used to provide a list of field names
|
||||
that can be excluded from validation and cleaning. ``ModelForm`` uses this
|
||||
argument to exclude fields that aren't present on your form from being
|
||||
validated since any errors raised could not be corrected by the user.
|
||||
|
||||
Note that ``full_validate`` will NOT be called automatically when you call
|
||||
Note that ``full_clean()`` will NOT be called automatically when you call
|
||||
your model's ``save()`` method. You'll need to call it manually if you want
|
||||
to run your model validators. (This is for backwards compatibility.) However,
|
||||
if you're using a ``ModelForm``, it will call ``full_validate`` for you and
|
||||
will present any errors along with the other form error messages.
|
||||
to run model validation outside of a ``ModelForm``. (This is for backwards
|
||||
compatibility.)
|
||||
|
||||
.. method:: Model.validate()
|
||||
Example::
|
||||
|
||||
The ``validate()`` method on ``Model`` by default checks for uniqueness of
|
||||
fields and group of fields that are declared to be unique, so remember to call
|
||||
``self.validate_unique()`` or the superclass' ``validate`` method if you want
|
||||
this validation to run.
|
||||
try:
|
||||
article.full_validate()
|
||||
except ValidationError, e:
|
||||
# Do something based on the errors contained in e.error_dict.
|
||||
# Display them to a user, or handle them programatically.
|
||||
|
||||
The first step ``full_clean()`` performs is to clean each individual field.
|
||||
|
||||
.. method:: Model.clean_fields(exclude=None)
|
||||
|
||||
This method will validate all fields on your model. The optional ``exclude``
|
||||
argument lets you provide a list of field names to exclude from validation. It
|
||||
will raise a ``ValidationError`` if any fields fail validation.
|
||||
|
||||
The second step ``full_clean()`` performs is to call ``Model.clean()``.
|
||||
This method should be overridden to perform custom validation on your model.
|
||||
|
||||
.. method:: Model.clean()
|
||||
|
||||
This method should be used to provide custom model validation, and to modify
|
||||
attributes on your model if desired. For instance, you could use it to
|
||||
automatically provide a value for a field, or to do validation that requires
|
||||
access to more than a single field::
|
||||
|
||||
def clean(self):
|
||||
from django.core.exceptions import ValidationError
|
||||
# Don't allow draft entries to have a pub_date.
|
||||
if self.status == 'draft' and self.pub_date is not None:
|
||||
raise ValidationError('Draft entries may not have a publication date.')
|
||||
# Set the pub_date for published items if it hasn't been set already.
|
||||
if self.status == 'published' and self.pub_date is None:
|
||||
self.pub_date = datetime.datetime.now()
|
||||
|
||||
Any ``ValidationError`` raised by ``Model.clean()`` will be stored under a
|
||||
special key that is used for errors that are tied to the entire model instead
|
||||
of to a specific field. You can access these errors with ``NON_FIELD_ERRORS``::
|
||||
|
||||
|
||||
from django.core.validators import ValidationError, NON_FIELD_ERRORS
|
||||
try:
|
||||
article.full_clean():
|
||||
except ValidationError, e:
|
||||
non_field_errors = e.message_dict[NON_FIELD_ERRORS]
|
||||
|
||||
Finally, ``full_clean()`` will check any unique constraints on your model.
|
||||
|
||||
.. method:: Model.validate_unique(exclude=None)
|
||||
|
||||
This method is similar to ``clean_fields``, but validates all uniqueness
|
||||
constraints on your model instead of individual field values. The optional
|
||||
``exclude`` argument allows you to provide a list of field names to exclude
|
||||
from validation. It will raise a ``ValidationError`` if any fields fail
|
||||
validation.
|
||||
|
||||
Note that if you provide an ``exclude`` argument to ``validate_unique``, any
|
||||
``unique_together`` constraint that contains one of the fields you provided
|
||||
will not be checked.
|
||||
|
||||
Any ``ValidationError`` raised in this method will be included in the
|
||||
``message_dict`` under ``NON_FIELD_ERRORS``.
|
||||
|
||||
Saving objects
|
||||
==============
|
||||
|
|
|
@ -515,6 +515,18 @@ There are a couple of things to note, however.
|
|||
Chances are these notes won't affect you unless you're trying to do something
|
||||
tricky with subclassing.
|
||||
|
||||
Interaction with model validation
|
||||
---------------------------------
|
||||
|
||||
As part of its validation process, ``ModelForm`` will call the ``clean()``
|
||||
method of each field on your model that has a corresponding field on your form.
|
||||
If you have excluded any model fields, validation will not be run on those
|
||||
fields. See the :ref:`form validation <ref-forms-validation>` documentation
|
||||
for more on how field cleaning and validation work. Also, your model's
|
||||
``clean()`` method will be called before any uniqueness checks are made. See
|
||||
:ref:`Validating objects <validating-objects>` for more information on the
|
||||
model's ``clean()`` hook.
|
||||
|
||||
.. _model-formsets:
|
||||
|
||||
Model formsets
|
||||
|
|
|
@ -1135,6 +1135,15 @@ True
|
|||
|
||||
>>> instance.delete()
|
||||
|
||||
# Test the non-required FileField
|
||||
>>> f = TextFileForm(data={'description': u'Assistance'})
|
||||
>>> f.fields['file'].required = False
|
||||
>>> f.is_valid()
|
||||
True
|
||||
>>> instance = f.save()
|
||||
>>> instance.file
|
||||
<FieldFile: None>
|
||||
|
||||
>>> f = TextFileForm(data={'description': u'Assistance'}, files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance)
|
||||
>>> f.is_valid()
|
||||
True
|
||||
|
@ -1173,7 +1182,7 @@ True
|
|||
>>> class BigIntForm(forms.ModelForm):
|
||||
... class Meta:
|
||||
... model = BigInt
|
||||
...
|
||||
...
|
||||
>>> bif = BigIntForm({'biggie': '-9223372036854775808'})
|
||||
>>> bif.is_valid()
|
||||
True
|
||||
|
@ -1446,16 +1455,41 @@ False
|
|||
>>> form._errors
|
||||
{'__all__': [u'Price with this Price and Quantity already exists.']}
|
||||
|
||||
# This form is never valid because quantity is blank=False.
|
||||
This Price instance generated by this form is not valid because the quantity
|
||||
field is required, but the form is valid because the field is excluded from
|
||||
the form. This is for backwards compatibility.
|
||||
|
||||
>>> class PriceForm(ModelForm):
|
||||
... class Meta:
|
||||
... model = Price
|
||||
... exclude = ('quantity',)
|
||||
>>> form = PriceForm({'price': '6.00'})
|
||||
>>> form.is_valid()
|
||||
True
|
||||
>>> price = form.save(commit=False)
|
||||
>>> price.full_clean()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
UnresolvableValidationError: {'quantity': [u'This field cannot be null.']}
|
||||
ValidationError: {'quantity': [u'This field cannot be null.']}
|
||||
|
||||
The form should not validate fields that it doesn't contain even if they are
|
||||
specified using 'fields', not 'exclude'.
|
||||
... class Meta:
|
||||
... model = Price
|
||||
... fields = ('price',)
|
||||
>>> form = PriceForm({'price': '6.00'})
|
||||
>>> form.is_valid()
|
||||
True
|
||||
|
||||
The form should still have an instance of a model that is not complete and
|
||||
not saved into a DB yet.
|
||||
|
||||
>>> form.instance.price
|
||||
Decimal('6.00')
|
||||
>>> form.instance.quantity is None
|
||||
True
|
||||
>>> form.instance.pk is None
|
||||
True
|
||||
|
||||
# Unique & unique together with null values
|
||||
>>> class BookForm(ModelForm):
|
||||
|
|
|
@ -543,6 +543,10 @@ This is used in the admin for save_as functionality.
|
|||
... 'book_set-2-title': '',
|
||||
... }
|
||||
|
||||
>>> formset = AuthorBooksFormSet(data, instance=Author(), save_as_new=True)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
>>> new_author = Author.objects.create(name='Charles Baudelaire')
|
||||
>>> formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True)
|
||||
>>> [book for book in formset.save() if book.author.pk == new_author.pk]
|
||||
|
@ -1031,6 +1035,19 @@ False
|
|||
>>> formset._non_form_errors
|
||||
[u'Please correct the duplicate data for price and quantity, which must be unique.']
|
||||
|
||||
# Only the price field is specified, this should skip any unique checks since
|
||||
# the unique_together is not fulfilled. This will fail with a KeyError if broken.
|
||||
>>> FormSet = modelformset_factory(Price, fields=("price",), extra=2)
|
||||
>>> data = {
|
||||
... 'form-TOTAL_FORMS': '2',
|
||||
... 'form-INITIAL_FORMS': '0',
|
||||
... 'form-0-price': '24',
|
||||
... 'form-1-price': '24',
|
||||
... }
|
||||
>>> formset = FormSet(data)
|
||||
>>> formset.is_valid()
|
||||
True
|
||||
|
||||
>>> FormSet = inlineformset_factory(Author, Book, extra=0)
|
||||
>>> author = Author.objects.order_by('id')[0]
|
||||
>>> book_ids = author.book_set.values_list('id', flat=True)
|
||||
|
|
|
@ -17,8 +17,8 @@ class ModelToValidate(models.Model):
|
|||
url = models.URLField(blank=True)
|
||||
f_with_custom_validator = models.IntegerField(blank=True, null=True, validators=[validate_answer_to_universe])
|
||||
|
||||
def validate(self):
|
||||
super(ModelToValidate, self).validate()
|
||||
def clean(self):
|
||||
super(ModelToValidate, self).clean()
|
||||
if self.number == 11:
|
||||
raise ValidationError('Invalid number supplied!')
|
||||
|
||||
|
@ -36,7 +36,7 @@ class UniqueTogetherModel(models.Model):
|
|||
efield = models.EmailField()
|
||||
|
||||
class Meta:
|
||||
unique_together = (('ifield', 'cfield',),('ifield', 'efield'), )
|
||||
unique_together = (('ifield', 'cfield',), ('ifield', 'efield'))
|
||||
|
||||
class UniqueForDateModel(models.Model):
|
||||
start_date = models.DateField()
|
||||
|
@ -51,3 +51,15 @@ class CustomMessagesModel(models.Model):
|
|||
error_messages={'null': 'NULL', 'not42': 'AAARGH', 'not_equal': '%s != me'},
|
||||
validators=[validate_answer_to_universe]
|
||||
)
|
||||
|
||||
class Author(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Article(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
author = models.ForeignKey(Author)
|
||||
pub_date = models.DateTimeField(blank=True)
|
||||
|
||||
def clean(self):
|
||||
if self.pub_date is None:
|
||||
self.pub_date = datetime.now()
|
||||
|
|
|
@ -5,9 +5,9 @@ from models import CustomMessagesModel
|
|||
class CustomMessagesTest(ValidationTestCase):
|
||||
def test_custom_simple_validator_message(self):
|
||||
cmm = CustomMessagesModel(number=12)
|
||||
self.assertFieldFailsValidationWithMessage(cmm.full_validate, 'number', ['AAARGH'])
|
||||
self.assertFieldFailsValidationWithMessage(cmm.full_clean, 'number', ['AAARGH'])
|
||||
|
||||
def test_custom_null_message(self):
|
||||
cmm = CustomMessagesModel()
|
||||
self.assertFieldFailsValidationWithMessage(cmm.full_validate, 'number', ['NULL'])
|
||||
self.assertFieldFailsValidationWithMessage(cmm.full_clean, 'number', ['NULL'])
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import unittest
|
||||
import datetime
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from models import CustomPKModel, UniqueTogetherModel, UniqueFieldsModel, UniqueForDateModel, ModelToValidate
|
||||
|
@ -26,8 +27,8 @@ class GetUniqueCheckTests(unittest.TestCase):
|
|||
def test_unique_for_date_gets_picked_up(self):
|
||||
m = UniqueForDateModel()
|
||||
self.assertEqual((
|
||||
[('id',)],
|
||||
[('date', 'count', 'start_date'), ('year', 'count', 'end_date'), ('month', 'order', 'end_date')]
|
||||
[('id',)],
|
||||
[('date', 'count', 'start_date'), ('year', 'count', 'end_date'), ('month', 'order', 'end_date')]
|
||||
), m._get_unique_checks()
|
||||
)
|
||||
|
||||
|
@ -47,12 +48,13 @@ class PerformUniqueChecksTest(unittest.TestCase):
|
|||
l = len(connection.queries)
|
||||
mtv = ModelToValidate(number=10, name='Some Name')
|
||||
setattr(mtv, '_adding', True)
|
||||
mtv.full_validate()
|
||||
mtv.full_clean()
|
||||
self.assertEqual(l+1, len(connection.queries))
|
||||
|
||||
def test_primary_key_unique_check_not_performed_when_not_adding(self):
|
||||
"""Regression test for #12132"""
|
||||
l = len(connection.queries)
|
||||
mtv = ModelToValidate(number=10, name='Some Name')
|
||||
mtv.full_validate()
|
||||
mtv.full_clean()
|
||||
self.assertEqual(l, len(connection.queries))
|
||||
|
||||
|
|
|
@ -1,58 +1,107 @@
|
|||
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
|
||||
from django.db import models
|
||||
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import NON_FIELD_ERRORS
|
||||
from modeltests.validation import ValidationTestCase
|
||||
from models import *
|
||||
from modeltests.validation.models import Author, Article, ModelToValidate
|
||||
|
||||
from validators import TestModelsWithValidators
|
||||
from test_unique import GetUniqueCheckTests, PerformUniqueChecksTest
|
||||
from test_custom_messages import CustomMessagesTest
|
||||
# Import other tests for this package.
|
||||
from modeltests.validation.validators import TestModelsWithValidators
|
||||
from modeltests.validation.test_unique import GetUniqueCheckTests, PerformUniqueChecksTest
|
||||
from modeltests.validation.test_custom_messages import CustomMessagesTest
|
||||
|
||||
|
||||
class BaseModelValidationTests(ValidationTestCase):
|
||||
|
||||
def test_missing_required_field_raises_error(self):
|
||||
mtv = ModelToValidate(f_with_custom_validator=42)
|
||||
self.assertFailsValidation(mtv.full_validate, ['name', 'number'])
|
||||
self.assertFailsValidation(mtv.full_clean, ['name', 'number'])
|
||||
|
||||
def test_with_correct_value_model_validates(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name')
|
||||
self.assertEqual(None, mtv.full_validate())
|
||||
self.assertEqual(None, mtv.full_clean())
|
||||
|
||||
def test_custom_validate_method_is_called(self):
|
||||
def test_custom_validate_method(self):
|
||||
mtv = ModelToValidate(number=11)
|
||||
self.assertFailsValidation(mtv.full_validate, [NON_FIELD_ERRORS, 'name'])
|
||||
self.assertFailsValidation(mtv.full_clean, [NON_FIELD_ERRORS, 'name'])
|
||||
|
||||
def test_wrong_FK_value_raises_error(self):
|
||||
mtv=ModelToValidate(number=10, name='Some Name', parent_id=3)
|
||||
self.assertFailsValidation(mtv.full_validate, ['parent'])
|
||||
self.assertFailsValidation(mtv.full_clean, ['parent'])
|
||||
|
||||
def test_correct_FK_value_validates(self):
|
||||
parent = ModelToValidate.objects.create(number=10, name='Some Name')
|
||||
mtv=ModelToValidate(number=10, name='Some Name', parent_id=parent.pk)
|
||||
self.assertEqual(None, mtv.full_validate())
|
||||
self.assertEqual(None, mtv.full_clean())
|
||||
|
||||
def test_wrong_email_value_raises_error(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name', email='not-an-email')
|
||||
self.assertFailsValidation(mtv.full_validate, ['email'])
|
||||
self.assertFailsValidation(mtv.full_clean, ['email'])
|
||||
|
||||
def test_correct_email_value_passes(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name', email='valid@email.com')
|
||||
self.assertEqual(None, mtv.full_validate())
|
||||
self.assertEqual(None, mtv.full_clean())
|
||||
|
||||
def test_wrong_url_value_raises_error(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name', url='not a url')
|
||||
self.assertFieldFailsValidationWithMessage(mtv.full_validate, 'url', [u'Enter a valid value.'])
|
||||
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'Enter a valid value.'])
|
||||
|
||||
def test_correct_url_but_nonexisting_gives_404(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name', url='http://google.com/we-love-microsoft.html')
|
||||
self.assertFieldFailsValidationWithMessage(mtv.full_validate, 'url', [u'This URL appears to be a broken link.'])
|
||||
self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'This URL appears to be a broken link.'])
|
||||
|
||||
def test_correct_url_value_passes(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name', url='http://www.djangoproject.com/')
|
||||
self.assertEqual(None, mtv.full_validate()) # This will fail if there's no Internet connection
|
||||
self.assertEqual(None, mtv.full_clean()) # This will fail if there's no Internet connection
|
||||
|
||||
def test_text_greater_that_charfields_max_length_eaises_erros(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name'*100)
|
||||
self.assertFailsValidation(mtv.full_validate, ['name',])
|
||||
self.assertFailsValidation(mtv.full_clean, ['name',])
|
||||
|
||||
class ArticleForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Article
|
||||
exclude = ['author']
|
||||
|
||||
class ModelFormsTests(TestCase):
|
||||
def setUp(self):
|
||||
self.author = Author.objects.create(name='Joseph Kocherhans')
|
||||
|
||||
def test_partial_validation(self):
|
||||
# Make sure the "commit=False and set field values later" idiom still
|
||||
# works with model validation.
|
||||
data = {
|
||||
'title': 'The state of model validation',
|
||||
'pub_date': '2010-1-10 14:49:00'
|
||||
}
|
||||
form = ArticleForm(data)
|
||||
self.assertEqual(form.errors.keys(), [])
|
||||
article = form.save(commit=False)
|
||||
article.author = self.author
|
||||
article.save()
|
||||
|
||||
def test_validation_with_empty_blank_field(self):
|
||||
# Since a value for pub_date wasn't provided and the field is
|
||||
# blank=True, model-validation should pass.
|
||||
# Also, Article.clean() should be run, so pub_date will be filled after
|
||||
# validation, so the form should save cleanly even though pub_date is
|
||||
# not allowed to be null.
|
||||
data = {
|
||||
'title': 'The state of model validation',
|
||||
}
|
||||
article = Article(author_id=self.author.id)
|
||||
form = ArticleForm(data, instance=article)
|
||||
self.assertEqual(form.errors.keys(), [])
|
||||
self.assertNotEqual(form.instance.pub_date, None)
|
||||
article = form.save()
|
||||
|
||||
def test_validation_with_invalid_blank_field(self):
|
||||
# Even though pub_date is set to blank=True, an invalid value was
|
||||
# provided, so it should fail validation.
|
||||
data = {
|
||||
'title': 'The state of model validation',
|
||||
'pub_date': 'never'
|
||||
}
|
||||
article = Article(author_id=self.author.id)
|
||||
form = ArticleForm(data, instance=article)
|
||||
self.assertEqual(form.errors.keys(), ['pub_date'])
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@ from models import *
|
|||
class TestModelsWithValidators(ValidationTestCase):
|
||||
def test_custom_validator_passes_for_correct_value(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name', f_with_custom_validator=42)
|
||||
self.assertEqual(None, mtv.full_validate())
|
||||
self.assertEqual(None, mtv.full_clean())
|
||||
|
||||
def test_custom_validator_raises_error_for_incorrect_value(self):
|
||||
mtv = ModelToValidate(number=10, name='Some Name', f_with_custom_validator=12)
|
||||
self.assertFailsValidation(mtv.full_validate, ['f_with_custom_validator'])
|
||||
self.assertFailsValidation(mtv.full_clean, ['f_with_custom_validator'])
|
||||
self.assertFieldFailsValidationWithMessage(
|
||||
mtv.full_validate,
|
||||
mtv.full_clean,
|
||||
'f_with_custom_validator',
|
||||
[u'This is not the answer to life, universe and everything!']
|
||||
)
|
||||
|
|
|
@ -4,7 +4,8 @@ import re
|
|||
import datetime
|
||||
from django.core.files import temp as tempfile
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User, Permission
|
||||
from django.contrib.auth import admin # Register auth models with the admin.
|
||||
from django.contrib.auth.models import User, Permission, UNUSABLE_PASSWORD
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.admin.models import LogEntry, DELETION
|
||||
from django.contrib.admin.sites import LOGIN_FORM_KEY
|
||||
|
@ -1766,3 +1767,37 @@ class ReadonlyTest(TestCase):
|
|||
self.assertEqual(Post.objects.count(), 2)
|
||||
p = Post.objects.order_by('-id')[0]
|
||||
self.assertEqual(p.posted, datetime.date.today())
|
||||
|
||||
class IncompleteFormTest(TestCase):
|
||||
"""
|
||||
Tests validation of a ModelForm that doesn't explicitly have all data
|
||||
corresponding to model fields. Model validation shouldn't fail
|
||||
such a forms.
|
||||
"""
|
||||
fixtures = ['admin-views-users.xml']
|
||||
|
||||
def setUp(self):
|
||||
self.client.login(username='super', password='secret')
|
||||
|
||||
def tearDown(self):
|
||||
self.client.logout()
|
||||
|
||||
def test_user_creation(self):
|
||||
response = self.client.post('/test_admin/admin/auth/user/add/', {
|
||||
'username': 'newuser',
|
||||
'password1': 'newpassword',
|
||||
'password2': 'newpassword',
|
||||
})
|
||||
new_user = User.objects.order_by('-id')[0]
|
||||
self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk)
|
||||
self.assertNotEquals(new_user.password, UNUSABLE_PASSWORD)
|
||||
|
||||
def test_password_mismatch(self):
|
||||
response = self.client.post('/test_admin/admin/auth/user/add/', {
|
||||
'username': 'newuser',
|
||||
'password1': 'newpassword',
|
||||
'password2': 'mismatch',
|
||||
})
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assert_('password' not in response.context['form'].errors)
|
||||
self.assertFormError(response, 'form', 'password2', ["The two password fields didn't match."])
|
||||
|
|
|
@ -81,7 +81,7 @@ class DeletionTests(TestCase):
|
|||
regression for #10750
|
||||
"""
|
||||
# exclude some required field from the forms
|
||||
ChildFormSet = inlineformset_factory(School, Child)
|
||||
ChildFormSet = inlineformset_factory(School, Child, exclude=['father', 'mother'])
|
||||
school = School.objects.create(name=u'test')
|
||||
mother = Parent.objects.create(name=u'mother')
|
||||
father = Parent.objects.create(name=u'father')
|
||||
|
@ -89,13 +89,13 @@ class DeletionTests(TestCase):
|
|||
'child_set-TOTAL_FORMS': u'1',
|
||||
'child_set-INITIAL_FORMS': u'0',
|
||||
'child_set-0-name': u'child',
|
||||
'child_set-0-mother': unicode(mother.pk),
|
||||
'child_set-0-father': unicode(father.pk),
|
||||
}
|
||||
formset = ChildFormSet(data, instance=school)
|
||||
self.assertEqual(formset.is_valid(), True)
|
||||
objects = formset.save(commit=False)
|
||||
self.assertEqual(school.child_set.count(), 0)
|
||||
objects[0].save()
|
||||
for obj in objects:
|
||||
obj.mother = mother
|
||||
obj.father = father
|
||||
obj.save()
|
||||
self.assertEqual(school.child_set.count(), 1)
|
||||
|
||||
|
|
Loading…
Reference in New Issue