Fixed #3639: updated generic create_update views to use newforms. This is a backwards-incompatible change.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@7952 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2008-07-18 19:45:00 +00:00
parent cd80ce7a3d
commit 7997133a3d
14 changed files with 555 additions and 187 deletions

View File

@ -0,0 +1,3 @@
class GenericViewError(Exception):
"""A problem in a generic view."""
pass

View File

@ -1,154 +1,195 @@
from django.core.xheaders import populate_xheaders from django.newforms.models import ModelFormMetaclass, ModelForm
from django.template import loader from django.template import RequestContext, loader
from django import oldforms
from django.db.models import FileField
from django.contrib.auth.views import redirect_to_login
from django.template import RequestContext
from django.http import Http404, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.core.xheaders import populate_xheaders
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.contrib.auth.views import redirect_to_login
from django.views.generic import GenericViewError
def create_object(request, model, template_name=None, def deprecate_follow(follow):
template_loader=loader, extra_context=None, post_save_redirect=None,
login_required=False, follow=None, context_processors=None):
""" """
Generic object-creation function. Issues a DeprecationWarning if follow is anything but None.
Templates: ``<app_label>/<model_name>_form.html`` The old Manipulator-based forms used a follow argument that is no longer
Context: needed for newforms-based forms.
form
the form wrapper for the object
""" """
if extra_context is None: extra_context = {} if follow is not None:
if login_required and not request.user.is_authenticated(): import warning
return redirect_to_login(request.path) msg = ("Generic views have been changed to use newforms, and the"
"'follow' argument is no longer used. Please update your code"
"to not use the 'follow' argument.")
warning.warn(msg, DeprecationWarning, stacklevel=3)
manipulator = model.AddManipulator(follow=follow) def apply_extra_context(extra_context, context):
if request.POST: """
# If data was POSTed, we're trying to create a new object Adds items from extra_context dict to context. If a value in extra_context
new_data = request.POST.copy() is callable, then it is called and the result is added to context.
"""
if model._meta.has_field_type(FileField): for key, value in extra_context.iteritems():
new_data.update(request.FILES)
# Check for errors
errors = manipulator.get_validation_errors(new_data)
manipulator.do_html2python(new_data)
if not errors:
# No errors -- this means we can save the data!
new_object = manipulator.save(new_data)
if request.user.is_authenticated():
request.user.message_set.create(message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name})
# Redirect to the new object: first by trying post_save_redirect,
# then by obj.get_absolute_url; fail if neither works.
if post_save_redirect:
return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
elif hasattr(new_object, 'get_absolute_url'):
return HttpResponseRedirect(new_object.get_absolute_url())
else:
raise ImproperlyConfigured("No URL to redirect to from generic create view.")
else:
# No POST, so we want a brand new form without any data or errors
errors = {}
new_data = manipulator.flatten_data()
# Create the FormWrapper, template, context, response
form = oldforms.FormWrapper(manipulator, new_data, errors)
if not template_name:
template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
t = template_loader.get_template(template_name)
c = RequestContext(request, {
'form': form,
}, context_processors)
for key, value in extra_context.items():
if callable(value): if callable(value):
c[key] = value() context[key] = value()
else: else:
c[key] = value context[key] = value
return HttpResponse(t.render(c))
def update_object(request, model, object_id=None, slug=None, def get_model_and_form_class(model, form_class):
slug_field='slug', template_name=None, template_loader=loader,
extra_context=None, post_save_redirect=None,
login_required=False, follow=None, context_processors=None,
template_object_name='object'):
""" """
Generic object-update function. Returns a model and form class based on the model and form_class
parameters that were passed to the generic view.
Templates: ``<app_label>/<model_name>_form.html`` If ``form_class`` is given then its associated model will be returned along
Context: with ``form_class`` itself. Otherwise, if ``model`` is given, ``model``
form itself will be returned along with a ``ModelForm`` class created from
the form wrapper for the object ``model``.
object
the original object being edited
""" """
if extra_context is None: extra_context = {} if form_class:
if login_required and not request.user.is_authenticated(): return form_class._meta.model, form_class
return redirect_to_login(request.path) if model:
# The inner Meta class fails if model = model is used for some reason.
tmp_model = model
# TODO: we should be able to construct a ModelForm without creating
# and passing in a temporary inner class.
class Meta:
model = tmp_model
class_name = model.__name__ + 'Form'
form_class = ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
return model, form_class
raise GenericViewError("Generic view must be called with either a model or"
" form_class argument.")
# Look up the object to be edited def redirect(post_save_redirect, obj):
"""
Returns a HttpResponseRedirect to ``post_save_redirect``.
``post_save_redirect`` should be a string, and can contain named string-
substitution place holders of ``obj`` field names.
If ``post_save_redirect`` is None, then redirect to ``obj``'s URL returned
by ``get_absolute_url()``. If ``obj`` has no ``get_absolute_url`` method,
then raise ImproperlyConfigured.
This method is meant to handle the post_save_redirect parameter to the
``create_object`` and ``update_object`` views.
"""
if post_save_redirect:
return HttpResponseRedirect(post_save_redirect % obj.__dict__)
elif hasattr(obj, 'get_absolute_url'):
return HttpResponseRedirect(obj.get_absolute_url())
else:
raise ImproperlyConfigured(
"No URL to redirect to. Either pass a post_save_redirect"
" parameter to the generic view or define a get_absolute_url"
" method on the Model.")
def lookup_object(model, object_id, slug, slug_field):
"""
Return the ``model`` object with the passed ``object_id``. If
``object_id`` is None, then return the the object whose ``slug_field``
equals the passed ``slug``. If ``slug`` and ``slug_field`` are not passed,
then raise Http404 exception.
"""
lookup_kwargs = {} lookup_kwargs = {}
if object_id: if object_id:
lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id
elif slug and slug_field: elif slug and slug_field:
lookup_kwargs['%s__exact' % slug_field] = slug lookup_kwargs['%s__exact' % slug_field] = slug
else: else:
raise AttributeError("Generic edit view must be called with either an object_id or a slug/slug_field") raise GenericViewError(
"Generic view must be called with either an object_id or a"
" slug/slug_field.")
try: try:
object = model.objects.get(**lookup_kwargs) return model.objects.get(**lookup_kwargs)
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs) raise Http404("No %s found for %s"
% (model._meta.verbose_name, lookup_kwargs))
manipulator = model.ChangeManipulator(getattr(object, object._meta.pk.attname), follow=follow) def create_object(request, model=None, template_name=None,
template_loader=loader, extra_context=None, post_save_redirect=None,
login_required=False, follow=None, context_processors=None,
form_class=None):
"""
Generic object-creation function.
if request.POST: Templates: ``<app_label>/<model_name>_form.html``
new_data = request.POST.copy() Context:
if model._meta.has_field_type(FileField): form
new_data.update(request.FILES) the form for the object
errors = manipulator.get_validation_errors(new_data) """
manipulator.do_html2python(new_data) deprecate_follow(follow)
if not errors: if extra_context is None: extra_context = {}
object = manipulator.save(new_data) if login_required and not request.user.is_authenticated():
return redirect_to_login(request.path)
model, form_class = get_model_and_form_class(model, form_class)
if request.method == 'POST':
form = form_class(request.POST, request.FILES)
if form.is_valid():
new_object = form.save()
if request.user.is_authenticated(): if request.user.is_authenticated():
request.user.message_set.create(message=ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": model._meta.verbose_name}) request.user.message_set.create(message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name})
return redirect(post_save_redirect, new_object)
# Do a post-after-redirect so that reload works, etc.
if post_save_redirect:
return HttpResponseRedirect(post_save_redirect % object.__dict__)
elif hasattr(object, 'get_absolute_url'):
return HttpResponseRedirect(object.get_absolute_url())
else:
raise ImproperlyConfigured("No URL to redirect to from generic create view.")
else: else:
errors = {} form = form_class()
# This makes sure the form acurate represents the fields of the place.
new_data = manipulator.flatten_data()
form = oldforms.FormWrapper(manipulator, new_data, errors) # Create the template, context, response
if not template_name: if not template_name:
template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
t = template_loader.get_template(template_name) t = template_loader.get_template(template_name)
c = RequestContext(request, { c = RequestContext(request, {
'form': form, 'form': form,
template_object_name: object,
}, context_processors) }, context_processors)
for key, value in extra_context.items(): apply_extra_context(extra_context, c)
if callable(value): return HttpResponse(t.render(c))
c[key] = value()
else: def update_object(request, model=None, object_id=None, slug=None,
c[key] = value slug_field='slug', template_name=None, template_loader=loader,
extra_context=None, post_save_redirect=None,
login_required=False, follow=None, context_processors=None,
template_object_name='object', form_class=None):
"""
Generic object-update function.
Templates: ``<app_label>/<model_name>_form.html``
Context:
form
the form for the object
object
the original object being edited
"""
deprecate_follow(follow)
if extra_context is None: extra_context = {}
if login_required and not request.user.is_authenticated():
return redirect_to_login(request.path)
model, form_class = get_model_and_form_class(model, form_class)
obj = lookup_object(model, object_id, slug, slug_field)
if request.method == 'POST':
form = form_class(request.POST, request.FILES, instance=obj)
if form.is_valid():
obj = form.save()
if request.user.is_authenticated():
request.user.message_set.create(message=ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": model._meta.verbose_name})
return redirect(post_save_redirect, obj)
else:
form = form_class(instance=obj)
if not template_name:
template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
t = template_loader.get_template(template_name)
c = RequestContext(request, {
'form': form,
template_object_name: obj,
}, context_processors)
apply_extra_context(extra_context, c)
response = HttpResponse(t.render(c)) response = HttpResponse(t.render(c))
populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname)) populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
return response return response
def delete_object(request, model, post_delete_redirect, def delete_object(request, model, post_delete_redirect, object_id=None,
object_id=None, slug=None, slug_field='slug', template_name=None, slug=None, slug_field='slug', template_name=None,
template_loader=loader, extra_context=None, template_loader=loader, extra_context=None, login_required=False,
login_required=False, context_processors=None, template_object_name='object'): context_processors=None, template_object_name='object'):
""" """
Generic object-delete function. Generic object-delete function.
@ -165,21 +206,10 @@ def delete_object(request, model, post_delete_redirect,
if login_required and not request.user.is_authenticated(): if login_required and not request.user.is_authenticated():
return redirect_to_login(request.path) return redirect_to_login(request.path)
# Look up the object to be edited obj = lookup_object(model, object_id, slug, slug_field)
lookup_kwargs = {}
if object_id:
lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id
elif slug and slug_field:
lookup_kwargs['%s__exact' % slug_field] = slug
else:
raise AttributeError("Generic delete view must be called with either an object_id or a slug/slug_field")
try:
object = model._default_manager.get(**lookup_kwargs)
except ObjectDoesNotExist:
raise Http404, "No %s found for %s" % (model._meta.app_label, lookup_kwargs)
if request.method == 'POST': if request.method == 'POST':
object.delete() obj.delete()
if request.user.is_authenticated(): if request.user.is_authenticated():
request.user.message_set.create(message=ugettext("The %(verbose_name)s was deleted.") % {"verbose_name": model._meta.verbose_name}) request.user.message_set.create(message=ugettext("The %(verbose_name)s was deleted.") % {"verbose_name": model._meta.verbose_name})
return HttpResponseRedirect(post_delete_redirect) return HttpResponseRedirect(post_delete_redirect)
@ -188,13 +218,9 @@ def delete_object(request, model, post_delete_redirect,
template_name = "%s/%s_confirm_delete.html" % (model._meta.app_label, model._meta.object_name.lower()) template_name = "%s/%s_confirm_delete.html" % (model._meta.app_label, model._meta.object_name.lower())
t = template_loader.get_template(template_name) t = template_loader.get_template(template_name)
c = RequestContext(request, { c = RequestContext(request, {
template_object_name: object, template_object_name: obj,
}, context_processors) }, context_processors)
for key, value in extra_context.items(): apply_extra_context(extra_context, c)
if callable(value):
c[key] = value()
else:
c[key] = value
response = HttpResponse(t.render(c)) response = HttpResponse(t.render(c))
populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname)) populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
return response return response

View File

@ -701,7 +701,7 @@ A page representing a list of objects.
query string parameter (via ``GET``) or a ``page`` variable specified in query string parameter (via ``GET``) or a ``page`` variable specified in
the URLconf. See `Notes on pagination`_ below. the URLconf. See `Notes on pagination`_ below.
* ``page``: The current page number, as an integer. This is 1-based. * ``page``: The current page number, as an integer. This is 1-based.
See `Notes on pagination`_ below. See `Notes on pagination`_ below.
* ``template_name``: The full name of a template to use in rendering the * ``template_name``: The full name of a template to use in rendering the
@ -809,25 +809,25 @@ specify the page number in the URL in one of two ways:
/objects/?page=3 /objects/?page=3
* To loop over all the available page numbers, use the ``page_range`` * To loop over all the available page numbers, use the ``page_range``
variable. You can iterate over the list provided by ``page_range`` variable. You can iterate over the list provided by ``page_range``
to create a link to every page of results. to create a link to every page of results.
These values and lists are 1-based, not 0-based, so the first page would be These values and lists are 1-based, not 0-based, so the first page would be
represented as page ``1``. represented as page ``1``.
For more on pagination, read the `pagination documentation`_. For more on pagination, read the `pagination documentation`_.
.. _`pagination documentation`: ../pagination/ .. _`pagination documentation`: ../pagination/
**New in Django development version:** **New in Django development version:**
As a special case, you are also permitted to use ``last`` as a value for As a special case, you are also permitted to use ``last`` as a value for
``page``:: ``page``::
/objects/?page=last /objects/?page=last
This allows you to access the final page of results without first having to This allows you to access the final page of results without first having to
determine how many pages there are. determine how many pages there are.
Note that ``page`` *must* be either a valid page number or the value ``last``; Note that ``page`` *must* be either a valid page number or the value ``last``;
@ -906,19 +906,33 @@ Create/update/delete generic views
The ``django.views.generic.create_update`` module contains a set of functions The ``django.views.generic.create_update`` module contains a set of functions
for creating, editing and deleting objects. for creating, editing and deleting objects.
**Changed in Django development version:**
``django.views.generic.create_update.create_object`` and
``django.views.generic.create_update.update_object`` now use `newforms`_ to
build and display the form.
.. _newforms: ../newforms/
``django.views.generic.create_update.create_object`` ``django.views.generic.create_update.create_object``
---------------------------------------------------- ----------------------------------------------------
**Description:** **Description:**
A page that displays a form for creating an object, redisplaying the form with A page that displays a form for creating an object, redisplaying the form with
validation errors (if there are any) and saving the object. This uses the validation errors (if there are any) and saving the object.
automatic manipulators that come with Django models.
**Required arguments:** **Required arguments:**
* ``model``: The Django model class of the object that the form will * Either ``form_class`` or ``model`` is required.
create.
If you provide ``form_class``, it should be a
``django.newforms.ModelForm`` subclass. Use this argument when you need
to customize the model's form. See the `ModelForm docs`_ for more
information.
Otherwise, ``model`` should be a Django model class and the form used
will be a standard ``ModelForm`` for ``model``.
**Optional arguments:** **Optional arguments:**
@ -959,22 +973,23 @@ If ``template_name`` isn't specified, this view will use the template
In addition to ``extra_context``, the template's context will be: In addition to ``extra_context``, the template's context will be:
* ``form``: A ``django.oldforms.FormWrapper`` instance representing the form * ``form``: A ``django.newforms.ModelForm`` instance representing the form
for editing the object. This lets you refer to form fields easily in the for creating the object. This lets you refer to form fields easily in the
template system. template system.
For example, if ``model`` has two fields, ``name`` and ``address``:: For example, if the model has two fields, ``name`` and ``address``::
<form action="" method="post"> <form action="" method="post">
<p><label for="id_name">Name:</label> {{ form.name }}</p> <p>{{ form.name.label_tag }} {{ form.name }}</p>
<p><label for="id_address">Address:</label> {{ form.address }}</p> <p>{{ form.address.label_tag }} {{ form.address }}</p>
</form> </form>
See the `manipulator and formfield documentation`_ for more information See the `newforms documentation`_ for more information about using
about using ``FormWrapper`` objects in templates. ``Form`` objects in templates.
.. _authentication system: ../authentication/ .. _authentication system: ../authentication/
.. _manipulator and formfield documentation: ../forms/ .. _ModelForm docs: ../newforms/modelforms
.. _newforms documentation: ../newforms/
``django.views.generic.create_update.update_object`` ``django.views.generic.create_update.update_object``
---------------------------------------------------- ----------------------------------------------------
@ -987,8 +1002,15 @@ object. This uses the automatic manipulators that come with Django models.
**Required arguments:** **Required arguments:**
* ``model``: The Django model class of the object that the form will * Either ``form_class`` or ``model`` is required.
create.
If you provide ``form_class``, it should be a
``django.newforms.ModelForm`` subclass. Use this argument when you need
to customize the model's form. See the `ModelForm docs`_ for more
information.
Otherwise, ``model`` should be a Django model class and the form used
will be a standard ``ModelForm`` for ``model``.
* Either ``object_id`` or (``slug`` *and* ``slug_field``) is required. * Either ``object_id`` or (``slug`` *and* ``slug_field``) is required.
@ -1041,19 +1063,19 @@ If ``template_name`` isn't specified, this view will use the template
In addition to ``extra_context``, the template's context will be: In addition to ``extra_context``, the template's context will be:
* ``form``: A ``django.oldforms.FormWrapper`` instance representing the form * ``form``: A ``django.newforms.ModelForm`` instance representing the form
for editing the object. This lets you refer to form fields easily in the for editing the object. This lets you refer to form fields easily in the
template system. template system.
For example, if ``model`` has two fields, ``name`` and ``address``:: For example, if the model has two fields, ``name`` and ``address``::
<form action="" method="post"> <form action="" method="post">
<p><label for="id_name">Name:</label> {{ form.name }}</p> <p>{{ form.name.label_tag }} {{ form.name }}</p>
<p><label for="id_address">Address:</label> {{ form.address }}</p> <p>{{ form.address.label_tag }} {{ form.address }}</p>
</form> </form>
See the `manipulator and formfield documentation`_ for more information See the `newforms documentation`_ for more information about using
about using ``FormWrapper`` objects in templates. ``Form`` objects in templates.
* ``object``: The original object being edited. This variable's name * ``object``: The original object being edited. This variable's name
depends on the ``template_object_name`` parameter, which is ``'object'`` depends on the ``template_object_name`` parameter, which is ``'object'``

View File

@ -1,4 +1,22 @@
[ [
{
"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, "pk": 1,
"model": "views.article", "model": "views.article",
@ -29,7 +47,16 @@
"date_created": "3000-01-01 21:22:23" "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, "pk": 1,
"model": "views.author", "model": "views.author",

View File

@ -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.db import models
from django.conf import settings
class Author(models.Model): class Author(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
@ -14,13 +13,28 @@ class Author(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return '/views/authors/%s/' % self.id return '/views/authors/%s/' % self.id
class BaseArticle(models.Model):
class Article(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) title = models.CharField(max_length=100)
slug = models.SlugField() slug = models.SlugField()
author = models.ForeignKey(Author) author = models.ForeignKey(Author)
date_created = models.DateTimeField() date_created = models.DateTimeField()
class Meta:
abstract = True
def __unicode__(self): def __unicode__(self):
return self.title 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

View File

@ -1,4 +1,5 @@
from defaults import * from defaults import *
from i18n import * from i18n import *
from static import * from static import *
from generic.date_based import * from generic.date_based import *
from generic.create_update import *

View File

@ -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

View File

@ -5,6 +5,7 @@ from django.conf.urls.defaults import *
from models import * from models import *
import views import views
base_dir = path.dirname(path.abspath(__file__)) base_dir = path.dirname(path.abspath(__file__))
media_dir = path.join(base_dir, 'media') media_dir = path.join(base_dir, 'media')
locale_dir = path.join(base_dir, 'locale') locale_dir = path.join(base_dir, 'locale')
@ -14,35 +15,66 @@ js_info_dict = {
'packages': ('regressiontests.views',), 'packages': ('regressiontests.views',),
} }
date_based_info_dict = { date_based_info_dict = {
'queryset': Article.objects.all(), 'queryset': Article.objects.all(),
'date_field': 'date_created', 'date_field': 'date_created',
'month_format': '%m', 'month_format': '%m',
} }
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^$', views.index_page), (r'^$', views.index_page),
# Default views # Default views
(r'^shortcut/(\d+)/(.*)/$', 'django.views.defaults.shortcut'), (r'^shortcut/(\d+)/(.*)/$', 'django.views.defaults.shortcut'),
(r'^non_existing_url/', 'django.views.defaults.page_not_found'), (r'^non_existing_url/', 'django.views.defaults.page_not_found'),
(r'^server_error/', 'django.views.defaults.server_error'), (r'^server_error/', 'django.views.defaults.server_error'),
# i18n views # 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), (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
# Static views # Static views
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}), (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]+)/$', # Date-based generic views.
'django.views.generic.date_based.object_detail', urlpatterns += patterns('django.views.generic.date_based',
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]+)/$',
(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',
'django.views.generic.date_based.object_detail', dict(slug_field='slug', **date_based_info_dict)),
dict(allow_future=True, 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/$',
(r'^date_based/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$', 'object_detail',
'django.views.generic.date_based.archive_month', dict(allow_future=True, slug_field='slug', **date_based_info_dict)),
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)),
) )

View File

@ -1,5 +1,29 @@
from django.http import HttpResponse from django.http import HttpResponse
import django.newforms as forms
from django.views.generic.create_update import create_object
from models import Article
def index_page(request): def index_page(request):
"""Dummy index page""" """Dummy index page"""
return HttpResponse('<html><body>Dummy page</body></html>') 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)

View File

@ -0,0 +1 @@
This template intentionally left blank

View File

@ -1 +1 @@
This template intentionally left blank Article detail template.

View File

@ -0,0 +1,3 @@
Article form template.
{{ form.errors }}

View File

@ -0,0 +1 @@
UrlArticle detail template.

View File

@ -0,0 +1,3 @@
UrlArticle form template.
{{ form.errors }}