Ticket #3639: 3639.diff
File 3639.diff, 24.3 KB (added by , 16 years ago) |
---|
-
tests/regressiontests/views/tests/generic/create_update.py
=== added file 'tests/regressiontests/views/tests/generic/create_update.py'
1 import datetime 2 3 from django.test import TestCase 4 from regressiontests.views.models import Article, Author 5 6 class CreateObjectTest(TestCase): 7 fixtures = ['testdata.json'] 8 9 def test_not_logged_in(self): 10 """Verifies the user is logged in through the login_required kwarg""" 11 view_url = '/views/create_update/member/create/article/' 12 response = self.client.get(view_url) 13 self.assertRedirects(response, 'http://testserver/accounts/login/?next=%s' % view_url) 14 15 def test_create_article_display_page(self): 16 """Ensures the generic view returned the page and contains a form.""" 17 view_url = '/views/create_update/create/article/' 18 response = self.client.get(view_url) 19 self.assertEqual(response.status_code, 200) 20 if not response.context.get('form'): 21 self.fail('No form found in the response.') 22 23 def test_create_article_with_errors(self): 24 """POSTs a form that contain validation errors.""" 25 view_url = '/views/create_update/create/article/' 26 response = self.client.post(view_url, { 27 'title': 'My First Article', 28 }) 29 self.assertFormError(response, 'form', 'slug', [u'This field is required.']) 30 31 def test_create_article(self): 32 """Creates a new article through the generic view and ensures it gets 33 redirected to the correct URL defined by post_save_redirect""" 34 view_url = '/views/create_update/create/article/' 35 response = self.client.post(view_url, { 36 'title': 'My First Article', 37 'slug': 'my-first-article', 38 'author': '1', 39 'date_created': datetime.datetime(2007, 6, 25), 40 }) 41 self.assertRedirects(response, 42 'http://testserver/views/create_update/view/article/my-first-article/', 43 target_status_code=404) 44 45 def test_create_custom_save_article(self): 46 """ 47 Creates a new article with a save method override to adjust the slug 48 before committing to the database. 49 """ 50 view_url = '/views/create_update/create_custom/article/' 51 response = self.client.post(view_url, { 52 'title': 'Test Article', 53 'slug': 'this-should-get-replaced', 54 'author': 1, 55 'date_created': datetime.datetime(2007, 6, 25), 56 }) 57 self.assertRedirects(response, 58 'http://testserver/views/create_update/view/article/some-other-slug/', 59 target_status_code=404) 60 61 class UpdateDeleteObjectTest(TestCase): 62 fixtures = ['testdata.json'] 63 64 def test_update_object_form_display(self): 65 """Verifies that the form was created properly and with initial values.""" 66 response = self.client.get('/views/create_update/update/article/old_article/') 67 self.assertEquals(unicode(response.context['form']['title']), 68 u'<input id="id_title" type="text" name="title" value="Old Article" maxlength="100" />') 69 70 def test_update_object(self): 71 """Verifies the form POST data and performs a redirect to 72 post_save_redirect""" 73 response = self.client.post('/views/create_update/update/article/old_article/', { 74 'title': 'Another Article', 75 'slug': 'another-article-slug', 76 'author': 1, 77 'date_created': datetime.datetime(2007, 6, 25), 78 }) 79 self.assertRedirects(response, 80 'http://testserver/views/create_update/view/article/another-article-slug/', 81 target_status_code=404) 82 article = Article.objects.get(pk=1) 83 self.assertEquals(article.title, "Another Article") 84 85 def test_delete_object_confirm(self): 86 """Verifies the confirm deletion page is displayed using a GET.""" 87 response = self.client.get('/views/create_update/delete/article/old_article/') 88 self.assertTemplateUsed(response, 'views/article_confirm_delete.html') 89 90 def test_delete_object_redirect(self): 91 """Verifies that post_delete_redirect works properly.""" 92 response = self.client.post('/views/create_update/delete/article/old_article/') 93 self.assertRedirects(response, 94 'http://testserver/views/create_update/', 95 target_status_code=404) 96 97 def test_delete_object(self): 98 """Verifies the object actually gets deleted on a POST.""" 99 response = self.client.post('/views/create_update/delete/article/old_article/') 100 try: 101 Article.objects.get(slug='old_article') 102 except Article.DoesNotExist: 103 pass 104 else: 105 self.fail('Object was not deleted.') -
tests/templates/views/article_confirm_delete.html
=== added file 'tests/templates/views/article_confirm_delete.html'
1 This template intentionally left blank 2 No newline at end of file -
tests/templates/views/article_form.html
=== added file 'tests/templates/views/article_form.html'
1 This template intentionally left blank 2 No newline at end of file -
django/views/generic/__init__.py
=== modified file 'django/views/generic/__init__.py'
1 class 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'
1 from django.newforms.models import ModelFormMetaclass, ModelForm 2 from django.template import RequestContext, loader 3 from django.http import Http404, HttpResponse, HttpResponseRedirect 1 4 from django.core.xheaders import populate_xheaders 2 from django.template import loader3 from django import oldforms4 from django.db.models import FileField5 from django.contrib.auth.views import redirect_to_login6 from django.template import RequestContext7 from django.http import Http404, HttpResponse, HttpResponseRedirect8 5 from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured 9 6 from django.utils.translation import ugettext 10 11 def create_object(request, model, template_name=None, 7 from django.contrib.auth.views import redirect_to_login 8 from django.views.generic import GenericViewError 9 10 def deprecate_follow(follow): 11 """ 12 Raises a DeprecationWarning if follow is anything but None. 13 14 The old Manipulator-based forms used an argument named follow, but it is 15 no longer needed for newforms-based forms. 16 """ 17 if follow is not None: 18 raise DeprecationWarning( 19 "Generic views have been changed to use newforms and the 'follow'" 20 " argument is no longer supported. Please update your code to use" 21 " the new form_class argument in order to use a custom form.") 22 23 def get_model_and_form_class(model, form_class): 24 """ 25 Returns a model and form class based on the model and form_class 26 parameters that were passed to the generic view. 27 28 If ``form_class`` is given then its associated model will be returned along 29 with ``form_class`` itself. Otherwise, if ``model`` is given, ``model`` 30 itself will be returned along with a ``ModelForm`` class created from 31 ``model``. 32 """ 33 if form_class: 34 return form_class._meta.model, form_class 35 if model: 36 # The inner Meta class fails if model = model is used for some reason. 37 tmp_model = model 38 # TODO: we should be able to construct a ModelForm without creating 39 # and passing in a temporary inner class. 40 class Meta: 41 model = tmp_model 42 class_name = model.__name__ + 'Form' 43 form_class = ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta}) 44 return model, form_class 45 raise GenericViewError("Generic view must be called with either a model or" 46 " form_class argument.") 47 48 def create_object(request, model=None, template_name=None, 12 49 template_loader=loader, extra_context=None, post_save_redirect=None, 13 login_required=False, follow=None, context_processors=None): 50 login_required=False, follow=None, context_processors=None, 51 form_class=None): 14 52 """ 15 53 Generic object-creation function. 16 54 17 55 Templates: ``<app_label>/<model_name>_form.html`` 18 56 Context: 19 57 form 20 the form wrapperfor the object58 the form for the object 21 59 """ 60 deprecate_follow(follow) 22 61 if extra_context is None: extra_context = {} 23 62 if login_required and not request.user.is_authenticated(): 24 63 return redirect_to_login(request.path) 25 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 64 65 model, form_class = get_model_and_form_class(model, form_class) 66 if request.method == 'POST': 67 form = form_class(request.POST, request.FILES) 68 if form.is_valid(): 69 new_object = form.save() 70 42 71 if request.user.is_authenticated(): 43 72 request.user.message_set.create(message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name}) 44 73 … … 51 80 else: 52 81 raise ImproperlyConfigured("No URL to redirect to from generic create view.") 53 82 else: 54 # No POST, so we want a brand new form without any data or errors 55 errors = {} 56 new_data = manipulator.flatten_data() 83 form = form_class() 57 84 58 # Create the FormWrapper, template, context, response 59 form = oldforms.FormWrapper(manipulator, new_data, errors) 85 # Create the template, context, response 60 86 if not template_name: 61 87 template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) 62 88 t = template_loader.get_template(template_name) … … 70 96 c[key] = value 71 97 return HttpResponse(t.render(c)) 72 98 73 def update_object(request, model , object_id=None, slug=None,99 def update_object(request, model=None, object_id=None, slug=None, 74 100 slug_field='slug', template_name=None, template_loader=loader, 75 101 extra_context=None, post_save_redirect=None, 76 102 login_required=False, follow=None, context_processors=None, 77 template_object_name='object' ):103 template_object_name='object', form_class=None): 78 104 """ 79 105 Generic object-update function. 80 106 81 107 Templates: ``<app_label>/<model_name>_form.html`` 82 108 Context: 83 109 form 84 the form wrapperfor the object110 the form for the object 85 111 object 86 112 the original object being edited 87 113 """ 114 deprecate_follow(follow) 88 115 if extra_context is None: extra_context = {} 89 116 if login_required and not request.user.is_authenticated(): 90 117 return redirect_to_login(request.path) 91 118 119 model, form_class = get_model_and_form_class(model, form_class) 120 92 121 # Look up the object to be edited 93 122 lookup_kwargs = {} 94 123 if object_id: … … 96 125 elif slug and slug_field: 97 126 lookup_kwargs['%s__exact' % slug_field] = slug 98 127 else: 99 raise AttributeError("Generic edit view must be called with either an object_id or a slug/slug_field")128 raise GenericViewError("Generic edit view must be called with either an object_id or a slug/slug_field") 100 129 try: 101 130 object = model.objects.get(**lookup_kwargs) 102 131 except ObjectDoesNotExist: 103 132 raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs) 104 133 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) 134 if request.method == 'POST': 135 form = form_class(request.POST, request.FILES, instance=object) 136 if form.is_valid(): 137 object = form.save() 115 138 116 139 if request.user.is_authenticated(): 117 140 request.user.message_set.create(message=ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": model._meta.verbose_name}) … … 124 147 else: 125 148 raise ImproperlyConfigured("No URL to redirect to from generic create view.") 126 149 else: 127 errors = {} 128 # This makes sure the form acurate represents the fields of the place. 129 new_data = manipulator.flatten_data() 150 form = form_class(instance=object) 130 151 131 form = oldforms.FormWrapper(manipulator, new_data, errors)132 152 if not template_name: 133 153 template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) 134 154 t = template_loader.get_template(template_name) … … 145 165 populate_xheaders(request, response, model, getattr(object, object._meta.pk.attname)) 146 166 return response 147 167 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'):168 def delete_object(request, model, post_delete_redirect, object_id=None, 169 slug=None, slug_field='slug', template_name=None, 170 template_loader=loader, extra_context=None, login_required=False, 171 context_processors=None, template_object_name='object'): 152 172 """ 153 173 Generic object-delete function. 154 174 … … 172 192 elif slug and slug_field: 173 193 lookup_kwargs['%s__exact' % slug_field] = slug 174 194 else: 175 raise AttributeError("Generic delete view must be called with either an object_id or a slug/slug_field")195 raise GenericViewError("Generic delete view must be called with either an object_id or a slug/slug_field") 176 196 try: 177 197 object = model._default_manager.get(**lookup_kwargs) 178 198 except ObjectDoesNotExist: -
docs/generic_views.txt
=== modified file 'docs/generic_views.txt'
701 701 query string parameter (via ``GET``) or a ``page`` variable specified in 702 702 the URLconf. See `Notes on pagination`_ below. 703 703 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. 705 705 See `Notes on pagination`_ below. 706 706 707 707 * ``template_name``: The full name of a template to use in rendering the … … 809 809 810 810 /objects/?page=3 811 811 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`` 814 814 to create a link to every page of results. 815 815 816 816 These values and lists are 1-based, not 0-based, so the first page would be 817 represented as page ``1``. 817 represented as page ``1``. 818 818 819 819 An example of the use of pagination can be found in the `object pagination`_ 820 example model. 821 820 example model. 821 822 822 .. _`object pagination`: ../models/pagination/ 823 823 824 **New in Django development version:** 824 **New in Django development version:** 825 825 826 As a special case, you are also permitted to use 826 As a special case, you are also permitted to use 827 827 ``last`` as a value for ``page``:: 828 828 829 829 /objects/?page=last 830 830 831 This allows you to access the final page of results without first having to 831 This allows you to access the final page of results without first having to 832 832 determine how many pages there are. 833 833 834 834 Note that ``page`` *must* be either a valid page number or the value ``last``; … … 907 907 The ``django.views.generic.create_update`` module contains a set of functions 908 908 for creating, editing and deleting objects. 909 909 910 **New in Django development version:** 911 912 ``django.views.generic.create_update.create_object`` and 913 ``django.views.generic.create_update.update_object`` now use `newforms`_ to 914 build and display the form. 915 916 .. _newforms: ../newforms/ 917 910 918 ``django.views.generic.create_update.create_object`` 911 919 ---------------------------------------------------- 912 920 913 921 **Description:** 914 922 915 923 A page that displays a form for creating an object, redisplaying the form with 916 validation errors (if there are any) and saving the object. This uses the 917 automatic manipulators that come with Django models. 924 validation errors (if there are any) and saving the object. 918 925 919 926 **Required arguments:** 920 927 921 * ``model``: The Django model class of the object that the form will 922 create. 928 * Either ``form_class`` or ``model`` is required. 929 930 If you provide ``form_class``, it should be a 931 ``django.newforms.ModelForm`` subclass. Use this argument when you need 932 to customize the model's form. See the `ModelForm docs`_ for more 933 information. 934 935 Otherwise, ``model`` should be a Django model class and the form used 936 will be a standard ``ModelForm`` for ``model``. 923 937 924 938 **Optional arguments:** 925 939 … … 960 974 961 975 In addition to ``extra_context``, the template's context will be: 962 976 963 * ``form``: A ``django. oldforms.FormWrapper`` instance representing the form964 for editing the object. This lets you refer to form fields easily in the977 * ``form``: A ``django.newforms.ModelForm`` instance representing the form 978 for creating the object. This lets you refer to form fields easily in the 965 979 template system. 966 980 967 For example, if ``model``has two fields, ``name`` and ``address``::981 For example, if the model has two fields, ``name`` and ``address``:: 968 982 969 983 <form action="" method="post"> 970 <p> <label for="id_name">Name:</label>{{ form.name }}</p>971 <p> <label for="id_address">Address:</label>{{ form.address }}</p>984 <p>{{ form.name.label_tag }} {{ form.name }}</p> 985 <p>{{ form.address.label_tag }} {{ form.address }}</p> 972 986 </form> 973 987 974 See the ` manipulator and formfield documentation`_ for more information975 about using ``FormWrapper`` objects in templates.988 See the `newforms documentation`_ for more information about using 989 ``Form`` objects in templates. 976 990 977 991 .. _authentication system: ../authentication/ 978 .. _manipulator and formfield documentation: ../forms/ 992 .. _ModelForm docs: ../newforms/modelforms 993 .. _newforms documentation: ../newforms/ 979 994 980 995 ``django.views.generic.create_update.update_object`` 981 996 ---------------------------------------------------- … … 988 1003 989 1004 **Required arguments:** 990 1005 991 * ``model``: The Django model class of the object that the form will 992 create. 1006 * Either ``form_class`` or ``model`` is required. 1007 1008 If you provide ``form_class``, it should be a 1009 ``django.newforms.ModelForm`` subclass. Use this argument when you need 1010 to customize the model's form. See the `ModelForm docs`_ for more 1011 information. 1012 1013 Otherwise, ``model`` should be a Django model class and the form used 1014 will be a standard ``ModelForm`` for ``model``. 993 1015 994 1016 * Either ``object_id`` or (``slug`` *and* ``slug_field``) is required. 995 1017 … … 1042 1064 1043 1065 In addition to ``extra_context``, the template's context will be: 1044 1066 1045 * ``form``: A ``django. oldforms.FormWrapper`` instance representing the form1067 * ``form``: A ``django.newforms.ModelForm`` instance representing the form 1046 1068 for editing the object. This lets you refer to form fields easily in the 1047 1069 template system. 1048 1070 1049 For example, if ``model``has two fields, ``name`` and ``address``::1071 For example, if the model has two fields, ``name`` and ``address``:: 1050 1072 1051 1073 <form action="" method="post"> 1052 <p> <label for="id_name">Name:</label>{{ form.name }}</p>1053 <p> <label for="id_address">Address:</label>{{ form.address }}</p>1074 <p>{{ form.name.label_tag }} {{ form.name }}</p> 1075 <p>{{ form.address.label_tag }} {{ form.address }}</p> 1054 1076 </form> 1055 1077 1056 See the ` manipulator and formfield documentation`_ for more information1057 about using ``FormWrapper`` objects in templates.1078 See the `newforms documentation`_ for more information about using 1079 ``Form`` objects in templates. 1058 1080 1059 1081 * ``object``: The original object being edited. This variable's name 1060 1082 depends on the ``template_object_name`` parameter, which is ``'object'`` -
tests/regressiontests/views/tests/__init__.py
=== modified file 'tests/regressiontests/views/tests/__init__.py'
1 1 from defaults import * 2 2 from i18n import * 3 3 from static import * 4 from generic.date_based import * 5 No newline at end of file 4 from generic.date_based import * 5 from generic.create_update import * 6 No newline at end of file -
tests/regressiontests/views/urls.py
=== modified file 'tests/regressiontests/views/urls.py'
20 20 'month_format': '%m', 21 21 } 22 22 23 crud_form_info_dict = { 24 'model': Article, 25 } 26 23 27 urlpatterns = patterns('', 24 28 (r'^$', views.index_page), 25 29 … … 44 48 dict(allow_future=True, slug_field='slug', **date_based_info_dict)), 45 49 (r'^date_based/archive_month/(?P<year>\d{4})/(?P<month>\d{1,2})/$', 46 50 'django.views.generic.date_based.archive_month', 47 date_based_info_dict), 51 date_based_info_dict), 52 53 # crud generic views 54 (r'^create_update/member/create/article/$', 'django.views.generic.create_update.create_object', 55 dict(login_required=True, **crud_form_info_dict)), 56 (r'^create_update/create/article/$', 'django.views.generic.create_update.create_object', 57 dict(post_save_redirect='/views/create_update/view/article/%(slug)s/', **crud_form_info_dict)), 58 (r'^create_update/create_custom/article/$', views.custom_create), 59 (r'create_update/update/article/(?P<slug>[-\w]+)/$', 'django.views.generic.create_update.update_object', 60 dict(post_save_redirect='/views/create_update/view/article/%(slug)s/', slug_field='slug', **crud_form_info_dict)), 61 (r'create_update/delete/article/(?P<slug>[-\w]+)/$', 'django.views.generic.create_update.delete_object', 62 dict(post_delete_redirect='/views/create_update/', slug_field='slug', **crud_form_info_dict)), 48 63 ) -
tests/regressiontests/views/views.py
=== modified file 'tests/regressiontests/views/views.py'
1 1 from django.http import HttpResponse 2 import django.newforms as forms 3 from django.views.generic.create_update import create_object 4 5 from models import Article 2 6 3 7 def index_page(request): 4 8 """Dummy index page""" 5 9 return HttpResponse('<html><body>Dummy page</body></html>') 10 11 def custom_create(request): 12 """ 13 Calls create_object generic view with a custom form class. 14 """ 15 class SlugChangingArticleForm(forms.ModelForm): 16 """Custom form class to overwrite the slug.""" 17 18 class Meta: 19 model = Article 20 21 def save(self, *args, **kwargs): 22 self.cleaned_data['slug'] = 'some-other-slug' 23 return super(SlugChangingArticleForm, self).save(*args, **kwargs) 24 25 return create_object(request, 26 post_save_redirect='/views/create_update/view/article/%(slug)s/', 27 form_class=SlugChangingArticleForm)