Code

Ticket #6735: generic-views.2.diff

File generic-views.2.diff, 21.3 KB (added by jkocherhans, 6 years ago)
Line 
1diff --git a/django/views/generic/base.py b/django/views/generic/base.py
2new file mode 100644
3index 0000000..ab90d91
4--- /dev/null
5+++ b/django/views/generic/base.py
6@@ -0,0 +1,55 @@
7+from django.http import Http404, HttpResponse
8+from django.core.exceptions import ObjectDoesNotExist
9+from django.template import RequestContext
10+
11+class BaseView(object):
12+    """
13+    Base class for creating class based view objects.
14+    """
15+    def __init__(self, queryset):
16+        self.queryset = queryset
17+        self.model = queryset.model
18+
19+    def get_query_set(self, request):
20+        """
21+        Hook to provide a custom queryset based on the request.
22+        """
23+        return self.queryset._clone()
24+
25+    def get_template(self, request, obj=None):
26+        """
27+        Returns a template object used to render this view.
28+        """
29+        raise NotImplementedError(u'Views must implement their own get_template method.')
30+
31+    def render_response(self, request, template, context_vars, mimetype=None):
32+        """
33+        Returns an HttpResponse for the given request, template object,
34+        dictionary of context variables, and optional mimetype.
35+        """
36+        context = RequestContext(request, context_vars)
37+        template = template.render(context)
38+        return HttpResponse(template, mimetype=mimetype)
39+
40+class BaseDetailView(BaseView):
41+    def __init__(self, queryset, slug_field='slug'):
42+        self.slug_field = slug_field
43+        super(BaseDetailView, self).__init__(queryset)
44+
45+    def get_object(self, request, object_pk=None, slug=None):
46+        """
47+        Returns the object to be viewed, changed or deleted, or raises a 404
48+        if it doesn't exist.
49+        """
50+        # Look up the object to be changed or deleted
51+        lookup_kwargs = {}
52+        if object_pk:
53+            lookup_kwargs['pk'] = object_pk
54+        elif slug and self.slug_field:
55+            lookup_kwargs[self.slug_field] = slug
56+        else:
57+            raise AttributeError("Generic view must be called with either an object_pk or a slug/slug_field")
58+        try:
59+            return self.get_query_set(request).get(**lookup_kwargs)
60+        except ObjectDoesNotExist:
61+            raise Http404, "No %s found for %s" % (self.model._meta.verbose_name, lookup_kwargs)
62diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py
63index 46e92fe..fc88f13 100644
64--- a/django/views/generic/create_update.py
65+++ b/django/views/generic/create_update.py
66@@ -7,6 +7,140 @@ from django.template import RequestContext
67 from django.http import Http404, HttpResponse, HttpResponseRedirect
68 from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
69 from django.utils.translation import ugettext
70+from django.newforms.models import ModelFormMetaclass, ModelForm
71+from django.views.generic.base import BaseDetailView
72+
73+class EditView(BaseDetailView):
74+    """
75+    Create/Update generic view.
76+
77+    Templates: ``<app_label>/<model_name>_form.html``
78+    Context:
79+        form
80+            the ``ModelForm`` instance for the object
81+        object
82+            the ``Model`` instance being changed
83+    """
84+    def __init__(self, queryset, slug_field='slug', post_save_redirect=None):
85+        self.post_save_redirect = post_save_redirect
86+        super(EditView, self).__init__(queryset, slug_field)
87+
88+    def __call__(self, request, object_pk=None, slug=None):
89+        # If we didn't get object_pk or slug, assume this is an add view.
90+        if object_pk is None and slug is None:
91+            obj = None
92+        else:
93+            obj = self.get_object(request, object_pk, slug)
94+        Form = self.get_form(request)
95+        if request.POST:
96+            form = Form(request.POST, request.FILES, instance=obj)
97+            if form.is_valid():
98+                new_obj = self.save_form(request, form)
99+                return self.on_success(request, obj, new_obj)
100+        else:
101+            form = Form()
102+        context_vars = {'object': obj, 'form': form}
103+        template = self.get_template(request, obj)
104+        return self.render_response(request, template, context_vars)
105+
106+    def get_form(self, request):
107+        """
108+        Returns a ``ModelForm`` class to be used in this view.
109+        """
110+        # TODO: we should be able to construct a ModelForm without creating
111+        # and passing in a temporary inner class
112+        class Meta:
113+            model = self.model
114+        class_name = self.model.__name__ + 'Form'
115+        return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
116+
117+    def get_template(self, request, obj=None):
118+        """
119+        Returns the template to be used when rendering this view. Those who
120+        wish to use a custom template loader should do so here.
121+        """
122+        opts = self.model._meta
123+        template_name = "%s/%s_form.html" % (opts.app_label, opts.object_name.lower())
124+        return loader.get_template(template_name)
125+
126+    def get_message(self, request, obj, new_obj):
127+        # If the primary ke of the original object is None, we just created an
128+        # object, otherwise, we updated one.
129+        if obj.pk is None:
130+            return ugettext("The %s was created successfully.") % self.model._meta.verbose_name
131+        return ugettext("The %s was updated successfully.") % self.model._meta.verbose_name
132+
133+    def save_form(self, request, form):
134+        """
135+        Saves and returns the object represented by the given form. This
136+        method will only be called if the form is valid.
137+        """
138+        return form.save()
139+
140+    def on_success(self, request, obj, new_obj):
141+        """
142+        Returns an HttpResonse, generally an HttpResponse redirect. This will
143+        be the final return value of the view and will only be called if the
144+        object was saved successfuly.
145+        """
146+        # We can only do messaging for authenticated users right now
147+        if request.user.is_authenticated():
148+            message = self.get_message(request, new_obj)
149+            request.user.message_set.create(message=message)
150+        # Redirect to the new object: first by trying post_save_redirect,
151+        # then by obj.get_absolute_url; fail if neither works.
152+        if self.post_save_redirect:
153+            return HttpResponseRedirect(self.post_save_redirect % new_obj.__dict__)
154+        elif hasattr(new_obj, 'get_absolute_url'):
155+            return HttpResponseRedirect(new_obj.get_absolute_url())
156+        else:
157+            raise ImproperlyConfigured("No URL to redirect to from generic create view.")
158+
159+class DeleteView(BaseDetailView):
160+    def __init__(self, queryset, slug_field='slug', post_save_redirect=None):
161+        self.post_save_redirect = post_save_redirect
162+        super(DeleteView, self).__init__(queryset, slug_field)
163+
164+    def __call__(self, request, object_pk=None, slug=None):
165+        obj = self.get_object(request, object_pk, slug)
166+        if request.method == 'POST':
167+            self.delete(obj)
168+            return self.on_success(request, obj)
169+        context_vars = {'object': obj}
170+        template = self.get_template(request, obj)
171+        response = self.render_response(request, template, context_vars)
172+        populate_xheaders(request, response, self.model, obj.pk)
173+        return response
174+
175+    def get_template(self, request, obj=None):
176+        opts = self.model._meta
177+        template_name = "%s/%s_confirm_delete.html" % (opts.app_label, opts.object_name.lower())
178+        return loader.get_template(template_name)
179+
180+    def delete(request, obj):
181+        """
182+        Deletes the given instance. Subclasses that wish to veto deletion
183+        should do so here.
184+        """
185+        obj.delete()
186+
187+    def on_success(self, request, obj):
188+        """
189+        Redirects to self.post_save_redirect after setting a message if the
190+        user is logged in.
191+       
192+        This method is only called if saving the object was successful.
193+        """
194+        if request.user.is_authenticated():
195+            message = self.get_message(request, obj)
196+            request.user.message_set.create(message=message)
197+        return HttpResponseRedirect(self.post_save_redirect)
198+
199+    def get_message(self, request, new_object):
200+        return ugettext("The %s was deleted.") % self.model._meta.verbose_name
201+
202+
203+# Classic generic views ######################################################
204 
205 def create_object(request, model, template_name=None,
206         template_loader=loader, extra_context=None, post_save_redirect=None,
207diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py
208index cb9b014..2029a8a 100644
209--- a/django/views/generic/list_detail.py
210+++ b/django/views/generic/list_detail.py
211@@ -3,6 +3,104 @@ from django.http import Http404, HttpResponse
212 from django.core.xheaders import populate_xheaders
213 from django.core.paginator import QuerySetPaginator, InvalidPage
214 from django.core.exceptions import ObjectDoesNotExist
215+from django.views.generic.base import BaseView, BaseDetailView
216+
217+class ObjectList(BaseView):
218+    """
219+    Generic list of objects.
220+
221+    Templates: ``<app_label>/<model_name>_list.html``
222+    Context:
223+        object_list
224+            list of objects
225+        paginator
226+            a ``QuerySetPaginator``object if pagination is enabled, None otherwise
227+        page_obj
228+            a ``Page`` object if pagination is enabled, None otherwise
229+    """
230+    def __init__(self, queryset, paginate_by=None, allow_empty=True):
231+        self.paginate_by = paginate_by
232+        self.allow_empty = allow_empty
233+        super(ObjectList, self).__init__(queryset)
234+
235+    def __call__(self, request, page=None):
236+        queryset = self.get_query_set(request)
237+        if self.paginate_by:
238+            paginator = QuerySetPaginator(queryset, self.paginate_by,
239+                                          allow_empty_first_page=self.allow_empty)
240+            if not page:
241+                page = request.GET.get('page', 1)
242+            try:
243+                page_number = int(page)
244+            except ValueError:
245+                if page == 'last':
246+                    page_number = paginator.num_pages
247+                else:
248+                    # Page is not 'last', nor can it be converted to an int.
249+                    raise Http404
250+            try:
251+                page_obj = paginator.page(page_number)
252+            except InvalidPage:
253+                raise Http404
254+            object_list = page_obj.object_list
255+        else:
256+            object_list = queryset
257+            paginator = None
258+            page_obj = None
259+            if not self.allow_empty and len(queryset) == 0:
260+                raise Http404
261+        context_vars = {
262+            'object_list': object_list,
263+            'paginator': paginator,
264+            'page_obj': page_obj
265+        }
266+        template = self.get_template(request)
267+        return self.render_response(request, template, context_vars)
268+
269+    def get_template(self, request, obj=None):
270+        """
271+        Returns the template to be used when rendering this view. Those who
272+        wish to use a custom template loader should do so here.
273+        """
274+        opts = self.queryset.model._meta
275+        template_name = "%s/%s_list.html" % (opts.app_label, opts.object_name.lower())
276+        return loader.get_template(template_name)
277+
278+class ObjectDetail(BaseDetailView):
279+    """
280+    Generic detail of an object.
281+
282+    Templates: ``<app_label>/<model_name>_detail.html``
283+    Context:
284+        object
285+            the object
286+    """
287+    def __init__(self, queryset, slug_field='slug'):
288+        super(ObjectDetail, self).__init__(queryset, slug_field)
289+
290+    def __call__(self, request, object_pk=None, slug=None):
291+        queryset = self.get_query_set(request)
292+        opts = queryset.model._meta
293+        try:
294+            obj = self.get_object(request, object_pk, slug)
295+        except ObjectDoesNotExist:
296+            raise Http404(u"No %s found matching the query" % opts.verbose_name)
297+        context_vars = {'object': obj}
298+        template = self.get_template(request, obj)
299+        response = self.render_response(request, template, context_vars)
300+        populate_xheaders(request, response, queryset.model, getattr(obj, opts.pk.name))
301+        return response
302+
303+    def get_template(self, request, obj=None):
304+        """
305+        Returns the template to be used when rendering this view. Those who
306+        wish to use a custom template loader should do so here.
307+        """
308+        opts = self.queryset.model._meta
309+        template_name = "%s/%s_detail.html" % (opts.app_label, opts.object_name.lower())
310+        return loader.get_template(template_name)
311+
312+# Legacy generic views #######################################################
313 
314 def object_list(request, queryset, paginate_by=None, page=None,
315         allow_empty=True, template_name=None, template_loader=loader,
316diff --git a/tests/regressiontests/views/models.py b/tests/regressiontests/views/models.py
317index 4bed1f3..472ac2b 100644
318--- a/tests/regressiontests/views/models.py
319+++ b/tests/regressiontests/views/models.py
320@@ -20,7 +20,6 @@ class Article(models.Model):
321     slug = models.SlugField()
322     author = models.ForeignKey(Author)
323     date_created = models.DateTimeField()
324-   
325+
326     def __unicode__(self):
327         return self.title
328-
329diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py
330index 2c8c5b4..3def6a9 100644
331--- a/tests/regressiontests/views/tests/__init__.py
332+++ b/tests/regressiontests/views/tests/__init__.py
333@@ -1,4 +1,6 @@
334 from defaults import *
335 from i18n import *
336 from static import *
337-from generic.date_based import *
338\ No newline at end of file
339+from generic.list_detail import *
340+from generic.date_based import *
341+from generic.create_update import *
342diff --git a/tests/regressiontests/views/tests/generic/create_update.py b/tests/regressiontests/views/tests/generic/create_update.py
343new file mode 100644
344index 0000000..7c59eb4
345--- /dev/null
346+++ b/tests/regressiontests/views/tests/generic/create_update.py
347@@ -0,0 +1,81 @@
348+# coding: utf-8
349+from django.test import TestCase
350+from regressiontests.views.models import Article, Author
351+
352+class AddViewTest(TestCase):
353+    fixtures = ['testdata.json']
354+
355+    def test_initial(self):
356+        response = self.client.get('/views/create_update/article/add/')
357+        self.assertEqual(response.status_code, 200)
358+        self.assertEqual(response.context['form']._meta.model, Article)
359+        self.assertEqual(response.context['object'], None)
360+
361+    def test_submit(self):
362+        response = self.client.post('/views/create_update/article/add/', {
363+            'author': '1',
364+            'title': "Don't read this",
365+            'slug': 'dont-read-this',
366+            'date_created': '2001-01-01 21:22:23'
367+        })
368+        self.assertEqual(response.status_code, 302)
369+
370+class ChangeViewByIdTest(TestCase):
371+    fixtures = ['testdata.json']
372+
373+    def test_initial(self):
374+        response = self.client.get('/views/create_update/article/1/change/')
375+        self.assertEqual(response.status_code, 200)
376+        self.assertEqual(response.context['form']._meta.model, Article)
377+        self.assertEqual(response.context['object'].title, u'Old Article')
378+
379+    def test_submit(self):
380+        response = self.client.post('/views/create_update/article/1/change/', {
381+            'author': '1',
382+            'title': 'Ta Da!',
383+            'slug': 'ta-da',
384+            'date_created': '2001-01-01 21:22:23'
385+        })
386+        self.assertEqual(response.status_code, 302)
387+
388+class ChangeViewBySlugTest(TestCase):
389+    fixtures = ['testdata.json']
390+
391+    def test_initial(self):
392+        response = self.client.get('/views/create_update/article/old_article/change/')
393+        self.assertEqual(response.status_code, 200)
394+        self.assertEqual(response.context['form']._meta.model, Article)
395+        self.assertEqual(response.context['object'].title, u'Old Article')
396+
397+    def test_submit(self):
398+        response = self.client.post('/views/create_update/article/old_article/change/', {
399+            'author': '1',
400+            'title': 'Ta Da!',
401+            'slug': 'ta-da',
402+            'date_created': '2001-01-01 21:22:23'
403+        })
404+        self.assertEqual(response.status_code, 302)
405+
406+class DeleteViewByIdTest(TestCase):
407+    fixtures = ['testdata.json']
408+
409+    def test_initial(self):
410+        response = self.client.get('/views/create_update/article/1/delete/')
411+        self.assertEqual(response.status_code, 200)
412+        self.assertEqual(response.context['object'].title, u'Old Article')
413+
414+    def test_submit(self):
415+        response = self.client.post('/views/create_update/article/1/delete/', {})
416+        self.assertEqual(response.status_code, 302)
417+
418+class DeleteViewBySlugTest(TestCase):
419+    fixtures = ['testdata.json']
420+
421+    def test_initial(self):
422+        response = self.client.get('/views/create_update/article/old_article/delete/')
423+        self.assertEqual(response.status_code, 200)
424+        self.assertEqual(response.context['object'].title, u'Old Article')
425+
426+    def test_submit(self):
427+        response = self.client.post('/views/create_update/article/old_article/delete/', {})
428+        self.assertEqual(response.status_code, 302)
429diff --git a/tests/regressiontests/views/tests/generic/list_detail.py b/tests/regressiontests/views/tests/generic/list_detail.py
430new file mode 100644
431index 0000000..de9d50e
432--- /dev/null
433+++ b/tests/regressiontests/views/tests/generic/list_detail.py
434@@ -0,0 +1,29 @@
435+# coding: utf-8
436+from django.test import TestCase
437+from regressiontests.views.models import Article, Author
438+
439+class ObjectListViewTest(TestCase):
440+    fixtures = ['testdata.json']
441+
442+    def test_basic(self):
443+        response = self.client.get('/views/articles/')
444+        self.assertEqual(response.status_code, 200)
445+        self.assertEqual(response.template.name, 'views/article_list.html')
446+        self.assertEqual(len(response.context['object_list']), 3)
447+        self.assertEqual(response.context['paginator'], None)
448+        self.assertEqual(response.context['page_obj'], None)
449+
450+class ObjectDetailViewTest(TestCase):
451+    fixtures = ['testdata.json']
452+
453+    def test_by_id(self):
454+        response = self.client.get('/views/articles/1/')
455+        self.assertEqual(response.status_code, 200)
456+        self.assertEqual(response.template.name, 'views/article_detail.html')
457+        self.assertEqual(response.context['object'].title, u'Old Article')
458+
459+    def test_by_slug(self):
460+        response = self.client.get('/views/articles/old_article/')
461+        self.assertEqual(response.status_code, 200)
462+        self.assertEqual(response.template.name, 'views/article_detail.html')
463+        self.assertEqual(response.context['object'].title, u'Old Article')
464diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py
465index 5ef0c51..5c3325e 100644
466--- a/tests/regressiontests/views/urls.py
467+++ b/tests/regressiontests/views/urls.py
468@@ -1,6 +1,8 @@
469 from os import path
470 
471 from django.conf.urls.defaults import *
472+from django.views.generic.list_detail import ObjectList, ObjectDetail
473+from django.views.generic.create_update import EditView, DeleteView
474 
475 from models import *
476 import views
477@@ -9,6 +11,15 @@ base_dir = path.dirname(path.abspath(__file__))
478 media_dir = path.join(base_dir, 'media')
479 locale_dir = path.join(base_dir, 'locale')
480 
481+# List/Detail views
482+article_list = ObjectList(Article.objects.all())
483+article_detail = ObjectDetail(Article.objects.all())
484+
485+# Create/Update/Delete Views
486+article_add = article_change = EditView(Article.objects.all(), post_save_redirect='../')
487+article_delete = DeleteView(Article.objects.all())
488+
489+
490 js_info_dict = {
491     'domain': 'djangojs',
492     'packages': ('regressiontests.views',),
493@@ -34,8 +45,20 @@ urlpatterns = patterns('',
494     
495     # Static views
496     (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}),
497-   
498-       # Date-based generic views
499+
500+    # Create/Update generic views
501+    (r'create_update/article/add/$', article_add),
502+    (r'create_update/article/(?P<object_pk>\d+)/change/$', article_change),
503+    (r'create_update/article/(?P<slug>\w+)/change/$', article_change),
504+    (r'create_update/article/(?P<object_pk>\d+)/delete/$', article_delete),
505+    (r'create_update/article/(?P<slug>\w+)/delete/$', article_delete),
506+
507+    (r'articles/$', article_list),
508+    (r'articles/(?P<object_pk>\d+)/$', article_detail),
509+    (r'articles/(?P<slug>\w+)/$', article_detail),
510+
511+
512+    # Date-based generic views
513     (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$',
514         'django.views.generic.date_based.object_detail',
515         dict(slug_field='slug', **date_based_info_dict)),
516diff --git a/tests/templates/views/article_confirm_delete.html b/tests/templates/views/article_confirm_delete.html
517new file mode 100644
518index 0000000..3f8ff55
519--- /dev/null
520+++ b/tests/templates/views/article_confirm_delete.html
521@@ -0,0 +1 @@
522+This template intentionally left blank
523\ No newline at end of file
524diff --git a/tests/templates/views/article_form.html b/tests/templates/views/article_form.html
525new file mode 100644
526index 0000000..3f8ff55
527--- /dev/null
528+++ b/tests/templates/views/article_form.html
529@@ -0,0 +1 @@
530+This template intentionally left blank
531\ No newline at end of file
532diff --git a/tests/templates/views/article_list.html b/tests/templates/views/article_list.html
533new file mode 100644
534index 0000000..3f8ff55
535--- /dev/null
536+++ b/tests/templates/views/article_list.html
537@@ -0,0 +1 @@
538+This template intentionally left blank
539\ No newline at end of file