Ticket #6735: new-generic-views.2.diff
File new-generic-views.2.diff, 26.9 KB (added by , 17 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 """ 2 FormWizard class -- implements a multi-page form, validating between each 3 step and storing the form's state as HTML hidden fields so that no state is 4 stored on the server side. 5 """ 6 7 from django import newforms as forms 8 from django.conf import settings 9 from django.http import Http404 10 from django.shortcuts import render_to_response 11 from django.template.context import RequestContext 12 import cPickle as pickle 13 import md5 14 15 class 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 7 7 from django.http import Http404, HttpResponse, HttpResponseRedirect 8 8 from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured 9 9 from django.utils.translation import ugettext 10 from django import newforms as forms 11 from django.newforms.models import ModelFormMetaclass, ModelForm 12 13 14 # New-sytle, class based generic views ####################################### 15 16 class 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 106 class 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 116 class 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 150 class 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 ###################################################### 10 200 11 201 def create_object(request, model, template_name=None, 12 202 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``. 309 309 310 310 For more information, see `Chapter 12 of the Django book`_. 311 311 312 .. _Chapter 12 of the Django book: http://www.djangobook.com/en/ beta/chapter12/#cn226312 .. _Chapter 12 of the Django book: http://www.djangobook.com/en/1.0/chapter12/#cn222 313 313 314 314 Authentication in Web requests 315 315 ============================== -
docs/i18n.txt
diff --git a/docs/i18n.txt b/docs/i18n.txt index bb6cf74..8da19cd 100644
a b following this algorithm: 547 547 548 548 * First, it looks for a ``django_language`` key in the the current user's 549 549 `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``.) 551 551 * Failing that, it looks at the ``Accept-Language`` HTTP header. This 552 552 header is sent by your browser and tells the server which language(s) you 553 553 prefer, in order by priority. Django tries each language in the header … … Activate this view by adding the following line to your URLconf:: 719 719 The view expects to be called via the ``POST`` method, with a ``language`` 720 720 parameter set in request. If session support is enabled, the view 721 721 saves 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). 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 if you're 724 using the Django development version.) 724 725 725 726 After setting the language choice, Django redirects the user, following this 726 727 algorithm: -
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 788 788 cannot use a string to reference a model defined in another application or 789 789 imported from elsewhere. 790 790 791 **New in Django development version:** to refer to models defined in another792 application, you must instead explicit ially specify the application label. That793 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 792 application, you must instead explicitly specify the application label. For 793 example, if the ``Manufacturer`` model above is defined in another application 794 called ``production``, you'd need to use:: 795 795 796 796 class Car(models.Model): 797 797 manufacturer = models.ForeignKey('production.Manufacturer') … … See the `One-to-one relationship model example`_ for a full example. 1022 1022 Custom field types 1023 1023 ------------------ 1024 1024 1025 **New in Django development version** 1026 1025 1027 If one of the existing model fields cannot be used to fit your purposes, or if 1026 1028 you wish to take advantage of some less common database column types, you can 1027 1029 create 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 582 582 LANGUAGE_COOKIE_NAME 583 583 -------------------- 584 584 585 **New in Django development version** 586 585 587 Default: ``'django_language'`` 586 588 587 589 The 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 the590 you want (but should be different from ``SESSION_COOKIE_NAME``). See the 589 591 `internationalization docs`_ for details. 590 592 591 592 593 LANGUAGES 593 594 --------- 594 595 -
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 1 1 from defaults import * 2 2 from i18n import * 3 3 from static import * 4 from generic.create_update import * 4 5 from 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 2 from django.test import TestCase 3 from regressiontests.views.models import Article, Author 4 5 class 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 19 class 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 34 class 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 1 1 from os import path 2 2 3 3 from django.conf.urls.defaults import * 4 from django.views.generic.create_update import AddView, ChangeView, DeleteView 4 5 5 6 from models import * 6 7 import views … … base_dir = path.dirname(path.abspath(__file__)) 9 10 media_dir = path.join(base_dir, 'media') 10 11 locale_dir = path.join(base_dir, 'locale') 11 12 13 author_add = AddView(Author) 14 author_change = ChangeView(Author) 15 author_delete = DeleteView(Author) 16 12 17 js_info_dict = { 13 18 'domain': 'djangojs', 14 19 'packages': ('regressiontests.views',), … … urlpatterns = patterns('', 34 39 35 40 # Static views 36 41 (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 39 50 (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$', 40 51 'django.views.generic.date_based.object_detail', 41 52 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
- + 1 This 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
- + 1 This template intentionally left blank 2 No newline at end of file