diff options
| author | Justin Bronn <jbronn@gmail.com> | 2008-07-19 13:30:47 +0000 |
|---|---|---|
| committer | Justin Bronn <jbronn@gmail.com> | 2008-07-19 13:30:47 +0000 |
| commit | 149e731c3c5a2cc96b7d3c72070401df6c2a238e (patch) | |
| tree | 2a819f695246ab36139b7f8846085c9df1563bd8 /tests/regressiontests/forms | |
| parent | 5bf3565a263533e37b2e1217e8d447cb7e02f5b4 (diff) | |
gis: Merged revisions 7921,7926-7928,7938-7941,7945-7947,7949-7950,7952,7955-7956,7961,7964-7968,7970-7978 via svnmerge from trunk.
This includes the newforms-admin branch, and thus is backwards-incompatible. The geographic admin is _not_ in this changeset, and is forthcoming.
git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7979 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'tests/regressiontests/forms')
| -rw-r--r-- | tests/regressiontests/forms/error_messages.py | 2 | ||||
| -rw-r--r-- | tests/regressiontests/forms/extra.py | 8 | ||||
| -rw-r--r-- | tests/regressiontests/forms/fields.py | 75 | ||||
| -rw-r--r-- | tests/regressiontests/forms/forms.py | 74 | ||||
| -rw-r--r-- | tests/regressiontests/forms/formsets.py | 575 | ||||
| -rw-r--r-- | tests/regressiontests/forms/media.py | 359 | ||||
| -rw-r--r-- | tests/regressiontests/forms/models.py | 2 | ||||
| -rw-r--r-- | tests/regressiontests/forms/regressions.py | 2 | ||||
| -rw-r--r-- | tests/regressiontests/forms/tests.py | 4 | ||||
| -rw-r--r-- | tests/regressiontests/forms/util.py | 6 | ||||
| -rw-r--r-- | tests/regressiontests/forms/widgets.py | 162 |
11 files changed, 1239 insertions, 30 deletions
diff --git a/tests/regressiontests/forms/error_messages.py b/tests/regressiontests/forms/error_messages.py index 580326f894..ec91b57a06 100644 --- a/tests/regressiontests/forms/error_messages.py +++ b/tests/regressiontests/forms/error_messages.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * +>>> from django.forms import * >>> from django.core.files.uploadedfile import SimpleUploadedFile # CharField ################################################################### diff --git a/tests/regressiontests/forms/extra.py b/tests/regressiontests/forms/extra.py index a8b369715d..80f7ef6535 100644 --- a/tests/regressiontests/forms/extra.py +++ b/tests/regressiontests/forms/extra.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * +>>> from django.forms import * >>> from django.utils.encoding import force_unicode >>> import datetime >>> import time @@ -14,12 +14,12 @@ tests = r""" # Extra stuff # ############### -The newforms library comes with some extra, higher-level Field and Widget +The forms library comes with some extra, higher-level Field and Widget classes that demonstrate some of the library's abilities. # SelectDateWidget ############################################################ ->>> from django.newforms.extras import SelectDateWidget +>>> from django.forms.extras import SelectDateWidget >>> w = SelectDateWidget(years=('2007','2008','2009','2010','2011','2012','2013','2014','2015','2016')) >>> print w.render('mydate', '') <select name="mydate_month" id="id_mydate_month"> @@ -424,7 +424,7 @@ u'sirrobin' # Test overriding ErrorList in a form # ####################################### ->>> from django.newforms.util import ErrorList +>>> from django.forms.util import ErrorList >>> class DivErrorList(ErrorList): ... def __unicode__(self): ... return self.as_divs() diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py index f266e7bc50..c70ff2dff3 100644 --- a/tests/regressiontests/forms/fields.py +++ b/tests/regressiontests/forms/fields.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * ->>> from django.newforms.widgets import RadioFieldRenderer +>>> from django.forms import * +>>> from django.forms.widgets import RadioFieldRenderer >>> from django.core.files.uploadedfile import SimpleUploadedFile >>> import datetime >>> import time @@ -17,7 +17,7 @@ tests = r""" ########## Each Field class does some sort of validation. Each Field has a clean() method, -which either raises django.newforms.ValidationError or returns the "clean" +which either raises django.forms.ValidationError or returns the "clean" data -- usually a Unicode object, but, in some rare cases, a list. Each Field's __init__() takes at least these parameters: @@ -980,7 +980,7 @@ False # ChoiceField ################################################################# ->>> f = ChoiceField(choices=[('1', '1'), ('2', '2')]) +>>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')]) >>> f.clean('') Traceback (most recent call last): ... @@ -996,9 +996,9 @@ u'1' >>> f.clean('3') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] ->>> f = ChoiceField(choices=[('1', '1'), ('2', '2')], required=False) +>>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) >>> f.clean('') u'' >>> f.clean(None) @@ -1010,7 +1010,7 @@ u'1' >>> f.clean('3') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] >>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) >>> f.clean('J') @@ -1018,7 +1018,25 @@ u'J' >>> f.clean('John') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +ValidationError: [u'Select a valid choice. John is not one of the available choices.'] + +>>> f = ChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) +>>> f.clean(1) +u'1' +>>> f.clean('1') +u'1' +>>> f.clean(3) +u'3' +>>> f.clean('3') +u'3' +>>> f.clean(5) +u'5' +>>> f.clean('5') +u'5' +>>> f.clean('6') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] # NullBooleanField ############################################################ @@ -1036,7 +1054,7 @@ False # MultipleChoiceField ######################################################### ->>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')]) +>>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')]) >>> f.clean('') Traceback (most recent call last): ... @@ -1072,7 +1090,7 @@ Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] ->>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')], required=False) +>>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) >>> f.clean('') [] >>> f.clean(None) @@ -1100,6 +1118,29 @@ Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] +>>> f = MultipleChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) +>>> f.clean([1]) +[u'1'] +>>> f.clean(['1']) +[u'1'] +>>> f.clean([1, 5]) +[u'1', u'5'] +>>> f.clean([1, '5']) +[u'1', u'5'] +>>> f.clean(['1', 5]) +[u'1', u'5'] +>>> f.clean(['1', '5']) +[u'1', u'5'] +>>> f.clean(['6']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] +>>> f.clean(['1','6']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] + + # ComboField ################################################################## ComboField takes a list of fields that should be used to validate a value, @@ -1153,29 +1194,29 @@ u'' ... return x ... >>> import os ->>> from django import newforms as forms +>>> from django import forms >>> path = forms.__file__ >>> path = os.path.dirname(path) + '/' >>> fix_os_paths(path) -'.../django/newforms/' +'.../django/forms/' >>> f = forms.FilePathField(path=path) >>> f.choices.sort() >>> fix_os_paths(f.choices) -[('.../django/newforms/__init__.py', '__init__.py'), ('.../django/newforms/__init__.pyc', '__init__.pyc'), ('.../django/newforms/fields.py', 'fields.py'), ('.../django/newforms/fields.pyc', 'fields.pyc'), ('.../django/newforms/forms.py', 'forms.py'), ('.../django/newforms/forms.pyc', 'forms.pyc'), ('.../django/newforms/models.py', 'models.py'), ('.../django/newforms/models.pyc', 'models.pyc'), ('.../django/newforms/util.py', 'util.py'), ('.../django/newforms/util.pyc', 'util.pyc'), ('.../django/newforms/widgets.py', 'widgets.py'), ('.../django/newforms/widgets.pyc', 'widgets.pyc')] +[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/__init__.pyc', '__init__.pyc'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/fields.pyc', 'fields.pyc'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/forms.pyc', 'forms.pyc'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/models.pyc', 'models.pyc'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/util.pyc', 'util.pyc'), ('.../django/forms/widgets.py', 'widgets.py'), ('.../django/forms/widgets.pyc', 'widgets.pyc')] >>> f.clean('fields.py') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +ValidationError: [u'Select a valid choice. fields.py is not one of the available choices.'] >>> fix_os_paths(f.clean(path + 'fields.py')) -u'.../django/newforms/fields.py' +u'.../django/forms/fields.py' >>> f = forms.FilePathField(path=path, match='^.*?\.py$') >>> f.choices.sort() >>> fix_os_paths(f.choices) -[('.../django/newforms/__init__.py', '__init__.py'), ('.../django/newforms/fields.py', 'fields.py'), ('.../django/newforms/forms.py', 'forms.py'), ('.../django/newforms/models.py', 'models.py'), ('.../django/newforms/util.py', 'util.py'), ('.../django/newforms/widgets.py', 'widgets.py')] +[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/widgets.py', 'widgets.py')] >>> f = forms.FilePathField(path=path, recursive=True, match='^.*?\.py$') >>> f.choices.sort() >>> fix_os_paths(f.choices) -[('.../django/newforms/__init__.py', '__init__.py'), ('.../django/newforms/extras/__init__.py', 'extras/__init__.py'), ('.../django/newforms/extras/widgets.py', 'extras/widgets.py'), ('.../django/newforms/fields.py', 'fields.py'), ('.../django/newforms/forms.py', 'forms.py'), ('.../django/newforms/models.py', 'models.py'), ('.../django/newforms/util.py', 'util.py'), ('.../django/newforms/widgets.py', 'widgets.py')] +[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/extras/__init__.py', 'extras/__init__.py'), ('.../django/forms/extras/widgets.py', 'extras/widgets.py'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/widgets.py', 'widgets.py')] # SplitDateTimeField ########################################################## diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index 041fa4054c..6e6e4f79bf 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * +>>> from django.forms import * >>> from django.core.files.uploadedfile import SimpleUploadedFile >>> import datetime >>> import time @@ -1667,4 +1667,76 @@ the list of errors is empty). You can also use it in {% if %} statements. <p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p> <input type="submit" /> </form> + + +# The empty_permitted attribute ############################################## + +Sometimes (pretty much in formsets) we want to allow a form to pass validation +if it is completely empty. We can accomplish this by using the empty_permitted +agrument to a form constructor. + +>>> class SongForm(Form): +... artist = CharField() +... name = CharField() + +First let's show what happens id empty_permitted=False (the default): + +>>> data = {'artist': '', 'song': ''} + +>>> form = SongForm(data, empty_permitted=False) +>>> form.is_valid() +False +>>> form.errors +{'name': [u'This field is required.'], 'artist': [u'This field is required.']} +>>> form.cleaned_data +Traceback (most recent call last): +... +AttributeError: 'SongForm' object has no attribute 'cleaned_data' + + +Now let's show what happens when empty_permitted=True and the form is empty. + +>>> form = SongForm(data, empty_permitted=True) +>>> form.is_valid() +True +>>> form.errors +{} +>>> form.cleaned_data +{} + +But if we fill in data for one of the fields, the form is no longer empty and +the whole thing must pass validation. + +>>> data = {'artist': 'The Doors', 'song': ''} +>>> form = SongForm(data, empty_permitted=False) +>>> form.is_valid() +False +>>> form.errors +{'name': [u'This field is required.']} +>>> form.cleaned_data +Traceback (most recent call last): +... +AttributeError: 'SongForm' object has no attribute 'cleaned_data' + +If a field is not given in the data then None is returned for its data. Lets +make sure that when checking for empty_permitted that None is treated +accordingly. + +>>> data = {'artist': None, 'song': ''} +>>> form = SongForm(data, empty_permitted=True) +>>> form.is_valid() +True + +However, we *really* need to be sure we are checking for None as any data in +initial that returns False on a boolean call needs to be treated literally. + +>>> class PriceForm(Form): +... amount = FloatField() +... qty = IntegerField() + +>>> data = {'amount': '0.0', 'qty': ''} +>>> form = PriceForm(data, initial={'amount': 0.0}, empty_permitted=True) +>>> form.is_valid() +True + """ diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py new file mode 100644 index 0000000000..bbbd4cee5a --- /dev/null +++ b/tests/regressiontests/forms/formsets.py @@ -0,0 +1,575 @@ +# -*- coding: utf-8 -*- +tests = """ +# Basic FormSet creation and usage ############################################ + +FormSet allows us to use multiple instance of the same form on 1 page. For now, +the best way to create a FormSet is by using the formset_factory function. + +>>> from django.forms import Form, CharField, IntegerField, ValidationError +>>> from django.forms.formsets import formset_factory, BaseFormSet + +>>> class Choice(Form): +... choice = CharField() +... votes = IntegerField() + +>>> ChoiceFormSet = formset_factory(Choice) + +A FormSet constructor takes the same arguments as Form. Let's create a FormSet +for adding data. By default, it displays 1 blank form. It can display more, +but we'll look at how to do so later. + +>>> formset = ChoiceFormSet(auto_id=False, prefix='choices') +>>> print formset +<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_FORMS" value="0" /> +<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr> +<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr> + + +On thing to note is that there needs to be a special value in the data. This +value tells the FormSet how many forms were displayed so it can tell how +many forms it needs to clean and validate. You could use javascript to create +new forms on the client side, but they won't get validated unless you increment +the TOTAL_FORMS field appropriately. + +>>> data = { +... 'choices-TOTAL_FORMS': '1', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... } + +We treat FormSet pretty much like we would treat a normal Form. FormSet has an +is_valid method, and a cleaned_data or errors attribute depending on whether all +the forms passed validation. However, unlike a Form instance, cleaned_data and +errors will be a list of dicts rather than just a single dict. + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{'votes': 100, 'choice': u'Calexico'}] + +If a FormSet was not passed any data, its is_valid method should return False. +>>> formset = ChoiceFormSet() +>>> formset.is_valid() +False + +FormSet instances can also have an error attribute if validation failed for +any of the forms. + +>>> data = { +... 'choices-TOTAL_FORMS': '1', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{'votes': [u'This field is required.']}] + + +We can also prefill a FormSet with existing data by providing an ``initial`` +argument to the constructor. ``initial`` should be a list of dicts. By default, +an extra blank form is included. + +>>> initial = [{'choice': u'Calexico', 'votes': 100}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li> + + +Let's simulate what would happen if we submitted this form. + +>>> data = { +... 'choices-TOTAL_FORMS': '2', # the number of forms rendered +... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{'votes': 100, 'choice': u'Calexico'}, {}] + +But the second form was blank! Shouldn't we get some errors? No. If we display +a form as blank, it's ok for it to be submitted as blank. If we fill out even +one of the fields of a blank form though, it will be validated. We may want to +required that at least x number of forms are completed, but we'll show how to +handle that later. + +>>> data = { +... 'choices-TOTAL_FORMS': '2', # the number of forms rendered +... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': 'The Decemberists', +... 'choices-1-votes': '', # missing value +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{}, {'votes': [u'This field is required.']}] + +If we delete data that was pre-filled, we should get an error. Simply removing +data from form fields isn't the proper way to delete it. We'll see how to +handle that case later. + +>>> data = { +... 'choices-TOTAL_FORMS': '2', # the number of forms rendered +... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': '', # deleted value +... 'choices-0-votes': '', # deleted value +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}] + + +# Displaying more than 1 blank form ########################################### + +We can also display more than 1 empty form at a time. To do so, pass a +extra argument to formset_factory. + +>>> ChoiceFormSet = formset_factory(Choice, extra=3) + +>>> formset = ChoiceFormSet(auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" /></li> +<li>Votes: <input type="text" name="choices-0-votes" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> + +Since we displayed every form as blank, we will also accept them back as blank. +This may seem a little strange, but later we will show how to require a minimum +number of forms to be completed. + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': '', +... 'choices-0-votes': '', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{}, {}, {}] + + +We can just fill out one of the forms. + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{'votes': 100, 'choice': u'Calexico'}, {}, {}] + + +And once again, if we try to partially complete a form, validation will fail. + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': 'The Decemberists', +... 'choices-1-votes': '', # missing value +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{}, {'votes': [u'This field is required.']}, {}] + + +The extra argument also works when the formset is pre-filled with initial +data. + +>>> initial = [{'choice': u'Calexico', 'votes': 100}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Choice: <input type="text" name="choices-3-choice" /></li> +<li>Votes: <input type="text" name="choices-3-votes" /></li> + + +# FormSets with deletion ###################################################### + +We can easily add deletion ability to a FormSet with an agrument to +formset_factory. This will add a boolean field to each form instance. When +that boolean field is True, the form will be in formset.deleted_forms + +>>> ChoiceFormSet = formset_factory(Choice, can_delete=True) + +>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li> + +To delete something, we just need to set that form's special delete field to +'on'. Let's go ahead and delete Fergie. + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-DELETE': '', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-DELETE': 'on', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... 'choices-2-DELETE': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}] +>>> [form.cleaned_data for form in formset.deleted_forms] +[{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}] + + +# FormSets with ordering ###################################################### + +We can also add ordering ability to a FormSet with an agrument to +formset_factory. This will add a integer field to each form instance. When +form validation succeeds, [form.cleaned_data for form in formset.forms] will have the data in the correct +order specified by the ordering fields. If a number is duplicated in the set +of ordering fields, for instance form 0 and form 3 are both marked as 1, then +the form index used as a secondary ordering criteria. In order to put +something at the front of the list, you'd need to set it's order to 0. + +>>> ChoiceFormSet = formset_factory(Choice, can_order=True) + +>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Order: <input type="text" name="choices-2-ORDER" /></li> + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-ORDER': '1', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-ORDER': '2', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '500', +... 'choices-2-ORDER': '0', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> for form in formset.ordered_forms: +... print form.cleaned_data +{'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'} +{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'} +{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'} + +Ordering fields are allowed to be left blank, and if they *are* left blank, +they will be sorted below everything else. + +>>> data = { +... 'choices-TOTAL_FORMS': '4', # the number of forms rendered +... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-ORDER': '1', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-ORDER': '2', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '500', +... 'choices-2-ORDER': '', +... 'choices-3-choice': 'Basia Bulat', +... 'choices-3-votes': '50', +... 'choices-3-ORDER': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> for form in formset.ordered_forms: +... print form.cleaned_data +{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'} +{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'} +{'votes': 500, 'ORDER': None, 'choice': u'The Decemberists'} +{'votes': 50, 'ORDER': None, 'choice': u'Basia Bulat'} + + +# FormSets with ordering + deletion ########################################### + +Let's try throwing ordering and deletion into the same form. + +>>> ChoiceFormSet = formset_factory(Choice, can_order=True, can_delete=True) + +>>> initial = [ +... {'choice': u'Calexico', 'votes': 100}, +... {'choice': u'Fergie', 'votes': 900}, +... {'choice': u'The Decemberists', 'votes': 500}, +... ] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li> +<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li> +<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li> +<li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists" /></li> +<li>Votes: <input type="text" name="choices-2-votes" value="500" /></li> +<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li> +<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li> +<li>Choice: <input type="text" name="choices-3-choice" /></li> +<li>Votes: <input type="text" name="choices-3-votes" /></li> +<li>Order: <input type="text" name="choices-3-ORDER" /></li> +<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li> + +Let's delete Fergie, and put The Decemberists ahead of Calexico. + +>>> data = { +... 'choices-TOTAL_FORMS': '4', # the number of forms rendered +... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-ORDER': '1', +... 'choices-0-DELETE': '', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-ORDER': '2', +... 'choices-1-DELETE': 'on', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '500', +... 'choices-2-ORDER': '0', +... 'choices-2-DELETE': '', +... 'choices-3-choice': '', +... 'choices-3-votes': '', +... 'choices-3-ORDER': '', +... 'choices-3-DELETE': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> for form in formset.ordered_forms: +... print form.cleaned_data +{'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'} +{'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'} +>>> [form.cleaned_data for form in formset.deleted_forms] +[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}] + + +# FormSet clean hook ########################################################## + +FormSets have a hook for doing extra validation that shouldn't be tied to any +particular form. It follows the same pattern as the clean hook on Forms. + +Let's define a FormSet that takes a list of favorite drinks, but raises am +error if there are any duplicates. + +>>> class FavoriteDrinkForm(Form): +... name = CharField() +... + +>>> class BaseFavoriteDrinksFormSet(BaseFormSet): +... def clean(self): +... seen_drinks = [] +... for drink in self.cleaned_data: +... if drink['name'] in seen_drinks: +... raise ValidationError('You may only specify a drink once.') +... seen_drinks.append(drink['name']) +... + +>>> FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, +... formset=BaseFavoriteDrinksFormSet, extra=3) + +We start out with a some duplicate data. + +>>> data = { +... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered +... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data +... 'drinks-MAX_FORMS': '0', # the max number of forms +... 'drinks-0-name': 'Gin and Tonic', +... 'drinks-1-name': 'Gin and Tonic', +... } + +>>> formset = FavoriteDrinksFormSet(data, prefix='drinks') +>>> formset.is_valid() +False + +Any errors raised by formset.clean() are available via the +formset.non_form_errors() method. + +>>> for error in formset.non_form_errors(): +... print error +You may only specify a drink once. + + +Make sure we didn't break the valid case. + +>>> data = { +... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered +... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data +... 'drinks-MAX_FORMS': '0', # the max number of forms +... 'drinks-0-name': 'Gin and Tonic', +... 'drinks-1-name': 'Bloody Mary', +... } + +>>> formset = FavoriteDrinksFormSet(data, prefix='drinks') +>>> formset.is_valid() +True +>>> for error in formset.non_form_errors(): +... print error + +# Limiting the maximum number of forms ######################################## + +# Base case for max_num. + +>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2) +>>> formset = LimitedFavoriteDrinkFormSet() +>>> for form in formset.forms: +... print form +<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr> + +# Ensure the that max_num has no affect when extra is less than max_forms. + +>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2) +>>> formset = LimitedFavoriteDrinkFormSet() +>>> for form in formset.forms: +... print form +<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr> + +# max_num with initial data + +# More initial forms than max_num will result in only the first max_num of +# them to be displayed with no extra forms. + +>>> initial = [ +... {'name': 'Gin Tonic'}, +... {'name': 'Bloody Mary'}, +... {'name': 'Jack and Coke'}, +... ] +>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2) +>>> formset = LimitedFavoriteDrinkFormSet(initial=initial) +>>> for form in formset.forms: +... print form +<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" value="Bloody Mary" id="id_form-1-name" /></td></tr> + +# One form from initial and extra=3 with max_num=2 should result in the one +# initial form and one extra. + +>>> initial = [ +... {'name': 'Gin Tonic'}, +... ] +>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=2) +>>> formset = LimitedFavoriteDrinkFormSet(initial=initial) +>>> for form in formset.forms: +... print form +<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr> + + +# Regression test for #6926 ################################################## + +Make sure the management form has the correct prefix. + +>>> formset = FavoriteDrinksFormSet() +>>> formset.management_form.prefix +'form' + +>>> formset = FavoriteDrinksFormSet(data={}) +>>> formset.management_form.prefix +'form' + +>>> formset = FavoriteDrinksFormSet(initial={}) +>>> formset.management_form.prefix +'form' + +""" diff --git a/tests/regressiontests/forms/media.py b/tests/regressiontests/forms/media.py new file mode 100644 index 0000000000..d05db1f164 --- /dev/null +++ b/tests/regressiontests/forms/media.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +# Tests for the media handling on widgets and forms + +media_tests = r""" +>>> from django.forms import TextInput, Media, TextInput, CharField, Form, MultiWidget +>>> from django.conf import settings +>>> ORIGINAL_MEDIA_URL = settings.MEDIA_URL +>>> settings.MEDIA_URL = 'http://media.example.com/media/' + +# Check construction of media objects +>>> m = Media(css={'all': ('path/to/css1','/path/to/css2')}, js=('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3')) +>>> print m +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +>>> class Foo: +... css = { +... 'all': ('path/to/css1','/path/to/css2') +... } +... js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') +>>> m3 = Media(Foo) +>>> print m3 +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +>>> m3 = Media(Foo) +>>> print m3 +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +# A widget can exist without a media definition +>>> class MyWidget(TextInput): +... pass + +>>> w = MyWidget() +>>> print w.media +<BLANKLINE> + +############################################################### +# DSL Class-based media definitions +############################################################### + +# A widget can define media if it needs to. +# Any absolute path will be preserved; relative paths are combined +# with the value of settings.MEDIA_URL +>>> class MyWidget1(TextInput): +... class Media: +... css = { +... 'all': ('path/to/css1','/path/to/css2') +... } +... js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + +>>> w1 = MyWidget1() +>>> print w1.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +# Media objects can be interrogated by media type +>>> print w1.media['css'] +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> + +>>> print w1.media['js'] +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +# Media objects can be combined. Any given media resource will appear only +# once. Duplicated media definitions are ignored. +>>> class MyWidget2(TextInput): +... class Media: +... css = { +... 'all': ('/path/to/css2','/path/to/css3') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> class MyWidget3(TextInput): +... class Media: +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w2 = MyWidget2() +>>> w3 = MyWidget3() +>>> print w1.media + w2.media + w3.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# Check that media addition hasn't affected the original objects +>>> print w1.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +############################################################### +# Property-based media definitions +############################################################### + +# Widget media can be defined as a property +>>> class MyWidget4(TextInput): +... def _media(self): +... return Media(css={'all': ('/some/path',)}, js = ('/some/js',)) +... media = property(_media) + +>>> w4 = MyWidget4() +>>> print w4.media +<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script> + +# Media properties can reference the media of their parents +>>> class MyWidget5(MyWidget4): +... def _media(self): +... return super(MyWidget5, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',)) +... media = property(_media) + +>>> w5 = MyWidget5() +>>> print w5.media +<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script> +<script type="text/javascript" src="/other/js"></script> + +# Media properties can reference the media of their parents, +# even if the parent media was defined using a class +>>> class MyWidget6(MyWidget1): +... def _media(self): +... return super(MyWidget6, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',)) +... media = property(_media) + +>>> w6 = MyWidget6() +>>> print w6.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/other/js"></script> + +############################################################### +# Inheritance of media +############################################################### + +# If a widget extends another but provides no media definition, it inherits the parent widget's media +>>> class MyWidget7(MyWidget1): +... pass + +>>> w7 = MyWidget7() +>>> print w7.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +# If a widget extends another but defines media, it extends the parent widget's media by default +>>> class MyWidget8(MyWidget1): +... class Media: +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w8 = MyWidget8() +>>> print w8.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# If a widget extends another but defines media, it extends the parents widget's media, +# even if the parent defined media using a property. +>>> class MyWidget9(MyWidget4): +... class Media: +... css = { +... 'all': ('/other/path',) +... } +... js = ('/other/js',) + +>>> w9 = MyWidget9() +>>> print w9.media +<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script> +<script type="text/javascript" src="/other/js"></script> + +# A widget can disable media inheritance by specifying 'extend=False' +>>> class MyWidget10(MyWidget1): +... class Media: +... extend = False +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w10 = MyWidget10() +>>> print w10.media +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# A widget can explicitly enable full media inheritance by specifying 'extend=True' +>>> class MyWidget11(MyWidget1): +... class Media: +... extend = True +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w11 = MyWidget11() +>>> print w11.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# A widget can enable inheritance of one media type by specifying extend as a tuple +>>> class MyWidget12(MyWidget1): +... class Media: +... extend = ('css',) +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w12 = MyWidget12() +>>> print w12.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +############################################################### +# Multi-media handling for CSS +############################################################### + +# A widget can define CSS media for multiple output media types +>>> class MultimediaWidget(TextInput): +... class Media: +... css = { +... 'screen, print': ('/file1','/file2'), +... 'screen': ('/file3',), +... 'print': ('/file4',) +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> multimedia = MultimediaWidget() +>>> print multimedia.media +<link href="/file4" type="text/css" media="print" rel="stylesheet" /> +<link href="/file3" type="text/css" media="screen" rel="stylesheet" /> +<link href="/file1" type="text/css" media="screen, print" rel="stylesheet" /> +<link href="/file2" type="text/css" media="screen, print" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +############################################################### +# Multiwidget media handling +############################################################### + +# MultiWidgets have a default media definition that gets all the +# media from the component widgets +>>> class MyMultiWidget(MultiWidget): +... def __init__(self, attrs=None): +... widgets = [MyWidget1, MyWidget2, MyWidget3] +... super(MyMultiWidget, self).__init__(widgets, attrs) + +>>> mymulti = MyMultiWidget() +>>> print mymulti.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +############################################################### +# Media processing for forms +############################################################### + +# You can ask a form for the media required by its widgets. +>>> class MyForm(Form): +... field1 = CharField(max_length=20, widget=MyWidget1()) +... field2 = CharField(max_length=20, widget=MyWidget2()) +>>> f1 = MyForm() +>>> print f1.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# Form media can be combined to produce a single media definition. +>>> class AnotherForm(Form): +... field3 = CharField(max_length=20, widget=MyWidget3()) +>>> f2 = AnotherForm() +>>> print f1.media + f2.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# Forms can also define media, following the same rules as widgets. +>>> class FormWithMedia(Form): +... field1 = CharField(max_length=20, widget=MyWidget1()) +... field2 = CharField(max_length=20, widget=MyWidget2()) +... class Media: +... js = ('/some/form/javascript',) +... css = { +... 'all': ('/some/form/css',) +... } +>>> f3 = FormWithMedia() +>>> print f3.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<link href="/some/form/css" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> +<script type="text/javascript" src="/some/form/javascript"></script> + +>>> settings.MEDIA_URL = ORIGINAL_MEDIA_URL +"""
\ No newline at end of file diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py index c7ce128560..98b9233d80 100644 --- a/tests/regressiontests/forms/models.py +++ b/tests/regressiontests/forms/models.py @@ -15,7 +15,7 @@ class ChoiceModel(models.Model): name = models.CharField(max_length=10) __test__ = {'API_TESTS': """ ->>> from django.newforms import form_for_model, form_for_instance +>>> from django.forms import form_for_model, form_for_instance # Boundary conditions on a PostitiveIntegerField ######################### >>> BoundaryForm = form_for_model(BoundaryModel) diff --git a/tests/regressiontests/forms/regressions.py b/tests/regressiontests/forms/regressions.py index cbc8095e60..87390d3cd1 100644 --- a/tests/regressiontests/forms/regressions.py +++ b/tests/regressiontests/forms/regressions.py @@ -3,7 +3,7 @@ tests = r""" It should be possible to re-use attribute dictionaries (#3810) ->>> from django.newforms import * +>>> from django.forms import * >>> extra_attrs = {'class': 'special'} >>> class TestForm(Form): ... f1 = CharField(max_length=10, widget=TextInput(attrs=extra_attrs)) diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index bb0e30b874..ff8213c8d9 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -26,6 +26,8 @@ from localflavor.za import tests as localflavor_za_tests from regressions import tests as regression_tests from util import tests as util_tests from widgets import tests as widgets_tests +from formsets import tests as formset_tests +from media import media_tests __test__ = { 'extra_tests': extra_tests, @@ -53,6 +55,8 @@ __test__ = { 'localflavor_us_tests': localflavor_us_tests, 'localflavor_za_tests': localflavor_za_tests, 'regression_tests': regression_tests, + 'formset_tests': formset_tests, + 'media_tests': media_tests, 'util_tests': util_tests, 'widgets_tests': widgets_tests, } diff --git a/tests/regressiontests/forms/util.py b/tests/regressiontests/forms/util.py index bfaf73f6bc..68c082c114 100644 --- a/tests/regressiontests/forms/util.py +++ b/tests/regressiontests/forms/util.py @@ -1,17 +1,17 @@ # coding: utf-8 """ -Tests for newforms/util.py module. +Tests for forms/util.py module. """ tests = r""" ->>> from django.newforms.util import * +>>> from django.forms.util import * >>> from django.utils.translation import ugettext_lazy ########### # flatatt # ########### ->>> from django.newforms.util import flatatt +>>> from django.forms.util import flatatt >>> flatatt({'id': "header"}) u' id="header"' >>> flatatt({'class': "news", 'title': "Read this"}) diff --git a/tests/regressiontests/forms/widgets.py b/tests/regressiontests/forms/widgets.py index 2c6b51a8ec..40c4d01793 100644 --- a/tests/regressiontests/forms/widgets.py +++ b/tests/regressiontests/forms/widgets.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * ->>> from django.newforms.widgets import RadioFieldRenderer +>>> from django.forms import * +>>> from django.forms.widgets import RadioFieldRenderer >>> from django.utils.safestring import mark_safe >>> import datetime >>> import time @@ -202,6 +202,30 @@ u'<input type="file" class="fun" name="email" />' >>> w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}) u'<input type="file" class="fun" name="email" />' +Test for the behavior of _has_changed for FileInput. The value of data will +more than likely come from request.FILES. The value of initial data will +likely be a filename stored in the database. Since its value is of no use to +a FileInput it is ignored. + +>>> w = FileInput() + +# No file was uploaded and no initial data. +>>> w._has_changed(u'', None) +False + +# A file was uploaded and no initial data. +>>> w._has_changed(u'', {'filename': 'resume.txt', 'content': 'My resume'}) +True + +# A file was not uploaded, but there is initial data +>>> w._has_changed(u'resume.txt', None) +False + +# A file was uploaded and there is initial data (file identity is not dealt +# with here) +>>> w._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'}) +True + # Textarea Widget ############################################################# >>> w = Textarea() @@ -292,6 +316,21 @@ checkboxes). >>> w.value_from_datadict({}, {}, 'testing') False +>>> w._has_changed(None, None) +False +>>> w._has_changed(None, u'') +False +>>> w._has_changed(u'', None) +False +>>> w._has_changed(u'', u'') +False +>>> w._has_changed(False, u'on') +True +>>> w._has_changed(True, u'on') +False +>>> w._has_changed(True, u'') +True + # Select Widget ############################################################### >>> w = Select() @@ -419,6 +458,35 @@ over multiple times without getting consumed: <option value="4">4</option> </select> +Choices can be nested one level in order to create HTML optgroups: +>>> w.choices=(('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) +>>> print w.render('nestchoice', None) +<select name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', 'outer1') +<select name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', 'inner1') +<select name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1" selected="selected">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + # NullBooleanSelect Widget #################################################### >>> w = NullBooleanSelect() @@ -573,6 +641,58 @@ If 'choices' is passed to both the constructor and render(), then they'll both b >>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]) u'<select multiple="multiple" name="nums">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" selected="selected">\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</option>\n<option value="\u0107\u017e\u0161\u0111">abc\u0107\u017e\u0161\u0111</option>\n</select>' +# Test the usage of _has_changed +>>> w._has_changed(None, None) +False +>>> w._has_changed([], None) +False +>>> w._has_changed(None, [u'1']) +True +>>> w._has_changed([1, 2], [u'1', u'2']) +False +>>> w._has_changed([1, 2], [u'1']) +True +>>> w._has_changed([1, 2], [u'1', u'3']) +True + +# Choices can be nested one level in order to create HTML optgroups: +>>> w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) +>>> print w.render('nestchoice', None) +<select multiple="multiple" name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', ['outer1']) +<select multiple="multiple" name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', ['inner1']) +<select multiple="multiple" name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1" selected="selected">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', ['outer1', 'inner2']) +<select multiple="multiple" name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2" selected="selected">Inner 2</option> +</optgroup> +</select> + # RadioSelect Widget ########################################################## >>> w = RadioSelect() @@ -871,6 +991,20 @@ If 'choices' is passed to both the constructor and render(), then they'll both b <li><label><input type="checkbox" name="escape" value="good" /> you > me</label></li> </ul> +# Test the usage of _has_changed +>>> w._has_changed(None, None) +False +>>> w._has_changed([], None) +False +>>> w._has_changed(None, [u'1']) +True +>>> w._has_changed([1, 2], [u'1', u'2']) +False +>>> w._has_changed([1, 2], [u'1']) +True +>>> w._has_changed([1, 2], [u'1', u'3']) +True + # Unicode choices are correctly rendered as HTML >>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]) u'<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>' @@ -895,6 +1029,25 @@ u'<input id="foo_0" type="text" class="big" value="john" name="name_0" /><br />< >>> w.render('name', ['john', 'lennon']) u'<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />' +>>> w = MyMultiWidget(widgets=(TextInput(), TextInput())) + +# test with no initial data +>>> w._has_changed(None, [u'john', u'lennon']) +True + +# test when the data is the same as initial +>>> w._has_changed(u'john__lennon', [u'john', u'lennon']) +False + +# test when the first widget's data has changed +>>> w._has_changed(u'john__lennon', [u'alfred', u'lennon']) +True + +# test when the last widget's data has changed. this ensures that it is not +# short circuiting while testing the widgets. +>>> w._has_changed(u'john__lennon', [u'john', u'denver']) +True + # SplitDateTimeWidget ######################################################### >>> w = SplitDateTimeWidget() @@ -913,6 +1066,11 @@ included on both widgets. >>> w.render('date', datetime.datetime(2006, 1, 10, 7, 30)) u'<input type="text" class="pretty" value="2006-01-10" name="date_0" /><input type="text" class="pretty" value="07:30:00" name="date_1" />' +>>> w._has_changed(datetime.datetime(2008, 5, 5, 12, 40, 00), [u'2008-05-05', u'12:40:00']) +False +>>> w._has_changed(datetime.datetime(2008, 5, 5, 12, 40, 00), [u'2008-05-05', u'12:41:00']) +True + # DateTimeInput ############################################################### >>> w = DateTimeInput() |
