Ticket #6735: new-generic-views.2.diff

File new-generic-views.2.diff, 26.9 KB (added by jkocherhans, 16 years ago)
  • new file django/contrib/formtools/wizard.py

    diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py
    new file mode 100644
    index 0000000..4fc7f88
    - +  
     1"""
     2FormWizard class -- implements a multi-page form, validating between each
     3step and storing the form's state as HTML hidden fields so that no state is
     4stored on the server side.
     5"""
     6
     7from django import newforms as forms
     8from django.conf import settings
     9from django.http import Http404
     10from django.shortcuts import render_to_response
     11from django.template.context import RequestContext
     12import cPickle as pickle
     13import md5
     14
     15class FormWizard(object):
     16    # Dictionary of extra template context variables.
     17    extra_context = {}
     18
     19    # The HTML (and POST data) field name for the "step" variable.
     20    step_field_name="wizard_step"
     21
     22    # METHODS SUBCLASSES SHOULDN'T OVERRIDE ###################################
     23
     24    def __init__(self, form_list, initial=None):
     25        "form_list should be a list of Form classes (not instances)."
     26        self.form_list = form_list[:]
     27        self.initial = initial or {}
     28        self.step = 0 # A zero-based counter keeping track of which step we're in.
     29
     30    def __repr__(self):
     31        return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial)
     32
     33    def get_form(self, step, data=None):
     34        "Helper method that returns the Form instance for the given step."
     35        return self.form_list[step](data, prefix=self.prefix_for_step(step), initial=self.initial.get(step, None))
     36
     37    def num_steps(self):
     38        "Helper method that returns the number of steps."
     39        # You might think we should just set "self.form_list = len(form_list)"
     40        # in __init__(), but this calculation needs to be dynamic, because some
     41        # hook methods might alter self.form_list.
     42        return len(self.form_list)
     43
     44    def __call__(self, request, *args, **kwargs):
     45        """
     46        Main method that does all the hard work, conforming to the Django view
     47        interface.
     48        """
     49        if 'extra_context' in kwargs:
     50            self.extra_context.update(kwargs['extra_context'])
     51        current_step = self.determine_step(request, *args, **kwargs)
     52
     53        # Sanity check.
     54        if current_step >= self.num_steps():
     55            raise Http404('Step %s does not exist' % current_step)
     56
     57        # For each previous step, verify the hash and process.
     58        # TODO: Move "hash_%d" to a method to make it configurable.
     59        for i in range(current_step):
     60            form = self.get_form(i, request.POST)
     61            if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form):
     62                return self.render_hash_failure(request, i)
     63            self.process_step(request, form, i)
     64
     65        # Process the current step. If it's valid, go to the next step or call
     66        # done(), depending on whether any steps remain.
     67        if request.method == 'POST':
     68            form = self.get_form(current_step, request.POST)
     69        else:
     70            form = self.get_form(current_step)
     71        if form.is_valid():
     72            self.process_step(request, form, current_step)
     73            next_step = current_step + 1
     74
     75            # If this was the last step, validate all of the forms one more
     76            # time, as a sanity check, and call done().
     77            num = self.num_steps()
     78            if next_step == num:
     79                final_form_list = [self.get_form(i, request.POST) for i in range(num)]
     80
     81                # Validate all the forms. If any of them fail validation, that
     82                # must mean the validator relied on some other input, such as
     83                # an external Web site.
     84                for i, f in enumerate(final_form_list):
     85                    if not f.is_valid():
     86                        return self.render_revalidation_failure(request, i, f)
     87                return self.done(request, final_form_list)
     88
     89            # Otherwise, move along to the next step.
     90            else:
     91                form = self.get_form(next_step)
     92                current_step = next_step
     93
     94        return self.render(form, request, current_step)
     95
     96    def render(self, form, request, step, context=None):
     97        "Renders the given Form object, returning an HttpResponse."
     98        old_data = request.POST
     99        prev_fields = []
     100        if old_data:
     101            hidden = forms.HiddenInput()
     102            # Collect all data from previous steps and render it as HTML hidden fields.
     103            for i in range(step):
     104                old_form = self.get_form(i, old_data)
     105                hash_name = 'hash_%s' % i
     106                prev_fields.extend([bf.as_hidden() for bf in old_form])
     107                prev_fields.append(hidden.render(hash_name, old_data.get(hash_name, self.security_hash(request, old_form))))
     108        return self.render_template(request, form, ''.join(prev_fields), step, context)
     109
     110    # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE ########################
     111
     112    def prefix_for_step(self, step):
     113        "Given the step, returns a Form prefix to use."
     114        return str(step)
     115
     116    def render_hash_failure(self, request, step):
     117        """
     118        Hook for rendering a template if a hash check failed.
     119
     120        step is the step that failed. Any previous step is guaranteed to be
     121        valid.
     122
     123        This default implementation simply renders the form for the given step,
     124        but subclasses may want to display an error message, etc.
     125        """
     126        return self.render(self.get_form(step), request, step, context={'wizard_error': 'We apologize, but your form has expired. Please continue filling out the form from this page.'})
     127
     128    def render_revalidation_failure(self, request, step, form):
     129        """
     130        Hook for rendering a template if final revalidation failed.
     131
     132        It is highly unlikely that this point would ever be reached, but See
     133        the comment in __call__() for an explanation.
     134        """
     135        return self.render(form, request, step)
     136
     137    def security_hash(self, request, form):
     138        """
     139        Calculates the security hash for the given HttpRequest and Form instances.
     140
     141        This creates a list of the form field names/values in a deterministic
     142        order, pickles the result with the SECRET_KEY setting and takes an md5
     143        hash of that.
     144
     145        Subclasses may want to take into account request-specific information,
     146        such as the IP address.
     147        """
     148        data = [(bf.name, bf.data) for bf in form] + [settings.SECRET_KEY]
     149        # Use HIGHEST_PROTOCOL because it's the most efficient. It requires
     150        # Python 2.3, but Django requires 2.3 anyway, so that's OK.
     151        pickled = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
     152        return md5.new(pickled).hexdigest()
     153
     154    def determine_step(self, request, *args, **kwargs):
     155        """
     156        Given the request object and whatever *args and **kwargs were passed to
     157        __call__(), returns the current step (which is zero-based).
     158
     159        Note that the result should not be trusted. It may even be a completely
     160        invalid number. It's not the job of this method to validate it.
     161        """
     162        if not request.POST:
     163            return 0
     164        try:
     165            step = int(request.POST.get(self.step_field_name, 0))
     166        except ValueError:
     167            return 0
     168        return step
     169
     170    def get_template(self, step):
     171        """
     172        Hook for specifying the name of the template to use for a given step.
     173
     174        Note that this can return a tuple of template names if you'd like to
     175        use the template system's select_template() hook.
     176        """
     177        return 'forms/wizard.html'
     178
     179    def render_template(self, request, form, previous_fields, step, context=None):
     180        """
     181        Renders the template for the given step, returning an HttpResponse object.
     182
     183        Override this method if you want to add a custom context, return a
     184        different MIME type, etc. If you only need to override the template
     185        name, use get_template() instead.
     186
     187        The template will be rendered with the following context:
     188            step_field -- The name of the hidden field containing the step.
     189            step0      -- The current step (zero-based).
     190            step       -- The current step (one-based).
     191            form       -- The Form instance for the current step (either empty
     192                          or with errors).
     193            previous_fields -- A string representing every previous data field,
     194                          plus hashes for completed forms, all in the form of
     195                          hidden fields. Note that you'll need to run this
     196                          through the "safe" template filter, to prevent
     197                          auto-escaping, because it's raw HTML.
     198        """
     199        context = context or {}
     200        context.update(self.extra_context)
     201        return render_to_response(self.get_template(self.step), dict(context,
     202            step_field=self.step_field_name,
     203            step0=step,
     204            step=step + 1,
     205            step_count=self.num_steps(),
     206            form=form,
     207            previous_fields=previous_fields
     208        ), context_instance=RequestContext(request))
     209
     210    def process_step(self, request, form, step):
     211        """
     212        Hook for modifying the FormWizard's internal state, given a fully
     213        validated Form object. The Form is guaranteed to have clean, valid
     214        data.
     215
     216        This method should *not* modify any of that data. Rather, it might want
     217        to set self.extra_context or dynamically alter self.form_list, based on
     218        previously submitted forms.
     219
     220        Note that this method is called every time a page is rendered for *all*
     221        submitted steps.
     222        """
     223        pass
     224
     225    # METHODS SUBCLASSES MUST OVERRIDE ########################################
     226
     227    def done(self, request, form_list):
     228        """
     229        Hook for doing something with the validated data. This is responsible
     230        for the final processing.
     231
     232        form_list is a list of Form instances, each containing clean, valid
     233        data.
     234        """
     235        raise NotImplementedError("Your %s class has not defined a done() method, which is required." % self.__class__.__name__)
  • django/views/generic/create_update.py

    diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py
    index 46e92fe..e9a2dc9 100644
    a b from django.template import RequestContext  
    77from django.http import Http404, HttpResponse, HttpResponseRedirect
    88from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
    99from django.utils.translation import ugettext
     10from django import newforms as forms
     11from django.newforms.models import ModelFormMetaclass, ModelForm
     12
     13
     14# New-sytle, class based generic views #######################################
     15
     16class BaseView(object):
     17    """
     18    Base class for generic object creation and update view.
     19
     20    Templates: ``<app_label>/<model_name>_form.html``
     21    Context:
     22        form
     23            the ``ModelForm`` instance for the object
     24    """
     25    def __init__(self, model, post_save_redirect=None):
     26        self.model = model
     27        self.post_save_redirect = None
     28
     29    def __call__(self, request):
     30        return self.main(request, self.get_instance(request))
     31
     32    def main(self, request, instance):
     33        Form = self.get_form(request)
     34        if request.POST:
     35            form = Form(request.POST, request.FILES, instance=instance)
     36            if form.is_valid():
     37                new_object = self.save(request, form)
     38                return self.on_success(request, new_object)
     39        else:
     40            form = Form()
     41        rendered_template = self.get_rendered_template(request, instance, form)
     42        return HttpResponse(rendered_template)
     43
     44    def get_form(self, request):
     45        """
     46        Returns a ``ModelForm`` class to be used in this view.
     47        """
     48        # TODO: we should be able to construct a ModelForm without creating
     49        # and passing in a temporary inner class
     50        class Meta:
     51            model = self.model
     52        class_name = self.model.__name__ + 'Form'
     53        return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
     54
     55    def get_context(self, request, instance, form=None):
     56        """
     57        Returns a ``Context`` instance to be used when rendering this view.
     58        """
     59        return RequestContext(request, {'form': form, 'object': instance})
     60
     61    def get_template(self, request):
     62        """
     63        Returns the template to be used when rendering this view. Those who
     64        wish to use a custom template loader should do so here.
     65        """
     66        opts = self.model._meta
     67        template_name = "%s/%s_form.html" % (opts.app_label, opts.object_name.lower())
     68        return loader.get_template(template_name)
     69
     70    def get_rendered_template(self, request, instance, form=None):
     71        """
     72        Returns a rendered template. This will be passed as the sole argument
     73        to HttpResponse()
     74        """
     75        template = self.get_template(request)
     76        context = self.get_context(request, instance, form)
     77        return template.render(context)
     78
     79    def save(self, request, form):
     80        """
     81        Saves the object represented by the given ``form``. This method will
     82        only be called if the form is valid, and should in most cases return
     83        an HttpResponseRediect. It's return value will be the return value
     84        for the view on success.
     85        """
     86        return form.save()
     87
     88    def on_success(self, request, new_object):
     89        """
     90        Returns an HttpResonse, generally an HttpResponse redirect. This will
     91        be the final return value of the view and will only be called if the
     92        object was saved successfuly.
     93        """
     94        if request.user.is_authenticated():
     95            message = self.get_message(request, new_object)
     96            request.user.message_set.create(message=message)
     97        # Redirect to the new object: first by trying post_save_redirect,
     98        # then by obj.get_absolute_url; fail if neither works.
     99        if self.post_save_redirect:
     100            return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
     101        elif hasattr(new_object, 'get_absolute_url'):
     102            return HttpResponseRedirect(new_object.get_absolute_url())
     103        else:
     104            raise ImproperlyConfigured("No URL to redirect to from generic create view.")
     105
     106class AddView(BaseView):
     107    def get_instance(self, request):
     108        """
     109        Returns the object instance to create.
     110        """
     111        return self.model()
     112
     113    def get_message(self, request, new_object):
     114        return ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": self.model._meta.verbose_name}
     115
     116class ChangeView(BaseView):
     117    def __init__(self, model, post_save_redirect=None, slug_field='slug'):
     118        self.slug_field = slug_field
     119        super(ChangeView, self).__init__(model, post_save_redirect=post_save_redirect)
     120
     121    def __call__(self, request, object_id=None, slug=None):
     122        return self.main(request, self.get_instance(request, object_id, slug))
     123
     124    def get_query_set(self, request):
     125        """
     126        Returns a queryset to use when trying to look up the object to change.
     127        """
     128        return self.model._default_manager.get_query_set()
     129
     130    def get_instance(self, request, object_id=None, slug=None):
     131        """
     132        Returns the object to be changed, or raises a 404 if it doesn't exist.
     133        """
     134        # Look up the object to be edited
     135        lookup_kwargs = {}
     136        if object_id:
     137            lookup_kwargs['pk'] = object_id
     138        elif slug and self.slug_field:
     139            lookup_kwargs['%s__exact' % slug_field] = slug
     140        else:
     141            raise AttributeError("Generic view must be called with either an object_id or a slug/slug_field")
     142        try:
     143            return self.get_query_set(request).get(**lookup_kwargs)
     144        except ObjectDoesNotExist:
     145            raise Http404, "No %s found for %s" % (self.model._meta.verbose_name, lookup_kwargs)
     146
     147    def get_message(self, request, new_object):
     148        return ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": self.model._meta.verbose_name}
     149
     150class DeleteView(ChangeView):
     151    def __init__(self, model, post_save_redirect=None, slug_field='slug'):
     152        self.model = model
     153        self.slug_field = slug_field
     154        self.post_save_redirect = post_save_redirect
     155
     156    def main(self, request, instance):
     157        if request.method == 'POST':
     158            self.delete(instance)
     159            return self.on_success(request, instance)
     160        rendered_template = self.get_rendered_template(request, instance)
     161        response = HttpResponse(rendered_template)
     162        populate_xheaders(request, response, self.model, instance.pk)
     163        return response
     164
     165    def get_context(self, request, instance, form=None):
     166        """
     167        Returns a ``Context`` instance to be used when rendering this view.
     168        """
     169        return RequestContext(request, {'object': instance})
     170
     171    def get_template(self, request):
     172        opts = self.model._meta
     173        template_name = "%s/%s_confirm_delete.html" % (opts.app_label, opts.object_name.lower())
     174        return loader.get_template(template_name)
     175
     176    def delete(request, instance):
     177        """
     178        Deletes the given instance. Subclasses that wish to veto deletion
     179        should do so here.
     180        """
     181        instance.delete()
     182
     183    def on_success(self, request, new_object):
     184        """
     185        Redirects to self.post_save_redirect after setting a message if the
     186        user is logged in.
     187       
     188        This method is only called if saving the object was successful.
     189        """
     190        if request.user.is_authenticated():
     191            message = self.get_message(request, new_object)
     192            request.user.message_set.create(message=message)
     193        return HttpResponseRedirect(self.post_save_redirect)
     194
     195    def get_message(self, request, new_object):
     196        return ugettext("The %(verbose_name)s was deleted.") % {"verbose_name": model._meta.verbose_name}
     197
     198
     199# Classic generic views ######################################################
    10200
    11201def create_object(request, model, template_name=None,
    12202        template_loader=loader, extra_context=None, post_save_redirect=None,
  • docs/authentication.txt

    diff --git a/docs/authentication.txt b/docs/authentication.txt
    index ade2d71..9167458 100644
    a b with that ``User``.  
    309309
    310310For more information, see `Chapter 12 of the Django book`_.
    311311
    312 .. _Chapter 12 of the Django book: http://www.djangobook.com/en/beta/chapter12/#cn226
     312.. _Chapter 12 of the Django book: http://www.djangobook.com/en/1.0/chapter12/#cn222
    313313
    314314Authentication in Web requests
    315315==============================
  • docs/i18n.txt

    diff --git a/docs/i18n.txt b/docs/i18n.txt
    index bb6cf74..8da19cd 100644
    a b following this algorithm:  
    547547
    548548    * First, it looks for a ``django_language`` key in the the current user's
    549549      `session`_.
    550     * Failing that, it looks for a cookie that is named according to your ``LANGUAGE_COOKIE_NAME`` setting (the default name is: ``django_language``).
     550    * Failing that, it looks for a cookie that is named according to your ``LANGUAGE_COOKIE_NAME`` setting. (The default name is ``django_language``, and this setting is new in the Django development version. In Django version 0.96 and before, the cookie's name is hard-coded to ``django_language``.)
    551551    * Failing that, it looks at the ``Accept-Language`` HTTP header. This
    552552      header is sent by your browser and tells the server which language(s) you
    553553      prefer, in order by priority. Django tries each language in the header
    Activate this view by adding the following line to your URLconf::  
    719719The view expects to be called via the ``POST`` method, with a ``language``
    720720parameter set in request. If session support is enabled, the view
    721721saves the language choice in the user's session. Otherwise, it saves the
    722 language choice in a cookie that is by default named ``django_language``
    723 (the name can be changed through the ``LANGUAGE_COOKIE_NAME`` setting).
     722language choice in a cookie that is by default named ``django_language``.
     723(The name can be changed through the ``LANGUAGE_COOKIE_NAME`` setting if you're
     724using the Django development version.)
    724725
    725726After setting the language choice, Django redirects the user, following this
    726727algorithm:
  • docs/model-api.txt

    diff --git a/docs/model-api.txt b/docs/model-api.txt
    index 66fa63e..4901a9a 100644
    a b Note, however, that this only refers to models in the same models.py file -- you  
    788788cannot use a string to reference a model defined in another application or
    789789imported from elsewhere.
    790790
    791 **New in Django development version:** to refer to models defined in another
    792 application, you must instead explicitially specify the application label. That
    793 is, if the ``Manufacturer`` model above is defined in another application called
    794 ``production``, you'd need to use::
     791**New in Django development version:** To refer to models defined in another
     792application, you must instead explicitly specify the application label. For
     793example, if the ``Manufacturer`` model above is defined in another application
     794called ``production``, you'd need to use::
    795795
    796796    class Car(models.Model):
    797797        manufacturer = models.ForeignKey('production.Manufacturer')
    See the `One-to-one relationship model example`_ for a full example.  
    10221022Custom field types
    10231023------------------
    10241024
     1025**New in Django development version**
     1026
    10251027If one of the existing model fields cannot be used to fit your purposes, or if
    10261028you wish to take advantage of some less common database column types, you can
    10271029create your own field class. Full coverage of creating your own fields is
  • docs/settings.txt

    diff --git a/docs/settings.txt b/docs/settings.txt
    index ace893f..77e3c66 100644
    a b in standard language format. For example, U.S. English is ``"en-us"``. See the  
    582582LANGUAGE_COOKIE_NAME
    583583--------------------
    584584
     585**New in Django development version**
     586
    585587Default: ``'django_language'``
    586588
    587589The name of the cookie to use for the language cookie. This can be whatever
    588 you want (but should be different from SESSION_COOKIE_NAME). See the
     590you want (but should be different from ``SESSION_COOKIE_NAME``). See the
    589591`internationalization docs`_ for details.
    590592
    591 
    592593LANGUAGES
    593594---------
    594595
  • tests/regressiontests/views/tests/__init__.py

    diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py
    index 2c8c5b4..f0f6f59 100644
    a b  
    11from defaults import *
    22from i18n import *
    33from static import *
     4from generic.create_update import *
    45from generic.date_based import *
     6 No newline at end of file
  • new file tests/regressiontests/views/tests/generic/create_update.py

    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 0000000..7cdbb9e
    - +  
     1# coding: utf-8
     2from django.test import TestCase
     3from regressiontests.views.models import Article, Author
     4
     5class AddViewTest(TestCase):
     6
     7    def test_initial(self):
     8        response = self.client.get('/views/create_update/add/')
     9        self.assertEqual(response.status_code, 200)
     10        self.assertEqual(response.context['form']._meta.model, Author)
     11        self.assertEqual(response.context['object'].name, "")
     12
     13    def test_submit(self):
     14        response = self.client.post('/views/create_update/add/', {
     15            'name': 'Boris',
     16        })
     17        self.assertEqual(response.status_code, 302)
     18
     19class ChangeViewTest(TestCase):
     20    fixtures = ['testdata.json']
     21
     22    def test_initial(self):
     23        response = self.client.get('/views/create_update/1/change/')
     24        self.assertEqual(response.status_code, 200)
     25        self.assertEqual(response.context['form']._meta.model, Author)
     26        self.assertEqual(response.context['object'].name, "Boris")
     27
     28    def test_submit(self):
     29        response = self.client.post('/views/create_update/1/change/', {
     30            'name': 'Jack Kerouac',
     31        })
     32        self.assertEqual(response.status_code, 302)
     33
     34class DeleteViewTest(TestCase):
     35    fixtures = ['testdata.json']
     36
     37    def test_initial(self):
     38        response = self.client.get('/views/create_update/1/delete/')
     39        self.assertEqual(response.status_code, 200)
     40        self.assertEqual(response.context['object'].name, "Boris")
     41
     42    def test_submit(self):
     43        response = self.client.post('/views/create_update/1/delete/', {})
     44        self.assertEqual(response.status_code, 302)
  • tests/regressiontests/views/urls.py

    diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py
    index 5ef0c51..ebd5721 100644
    a b  
    11from os import path
    22
    33from django.conf.urls.defaults import *
     4from django.views.generic.create_update import AddView, ChangeView, DeleteView
    45
    56from models import *
    67import views
    base_dir = path.dirname(path.abspath(__file__))  
    910media_dir = path.join(base_dir, 'media')
    1011locale_dir = path.join(base_dir, 'locale')
    1112
     13author_add = AddView(Author)
     14author_change = ChangeView(Author)
     15author_delete = DeleteView(Author)
     16
    1217js_info_dict = {
    1318    'domain': 'djangojs',
    1419    'packages': ('regressiontests.views',),
    urlpatterns = patterns('',  
    3439   
    3540    # Static views
    3641    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}),
    37    
    38         # Date-based generic views
     42
     43    # Create/Update generic views
     44    (r'create_update/add/', author_add),
     45    (r'create_update/(?P<object_id>\d+)/change/', author_change),
     46    (r'create_update/(?P<object_id>\d+)/delete/', author_delete),
     47
     48
     49    # Date-based generic views
    3950    (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$',
    4051        'django.views.generic.date_based.object_detail',
    4152        dict(slug_field='slug', **date_based_info_dict)),
  • new file tests/templates/views/author_confirm_delete.html

    diff --git a/tests/templates/views/author_confirm_delete.html b/tests/templates/views/author_confirm_delete.html
    new file mode 100644
    index 0000000..3f8ff55
    - +  
     1This template intentionally left blank
     2 No newline at end of file
  • new file tests/templates/views/author_form.html

    diff --git a/tests/templates/views/author_form.html b/tests/templates/views/author_form.html
    new file mode 100644
    index 0000000..3f8ff55
    - +  
     1This template intentionally left blank
     2 No newline at end of file
Back to Top