diff options
| author | Justin Bronn <jbronn@gmail.com> | 2008-07-19 13:30:47 +0000 |
|---|---|---|
| committer | Justin Bronn <jbronn@gmail.com> | 2008-07-19 13:30:47 +0000 |
| commit | 149e731c3c5a2cc96b7d3c72070401df6c2a238e (patch) | |
| tree | 2a819f695246ab36139b7f8846085c9df1563bd8 /tests | |
| parent | 5bf3565a263533e37b2e1217e8d447cb7e02f5b4 (diff) | |
gis: Merged revisions 7921,7926-7928,7938-7941,7945-7947,7949-7950,7952,7955-7956,7961,7964-7968,7970-7978 via svnmerge from trunk.
This includes the newforms-admin branch, and thus is backwards-incompatible. The geographic admin is _not_ in this changeset, and is forthcoming.
git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7979 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'tests')
61 files changed, 3670 insertions, 430 deletions
diff --git a/tests/modeltests/fixtures/models.py b/tests/modeltests/fixtures/models.py index 5b53115733..8f8893ac88 100644 --- a/tests/modeltests/fixtures/models.py +++ b/tests/modeltests/fixtures/models.py @@ -58,7 +58,7 @@ __test__ = {'API_TESTS': """ # Database flushing does not work on MySQL with the default storage engine # because it requires transaction support. -if settings.DATABASE_ENGINE not in ('mysql', 'mysql_old'): +if settings.DATABASE_ENGINE != 'mysql': __test__['API_TESTS'] += \ """ # Reset the database representation of this app. This will delete all data. 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/lookup/models.py b/tests/modeltests/lookup/models.py index 7a53e93aec..3261a678d6 100644 --- a/tests/modeltests/lookup/models.py +++ b/tests/modeltests/lookup/models.py @@ -380,7 +380,7 @@ FieldError: Join on field 'headline' not permitted. """} -if settings.DATABASE_ENGINE not in ('mysql', 'mysql_old'): +if settings.DATABASE_ENGINE != 'mysql': __test__['API_TESTS'] += r""" # grouping and backreferences >>> Article.objects.filter(headline__regex=r'b(.).*b\1') diff --git a/tests/modeltests/many_to_one/models.py b/tests/modeltests/many_to_one/models.py index dfb17b8344..081cffb807 100644 --- a/tests/modeltests/many_to_one/models.py +++ b/tests/modeltests/many_to_one/models.py @@ -155,7 +155,7 @@ False [<Article: John's second story>, <Article: This is a test>] # And should work fine with the unicode that comes out of -# newforms.Form.cleaned_data +# forms.Form.cleaned_data >>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["many_to_one_reporter.last_name='%s'" % u'Smith']) [<Article: John's second story>, <Article: This is a test>] diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index e8598bd68f..cc9efd0f94 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -79,8 +79,8 @@ class ImageFile(models.Model): return self.description __test__ = {'API_TESTS': """ ->>> from django import newforms as forms ->>> from django.newforms.models import ModelForm +>>> from django import forms +>>> from django.forms.models import ModelForm >>> from django.core.files.uploadedfile import SimpleUploadedFile The bare bones, absolutely nothing custom, basic case. @@ -113,7 +113,7 @@ Replacing a field. ... model = Category >>> CategoryForm.base_fields['url'].__class__ -<class 'django.newforms.fields.BooleanField'> +<class 'django.forms.fields.BooleanField'> Using 'fields'. @@ -211,7 +211,7 @@ We can also subclass the Meta inner class to change the fields list. # Old form_for_x tests ####################################################### ->>> from django.newforms import ModelForm, CharField +>>> from django.forms import ModelForm, CharField >>> import datetime >>> Category.objects.all() @@ -605,7 +605,7 @@ the data in the database when the form is instantiated. # ModelChoiceField ############################################################ ->>> from django.newforms import ModelChoiceField, ModelMultipleChoiceField +>>> from django.forms import ModelChoiceField, ModelMultipleChoiceField >>> f = ModelChoiceField(Category.objects.all()) >>> list(f.choices) @@ -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..5958b8c27a --- /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.forms.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.forms.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/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py index f4eab6462d..111c72e7f5 100644 --- a/tests/modeltests/test_client/views.py +++ b/tests/modeltests/test_client/views.py @@ -4,8 +4,8 @@ from django.core.mail import EmailMessage, SMTPConnection from django.template import Context, Template from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound from django.contrib.auth.decorators import login_required, permission_required -from django.newforms.forms import Form -from django.newforms import fields +from django.forms.forms import Form +from django.forms import fields from django.shortcuts import render_to_response def get_view(request): diff --git a/tests/modeltests/transactions/models.py b/tests/modeltests/transactions/models.py index a3222cd511..06d21bbdd4 100644 --- a/tests/modeltests/transactions/models.py +++ b/tests/modeltests/transactions/models.py @@ -25,7 +25,7 @@ from django.conf import settings building_docs = getattr(settings, 'BUILDING_DOCS', False) -if building_docs or settings.DATABASE_ENGINE not in ('mysql', 'mysql_old'): +if building_docs or settings.DATABASE_ENGINE != 'mysql': __test__['API_TESTS'] += """ # the default behavior is to autocommit after each save() action >>> def create_a_reporter_then_fail(first, last): 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_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index 1bb0f7bed6..442f357782 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -53,7 +53,7 @@ class AdminScriptTestCase(unittest.TestCase): # Build the command line cmd = 'python "%s"' % script - cmd += ''.join(' %s' % arg for arg in args) + cmd += ''.join([' %s' % arg for arg in args]) # Remember the old environment old_django_settings_module = os.environ.get('DJANGO_SETTINGS_MODULE', None) @@ -108,7 +108,7 @@ class AdminScriptTestCase(unittest.TestCase): self.assertEquals(len(stream), 0, "Stream should be empty: actually contains '%s'" % stream) def assertOutput(self, stream, msg): "Utility assertion: assert that the given message exists in the output" - self.assertTrue(msg in stream, "'%s' does not match actual output text '%s'" % (msg, stream)) + self.failUnless(msg in stream, "'%s' does not match actual output text '%s'" % (msg, stream)) ########################################################################## # DJANGO ADMIN TESTS 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/datetime_safe/__init__.py b/tests/regressiontests/datetime_safe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/datetime_safe/__init__.py diff --git a/tests/regressiontests/datetime_safe/models.py b/tests/regressiontests/datetime_safe/models.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/regressiontests/datetime_safe/models.py diff --git a/tests/regressiontests/datetime_safe/tests.py b/tests/regressiontests/datetime_safe/tests.py new file mode 100644 index 0000000000..e1fe2b0f0e --- /dev/null +++ b/tests/regressiontests/datetime_safe/tests.py @@ -0,0 +1,37 @@ +r""" +>>> from datetime import date as original_date, datetime as original_datetime +>>> from django.utils.datetime_safe import date, datetime +>>> just_safe = (1900, 1, 1) +>>> just_unsafe = (1899, 12, 31, 23, 59, 59) +>>> really_old = (20, 1, 1) +>>> more_recent = (2006, 1, 1) + +>>> original_datetime(*more_recent) == datetime(*more_recent) +True +>>> original_datetime(*really_old) == datetime(*really_old) +True +>>> original_date(*more_recent) == date(*more_recent) +True +>>> original_date(*really_old) == date(*really_old) +True + +>>> original_date(*just_safe).strftime('%Y-%m-%d') == date(*just_safe).strftime('%Y-%m-%d') +True +>>> original_datetime(*just_safe).strftime('%Y-%m-%d') == datetime(*just_safe).strftime('%Y-%m-%d') +True + +>>> date(*just_unsafe[:3]).strftime('%Y-%m-%d (weekday %w)') +'1899-12-31 (weekday 0)' +>>> date(*just_safe).strftime('%Y-%m-%d (weekday %w)') +'1900-01-01 (weekday 1)' + +>>> datetime(*just_unsafe).strftime('%Y-%m-%d %H:%M:%S (weekday %w)') +'1899-12-31 23:59:59 (weekday 0)' +>>> datetime(*just_safe).strftime('%Y-%m-%d %H:%M:%S (weekday %w)') +'1900-01-01 00:00:00 (weekday 1)' + +>>> date(*just_safe).strftime('%y') # %y will error before this date +'00' +>>> datetime(*just_safe).strftime('%y') +'00' +""" diff --git a/tests/regressiontests/forms/error_messages.py b/tests/regressiontests/forms/error_messages.py index 580326f894..ec91b57a06 100644 --- a/tests/regressiontests/forms/error_messages.py +++ b/tests/regressiontests/forms/error_messages.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * +>>> from django.forms import * >>> from django.core.files.uploadedfile import SimpleUploadedFile # CharField ################################################################### diff --git a/tests/regressiontests/forms/extra.py b/tests/regressiontests/forms/extra.py index a8b369715d..80f7ef6535 100644 --- a/tests/regressiontests/forms/extra.py +++ b/tests/regressiontests/forms/extra.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * +>>> from django.forms import * >>> from django.utils.encoding import force_unicode >>> import datetime >>> import time @@ -14,12 +14,12 @@ tests = r""" # Extra stuff # ############### -The newforms library comes with some extra, higher-level Field and Widget +The forms library comes with some extra, higher-level Field and Widget classes that demonstrate some of the library's abilities. # SelectDateWidget ############################################################ ->>> from django.newforms.extras import SelectDateWidget +>>> from django.forms.extras import SelectDateWidget >>> w = SelectDateWidget(years=('2007','2008','2009','2010','2011','2012','2013','2014','2015','2016')) >>> print w.render('mydate', '') <select name="mydate_month" id="id_mydate_month"> @@ -424,7 +424,7 @@ u'sirrobin' # Test overriding ErrorList in a form # ####################################### ->>> from django.newforms.util import ErrorList +>>> from django.forms.util import ErrorList >>> class DivErrorList(ErrorList): ... def __unicode__(self): ... return self.as_divs() diff --git a/tests/regressiontests/forms/fields.py b/tests/regressiontests/forms/fields.py index f266e7bc50..c70ff2dff3 100644 --- a/tests/regressiontests/forms/fields.py +++ b/tests/regressiontests/forms/fields.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * ->>> from django.newforms.widgets import RadioFieldRenderer +>>> from django.forms import * +>>> from django.forms.widgets import RadioFieldRenderer >>> from django.core.files.uploadedfile import SimpleUploadedFile >>> import datetime >>> import time @@ -17,7 +17,7 @@ tests = r""" ########## Each Field class does some sort of validation. Each Field has a clean() method, -which either raises django.newforms.ValidationError or returns the "clean" +which either raises django.forms.ValidationError or returns the "clean" data -- usually a Unicode object, but, in some rare cases, a list. Each Field's __init__() takes at least these parameters: @@ -980,7 +980,7 @@ False # ChoiceField ################################################################# ->>> f = ChoiceField(choices=[('1', '1'), ('2', '2')]) +>>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')]) >>> f.clean('') Traceback (most recent call last): ... @@ -996,9 +996,9 @@ u'1' >>> f.clean('3') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] ->>> f = ChoiceField(choices=[('1', '1'), ('2', '2')], required=False) +>>> f = ChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) >>> f.clean('') u'' >>> f.clean(None) @@ -1010,7 +1010,7 @@ u'1' >>> f.clean('3') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] >>> f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')]) >>> f.clean('J') @@ -1018,7 +1018,25 @@ u'J' >>> f.clean('John') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +ValidationError: [u'Select a valid choice. John is not one of the available choices.'] + +>>> f = ChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) +>>> f.clean(1) +u'1' +>>> f.clean('1') +u'1' +>>> f.clean(3) +u'3' +>>> f.clean('3') +u'3' +>>> f.clean(5) +u'5' +>>> f.clean('5') +u'5' +>>> f.clean('6') +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] # NullBooleanField ############################################################ @@ -1036,7 +1054,7 @@ False # MultipleChoiceField ######################################################### ->>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')]) +>>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')]) >>> f.clean('') Traceback (most recent call last): ... @@ -1072,7 +1090,7 @@ Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] ->>> f = MultipleChoiceField(choices=[('1', '1'), ('2', '2')], required=False) +>>> f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')], required=False) >>> f.clean('') [] >>> f.clean(None) @@ -1100,6 +1118,29 @@ Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 3 is not one of the available choices.'] +>>> f = MultipleChoiceField(choices=[('Numbers', (('1', 'One'), ('2', 'Two'))), ('Letters', (('3','A'),('4','B'))), ('5','Other')]) +>>> f.clean([1]) +[u'1'] +>>> f.clean(['1']) +[u'1'] +>>> f.clean([1, 5]) +[u'1', u'5'] +>>> f.clean([1, '5']) +[u'1', u'5'] +>>> f.clean(['1', 5]) +[u'1', u'5'] +>>> f.clean(['1', '5']) +[u'1', u'5'] +>>> f.clean(['6']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] +>>> f.clean(['1','6']) +Traceback (most recent call last): +... +ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] + + # ComboField ################################################################## ComboField takes a list of fields that should be used to validate a value, @@ -1153,29 +1194,29 @@ u'' ... return x ... >>> import os ->>> from django import newforms as forms +>>> from django import forms >>> path = forms.__file__ >>> path = os.path.dirname(path) + '/' >>> fix_os_paths(path) -'.../django/newforms/' +'.../django/forms/' >>> f = forms.FilePathField(path=path) >>> f.choices.sort() >>> fix_os_paths(f.choices) -[('.../django/newforms/__init__.py', '__init__.py'), ('.../django/newforms/__init__.pyc', '__init__.pyc'), ('.../django/newforms/fields.py', 'fields.py'), ('.../django/newforms/fields.pyc', 'fields.pyc'), ('.../django/newforms/forms.py', 'forms.py'), ('.../django/newforms/forms.pyc', 'forms.pyc'), ('.../django/newforms/models.py', 'models.py'), ('.../django/newforms/models.pyc', 'models.pyc'), ('.../django/newforms/util.py', 'util.py'), ('.../django/newforms/util.pyc', 'util.pyc'), ('.../django/newforms/widgets.py', 'widgets.py'), ('.../django/newforms/widgets.pyc', 'widgets.pyc')] +[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/__init__.pyc', '__init__.pyc'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/fields.pyc', 'fields.pyc'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/forms.pyc', 'forms.pyc'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/models.pyc', 'models.pyc'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/util.pyc', 'util.pyc'), ('.../django/forms/widgets.py', 'widgets.py'), ('.../django/forms/widgets.pyc', 'widgets.pyc')] >>> f.clean('fields.py') Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] +ValidationError: [u'Select a valid choice. fields.py is not one of the available choices.'] >>> fix_os_paths(f.clean(path + 'fields.py')) -u'.../django/newforms/fields.py' +u'.../django/forms/fields.py' >>> f = forms.FilePathField(path=path, match='^.*?\.py$') >>> f.choices.sort() >>> fix_os_paths(f.choices) -[('.../django/newforms/__init__.py', '__init__.py'), ('.../django/newforms/fields.py', 'fields.py'), ('.../django/newforms/forms.py', 'forms.py'), ('.../django/newforms/models.py', 'models.py'), ('.../django/newforms/util.py', 'util.py'), ('.../django/newforms/widgets.py', 'widgets.py')] +[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/widgets.py', 'widgets.py')] >>> f = forms.FilePathField(path=path, recursive=True, match='^.*?\.py$') >>> f.choices.sort() >>> fix_os_paths(f.choices) -[('.../django/newforms/__init__.py', '__init__.py'), ('.../django/newforms/extras/__init__.py', 'extras/__init__.py'), ('.../django/newforms/extras/widgets.py', 'extras/widgets.py'), ('.../django/newforms/fields.py', 'fields.py'), ('.../django/newforms/forms.py', 'forms.py'), ('.../django/newforms/models.py', 'models.py'), ('.../django/newforms/util.py', 'util.py'), ('.../django/newforms/widgets.py', 'widgets.py')] +[('.../django/forms/__init__.py', '__init__.py'), ('.../django/forms/extras/__init__.py', 'extras/__init__.py'), ('.../django/forms/extras/widgets.py', 'extras/widgets.py'), ('.../django/forms/fields.py', 'fields.py'), ('.../django/forms/forms.py', 'forms.py'), ('.../django/forms/models.py', 'models.py'), ('.../django/forms/util.py', 'util.py'), ('.../django/forms/widgets.py', 'widgets.py')] # SplitDateTimeField ########################################################## diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py index 041fa4054c..6e6e4f79bf 100644 --- a/tests/regressiontests/forms/forms.py +++ b/tests/regressiontests/forms/forms.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * +>>> from django.forms import * >>> from django.core.files.uploadedfile import SimpleUploadedFile >>> import datetime >>> import time @@ -1667,4 +1667,76 @@ the list of errors is empty). You can also use it in {% if %} statements. <p><label>Password (again): <input type="password" name="password2" value="bar" /></label></p> <input type="submit" /> </form> + + +# The empty_permitted attribute ############################################## + +Sometimes (pretty much in formsets) we want to allow a form to pass validation +if it is completely empty. We can accomplish this by using the empty_permitted +agrument to a form constructor. + +>>> class SongForm(Form): +... artist = CharField() +... name = CharField() + +First let's show what happens id empty_permitted=False (the default): + +>>> data = {'artist': '', 'song': ''} + +>>> form = SongForm(data, empty_permitted=False) +>>> form.is_valid() +False +>>> form.errors +{'name': [u'This field is required.'], 'artist': [u'This field is required.']} +>>> form.cleaned_data +Traceback (most recent call last): +... +AttributeError: 'SongForm' object has no attribute 'cleaned_data' + + +Now let's show what happens when empty_permitted=True and the form is empty. + +>>> form = SongForm(data, empty_permitted=True) +>>> form.is_valid() +True +>>> form.errors +{} +>>> form.cleaned_data +{} + +But if we fill in data for one of the fields, the form is no longer empty and +the whole thing must pass validation. + +>>> data = {'artist': 'The Doors', 'song': ''} +>>> form = SongForm(data, empty_permitted=False) +>>> form.is_valid() +False +>>> form.errors +{'name': [u'This field is required.']} +>>> form.cleaned_data +Traceback (most recent call last): +... +AttributeError: 'SongForm' object has no attribute 'cleaned_data' + +If a field is not given in the data then None is returned for its data. Lets +make sure that when checking for empty_permitted that None is treated +accordingly. + +>>> data = {'artist': None, 'song': ''} +>>> form = SongForm(data, empty_permitted=True) +>>> form.is_valid() +True + +However, we *really* need to be sure we are checking for None as any data in +initial that returns False on a boolean call needs to be treated literally. + +>>> class PriceForm(Form): +... amount = FloatField() +... qty = IntegerField() + +>>> data = {'amount': '0.0', 'qty': ''} +>>> form = PriceForm(data, initial={'amount': 0.0}, empty_permitted=True) +>>> form.is_valid() +True + """ diff --git a/tests/regressiontests/forms/formsets.py b/tests/regressiontests/forms/formsets.py new file mode 100644 index 0000000000..bbbd4cee5a --- /dev/null +++ b/tests/regressiontests/forms/formsets.py @@ -0,0 +1,575 @@ +# -*- coding: utf-8 -*- +tests = """ +# Basic FormSet creation and usage ############################################ + +FormSet allows us to use multiple instance of the same form on 1 page. For now, +the best way to create a FormSet is by using the formset_factory function. + +>>> from django.forms import Form, CharField, IntegerField, ValidationError +>>> from django.forms.formsets import formset_factory, BaseFormSet + +>>> class Choice(Form): +... choice = CharField() +... votes = IntegerField() + +>>> ChoiceFormSet = formset_factory(Choice) + +A FormSet constructor takes the same arguments as Form. Let's create a FormSet +for adding data. By default, it displays 1 blank form. It can display more, +but we'll look at how to do so later. + +>>> formset = ChoiceFormSet(auto_id=False, prefix='choices') +>>> print formset +<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_FORMS" value="0" /> +<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr> +<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr> + + +On thing to note is that there needs to be a special value in the data. This +value tells the FormSet how many forms were displayed so it can tell how +many forms it needs to clean and validate. You could use javascript to create +new forms on the client side, but they won't get validated unless you increment +the TOTAL_FORMS field appropriately. + +>>> data = { +... 'choices-TOTAL_FORMS': '1', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... } + +We treat FormSet pretty much like we would treat a normal Form. FormSet has an +is_valid method, and a cleaned_data or errors attribute depending on whether all +the forms passed validation. However, unlike a Form instance, cleaned_data and +errors will be a list of dicts rather than just a single dict. + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{'votes': 100, 'choice': u'Calexico'}] + +If a FormSet was not passed any data, its is_valid method should return False. +>>> formset = ChoiceFormSet() +>>> formset.is_valid() +False + +FormSet instances can also have an error attribute if validation failed for +any of the forms. + +>>> data = { +... 'choices-TOTAL_FORMS': '1', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{'votes': [u'This field is required.']}] + + +We can also prefill a FormSet with existing data by providing an ``initial`` +argument to the constructor. ``initial`` should be a list of dicts. By default, +an extra blank form is included. + +>>> initial = [{'choice': u'Calexico', 'votes': 100}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li> + + +Let's simulate what would happen if we submitted this form. + +>>> data = { +... 'choices-TOTAL_FORMS': '2', # the number of forms rendered +... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{'votes': 100, 'choice': u'Calexico'}, {}] + +But the second form was blank! Shouldn't we get some errors? No. If we display +a form as blank, it's ok for it to be submitted as blank. If we fill out even +one of the fields of a blank form though, it will be validated. We may want to +required that at least x number of forms are completed, but we'll show how to +handle that later. + +>>> data = { +... 'choices-TOTAL_FORMS': '2', # the number of forms rendered +... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': 'The Decemberists', +... 'choices-1-votes': '', # missing value +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{}, {'votes': [u'This field is required.']}] + +If we delete data that was pre-filled, we should get an error. Simply removing +data from form fields isn't the proper way to delete it. We'll see how to +handle that case later. + +>>> data = { +... 'choices-TOTAL_FORMS': '2', # the number of forms rendered +... 'choices-INITIAL_FORMS': '1', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': '', # deleted value +... 'choices-0-votes': '', # deleted value +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{'votes': [u'This field is required.'], 'choice': [u'This field is required.']}, {}] + + +# Displaying more than 1 blank form ########################################### + +We can also display more than 1 empty form at a time. To do so, pass a +extra argument to formset_factory. + +>>> ChoiceFormSet = formset_factory(Choice, extra=3) + +>>> formset = ChoiceFormSet(auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" /></li> +<li>Votes: <input type="text" name="choices-0-votes" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> + +Since we displayed every form as blank, we will also accept them back as blank. +This may seem a little strange, but later we will show how to require a minimum +number of forms to be completed. + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': '', +... 'choices-0-votes': '', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{}, {}, {}] + + +We can just fill out one of the forms. + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': '', +... 'choices-1-votes': '', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{'votes': 100, 'choice': u'Calexico'}, {}, {}] + + +And once again, if we try to partially complete a form, validation will fail. + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '0', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-1-choice': 'The Decemberists', +... 'choices-1-votes': '', # missing value +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +False +>>> formset.errors +[{}, {'votes': [u'This field is required.']}, {}] + + +The extra argument also works when the formset is pre-filled with initial +data. + +>>> initial = [{'choice': u'Calexico', 'votes': 100}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Choice: <input type="text" name="choices-1-choice" /></li> +<li>Votes: <input type="text" name="choices-1-votes" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Choice: <input type="text" name="choices-3-choice" /></li> +<li>Votes: <input type="text" name="choices-3-votes" /></li> + + +# FormSets with deletion ###################################################### + +We can easily add deletion ability to a FormSet with an agrument to +formset_factory. This will add a boolean field to each form instance. When +that boolean field is True, the form will be in formset.deleted_forms + +>>> ChoiceFormSet = formset_factory(Choice, can_delete=True) + +>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li> + +To delete something, we just need to set that form's special delete field to +'on'. Let's go ahead and delete Fergie. + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-DELETE': '', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-DELETE': 'on', +... 'choices-2-choice': '', +... 'choices-2-votes': '', +... 'choices-2-DELETE': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> [form.cleaned_data for form in formset.forms] +[{'votes': 100, 'DELETE': False, 'choice': u'Calexico'}, {'votes': 900, 'DELETE': True, 'choice': u'Fergie'}, {}] +>>> [form.cleaned_data for form in formset.deleted_forms] +[{'votes': 900, 'DELETE': True, 'choice': u'Fergie'}] + + +# FormSets with ordering ###################################################### + +We can also add ordering ability to a FormSet with an agrument to +formset_factory. This will add a integer field to each form instance. When +form validation succeeds, [form.cleaned_data for form in formset.forms] will have the data in the correct +order specified by the ordering fields. If a number is duplicated in the set +of ordering fields, for instance form 0 and form 3 are both marked as 1, then +the form index used as a secondary ordering criteria. In order to put +something at the front of the list, you'd need to set it's order to 0. + +>>> ChoiceFormSet = formset_factory(Choice, can_order=True) + +>>> initial = [{'choice': u'Calexico', 'votes': 100}, {'choice': u'Fergie', 'votes': 900}] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li> +<li>Choice: <input type="text" name="choices-2-choice" /></li> +<li>Votes: <input type="text" name="choices-2-votes" /></li> +<li>Order: <input type="text" name="choices-2-ORDER" /></li> + +>>> data = { +... 'choices-TOTAL_FORMS': '3', # the number of forms rendered +... 'choices-INITIAL_FORMS': '2', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-ORDER': '1', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-ORDER': '2', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '500', +... 'choices-2-ORDER': '0', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> for form in formset.ordered_forms: +... print form.cleaned_data +{'votes': 500, 'ORDER': 0, 'choice': u'The Decemberists'} +{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'} +{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'} + +Ordering fields are allowed to be left blank, and if they *are* left blank, +they will be sorted below everything else. + +>>> data = { +... 'choices-TOTAL_FORMS': '4', # the number of forms rendered +... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-ORDER': '1', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-ORDER': '2', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '500', +... 'choices-2-ORDER': '', +... 'choices-3-choice': 'Basia Bulat', +... 'choices-3-votes': '50', +... 'choices-3-ORDER': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> for form in formset.ordered_forms: +... print form.cleaned_data +{'votes': 100, 'ORDER': 1, 'choice': u'Calexico'} +{'votes': 900, 'ORDER': 2, 'choice': u'Fergie'} +{'votes': 500, 'ORDER': None, 'choice': u'The Decemberists'} +{'votes': 50, 'ORDER': None, 'choice': u'Basia Bulat'} + + +# FormSets with ordering + deletion ########################################### + +Let's try throwing ordering and deletion into the same form. + +>>> ChoiceFormSet = formset_factory(Choice, can_order=True, can_delete=True) + +>>> initial = [ +... {'choice': u'Calexico', 'votes': 100}, +... {'choice': u'Fergie', 'votes': 900}, +... {'choice': u'The Decemberists', 'votes': 500}, +... ] +>>> formset = ChoiceFormSet(initial=initial, auto_id=False, prefix='choices') +>>> for form in formset.forms: +... print form.as_ul() +<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li> +<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li> +<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li> +<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li> +<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li> +<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li> +<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li> +<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li> +<li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists" /></li> +<li>Votes: <input type="text" name="choices-2-votes" value="500" /></li> +<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li> +<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li> +<li>Choice: <input type="text" name="choices-3-choice" /></li> +<li>Votes: <input type="text" name="choices-3-votes" /></li> +<li>Order: <input type="text" name="choices-3-ORDER" /></li> +<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li> + +Let's delete Fergie, and put The Decemberists ahead of Calexico. + +>>> data = { +... 'choices-TOTAL_FORMS': '4', # the number of forms rendered +... 'choices-INITIAL_FORMS': '3', # the number of forms with initial data +... 'choices-MAX_FORMS': '0', # the max number of forms +... 'choices-0-choice': 'Calexico', +... 'choices-0-votes': '100', +... 'choices-0-ORDER': '1', +... 'choices-0-DELETE': '', +... 'choices-1-choice': 'Fergie', +... 'choices-1-votes': '900', +... 'choices-1-ORDER': '2', +... 'choices-1-DELETE': 'on', +... 'choices-2-choice': 'The Decemberists', +... 'choices-2-votes': '500', +... 'choices-2-ORDER': '0', +... 'choices-2-DELETE': '', +... 'choices-3-choice': '', +... 'choices-3-votes': '', +... 'choices-3-ORDER': '', +... 'choices-3-DELETE': '', +... } + +>>> formset = ChoiceFormSet(data, auto_id=False, prefix='choices') +>>> formset.is_valid() +True +>>> for form in formset.ordered_forms: +... print form.cleaned_data +{'votes': 500, 'DELETE': False, 'ORDER': 0, 'choice': u'The Decemberists'} +{'votes': 100, 'DELETE': False, 'ORDER': 1, 'choice': u'Calexico'} +>>> [form.cleaned_data for form in formset.deleted_forms] +[{'votes': 900, 'DELETE': True, 'ORDER': 2, 'choice': u'Fergie'}] + + +# FormSet clean hook ########################################################## + +FormSets have a hook for doing extra validation that shouldn't be tied to any +particular form. It follows the same pattern as the clean hook on Forms. + +Let's define a FormSet that takes a list of favorite drinks, but raises am +error if there are any duplicates. + +>>> class FavoriteDrinkForm(Form): +... name = CharField() +... + +>>> class BaseFavoriteDrinksFormSet(BaseFormSet): +... def clean(self): +... seen_drinks = [] +... for drink in self.cleaned_data: +... if drink['name'] in seen_drinks: +... raise ValidationError('You may only specify a drink once.') +... seen_drinks.append(drink['name']) +... + +>>> FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, +... formset=BaseFavoriteDrinksFormSet, extra=3) + +We start out with a some duplicate data. + +>>> data = { +... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered +... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data +... 'drinks-MAX_FORMS': '0', # the max number of forms +... 'drinks-0-name': 'Gin and Tonic', +... 'drinks-1-name': 'Gin and Tonic', +... } + +>>> formset = FavoriteDrinksFormSet(data, prefix='drinks') +>>> formset.is_valid() +False + +Any errors raised by formset.clean() are available via the +formset.non_form_errors() method. + +>>> for error in formset.non_form_errors(): +... print error +You may only specify a drink once. + + +Make sure we didn't break the valid case. + +>>> data = { +... 'drinks-TOTAL_FORMS': '2', # the number of forms rendered +... 'drinks-INITIAL_FORMS': '0', # the number of forms with initial data +... 'drinks-MAX_FORMS': '0', # the max number of forms +... 'drinks-0-name': 'Gin and Tonic', +... 'drinks-1-name': 'Bloody Mary', +... } + +>>> formset = FavoriteDrinksFormSet(data, prefix='drinks') +>>> formset.is_valid() +True +>>> for error in formset.non_form_errors(): +... print error + +# Limiting the maximum number of forms ######################################## + +# Base case for max_num. + +>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=5, max_num=2) +>>> formset = LimitedFavoriteDrinkFormSet() +>>> for form in formset.forms: +... print form +<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr> + +# Ensure the that max_num has no affect when extra is less than max_forms. + +>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2) +>>> formset = LimitedFavoriteDrinkFormSet() +>>> for form in formset.forms: +... print form +<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" id="id_form-0-name" /></td></tr> + +# max_num with initial data + +# More initial forms than max_num will result in only the first max_num of +# them to be displayed with no extra forms. + +>>> initial = [ +... {'name': 'Gin Tonic'}, +... {'name': 'Bloody Mary'}, +... {'name': 'Jack and Coke'}, +... ] +>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=1, max_num=2) +>>> formset = LimitedFavoriteDrinkFormSet(initial=initial) +>>> for form in formset.forms: +... print form +<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" value="Bloody Mary" id="id_form-1-name" /></td></tr> + +# One form from initial and extra=3 with max_num=2 should result in the one +# initial form and one extra. + +>>> initial = [ +... {'name': 'Gin Tonic'}, +... ] +>>> LimitedFavoriteDrinkFormSet = formset_factory(FavoriteDrinkForm, extra=3, max_num=2) +>>> formset = LimitedFavoriteDrinkFormSet(initial=initial) +>>> for form in formset.forms: +... print form +<tr><th><label for="id_form-0-name">Name:</label></th><td><input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name" /></td></tr> +<tr><th><label for="id_form-1-name">Name:</label></th><td><input type="text" name="form-1-name" id="id_form-1-name" /></td></tr> + + +# Regression test for #6926 ################################################## + +Make sure the management form has the correct prefix. + +>>> formset = FavoriteDrinksFormSet() +>>> formset.management_form.prefix +'form' + +>>> formset = FavoriteDrinksFormSet(data={}) +>>> formset.management_form.prefix +'form' + +>>> formset = FavoriteDrinksFormSet(initial={}) +>>> formset.management_form.prefix +'form' + +""" diff --git a/tests/regressiontests/forms/media.py b/tests/regressiontests/forms/media.py new file mode 100644 index 0000000000..d05db1f164 --- /dev/null +++ b/tests/regressiontests/forms/media.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +# Tests for the media handling on widgets and forms + +media_tests = r""" +>>> from django.forms import TextInput, Media, TextInput, CharField, Form, MultiWidget +>>> from django.conf import settings +>>> ORIGINAL_MEDIA_URL = settings.MEDIA_URL +>>> settings.MEDIA_URL = 'http://media.example.com/media/' + +# Check construction of media objects +>>> m = Media(css={'all': ('path/to/css1','/path/to/css2')}, js=('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3')) +>>> print m +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +>>> class Foo: +... css = { +... 'all': ('path/to/css1','/path/to/css2') +... } +... js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') +>>> m3 = Media(Foo) +>>> print m3 +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +>>> m3 = Media(Foo) +>>> print m3 +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +# A widget can exist without a media definition +>>> class MyWidget(TextInput): +... pass + +>>> w = MyWidget() +>>> print w.media +<BLANKLINE> + +############################################################### +# DSL Class-based media definitions +############################################################### + +# A widget can define media if it needs to. +# Any absolute path will be preserved; relative paths are combined +# with the value of settings.MEDIA_URL +>>> class MyWidget1(TextInput): +... class Media: +... css = { +... 'all': ('path/to/css1','/path/to/css2') +... } +... js = ('/path/to/js1','http://media.other.com/path/to/js2','https://secure.other.com/path/to/js3') + +>>> w1 = MyWidget1() +>>> print w1.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +# Media objects can be interrogated by media type +>>> print w1.media['css'] +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> + +>>> print w1.media['js'] +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +# Media objects can be combined. Any given media resource will appear only +# once. Duplicated media definitions are ignored. +>>> class MyWidget2(TextInput): +... class Media: +... css = { +... 'all': ('/path/to/css2','/path/to/css3') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> class MyWidget3(TextInput): +... class Media: +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w2 = MyWidget2() +>>> w3 = MyWidget3() +>>> print w1.media + w2.media + w3.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# Check that media addition hasn't affected the original objects +>>> print w1.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +############################################################### +# Property-based media definitions +############################################################### + +# Widget media can be defined as a property +>>> class MyWidget4(TextInput): +... def _media(self): +... return Media(css={'all': ('/some/path',)}, js = ('/some/js',)) +... media = property(_media) + +>>> w4 = MyWidget4() +>>> print w4.media +<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script> + +# Media properties can reference the media of their parents +>>> class MyWidget5(MyWidget4): +... def _media(self): +... return super(MyWidget5, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',)) +... media = property(_media) + +>>> w5 = MyWidget5() +>>> print w5.media +<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script> +<script type="text/javascript" src="/other/js"></script> + +# Media properties can reference the media of their parents, +# even if the parent media was defined using a class +>>> class MyWidget6(MyWidget1): +... def _media(self): +... return super(MyWidget6, self).media + Media(css={'all': ('/other/path',)}, js = ('/other/js',)) +... media = property(_media) + +>>> w6 = MyWidget6() +>>> print w6.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/other/js"></script> + +############################################################### +# Inheritance of media +############################################################### + +# If a widget extends another but provides no media definition, it inherits the parent widget's media +>>> class MyWidget7(MyWidget1): +... pass + +>>> w7 = MyWidget7() +>>> print w7.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> + +# If a widget extends another but defines media, it extends the parent widget's media by default +>>> class MyWidget8(MyWidget1): +... class Media: +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w8 = MyWidget8() +>>> print w8.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# If a widget extends another but defines media, it extends the parents widget's media, +# even if the parent defined media using a property. +>>> class MyWidget9(MyWidget4): +... class Media: +... css = { +... 'all': ('/other/path',) +... } +... js = ('/other/js',) + +>>> w9 = MyWidget9() +>>> print w9.media +<link href="/some/path" type="text/css" media="all" rel="stylesheet" /> +<link href="/other/path" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/some/js"></script> +<script type="text/javascript" src="/other/js"></script> + +# A widget can disable media inheritance by specifying 'extend=False' +>>> class MyWidget10(MyWidget1): +... class Media: +... extend = False +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w10 = MyWidget10() +>>> print w10.media +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# A widget can explicitly enable full media inheritance by specifying 'extend=True' +>>> class MyWidget11(MyWidget1): +... class Media: +... extend = True +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w11 = MyWidget11() +>>> print w11.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# A widget can enable inheritance of one media type by specifying extend as a tuple +>>> class MyWidget12(MyWidget1): +... class Media: +... extend = ('css',) +... css = { +... 'all': ('/path/to/css3','path/to/css1') +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> w12 = MyWidget12() +>>> print w12.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +############################################################### +# Multi-media handling for CSS +############################################################### + +# A widget can define CSS media for multiple output media types +>>> class MultimediaWidget(TextInput): +... class Media: +... css = { +... 'screen, print': ('/file1','/file2'), +... 'screen': ('/file3',), +... 'print': ('/file4',) +... } +... js = ('/path/to/js1','/path/to/js4') + +>>> multimedia = MultimediaWidget() +>>> print multimedia.media +<link href="/file4" type="text/css" media="print" rel="stylesheet" /> +<link href="/file3" type="text/css" media="screen" rel="stylesheet" /> +<link href="/file1" type="text/css" media="screen, print" rel="stylesheet" /> +<link href="/file2" type="text/css" media="screen, print" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +############################################################### +# Multiwidget media handling +############################################################### + +# MultiWidgets have a default media definition that gets all the +# media from the component widgets +>>> class MyMultiWidget(MultiWidget): +... def __init__(self, attrs=None): +... widgets = [MyWidget1, MyWidget2, MyWidget3] +... super(MyMultiWidget, self).__init__(widgets, attrs) + +>>> mymulti = MyMultiWidget() +>>> print mymulti.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +############################################################### +# Media processing for forms +############################################################### + +# You can ask a form for the media required by its widgets. +>>> class MyForm(Form): +... field1 = CharField(max_length=20, widget=MyWidget1()) +... field2 = CharField(max_length=20, widget=MyWidget2()) +>>> f1 = MyForm() +>>> print f1.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# Form media can be combined to produce a single media definition. +>>> class AnotherForm(Form): +... field3 = CharField(max_length=20, widget=MyWidget3()) +>>> f2 = AnotherForm() +>>> print f1.media + f2.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> + +# Forms can also define media, following the same rules as widgets. +>>> class FormWithMedia(Form): +... field1 = CharField(max_length=20, widget=MyWidget1()) +... field2 = CharField(max_length=20, widget=MyWidget2()) +... class Media: +... js = ('/some/form/javascript',) +... css = { +... 'all': ('/some/form/css',) +... } +>>> f3 = FormWithMedia() +>>> print f3.media +<link href="http://media.example.com/media/path/to/css1" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css2" type="text/css" media="all" rel="stylesheet" /> +<link href="/path/to/css3" type="text/css" media="all" rel="stylesheet" /> +<link href="/some/form/css" type="text/css" media="all" rel="stylesheet" /> +<script type="text/javascript" src="/path/to/js1"></script> +<script type="text/javascript" src="http://media.other.com/path/to/js2"></script> +<script type="text/javascript" src="https://secure.other.com/path/to/js3"></script> +<script type="text/javascript" src="/path/to/js4"></script> +<script type="text/javascript" src="/some/form/javascript"></script> + +>>> settings.MEDIA_URL = ORIGINAL_MEDIA_URL +"""
\ No newline at end of file diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py index c7ce128560..98b9233d80 100644 --- a/tests/regressiontests/forms/models.py +++ b/tests/regressiontests/forms/models.py @@ -15,7 +15,7 @@ class ChoiceModel(models.Model): name = models.CharField(max_length=10) __test__ = {'API_TESTS': """ ->>> from django.newforms import form_for_model, form_for_instance +>>> from django.forms import form_for_model, form_for_instance # Boundary conditions on a PostitiveIntegerField ######################### >>> BoundaryForm = form_for_model(BoundaryModel) diff --git a/tests/regressiontests/forms/regressions.py b/tests/regressiontests/forms/regressions.py index cbc8095e60..87390d3cd1 100644 --- a/tests/regressiontests/forms/regressions.py +++ b/tests/regressiontests/forms/regressions.py @@ -3,7 +3,7 @@ tests = r""" It should be possible to re-use attribute dictionaries (#3810) ->>> from django.newforms import * +>>> from django.forms import * >>> extra_attrs = {'class': 'special'} >>> class TestForm(Form): ... f1 = CharField(max_length=10, widget=TextInput(attrs=extra_attrs)) diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py index bb0e30b874..ff8213c8d9 100644 --- a/tests/regressiontests/forms/tests.py +++ b/tests/regressiontests/forms/tests.py @@ -26,6 +26,8 @@ from localflavor.za import tests as localflavor_za_tests from regressions import tests as regression_tests from util import tests as util_tests from widgets import tests as widgets_tests +from formsets import tests as formset_tests +from media import media_tests __test__ = { 'extra_tests': extra_tests, @@ -53,6 +55,8 @@ __test__ = { 'localflavor_us_tests': localflavor_us_tests, 'localflavor_za_tests': localflavor_za_tests, 'regression_tests': regression_tests, + 'formset_tests': formset_tests, + 'media_tests': media_tests, 'util_tests': util_tests, 'widgets_tests': widgets_tests, } diff --git a/tests/regressiontests/forms/util.py b/tests/regressiontests/forms/util.py index bfaf73f6bc..68c082c114 100644 --- a/tests/regressiontests/forms/util.py +++ b/tests/regressiontests/forms/util.py @@ -1,17 +1,17 @@ # coding: utf-8 """ -Tests for newforms/util.py module. +Tests for forms/util.py module. """ tests = r""" ->>> from django.newforms.util import * +>>> from django.forms.util import * >>> from django.utils.translation import ugettext_lazy ########### # flatatt # ########### ->>> from django.newforms.util import flatatt +>>> from django.forms.util import flatatt >>> flatatt({'id': "header"}) u' id="header"' >>> flatatt({'class': "news", 'title': "Read this"}) diff --git a/tests/regressiontests/forms/widgets.py b/tests/regressiontests/forms/widgets.py index 2c6b51a8ec..40c4d01793 100644 --- a/tests/regressiontests/forms/widgets.py +++ b/tests/regressiontests/forms/widgets.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- tests = r""" ->>> from django.newforms import * ->>> from django.newforms.widgets import RadioFieldRenderer +>>> from django.forms import * +>>> from django.forms.widgets import RadioFieldRenderer >>> from django.utils.safestring import mark_safe >>> import datetime >>> import time @@ -202,6 +202,30 @@ u'<input type="file" class="fun" name="email" />' >>> w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}) u'<input type="file" class="fun" name="email" />' +Test for the behavior of _has_changed for FileInput. The value of data will +more than likely come from request.FILES. The value of initial data will +likely be a filename stored in the database. Since its value is of no use to +a FileInput it is ignored. + +>>> w = FileInput() + +# No file was uploaded and no initial data. +>>> w._has_changed(u'', None) +False + +# A file was uploaded and no initial data. +>>> w._has_changed(u'', {'filename': 'resume.txt', 'content': 'My resume'}) +True + +# A file was not uploaded, but there is initial data +>>> w._has_changed(u'resume.txt', None) +False + +# A file was uploaded and there is initial data (file identity is not dealt +# with here) +>>> w._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'}) +True + # Textarea Widget ############################################################# >>> w = Textarea() @@ -292,6 +316,21 @@ checkboxes). >>> w.value_from_datadict({}, {}, 'testing') False +>>> w._has_changed(None, None) +False +>>> w._has_changed(None, u'') +False +>>> w._has_changed(u'', None) +False +>>> w._has_changed(u'', u'') +False +>>> w._has_changed(False, u'on') +True +>>> w._has_changed(True, u'on') +False +>>> w._has_changed(True, u'') +True + # Select Widget ############################################################### >>> w = Select() @@ -419,6 +458,35 @@ over multiple times without getting consumed: <option value="4">4</option> </select> +Choices can be nested one level in order to create HTML optgroups: +>>> w.choices=(('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) +>>> print w.render('nestchoice', None) +<select name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', 'outer1') +<select name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', 'inner1') +<select name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1" selected="selected">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + # NullBooleanSelect Widget #################################################### >>> w = NullBooleanSelect() @@ -573,6 +641,58 @@ If 'choices' is passed to both the constructor and render(), then they'll both b >>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]) u'<select multiple="multiple" name="nums">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" selected="selected">\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</option>\n<option value="\u0107\u017e\u0161\u0111">abc\u0107\u017e\u0161\u0111</option>\n</select>' +# Test the usage of _has_changed +>>> w._has_changed(None, None) +False +>>> w._has_changed([], None) +False +>>> w._has_changed(None, [u'1']) +True +>>> w._has_changed([1, 2], [u'1', u'2']) +False +>>> w._has_changed([1, 2], [u'1']) +True +>>> w._has_changed([1, 2], [u'1', u'3']) +True + +# Choices can be nested one level in order to create HTML optgroups: +>>> w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) +>>> print w.render('nestchoice', None) +<select multiple="multiple" name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', ['outer1']) +<select multiple="multiple" name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', ['inner1']) +<select multiple="multiple" name="nestchoice"> +<option value="outer1">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1" selected="selected">Inner 1</option> +<option value="inner2">Inner 2</option> +</optgroup> +</select> + +>>> print w.render('nestchoice', ['outer1', 'inner2']) +<select multiple="multiple" name="nestchoice"> +<option value="outer1" selected="selected">Outer 1</option> +<optgroup label="Group "1""> +<option value="inner1">Inner 1</option> +<option value="inner2" selected="selected">Inner 2</option> +</optgroup> +</select> + # RadioSelect Widget ########################################################## >>> w = RadioSelect() @@ -871,6 +991,20 @@ If 'choices' is passed to both the constructor and render(), then they'll both b <li><label><input type="checkbox" name="escape" value="good" /> you > me</label></li> </ul> +# Test the usage of _has_changed +>>> w._has_changed(None, None) +False +>>> w._has_changed([], None) +False +>>> w._has_changed(None, [u'1']) +True +>>> w._has_changed([1, 2], [u'1', u'2']) +False +>>> w._has_changed([1, 2], [u'1']) +True +>>> w._has_changed([1, 2], [u'1', u'3']) +True + # Unicode choices are correctly rendered as HTML >>> w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]) u'<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>' @@ -895,6 +1029,25 @@ u'<input id="foo_0" type="text" class="big" value="john" name="name_0" /><br />< >>> w.render('name', ['john', 'lennon']) u'<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />' +>>> w = MyMultiWidget(widgets=(TextInput(), TextInput())) + +# test with no initial data +>>> w._has_changed(None, [u'john', u'lennon']) +True + +# test when the data is the same as initial +>>> w._has_changed(u'john__lennon', [u'john', u'lennon']) +False + +# test when the first widget's data has changed +>>> w._has_changed(u'john__lennon', [u'alfred', u'lennon']) +True + +# test when the last widget's data has changed. this ensures that it is not +# short circuiting while testing the widgets. +>>> w._has_changed(u'john__lennon', [u'john', u'denver']) +True + # SplitDateTimeWidget ######################################################### >>> w = SplitDateTimeWidget() @@ -913,6 +1066,11 @@ included on both widgets. >>> w.render('date', datetime.datetime(2006, 1, 10, 7, 30)) u'<input type="text" class="pretty" value="2006-01-10" name="date_0" /><input type="text" class="pretty" value="07:30:00" name="date_1" />' +>>> w._has_changed(datetime.datetime(2008, 5, 5, 12, 40, 00), [u'2008-05-05', u'12:40:00']) +False +>>> w._has_changed(datetime.datetime(2008, 5, 5, 12, 40, 00), [u'2008-05-05', u'12:41:00']) +True + # DateTimeInput ############################################################### >>> w = DateTimeInput() 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..c00703852f --- /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.forms.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/mail/__init__.py b/tests/regressiontests/mail/__init__.py new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/tests/regressiontests/mail/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/tests/regressiontests/mail/models.py b/tests/regressiontests/mail/models.py new file mode 100644 index 0000000000..7ff128fa69 --- /dev/null +++ b/tests/regressiontests/mail/models.py @@ -0,0 +1 @@ +# This file intentionally left blank
\ No newline at end of file diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py new file mode 100644 index 0000000000..9d2e2abe96 --- /dev/null +++ b/tests/regressiontests/mail/tests.py @@ -0,0 +1,41 @@ +# coding: utf-8 +r""" +# Tests for the django.core.mail. + +>>> from django.core.mail import EmailMessage + +# Test normal ascii character case: + +>>> email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com']) +>>> message = email.message() +>>> message['Subject'] +'Subject' +>>> message.get_payload() +'Content' +>>> message['From'] +'from@example.com' +>>> message['To'] +'to@example.com' + +# Test multiple-recipient case + +>>> email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com','other@example.com']) +>>> message = email.message() +>>> message['Subject'] +'Subject' +>>> message.get_payload() +'Content' +>>> message['From'] +'from@example.com' +>>> message['To'] +'to@example.com, other@example.com' + +# Test for header injection + +>>> email = EmailMessage('Subject\nInjection Test', 'Content', 'from@example.com', ['to@example.com']) +>>> message = email.message() +Traceback (most recent call last): + ... +BadHeaderError: Header values can't contain newlines (got 'Subject\nInjection Test' for header 'Subject') + +""" 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..6a7da7d362 --- /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 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.forms.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.forms.widgets.Select'> +>>> list(cmafa.base_fields['opening_band'].widget.choices) +[(u'', u'---------'), (1, u'The Doors')] + +>>> type(cmafa.base_fields['day'].widget) +<class 'django.forms.widgets.Select'> +>>> list(cmafa.base_fields['day'].widget.choices) +[('', '---------'), (1, 'Fri'), (2, 'Sat')] + +>>> type(cmafa.base_fields['transport'].widget) +<class 'django.forms.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.forms.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/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 65d0d6ec65..847d515422 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -4,16 +4,17 @@ Various complex queries that have been problematic in the past. import datetime import pickle +import sys from django.db import models -from django.db.models.query import Q +from django.db.models.query import Q, ITER_CHUNK_SIZE # Python 2.3 doesn't have sorted() try: sorted except NameError: from django.utils.itercompat import sorted - + class Tag(models.Model): name = models.CharField(max_length=10) parent = models.ForeignKey('self', blank=True, null=True, @@ -483,23 +484,6 @@ Bug #2076 >>> Cover.objects.all() [<Cover: first>, <Cover: second>] -# If you're not careful, it's possible to introduce infinite loops via default -# ordering on foreign keys in a cycle. We detect that. ->>> LoopX.objects.all() -Traceback (most recent call last): -... -FieldError: Infinite loop caused by ordering. - ->>> LoopZ.objects.all() -Traceback (most recent call last): -... -FieldError: Infinite loop caused by ordering. - -# ... but you can still order in a non-recursive fashion amongst linked fields -# (the previous test failed because the default ordering was recursive). ->>> LoopX.objects.all().order_by('y__x__y__x__id') -[] - # If the remote model does not have a default ordering, we order by its 'id' # field. >>> Item.objects.order_by('creator', 'name') @@ -820,5 +804,47 @@ Bug #7698 -- People like to slice with '0' as the high-water mark. >>> Item.objects.all()[0:0] [] +Bug #7411 - saving to db must work even with partially read result set in +another cursor. + +>>> for num in range(2 * ITER_CHUNK_SIZE + 1): +... _ = Number.objects.create(num=num) + +>>> for i, obj in enumerate(Number.objects.all()): +... obj.save() +... if i > 10: break + +Bug #7759 -- count should work with a partially read result set. +>>> count = Number.objects.count() +>>> qs = Number.objects.all() +>>> for obj in qs: +... qs.count() == count +... break +True + """} +# In Python 2.3, exceptions raised in __len__ are swallowed (Python issue +# 1242657), so these cases return an empty list, rather than raising an +# exception. Not a lot we can do about that, unfortunately, due to the way +# Python handles list() calls internally. Thus, we skip the tests for Python +# 2.3. +if sys.version_info >= (2, 4): + __test__["API_TESTS"] += """ +# If you're not careful, it's possible to introduce infinite loops via default +# ordering on foreign keys in a cycle. We detect that. +>>> LoopX.objects.all() +Traceback (most recent call last): +... +FieldError: Infinite loop caused by ordering. + +>>> LoopZ.objects.all() +Traceback (most recent call last): +... +FieldError: Infinite loop caused by ordering. + +# ... but you can still order in a non-recursive fashion amongst linked fields +# (the previous test failed because the default ordering was recursive). +>>> LoopX.objects.all().order_by('y__x__y__x__id') +[] +""" diff --git a/tests/regressiontests/views/fixtures/testdata.json b/tests/regressiontests/views/fixtures/testdata.json index 449e91913a..e3fed9eac7 100644 --- a/tests/regressiontests/views/fixtures/testdata.json +++ b/tests/regressiontests/views/fixtures/testdata.json @@ -1,5 +1,23 @@ [ { + "pk": "1", + "model": "auth.user", + "fields": { + "username": "testclient", + "first_name": "Test", + "last_name": "Client", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": "2006-12-17 07:03:31", + "groups": [], + "user_permissions": [], + "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161", + "email": "testclient@example.com", + "date_joined": "2006-12-17 07:03:31" + } + }, + { "pk": 1, "model": "views.article", "fields": { @@ -29,7 +47,16 @@ "date_created": "3000-01-01 21:22:23" } }, - + { + "pk": 1, + "model": "views.urlarticle", + "fields": { + "author": 1, + "title": "Old Article", + "slug": "old_article", + "date_created": "2001-01-01 21:22:23" + } + }, { "pk": 1, "model": "views.author", diff --git a/tests/regressiontests/views/models.py b/tests/regressiontests/views/models.py index 4bed1f3bde..ce31778177 100644 --- a/tests/regressiontests/views/models.py +++ b/tests/regressiontests/views/models.py @@ -1,9 +1,8 @@ """ -Regression tests for Django built-in views +Regression tests for Django built-in views. """ from django.db import models -from django.conf import settings class Author(models.Model): name = models.CharField(max_length=100) @@ -14,13 +13,28 @@ class Author(models.Model): def get_absolute_url(self): return '/views/authors/%s/' % self.id - -class Article(models.Model): +class BaseArticle(models.Model): + """ + An abstract article Model so that we can create article models with and + without a get_absolute_url method (for create_update generic views tests). + """ title = models.CharField(max_length=100) slug = models.SlugField() author = models.ForeignKey(Author) date_created = models.DateTimeField() - + + class Meta: + abstract = True + def __unicode__(self): return self.title +class Article(BaseArticle): + pass + +class UrlArticle(BaseArticle): + """ + An Article class with a get_absolute_url defined. + """ + def get_absolute_url(self): + return '/urlarticles/%s/' % self.slug diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py index 2c8c5b4a92..9964cd5833 100644 --- a/tests/regressiontests/views/tests/__init__.py +++ b/tests/regressiontests/views/tests/__init__.py @@ -1,4 +1,5 @@ from defaults import * from i18n import * from static import * -from generic.date_based import *
\ No newline at end of file +from generic.date_based import * +from generic.create_update import * diff --git a/tests/regressiontests/views/tests/generic/create_update.py b/tests/regressiontests/views/tests/generic/create_update.py new file mode 100644 index 0000000000..3975c65706 --- /dev/null +++ b/tests/regressiontests/views/tests/generic/create_update.py @@ -0,0 +1,211 @@ +import datetime + +from django.test import TestCase +from django.core.exceptions import ImproperlyConfigured +from regressiontests.views.models import Article, UrlArticle + +class CreateObjectTest(TestCase): + + fixtures = ['testdata.json'] + + def test_login_required_view(self): + """ + Verifies that an unauthenticated user attempting to access a + login_required view gets redirected to the login page and that + an authenticated user is let through. + """ + view_url = '/views/create_update/member/create/article/' + response = self.client.get(view_url) + self.assertRedirects(response, '/accounts/login/?next=%s' % view_url) + # Now login and try again. + login = self.client.login(username='testclient', password='password') + self.failUnless(login, 'Could not log in') + response = self.client.get(view_url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'views/article_form.html') + + def test_create_article_display_page(self): + """ + Ensures the generic view returned the page and contains a form. + """ + view_url = '/views/create_update/create/article/' + response = self.client.get(view_url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'views/article_form.html') + if not response.context.get('form'): + self.fail('No form found in the response.') + + def test_create_article_with_errors(self): + """ + POSTs a form that contains validation errors. + """ + view_url = '/views/create_update/create/article/' + num_articles = Article.objects.count() + response = self.client.post(view_url, { + 'title': 'My First Article', + }) + self.assertFormError(response, 'form', 'slug', [u'This field is required.']) + self.assertTemplateUsed(response, 'views/article_form.html') + self.assertEqual(num_articles, Article.objects.count(), + "Number of Articles should not have changed.") + + def test_create_custom_save_article(self): + """ + Creates a new article using a custom form class with a save method + that alters the slug entered. + """ + view_url = '/views/create_update/create_custom/article/' + response = self.client.post(view_url, { + 'title': 'Test Article', + 'slug': 'this-should-get-replaced', + 'author': 1, + 'date_created': datetime.datetime(2007, 6, 25), + }) + self.assertRedirects(response, + '/views/create_update/view/article/some-other-slug/', + target_status_code=404) + +class UpdateDeleteObjectTest(TestCase): + + fixtures = ['testdata.json'] + + def test_update_object_form_display(self): + """ + Verifies that the form was created properly and with initial values. + """ + response = self.client.get('/views/create_update/update/article/old_article/') + self.assertTemplateUsed(response, 'views/article_form.html') + self.assertEquals(unicode(response.context['form']['title']), + u'<input id="id_title" type="text" name="title" value="Old Article" maxlength="100" />') + + def test_update_object(self): + """ + Verifies the updating of an Article. + """ + response = self.client.post('/views/create_update/update/article/old_article/', { + 'title': 'Another Article', + 'slug': 'another-article-slug', + 'author': 1, + 'date_created': datetime.datetime(2007, 6, 25), + }) + article = Article.objects.get(pk=1) + self.assertEquals(article.title, "Another Article") + + def test_delete_object_confirm(self): + """ + Verifies the confirm deletion page is displayed using a GET. + """ + response = self.client.get('/views/create_update/delete/article/old_article/') + self.assertTemplateUsed(response, 'views/article_confirm_delete.html') + + def test_delete_object(self): + """ + Verifies the object actually gets deleted on a POST. + """ + view_url = '/views/create_update/delete/article/old_article/' + response = self.client.post(view_url) + try: + Article.objects.get(slug='old_article') + except Article.DoesNotExist: + pass + else: + self.fail('Object was not deleted.') + +class PostSaveRedirectTests(TestCase): + """ + Verifies that the views redirect to the correct locations depending on + if a post_save_redirect was passed and a get_absolute_url method exists + on the Model. + """ + + fixtures = ['testdata.json'] + article_model = Article + + create_url = '/views/create_update/create/article/' + update_url = '/views/create_update/update/article/old_article/' + delete_url = '/views/create_update/delete/article/old_article/' + + create_redirect = '/views/create_update/view/article/my-first-article/' + update_redirect = '/views/create_update/view/article/another-article-slug/' + delete_redirect = '/views/create_update/' + + def test_create_article(self): + num_articles = self.article_model.objects.count() + response = self.client.post(self.create_url, { + 'title': 'My First Article', + 'slug': 'my-first-article', + 'author': '1', + 'date_created': datetime.datetime(2007, 6, 25), + }) + self.assertRedirects(response, self.create_redirect, + target_status_code=404) + self.assertEqual(num_articles + 1, self.article_model.objects.count(), + "A new Article should have been created.") + + def test_update_article(self): + num_articles = self.article_model.objects.count() + response = self.client.post(self.update_url, { + 'title': 'Another Article', + 'slug': 'another-article-slug', + 'author': 1, + 'date_created': datetime.datetime(2007, 6, 25), + }) + self.assertRedirects(response, self.update_redirect, + target_status_code=404) + self.assertEqual(num_articles, self.article_model.objects.count(), + "A new Article should not have been created.") + + def test_delete_article(self): + num_articles = self.article_model.objects.count() + response = self.client.post(self.delete_url) + self.assertRedirects(response, self.delete_redirect, + target_status_code=404) + self.assertEqual(num_articles - 1, self.article_model.objects.count(), + "An Article should have been deleted.") + +class NoPostSaveNoAbsoluteUrl(PostSaveRedirectTests): + """ + Tests that when no post_save_redirect is passed and no get_absolute_url + method exists on the Model that the view raises an ImproperlyConfigured + error. + """ + + create_url = '/views/create_update/no_redirect/create/article/' + update_url = '/views/create_update/no_redirect/update/article/old_article/' + + def test_create_article(self): + self.assertRaises(ImproperlyConfigured, + super(NoPostSaveNoAbsoluteUrl, self).test_create_article) + + def test_update_article(self): + self.assertRaises(ImproperlyConfigured, + super(NoPostSaveNoAbsoluteUrl, self).test_update_article) + + def test_delete_article(self): + """ + The delete_object view requires a post_delete_redirect, so skip testing + here. + """ + pass + +class AbsoluteUrlNoPostSave(PostSaveRedirectTests): + """ + Tests that the views redirect to the Model's get_absolute_url when no + post_save_redirect is passed. + """ + + # Article model with get_absolute_url method. + article_model = UrlArticle + + create_url = '/views/create_update/no_url/create/article/' + update_url = '/views/create_update/no_url/update/article/old_article/' + + create_redirect = '/urlarticles/my-first-article/' + update_redirect = '/urlarticles/another-article-slug/' + + def test_delete_article(self): + """ + The delete_object view requires a post_delete_redirect, so skip testing + here. + """ + pass diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py index 5ef0c5129d..3a4700b998 100644 --- a/tests/regressiontests/views/urls.py +++ b/tests/regressiontests/views/urls.py @@ -5,6 +5,7 @@ from django.conf.urls.defaults import * from models import * import views + base_dir = path.dirname(path.abspath(__file__)) media_dir = path.join(base_dir, 'media') locale_dir = path.join(base_dir, 'locale') @@ -14,35 +15,66 @@ js_info_dict = { 'packages': ('regressiontests.views',), } -date_based_info_dict = { - 'queryset': Article.objects.all(), - 'date_field': 'date_created', - 'month_format': '%m', -} +date_based_info_dict = { + 'queryset': Article.objects.all(), + 'date_field': 'date_created', + 'month_format': '%m', +} urlpatterns = patterns('', (r'^$', views.index_page), - + # Default views (r'^shortcut/(\d+)/(.*)/$', 'django.views.defaults.shortcut'), (r'^non_existing_url/', 'django.views.defaults.page_not_found'), (r'^server_error/', 'django.views.defaults.server_error'), - + # i18n views - (r'^i18n/', include('django.conf.urls.i18n')), + (r'^i18n/', include('django.conf.urls.i18n')), (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict), - + # Static views (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}), - - # Date-based generic views - (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$', - 'django.views.generic.date_based.object_detail', - dict(slug_field='slug', **date_based_info_dict)), - (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/allow_future/$', - 'django.views.generic.date_based.object_detail', - dict(allow_future=True, slug_field='slug', **date_based_info_dict)), - (r'^date_based/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$', - 'django.views.generic.date_based.archive_month', - date_based_info_dict), +) + +# Date-based generic views. +urlpatterns += patterns('django.views.generic.date_based', + (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$', + 'object_detail', + dict(slug_field='slug', **date_based_info_dict)), + (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/allow_future/$', + 'object_detail', + dict(allow_future=True, slug_field='slug', **date_based_info_dict)), + (r'^date_based/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$', + 'archive_month', + date_based_info_dict), +) + +# crud generic views. + +urlpatterns += patterns('django.views.generic.create_update', + (r'^create_update/member/create/article/$', 'create_object', + dict(login_required=True, model=Article)), + (r'^create_update/create/article/$', 'create_object', + dict(post_save_redirect='/views/create_update/view/article/%(slug)s/', + model=Article)), + (r'^create_update/update/article/(?P<slug>[-\w]+)/$', 'update_object', + dict(post_save_redirect='/views/create_update/view/article/%(slug)s/', + slug_field='slug', model=Article)), + (r'^create_update/create_custom/article/$', views.custom_create), + (r'^create_update/delete/article/(?P<slug>[-\w]+)/$', 'delete_object', + dict(post_delete_redirect='/views/create_update/', slug_field='slug', + model=Article)), + + # No post_save_redirect and no get_absolute_url on model. + (r'^create_update/no_redirect/create/article/$', 'create_object', + dict(model=Article)), + (r'^create_update/no_redirect/update/article/(?P<slug>[-\w]+)/$', + 'update_object', dict(slug_field='slug', model=Article)), + + # get_absolute_url on model, but no passed post_save_redirect. + (r'^create_update/no_url/create/article/$', 'create_object', + dict(model=UrlArticle)), + (r'^create_update/no_url/update/article/(?P<slug>[-\w]+)/$', + 'update_object', dict(slug_field='slug', model=UrlArticle)), ) diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py index 956432e7d5..b90852189c 100644 --- a/tests/regressiontests/views/views.py +++ b/tests/regressiontests/views/views.py @@ -1,5 +1,29 @@ from django.http import HttpResponse +from django import forms +from django.views.generic.create_update import create_object + +from models import Article + def index_page(request): """Dummy index page""" return HttpResponse('<html><body>Dummy page</body></html>') + + +def custom_create(request): + """ + Calls create_object generic view with a custom form class. + """ + class SlugChangingArticleForm(forms.ModelForm): + """Custom form class to overwrite the slug.""" + + class Meta: + model = Article + + def save(self, *args, **kwargs): + self.cleaned_data['slug'] = 'some-other-slug' + return super(SlugChangingArticleForm, self).save(*args, **kwargs) + + return create_object(request, + post_save_redirect='/views/create_update/view/article/%(slug)s/', + form_class=SlugChangingArticleForm) 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/templates/views/article_confirm_delete.html b/tests/templates/views/article_confirm_delete.html new file mode 100644 index 0000000000..3f8ff55da6 --- /dev/null +++ b/tests/templates/views/article_confirm_delete.html @@ -0,0 +1 @@ +This template intentionally left blank
\ No newline at end of file diff --git a/tests/templates/views/article_detail.html b/tests/templates/views/article_detail.html index 3f8ff55da6..952299db91 100644 --- a/tests/templates/views/article_detail.html +++ b/tests/templates/views/article_detail.html @@ -1 +1 @@ -This template intentionally left blank
\ No newline at end of file +Article detail template. diff --git a/tests/templates/views/article_form.html b/tests/templates/views/article_form.html new file mode 100644 index 0000000000..e2aa1f9535 --- /dev/null +++ b/tests/templates/views/article_form.html @@ -0,0 +1,3 @@ +Article form template. + +{{ form.errors }} diff --git a/tests/templates/views/urlarticle_detail.html b/tests/templates/views/urlarticle_detail.html new file mode 100644 index 0000000000..924f310300 --- /dev/null +++ b/tests/templates/views/urlarticle_detail.html @@ -0,0 +1 @@ +UrlArticle detail template. diff --git a/tests/templates/views/urlarticle_form.html b/tests/templates/views/urlarticle_form.html new file mode 100644 index 0000000000..578dd98ca6 --- /dev/null +++ b/tests/templates/views/urlarticle_form.html @@ -0,0 +1,3 @@ +UrlArticle form template. + +{{ form.errors }} 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')), |
