Commit 7997133a authored by Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss
Browse files

Fixed #3639: updated generic create_update views to use newforms. This is a...

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
parent cd80ce7a
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
class GenericViewError(Exception):
    """A problem in a generic view."""
    pass
+139 −113
Original line number Diff line number Diff line
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 deprecate_follow(follow):
    """
    Issues a DeprecationWarning if follow is anything but None.

    The old Manipulator-based forms used a follow argument that is no longer
    needed for newforms-based forms.
    """
    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)

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):
            context[key] = value()
        else:
            context[key] = value

def get_model_and_form_class(model, form_class):
    """
    Returns a model and form class based on the model and form_class
    parameters that were passed to the generic view.

    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 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.")

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.

def create_object(request, model, template_name=None,
    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 GenericViewError(
            "Generic view must be called with either an object_id or a"
            " slug/slug_field.")
    try:
        return model.objects.get(**lookup_kwargs)
    except ObjectDoesNotExist:
        raise Http404("No %s found for %s"
                      % (model._meta.verbose_name, lookup_kwargs))

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):
        login_required=False, follow=None, context_processors=None,
        form_class=None):
    """
    Generic object-creation function.

    Templates: ``<app_label>/<model_name>_form.html``
    Context:
        form
            the form wrapper for the object
            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)

    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)

    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 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.")
            return redirect(post_save_redirect, new_object)
    else:
        # No POST, so we want a brand new form without any data or errors
        errors = {}
        new_data = manipulator.flatten_data()
        form = form_class()

    # Create the FormWrapper, template, context, response
    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,
    }, 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, object_id=None, slug=None,
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'):
        template_object_name='object', form_class=None):
    """
    Generic object-update function.

    Templates: ``<app_label>/<model_name>_form.html``
    Context:
        form
            the form wrapper for the object
            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)

    # 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 edit view must be called with either an object_id or a slug/slug_field")
    try:
        object = model.objects.get(**lookup_kwargs)
    except ObjectDoesNotExist:
        raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs)

    manipulator = model.ChangeManipulator(getattr(object, object._meta.pk.attname), follow=follow)

    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)
    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})

            # 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.")
            return redirect(post_save_redirect, obj)
    else:
        errors = {}
        # This makes sure the form acurate represents the fields of the place.
        new_data = manipulator.flatten_data()
        form = form_class(instance=obj)

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

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
+48 −26
Original line number Diff line number Diff line
@@ -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 action="" method="post">
          <p><label for="id_name">Name:</label> {{ form.name }}</p>
          <p><label for="id_address">Address:</label> {{ form.address }}</p>
          <p>{{ form.name.label_tag }} {{ form.name }}</p>
          <p>{{ form.address.label_tag }} {{ form.address }}</p>
          </form>

      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 action="" method="post">
          <p><label for="id_name">Name:</label> {{ form.name }}</p>
          <p><label for="id_address">Address:</label> {{ form.address }}</p>
          <p>{{ form.name.label_tag }} {{ form.name }}</p>
          <p>{{ form.address.label_tag }} {{ form.address }}</p>
          </form>

      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'``
+28 −1
Original line number Diff line number Diff line
[
    {
        "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", 
+19 −5
Original line number Diff line number Diff line
"""
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
Loading