From f5d4849cbeda994e7d2e43c4aaf2aac69d5c95bb Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Mon, 10 Jun 2013 12:22:40 +0200 Subject: Revert "Began implementing a shared set of test models to speed up tests." This reverts commit 22b7870e40a3ecf022b423de6cd867dcb35a6940. --- tests/model_forms/models.py | 27 +++++++++++--- tests/model_forms/tests.py | 89 +++++++++++++++++++++------------------------ 2 files changed, 64 insertions(+), 52 deletions(-) (limited to 'tests/model_forms') diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index 610dc34001..a798f9bf95 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -17,7 +17,6 @@ from django.db import models from django.utils import six from django.utils.encoding import python_2_unicode_compatible -from shared_models.models import Author, Book temp_storage_dir = tempfile.mkdtemp(dir=os.environ['DJANGO_TEST_TEMP_DIR']) temp_storage = FileSystemStorage(temp_storage_dir) @@ -46,13 +45,23 @@ class Category(models.Model): def __repr__(self): return self.__str__() +@python_2_unicode_compatible +class Writer(models.Model): + name = models.CharField(max_length=50, help_text='Use both first and last names.') + + class Meta: + ordering = ('name',) + + def __str__(self): + return self.name + @python_2_unicode_compatible class Article(models.Model): headline = models.CharField(max_length=50) slug = models.SlugField() pub_date = models.DateField() created = models.DateField(editable=False) - writer = models.ForeignKey(Author) + writer = models.ForeignKey(Writer) article = models.TextField() categories = models.ManyToManyField(Category, blank=True) status = models.PositiveIntegerField(choices=ARTICLE_STATUS, blank=True, null=True) @@ -72,12 +81,12 @@ class ImprovedArticle(models.Model): class ImprovedArticleWithParentLink(models.Model): article = models.OneToOneField(Article, parent_link=True) -class BetterAuthor(Author): +class BetterWriter(Writer): score = models.IntegerField() @python_2_unicode_compatible -class AuthorProfile(models.Model): - writer = models.OneToOneField(Author, primary_key=True) +class WriterProfile(models.Model): + writer = models.OneToOneField(Writer, primary_key=True) age = models.PositiveIntegerField() def __str__(self): @@ -177,6 +186,14 @@ class Inventory(models.Model): def __repr__(self): return self.__str__() +class Book(models.Model): + title = models.CharField(max_length=40) + author = models.ForeignKey(Writer, blank=True, null=True) + special_id = models.IntegerField(blank=True, null=True, unique=True) + + class Meta: + unique_together = ('title', 'author') + class BookXtra(models.Model): isbn = models.CharField(max_length=16, unique=True) suffix1 = models.IntegerField(blank=True, default=0) diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 58dde13a8a..509413fb61 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -17,13 +17,11 @@ from django.utils.unittest import skipUnless from django.test import TestCase from django.utils import six -from shared_models.models import Author, Book - -from .models import (Article, ArticleStatus, BetterAuthor, BigInt, +from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book, Category, CommaSeparatedInteger, CustomFieldForExclusionModel, DerivedBook, DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle, ImprovedArticleWithParentLink, Inventory, Post, Price, - Product, TextFile, AuthorProfile, Colour, ColourfulItem, + Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem, ArticleStatusNote, DateTimePost, test_images) if test_images: @@ -54,13 +52,11 @@ class PriceForm(forms.ModelForm): class BookForm(forms.ModelForm): class Meta: - fields = ['title', 'author', 'pubdate'] - model = Book + model = Book class DerivedBookForm(forms.ModelForm): class Meta: - fields = ['title', 'author', 'isbn', 'suffix1', 'suffix2'] model = DerivedBook @@ -88,11 +84,11 @@ class DerivedPostForm(forms.ModelForm): fields = '__all__' -class CustomAuthorForm(forms.ModelForm): +class CustomWriterForm(forms.ModelForm): name = forms.CharField(required=False) class Meta: - model = Author + model = Writer fields = '__all__' @@ -128,7 +124,7 @@ class PartialArticleForm(forms.ModelForm): class RoykoForm(forms.ModelForm): class Meta: - model = Author + model = Writer fields = '__all__' @@ -188,15 +184,14 @@ class ImprovedArticleWithParentLinkForm(forms.ModelForm): fields = '__all__' -class BetterAuthorForm(forms.ModelForm): +class BetterWriterForm(forms.ModelForm): class Meta: - model = BetterAuthor + model = BetterWriter fields = '__all__' - -class AuthorProfileForm(forms.ModelForm): +class WriterProfileForm(forms.ModelForm): class Meta: - model = AuthorProfile + model = WriterProfile fields = '__all__' @@ -324,14 +319,14 @@ class ModelFormBaseTest(TestCase): forms.fields.BooleanField) def test_override_field(self): - class AuthorForm(forms.ModelForm): + class WriterForm(forms.ModelForm): book = forms.CharField(required=False) class Meta: - model = Author + model = Writer fields = '__all__' - wf = AuthorForm({'name': 'Richard Lockridge'}) + wf = WriterForm({'name': 'Richard Lockridge'}) self.assertTrue(wf.is_valid()) def test_limit_nonexistent_field(self): @@ -556,7 +551,7 @@ class ValidationTest(TestCase): assert form.is_valid() def test_notrequired_overrides_notblank(self): - form = CustomAuthorForm({}) + form = CustomWriterForm({}) assert form.is_valid() @@ -565,7 +560,7 @@ class ValidationTest(TestCase): # unique/unique_together validation class UniqueTest(TestCase): def setUp(self): - self.author = Author.objects.create(name='Mike Royko') + self.writer = Writer.objects.create(name='Mike Royko') def test_simple_unique(self): form = ProductForm({'slug': 'teddy-bear-blue'}) @@ -589,31 +584,33 @@ class UniqueTest(TestCase): def test_unique_null(self): title = 'I May Be Wrong But I Doubt It' - form = BookForm({'title': title, 'author': self.author.pk, 'pubdate': '2012-12-12 00:00:00'}) + form = BookForm({'title': title, 'author': self.writer.pk}) self.assertTrue(form.is_valid()) form.save() - form = BookForm({'title': title, 'author': self.author.pk, 'pubdate': '2012-12-12 00:00:00'}) + form = BookForm({'title': title, 'author': self.writer.pk}) self.assertFalse(form.is_valid()) self.assertEqual(len(form.errors), 1) self.assertEqual(form.errors['__all__'], ['Book with this Title and Author already exists.']) - form = BookForm({'title': title, 'pubdate': '2012-12-12 00:00:00'}) + form = BookForm({'title': title}) self.assertTrue(form.is_valid()) form.save() - form = BookForm({'title': title, 'pubdate': '2012-12-12 00:00:00'}) + form = BookForm({'title': title}) self.assertTrue(form.is_valid()) def test_inherited_unique(self): - form = BetterAuthorForm({'name': 'Mike Royko', 'score': 3}) + title = 'Boss' + Book.objects.create(title=title, author=self.writer, special_id=1) + form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'special_id': '1', 'isbn': '12345'}) self.assertFalse(form.is_valid()) self.assertEqual(len(form.errors), 1) - self.assertEqual(form.errors['name'], ['Author with this Name already exists.']) + self.assertEqual(form.errors['special_id'], ['Book with this Special id already exists.']) def test_inherited_unique_together(self): title = 'Boss' - form = BookForm({'title': title, 'author': self.author.pk, 'pubdate': '2012-12-12 00:00:00'}) + form = BookForm({'title': title, 'author': self.writer.pk}) self.assertTrue(form.is_valid()) form.save() - form = DerivedBookForm({'title': title, 'author': self.author.pk, 'isbn': '12345'}) + form = DerivedBookForm({'title': title, 'author': self.writer.pk, 'isbn': '12345'}) self.assertFalse(form.is_valid()) self.assertEqual(len(form.errors), 1) self.assertEqual(form.errors['__all__'], ['Book with this Title and Author already exists.']) @@ -621,9 +618,8 @@ class UniqueTest(TestCase): def test_abstract_inherited_unique(self): title = 'Boss' isbn = '12345' - dbook = DerivedBook.objects.create(title=title, author=self.author, isbn=isbn, - pubdate='2012-12-12 00:00') - form = DerivedBookForm({'title': 'Other', 'author': self.author.pk, 'isbn': isbn}) + dbook = DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn) + form = DerivedBookForm({'title': 'Other', 'author': self.writer.pk, 'isbn': isbn}) self.assertFalse(form.is_valid()) self.assertEqual(len(form.errors), 1) self.assertEqual(form.errors['isbn'], ['Derived book with this Isbn already exists.']) @@ -631,11 +627,10 @@ class UniqueTest(TestCase): def test_abstract_inherited_unique_together(self): title = 'Boss' isbn = '12345' - dbook = DerivedBook.objects.create(title=title, author=self.author, isbn=isbn, - pubdate='2012-12-12 00:00') + dbook = DerivedBook.objects.create(title=title, author=self.writer, isbn=isbn) form = DerivedBookForm({ 'title': 'Other', - 'author': self.author.pk, + 'author': self.writer.pk, 'isbn': '9876', 'suffix1': '0', 'suffix2': '0' @@ -753,7 +748,7 @@ class ModelToDictTests(TestCase): ] for c in categories: c.save() - writer = Author(name='Test writer') + writer = Writer(name='Test writer') writer.save() art = Article( @@ -862,10 +857,10 @@ class OldFormForXTests(TestCase): with self.assertRaises(ValueError): f.save() - # Create a couple of Authors. - w_royko = Author(name='Mike Royko') + # Create a couple of Writers. + w_royko = Writer(name='Mike Royko') w_royko.save() - w_woodward = Author(name='Bob Woodward') + w_woodward = Writer(name='Bob Woodward') w_woodward.save() # ManyToManyFields are represented by a MultipleChoiceField, ForeignKeys and any # fields with the 'choices' attribute are represented by a ChoiceField. @@ -903,9 +898,9 @@ class OldFormForXTests(TestCase): # When the ModelForm is passed an instance, that instance's current values are # inserted as 'initial' data in each Field. - w = Author.objects.get(name='Mike Royko') + w = Writer.objects.get(name='Mike Royko') f = RoykoForm(auto_id=False, instance=w) - self.assertHTMLEqual(six.text_type(f), '''Name:
Use both first and last names.''') + self.assertHTMLEqual(six.text_type(f), '''Name:
Use both first and last names.''') art = Article( headline='Test article', @@ -1117,7 +1112,7 @@ class OldFormForXTests(TestCase): c4 = Category.objects.create(name='Fourth', url='4th') self.assertEqual(c4.name, 'Fourth') - w_bernstein = Author.objects.create(name='Carl Bernstein') + w_bernstein = Writer.objects.create(name='Carl Bernstein') self.assertEqual(w_bernstein.name, 'Carl Bernstein') self.assertHTMLEqual(f.as_ul(), '''
  • Headline:
  • Slug:
  • @@ -1294,17 +1289,17 @@ class OldFormForXTests(TestCase): self.assertEqual(list(ImprovedArticleWithParentLinkForm.base_fields), []) - bw = BetterAuthor(name='Joe Better', score=10) + bw = BetterWriter(name='Joe Better', score=10) bw.save() self.assertEqual(sorted(model_to_dict(bw)), - ['author_ptr', 'id', 'name', 'score']) + ['id', 'name', 'score', 'writer_ptr']) - form = BetterAuthorForm({'name': 'Some Name', 'score': 12}) + form = BetterWriterForm({'name': 'Some Name', 'score': 12}) self.assertEqual(form.is_valid(), True) bw2 = form.save() bw2.delete() - form = AuthorProfileForm() + form = WriterProfileForm() self.assertHTMLEqual(form.as_p(), '''

    -- cgit v1.3 From 9e50833e2293aa2734f14f9873abe4f83d03b8c6 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Thu, 4 Apr 2013 02:51:37 +0700 Subject: Fixed #20000 -- Allowed ModelForm meta overrides for label, help_text and error_messages --- django/forms/models.py | 56 ++++++++++++++++++++++++++++---- docs/ref/forms/models.txt | 29 +++++++++++------ docs/releases/1.6.txt | 11 +++++-- docs/topics/forms/modelforms.txt | 59 +++++++++++++++++++++++++--------- tests/model_forms/tests.py | 69 ++++++++++++++++++++++++++++++++++------ tests/model_formsets/tests.py | 52 +++++++++++++++++++++++++++++- 6 files changed, 231 insertions(+), 45 deletions(-) (limited to 'tests/model_forms') diff --git a/django/forms/models.py b/django/forms/models.py index 65434a6f6e..b0db4b62ac 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -138,7 +138,9 @@ def model_to_dict(instance, fields=None, exclude=None): data[f.name] = f.value_from_object(instance) return data -def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, localized_fields=None): +def fields_for_model(model, fields=None, exclude=None, widgets=None, + formfield_callback=None, localized_fields=None, + labels=None, help_texts=None, error_messages=None): """ Returns a ``SortedDict`` containing form fields for the given model. @@ -149,7 +151,16 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c fields will be excluded from the returned fields, even if they are listed in the ``fields`` argument. - ``widgets`` is a dictionary of model field names mapped to a widget + ``widgets`` is a dictionary of model field names mapped to a widget. + + ``localized_fields`` is a list of names of fields which should be localized. + + ``labels`` is a dictionary of model field names mapped to a label. + + ``help_texts`` is a dictionary of model field names mapped to a help text. + + ``error_messages`` is a dictionary of model field names mapped to a + dictionary of error messages. ``formfield_callback`` is a callable that takes a model field and returns a form field. @@ -170,6 +181,12 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c kwargs['widget'] = widgets[f.name] if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields): kwargs['localize'] = True + if labels and f.name in labels: + kwargs['label'] = labels[f.name] + if help_texts and f.name in help_texts: + kwargs['help_text'] = help_texts[f.name] + if error_messages and f.name in error_messages: + kwargs['error_messages'] = error_messages[f.name] if formfield_callback is None: formfield = f.formfield(**kwargs) @@ -197,6 +214,9 @@ class ModelFormOptions(object): self.exclude = getattr(options, 'exclude', None) self.widgets = getattr(options, 'widgets', None) self.localized_fields = getattr(options, 'localized_fields', None) + self.labels = getattr(options, 'labels', None) + self.help_texts = getattr(options, 'help_texts', None) + self.error_messages = getattr(options, 'error_messages', None) class ModelFormMetaclass(type): @@ -248,7 +268,9 @@ class ModelFormMetaclass(type): opts.fields = None fields = fields_for_model(opts.model, opts.fields, opts.exclude, - opts.widgets, formfield_callback, opts.localized_fields) + opts.widgets, formfield_callback, + opts.localized_fields, opts.labels, + opts.help_texts, opts.error_messages) # make sure opts.fields doesn't specify an invalid field none_model_fields = [k for k, v in six.iteritems(fields) if not v] @@ -416,7 +438,8 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)): pass def modelform_factory(model, form=ModelForm, fields=None, exclude=None, - formfield_callback=None, widgets=None, localized_fields=None): + formfield_callback=None, widgets=None, localized_fields=None, + labels=None, help_texts=None, error_messages=None): """ Returns a ModelForm containing form fields for the given model. @@ -434,6 +457,13 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, ``formfield_callback`` is a callable that takes a model field and returns a form field. + + ``labels`` is a dictionary of model field names mapped to a label. + + ``help_texts`` is a dictionary of model field names mapped to a help text. + + ``error_messages`` is a dictionary of model field names mapped to a + dictionary of error messages. """ # Create the inner Meta class. FIXME: ideally, we should be able to # construct a ModelForm without creating and passing in a temporary @@ -449,6 +479,12 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, attrs['widgets'] = widgets if localized_fields is not None: attrs['localized_fields'] = localized_fields + if labels is not None: + attrs['labels'] = labels + if help_texts is not None: + attrs['help_texts'] = help_texts + if error_messages is not None: + attrs['error_messages'] = error_messages # If parent form class already has an inner Meta, the Meta we're # creating needs to inherit from the parent's inner meta. @@ -738,7 +774,8 @@ class BaseModelFormSet(BaseFormSet): def modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, - widgets=None, validate_max=False, localized_fields=None): + widgets=None, validate_max=False, localized_fields=None, + labels=None, help_texts=None, error_messages=None): """ Returns a FormSet class for the given Django model class. """ @@ -759,7 +796,8 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None, form = modelform_factory(model, form=form, fields=fields, exclude=exclude, formfield_callback=formfield_callback, - widgets=widgets, localized_fields=localized_fields) + widgets=widgets, localized_fields=localized_fields, + labels=labels, help_texts=help_texts, error_messages=error_messages) FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, can_order=can_order, can_delete=can_delete, validate_max=validate_max) @@ -898,7 +936,8 @@ def inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, - widgets=None, validate_max=False, localized_fields=None): + widgets=None, validate_max=False, localized_fields=None, + labels=None, help_texts=None, error_messages=None): """ Returns an ``InlineFormSet`` for the given kwargs. @@ -922,6 +961,9 @@ def inlineformset_factory(parent_model, model, form=ModelForm, 'widgets': widgets, 'validate_max': validate_max, 'localized_fields': localized_fields, + 'labels': labels, + 'help_texts': help_texts, + 'error_messages': error_messages, } FormSet = modelformset_factory(model, **kwargs) FormSet.fk = fk diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index b54056af0c..840a896d75 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -5,7 +5,7 @@ Model Form Functions .. module:: django.forms.models :synopsis: Django's functions for building model forms and formsets. -.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None) +.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None, labels=None, help_texts=None, error_messages=None) Returns a :class:`~django.forms.ModelForm` class for the given ``model``. You can optionally pass a ``form`` argument to use as a starting point for @@ -20,11 +20,18 @@ Model Form Functions ``widgets`` is a dictionary of model field names mapped to a widget. - ``localized_fields`` is a list of names of fields which should be localized. - ``formfield_callback`` is a callable that takes a model field and returns a form field. + ``localized_fields`` is a list of names of fields which should be localized. + + ``labels`` is a dictionary of model field names mapped to a label. + + ``help_texts`` is a dictionary of model field names mapped to a help text. + + ``error_messages`` is a dictionary of model field names mapped to a + dictionary of error messages. + See :ref:`modelforms-factory` for example usage. .. versionchanged:: 1.6 @@ -35,14 +42,16 @@ Model Form Functions information. Omitting any definition of the fields to use will result in all fields being used, but this behavior is deprecated. - The ``localized_fields`` parameter was added. + The ``localized_fields``, ``labels``, ``help_texts``, and + ``error_messages`` parameters were added. -.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None) +.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None) Returns a ``FormSet`` class for the given ``model`` class. Arguments ``model``, ``form``, ``fields``, ``exclude``, - ``formfield_callback``, ``widgets`` and ``localized_fields`` are all passed through to + ``formfield_callback``, ``widgets``, ``localized_fields``, ``labels``, + ``help_texts``, and ``error_messages`` are all passed through to :func:`~django.forms.models.modelform_factory`. Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, @@ -54,9 +63,10 @@ Model Form Functions .. versionchanged:: 1.6 - The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. + The ``widgets``, ``validate_max``, ``localized_fields``, ``labels``, + ``help_texts``, and ``error_messages`` parameters were added. -.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None) +.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None, labels=None, help_texts=None, error_messages=None) Returns an ``InlineFormSet`` using :func:`modelformset_factory` with defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and @@ -69,4 +79,5 @@ Model Form Functions .. versionchanged:: 1.6 - The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. + The ``widgets``, ``validate_max`` and ``localized_fields``, ``labels``, + ``help_texts``, and ``error_messages`` parameters were added. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index e5cd3273dc..d3ba93c4a7 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -236,9 +236,14 @@ Minor features .. _`Pillow`: https://pypi.python.org/pypi/Pillow .. _`PIL`: https://pypi.python.org/pypi/PIL -* :doc:`ModelForm ` accepts a new - Meta option: ``localized_fields``. Fields included in this list will be localized - (by setting ``localize`` on the form field). +* :class:`~django.forms.ModelForm` accepts several new ``Meta`` + options. + + * Fields included in the ``localized_fields`` list will be localized + (by setting ``localize`` on the form field). + * The ``labels``, ``help_texts`` and ``error_messages`` options may be used + to customize the default fields, see + :ref:`modelforms-overriding-default-fields` for details. * The ``choices`` argument to model fields now accepts an iterable of iterables instead of requiring an iterable of lists or tuples. diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index d325a15cca..bd9e14aea4 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -141,7 +141,7 @@ In addition, each generated form field has attributes set as follows: ``default`` value will be initially selected instead). Finally, note that you can override the form field used for a given model -field. See `Overriding the default field types or widgets`_ below. +field. See `Overriding the default fields`_ below. A full example -------------- @@ -388,8 +388,10 @@ include that field. .. _section on saving forms: `The save() method`_ -Overriding the default field types or widgets ---------------------------------------------- +.. _modelforms-overriding-default-fields: + +Overriding the default fields +----------------------------- The default field types, as described in the `Field types`_ table above, are sensible defaults. If you have a ``DateField`` in your model, chances are you'd @@ -420,38 +422,65 @@ widget:: The ``widgets`` dictionary accepts either widget instances (e.g., ``Textarea(...)``) or classes (e.g., ``Textarea``). -If you want to further customize a field -- including its type, label, etc. -- -you can do this by declaratively specifying fields like you would in a regular -``Form``. Declared fields will override the default ones generated by using the -``model`` attribute. +.. versionadded:: 1.6 + + The ``labels``, ``help_texts`` and ``error_messages`` options were added. + +Similarly, you can specify the ``labels``, ``help_texts`` and ``error_messages`` +attributes of the inner ``Meta`` class if you want to further customize a field. -For example, if you wanted to use ``MyDateFormField`` for the ``pub_date`` +For example if you wanted to customize the wording of all user facing strings for +the ``name`` field:: + + class AuthorForm(ModelForm): + class Meta: + model = Author + fields = ('name', 'title', 'birth_date') + labels = { + 'name': _('Writer'), + } + help_texts = { + 'name': _('Some useful help text.'), + } + error_messages = { + 'name': { + 'max_length': _("This writer's name is too long."), + }, + } + +Finally, if you want complete control over of a field -- including its type, +validators, etc. -- you can do this by declaratively specifying fields like you +would in a regular ``Form``. Declared fields will override the default ones +generated by using the ``model`` attribute. Fields declared like this will +ignore any customizations in the ``widgets``, ``labels``, ``help_texts``, and +``error_messages`` options declared on ``Meta``. + +For example, if you wanted to use ``MySlugFormField`` for the ``slug`` field, you could do the following:: from django.forms import ModelForm from myapp.models import Article class ArticleForm(ModelForm): - pub_date = MyDateFormField() + slug = MySlugFormField() class Meta: model = Article fields = ['pub_date', 'headline', 'content', 'reporter'] -If you want to override a field's default label, then specify the ``label`` -parameter when declaring the form field:: +If you want to override a field's default validators, then specify the +``validators`` parameter when declaring the form field:: from django.forms import ModelForm, DateField from myapp.models import Article class ArticleForm(ModelForm): - pub_date = DateField(label='Publication date') + slug = CharField(validators=[validate_slug]) class Meta: model = Article - fields = ['pub_date', 'headline', 'content', 'reporter'] - + fields = ['pub_date', 'headline', 'content', 'reporter', 'slug'] .. note:: @@ -597,7 +626,7 @@ example by specifying the widgets to be used for a given field:: >>> from django.forms import Textarea >>> Form = modelform_factory(Book, form=BookForm, - widgets={"title": Textarea()}) + ... widgets={"title": Textarea()}) The fields to include can be specified using the ``fields`` and ``exclude`` keyword arguments, or the corresponding attributes on the ``ModelForm`` inner diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 509413fb61..aadc7a618e 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -490,7 +490,7 @@ class ModelFormBaseTest(TestCase): ['slug', 'name']) -class TestWidgetForm(forms.ModelForm): +class FieldOverridesTroughFormMetaForm(forms.ModelForm): class Meta: model = Category fields = ['name', 'url', 'slug'] @@ -498,25 +498,74 @@ class TestWidgetForm(forms.ModelForm): 'name': forms.Textarea, 'url': forms.TextInput(attrs={'class': 'url'}) } + labels = { + 'name': 'Title', + } + help_texts = { + 'slug': 'Watch out! Letters, numbers, underscores and hyphens only.', + } + error_messages = { + 'slug': { + 'invalid': ( + "Didn't you read the help text? " + "We said letters, numbers, underscores and hyphens only!" + ) + } + } +class TestFieldOverridesTroughFormMeta(TestCase): + def test_widget_overrides(self): + form = FieldOverridesTroughFormMetaForm() + self.assertHTMLEqual( + str(form['name']), + '', + ) + self.assertHTMLEqual( + str(form['url']), + '', + ) + self.assertHTMLEqual( + str(form['slug']), + '', + ) -class TestWidgets(TestCase): - def test_base_widgets(self): - frm = TestWidgetForm() + def test_label_overrides(self): + form = FieldOverridesTroughFormMetaForm() self.assertHTMLEqual( - str(frm['name']), - '' + str(form['name'].label_tag()), + '', ) self.assertHTMLEqual( - str(frm['url']), - '' + str(form['url'].label_tag()), + '', ) self.assertHTMLEqual( - str(frm['slug']), - '' + str(form['slug'].label_tag()), + '', ) + def test_help_text_overrides(self): + form = FieldOverridesTroughFormMetaForm() + self.assertEqual( + form['slug'].help_text, + 'Watch out! Letters, numbers, underscores and hyphens only.', + ) + + def test_error_messages_overrides(self): + form = FieldOverridesTroughFormMetaForm(data={ + 'name': 'Category', + 'url': '/category/', + 'slug': '!%#*@', + }) + form.full_clean() + + error = [ + "Didn't you read the help text? " + "We said letters, numbers, underscores and hyphens only!", + ] + self.assertEqual(form.errors, {'slug': error}) + class IncompleteCategoryFormWithFields(forms.ModelForm): """ diff --git a/tests/model_formsets/tests.py b/tests/model_formsets/tests.py index 8cfdf53995..03cd3b0159 100644 --- a/tests/model_formsets/tests.py +++ b/tests/model_formsets/tests.py @@ -1261,7 +1261,7 @@ class ModelFormsetTest(TestCase): ['Please correct the duplicate data for subtitle which must be unique for the month in posted.']) -class TestModelFormsetWidgets(TestCase): +class TestModelFormsetOverridesTroughFormMeta(TestCase): def test_modelformset_factory_widgets(self): widgets = { 'name': forms.TextInput(attrs={'class': 'poet'}) @@ -1283,3 +1283,53 @@ class TestModelFormsetWidgets(TestCase): "%s" % form['title'], '' ) + + def test_modelformset_factory_labels_overrides(self): + BookFormSet = modelformset_factory(Book, fields="__all__", labels={ + 'title': 'Name' + }) + form = BookFormSet.form() + self.assertHTMLEqual(form['title'].label_tag(), '') + + def test_inlineformset_factory_labels_overrides(self): + BookFormSet = inlineformset_factory(Author, Book, fields="__all__", labels={ + 'title': 'Name' + }) + form = BookFormSet.form() + self.assertHTMLEqual(form['title'].label_tag(), '') + + def test_modelformset_factory_help_text_overrides(self): + BookFormSet = modelformset_factory(Book, fields="__all__", help_texts={ + 'title': 'Choose carefully.' + }) + form = BookFormSet.form() + self.assertEqual(form['title'].help_text, 'Choose carefully.') + + def test_inlineformset_factory_help_text_overrides(self): + BookFormSet = inlineformset_factory(Author, Book, fields="__all__", help_texts={ + 'title': 'Choose carefully.' + }) + form = BookFormSet.form() + self.assertEqual(form['title'].help_text, 'Choose carefully.') + + def test_modelformset_factory_error_messages_overrides(self): + author = Author.objects.create(pk=1, name='Charles Baudelaire') + BookFormSet = modelformset_factory(Book, fields="__all__", error_messages={ + 'title': { + 'max_length': 'Title too long!!' + } + }) + form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id}) + form.full_clean() + self.assertEqual(form.errors, {'title': ['Title too long!!']}) + + def test_inlineformset_factory_error_messages_overrides(self): + author = Author.objects.create(pk=1, name='Charles Baudelaire') + BookFormSet = inlineformset_factory(Author, Book, fields="__all__", error_messages={ + 'title': { + 'max_length': 'Title too long!!' + } + }) + form = BookFormSet.form(data={'title': 'Foo ' * 30, 'author': author.id}) + form.full_clean() + self.assertEqual(form.errors, {'title': ['Title too long!!']}) -- cgit v1.3 From 46789e76c68b5713795fbf71ce5fdccb727074fa Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Fri, 14 Jun 2013 15:02:30 +0100 Subject: Fixed #20548 -- Removed all PendingDeprecationWarnings from django test suite --- tests/forms_tests/tests/tests.py | 6 ++++-- tests/model_forms/tests.py | 5 ++++- tests/model_forms_regress/tests.py | 4 +++- tests/runtests.py | 5 ++++- tests/test_runner/tests.py | 14 ++++++++------ tests/test_suite_override/tests.py | 5 +++-- tests/test_utils/tests.py | 6 +++--- 7 files changed, 29 insertions(+), 16 deletions(-) (limited to 'tests/model_forms') diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py index 2616ddaf7d..99a67c320c 100644 --- a/tests/forms_tests/tests/tests.py +++ b/tests/forms_tests/tests/tests.py @@ -208,7 +208,8 @@ class RelatedModelFormTests(TestCase): ref = models.ForeignKey("B") class Meta: - model=A + model = A + fields = '__all__' self.assertRaises(ValueError, ModelFormMetaclass, str('Form'), (ModelForm,), {'Meta': Meta}) @@ -226,7 +227,8 @@ class RelatedModelFormTests(TestCase): pass class Meta: - model=A + model = A + fields = '__all__' self.assertTrue(issubclass(ModelFormMetaclass(str('Form'), (ModelForm,), {'Meta': Meta}), ModelForm)) diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index aadc7a618e..eea1ef9b68 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -52,12 +52,15 @@ class PriceForm(forms.ModelForm): class BookForm(forms.ModelForm): class Meta: - model = Book + model = Book + fields = '__all__' class DerivedBookForm(forms.ModelForm): class Meta: model = DerivedBook + fields = '__all__' + class ExplicitPKForm(forms.ModelForm): diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py index 80900a59e0..39ae857219 100644 --- a/tests/model_forms_regress/tests.py +++ b/tests/model_forms_regress/tests.py @@ -97,12 +97,14 @@ class PartiallyLocalizedTripleForm(forms.ModelForm): class Meta: model = Triple localized_fields = ('left', 'right',) + fields = '__all__' class FullyLocalizedTripleForm(forms.ModelForm): class Meta: model = Triple - localized_fields = "__all__" + localized_fields = '__all__' + fields = '__all__' class LocalizedModelFormTest(TestCase): def test_model_form_applies_localize_to_some_fields(self): diff --git a/tests/runtests.py b/tests/runtests.py index 2414410375..da4592ecc0 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -5,6 +5,7 @@ import shutil import subprocess import sys import tempfile +import warnings from django import contrib from django.utils._os import upath @@ -107,7 +108,9 @@ def setup(verbosity, test_labels): logger.addHandler(handler) # Load all the ALWAYS_INSTALLED_APPS. - get_apps() + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'django.contrib.comments is deprecated and will be removed before Django 1.8.', PendingDeprecationWarning) + get_apps() # Load all the test model apps. test_modules = get_test_modules() diff --git a/tests/test_runner/tests.py b/tests/test_runner/tests.py index 74a557af26..03a3619c7e 100644 --- a/tests/test_runner/tests.py +++ b/tests/test_runner/tests.py @@ -10,8 +10,8 @@ from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django import db from django.test import runner, TransactionTestCase, skipUnlessDBFeature -from django.test.simple import DjangoTestSuiteRunner, get_tests from django.test.testcases import connections_support_transactions +from django.test.utils import IgnorePendingDeprecationWarningsMixin from django.utils import unittest from django.utils.importlib import import_module @@ -225,15 +225,17 @@ class Ticket17477RegressionTests(AdminScriptTestCase): self.assertNoOutput(err) -class ModulesTestsPackages(unittest.TestCase): +class ModulesTestsPackages(IgnorePendingDeprecationWarningsMixin, unittest.TestCase): def test_get_tests(self): "Check that the get_tests helper function can find tests in a directory" + from django.test.simple import get_tests module = import_module(TEST_APP_OK) tests = get_tests(module) self.assertIsInstance(tests, type(module)) def test_import_error(self): "Test for #12658 - Tests with ImportError's shouldn't fail silently" + from django.test.simple import get_tests module = import_module(TEST_APP_ERROR) self.assertRaises(ImportError, get_tests, module) @@ -258,7 +260,7 @@ class Sqlite3InMemoryTestDbs(unittest.TestCase): }, }) other = db.connections['other'] - DjangoTestSuiteRunner(verbosity=0).setup_databases() + runner.DiscoverRunner(verbosity=0).setup_databases() msg = "DATABASES setting '%s' option set to sqlite3's ':memory:' value shouldn't interfere with transaction support detection." % option # Transaction support should be properly initialised for the 'other' DB self.assertTrue(other.features.supports_transactions, msg) @@ -273,12 +275,12 @@ class DummyBackendTest(unittest.TestCase): """ Test that setup_databases() doesn't fail with dummy database backend. """ - runner = DjangoTestSuiteRunner(verbosity=0) + runner_instance = runner.DiscoverRunner(verbosity=0) old_db_connections = db.connections try: db.connections = db.ConnectionHandler({}) - old_config = runner.setup_databases() - runner.teardown_databases(old_config) + old_config = runner_instance.setup_databases() + runner_instance.teardown_databases(old_config) except Exception as e: self.fail("setup_databases/teardown_databases unexpectedly raised " "an error: %s" % e) diff --git a/tests/test_suite_override/tests.py b/tests/test_suite_override/tests.py index dea110434f..35ca2b060b 100644 --- a/tests/test_suite_override/tests.py +++ b/tests/test_suite_override/tests.py @@ -1,5 +1,5 @@ from django.db.models import get_app -from django.test.simple import build_suite +from django.test.utils import IgnorePendingDeprecationWarningsMixin from django.utils import unittest @@ -9,7 +9,7 @@ def suite(): return testSuite -class SuiteOverrideTest(unittest.TestCase): +class SuiteOverrideTest(IgnorePendingDeprecationWarningsMixin, unittest.TestCase): def test_suite_override(self): """ Validate that you can define a custom suite when running tests with @@ -17,6 +17,7 @@ class SuiteOverrideTest(unittest.TestCase): suite using ``build_suite``). """ + from django.test.simple import build_suite app = get_app("test_suite_override") suite = build_suite(app) self.assertEqual(suite.countTestCases(), 1) diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index d72a482819..5f7fb05eb4 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -8,8 +8,7 @@ from django.http import HttpResponse from django.template.loader import render_to_string from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.test.html import HTMLParseError, parse_html -from django.test.simple import make_doctest -from django.test.utils import CaptureQueriesContext +from django.test.utils import CaptureQueriesContext, IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils import unittest from django.utils.unittest import skip @@ -624,9 +623,10 @@ class AssertFieldOutputTests(SimpleTestCase): self.assertFieldOutput(MyCustomField, {}, {}, empty_value=None) -class DoctestNormalizerTest(SimpleTestCase): +class DoctestNormalizerTest(IgnorePendingDeprecationWarningsMixin, SimpleTestCase): def test_normalizer(self): + from django.test.simple import make_doctest suite = make_doctest("test_utils.doctest_output") failures = unittest.TextTestRunner(stream=six.StringIO()).run(suite) self.assertEqual(failures.failures, []) -- cgit v1.3 From ee77d4b25360a9fc050c32769a334fd69a011a63 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Wed, 5 Jun 2013 14:55:05 -0400 Subject: Fixed #20199 -- Allow ModelForm fields to override error_messages from model fields --- django/contrib/admin/forms.py | 9 +- django/contrib/admin/options.py | 10 +-- django/contrib/auth/forms.py | 33 ++++++-- django/contrib/flatpages/forms.py | 16 +++- django/contrib/formtools/wizard/views.py | 5 +- django/contrib/gis/forms/fields.py | 6 +- django/contrib/sites/models.py | 4 +- django/core/exceptions.py | 4 +- django/core/validators.py | 8 +- django/db/models/fields/__init__.py | 136 ++++++++++++++++++++----------- django/db/models/fields/related.py | 7 +- django/forms/fields.py | 96 ++++++++++++++-------- django/forms/formsets.py | 9 +- django/forms/models.py | 32 ++++++-- django/forms/util.py | 13 ++- django/utils/ipv6.py | 5 +- docs/ref/forms/validation.txt | 110 +++++++++++++++++++++++-- docs/ref/models/instances.txt | 14 +++- docs/releases/1.6.txt | 7 ++ tests/model_forms/models.py | 10 +++ tests/model_forms/tests.py | 20 ++++- tests/validators/tests.py | 4 +- 22 files changed, 406 insertions(+), 152 deletions(-) (limited to 'tests/model_forms') diff --git a/django/contrib/admin/forms.py b/django/contrib/admin/forms.py index 38c445f71a..6814fc9083 100644 --- a/django/contrib/admin/forms.py +++ b/django/contrib/admin/forms.py @@ -22,15 +22,12 @@ class AdminAuthenticationForm(AuthenticationForm): username = self.cleaned_data.get('username') password = self.cleaned_data.get('password') message = ERROR_MESSAGE + params = {'username': self.username_field.verbose_name} if username and password: self.user_cache = authenticate(username=username, password=password) if self.user_cache is None: - raise forms.ValidationError(message % { - 'username': self.username_field.verbose_name - }) + raise forms.ValidationError(message, code='invalid', params=params) elif not self.user_cache.is_active or not self.user_cache.is_staff: - raise forms.ValidationError(message % { - 'username': self.username_field.verbose_name - }) + raise forms.ValidationError(message, code='invalid', params=params) return self.cleaned_data diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 34583ebf74..be9671a76e 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1574,13 +1574,13 @@ class InlineModelAdmin(BaseModelAdmin): 'class_name': p._meta.verbose_name, 'instance': p} ) - msg_dict = {'class_name': self._meta.model._meta.verbose_name, - 'instance': self.instance, - 'related_objects': get_text_list(objs, _('and'))} + params = {'class_name': self._meta.model._meta.verbose_name, + 'instance': self.instance, + 'related_objects': get_text_list(objs, _('and'))} msg = _("Deleting %(class_name)s %(instance)s would require " "deleting the following protected related objects: " - "%(related_objects)s") % msg_dict - raise ValidationError(msg) + "%(related_objects)s") + raise ValidationError(msg, code='deleting_protected', params=params) def is_valid(self): result = super(DeleteProtectedModelForm, self).is_valid() diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index edf2727b07..028ec8eafc 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -97,14 +97,19 @@ class UserCreationForm(forms.ModelForm): User._default_manager.get(username=username) except User.DoesNotExist: return username - raise forms.ValidationError(self.error_messages['duplicate_username']) + raise forms.ValidationError( + self.error_messages['duplicate_username'], + code='duplicate_username', + ) def clean_password2(self): password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: raise forms.ValidationError( - self.error_messages['password_mismatch']) + self.error_messages['password_mismatch'], + code='password_mismatch', + ) return password2 def save(self, commit=True): @@ -183,11 +188,15 @@ class AuthenticationForm(forms.Form): password=password) if self.user_cache is None: raise forms.ValidationError( - self.error_messages['invalid_login'] % { - 'username': self.username_field.verbose_name - }) + self.error_messages['invalid_login'], + code='invalid_login', + params={'username': self.username_field.verbose_name}, + ) elif not self.user_cache.is_active: - raise forms.ValidationError(self.error_messages['inactive']) + raise forms.ValidationError( + self.error_messages['inactive'], + code='inactive', + ) return self.cleaned_data def check_for_test_cookie(self): @@ -269,7 +278,9 @@ class SetPasswordForm(forms.Form): if password1 and password2: if password1 != password2: raise forms.ValidationError( - self.error_messages['password_mismatch']) + self.error_messages['password_mismatch'], + code='password_mismatch', + ) return password2 def save(self, commit=True): @@ -298,7 +309,9 @@ class PasswordChangeForm(SetPasswordForm): old_password = self.cleaned_data["old_password"] if not self.user.check_password(old_password): raise forms.ValidationError( - self.error_messages['password_incorrect']) + self.error_messages['password_incorrect'], + code='password_incorrect', + ) return old_password PasswordChangeForm.base_fields = SortedDict([ @@ -329,7 +342,9 @@ class AdminPasswordChangeForm(forms.Form): if password1 and password2: if password1 != password2: raise forms.ValidationError( - self.error_messages['password_mismatch']) + self.error_messages['password_mismatch'], + code='password_mismatch', + ) return password2 def save(self, commit=True): diff --git a/django/contrib/flatpages/forms.py b/django/contrib/flatpages/forms.py index 80938116ad..a93a494096 100644 --- a/django/contrib/flatpages/forms.py +++ b/django/contrib/flatpages/forms.py @@ -17,11 +17,17 @@ class FlatpageForm(forms.ModelForm): def clean_url(self): url = self.cleaned_data['url'] if not url.startswith('/'): - raise forms.ValidationError(ugettext("URL is missing a leading slash.")) + raise forms.ValidationError( + ugettext("URL is missing a leading slash."), + code='missing_leading_slash', + ) if (settings.APPEND_SLASH and 'django.middleware.common.CommonMiddleware' in settings.MIDDLEWARE_CLASSES and not url.endswith('/')): - raise forms.ValidationError(ugettext("URL is missing a trailing slash.")) + raise forms.ValidationError( + ugettext("URL is missing a trailing slash."), + code='missing_trailing_slash', + ) return url def clean(self): @@ -36,7 +42,9 @@ class FlatpageForm(forms.ModelForm): for site in sites: if same_url.filter(sites=site).exists(): raise forms.ValidationError( - _('Flatpage with url %(url)s already exists for site %(site)s') % - {'url': url, 'site': site}) + _('Flatpage with url %(url)s already exists for site %(site)s'), + code='duplicate_url', + params={'url': url, 'site': site}, + ) return super(FlatpageForm, self).clean() diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py index 17cfa6baa7..c478f20854 100644 --- a/django/contrib/formtools/wizard/views.py +++ b/django/contrib/formtools/wizard/views.py @@ -7,6 +7,7 @@ from django.forms import formsets, ValidationError from django.views.generic import TemplateView from django.utils.datastructures import SortedDict from django.utils.decorators import classonlymethod +from django.utils.translation import ugettext as _ from django.utils import six from django.contrib.formtools.wizard.storage import get_storage @@ -271,7 +272,9 @@ class WizardView(TemplateView): management_form = ManagementForm(self.request.POST, prefix=self.prefix) if not management_form.is_valid(): raise ValidationError( - 'ManagementForm data is missing or has been tampered.') + _('ManagementForm data is missing or has been tampered.'), + code='missing_management_form', + ) form_current_step = management_form.cleaned_data['current_step'] if (form_current_step != self.steps.current and diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py index 6e2cbd59f5..59e725926c 100644 --- a/django/contrib/gis/forms/fields.py +++ b/django/contrib/gis/forms/fields.py @@ -50,7 +50,7 @@ class GeometryField(forms.Field): try: return GEOSGeometry(value) except (GEOSException, ValueError, TypeError): - raise forms.ValidationError(self.error_messages['invalid_geom']) + raise forms.ValidationError(self.error_messages['invalid_geom'], code='invalid_geom') def clean(self, value): """ @@ -65,7 +65,7 @@ class GeometryField(forms.Field): # Ensuring that the geometry is of the correct type (indicated # using the OGC string label). if str(geom.geom_type).upper() != self.geom_type and not self.geom_type == 'GEOMETRY': - raise forms.ValidationError(self.error_messages['invalid_geom_type']) + raise forms.ValidationError(self.error_messages['invalid_geom_type'], code='invalid_geom_type') # Transforming the geometry if the SRID was set. if self.srid: @@ -76,7 +76,7 @@ class GeometryField(forms.Field): try: geom.transform(self.srid) except: - raise forms.ValidationError(self.error_messages['transform_error']) + raise forms.ValidationError(self.error_messages['transform_error'], code='transform_error') return geom diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py index 879497deb3..bbd85ed3f6 100644 --- a/django/contrib/sites/models.py +++ b/django/contrib/sites/models.py @@ -22,7 +22,9 @@ def _simple_domain_name_validator(value): checks = ((s in value) for s in string.whitespace) if any(checks): raise ValidationError( - _("The domain name cannot contain any spaces or tabs.")) + _("The domain name cannot contain any spaces or tabs."), + code='invalid', + ) class SiteManager(models.Manager): diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 5975164d07..829d6e774e 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -117,9 +117,7 @@ class ValidationError(Exception): message = message.message if params: message %= params - message = force_text(message) - else: - message = force_text(message) + message = force_text(message) messages.append(message) return messages diff --git a/django/core/validators.py b/django/core/validators.py index d0b713be32..200d28fe02 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -76,7 +76,7 @@ def validate_integer(value): try: int(value) except (ValueError, TypeError): - raise ValidationError('') + raise ValidationError(_('Enter a valid integer.'), code='invalid') class EmailValidator(object): @@ -188,11 +188,7 @@ class BaseValidator(object): cleaned = self.clean(value) params = {'limit_value': self.limit_value, 'show_value': cleaned} if self.compare(cleaned, self.limit_value): - raise ValidationError( - self.message % params, - code=self.code, - params=params, - ) + raise ValidationError(self.message, code=self.code, params=params) class MaxValueValidator(BaseValidator): diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 25ceb3f868..c0dce2e58e 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -77,7 +77,7 @@ class Field(object): auto_creation_counter = -1 default_validators = [] # Default set of validators default_error_messages = { - 'invalid_choice': _('Value %r is not a valid choice.'), + 'invalid_choice': _('Value %(value)r is not a valid choice.'), 'null': _('This field cannot be null.'), 'blank': _('This field cannot be blank.'), 'unique': _('%(model_name)s with this %(field_label)s ' @@ -233,14 +233,17 @@ class Field(object): return elif value == option_key: return - msg = self.error_messages['invalid_choice'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid_choice'], + code='invalid_choice', + params={'value': value}, + ) if value is None and not self.null: - raise exceptions.ValidationError(self.error_messages['null']) + raise exceptions.ValidationError(self.error_messages['null'], code='null') if not self.blank and value in self.empty_values: - raise exceptions.ValidationError(self.error_messages['blank']) + raise exceptions.ValidationError(self.error_messages['blank'], code='blank') def clean(self, value, model_instance): """ @@ -568,7 +571,7 @@ class AutoField(Field): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value must be an integer."), + 'invalid': _("'%(value)s' value must be an integer."), } def __init__(self, *args, **kwargs): @@ -586,8 +589,11 @@ class AutoField(Field): try: return int(value) except (TypeError, ValueError): - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def validate(self, value, model_instance): pass @@ -616,7 +622,7 @@ class AutoField(Field): class BooleanField(Field): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value must be either True or False."), + 'invalid': _("'%(value)s' value must be either True or False."), } description = _("Boolean (Either True or False)") @@ -636,8 +642,11 @@ class BooleanField(Field): return True if value in ('f', 'False', '0'): return False - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def get_prep_lookup(self, lookup_type, value): # Special-case handling for filters coming from a Web request (e.g. the @@ -709,9 +718,9 @@ class CommaSeparatedIntegerField(CharField): class DateField(Field): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value has an invalid date format. It must be " + 'invalid': _("'%(value)s' value has an invalid date format. It must be " "in YYYY-MM-DD format."), - 'invalid_date': _("'%s' value has the correct format (YYYY-MM-DD) " + 'invalid_date': _("'%(value)s' value has the correct format (YYYY-MM-DD) " "but it is an invalid date."), } description = _("Date (without time)") @@ -745,11 +754,17 @@ class DateField(Field): if parsed is not None: return parsed except ValueError: - msg = self.error_messages['invalid_date'] % value - raise exceptions.ValidationError(msg) - - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid_date'], + code='invalid_date', + params={'value': value}, + ) + + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def pre_save(self, model_instance, add): if self.auto_now or (self.auto_now_add and add): @@ -797,11 +812,11 @@ class DateField(Field): class DateTimeField(DateField): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value has an invalid format. It must be in " + 'invalid': _("'%(value)s' value has an invalid format. It must be in " "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."), - 'invalid_date': _("'%s' value has the correct format " + 'invalid_date': _("'%(value)s' value has the correct format " "(YYYY-MM-DD) but it is an invalid date."), - 'invalid_datetime': _("'%s' value has the correct format " + 'invalid_datetime': _("'%(value)s' value has the correct format " "(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) " "but it is an invalid date/time."), } @@ -836,19 +851,28 @@ class DateTimeField(DateField): if parsed is not None: return parsed except ValueError: - msg = self.error_messages['invalid_datetime'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid_datetime'], + code='invalid_datetime', + params={'value': value}, + ) try: parsed = parse_date(value) if parsed is not None: return datetime.datetime(parsed.year, parsed.month, parsed.day) except ValueError: - msg = self.error_messages['invalid_date'] % value - raise exceptions.ValidationError(msg) - - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid_date'], + code='invalid_date', + params={'value': value}, + ) + + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def pre_save(self, model_instance, add): if self.auto_now or (self.auto_now_add and add): @@ -894,7 +918,7 @@ class DateTimeField(DateField): class DecimalField(Field): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value must be a decimal number."), + 'invalid': _("'%(value)s' value must be a decimal number."), } description = _("Decimal number") @@ -912,8 +936,11 @@ class DecimalField(Field): try: return decimal.Decimal(value) except decimal.InvalidOperation: - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def _format(self, value): if isinstance(value, six.string_types) or value is None: @@ -999,7 +1026,7 @@ class FilePathField(Field): class FloatField(Field): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value must be a float."), + 'invalid': _("'%(value)s' value must be a float."), } description = _("Floating point number") @@ -1017,8 +1044,11 @@ class FloatField(Field): try: return float(value) except (TypeError, ValueError): - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def formfield(self, **kwargs): defaults = {'form_class': forms.FloatField} @@ -1028,7 +1058,7 @@ class FloatField(Field): class IntegerField(Field): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value must be an integer."), + 'invalid': _("'%(value)s' value must be an integer."), } description = _("Integer") @@ -1052,8 +1082,11 @@ class IntegerField(Field): try: return int(value) except (TypeError, ValueError): - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def formfield(self, **kwargs): defaults = {'form_class': forms.IntegerField} @@ -1135,7 +1168,7 @@ class GenericIPAddressField(Field): class NullBooleanField(Field): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value must be either None, True or False."), + 'invalid': _("'%(value)s' value must be either None, True or False."), } description = _("Boolean (Either True, False or None)") @@ -1158,8 +1191,11 @@ class NullBooleanField(Field): return True if value in ('f', 'False', '0'): return False - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def get_prep_lookup(self, lookup_type, value): # Special-case handling for filters coming from a Web request (e.g. the @@ -1251,9 +1287,9 @@ class TextField(Field): class TimeField(Field): empty_strings_allowed = False default_error_messages = { - 'invalid': _("'%s' value has an invalid format. It must be in " + 'invalid': _("'%(value)s' value has an invalid format. It must be in " "HH:MM[:ss[.uuuuuu]] format."), - 'invalid_time': _("'%s' value has the correct format " + 'invalid_time': _("'%(value)s' value has the correct format " "(HH:MM[:ss[.uuuuuu]]) but it is an invalid time."), } description = _("Time") @@ -1285,11 +1321,17 @@ class TimeField(Field): if parsed is not None: return parsed except ValueError: - msg = self.error_messages['invalid_time'] % value - raise exceptions.ValidationError(msg) - - msg = self.error_messages['invalid'] % value - raise exceptions.ValidationError(msg) + raise exceptions.ValidationError( + self.error_messages['invalid_time'], + code='invalid_time', + params={'value': value}, + ) + + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'value': value}, + ) def pre_save(self, model_instance, add): if self.auto_now or (self.auto_now_add and add): diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 1b79e5bade..80249817e2 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1173,8 +1173,11 @@ class ForeignKey(ForeignObject): ) qs = qs.complex_filter(self.rel.limit_choices_to) if not qs.exists(): - raise exceptions.ValidationError(self.error_messages['invalid'] % { - 'model': self.rel.to._meta.verbose_name, 'pk': value}) + raise exceptions.ValidationError( + self.error_messages['invalid'], + code='invalid', + params={'model': self.rel.to._meta.verbose_name, 'pk': value}, + ) def get_attname(self): return '%s_id' % self.name diff --git a/django/forms/fields.py b/django/forms/fields.py index 219c82100e..52bcf9485c 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -125,7 +125,7 @@ class Field(object): def validate(self, value): if value in self.empty_values and self.required: - raise ValidationError(self.error_messages['required']) + raise ValidationError(self.error_messages['required'], code='required') def run_validators(self, value): if value in self.empty_values: @@ -246,7 +246,7 @@ class IntegerField(Field): try: value = int(str(value)) except (ValueError, TypeError): - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') return value def widget_attrs(self, widget): @@ -277,7 +277,7 @@ class FloatField(IntegerField): try: value = float(value) except (ValueError, TypeError): - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') return value def widget_attrs(self, widget): @@ -323,7 +323,7 @@ class DecimalField(IntegerField): try: value = Decimal(value) except DecimalException: - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') return value def validate(self, value): @@ -334,7 +334,7 @@ class DecimalField(IntegerField): # since it is never equal to itself. However, NaN is the only value that # isn't equal to itself, so we can use this to identify NaN if value != value or value == Decimal("Inf") or value == Decimal("-Inf"): - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') sign, digittuple, exponent = value.as_tuple() decimals = abs(exponent) # digittuple doesn't include any leading zeros. @@ -348,15 +348,24 @@ class DecimalField(IntegerField): whole_digits = digits - decimals if self.max_digits is not None and digits > self.max_digits: - raise ValidationError(self.error_messages['max_digits'] % { - 'max': self.max_digits}) + raise ValidationError( + self.error_messages['max_digits'], + code='max_digits', + params={'max': self.max_digits}, + ) if self.decimal_places is not None and decimals > self.decimal_places: - raise ValidationError(self.error_messages['max_decimal_places'] % { - 'max': self.decimal_places}) + raise ValidationError( + self.error_messages['max_decimal_places'], + code='max_decimal_places', + params={'max': self.decimal_places}, + ) if (self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places)): - raise ValidationError(self.error_messages['max_whole_digits'] % { - 'max': (self.max_digits - self.decimal_places)}) + raise ValidationError( + self.error_messages['max_whole_digits'], + code='max_whole_digits', + params={'max': (self.max_digits - self.decimal_places)}, + ) return value def widget_attrs(self, widget): @@ -391,7 +400,7 @@ class BaseTemporalField(Field): return self.strptime(value, format) except (ValueError, TypeError): continue - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') def strptime(self, value, format): raise NotImplementedError('Subclasses must define this method.') @@ -471,7 +480,7 @@ class DateTimeField(BaseTemporalField): # Input comes from a SplitDateTimeWidget, for example. So, it's two # components: date and time. if len(value) != 2: - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') if value[0] in self.empty_values and value[1] in self.empty_values: return None value = '%s %s' % tuple(value) @@ -548,22 +557,22 @@ class FileField(Field): file_name = data.name file_size = data.size except AttributeError: - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') if self.max_length is not None and len(file_name) > self.max_length: - error_values = {'max': self.max_length, 'length': len(file_name)} - raise ValidationError(self.error_messages['max_length'] % error_values) + params = {'max': self.max_length, 'length': len(file_name)} + raise ValidationError(self.error_messages['max_length'], code='max_length', params=params) if not file_name: - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') if not self.allow_empty_file and not file_size: - raise ValidationError(self.error_messages['empty']) + raise ValidationError(self.error_messages['empty'], code='empty') return data def clean(self, data, initial=None): # If the widget got contradictory inputs, we raise a validation error if data is FILE_INPUT_CONTRADICTION: - raise ValidationError(self.error_messages['contradiction']) + raise ValidationError(self.error_messages['contradiction'], code='contradiction') # False means the field value should be cleared; further validation is # not needed. if data is False: @@ -623,7 +632,10 @@ class ImageField(FileField): Image.open(file).verify() except Exception: # Pillow (or PIL) doesn't recognize it as an image. - six.reraise(ValidationError, ValidationError(self.error_messages['invalid_image']), sys.exc_info()[2]) + six.reraise(ValidationError, ValidationError( + self.error_messages['invalid_image'], + code='invalid_image', + ), sys.exc_info()[2]) if hasattr(f, 'seek') and callable(f.seek): f.seek(0) return f @@ -648,7 +660,7 @@ class URLField(CharField): except ValueError: # urlparse.urlsplit can raise a ValueError with some # misformatted URLs. - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') value = super(URLField, self).to_python(value) if value: @@ -692,7 +704,7 @@ class BooleanField(Field): def validate(self, value): if not value and self.required: - raise ValidationError(self.error_messages['required']) + raise ValidationError(self.error_messages['required'], code='required') def _has_changed(self, initial, data): # Sometimes data or initial could be None or '' which should be the @@ -776,7 +788,11 @@ class ChoiceField(Field): """ super(ChoiceField, self).validate(value) if value and not self.valid_value(value): - raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) + raise ValidationError( + self.error_messages['invalid_choice'], + code='invalid_choice', + params={'value': value}, + ) def valid_value(self, value): "Check to see if the provided value is a valid choice" @@ -810,7 +826,11 @@ class TypedChoiceField(ChoiceField): try: value = self.coerce(value) except (ValueError, TypeError, ValidationError): - raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) + raise ValidationError( + self.error_messages['invalid_choice'], + code='invalid_choice', + params={'value': value}, + ) return value @@ -826,7 +846,7 @@ class MultipleChoiceField(ChoiceField): if not value: return [] elif not isinstance(value, (list, tuple)): - raise ValidationError(self.error_messages['invalid_list']) + raise ValidationError(self.error_messages['invalid_list'], code='invalid_list') return [smart_text(val) for val in value] def validate(self, value): @@ -834,11 +854,15 @@ class MultipleChoiceField(ChoiceField): Validates that the input is a list or tuple. """ if self.required and not value: - raise ValidationError(self.error_messages['required']) + raise ValidationError(self.error_messages['required'], code='required') # Validate that each value in the value list is in self.choices. for val in value: if not self.valid_value(val): - raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) + raise ValidationError( + self.error_messages['invalid_choice'], + code='invalid_choice', + params={'value': val}, + ) def _has_changed(self, initial, data): if initial is None: @@ -871,14 +895,18 @@ class TypedMultipleChoiceField(MultipleChoiceField): try: new_value.append(self.coerce(choice)) except (ValueError, TypeError, ValidationError): - raise ValidationError(self.error_messages['invalid_choice'] % {'value': choice}) + raise ValidationError( + self.error_messages['invalid_choice'], + code='invalid_choice', + params={'value': choice}, + ) return new_value def validate(self, value): if value != self.empty_value: super(TypedMultipleChoiceField, self).validate(value) elif self.required: - raise ValidationError(self.error_messages['required']) + raise ValidationError(self.error_messages['required'], code='required') class ComboField(Field): @@ -952,18 +980,18 @@ class MultiValueField(Field): if not value or isinstance(value, (list, tuple)): if not value or not [v for v in value if v not in self.empty_values]: if self.required: - raise ValidationError(self.error_messages['required']) + raise ValidationError(self.error_messages['required'], code='required') else: return self.compress([]) else: - raise ValidationError(self.error_messages['invalid']) + raise ValidationError(self.error_messages['invalid'], code='invalid') for i, field in enumerate(self.fields): try: field_value = value[i] except IndexError: field_value = None if self.required and field_value in self.empty_values: - raise ValidationError(self.error_messages['required']) + raise ValidationError(self.error_messages['required'], code='required') try: clean_data.append(field.clean(field_value)) except ValidationError as e: @@ -1078,9 +1106,9 @@ class SplitDateTimeField(MultiValueField): # Raise a validation error if time or date is empty # (possible if SplitDateTimeField has required=False). if data_list[0] in self.empty_values: - raise ValidationError(self.error_messages['invalid_date']) + raise ValidationError(self.error_messages['invalid_date'], code='invalid_date') if data_list[1] in self.empty_values: - raise ValidationError(self.error_messages['invalid_time']) + raise ValidationError(self.error_messages['invalid_time'], code='invalid_time') result = datetime.datetime.combine(*data_list) return from_current_timezone(result) return None diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 79e1c2298a..edd362c595 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -85,7 +85,10 @@ class BaseFormSet(object): if self.is_bound: form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix) if not form.is_valid(): - raise ValidationError('ManagementForm data is missing or has been tampered with') + raise ValidationError( + _('ManagementForm data is missing or has been tampered with'), + code='missing_management_form', + ) else: form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={ TOTAL_FORM_COUNT: self.total_form_count(), @@ -315,7 +318,9 @@ class BaseFormSet(object): self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max: raise ValidationError(ungettext( "Please submit %d or fewer forms.", - "Please submit %d or fewer forms.", self.max_num) % self.max_num) + "Please submit %d or fewer forms.", self.max_num) % self.max_num, + code='too_many_forms', + ) # Give self.clean() a chance to do cross-form validation. self.clean() except ValidationError as e: diff --git a/django/forms/models.py b/django/forms/models.py index c4070b97fe..821f64199b 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -314,7 +314,17 @@ class BaseModelForm(BaseForm): super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data, error_class, label_suffix, empty_permitted) - def _update_errors(self, message_dict): + def _update_errors(self, errors): + for field, messages in errors.error_dict.items(): + if field not in self.fields: + continue + field = self.fields[field] + for message in messages: + if isinstance(message, ValidationError): + if message.code in field.error_messages: + message.message = field.error_messages[message.code] + + message_dict = errors.message_dict for k, v in message_dict.items(): if k != NON_FIELD_ERRORS: self._errors.setdefault(k, self.error_class()).extend(v) @@ -1000,7 +1010,7 @@ class InlineForeignKeyField(Field): else: orig = self.parent_instance.pk if force_text(value) != force_text(orig): - raise ValidationError(self.error_messages['invalid_choice']) + raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice') return self.parent_instance def _has_changed(self, initial, data): @@ -1115,7 +1125,7 @@ class ModelChoiceField(ChoiceField): key = self.to_field_name or 'pk' value = self.queryset.get(**{key: value}) except (ValueError, self.queryset.model.DoesNotExist): - raise ValidationError(self.error_messages['invalid_choice']) + raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice') return value def validate(self, value): @@ -1150,22 +1160,30 @@ class ModelMultipleChoiceField(ModelChoiceField): def clean(self, value): if self.required and not value: - raise ValidationError(self.error_messages['required']) + raise ValidationError(self.error_messages['required'], code='required') elif not self.required and not value: return self.queryset.none() if not isinstance(value, (list, tuple)): - raise ValidationError(self.error_messages['list']) + raise ValidationError(self.error_messages['list'], code='list') key = self.to_field_name or 'pk' for pk in value: try: self.queryset.filter(**{key: pk}) except ValueError: - raise ValidationError(self.error_messages['invalid_pk_value'] % {'pk': pk}) + raise ValidationError( + self.error_messages['invalid_pk_value'], + code='invalid_pk_value', + params={'pk': pk}, + ) qs = self.queryset.filter(**{'%s__in' % key: value}) pks = set([force_text(getattr(o, key)) for o in qs]) for val in value: if force_text(val) not in pks: - raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) + raise ValidationError( + self.error_messages['invalid_choice'], + code='invalid_choice', + params={'value': val}, + ) # Since this overrides the inherited ModelChoiceField.clean # we run custom validators here self.run_validators(value) diff --git a/django/forms/util.py b/django/forms/util.py index f1b864e6b3..568cdd1086 100644 --- a/django/forms/util.py +++ b/django/forms/util.py @@ -80,12 +80,17 @@ def from_current_timezone(value): try: return timezone.make_aware(value, current_timezone) except Exception: - msg = _( + message = _( '%(datetime)s couldn\'t be interpreted ' 'in time zone %(current_timezone)s; it ' - 'may be ambiguous or it may not exist.') % {'datetime': value, 'current_timezone': - current_timezone} - six.reraise(ValidationError, ValidationError(msg), sys.exc_info()[2]) + 'may be ambiguous or it may not exist.' + ) + params = {'datetime': value, 'current_timezone': current_timezone} + six.reraise(ValidationError, ValidationError( + message, + code='ambiguous_timezone', + params=params, + ), sys.exc_info()[2]) return value def to_current_timezone(value): diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py index eaacfb4623..4d5352272b 100644 --- a/django/utils/ipv6.py +++ b/django/utils/ipv6.py @@ -2,10 +2,11 @@ # Copyright 2007 Google Inc. http://code.google.com/p/ipaddr-py/ # Licensed under the Apache License, Version 2.0 (the "License"). from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ from django.utils.six.moves import xrange def clean_ipv6_address(ip_str, unpack_ipv4=False, - error_message="This is not a valid IPv6 address."): + error_message=_("This is not a valid IPv6 address.")): """ Cleans a IPv6 address string. @@ -31,7 +32,7 @@ def clean_ipv6_address(ip_str, unpack_ipv4=False, doublecolon_len = 0 if not is_valid_ipv6_address(ip_str): - raise ValidationError(error_message) + raise ValidationError(error_message, code='invalid') # This algorithm can only handle fully exploded # IP strings diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 87c9764f64..8ab1c26831 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -12,13 +12,11 @@ validation (accessing the ``errors`` attribute or calling ``full_clean()`` directly), but normally they won't be needed. In general, any cleaning method can raise ``ValidationError`` if there is a -problem with the data it is processing, passing the relevant error message to -the ``ValidationError`` constructor. If no ``ValidationError`` is raised, the -method should return the cleaned (normalized) data as a Python object. - -If you detect multiple errors during a cleaning method and wish to signal all -of them to the form submitter, it is possible to pass a list of errors to the -``ValidationError`` constructor. +problem with the data it is processing, passing the relevant information to +the ``ValidationError`` constructor. :ref:`See below ` +for the best practice in raising ``ValidationError``. If no ``ValidationError`` +is raised, the method should return the cleaned (normalized) data as a Python +object. Most validation can be done using `validators`_ - simple helpers that can be reused easily. Validators are simple functions (or callables) that take a single @@ -87,7 +85,8 @@ overridden: "field" (called ``__all__``), which you can access via the ``non_field_errors()`` method if you need to. If you want to attach errors to a specific field in the form, you will need to access the - ``_errors`` attribute on the form, which is `described later`_. + ``_errors`` attribute on the form, which is + :ref:`described later `. Also note that there are special considerations when overriding the ``clean()`` method of a ``ModelForm`` subclass. (see the @@ -116,7 +115,100 @@ should iterate through ``self.cleaned_data.items()``, possibly considering the ``_errors`` dictionary attribute on the form as well. In this way, you will already know which fields have passed their individual validation requirements. -.. _described later: +.. _raising-validation-error: + +Raising ``ValidationError`` +--------------------------- + +.. versionchanged:: 1.6 + +In order to make error messages flexible and easy to override, consider the +following guidelines: + +* Provide a descriptive error ``code`` to the constructor:: + + # Good + ValidationError(_('Invalid value'), code='invalid') + + # Bad + ValidationError(_('Invalid value')) + +* Don't coerce variables into the message; use placeholders and the ``params`` + argument of the constructor:: + + # Good + ValidationError( + _('Invalid value: %(value)s'), + params={'value': '42'}, + ) + + # Bad + ValidationError(_('Invalid value: %s') % value) + +* Use mapping keys instead of positional formatting. This enables putting + the variables in any order or omitting them altogether when rewriting the + message:: + + # Good + ValidationError( + _('Invalid value: %(value)s'), + params={'value': '42'}, + ) + + # Bad + ValidationError( + _('Invalid value: %s'), + params=('42',), + ) + +* Wrap the message with ``gettext`` to enable translation:: + + # Good + ValidationError(_('Invalid value')) + + # Bad + ValidationError('Invalid value') + +Putting it all together:: + + raise ValidationErrror( + _('Invalid value: %(value)s'), + code='invalid', + params={'value': '42'}, + ) + +Following these guidelines is particularly necessary if you write reusable +forms, form fields, and model fields. + +While not recommended, if you are at the end of the validation chain +(i.e. your form ``clean()`` method) and you know you will *never* need +to override your error message you can still opt for the less verbose:: + + ValidationError(_('Invalid value: %s') % value) + +Raising multiple errors +~~~~~~~~~~~~~~~~~~~~~~~ + +If you detect multiple errors during a cleaning method and wish to signal all +of them to the form submitter, it is possible to pass a list of errors to the +``ValidationError`` constructor. + +As above, it is recommended to pass a list of ``ValidationError`` instances +with ``code``\s and ``params`` but a list of strings will also work:: + + # Good + raise ValidationError([ + ValidationError(_('Error 1'), code='error1'), + ValidationError(_('Error 2'), code='error2'), + ]) + + # Bad + raise ValidationError([ + _('Error 1'), + _('Error 2'), + ]) + +.. _modifying-field-errors: Form subclasses and modifying field errors ------------------------------------------ diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index f989ff1bec..cfc95db092 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -84,12 +84,18 @@ need to call a model's :meth:`~Model.full_clean()` method if you plan to handle validation errors yourself, or if you have excluded fields from the :class:`~django.forms.ModelForm` that require validation. -.. method:: Model.full_clean(exclude=None) +.. method:: Model.full_clean(exclude=None, validate_unique=True) + +.. versionchanged:: 1.6 + + The ``validate_unique`` parameter was added to allow skipping + :meth:`Model.validate_unique()`. Previously, :meth:`Model.validate_unique()` + was always called by ``full_clean``. This method calls :meth:`Model.clean_fields()`, :meth:`Model.clean()`, and -:meth:`Model.validate_unique()`, in that order and raises a -:exc:`~django.core.exceptions.ValidationError` that has a ``message_dict`` -attribute containing errors from all three stages. +:meth:`Model.validate_unique()` (if ``validate_unique`` is ``True``, in that +order and raises a :exc:`~django.core.exceptions.ValidationError` that has a +``message_dict`` attribute containing errors from all three stages. The optional ``exclude`` argument can be used to provide a list of field names that can be excluded from validation and cleaning. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 7d449edbe5..f260a1eac0 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -318,6 +318,13 @@ Minor features * Formsets now have a :meth:`~django.forms.formsets.BaseFormSet.total_error_count` method. +* :class:`~django.forms.ModelForm` fields can now override error messages + defined in model fields by using the + :attr:`~django.forms.Field.error_messages` argument of a ``Field``'s + constructor. To take advantage of this new feature with your custom fields, + :ref:`see the updated recommendation ` for raising + a ``ValidationError``. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index a798f9bf95..a4cf9471de 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -11,6 +11,7 @@ from __future__ import unicode_literals import os import tempfile +from django.core import validators from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import FileSystemStorage from django.db import models @@ -286,3 +287,12 @@ class ColourfulItem(models.Model): class ArticleStatusNote(models.Model): name = models.CharField(max_length=20) status = models.ManyToManyField(ArticleStatus) + +class CustomErrorMessage(models.Model): + name1 = models.CharField(max_length=50, + validators=[validators.validate_slug], + error_messages={'invalid': 'Model custom error message.'}) + + name2 = models.CharField(max_length=50, + validators=[validators.validate_slug], + error_messages={'invalid': 'Model custom error message.'}) diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index eea1ef9b68..39be824798 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -22,7 +22,7 @@ from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book, DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle, ImprovedArticleWithParentLink, Inventory, Post, Price, Product, TextFile, Writer, WriterProfile, Colour, ColourfulItem, - ArticleStatusNote, DateTimePost, test_images) + ArticleStatusNote, DateTimePost, CustomErrorMessage, test_images) if test_images: from .models import ImageFile, OptionalImageFile @@ -252,6 +252,12 @@ class StatusNoteCBM2mForm(forms.ModelForm): fields = '__all__' widgets = {'status': forms.CheckboxSelectMultiple} +class CustomErrorMessageForm(forms.ModelForm): + name1 = forms.CharField(error_messages={'invalid': 'Form custom error message.'}) + + class Meta: + model = CustomErrorMessage + class ModelFormBaseTest(TestCase): def test_base_form(self): @@ -1762,6 +1768,18 @@ class OldFormForXTests(TestCase): Hold down "Control", or "Command" on a Mac, to select more than one.

    """ % {'blue_pk': colour.pk}) + def test_custom_error_messages(self) : + data = {'name1': '@#$!!**@#$', 'name2': '@#$!!**@#$'} + errors = CustomErrorMessageForm(data).errors + self.assertHTMLEqual( + str(errors['name1']), + '' + ) + self.assertHTMLEqual( + str(errors['name2']), + '' + ) + class M2mHelpTextTest(TestCase): """Tests for ticket #9321.""" diff --git a/tests/validators/tests.py b/tests/validators/tests.py index 49389ef663..a1555d8e91 100644 --- a/tests/validators/tests.py +++ b/tests/validators/tests.py @@ -214,8 +214,8 @@ class TestSimpleValidators(TestCase): def test_message_dict(self): v = ValidationError({'first': ['First Problem']}) - self.assertEqual(str(v), str_prefix("{%(_)s'first': %(_)s'First Problem'}")) - self.assertEqual(repr(v), str_prefix("ValidationError({%(_)s'first': %(_)s'First Problem'})")) + self.assertEqual(str(v), str_prefix("{%(_)s'first': [%(_)s'First Problem']}")) + self.assertEqual(repr(v), str_prefix("ValidationError({%(_)s'first': [%(_)s'First Problem']})")) test_counter = 0 for validator, value, expected in TEST_DATA: -- cgit v1.3