Ticket #6735: generic-views.diff

File generic-views.diff, 21.6 KB (added by jkocherhans, 16 years ago)

Now list_detail views can play too.

  • new file django/views/generic/base.py

    diff --git a/django/views/generic/base.py b/django/views/generic/base.py
    new file mode 100644
    index 0000000..69880c2
    - +  
     1from django.db.models.base import ModelBase
     2from django.db.models.query import _QuerySet
     3from django.http import Http404, HttpResponse
     4from django.core.exceptions import ObjectDoesNotExist
     5from django.template import RequestContext
     6
     7class BaseView(object):
     8    """
     9    Base class for creating class based view objects.
     10    """
     11    def __init__(self, queryset):
     12        self.queryset = queryset
     13        self.model = queryset.model
     14
     15    def get_query_set(self, request):
     16        """
     17        Hook to provide a custom queryset based on the request.
     18        """
     19        return self.queryset._clone()
     20
     21    def get_template(self, request, obj=None):
     22        """
     23        Returns a template object used to render this view.
     24        """
     25        raise NotImplementedError(u'Views must implement their own get_template method.')
     26
     27    def render_response(self, request, template, context_vars, mimetype=None):
     28        """
     29        Returns an HttpResponse for the given request, template object,
     30        dictionary of context variables, and optional mimetype.
     31        """
     32        context = RequestContext(request, context_vars)
     33        template = template.render(context)
     34        return HttpResponse(template, mimetype=mimetype)
     35
     36class BaseDetailView(BaseView):
     37    def __init__(self, queryset, slug_field='slug'):
     38        self.slug_field = slug_field
     39        super(BaseDetailView, self).__init__(queryset)
     40
     41    def get_object(self, request, object_pk=None, slug=None):
     42        """
     43        Returns the object to be viewed, changed or deleted, or raises a 404
     44        if it doesn't exist.
     45        """
     46        # Look up the object to be changed or deleted
     47        lookup_kwargs = {}
     48        if object_pk:
     49            lookup_kwargs['pk'] = object_pk
     50        elif slug and self.slug_field:
     51            lookup_kwargs[self.slug_field] = slug
     52        else:
     53            raise AttributeError("Generic view must be called with either an object_pk or a slug/slug_field")
     54        try:
     55            return self.get_query_set(request).get(**lookup_kwargs)
     56        except ObjectDoesNotExist:
     57            raise Http404, "No %s found for %s" % (self.model._meta.verbose_name, lookup_kwargs)
  • django/views/generic/create_update.py

    diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py
    index 46e92fe..774792d 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.newforms.models import ModelFormMetaclass, ModelForm
     11from django.views.generic.base import BaseDetailView
     12
     13class EditView(BaseDetailView):
     14    """
     15    Create/Update generic view.
     16
     17    Templates: ``<app_label>/<model_name>_form.html``
     18    Context:
     19        form
     20            the ``ModelForm`` instance for the object
     21        object
     22            the ``Model`` instance being changed
     23    """
     24    def __init__(self, queryset, slug_field='slug', post_save_redirect=None):
     25        self.post_save_redirect = post_save_redirect
     26        super(EditView, self).__init__(queryset, slug_field)
     27
     28    def __call__(self, request, object_pk=None, slug=None):
     29        # If we didn't get object_pk or slug, assume this is an add view.
     30        if object_pk is None and slug is None:
     31            original = None
     32        else:
     33            original = self.get_object(request, object_pk, slug)
     34        Form = self.get_form(request)
     35        if request.POST:
     36            form = Form(request.POST, request.FILES, instance=original)
     37            if form.is_valid():
     38                new_object = self.save_form(request, form)
     39                return self.on_success(request, original, new_object)
     40        else:
     41            form = Form()
     42        context_vars = {'object': original, 'form': form}
     43        template = self.get_template(request, original)
     44        return self.render_response(request, template, context_vars)
     45
     46    def get_form(self, request):
     47        """
     48        Returns a ``ModelForm`` class to be used in this view.
     49        """
     50        # TODO: we should be able to construct a ModelForm without creating
     51        # and passing in a temporary inner class
     52        class Meta:
     53            model = self.model
     54        class_name = self.model.__name__ + 'Form'
     55        return ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta})
     56
     57    def get_template(self, request, obj=None):
     58        """
     59        Returns the template to be used when rendering this view. Those who
     60        wish to use a custom template loader should do so here.
     61        """
     62        opts = self.model._meta
     63        template_name = "%s/%s_form.html" % (opts.app_label, opts.object_name.lower())
     64        return loader.get_template(template_name)
     65
     66    def get_message(self, request, original, new_object):
     67        # If the primary ke of the original object is None, we just created an
     68        # object, otherwise, we updated one.
     69        if original.pk is None:
     70            return ugettext("The %s was created successfully.") % self.model._meta.verbose_name
     71        return ugettext("The %s was updated successfully.") % self.model._meta.verbose_name
     72
     73    def save_form(self, request, form):
     74        """
     75        Saves the object represented by the given ``form``. This method will
     76        only be called if the form is valid, and should in most cases return
     77        an HttpResponseRediect. It's return value will be the return value
     78        for the view on success.
     79        """
     80        return form.save()
     81
     82    def on_success(self, request, original, new_object):
     83        """
     84        Returns an HttpResonse, generally an HttpResponse redirect. This will
     85        be the final return value of the view and will only be called if the
     86        object was saved successfuly.
     87        """
     88        # We can only do messaging for authenticated users right now
     89        if request.user.is_authenticated():
     90            message = self.get_message(request, new_object)
     91            request.user.message_set.create(message=message)
     92        # Redirect to the new object: first by trying post_save_redirect,
     93        # then by obj.get_absolute_url; fail if neither works.
     94        if self.post_save_redirect:
     95            return HttpResponseRedirect(self.post_save_redirect % new_object.__dict__)
     96        elif hasattr(new_object, 'get_absolute_url'):
     97            return HttpResponseRedirect(new_object.get_absolute_url())
     98        else:
     99            raise ImproperlyConfigured("No URL to redirect to from generic create view.")
     100
     101class DeleteView(BaseDetailView):
     102    def __init__(self, queryset, slug_field='slug', post_save_redirect=None):
     103        self.post_save_redirect = post_save_redirect
     104        super(DeleteView, self).__init__(queryset, slug_field)
     105
     106    def __call__(self, request, object_pk=None, slug=None):
     107        original = self.get_object(request, object_pk, slug)
     108        if request.method == 'POST':
     109            self.delete(original)
     110            return self.on_success(request, original)
     111        context_vars = {'object': original}
     112        template = self.get_template(request, original)
     113        response = self.render_response(request, template, context_vars)
     114        populate_xheaders(request, response, self.model, original.pk)
     115        return response
     116
     117    def get_template(self, request, obj=None):
     118        opts = self.model._meta
     119        template_name = "%s/%s_confirm_delete.html" % (opts.app_label, opts.object_name.lower())
     120        return loader.get_template(template_name)
     121
     122    def delete(request, obj):
     123        """
     124        Deletes the given instance. Subclasses that wish to veto deletion
     125        should do so here.
     126        """
     127        obj.delete()
     128
     129    def on_success(self, request, new_object):
     130        """
     131        Redirects to self.post_save_redirect after setting a message if the
     132        user is logged in.
     133       
     134        This method is only called if saving the object was successful.
     135        """
     136        if request.user.is_authenticated():
     137            message = self.get_message(request, new_object)
     138            request.user.message_set.create(message=message)
     139        return HttpResponseRedirect(self.post_save_redirect)
     140
     141    def get_message(self, request, new_object):
     142        return ugettext("The %s was deleted.") % self.model._meta.verbose_name
     143
     144
     145# Classic generic views ######################################################
    10146
    11147def create_object(request, model, template_name=None,
    12148        template_loader=loader, extra_context=None, post_save_redirect=None,
  • django/views/generic/list_detail.py

    diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py
    index cb9b014..9286b55 100644
    a b from django.http import Http404, HttpResponse  
    33from django.core.xheaders import populate_xheaders
    44from django.core.paginator import QuerySetPaginator, InvalidPage
    55from django.core.exceptions import ObjectDoesNotExist
     6from django.views.generic.base import BaseView, BaseDetailView
     7
     8class ObjectList(BaseView):
     9    """
     10    Generic list of objects.
     11
     12    Templates: ``<app_label>/<model_name>_list.html``
     13    Context:
     14        object_list
     15            list of objects
     16        paginator
     17            a ``QuerySetPaginator``object if pagination is enabled, None otherwise
     18        page_obj
     19            a ``Page`` object if pagination is enabled, None otherwise
     20    """
     21    def __init__(self, queryset, paginate_by=None, allow_empty=True):
     22        self.paginate_by = paginate_by
     23        self.allow_empty = allow_empty
     24        super(ObjectList, self).__init__(queryset)
     25
     26    def __call__(self, request):
     27        queryset = self.get_query_set(request)
     28        if self.paginate_by:
     29            paginator = QuerySetPaginator(queryset, paginate_by,
     30                                          allow_empty_first_page=self.allow_empty)
     31            if not page:
     32                page = request.GET.get('page', 1)
     33            try:
     34                page_number = int(page)
     35            except ValueError:
     36                if page == 'last':
     37                    page_number = paginator.num_pages
     38                else:
     39                    # Page is not 'last', nor can it be converted to an int.
     40                    raise Http404
     41            try:
     42                page_obj = paginator.page(page_number)
     43            except InvalidPage:
     44                raise Http404
     45            object_list = page_obj.object_list
     46        else:
     47            object_list = queryset
     48            paginator = None
     49            page_obj = None
     50            if not self.allow_empty and len(queryset) == 0:
     51                raise Http404
     52        context_vars = {
     53            'object_list': object_list,
     54            'paginator': paginator,
     55            'page_obj': page_obj
     56        }
     57        template = self.get_template(request)
     58        return self.render_response(request, template, context_vars)
     59
     60    def get_template(self, request, obj=None):
     61        """
     62        Returns the template to be used when rendering this view. Those who
     63        wish to use a custom template loader should do so here.
     64        """
     65        opts = self.queryset.model._meta
     66        template_name = "%s/%s_list.html" % (opts.app_label, opts.object_name.lower())
     67        return loader.get_template(template_name)
     68
     69class ObjectDetail(BaseDetailView):
     70    """
     71    Generic detail of an object.
     72
     73    Templates: ``<app_label>/<model_name>_detail.html``
     74    Context:
     75        object
     76            the object
     77    """
     78    def __init__(self, queryset, slug_field='slug'):
     79        super(ObjectDetail, self).__init__(queryset, slug_field)
     80
     81    def __call__(self, request, object_pk=None, slug=None):
     82        queryset = self.get_query_set(request)
     83        opts = queryset.model._meta
     84        try:
     85            obj = self.get_object(request, object_pk, slug)
     86        except ObjectDoesNotExist:
     87            raise Http404(u"No %s found matching the query" % opts.verbose_name)
     88        context_vars = {'object': obj}
     89        template = self.get_template(request, obj)
     90        response = self.render_response(request, template, context_vars)
     91        populate_xheaders(request, response, queryset.model, getattr(obj, opts.pk.name))
     92        return response
     93
     94    def get_template(self, request, obj=None):
     95        """
     96        Returns the template to be used when rendering this view. Those who
     97        wish to use a custom template loader should do so here.
     98        """
     99        opts = self.queryset.model._meta
     100        template_name = "%s/%s_detail.html" % (opts.app_label, opts.object_name.lower())
     101        return loader.get_template(template_name)
     102
     103# Legacy generic views #######################################################
    6104
    7105def object_list(request, queryset, paginate_by=None, page=None,
    8106        allow_empty=True, template_name=None, template_loader=loader,
  • tests/regressiontests/views/models.py

    diff --git a/tests/regressiontests/views/models.py b/tests/regressiontests/views/models.py
    index 4bed1f3..472ac2b 100644
    a b class Article(models.Model):  
    2020    slug = models.SlugField()
    2121    author = models.ForeignKey(Author)
    2222    date_created = models.DateTimeField()
    23    
     23
    2424    def __unicode__(self):
    2525        return self.title
    26 
  • tests/regressiontests/views/tests/__init__.py

    diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py
    index 2c8c5b4..3def6a9 100644
    a b  
    11from defaults import *
    22from i18n import *
    33from static import *
    4 from generic.date_based import *
    5  No newline at end of file
     4from generic.list_detail import *
     5from generic.date_based import *
     6from generic.create_update import *
  • 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..7c59eb4
    - +  
     1# coding: utf-8
     2from django.test import TestCase
     3from regressiontests.views.models import Article, Author
     4
     5class AddViewTest(TestCase):
     6    fixtures = ['testdata.json']
     7
     8    def test_initial(self):
     9        response = self.client.get('/views/create_update/article/add/')
     10        self.assertEqual(response.status_code, 200)
     11        self.assertEqual(response.context['form']._meta.model, Article)
     12        self.assertEqual(response.context['object'], None)
     13
     14    def test_submit(self):
     15        response = self.client.post('/views/create_update/article/add/', {
     16            'author': '1',
     17            'title': "Don't read this",
     18            'slug': 'dont-read-this',
     19            'date_created': '2001-01-01 21:22:23'
     20        })
     21        self.assertEqual(response.status_code, 302)
     22
     23class ChangeViewByIdTest(TestCase):
     24    fixtures = ['testdata.json']
     25
     26    def test_initial(self):
     27        response = self.client.get('/views/create_update/article/1/change/')
     28        self.assertEqual(response.status_code, 200)
     29        self.assertEqual(response.context['form']._meta.model, Article)
     30        self.assertEqual(response.context['object'].title, u'Old Article')
     31
     32    def test_submit(self):
     33        response = self.client.post('/views/create_update/article/1/change/', {
     34            'author': '1',
     35            'title': 'Ta Da!',
     36            'slug': 'ta-da',
     37            'date_created': '2001-01-01 21:22:23'
     38        })
     39        self.assertEqual(response.status_code, 302)
     40
     41class ChangeViewBySlugTest(TestCase):
     42    fixtures = ['testdata.json']
     43
     44    def test_initial(self):
     45        response = self.client.get('/views/create_update/article/old_article/change/')
     46        self.assertEqual(response.status_code, 200)
     47        self.assertEqual(response.context['form']._meta.model, Article)
     48        self.assertEqual(response.context['object'].title, u'Old Article')
     49
     50    def test_submit(self):
     51        response = self.client.post('/views/create_update/article/old_article/change/', {
     52            'author': '1',
     53            'title': 'Ta Da!',
     54            'slug': 'ta-da',
     55            'date_created': '2001-01-01 21:22:23'
     56        })
     57        self.assertEqual(response.status_code, 302)
     58
     59class DeleteViewByIdTest(TestCase):
     60    fixtures = ['testdata.json']
     61
     62    def test_initial(self):
     63        response = self.client.get('/views/create_update/article/1/delete/')
     64        self.assertEqual(response.status_code, 200)
     65        self.assertEqual(response.context['object'].title, u'Old Article')
     66
     67    def test_submit(self):
     68        response = self.client.post('/views/create_update/article/1/delete/', {})
     69        self.assertEqual(response.status_code, 302)
     70
     71class DeleteViewBySlugTest(TestCase):
     72    fixtures = ['testdata.json']
     73
     74    def test_initial(self):
     75        response = self.client.get('/views/create_update/article/old_article/delete/')
     76        self.assertEqual(response.status_code, 200)
     77        self.assertEqual(response.context['object'].title, u'Old Article')
     78
     79    def test_submit(self):
     80        response = self.client.post('/views/create_update/article/old_article/delete/', {})
     81        self.assertEqual(response.status_code, 302)
  • new file tests/regressiontests/views/tests/generic/list_detail.py

    diff --git a/tests/regressiontests/views/tests/generic/list_detail.py b/tests/regressiontests/views/tests/generic/list_detail.py
    new file mode 100644
    index 0000000..de9d50e
    - +  
     1# coding: utf-8
     2from django.test import TestCase
     3from regressiontests.views.models import Article, Author
     4
     5class ObjectListViewTest(TestCase):
     6    fixtures = ['testdata.json']
     7
     8    def test_basic(self):
     9        response = self.client.get('/views/articles/')
     10        self.assertEqual(response.status_code, 200)
     11        self.assertEqual(response.template.name, 'views/article_list.html')
     12        self.assertEqual(len(response.context['object_list']), 3)
     13        self.assertEqual(response.context['paginator'], None)
     14        self.assertEqual(response.context['page_obj'], None)
     15
     16class ObjectDetailViewTest(TestCase):
     17    fixtures = ['testdata.json']
     18
     19    def test_by_id(self):
     20        response = self.client.get('/views/articles/1/')
     21        self.assertEqual(response.status_code, 200)
     22        self.assertEqual(response.template.name, 'views/article_detail.html')
     23        self.assertEqual(response.context['object'].title, u'Old Article')
     24
     25    def test_by_slug(self):
     26        response = self.client.get('/views/articles/old_article/')
     27        self.assertEqual(response.status_code, 200)
     28        self.assertEqual(response.template.name, 'views/article_detail.html')
     29        self.assertEqual(response.context['object'].title, u'Old Article')
  • tests/regressiontests/views/urls.py

    diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py
    index 5ef0c51..5c3325e 100644
    a b  
    11from os import path
    22
    33from django.conf.urls.defaults import *
     4from django.views.generic.list_detail import ObjectList, ObjectDetail
     5from django.views.generic.create_update import EditView, DeleteView
    46
    57from models import *
    68import views
    base_dir = path.dirname(path.abspath(__file__))  
    911media_dir = path.join(base_dir, 'media')
    1012locale_dir = path.join(base_dir, 'locale')
    1113
     14# List/Detail views
     15article_list = ObjectList(Article.objects.all())
     16article_detail = ObjectDetail(Article.objects.all())
     17
     18# Create/Update/Delete Views
     19article_add = article_change = EditView(Article.objects.all(), post_save_redirect='../')
     20article_delete = DeleteView(Article.objects.all())
     21
     22
    1223js_info_dict = {
    1324    'domain': 'djangojs',
    1425    'packages': ('regressiontests.views',),
    urlpatterns = patterns('',  
    3445   
    3546    # Static views
    3647    (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}),
    37    
    38         # Date-based generic views
     48
     49    # Create/Update generic views
     50    (r'create_update/article/add/$', article_add),
     51    (r'create_update/article/(?P<object_pk>\d+)/change/$', article_change),
     52    (r'create_update/article/(?P<slug>\w+)/change/$', article_change),
     53    (r'create_update/article/(?P<object_pk>\d+)/delete/$', article_delete),
     54    (r'create_update/article/(?P<slug>\w+)/delete/$', article_delete),
     55
     56    (r'articles/$', article_list),
     57    (r'articles/(?P<object_pk>\d+)/$', article_detail),
     58    (r'articles/(?P<slug>\w+)/$', article_detail),
     59
     60
     61    # Date-based generic views
    3962    (r'^date_based/object_detail/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$',
    4063        'django.views.generic.date_based.object_detail',
    4164        dict(slug_field='slug', **date_based_info_dict)),
  • new file tests/templates/views/article_confirm_delete.html

    diff --git a/tests/templates/views/article_confirm_delete.html b/tests/templates/views/article_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/article_form.html

    diff --git a/tests/templates/views/article_form.html b/tests/templates/views/article_form.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/article_list.html

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