Ticket #3639: 3639.2008-07-12.patch

File 3639.2008-07-12.patch, 38.8 KB (added by gwilson, 7 years ago)

updated patch that also includes more removal of duplicated code and more tests

  • django/views/generic/__init__.py

    === modified file 'django/views/generic/__init__.py'
     
     1class GenericViewError(Exception):
     2    """A problem in a generic view."""
     3    pass
  • django/views/generic/create_update.py

    === modified file 'django/views/generic/create_update.py'
     
     1from django.newforms.models import ModelFormMetaclass, ModelForm
     2from django.template import RequestContext, loader
     3from django.http import Http404, HttpResponse, HttpResponseRedirect
    14from django.core.xheaders import populate_xheaders
    2 from django.template import loader
    3 from django import oldforms
    4 from django.db.models import FileField
    5 from django.contrib.auth.views import redirect_to_login
    6 from django.template import RequestContext
    7 from django.http import Http404, HttpResponse, HttpResponseRedirect
    85from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
    96from django.utils.translation import ugettext
    10 
    11 def create_object(request, model, template_name=None,
     7from django.contrib.auth.views import redirect_to_login
     8from django.views.generic import GenericViewError
     9
     10
     11def deprecate_follow(follow):
     12    """
     13    Issues a DeprecationWarning if follow is anything but None.
     14
     15    The old Manipulator-based forms used a follow argument that is no longer
     16    needed for newforms-based forms.
     17    """
     18    if follow is not None:
     19        import warning
     20        msg = ("Generic views have been changed to use newforms, and the"
     21               "'follow' argument is no longer used.  Please update your code"
     22               "to not use the 'follow' argument.")
     23        warning.warn(msg, DeprecationWarning, stacklevel=3)
     24
     25
     26def apply_extra_context(extra_context, context):
     27    """
     28    Adds items from extra_context dict to context.  If a value in extra_context
     29    is callable, then it is called and the result is added to context.
     30    """
     31    for key, value in extra_context.iteritems():
     32        if callable(value):
     33            context[key] = value()
     34        else:
     35            context[key] = value
     36
     37
     38def get_model_and_form_class(model, form_class):
     39    """
     40    Returns a model and form class based on the model and form_class
     41    parameters that were passed to the generic view.
     42
     43    If ``form_class`` is given then its associated model will be returned along
     44    with ``form_class`` itself.  Otherwise, if ``model`` is given, ``model``
     45    itself will be returned along with a ``ModelForm`` class created from
     46    ``model``.
     47    """
     48    if form_class:
     49        return form_class._meta.model, form_class
     50    if model:
     51        # The inner Meta class fails if model = model is used for some reason.
     52        tmp_model = model
     53        # TODO: we should be able to construct a ModelForm without creating
     54        # and passing in a temporary inner class.
     55        class Meta:
     56            model = tmp_model
     57        class_name = model.__name__ + 'Form'
     58        form_class = ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
     59        return model, form_class
     60    raise GenericViewError("Generic view must be called with either a model or"
     61                           " form_class argument.")
     62
     63
     64def redirect(post_save_redirect, obj):
     65    """
     66    Returns a HttpResponseRedirect to ``post_save_redirect``.
     67
     68    ``post_save_redirect`` should be a string, and can contain named string-
     69    substitution place holders of ``obj`` field names.
     70
     71    If ``post_save_redirect`` is None, then redirect to ``obj``'s URL returned
     72    by ``get_absolute_url()``.  If ``obj`` has no ``get_absolute_url`` method,
     73    then raise ImproperlyConfigured.
     74
     75    This method is meant to handle the post_save_redirect parameter to the
     76    ``create_object`` and ``update_object`` views.
     77    """
     78    if post_save_redirect:
     79        return HttpResponseRedirect(post_save_redirect % obj.__dict__)
     80    elif hasattr(obj, 'get_absolute_url'):
     81        return HttpResponseRedirect(obj.get_absolute_url())
     82    else:
     83        raise ImproperlyConfigured(
     84            "No URL to redirect to.  Either pass a post_save_redirect"
     85            " parameter to the generic view or define a get_absolute_url"
     86            " method on the Model.")
     87
     88
     89def lookup_object(model, object_id, slug, slug_field):
     90    """
     91    Return the ``model`` object with the passed ``object_id``.  If
     92    ``object_id`` is None, then return the the object whose ``slug_field``
     93    equals the passed ``slug``.  If ``slug`` and ``slug_field`` are not passed,
     94    then raise Http404 exception.
     95    """
     96    lookup_kwargs = {}
     97    if object_id:
     98        lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id
     99    elif slug and slug_field:
     100        lookup_kwargs['%s__exact' % slug_field] = slug
     101    else:
     102        raise GenericViewError(
     103            "Generic view must be called with either an object_id or a"
     104            " slug/slug_field.")
     105    try:
     106        return model.objects.get(**lookup_kwargs)
     107    except ObjectDoesNotExist:
     108        raise Http404("No %s found for %s"
     109                      % (model._meta.verbose_name, lookup_kwargs))
     110
     111
     112def create_object(request, model=None, template_name=None,
    12113        template_loader=loader, extra_context=None, post_save_redirect=None,
    13         login_required=False, follow=None, context_processors=None):
     114        login_required=False, follow=None, context_processors=None,
     115        form_class=None):
    14116    """
    15117    Generic object-creation function.
    16118
    17119    Templates: ``<app_label>/<model_name>_form.html``
    18120    Context:
    19121        form
    20             the form wrapper for the object
     122            the form for the object
    21123    """
     124    deprecate_follow(follow)
    22125    if extra_context is None: extra_context = {}
    23126    if login_required and not request.user.is_authenticated():
    24127        return redirect_to_login(request.path)
    25128
    26     manipulator = model.AddManipulator(follow=follow)
    27     if request.POST:
    28         # If data was POSTed, we're trying to create a new object
    29         new_data = request.POST.copy()
    30 
    31         if model._meta.has_field_type(FileField):
    32             new_data.update(request.FILES)
    33 
    34         # Check for errors
    35         errors = manipulator.get_validation_errors(new_data)
    36         manipulator.do_html2python(new_data)
    37 
    38         if not errors:
    39             # No errors -- this means we can save the data!
    40             new_object = manipulator.save(new_data)
    41 
     129    model, form_class = get_model_and_form_class(model, form_class)
     130    if request.method == 'POST':
     131        form = form_class(request.POST, request.FILES)
     132        if form.is_valid():
     133            new_object = form.save()
    42134            if request.user.is_authenticated():
    43135                request.user.message_set.create(message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name})
    44 
    45             # Redirect to the new object: first by trying post_save_redirect,
    46             # then by obj.get_absolute_url; fail if neither works.
    47             if post_save_redirect:
    48                 return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
    49             elif hasattr(new_object, 'get_absolute_url'):
    50                 return HttpResponseRedirect(new_object.get_absolute_url())
    51             else:
    52                 raise ImproperlyConfigured("No URL to redirect to from generic create view.")
     136            return redirect(post_save_redirect, new_object)
    53137    else:
    54         # No POST, so we want a brand new form without any data or errors
    55         errors = {}
    56         new_data = manipulator.flatten_data()
     138        form = form_class()
    57139
    58     # Create the FormWrapper, template, context, response
    59     form = oldforms.FormWrapper(manipulator, new_data, errors)
     140    # Create the template, context, response
    60141    if not template_name:
    61142        template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
    62143    t = template_loader.get_template(template_name)
    63144    c = RequestContext(request, {
    64145        'form': form,
    65146    }, context_processors)
    66     for key, value in extra_context.items():
    67         if callable(value):
    68             c[key] = value()
    69         else:
    70             c[key] = value
     147    apply_extra_context(extra_context, c)
    71148    return HttpResponse(t.render(c))
    72149
    73 def update_object(request, model, object_id=None, slug=None,
     150
     151def update_object(request, model=None, object_id=None, slug=None,
    74152        slug_field='slug', template_name=None, template_loader=loader,
    75153        extra_context=None, post_save_redirect=None,
    76154        login_required=False, follow=None, context_processors=None,
    77         template_object_name='object'):
     155        template_object_name='object', form_class=None):
    78156    """
    79157    Generic object-update function.
    80158
    81159    Templates: ``<app_label>/<model_name>_form.html``
    82160    Context:
    83161        form
    84             the form wrapper for the object
     162            the form for the object
    85163        object
    86164            the original object being edited
    87165    """
     166    deprecate_follow(follow)
    88167    if extra_context is None: extra_context = {}
    89168    if login_required and not request.user.is_authenticated():
    90169        return redirect_to_login(request.path)
    91170
    92     # Look up the object to be edited
    93     lookup_kwargs = {}
    94     if object_id:
    95         lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id
    96     elif slug and slug_field:
    97         lookup_kwargs['%s__exact' % slug_field] = slug
    98     else:
    99         raise AttributeError("Generic edit view must be called with either an object_id or a slug/slug_field")
    100     try:
    101         object = model.objects.get(**lookup_kwargs)
    102     except ObjectDoesNotExist:
    103         raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs)
    104 
    105     manipulator = model.ChangeManipulator(getattr(object, object._meta.pk.attname), follow=follow)
    106 
    107     if request.POST:
    108         new_data = request.POST.copy()
    109         if model._meta.has_field_type(FileField):
    110             new_data.update(request.FILES)
    111         errors = manipulator.get_validation_errors(new_data)
    112         manipulator.do_html2python(new_data)
    113         if not errors:
    114             object = manipulator.save(new_data)
    115 
     171    model, form_class = get_model_and_form_class(model, form_class)
     172    obj = lookup_object(model, object_id, slug, slug_field)
     173
     174    if request.method == 'POST':
     175        form = form_class(request.POST, request.FILES, instance=obj)
     176        if form.is_valid():
     177            obj = form.save()
    116178            if request.user.is_authenticated():
    117179                request.user.message_set.create(message=ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": model._meta.verbose_name})
    118 
    119             # Do a post-after-redirect so that reload works, etc.
    120             if post_save_redirect:
    121                 return HttpResponseRedirect(post_save_redirect % object.__dict__)
    122             elif hasattr(object, 'get_absolute_url'):
    123                 return HttpResponseRedirect(object.get_absolute_url())
    124             else:
    125                 raise ImproperlyConfigured("No URL to redirect to from generic create view.")
     180            return redirect(post_save_redirect, obj)
    126181    else:
    127         errors = {}
    128         # This makes sure the form acurate represents the fields of the place.
    129         new_data = manipulator.flatten_data()
     182        form = form_class(instance=obj)
    130183
    131     form = oldforms.FormWrapper(manipulator, new_data, errors)
    132184    if not template_name:
    133185        template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
    134186    t = template_loader.get_template(template_name)
    135187    c = RequestContext(request, {
    136188        'form': form,
    137         template_object_name: object,
     189        template_object_name: obj,
    138190    }, context_processors)
    139     for key, value in extra_context.items():
    140         if callable(value):
    141             c[key] = value()
    142         else:
    143             c[key] = value
     191    apply_extra_context(extra_context, c)
    144192    response = HttpResponse(t.render(c))
    145     populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname))
     193    populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
    146194    return response
    147195
    148 def delete_object(request, model, post_delete_redirect,
    149         object_id=None, slug=None, slug_field='slug', template_name=None,
    150         template_loader=loader, extra_context=None,
    151         login_required=False, context_processors=None, template_object_name='object'):
     196
     197def delete_object(request, model, post_delete_redirect, object_id=None,
     198        slug=None, slug_field='slug', template_name=None,
     199        template_loader=loader, extra_context=None, login_required=False,
     200        context_processors=None, template_object_name='object'):
    152201    """
    153202    Generic object-delete function.
    154203
     
    165214    if login_required and not request.user.is_authenticated():
    166215        return redirect_to_login(request.path)
    167216
    168     # Look up the object to be edited
    169     lookup_kwargs = {}
    170     if object_id:
    171         lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id
    172     elif slug and slug_field:
    173         lookup_kwargs['%s__exact' % slug_field] = slug
    174     else:
    175         raise AttributeError("Generic delete view must be called with either an object_id or a slug/slug_field")
    176     try:
    177         object = model._default_manager.get(**lookup_kwargs)
    178     except ObjectDoesNotExist:
    179         raise Http404, "No %s found for %s" % (model._meta.app_label, lookup_kwargs)
     217    obj = lookup_object(model, object_id, slug, slug_field)
    180218
    181219    if request.method == 'POST':
    182         object.delete()
     220        obj.delete()
    183221        if request.user.is_authenticated():
    184222            request.user.message_set.create(message=ugettext("The %(verbose_name)s was deleted.") % {"verbose_name": model._meta.verbose_name})
    185223        return HttpResponseRedirect(post_delete_redirect)
     
    188226            template_name = "%s/%s_confirm_delete.html" % (model._meta.app_label, model._meta.object_name.lower())
    189227        t = template_loader.get_template(template_name)
    190228        c = RequestContext(request, {
    191             template_object_name: object,
     229            template_object_name: obj,
    192230        }, context_processors)
    193         for key, value in extra_context.items():
    194             if callable(value):
    195                 c[key] = value()
    196             else:
    197                 c[key] = value
     231        apply_extra_context(extra_context, c)
    198232        response = HttpResponse(t.render(c))
    199         populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname))
     233        populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname))
    200234        return response
  • docs/generic_views.txt

    === modified file 'docs/generic_views.txt'
     
    701701      query string parameter (via ``GET``) or a ``page`` variable specified in
    702702      the URLconf. See `Notes on pagination`_ below.
    703703
    704     * ``page``: The current page number, as an integer. This is 1-based. 
     704    * ``page``: The current page number, as an integer. This is 1-based.
    705705      See `Notes on pagination`_ below.
    706706
    707707    * ``template_name``: The full name of a template to use in rendering the
     
    809809
    810810        /objects/?page=3
    811811
    812     * To loop over all the available page numbers, use the ``page_range`` 
    813       variable. You can iterate over the list provided by ``page_range`` 
     812    * To loop over all the available page numbers, use the ``page_range``
     813      variable. You can iterate over the list provided by ``page_range``
    814814      to create a link to every page of results.
    815815
    816816These values and lists are 1-based, not 0-based, so the first page would be
    817 represented as page ``1``. 
     817represented as page ``1``.
    818818
    819819For more on pagination, read the `pagination documentation`_.
    820820         
    821821.. _`pagination documentation`: ../pagination/
    822822
    823 **New in Django development version:** 
     823**New in Django development version:**
    824824
    825825As a special case, you are also permitted to use ``last`` as a value for
    826826``page``::
    827827
    828828    /objects/?page=last
    829829
    830 This allows you to access the final page of results without first having to 
     830This allows you to access the final page of results without first having to
    831831determine how many pages there are.
    832832
    833833Note that ``page`` *must* be either a valid page number or the value ``last``;
     
    906906The ``django.views.generic.create_update`` module contains a set of functions
    907907for creating, editing and deleting objects.
    908908
     909**New in Django development version:**
     910
     911``django.views.generic.create_update.create_object`` and
     912``django.views.generic.create_update.update_object`` now use `newforms`_ to
     913build and display the form.
     914
     915.. _newforms: ../newforms/
     916
    909917``django.views.generic.create_update.create_object``
    910918----------------------------------------------------
    911919
    912920**Description:**
    913921
    914922A page that displays a form for creating an object, redisplaying the form with
    915 validation errors (if there are any) and saving the object. This uses the
    916 automatic manipulators that come with Django models.
     923validation errors (if there are any) and saving the object.
    917924
    918925**Required arguments:**
    919926
    920     * ``model``: The Django model class of the object that the form will
    921       create.
     927    * Either ``form_class`` or ``model`` is required.
     928
     929      If you provide ``form_class``, it should be a
     930      ``django.newforms.ModelForm`` subclass.  Use this argument when you need
     931      to customize the model's form.  See the `ModelForm docs`_ for more
     932      information.
     933
     934      Otherwise, ``model`` should be a Django model class and the form used
     935      will be a standard ``ModelForm`` for ``model``.
    922936
    923937**Optional arguments:**
    924938
     
    959973
    960974In addition to ``extra_context``, the template's context will be:
    961975
    962     * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form
    963       for editing the object. This lets you refer to form fields easily in the
     976    * ``form``: A ``django.newforms.ModelForm`` instance representing the form
     977      for creating the object. This lets you refer to form fields easily in the
    964978      template system.
    965979
    966       For example, if ``model`` has two fields, ``name`` and ``address``::
     980      For example, if the model has two fields, ``name`` and ``address``::
    967981
    968982          <form action="" method="post">
    969           <p><label for="id_name">Name:</label> {{ form.name }}</p>
    970           <p><label for="id_address">Address:</label> {{ form.address }}</p>
     983          <p>{{ form.name.label_tag }} {{ form.name }}</p>
     984          <p>{{ form.address.label_tag }} {{ form.address }}</p>
    971985          </form>
    972986
    973       See the `manipulator and formfield documentation`_ for more information
    974       about using ``FormWrapper`` objects in templates.
     987      See the `newforms documentation`_ for more information about using
     988      ``Form`` objects in templates.
    975989
    976990.. _authentication system: ../authentication/
    977 .. _manipulator and formfield documentation: ../forms/
     991.. _ModelForm docs: ../newforms/modelforms
     992.. _newforms documentation: ../newforms/
    978993
    979994``django.views.generic.create_update.update_object``
    980995----------------------------------------------------
     
    9871002
    9881003**Required arguments:**
    9891004
    990     * ``model``: The Django model class of the object that the form will
    991       create.
     1005    * Either ``form_class`` or ``model`` is required.
     1006
     1007      If you provide ``form_class``, it should be a
     1008      ``django.newforms.ModelForm`` subclass.  Use this argument when you need
     1009      to customize the model's form.  See the `ModelForm docs`_ for more
     1010      information.
     1011
     1012      Otherwise, ``model`` should be a Django model class and the form used
     1013      will be a standard ``ModelForm`` for ``model``.
    9921014
    9931015    * Either ``object_id`` or (``slug`` *and* ``slug_field``) is required.
    9941016
     
    10411063
    10421064In addition to ``extra_context``, the template's context will be:
    10431065
    1044     * ``form``: A ``django.oldforms.FormWrapper`` instance representing the form
     1066    * ``form``: A ``django.newforms.ModelForm`` instance representing the form
    10451067      for editing the object. This lets you refer to form fields easily in the
    10461068      template system.
    10471069
    1048       For example, if ``model`` has two fields, ``name`` and ``address``::
     1070      For example, if the model has two fields, ``name`` and ``address``::
    10491071
    10501072          <form action="" method="post">
    1051           <p><label for="id_name">Name:</label> {{ form.name }}</p>
    1052           <p><label for="id_address">Address:</label> {{ form.address }}</p>
     1073          <p>{{ form.name.label_tag }} {{ form.name }}</p>
     1074          <p>{{ form.address.label_tag }} {{ form.address }}</p>
    10531075          </form>
    10541076
    1055       See the `manipulator and formfield documentation`_ for more information
    1056       about using ``FormWrapper`` objects in templates.
     1077      See the `newforms documentation`_ for more information about using
     1078      ``Form`` objects in templates.
    10571079
    10581080    * ``object``: The original object being edited. This variable's name
    10591081      depends on the ``template_object_name`` parameter, which is ``'object'``
  • tests/regressiontests/views/fixtures/testdata.json

    === modified file 'tests/regressiontests/views/fixtures/testdata.json'
     
    11[
    22    {
     3        "pk": "1",
     4        "model": "auth.user",
     5        "fields": {
     6            "username": "testclient",
     7            "first_name": "Test",
     8            "last_name": "Client",
     9            "is_active": true,
     10            "is_superuser": false,
     11            "is_staff": false,
     12            "last_login": "2006-12-17 07:03:31",
     13            "groups": [],
     14            "user_permissions": [],
     15            "password": "sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161",
     16            "email": "testclient@example.com",
     17            "date_joined": "2006-12-17 07:03:31"
     18        }
     19    },
     20    {
    321        "pk": 1,
    422        "model": "views.article",
    523        "fields": {
     
    2947            "date_created": "3000-01-01 21:22:23"
    3048        }
    3149    },
    32 
     50        {
     51        "pk": 1,
     52        "model": "views.urlarticle",
     53        "fields": {
     54            "author": 1,
     55            "title": "Old Article",
     56            "slug": "old_article",
     57            "date_created": "2001-01-01 21:22:23"
     58        }
     59    },
    3360    {
    3461        "pk": 1,
    3562        "model": "views.author",
  • tests/regressiontests/views/models.py

    === modified file 'tests/regressiontests/views/models.py'
     
    11"""
    2 Regression tests for Django built-in views
     2Regression tests for Django built-in views.
    33"""
    44
    55from django.db import models
    6 from django.conf import settings
     6
    77
    88class Author(models.Model):
    99    name = models.CharField(max_length=100)
     
    1515        return '/views/authors/%s/' % self.id
    1616
    1717
    18 class Article(models.Model):
     18class BaseArticle(models.Model):
     19    """
     20    An abstract article Model so that we can create article models with and
     21    without a get_absolute_url method (for create_update generic views tests).
     22    """
    1923    title = models.CharField(max_length=100)
    2024    slug = models.SlugField()
    2125    author = models.ForeignKey(Author)
    2226    date_created = models.DateTimeField()
    23    
     27
     28    class Meta:
     29        abstract = True
     30
    2431    def __unicode__(self):
    2532        return self.title
    2633
     34
     35class Article(BaseArticle):
     36    pass
     37
     38
     39class UrlArticle(BaseArticle):
     40    """
     41    An Article class with a get_absolute_url defined.
     42    """
     43    def get_absolute_url(self):
     44        return '/urlarticles/%s/' % self.slug
  • tests/regressiontests/views/tests/__init__.py

    === modified file 'tests/regressiontests/views/tests/__init__.py'
     
    11from defaults import *
    22from i18n import *
    33from static import *
    4 from generic.date_based import *
    5  No newline at end of file
     4from generic.date_based import *
     5from generic.create_update import *
  • tests/regressiontests/views/tests/generic/create_update.py

    === added file 'tests/regressiontests/views/tests/generic/create_update.py'
     
     1import datetime
     2
     3from django.test import TestCase
     4from django.core.exceptions import ImproperlyConfigured
     5from regressiontests.views.models import Article, UrlArticle
     6
     7
     8class CreateObjectTest(TestCase):
     9
     10    fixtures = ['testdata.json']
     11
     12    def test_login_required_view(self):
     13        """
     14        Verifies that an unauthenticated user attempting to access a
     15        login_required view gets redirected to the login page and that
     16        an authenticated user is let through.
     17        """
     18        view_url = '/views/create_update/member/create/article/'
     19        response = self.client.get(view_url)
     20        self.assertRedirects(response, '/accounts/login/?next=%s' % view_url)
     21        # Now login and try again.
     22        login = self.client.login(username='testclient', password='password')
     23        self.failUnless(login, 'Could not log in')
     24        response = self.client.get(view_url)
     25        self.assertEqual(response.status_code, 200)
     26        self.assertTemplateUsed(response, 'views/article_form.html')
     27
     28    def test_create_article_display_page(self):
     29        """
     30        Ensures the generic view returned the page and contains a form.
     31        """
     32        view_url = '/views/create_update/create/article/'
     33        response = self.client.get(view_url)
     34        self.assertEqual(response.status_code, 200)
     35        self.assertTemplateUsed(response, 'views/article_form.html')
     36        if not response.context.get('form'):
     37            self.fail('No form found in the response.')
     38
     39    def test_create_article_with_errors(self):
     40        """
     41        POSTs a form that contains validation errors.
     42        """
     43        view_url = '/views/create_update/create/article/'
     44        num_articles = Article.objects.count()
     45        response = self.client.post(view_url, {
     46            'title': 'My First Article',
     47        })
     48        self.assertFormError(response, 'form', 'slug', [u'This field is required.'])
     49        self.assertTemplateUsed(response, 'views/article_form.html')
     50        self.assertEqual(num_articles, Article.objects.count(),
     51                         "Number of Articles should not have changed.")
     52
     53    def test_create_custom_save_article(self):
     54        """
     55        Creates a new article using a custom form class with a save method
     56        that alters the slug entered.
     57        """
     58        view_url = '/views/create_update/create_custom/article/'
     59        response = self.client.post(view_url, {
     60            'title': 'Test Article',
     61            'slug': 'this-should-get-replaced',
     62            'author': 1,
     63            'date_created': datetime.datetime(2007, 6, 25),
     64        })
     65        self.assertRedirects(response,
     66            '/views/create_update/view/article/some-other-slug/',
     67            target_status_code=404)
     68
     69
     70class UpdateDeleteObjectTest(TestCase):
     71
     72    fixtures = ['testdata.json']
     73
     74    def test_update_object_form_display(self):
     75        """
     76        Verifies that the form was created properly and with initial values.
     77        """
     78        response = self.client.get('/views/create_update/update/article/old_article/')
     79        self.assertTemplateUsed(response, 'views/article_form.html')
     80        self.assertEquals(unicode(response.context['form']['title']),
     81            u'<input id="id_title" type="text" name="title" value="Old Article" maxlength="100" />')
     82
     83    def test_update_object(self):
     84        """
     85        Verifies the updating of an Article.
     86        """
     87        response = self.client.post('/views/create_update/update/article/old_article/', {
     88            'title': 'Another Article',
     89            'slug': 'another-article-slug',
     90            'author': 1,
     91            'date_created': datetime.datetime(2007, 6, 25),
     92        })
     93        article = Article.objects.get(pk=1)
     94        self.assertEquals(article.title, "Another Article")
     95
     96    def test_delete_object_confirm(self):
     97        """
     98        Verifies the confirm deletion page is displayed using a GET.
     99        """
     100        response = self.client.get('/views/create_update/delete/article/old_article/')
     101        self.assertTemplateUsed(response, 'views/article_confirm_delete.html')
     102
     103    def test_delete_object(self):
     104        """
     105        Verifies the object actually gets deleted on a POST.
     106        """
     107        view_url = '/views/create_update/delete/article/old_article/'
     108        response = self.client.post(view_url)
     109        try:
     110            Article.objects.get(slug='old_article')
     111        except Article.DoesNotExist:
     112            pass
     113        else:
     114            self.fail('Object was not deleted.')
     115
     116
     117class PostSaveRedirectTests(TestCase):
     118    """
     119    Verifies that the views redirect to the correct locations depending on
     120    if a post_save_redirect was passed and a get_absolute_url method exists
     121    on the Model.
     122    """
     123
     124    fixtures = ['testdata.json']
     125    article_model = Article
     126
     127    create_url = '/views/create_update/create/article/'
     128    update_url = '/views/create_update/update/article/old_article/'
     129    delete_url = '/views/create_update/delete/article/old_article/'
     130
     131    create_redirect = '/views/create_update/view/article/my-first-article/'
     132    update_redirect = '/views/create_update/view/article/another-article-slug/'
     133    delete_redirect = '/views/create_update/'
     134
     135    def test_create_article(self):
     136        num_articles = self.article_model.objects.count()
     137        response = self.client.post(self.create_url, {
     138            'title': 'My First Article',
     139            'slug': 'my-first-article',
     140            'author': '1',
     141            'date_created': datetime.datetime(2007, 6, 25),
     142        })
     143        self.assertRedirects(response, self.create_redirect,
     144                             target_status_code=404)
     145        self.assertEqual(num_articles + 1, self.article_model.objects.count(),
     146                         "A new Article should have been created.")
     147
     148    def test_update_article(self):
     149        num_articles = self.article_model.objects.count()
     150        response = self.client.post(self.update_url, {
     151            'title': 'Another Article',
     152            'slug': 'another-article-slug',
     153            'author': 1,
     154            'date_created': datetime.datetime(2007, 6, 25),
     155        })
     156        self.assertRedirects(response, self.update_redirect,
     157                             target_status_code=404)
     158        self.assertEqual(num_articles, self.article_model.objects.count(),
     159                         "A new Article should not have been created.")
     160
     161    def test_delete_article(self):
     162        num_articles = self.article_model.objects.count()
     163        response = self.client.post(self.delete_url)
     164        self.assertRedirects(response, self.delete_redirect,
     165                             target_status_code=404)
     166        self.assertEqual(num_articles - 1, self.article_model.objects.count(),
     167                         "An Article should have been deleted.")
     168
     169
     170class NoPostSaveNoAbsoluteUrl(PostSaveRedirectTests):
     171    """
     172    Tests that when no post_save_redirect is passed and no get_absolute_url
     173    method exists on the Model that the view raises an ImproperlyConfigured
     174    error.
     175    """
     176
     177    create_url = '/views/create_update/no_redirect/create/article/'
     178    update_url = '/views/create_update/no_redirect/update/article/old_article/'
     179
     180    def test_create_article(self):
     181        self.assertRaises(ImproperlyConfigured,
     182            super(NoPostSaveNoAbsoluteUrl, self).test_create_article)
     183
     184    def test_update_article(self):
     185        self.assertRaises(ImproperlyConfigured,
     186            super(NoPostSaveNoAbsoluteUrl, self).test_update_article)
     187
     188    def test_delete_article(self):
     189        """
     190        The delete_object view requires a post_delete_redirect, so skip testing
     191        here.
     192        """
     193        pass
     194
     195
     196class AbsoluteUrlNoPostSave(PostSaveRedirectTests):
     197    """
     198    Tests that the views redirect to the Model's get_absolute_url when no
     199    post_save_redirect is passed.
     200    """
     201
     202    # Article model with get_absolute_url method.
     203    article_model = UrlArticle
     204
     205    create_url = '/views/create_update/no_url/create/article/'
     206    update_url = '/views/create_update/no_url/update/article/old_article/'
     207
     208    create_redirect = '/urlarticles/my-first-article/'
     209    update_redirect = '/urlarticles/another-article-slug/'
     210
     211    def test_delete_article(self):
     212        """
     213        The delete_object view requires a post_delete_redirect, so skip testing
     214        here.
     215        """
     216        pass
  • tests/regressiontests/views/urls.py

    === modified file 'tests/regressiontests/views/urls.py'
     
    55from models import *
    66import views
    77
     8
    89base_dir = path.dirname(path.abspath(__file__))
    910media_dir = path.join(base_dir, 'media')
    1011locale_dir = path.join(base_dir, 'locale')
     
    1415    'packages': ('regressiontests.views',),
    1516}
    1617
    17 date_based_info_dict = { 
    18     'queryset': Article.objects.all(), 
    19     'date_field': 'date_created', 
    20     'month_format': '%m', 
    21 } 
     18date_based_info_dict = {
     19    'queryset': Article.objects.all(),
     20    'date_field': 'date_created',
     21    'month_format': '%m',
     22}
    2223
    2324urlpatterns = patterns('',
    2425    (r'^$', views.index_page),
    25    
     26
    2627    # Default views
    2728    (r'^shortcut/(\d+)/(.*)/$', 'django.views.defaults.shortcut'),
    2829    (r'^non_existing_url/', 'django.views.defaults.page_not_found'),
    2930    (r'^server_error/', 'django.views.defaults.server_error'),
    30    
     31
    3132    # i18n views
    32     (r'^i18n/', include('django.conf.urls.i18n')),   
     33    (r'^i18n/', include('django.conf.urls.i18n')),
    3334    (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', js_info_dict),
    34    
     35
    3536    # Static views
    3637    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}),
    37    
    38         # Date-based generic views
    39     (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$',
    40         'django.views.generic.date_based.object_detail',
    41         dict(slug_field='slug', **date_based_info_dict)),
    42     (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/allow_future/$',
    43         'django.views.generic.date_based.object_detail',
    44         dict(allow_future=True, slug_field='slug', **date_based_info_dict)),
    45     (r'^date_based/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$',
    46         'django.views.generic.date_based.archive_month',
    47         date_based_info_dict),     
     38)
     39
     40# Date-based generic views.
     41urlpatterns += patterns('django.views.generic.date_based',
     42    (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$',
     43        'object_detail',
     44        dict(slug_field='slug', **date_based_info_dict)),
     45    (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/allow_future/$',
     46        'object_detail',
     47        dict(allow_future=True, slug_field='slug', **date_based_info_dict)),
     48    (r'^date_based/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$',
     49        'archive_month',
     50        date_based_info_dict),
     51)
     52
     53# crud generic views.
     54
     55urlpatterns += patterns('django.views.generic.create_update',
     56    (r'^create_update/member/create/article/$', 'create_object',
     57        dict(login_required=True, model=Article)),
     58    (r'^create_update/create/article/$', 'create_object',
     59        dict(post_save_redirect='/views/create_update/view/article/%(slug)s/',
     60             model=Article)),
     61    (r'^create_update/update/article/(?P<slug>[-\w]+)/$', 'update_object',
     62        dict(post_save_redirect='/views/create_update/view/article/%(slug)s/',
     63             slug_field='slug', model=Article)),
     64    (r'^create_update/create_custom/article/$', views.custom_create),
     65    (r'^create_update/delete/article/(?P<slug>[-\w]+)/$', 'delete_object',
     66        dict(post_delete_redirect='/views/create_update/', slug_field='slug',
     67             model=Article)),
     68
     69    # No post_save_redirect and no get_absolute_url on model.
     70    (r'^create_update/no_redirect/create/article/$', 'create_object',
     71        dict(model=Article)),
     72    (r'^create_update/no_redirect/update/article/(?P<slug>[-\w]+)/$',
     73        'update_object', dict(slug_field='slug', model=Article)),
     74
     75    # get_absolute_url on model, but no passed post_save_redirect.
     76    (r'^create_update/no_url/create/article/$', 'create_object',
     77        dict(model=UrlArticle)),
     78    (r'^create_update/no_url/update/article/(?P<slug>[-\w]+)/$',
     79        'update_object', dict(slug_field='slug', model=UrlArticle)),
    4880)
  • tests/regressiontests/views/views.py

    === modified file 'tests/regressiontests/views/views.py'
     
    11from django.http import HttpResponse
     2import django.newforms as forms
     3from django.views.generic.create_update import create_object
     4
     5from models import Article
     6
    27
    38def index_page(request):
    49    """Dummy index page"""
    510    return HttpResponse('<html><body>Dummy page</body></html>')
     11
     12
     13def custom_create(request):
     14    """
     15    Calls create_object generic view with a custom form class.
     16    """
     17    class SlugChangingArticleForm(forms.ModelForm):
     18        """Custom form class to overwrite the slug."""
     19
     20        class Meta:
     21            model = Article
     22
     23        def save(self, *args, **kwargs):
     24            self.cleaned_data['slug'] = 'some-other-slug'
     25            return super(SlugChangingArticleForm, self).save(*args, **kwargs)
     26
     27    return create_object(request,
     28        post_save_redirect='/views/create_update/view/article/%(slug)s/',
     29        form_class=SlugChangingArticleForm)
  • tests/templates/views/article_confirm_delete.html

    === added file 'tests/templates/views/article_confirm_delete.html'
     
     1This template intentionally left blank
     2 No newline at end of file
  • tests/templates/views/article_detail.html

    === modified file 'tests/templates/views/article_detail.html'
     
    1 This template intentionally left blank
    2  No newline at end of file
     1Article detail template.
  • tests/templates/views/article_form.html

    === added file 'tests/templates/views/article_form.html'
     
     1Article form template.
     2
     3{{ form.errors }}
  • tests/templates/views/urlarticle_detail.html

    === added file 'tests/templates/views/urlarticle_detail.html'
     
     1UrlArticle detail template.
  • tests/templates/views/urlarticle_form.html

    === added file 'tests/templates/views/urlarticle_form.html'
     
     1UrlArticle form template.
     2
     3{{ form.errors }}
Back to Top