diff --git a/django/views/generic/__init__.py b/django/views/generic/__init__.py index e69de29bb2..95c5fa9c50 100644 --- a/django/views/generic/__init__.py +++ b/django/views/generic/__init__.py @@ -0,0 +1,3 @@ +class GenericViewError(Exception): + """A problem in a generic view.""" + pass diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py index 46e92fe26e..17ba2de766 100644 --- a/django/views/generic/create_update.py +++ b/django/views/generic/create_update.py @@ -1,154 +1,195 @@ -from django.core.xheaders import populate_xheaders -from django.template import 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.newforms.models import ModelFormMetaclass, ModelForm +from django.template import RequestContext, loader from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.core.xheaders import populate_xheaders from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured 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, - template_loader=loader, extra_context=None, post_save_redirect=None, - login_required=False, follow=None, context_processors=None): +def deprecate_follow(follow): """ - Generic object-creation function. + Issues a DeprecationWarning if follow is anything but None. - Templates: ``/_form.html`` - Context: - form - the form wrapper for the object + The old Manipulator-based forms used a follow argument that is no longer + needed for newforms-based forms. """ - if extra_context is None: extra_context = {} - if login_required and not request.user.is_authenticated(): - return redirect_to_login(request.path) + if follow is not None: + import warning + 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) - if request.POST: - # If data was POSTed, we're trying to create a new object - new_data = request.POST.copy() - - if model._meta.has_field_type(FileField): - 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(): +def apply_extra_context(extra_context, context): + """ + Adds items from extra_context dict to context. If a value in extra_context + is callable, then it is called and the result is added to context. + """ + for key, value in extra_context.iteritems(): if callable(value): - c[key] = value() + context[key] = value() else: - c[key] = value - return HttpResponse(t.render(c)) + context[key] = value -def update_object(request, model, object_id=None, slug=None, - 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'): +def get_model_and_form_class(model, form_class): """ - 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: ``/_form.html`` - Context: - form - the form wrapper for the object - object - the original object being edited + If ``form_class`` is given then its associated model will be returned along + with ``form_class`` itself. Otherwise, if ``model`` is given, ``model`` + itself will be returned along with a ``ModelForm`` class created from + ``model``. """ - if extra_context is None: extra_context = {} - if login_required and not request.user.is_authenticated(): - return redirect_to_login(request.path) + if form_class: + return form_class._meta.model, form_class + 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 = {} 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 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: - object = model.objects.get(**lookup_kwargs) + return model.objects.get(**lookup_kwargs) 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: - new_data = request.POST.copy() - if model._meta.has_field_type(FileField): - new_data.update(request.FILES) - errors = manipulator.get_validation_errors(new_data) - manipulator.do_html2python(new_data) - if not errors: - object = manipulator.save(new_data) + Templates: ``/_form.html`` + Context: + form + the form for the object + """ + 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) + if request.method == 'POST': + form = form_class(request.POST, request.FILES) + if form.is_valid(): + new_object = 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}) - - # 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.") + 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) else: - errors = {} - # This makes sure the form acurate represents the fields of the place. - new_data = manipulator.flatten_data() + form = form_class() - form = oldforms.FormWrapper(manipulator, new_data, errors) + # Create the template, context, response 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: object, }, context_processors) - for key, value in extra_context.items(): - if callable(value): - c[key] = value() - else: - c[key] = value + apply_extra_context(extra_context, c) + return HttpResponse(t.render(c)) + +def update_object(request, model=None, object_id=None, slug=None, + 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: ``/_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)) - populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname)) + populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname)) return response -def delete_object(request, model, post_delete_redirect, - object_id=None, slug=None, slug_field='slug', template_name=None, - template_loader=loader, extra_context=None, - login_required=False, context_processors=None, template_object_name='object'): +def delete_object(request, model, post_delete_redirect, object_id=None, + slug=None, slug_field='slug', template_name=None, + template_loader=loader, extra_context=None, login_required=False, + context_processors=None, template_object_name='object'): """ 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(): return redirect_to_login(request.path) - # Look up the object to be edited - 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) + obj = lookup_object(model, object_id, slug, slug_field) if request.method == 'POST': - object.delete() + obj.delete() if request.user.is_authenticated(): request.user.message_set.create(message=ugettext("The %(verbose_name)s was deleted.") % {"verbose_name": model._meta.verbose_name}) 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()) t = template_loader.get_template(template_name) c = RequestContext(request, { - template_object_name: object, + template_object_name: obj, }, context_processors) - for key, value in extra_context.items(): - if callable(value): - c[key] = value() - else: - c[key] = value + apply_extra_context(extra_context, 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 diff --git a/docs/generic_views.txt b/docs/generic_views.txt index c4fea21016..a7602524a9 100644 --- a/docs/generic_views.txt +++ b/docs/generic_views.txt @@ -701,7 +701,7 @@ A page representing a list of objects. query string parameter (via ``GET``) or a ``page`` variable specified in 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. * ``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 - * To loop over all the available page numbers, use the ``page_range`` - variable. You can iterate over the list provided by ``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`` 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 -represented as page ``1``. +represented as page ``1``. For more on pagination, read the `pagination documentation`_. .. _`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 ``page``:: /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. 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 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`` ---------------------------------------------------- **Description:** 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 -automatic manipulators that come with Django models. +validation errors (if there are any) and saving the object. **Required arguments:** - * ``model``: The Django model class of the object that the form will - create. + * Either ``form_class`` or ``model`` is required. + + 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:** @@ -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: - * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form - for editing the object. This lets you refer to form fields easily in the + * ``form``: A ``django.newforms.ModelForm`` instance representing the form + for creating the object. This lets you refer to form fields easily in the template system. - For example, if ``model`` has two fields, ``name`` and ``address``:: + For example, if the model has two fields, ``name`` and ``address``::
-

{{ form.name }}

-

{{ form.address }}

+

{{ form.name.label_tag }} {{ form.name }}

+

{{ form.address.label_tag }} {{ form.address }}

- See the `manipulator and formfield documentation`_ for more information - about using ``FormWrapper`` objects in templates. + See the `newforms documentation`_ for more information about using + ``Form`` objects in templates. .. _authentication system: ../authentication/ -.. _manipulator and formfield documentation: ../forms/ +.. _ModelForm docs: ../newforms/modelforms +.. _newforms documentation: ../newforms/ ``django.views.generic.create_update.update_object`` ---------------------------------------------------- @@ -987,8 +1002,15 @@ object. This uses the automatic manipulators that come with Django models. **Required arguments:** - * ``model``: The Django model class of the object that the form will - create. + * Either ``form_class`` or ``model`` is required. + + 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. @@ -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: - * ``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 template system. - For example, if ``model`` has two fields, ``name`` and ``address``:: + For example, if the model has two fields, ``name`` and ``address``::
-

{{ form.name }}

-

{{ form.address }}

+

{{ form.name.label_tag }} {{ form.name }}

+

{{ form.address.label_tag }} {{ form.address }}

- See the `manipulator and formfield documentation`_ for more information - about using ``FormWrapper`` objects in templates. + See the `newforms documentation`_ for more information about using + ``Form`` objects in templates. * ``object``: The original object being edited. This variable's name depends on the ``template_object_name`` parameter, which is ``'object'`` 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,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, "model": "views.article", @@ -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'') + + 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.*)$', 'django.views.static.serve', {'document_root': media_dir}), - - # Date-based generic views - (r'^date_based/object_detail/(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/(?P[-\w]+)/$', - 'django.views.generic.date_based.object_detail', - dict(slug_field='slug', **date_based_info_dict)), - (r'^date_based/object_detail/(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/(?P[-\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\d{4})/(?P\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\d{4})/(?P\d{1,2})/(?P\d{1,2})/(?P[-\w]+)/$', + 'object_detail', + dict(slug_field='slug', **date_based_info_dict)), + (r'^date_based/object_detail/(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/(?P[-\w]+)/allow_future/$', + 'object_detail', + dict(allow_future=True, slug_field='slug', **date_based_info_dict)), + (r'^date_based/archive_month/(?P\d{4})/(?P\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[-\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[-\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[-\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[-\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..eda8aabc50 100644 --- a/tests/regressiontests/views/views.py +++ b/tests/regressiontests/views/views.py @@ -1,5 +1,29 @@ 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): """Dummy index page""" return HttpResponse('Dummy page') + + +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/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 }}