diff options
| author | Brian Rosner <brosner@gmail.com> | 2008-07-18 23:54:34 +0000 |
|---|---|---|
| committer | Brian Rosner <brosner@gmail.com> | 2008-07-18 23:54:34 +0000 |
| commit | a19ed8aea395e8e07164ff7d85bd7dff2f24edca (patch) | |
| tree | ec5fd01c30abc5fa22c1f02159bf68cfe89313cc /tests | |
| parent | dc375fb0f3b7fbae740e8cfcd791b8bccb8a4e66 (diff) | |
Merged the newforms-admin branch into trunk.
This is a backward incompatible change. The admin contrib app has been
refactored. The newforms module has several improvements including FormSets
and Media definitions.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@7967 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'tests')
31 files changed, 3048 insertions, 340 deletions
diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py index 8a480a2381..48e574af48 100644 --- a/tests/modeltests/invalid_models/models.py +++ b/tests/modeltests/invalid_models/models.py @@ -5,12 +5,11 @@ This example exists purely to point out errors in models. """ from django.db import models - +model_errors = "" class FieldErrors(models.Model): charfield = models.CharField() decimalfield = models.DecimalField() filefield = models.FileField() - prepopulate = models.CharField(max_length=10, prepopulate_from='bad') choices = models.CharField(max_length=10, choices='bad') choices2 = models.CharField(max_length=10, choices=[(1,2,3),(1,2,3)]) index = models.CharField(max_length=10, db_index='bad') @@ -116,7 +115,6 @@ model_errors = """invalid_models.fielderrors: "charfield": CharFields require a invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute. invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute. invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute. -invalid_models.fielderrors: "prepopulate": prepopulate_from should be a list or tuple. invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list). invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index e8598bd68f..6838f11d4e 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -992,4 +992,22 @@ True u'...test3.png' >>> instance.delete() +# Media on a ModelForm ######################################################## + +# Similar to a regular Form class you can define custom media to be used on +# the ModelForm. + +>>> class ModelFormWithMedia(ModelForm): +... class Media: +... js = ('/some/form/javascript',) +... css = { +... 'all': ('/some/form/css',) +... } +... class Meta: +... model = PhoneNumber +>>> f = ModelFormWithMedia() +>>> print f.media +<link href="/some/form/css" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/form/javascript"></script> + """} diff --git a/tests/regressiontests/invalid_admin_options/__init__.py b/tests/modeltests/model_formsets/__init__.py index e69de29bb2..e69de29bb2 100644 --- a/tests/regressiontests/invalid_admin_options/__init__.py +++ b/tests/modeltests/model_formsets/__init__.py diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py new file mode 100644 index 0000000000..84265450fc --- /dev/null +++ b/tests/modeltests/model_formsets/models.py @@ -0,0 +1,324 @@ +from django.db import models + +class Author(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class Book(models.Model): + author = models.ForeignKey(Author) + title = models.CharField(max_length=100) + + def __unicode__(self): + return self.title + +class AuthorMeeting(models.Model): + name = models.CharField(max_length=100) + authors = models.ManyToManyField(Author) + created = models.DateField(editable=False) + + def __unicode__(self): + return self.name + + +__test__ = {'API_TESTS': """ + +>>> from datetime import date + +>>> from django.newforms.models import modelformset_factory + +>>> qs = Author.objects.all() +>>> AuthorFormSet = modelformset_factory(Author, extra=3) + +>>> formset = AuthorFormSet(queryset=qs) +>>> for form in formset.forms: +... print form.as_p() +<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p> +<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /><input type="hidden" name="form-1-id" id="id_form-1-id" /></p> +<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p> + +>>> data = { +... 'form-TOTAL_FORMS': '3', # the number of forms rendered +... 'form-INITIAL_FORMS': '0', # the number of forms with initial data +... 'form-MAX_FORMS': '0', # the max number of forms +... 'form-0-name': 'Charles Baudelaire', +... 'form-1-name': 'Arthur Rimbaud', +... 'form-2-name': '', +... } + +>>> formset = AuthorFormSet(data=data, queryset=qs) +>>> formset.is_valid() +True + +>>> formset.save() +[<Author: Charles Baudelaire>, <Author: Arthur Rimbaud>] + +>>> for author in Author.objects.order_by('name'): +... print author.name +Arthur Rimbaud +Charles Baudelaire + + +Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing +authors with an extra form to add him. We *could* pass in a queryset to +restrict the Author objects we edit, but in this case we'll use it to display +them in alphabetical order by name. + +>>> qs = Author.objects.order_by('name') +>>> AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=False) + +>>> formset = AuthorFormSet(queryset=qs) +>>> for form in formset.forms: +... print form.as_p() +<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p> +<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p> +<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p> + + +>>> data = { +... 'form-TOTAL_FORMS': '3', # the number of forms rendered +... 'form-INITIAL_FORMS': '2', # the number of forms with initial data +... 'form-MAX_FORMS': '0', # the max number of forms +... 'form-0-id': '2', +... 'form-0-name': 'Arthur Rimbaud', +... 'form-1-id': '1', +... 'form-1-name': 'Charles Baudelaire', +... 'form-2-name': 'Paul Verlaine', +... } + +>>> formset = AuthorFormSet(data=data, queryset=qs) +>>> formset.is_valid() +True + +# Only changed or new objects are returned from formset.save() +>>> formset.save() +[<Author: Paul Verlaine>] + +>>> for author in Author.objects.order_by('name'): +... print author.name +Arthur Rimbaud +Charles Baudelaire +Paul Verlaine + + +This probably shouldn't happen, but it will. If an add form was marked for +deltetion, make sure we don't save that form. + +>>> qs = Author.objects.order_by('name') +>>> AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=True) + +>>> formset = AuthorFormSet(queryset=qs) +>>> for form in formset.forms: +... print form.as_p() +<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Arthur Rimbaud" maxlength="100" /></p> +<p><label for="id_form-0-DELETE">Delete:</label> <input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" /><input type="hidden" name="form-0-id" value="2" id="id_form-0-id" /></p> +<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" value="Charles Baudelaire" maxlength="100" /></p> +<p><label for="id_form-1-DELETE">Delete:</label> <input type="checkbox" name="form-1-DELETE" id="id_form-1-DELETE" /><input type="hidden" name="form-1-id" value="1" id="id_form-1-id" /></p> +<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" value="Paul Verlaine" maxlength="100" /></p> +<p><label for="id_form-2-DELETE">Delete:</label> <input type="checkbox" name="form-2-DELETE" id="id_form-2-DELETE" /><input type="hidden" name="form-2-id" value="3" id="id_form-2-id" /></p> +<p><label for="id_form-3-name">Name:</label> <input id="id_form-3-name" type="text" name="form-3-name" maxlength="100" /></p> +<p><label for="id_form-3-DELETE">Delete:</label> <input type="checkbox" name="form-3-DELETE" id="id_form-3-DELETE" /><input type="hidden" name="form-3-id" id="id_form-3-id" /></p> + +>>> data = { +... 'form-TOTAL_FORMS': '4', # the number of forms rendered +... 'form-INITIAL_FORMS': '3', # the number of forms with initial data +... 'form-MAX_FORMS': '0', # the max number of forms +... 'form-0-id': '2', +... 'form-0-name': 'Arthur Rimbaud', +... 'form-1-id': '1', +... 'form-1-name': 'Charles Baudelaire', +... 'form-2-id': '3', +... 'form-2-name': 'Paul Verlaine', +... 'form-3-name': 'Walt Whitman', +... 'form-3-DELETE': 'on', +... } + +>>> formset = AuthorFormSet(data=data, queryset=qs) +>>> formset.is_valid() +True + +# No objects were changed or saved so nothing will come back. +>>> formset.save() +[] + +>>> for author in Author.objects.order_by('name'): +... print author.name +Arthur Rimbaud +Charles Baudelaire +Paul Verlaine + +Let's edit a record to ensure save only returns that one record. + +>>> data = { +... 'form-TOTAL_FORMS': '4', # the number of forms rendered +... 'form-INITIAL_FORMS': '3', # the number of forms with initial data +... 'form-MAX_FORMS': '0', # the max number of forms +... 'form-0-id': '2', +... 'form-0-name': 'Walt Whitman', +... 'form-1-id': '1', +... 'form-1-name': 'Charles Baudelaire', +... 'form-2-id': '3', +... 'form-2-name': 'Paul Verlaine', +... 'form-3-name': '', +... 'form-3-DELETE': '', +... } + +>>> formset = AuthorFormSet(data=data, queryset=qs) +>>> formset.is_valid() +True + +# One record has changed. +>>> formset.save() +[<Author: Walt Whitman>] + +Test the behavior of commit=False and save_m2m + +>>> meeting = AuthorMeeting.objects.create(created=date.today()) +>>> meeting.authors = Author.objects.all() + +# create an Author instance to add to the meeting. +>>> new_author = Author.objects.create(name=u'John Steinbeck') + +>>> AuthorMeetingFormSet = modelformset_factory(AuthorMeeting, extra=1, can_delete=True) +>>> data = { +... 'form-TOTAL_FORMS': '2', # the number of forms rendered +... 'form-INITIAL_FORMS': '1', # the number of forms with initial data +... 'form-MAX_FORMS': '0', # the max number of forms +... 'form-0-id': '1', +... 'form-0-name': '2nd Tuesday of the Week Meeting', +... 'form-0-authors': [2, 1, 3, 4], +... 'form-1-name': '', +... 'form-1-authors': '', +... 'form-1-DELETE': '', +... } +>>> formset = AuthorMeetingFormSet(data=data, queryset=AuthorMeeting.objects.all()) +>>> formset.is_valid() +True +>>> instances = formset.save(commit=False) +>>> for instance in instances: +... instance.created = date.today() +... instance.save() +>>> formset.save_m2m() +>>> instances[0].authors.all() +[<Author: Charles Baudelaire>, <Author: Walt Whitman>, <Author: Paul Verlaine>, <Author: John Steinbeck>] + +# delete the author we created to allow later tests to continue working. +>>> new_author.delete() + +Test the behavior of max_num with model formsets. It should properly limit +the queryset to reduce the amount of objects being pulled in when not being +used. + +>>> qs = Author.objects.order_by('name') + +>>> AuthorFormSet = modelformset_factory(Author, max_num=2) +>>> formset = AuthorFormSet(queryset=qs) +>>> formset.initial +[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}] + +>>> AuthorFormSet = modelformset_factory(Author, max_num=3) +>>> formset = AuthorFormSet(queryset=qs) +>>> formset.initial +[{'id': 1, 'name': u'Charles Baudelaire'}, {'id': 3, 'name': u'Paul Verlaine'}, {'id': 2, 'name': u'Walt Whitman'}] + +# Inline Formsets ############################################################ + +We can also create a formset that is tied to a parent model. This is how the +admin system's edit inline functionality works. + +>>> from django.newforms.models import inlineformset_factory + +>>> AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=3) +>>> author = Author.objects.get(name='Charles Baudelaire') + +>>> formset = AuthorBooksFormSet(instance=author) +>>> for form in formset.forms: +... print form.as_p() +<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p> +<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p> +<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p> + +>>> data = { +... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered +... 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data +... 'book_set-MAX_FORMS': '0', # the max number of forms +... 'book_set-0-title': 'Les Fleurs du Mal', +... 'book_set-1-title': '', +... 'book_set-2-title': '', +... } + +>>> formset = AuthorBooksFormSet(data, instance=author) +>>> formset.is_valid() +True + +>>> formset.save() +[<Book: Les Fleurs du Mal>] + +>>> for book in author.book_set.all(): +... print book.title +Les Fleurs du Mal + + +Now that we've added a book to Charles Baudelaire, let's try adding another +one. This time though, an edit form will be available for every existing +book. + +>>> AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) +>>> author = Author.objects.get(name='Charles Baudelaire') + +>>> formset = AuthorBooksFormSet(instance=author) +>>> for form in formset.forms: +... print form.as_p() +<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p> +<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p> +<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p> + +>>> data = { +... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered +... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data +... 'book_set-MAX_FORMS': '0', # the max number of forms +... 'book_set-0-id': '1', +... 'book_set-0-title': 'Les Fleurs du Mal', +... 'book_set-1-title': 'Le Spleen de Paris', +... 'book_set-2-title': '', +... } + +>>> formset = AuthorBooksFormSet(data, instance=author) +>>> formset.is_valid() +True + +>>> formset.save() +[<Book: Le Spleen de Paris>] + +As you can see, 'Le Spleen de Paris' is now a book belonging to Charles Baudelaire. + +>>> for book in author.book_set.order_by('title'): +... print book.title +Le Spleen de Paris +Les Fleurs du Mal + +The save_as_new parameter lets you re-associate the data to a new instance. +This is used in the admin for save_as functionality. + +>>> data = { +... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered +... 'book_set-INITIAL_FORMS': '2', # the number of forms with initial data +... 'book_set-MAX_FORMS': '0', # the max number of forms +... 'book_set-0-id': '1', +... 'book_set-0-title': 'Les Fleurs du Mal', +... 'book_set-1-id': '2', +... 'book_set-1-title': 'Le Spleen de Paris', +... 'book_set-2-title': '', +... } + +>>> formset = AuthorBooksFormSet(data, instance=Author(), save_as_new=True) +>>> formset.is_valid() +True + +>>> new_author = Author.objects.create(name='Charles Baudelaire') +>>> formset.instance = new_author +>>> [book for book in formset.save() if book.author.pk == new_author.pk] +[<Book: Les Fleurs du Mal>, <Book: Le Spleen de Paris>] + +"""} diff --git a/tests/regressiontests/admin_ordering/__init__.py b/tests/regressiontests/admin_ordering/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/admin_ordering/__init__.py diff --git a/tests/regressiontests/admin_ordering/models.py b/tests/regressiontests/admin_ordering/models.py new file mode 100644 index 0000000000..601f06bb0a --- /dev/null +++ b/tests/regressiontests/admin_ordering/models.py @@ -0,0 +1,46 @@ +# coding: utf-8 +from django.db import models + +class Band(models.Model): + name = models.CharField(max_length=100) + bio = models.TextField() + rank = models.IntegerField() + + class Meta: + ordering = ('name',) + +__test__ = {'API_TESTS': """ + +Let's make sure that ModelAdmin.queryset uses the ordering we define in +ModelAdmin rather that ordering defined in the model's inner Meta +class. + +>>> from django.contrib.admin.options import ModelAdmin + +>>> b1 = Band(name='Aerosmith', bio='', rank=3) +>>> b1.save() +>>> b2 = Band(name='Radiohead', bio='', rank=1) +>>> b2.save() +>>> b3 = Band(name='Van Halen', bio='', rank=2) +>>> b3.save() + +The default ordering should be by name, as specified in the inner Meta class. + +>>> ma = ModelAdmin(Band, None) +>>> [b.name for b in ma.queryset(None)] +[u'Aerosmith', u'Radiohead', u'Van Halen'] + + +Let's use a custom ModelAdmin that changes the ordering, and make sure it +actually changes. + +>>> class BandAdmin(ModelAdmin): +... ordering = ('rank',) # default ordering is ('name',) +... + +>>> ma = BandAdmin(Band, None) +>>> [b.name for b in ma.queryset(None)] +[u'Radiohead', u'Van Halen', u'Aerosmith'] + +""" +} diff --git a/tests/regressiontests/admin_views/__init__.py b/tests/regressiontests/admin_views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/admin_views/__init__.py diff --git a/tests/regressiontests/admin_views/fixtures/admin-views-users.xml b/tests/regressiontests/admin_views/fixtures/admin-views-users.xml new file mode 100644 index 0000000000..8d6c62b58f --- /dev/null +++ b/tests/regressiontests/admin_views/fixtures/admin-views-users.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="100" model="auth.user"> + <field type="CharField" name="username">super</field> + <field type="CharField" name="first_name">Super</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">super@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">True</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="101" model="auth.user"> + <field type="CharField" name="username">adduser</field> + <field type="CharField" name="first_name">Add</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">auser@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="102" model="auth.user"> + <field type="CharField" name="username">changeuser</field> + <field type="CharField" name="first_name">Change</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">cuser@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="103" model="auth.user"> + <field type="CharField" name="username">deleteuser</field> + <field type="CharField" name="first_name">Delete</field> + <field type="CharField" name="last_name">User</field> + <field type="CharField" name="email">duser@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">True</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="104" model="auth.user"> + <field type="CharField" name="username">joepublic</field> + <field type="CharField" name="first_name">Joe</field> + <field type="CharField" name="last_name">Public</field> + <field type="CharField" name="email">joepublic@example.com</field> + <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> + <field type="BooleanField" name="is_staff">False</field> + <field type="BooleanField" name="is_active">True</field> + <field type="BooleanField" name="is_superuser">False</field> + <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> + <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> + <field to="auth.group" name="groups" rel="ManyToManyRel"></field> + <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> + </object> + <object pk="1" model="admin_views.section"> + <field type="CharField" name="name">Test section</field> + </object> + <object pk="1" model="admin_views.article"> + <field type="TextField" name="content"><p>test content</p></field> + <field type="DateTimeField" name="date">2008-03-18 11:54:58</field> + <field to="admin_views.section" name="section" rel="ManyToOneRel">1</field> + </object> +</django-objects>
\ No newline at end of file diff --git a/tests/regressiontests/admin_views/fixtures/string-primary-key.xml b/tests/regressiontests/admin_views/fixtures/string-primary-key.xml new file mode 100644 index 0000000000..8e1dbf047f --- /dev/null +++ b/tests/regressiontests/admin_views/fixtures/string-primary-key.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="1" model="admin_views.modelwithstringprimarykey"> + <field type="CharField" name="id"><![CDATA[abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 -_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`]]></field> + </object> +</django-objects>
\ No newline at end of file diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py new file mode 100644 index 0000000000..2062107397 --- /dev/null +++ b/tests/regressiontests/admin_views/models.py @@ -0,0 +1,61 @@ +from django.db import models +from django.contrib import admin + +class Section(models.Model): + """ + A simple section that links to articles, to test linking to related items + in admin views. + """ + name = models.CharField(max_length=100) + +class Article(models.Model): + """ + A simple article to test admin views. Test backwards compatibility. + """ + content = models.TextField() + date = models.DateTimeField() + section = models.ForeignKey(Section) + +class ArticleAdmin(admin.ModelAdmin): + list_display = ('content', 'date') + list_filter = ('date',) + + def changelist_view(self, request): + "Test that extra_context works" + return super(ArticleAdmin, self).changelist_view( + request, extra_context={ + 'extra_var': 'Hello!' + } + ) + +class CustomArticle(models.Model): + content = models.TextField() + date = models.DateTimeField() + +class CustomArticleAdmin(admin.ModelAdmin): + """ + Tests various hooks for using custom templates and contexts. + """ + change_list_template = 'custom_admin/change_list.html' + change_form_template = 'custom_admin/change_form.html' + object_history_template = 'custom_admin/object_history.html' + delete_confirmation_template = 'custom_admin/delete_confirmation.html' + + def changelist_view(self, request): + "Test that extra_context works" + return super(CustomArticleAdmin, self).changelist_view( + request, extra_context={ + 'extra_var': 'Hello!' + } + ) + +class ModelWithStringPrimaryKey(models.Model): + id = models.CharField(max_length=255, primary_key=True) + + def __unicode__(self): + return self.id + +admin.site.register(Article, ArticleAdmin) +admin.site.register(CustomArticle, CustomArticleAdmin) +admin.site.register(Section) +admin.site.register(ModelWithStringPrimaryKey) diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py new file mode 100644 index 0000000000..ad50928aa9 --- /dev/null +++ b/tests/regressiontests/admin_views/tests.py @@ -0,0 +1,362 @@ + +from django.test import TestCase +from django.contrib.auth.models import User, Permission +from django.contrib.contenttypes.models import ContentType +from django.contrib.admin.models import LogEntry +from django.contrib.admin.sites import LOGIN_FORM_KEY, _encode_post_data +from django.contrib.admin.util import quote +from django.utils.html import escape + +# local test models +from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey + +def get_perm(Model, perm): + """Return the permission object, for the Model""" + ct = ContentType.objects.get_for_model(Model) + return Permission.objects.get(content_type=ct,codename=perm) + +class AdminViewPermissionsTest(TestCase): + """Tests for Admin Views Permissions.""" + + fixtures = ['admin-views-users.xml'] + + def setUp(self): + """Test setup.""" + # Setup permissions, for our users who can add, change, and delete. + # We can't put this into the fixture, because the content type id + # and the permission id could be different on each run of the test. + + opts = Article._meta + + # User who can add Articles + add_user = User.objects.get(username='adduser') + add_user.user_permissions.add(get_perm(Article, + opts.get_add_permission())) + + # User who can change Articles + change_user = User.objects.get(username='changeuser') + change_user.user_permissions.add(get_perm(Article, + opts.get_change_permission())) + + # User who can delete Articles + delete_user = User.objects.get(username='deleteuser') + delete_user.user_permissions.add(get_perm(Article, + opts.get_delete_permission())) + + delete_user.user_permissions.add(get_perm(Section, + Section._meta.get_delete_permission())) + + # login POST dicts + self.super_login = {'post_data': _encode_post_data({}), + LOGIN_FORM_KEY: 1, + 'username': 'super', + 'password': 'secret'} + self.super_email_login = {'post_data': _encode_post_data({}), + LOGIN_FORM_KEY: 1, + 'username': 'super@example.com', + 'password': 'secret'} + self.super_email_bad_login = {'post_data': _encode_post_data({}), + LOGIN_FORM_KEY: 1, + 'username': 'super@example.com', + 'password': 'notsecret'} + self.adduser_login = {'post_data': _encode_post_data({}), + LOGIN_FORM_KEY: 1, + 'username': 'adduser', + 'password': 'secret'} + self.changeuser_login = {'post_data': _encode_post_data({}), + LOGIN_FORM_KEY: 1, + 'username': 'changeuser', + 'password': 'secret'} + self.deleteuser_login = {'post_data': _encode_post_data({}), + LOGIN_FORM_KEY: 1, + 'username': 'deleteuser', + 'password': 'secret'} + self.joepublic_login = {'post_data': _encode_post_data({}), + LOGIN_FORM_KEY: 1, + 'username': 'joepublic', + 'password': 'secret'} + + def testTrailingSlashRequired(self): + """ + If you leave off the trailing slash, app should redirect and add it. + """ + self.client.post('/test_admin/admin/', self.super_login) + + request = self.client.get( + '/test_admin/admin/admin_views/article/add' + ) + self.assertRedirects(request, + '/test_admin/admin/admin_views/article/add/' + ) + + def testLogin(self): + """ + Make sure only staff members can log in. + + Successful posts to the login page will redirect to the orignal url. + Unsuccessfull attempts will continue to render the login page with + a 200 status code. + """ + # Super User + request = self.client.get('/test_admin/admin/') + self.failUnlessEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.super_login) + self.assertRedirects(login, '/test_admin/admin/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Test if user enters e-mail address + request = self.client.get('/test_admin/admin/') + self.failUnlessEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.super_email_login) + self.assertContains(login, "Your e-mail address is not your username") + # only correct passwords get a username hint + login = self.client.post('/test_admin/admin/', self.super_email_bad_login) + self.assertContains(login, "Usernames cannot contain the '@' character") + new_user = User(username='jondoe', password='secret', email='super@example.com') + new_user.save() + # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 + login = self.client.post('/test_admin/admin/', self.super_email_login) + self.assertContains(login, "Usernames cannot contain the '@' character") + + # Add User + request = self.client.get('/test_admin/admin/') + self.failUnlessEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.adduser_login) + self.assertRedirects(login, '/test_admin/admin/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Change User + request = self.client.get('/test_admin/admin/') + self.failUnlessEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.changeuser_login) + self.assertRedirects(login, '/test_admin/admin/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Delete User + request = self.client.get('/test_admin/admin/') + self.failUnlessEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.deleteuser_login) + self.assertRedirects(login, '/test_admin/admin/') + self.assertFalse(login.context) + self.client.get('/test_admin/admin/logout/') + + # Regular User should not be able to login. + request = self.client.get('/test_admin/admin/') + self.failUnlessEqual(request.status_code, 200) + login = self.client.post('/test_admin/admin/', self.joepublic_login) + self.failUnlessEqual(login.status_code, 200) + # Login.context is a list of context dicts we just need to check the first one. + self.assert_(login.context[0].get('error_message')) + + def testAddView(self): + """Test add view restricts access and actually adds items.""" + + add_dict = {'content': '<p>great article</p>', + 'date_0': '2008-03-18', 'date_1': '10:54:39', + 'section': 1} + + # Change User should not have access to add articles + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.changeuser_login) + request = self.client.get('/test_admin/admin/admin_views/article/add/') + self.failUnlessEqual(request.status_code, 403) + # Try POST just to make sure + post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) + self.failUnlessEqual(post.status_code, 403) + self.failUnlessEqual(Article.objects.all().count(), 1) + self.client.get('/test_admin/admin/logout/') + + # Add user may login and POST to add view, then redirect to admin root + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.adduser_login) + post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) + self.assertRedirects(post, '/test_admin/admin/') + self.failUnlessEqual(Article.objects.all().count(), 2) + self.client.get('/test_admin/admin/logout/') + + # Super can add too, but is redirected to the change list view + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.super_login) + post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) + self.assertRedirects(post, '/test_admin/admin/admin_views/article/') + self.failUnlessEqual(Article.objects.all().count(), 3) + self.client.get('/test_admin/admin/logout/') + + # Check and make sure that if user expires, data still persists + post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) + self.assertContains(post, 'Please log in again, because your session has expired.') + self.super_login['post_data'] = _encode_post_data(add_dict) + post = self.client.post('/test_admin/admin/admin_views/article/add/', self.super_login) + self.assertRedirects(post, '/test_admin/admin/admin_views/article/') + self.failUnlessEqual(Article.objects.all().count(), 4) + self.client.get('/test_admin/admin/logout/') + + def testChangeView(self): + """Change view should restrict access and allow users to edit items.""" + + change_dict = {'content': '<p>edited article</p>', + 'date_0': '2008-03-18', 'date_1': '10:54:39', + 'section': 1} + + # add user shoud not be able to view the list of article or change any of them + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.adduser_login) + request = self.client.get('/test_admin/admin/admin_views/article/') + self.failUnlessEqual(request.status_code, 403) + request = self.client.get('/test_admin/admin/admin_views/article/1/') + self.failUnlessEqual(request.status_code, 403) + post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) + self.failUnlessEqual(post.status_code, 403) + self.client.get('/test_admin/admin/logout/') + + # change user can view all items and edit them + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.changeuser_login) + request = self.client.get('/test_admin/admin/admin_views/article/') + self.failUnlessEqual(request.status_code, 200) + request = self.client.get('/test_admin/admin/admin_views/article/1/') + self.failUnlessEqual(request.status_code, 200) + post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) + self.assertRedirects(post, '/test_admin/admin/admin_views/article/') + self.failUnlessEqual(Article.objects.get(pk=1).content, '<p>edited article</p>') + self.client.get('/test_admin/admin/logout/') + + def testCustomModelAdminTemplates(self): + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.super_login) + + # Test custom change list template with custom extra context + request = self.client.get('/test_admin/admin/admin_views/customarticle/') + self.failUnlessEqual(request.status_code, 200) + self.assert_("var hello = 'Hello!';" in request.content) + self.assertTemplateUsed(request, 'custom_admin/change_list.html') + + # Test custom change form template + request = self.client.get('/test_admin/admin/admin_views/customarticle/add/') + self.assertTemplateUsed(request, 'custom_admin/change_form.html') + + # Add an article so we can test delete and history views + post = self.client.post('/test_admin/admin/admin_views/customarticle/add/', { + 'content': '<p>great article</p>', + 'date_0': '2008-03-18', + 'date_1': '10:54:39' + }) + self.assertRedirects(post, '/test_admin/admin/admin_views/customarticle/') + self.failUnlessEqual(CustomArticle.objects.all().count(), 1) + + # Test custom delete and object history templates + request = self.client.get('/test_admin/admin/admin_views/customarticle/1/delete/') + self.assertTemplateUsed(request, 'custom_admin/delete_confirmation.html') + request = self.client.get('/test_admin/admin/admin_views/customarticle/1/history/') + self.assertTemplateUsed(request, 'custom_admin/object_history.html') + + self.client.get('/test_admin/admin/logout/') + + def testCustomAdminSiteTemplates(self): + from django.contrib import admin + self.assertEqual(admin.site.index_template, None) + self.assertEqual(admin.site.login_template, None) + + self.client.get('/test_admin/admin/logout/') + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'admin/login.html') + self.client.post('/test_admin/admin/', self.changeuser_login) + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'admin/index.html') + + self.client.get('/test_admin/admin/logout/') + admin.site.login_template = 'custom_admin/login.html' + admin.site.index_template = 'custom_admin/index.html' + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'custom_admin/login.html') + self.assert_('Hello from a custom login template' in request.content) + self.client.post('/test_admin/admin/', self.changeuser_login) + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'custom_admin/index.html') + self.assert_('Hello from a custom index template' in request.content) + + # Finally, using monkey patching check we can inject custom_context arguments in to index + original_index = admin.site.index + def index(*args, **kwargs): + kwargs['extra_context'] = {'foo': '*bar*'} + return original_index(*args, **kwargs) + admin.site.index = index + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'custom_admin/index.html') + self.assert_('Hello from a custom index template *bar*' in request.content) + + self.client.get('/test_admin/admin/logout/') + del admin.site.index # Resets to using the original + admin.site.login_template = None + admin.site.index_template = None + + def testDeleteView(self): + """Delete view should restrict access and actually delete items.""" + + delete_dict = {'post': 'yes'} + + # add user shoud not be able to delete articles + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.adduser_login) + request = self.client.get('/test_admin/admin/admin_views/article/1/delete/') + self.failUnlessEqual(request.status_code, 403) + post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict) + self.failUnlessEqual(post.status_code, 403) + self.failUnlessEqual(Article.objects.all().count(), 1) + self.client.get('/test_admin/admin/logout/') + + # Delete user can delete + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', self.deleteuser_login) + response = self.client.get('/test_admin/admin/admin_views/section/1/delete/') + # test response contains link to related Article + self.assertContains(response, "admin_views/article/1/") + + response = self.client.get('/test_admin/admin/admin_views/article/1/delete/') + self.failUnlessEqual(response.status_code, 200) + post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict) + self.assertRedirects(post, '/test_admin/admin/') + self.failUnlessEqual(Article.objects.all().count(), 0) + self.client.get('/test_admin/admin/logout/') + +class AdminViewStringPrimaryKeyTest(TestCase): + fixtures = ['admin-views-users.xml', 'string-primary-key.xml'] + + def __init__(self, *args): + super(AdminViewStringPrimaryKeyTest, self).__init__(*args) + self.pk = """abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 -_.!~*'() ;/?:@&=+$, <>#%" {}|\^[]`""" + + def setUp(self): + self.client.login(username='super', password='secret') + content_type_pk = ContentType.objects.get_for_model(ModelWithStringPrimaryKey).pk + LogEntry.objects.log_action(100, content_type_pk, self.pk, self.pk, 2, change_message='') + + def tearDown(self): + self.client.logout() + + def test_get_change_view(self): + "Retrieving the object using urlencoded form of primary key should work" + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(self.pk)) + self.assertContains(response, escape(self.pk)) + self.failUnlessEqual(response.status_code, 200) + + def test_changelist_to_changeform_link(self): + "The link from the changelist referring to the changeform of the object should be quoted" + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/') + should_contain = """<tr class="row1"><th><a href="%s/">%s</a></th></tr>""" % (quote(self.pk), escape(self.pk)) + self.assertContains(response, should_contain) + + def test_recentactions_link(self): + "The link from the recent actions list referring to the changeform of the object should be quoted" + response = self.client.get('/test_admin/admin/') + should_contain = """<a href="admin_views/modelwithstringprimarykey/%s/">%s</a>""" % (quote(self.pk), escape(self.pk)) + self.assertContains(response, should_contain) + + def test_deleteconfirmation_link(self): + "The link from the delete confirmation page referring back to the changeform of the object should be quoted" + response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk)) + should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk)) + self.assertContains(response, should_contain) diff --git a/tests/regressiontests/admin_views/urls.py b/tests/regressiontests/admin_views/urls.py new file mode 100644 index 0000000000..e556812a45 --- /dev/null +++ b/tests/regressiontests/admin_views/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import * +from django.contrib import admin + +urlpatterns = patterns('', + (r'^admin/doc/', include('django.contrib.admindocs.urls')), + (r'^admin/(.*)', admin.site.root), +) diff --git a/tests/regressiontests/admin_widgets/__init__.py b/tests/regressiontests/admin_widgets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/admin_widgets/__init__.py diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py new file mode 100644 index 0000000000..584d973c83 --- /dev/null +++ b/tests/regressiontests/admin_widgets/models.py @@ -0,0 +1,85 @@ + +from django.conf import settings +from django.db import models + +class Member(models.Model): + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +class Band(models.Model): + name = models.CharField(max_length=100) + members = models.ManyToManyField(Member) + + def __unicode__(self): + return self.name + +class Album(models.Model): + band = models.ForeignKey(Band) + name = models.CharField(max_length=100) + + def __unicode__(self): + return self.name + +__test__ = {'WIDGETS_TESTS': """ +>>> from datetime import datetime +>>> from django.utils.html import escape, conditional_escape +>>> from django.contrib.admin.widgets import FilteredSelectMultiple, AdminSplitDateTime +>>> from django.contrib.admin.widgets import AdminFileWidget, ForeignKeyRawIdWidget, ManyToManyRawIdWidget +>>> from django.contrib.admin.widgets import RelatedFieldWidgetWrapper + +Calling conditional_escape on the output of widget.render will simulate what +happens in the template. This is easier than setting up a template and context +for each test. + +Make sure that the Admin widgets render properly, that is, without their extra +HTML escaped. + +>>> w = FilteredSelectMultiple('test', False) +>>> print conditional_escape(w.render('test', 'test')) +<select multiple="multiple" name="test"> +</select><script type="text/javascript">addEvent(window, "load", function(e) {SelectFilter.init("id_test", "test", 0, "%(ADMIN_MEDIA_PREFIX)s"); });</script> +<BLANKLINE> + +>>> w = AdminSplitDateTime() +>>> print conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30))) +<p class="datetime">Date: <input value="2007-12-01" type="text" class="vDateField" name="test_0" size="10" /><br />Time: <input value="09:30:00" type="text" class="vTimeField" name="test_1" size="8" /></p> + +>>> w = AdminFileWidget() +>>> print conditional_escape(w.render('test', 'test')) +Currently: <a target="_blank" href="%(MEDIA_URL)stest">test</a> <br />Change: <input type="file" name="test" /> + +>>> band = Band.objects.create(pk=1, name='Linkin Park') +>>> album = band.album_set.create(name='Hybrid Theory') + +>>> rel = Album._meta.get_field('band').rel +>>> w = ForeignKeyRawIdWidget(rel) +>>> print conditional_escape(w.render('test', band.pk, attrs={})) +<input type="text" name="test" value="1" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/band/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> <strong>Linkin Park</strong> + +>>> m1 = Member.objects.create(pk=1, name='Chester') +>>> m2 = Member.objects.create(pk=2, name='Mike') +>>> band.members.add(m1, m2) + +>>> rel = Band._meta.get_field('members').rel +>>> w = ManyToManyRawIdWidget(rel) +>>> print conditional_escape(w.render('test', [m1.pk, m2.pk], attrs={})) +<input type="text" name="test" value="1,2" class="vManyToManyRawIdAdminField" /><a href="../../../admin_widgets/member/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_MEDIA_PREFIX)simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a> +>>> 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 + +""" % { + 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, + 'MEDIA_URL': settings.MEDIA_URL, +}} diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index 041fa4054c..9add15163a 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -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..dedc0a8e52 --- /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.newforms import Form, CharField, IntegerField, ValidationError +>>> from django.newforms.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..3ea48876f5 --- /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.newforms 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/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/widgets.py b/tests/regressiontests/forms/widgets.py index 2c6b51a8ec..e0837ab5b3 100644 --- a/tests/regressiontests/forms/widgets.py +++ b/tests/regressiontests/forms/widgets.py @@ -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() @@ -573,6 +612,20 @@ 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 + # RadioSelect Widget ########################################################## >>> w = RadioSelect() @@ -871,6 +924,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 +962,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 +999,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() diff --git a/tests/regressiontests/inline_formsets/__init__.py b/tests/regressiontests/inline_formsets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/inline_formsets/__init__.py diff --git a/tests/regressiontests/inline_formsets/models.py b/tests/regressiontests/inline_formsets/models.py new file mode 100644 index 0000000000..b22f5e297d --- /dev/null +++ b/tests/regressiontests/inline_formsets/models.py @@ -0,0 +1,55 @@ +# coding: utf-8 +from django.db import models + +class School(models.Model): + name = models.CharField(max_length=100) + +class Parent(models.Model): + name = models.CharField(max_length=100) + +class Child(models.Model): + mother = models.ForeignKey(Parent, related_name='mothers_children') + father = models.ForeignKey(Parent, related_name='fathers_children') + school = models.ForeignKey(School) + name = models.CharField(max_length=100) + +__test__ = {'API_TESTS': """ + +>>> from django.newforms.models import inlineformset_factory + + +Child has two ForeignKeys to Parent, so if we don't specify which one to use +for the inline formset, we should get an exception. + +>>> ifs = inlineformset_factory(Parent, Child) +Traceback (most recent call last): + ... +Exception: <class 'regressiontests.inline_formsets.models.Child'> has more than 1 ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> + + +These two should both work without a problem. + +>>> ifs = inlineformset_factory(Parent, Child, fk_name='mother') +>>> ifs = inlineformset_factory(Parent, Child, fk_name='father') + + +If we specify fk_name, but it isn't a ForeignKey from the child model to the +parent model, we should get an exception. + +>>> ifs = inlineformset_factory(Parent, Child, fk_name='school') +Traceback (most recent call last): + ... +Exception: fk_name 'school' is not a ForeignKey to <class 'regressiontests.inline_formsets.models.Parent'> + + +If the field specified in fk_name is not a ForeignKey, we should get an +exception. + +>>> ifs = inlineformset_factory(Parent, Child, fk_name='test') +Traceback (most recent call last): + ... +Exception: <class 'regressiontests.inline_formsets.models.Child'> has no field named 'test' + + +""" +} diff --git a/tests/regressiontests/invalid_admin_options/models.py b/tests/regressiontests/invalid_admin_options/models.py deleted file mode 100644 index 14db463735..0000000000 --- a/tests/regressiontests/invalid_admin_options/models.py +++ /dev/null @@ -1,337 +0,0 @@ -""" -Admin options - -Test invalid and valid admin options to make sure that -model validation is working properly. -""" - -from django.db import models -model_errors = "" - -# TODO: Invalid admin options should not cause a metaclass error -##This should fail gracefully but is causing a metaclass error -#class BadAdminOption(models.Model): -# "Test nonexistent admin option" -# name = models.CharField(max_length=30) -# -# class Admin: -# nonexistent = 'option' -# -#model_errors += """invalid_admin_options.badadminoption: "admin" attribute, if given, must be set to a models.AdminOptions() instance. -#""" - -class ListDisplayBadOne(models.Model): - "Test list_display, list_display must be a list or tuple" - first_name = models.CharField(max_length=30) - - class Admin: - list_display = 'first_name' - -model_errors += """invalid_admin_options.listdisplaybadone: "admin.list_display", if given, must be set to a list or tuple. -""" - -class ListDisplayBadTwo(models.Model): - "Test list_display, list_display items must be attributes, methods or properties." - first_name = models.CharField(max_length=30) - - class Admin: - list_display = ['first_name','nonexistent'] - -model_errors += """invalid_admin_options.listdisplaybadtwo: "admin.list_display" refers to 'nonexistent', which isn't an attribute, method or property. -""" -class ListDisplayBadThree(models.Model): - "Test list_display, list_display items can not be a ManyToManyField." - first_name = models.CharField(max_length=30) - nick_names = models.ManyToManyField('ListDisplayGood') - - class Admin: - list_display = ['first_name','nick_names'] - -model_errors += """invalid_admin_options.listdisplaybadthree: "admin.list_display" doesn't support ManyToManyFields ('nick_names'). -""" - -class ListDisplayGood(models.Model): - "Test list_display, Admin list_display can be a attribute, method or property." - first_name = models.CharField(max_length=30) - - def _last_name(self): - return self.first_name - last_name = property(_last_name) - - def full_name(self): - return "%s %s" % (self.first_name, self.last_name) - - class Admin: - list_display = ['first_name','last_name','full_name'] - -class ListDisplayLinksBadOne(models.Model): - "Test list_display_links, item must be included in list_display." - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - - class Admin: - list_display = ['last_name'] - list_display_links = ['first_name'] - -model_errors += """invalid_admin_options.listdisplaylinksbadone: "admin.list_display_links" refers to 'first_name', which is not defined in "admin.list_display". -""" - -class ListDisplayLinksBadTwo(models.Model): - "Test list_display_links, must be a list or tuple." - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - - class Admin: - list_display = ['first_name','last_name'] - list_display_links = 'last_name' - -model_errors += """invalid_admin_options.listdisplaylinksbadtwo: "admin.list_display_links", if given, must be set to a list or tuple. -""" - -# TODO: Fix list_display_links validation or remove the check for list_display -## This is failing but the validation which should fail is not. -#class ListDisplayLinksBadThree(models.Model): -# "Test list_display_links, must define list_display to use list_display_links." -# first_name = models.CharField(max_length=30) -# last_name = models.CharField(max_length=30) -# -# class Admin: -# list_display_links = ('first_name',) -# -#model_errors += """invalid_admin_options.listdisplaylinksbadthree: "admin.list_display" must be defined for "admin.list_display_links" to be used. -#""" - -class ListDisplayLinksGood(models.Model): - "Test list_display_links, Admin list_display_list can be a attribute, method or property." - first_name = models.CharField(max_length=30) - - def _last_name(self): - return self.first_name - last_name = property(_last_name) - - def full_name(self): - return "%s %s" % (self.first_name, self.last_name) - - class Admin: - list_display = ['first_name','last_name','full_name'] - list_display_links = ['first_name','last_name','full_name'] - -class ListFilterBadOne(models.Model): - "Test list_filter, must be a list or tuple." - first_name = models.CharField(max_length=30) - - class Admin: - list_filter = 'first_name' - -model_errors += """invalid_admin_options.listfilterbadone: "admin.list_filter", if given, must be set to a list or tuple. -""" - -class ListFilterBadTwo(models.Model): - "Test list_filter, must be a field not a property or method." - first_name = models.CharField(max_length=30) - - def _last_name(self): - return self.first_name - last_name = property(_last_name) - - def full_name(self): - return "%s %s" % (self.first_name, self.last_name) - - class Admin: - list_filter = ['first_name','last_name','full_name'] - -model_errors += """invalid_admin_options.listfilterbadtwo: "admin.list_filter" refers to 'last_name', which isn't a field. -invalid_admin_options.listfilterbadtwo: "admin.list_filter" refers to 'full_name', which isn't a field. -""" - -class DateHierarchyBadOne(models.Model): - "Test date_hierarchy, must be a date or datetime field." - first_name = models.CharField(max_length=30) - birth_day = models.DateField() - - class Admin: - date_hierarchy = 'first_name' - -# TODO: Date Hierarchy needs to check if field is a date/datetime field. -#model_errors += """invalid_admin_options.datehierarchybadone: "admin.date_hierarchy" refers to 'first_name', which isn't a date field or datetime field. -#""" - -class DateHierarchyBadTwo(models.Model): - "Test date_hieracrhy, must be a field." - first_name = models.CharField(max_length=30) - birth_day = models.DateField() - - class Admin: - date_hierarchy = 'nonexistent' - -model_errors += """invalid_admin_options.datehierarchybadtwo: "admin.date_hierarchy" refers to 'nonexistent', which isn't a field. -""" - -class DateHierarchyGood(models.Model): - "Test date_hieracrhy, must be a field." - first_name = models.CharField(max_length=30) - birth_day = models.DateField() - - class Admin: - date_hierarchy = 'birth_day' - -class SearchFieldsBadOne(models.Model): - "Test search_fields, must be a list or tuple." - first_name = models.CharField(max_length=30) - - class Admin: - search_fields = ('nonexistent') - -# TODO: Add search_fields validation -#model_errors += """invalid_admin_options.seacrhfieldsbadone: "admin.search_fields", if given, must be set to a list or tuple. -#""" - -class SearchFieldsBadTwo(models.Model): - "Test search_fields, must be a field." - first_name = models.CharField(max_length=30) - - def _last_name(self): - return self.first_name - last_name = property(_last_name) - - class Admin: - search_fields = ['first_name','last_name'] - -# TODO: Add search_fields validation -#model_errors += """invalid_admin_options.seacrhfieldsbadone: "admin.search_fields" refers to 'last_name', which isn't a field. -#""" - -class SearchFieldsGood(models.Model): - "Test search_fields, must be a list or tuple." - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - - class Admin: - search_fields = ['first_name','last_name'] - - -class JsBadOne(models.Model): - "Test js, must be a list or tuple" - name = models.CharField(max_length=30) - - class Admin: - js = 'test.js' - -# TODO: Add a js validator -#model_errors += """invalid_admin_options.jsbadone: "admin.js", if given, must be set to a list or tuple. -#""" - -class SaveAsBad(models.Model): - "Test save_as, should be True or False" - name = models.CharField(max_length=30) - - class Admin: - save_as = 'not True or False' - -# TODO: Add a save_as validator. -#model_errors += """invalid_admin_options.saveasbad: "admin.save_as", if given, must be set to True or False. -#""" - -class SaveOnTopBad(models.Model): - "Test save_on_top, should be True or False" - name = models.CharField(max_length=30) - - class Admin: - save_on_top = 'not True or False' - -# TODO: Add a save_on_top validator. -#model_errors += """invalid_admin_options.saveontopbad: "admin.save_on_top", if given, must be set to True or False. -#""" - -class ListSelectRelatedBad(models.Model): - "Test list_select_related, should be True or False" - name = models.CharField(max_length=30) - - class Admin: - list_select_related = 'not True or False' - -# TODO: Add a list_select_related validator. -#model_errors += """invalid_admin_options.listselectrelatebad: "admin.list_select_related", if given, must be set to True or False. -#""" - -class ListPerPageBad(models.Model): - "Test list_per_page, should be a positive integer value." - name = models.CharField(max_length=30) - - class Admin: - list_per_page = 89.3 - -# TODO: Add a list_per_page validator. -#model_errors += """invalid_admin_options.listperpagebad: "admin.list_per_page", if given, must be a positive integer. -#""" - -class FieldsBadOne(models.Model): - "Test fields, should be a tuple" - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - - class Admin: - fields = 'not a tuple' - -# TODO: Add a fields validator. -#model_errors += """invalid_admin_options.fieldsbadone: "admin.fields", if given, must be a tuple. -#""" - -class FieldsBadTwo(models.Model): - """Test fields, 'fields' dict option is required.""" - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - - class Admin: - fields = ('Name', {'description': 'this fieldset needs fields'}) - -# TODO: Add a fields validator. -#model_errors += """invalid_admin_options.fieldsbadtwo: "admin.fields" each fieldset must include a 'fields' dict. -#""" - -class FieldsBadThree(models.Model): - """Test fields, 'classes' and 'description' are the only allowable extra dict options.""" - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - - class Admin: - fields = ('Name', {'fields': ('first_name','last_name'),'badoption': 'verybadoption'}) - -# TODO: Add a fields validator. -#model_errors += """invalid_admin_options.fieldsbadthree: "admin.fields" fieldset options must be either 'classes' or 'description'. -#""" - -class FieldsGood(models.Model): - "Test fields, working example" - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - birth_day = models.DateField() - - class Admin: - fields = ( - ('Name', {'fields': ('first_name','last_name'),'classes': 'collapse'}), - (None, {'fields': ('birth_day',),'description': 'enter your b-day'}) - ) - -class OrderingBad(models.Model): - "Test ordering, must be a field." - first_name = models.CharField(max_length=30) - last_name = models.CharField(max_length=30) - - class Admin: - ordering = 'nonexistent' - -# TODO: Add a ordering validator. -#model_errors += """invalid_admin_options.orderingbad: "admin.ordering" refers to 'nonexistent', which isn't a field. -#""" - -## TODO: Add a manager validator, this should fail gracefully. -#class ManagerBad(models.Model): -# "Test manager, must be a manager object." -# first_name = models.CharField(max_length=30) -# -# class Admin: -# manager = 'nonexistent' -# -#model_errors += """invalid_admin_options.managerbad: "admin.manager" refers to 'nonexistent', which isn't a Manager(). -#"""
\ No newline at end of file diff --git a/tests/regressiontests/modeladmin/__init__.py b/tests/regressiontests/modeladmin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/modeladmin/__init__.py diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py new file mode 100644 index 0000000000..17e3974e1c --- /dev/null +++ b/tests/regressiontests/modeladmin/models.py @@ -0,0 +1,876 @@ +# coding: utf-8 +from datetime import date + +from django.db import models +from django.contrib.auth.models import User + +class Band(models.Model): + name = models.CharField(max_length=100) + bio = models.TextField() + sign_date = models.DateField() + + def __unicode__(self): + return self.name + +class Concert(models.Model): + main_band = models.ForeignKey(Band, related_name='main_concerts') + opening_band = models.ForeignKey(Band, related_name='opening_concerts', + blank=True) + day = models.CharField(max_length=3, choices=((1, 'Fri'), (2, 'Sat'))) + transport = models.CharField(max_length=100, choices=( + (1, 'Plane'), + (2, 'Train'), + (3, 'Bus') + ), blank=True) + +class ValidationTestModel(models.Model): + name = models.CharField(max_length=100) + slug = models.SlugField() + users = models.ManyToManyField(User) + state = models.CharField(max_length=2, choices=(("CO", "Colorado"), ("WA", "Washington"))) + is_active = models.BooleanField() + pub_date = models.DateTimeField() + band = models.ForeignKey(Band) + +class ValidationTestInlineModel(models.Model): + parent = models.ForeignKey(ValidationTestModel) + +__test__ = {'API_TESTS': """ + +>>> from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL +>>> from django.contrib.admin.sites import AdminSite + +None of the following tests really depend on the content of the request, so +we'll just pass in None. + +>>> request = None + +# the sign_date is not 100 percent accurate ;) +>>> band = Band(name='The Doors', bio='', sign_date=date(1965, 1, 1)) +>>> band.save() + +Under the covers, the admin system will initialize ModelAdmin with a Model +class and an AdminSite instance, so let's just go ahead and do that manually +for testing. + +>>> site = AdminSite() +>>> ma = ModelAdmin(Band, site) + +>>> ma.get_form(request).base_fields.keys() +['name', 'bio', 'sign_date'] + + +# form/fields/fieldsets interaction ########################################## + +fieldsets_add and fieldsets_change should return a special data structure that +is used in the templates. They should generate the "right thing" whether we +have specified a custom form, the fields arugment, or nothing at all. + +Here's the default case. There are no custom form_add/form_change methods, +no fields argument, and no fieldsets argument. + +>>> ma = ModelAdmin(Band, site) +>>> ma.get_fieldsets(request) +[(None, {'fields': ['name', 'bio', 'sign_date']})] +>>> ma.get_fieldsets(request, band) +[(None, {'fields': ['name', 'bio', 'sign_date']})] + + +If we specify the fields argument, fieldsets_add and fielsets_change should +just stick the fields into a formsets structure and return it. + +>>> class BandAdmin(ModelAdmin): +... fields = ['name'] + +>>> ma = BandAdmin(Band, site) +>>> ma.get_fieldsets(request) +[(None, {'fields': ['name']})] +>>> ma.get_fieldsets(request, band) +[(None, {'fields': ['name']})] + + + + +If we specify fields or fieldsets, it should exclude fields on the Form class +to the fields specified. This may cause errors to be raised in the db layer if +required model fields arent in fields/fieldsets, but that's preferable to +ghost errors where you have a field in your Form class that isn't being +displayed because you forgot to add it to fields/fielsets + +>>> class BandAdmin(ModelAdmin): +... fields = ['name'] + +>>> ma = BandAdmin(Band, site) +>>> ma.get_form(request).base_fields.keys() +['name'] +>>> ma.get_form(request, band).base_fields.keys() +['name'] + +>>> class BandAdmin(ModelAdmin): +... fieldsets = [(None, {'fields': ['name']})] + +>>> ma = BandAdmin(Band, site) +>>> ma.get_form(request).base_fields.keys() +['name'] +>>> ma.get_form(request, band).base_fields.keys() +['name'] + + +If we specify a form, it should use it allowing custom validation to work +properly. This won't, however, break any of the admin widgets or media. + +>>> from django import newforms as forms +>>> class AdminBandForm(forms.ModelForm): +... delete = forms.BooleanField() +... +... class Meta: +... model = Band + +>>> class BandAdmin(ModelAdmin): +... form = AdminBandForm + +>>> ma = BandAdmin(Band, site) +>>> ma.get_form(request).base_fields.keys() +['name', 'bio', 'sign_date', 'delete'] +>>> type(ma.get_form(request).base_fields['sign_date'].widget) +<class 'django.contrib.admin.widgets.AdminDateWidget'> + +If we need to override the queryset of a ModelChoiceField in our custom form +make sure that RelatedFieldWidgetWrapper doesn't mess that up. + +>>> band2 = Band(name='The Beetles', bio='', sign_date=date(1962, 1, 1)) +>>> band2.save() + +>>> class AdminConcertForm(forms.ModelForm): +... class Meta: +... model = Concert +... +... def __init__(self, *args, **kwargs): +... super(AdminConcertForm, self).__init__(*args, **kwargs) +... self.fields["main_band"].queryset = Band.objects.filter(name='The Doors') + +>>> class ConcertAdmin(ModelAdmin): +... form = AdminConcertForm + +>>> ma = ConcertAdmin(Concert, site) +>>> form = ma.get_form(request)() +>>> print form["main_band"] +<select name="main_band" id="id_main_band"> +<option value="" selected="selected">---------</option> +<option value="1">The Doors</option> +</select> + +>>> band2.delete() + +# radio_fields behavior ################################################ + +First, without any radio_fields specified, the widgets for ForeignKey +and fields with choices specified ought to be a basic Select widget. +ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so +they need to be handled properly when type checking. For Select fields, all of +the choices lists have a first entry of dashes. + +>>> cma = ModelAdmin(Concert, site) +>>> cmafa = cma.get_form(request) + +>>> type(cmafa.base_fields['main_band'].widget.widget) +<class 'django.newforms.widgets.Select'> +>>> list(cmafa.base_fields['main_band'].widget.choices) +[(u'', u'---------'), (1, u'The Doors')] + +>>> type(cmafa.base_fields['opening_band'].widget.widget) +<class 'django.newforms.widgets.Select'> +>>> list(cmafa.base_fields['opening_band'].widget.choices) +[(u'', u'---------'), (1, u'The Doors')] + +>>> type(cmafa.base_fields['day'].widget) +<class 'django.newforms.widgets.Select'> +>>> list(cmafa.base_fields['day'].widget.choices) +[('', '---------'), (1, 'Fri'), (2, 'Sat')] + +>>> type(cmafa.base_fields['transport'].widget) +<class 'django.newforms.widgets.Select'> +>>> list(cmafa.base_fields['transport'].widget.choices) +[('', '---------'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')] + +Now specify all the fields as radio_fields. Widgets should now be +RadioSelect, and the choices list should have a first entry of 'None' if +blank=True for the model field. Finally, the widget should have the +'radiolist' attr, and 'inline' as well if the field is specified HORIZONTAL. + +>>> class ConcertAdmin(ModelAdmin): +... radio_fields = { +... 'main_band': HORIZONTAL, +... 'opening_band': VERTICAL, +... 'day': VERTICAL, +... 'transport': HORIZONTAL, +... } + +>>> cma = ConcertAdmin(Concert, site) +>>> cmafa = cma.get_form(request) + +>>> type(cmafa.base_fields['main_band'].widget.widget) +<class 'django.contrib.admin.widgets.AdminRadioSelect'> +>>> cmafa.base_fields['main_band'].widget.attrs +{'class': 'radiolist inline'} +>>> list(cmafa.base_fields['main_band'].widget.choices) +[(1, u'The Doors')] + +>>> type(cmafa.base_fields['opening_band'].widget.widget) +<class 'django.contrib.admin.widgets.AdminRadioSelect'> +>>> cmafa.base_fields['opening_band'].widget.attrs +{'class': 'radiolist'} +>>> list(cmafa.base_fields['opening_band'].widget.choices) +[(u'', u'None'), (1, u'The Doors')] + +>>> type(cmafa.base_fields['day'].widget) +<class 'django.contrib.admin.widgets.AdminRadioSelect'> +>>> cmafa.base_fields['day'].widget.attrs +{'class': 'radiolist'} +>>> list(cmafa.base_fields['day'].widget.choices) +[(1, 'Fri'), (2, 'Sat')] + +>>> type(cmafa.base_fields['transport'].widget) +<class 'django.contrib.admin.widgets.AdminRadioSelect'> +>>> cmafa.base_fields['transport'].widget.attrs +{'class': 'radiolist inline'} +>>> list(cmafa.base_fields['transport'].widget.choices) +[('', u'None'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')] + +>>> band.delete() + +# ModelAdmin Option Validation ################################################ + +>>> from django.contrib.admin.validation import validate +>>> from django.conf import settings + +# Ensure validation only runs when DEBUG = True + +>>> settings.DEBUG = True + +>>> class ValidationTestModelAdmin(ModelAdmin): +... raw_id_fields = 10 +>>> site = AdminSite() +>>> site.register(ValidationTestModel, ValidationTestModelAdmin) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.raw_id_fields` must be a list or tuple. + +>>> settings.DEBUG = False + +>>> class ValidationTestModelAdmin(ModelAdmin): +... raw_id_fields = 10 +>>> site = AdminSite() +>>> site.register(ValidationTestModel, ValidationTestModelAdmin) + +# raw_id_fields + +>>> class ValidationTestModelAdmin(ModelAdmin): +... raw_id_fields = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.raw_id_fields` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... raw_id_fields = ('non_existent_field',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.raw_id_fields` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... raw_id_fields = ('name',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.raw_id_fields[0]`, `name` must be either a ForeignKey or ManyToManyField. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... raw_id_fields = ('users',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# fieldsets + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = ({},) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets[0]` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = ((),) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets[0]` does not have exactly two elements. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = (("General", ()),) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets[0][1]` must be a dictionary. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = (("General", {}),) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `fields` key is required in ValidationTestModelAdmin.fieldsets[0][1] field options dict. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = (("General", {"fields": ("non_existent_field",)}),) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.fieldsets[0][1]['fields']` refers to field `non_existent_field` that is missing from the form. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = (("General", {"fields": ("name",)}),) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +>>> class ValidationTestModelAdmin(ModelAdmin): +... fieldsets = (("General", {"fields": ("name",)}),) +... fields = ["name",] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: Both fieldsets and fields are specified in ValidationTestModelAdmin. + +# form + +>>> class FakeForm(object): +... pass +>>> class ValidationTestModelAdmin(ModelAdmin): +... form = FakeForm +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: ValidationTestModelAdmin.form does not inherit from BaseModelForm. + +# fielsets with custom form + +>>> class BandAdmin(ModelAdmin): +... fieldsets = ( +... ('Band', { +... 'fields': ('non_existent_field',) +... }), +... ) +>>> validate(BandAdmin, Band) +Traceback (most recent call last): +... +ImproperlyConfigured: `BandAdmin.fieldsets[0][1]['fields']` refers to field `non_existent_field` that is missing from the form. + +>>> class BandAdmin(ModelAdmin): +... fieldsets = ( +... ('Band', { +... 'fields': ('name',) +... }), +... ) +>>> validate(BandAdmin, Band) + +>>> class AdminBandForm(forms.ModelForm): +... class Meta: +... model = Band +>>> class BandAdmin(ModelAdmin): +... form = AdminBandForm +... +... fieldsets = ( +... ('Band', { +... 'fields': ('non_existent_field',) +... }), +... ) +>>> validate(BandAdmin, Band) +Traceback (most recent call last): +... +ImproperlyConfigured: `BandAdmin.fieldsets[0][1]['fields']` refers to field `non_existent_field` that is missing from the form. + +>>> class AdminBandForm(forms.ModelForm): +... delete = forms.BooleanField() +... +... class Meta: +... model = Band +>>> class BandAdmin(ModelAdmin): +... form = AdminBandForm +... +... fieldsets = ( +... ('Band', { +... 'fields': ('name', 'bio', 'sign_date', 'delete') +... }), +... ) +>>> validate(BandAdmin, Band) + +# filter_vertical + +>>> class ValidationTestModelAdmin(ModelAdmin): +... filter_vertical = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.filter_vertical` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... filter_vertical = ("non_existent_field",) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.filter_vertical` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... filter_vertical = ("name",) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.filter_vertical[0]` must be a ManyToManyField. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... filter_vertical = ("users",) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# filter_horizontal + +>>> class ValidationTestModelAdmin(ModelAdmin): +... filter_horizontal = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.filter_horizontal` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... filter_horizontal = ("non_existent_field",) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.filter_horizontal` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... filter_horizontal = ("name",) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.filter_horizontal[0]` must be a ManyToManyField. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... filter_horizontal = ("users",) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# radio_fields + +>>> class ValidationTestModelAdmin(ModelAdmin): +... radio_fields = () +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.radio_fields` must be a dictionary. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... radio_fields = {"non_existent_field": None} +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.radio_fields` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... radio_fields = {"name": None} +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.radio_fields['name']` is neither an instance of ForeignKey nor does have choices set. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... radio_fields = {"state": None} +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.radio_fields['state']` is neither admin.HORIZONTAL nor admin.VERTICAL. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... radio_fields = {"state": VERTICAL} +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# prepopulated_fields + +>>> class ValidationTestModelAdmin(ModelAdmin): +... prepopulated_fields = () +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.prepopulated_fields` must be a dictionary. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... prepopulated_fields = {"non_existent_field": None} +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.prepopulated_fields` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... prepopulated_fields = {"slug": ("non_existent_field",)} +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.prepopulated_fields['non_existent_field'][0]` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... prepopulated_fields = {"users": ("name",)} +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.prepopulated_fields['users']` is either a DateTimeField, ForeignKey or ManyToManyField. This isn't allowed. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... prepopulated_fields = {"slug": ("name",)} +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# list_display + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_display = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_display` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_display = ('non_existent_field',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_display[0]` refers to `non_existent_field` that is neither a field, method or property of model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_display = ('users',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_display[0]`, `users` is a ManyToManyField which is not supported. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_display = ('name',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# list_display_links + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_display_links = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_display_links` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_display_links = ('non_existent_field',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_display_links[0]` refers to `non_existent_field` that is neither a field, method or property of model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_display_links = ('name',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_display_links[0]`refers to `name` which is not defined in `list_display`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_display = ('name',) +... list_display_links = ('name',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# list_filter + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_filter = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_filter` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_filter = ('non_existent_field',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_filter[0]` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_filter = ('is_active',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# list_per_page + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_per_page = 'hello' +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_per_page` should be a integer. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_per_page = 100 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# search_fields + +>>> class ValidationTestModelAdmin(ModelAdmin): +... search_fields = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.search_fields` must be a list or tuple. + +# date_hierarchy + +>>> class ValidationTestModelAdmin(ModelAdmin): +... date_hierarchy = 'non_existent_field' +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.date_hierarchy` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... date_hierarchy = 'name' +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.date_hierarchy is neither an instance of DateField nor DateTimeField. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... date_hierarchy = 'pub_date' +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# ordering + +>>> class ValidationTestModelAdmin(ModelAdmin): +... ordering = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.ordering` must be a list or tuple. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... ordering = ('non_existent_field',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.ordering[0]` refers to field `non_existent_field` that is missing from model `ValidationTestModel`. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... ordering = ('?', 'name') +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.ordering` has the random ordering marker `?`, but contains other fields as well. Please either remove `?` or the other fields. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... ordering = ('?',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +>>> class ValidationTestModelAdmin(ModelAdmin): +... ordering = ('band__name',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +>>> class ValidationTestModelAdmin(ModelAdmin): +... ordering = ('name',) +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# list_select_related + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_select_related = 1 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.list_select_related` should be a boolean. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... list_select_related = False +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# save_as + +>>> class ValidationTestModelAdmin(ModelAdmin): +... save_as = 1 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.save_as` should be a boolean. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... save_as = True +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# save_on_top + +>>> class ValidationTestModelAdmin(ModelAdmin): +... save_on_top = 1 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.save_on_top` should be a boolean. + +>>> class ValidationTestModelAdmin(ModelAdmin): +... save_on_top = True +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# inlines + +>>> from django.contrib.admin.options import TabularInline + +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = 10 +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.inlines` must be a list or tuple. + +>>> class ValidationTestInline(object): +... pass +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.inlines[0]` does not inherit from BaseModelAdmin. + +>>> class ValidationTestInline(TabularInline): +... pass +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `model` is a required attribute of `ValidationTestModelAdmin.inlines[0]`. + +>>> class SomethingBad(object): +... pass +>>> class ValidationTestInline(TabularInline): +... model = SomethingBad +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestModelAdmin.inlines[0].model` does not inherit from models.Model. + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# fields + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... fields = 10 +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestInline.fields` must be a list or tuple. + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... fields = ("non_existent_field",) +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestInline.fields` refers to field `non_existent_field` that is missing from the form. + +# fk_name + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... fk_name = "non_existent_field" +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestInline.fk_name` refers to field `non_existent_field` that is missing from model `ValidationTestInlineModel`. + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... fk_name = "parent" +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# extra + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... extra = "hello" +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestInline.extra` should be a integer. + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... extra = 2 +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# max_num + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... max_num = "hello" +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestInline.max_num` should be a integer. + +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... max_num = 2 +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +# formset + +>>> from django.newforms.models import BaseModelFormSet + +>>> class FakeFormSet(object): +... pass +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... formset = FakeFormSet +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) +Traceback (most recent call last): +... +ImproperlyConfigured: `ValidationTestInline.formset` does not inherit from BaseModelFormSet. + +>>> class RealModelFormSet(BaseModelFormSet): +... pass +>>> class ValidationTestInline(TabularInline): +... model = ValidationTestInlineModel +... formset = RealModelFormSet +>>> class ValidationTestModelAdmin(ModelAdmin): +... inlines = [ValidationTestInline] +>>> validate(ValidationTestModelAdmin, ValidationTestModel) + +""" +} diff --git a/tests/templates/custom_admin/change_form.html b/tests/templates/custom_admin/change_form.html new file mode 100644 index 0000000000..f42ba4b649 --- /dev/null +++ b/tests/templates/custom_admin/change_form.html @@ -0,0 +1 @@ +{% extends "admin/change_form.html" %} diff --git a/tests/templates/custom_admin/change_list.html b/tests/templates/custom_admin/change_list.html new file mode 100644 index 0000000000..eebc9c7e30 --- /dev/null +++ b/tests/templates/custom_admin/change_list.html @@ -0,0 +1,7 @@ +{% extends "admin/change_list.html" %} + +{% block extrahead %} +<script type="text/javascript"> +var hello = '{{ extra_var }}'; +</script> +{% endblock %} diff --git a/tests/templates/custom_admin/delete_confirmation.html b/tests/templates/custom_admin/delete_confirmation.html new file mode 100644 index 0000000000..9353c5bfc8 --- /dev/null +++ b/tests/templates/custom_admin/delete_confirmation.html @@ -0,0 +1 @@ +{% extends "admin/delete_confirmation.html" %} diff --git a/tests/templates/custom_admin/index.html b/tests/templates/custom_admin/index.html new file mode 100644 index 0000000000..75b6ca3d18 --- /dev/null +++ b/tests/templates/custom_admin/index.html @@ -0,0 +1,6 @@ +{% extends "admin/index.html" %} + +{% block content %} +Hello from a custom index template {{ foo }} +{{ block.super }} +{% endblock %} diff --git a/tests/templates/custom_admin/login.html b/tests/templates/custom_admin/login.html new file mode 100644 index 0000000000..e10a26952f --- /dev/null +++ b/tests/templates/custom_admin/login.html @@ -0,0 +1,6 @@ +{% extends "admin/login.html" %} + +{% block content %} +Hello from a custom login template +{{ block.super }} +{% endblock %} diff --git a/tests/templates/custom_admin/object_history.html b/tests/templates/custom_admin/object_history.html new file mode 100644 index 0000000000..aee3b5bcba --- /dev/null +++ b/tests/templates/custom_admin/object_history.html @@ -0,0 +1 @@ +{% extends "admin/object_history.html" %} diff --git a/tests/urls.py b/tests/urls.py index cea453ef37..a8dc583aa1 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -20,6 +20,9 @@ urlpatterns = patterns('', # test urlconf for middleware tests (r'^middleware/', include('regressiontests.middleware.urls')), + + # admin view tests + (r'^test_admin/', include('regressiontests.admin_views.urls')), (r'^utils/', include('regressiontests.utils.urls')), |
