Ticket #6735: generic-views.2.diff

File generic-views.2.diff, 21.3 KB (added by jkocherhans, 16 years ago)
  • 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..ab90d91
    - +  
     1from django.http import Http404, HttpResponse
     2from django.core.exceptions import ObjectDoesNotExist
     3from django.template import RequestContext
     4
     5class BaseView(object):
     6    """
     7    Base class for creating class based view objects.
     8    """
     9    def __init__(self, queryset):
     10        self.queryset = queryset
     11        self.model = queryset.model
     12
     13    def get_query_set(self, request):
     14        """
     15        Hook to provide a custom queryset based on the request.
     16        """
     17        return self.queryset._clone()
     18
     19    def get_template(self, request, obj=None):
     20        """
     21        Returns a template object used to render this view.
     22        """
     23        raise NotImplementedError(u'Views must implement their own get_template method.')
     24
     25    def render_response(self, request, template, context_vars, mimetype=None):
     26        """
     27        Returns an HttpResponse for the given request, template object,
     28        dictionary of context variables, and optional mimetype.
     29        """
     30        context = RequestContext(request, context_vars)
     31        template = template.render(context)
     32        return HttpResponse(template, mimetype=mimetype)
     33
     34class BaseDetailView(BaseView):
     35    def __init__(self, queryset, slug_field='slug'):
     36        self.slug_field = slug_field
     37        super(BaseDetailView, self).__init__(queryset)
     38
     39    def get_object(self, request, object_pk=None, slug=None):
     40        """
     41        Returns the object to be viewed, changed or deleted, or raises a 404
     42        if it doesn't exist.
     43        """
     44        # Look up the object to be changed or deleted
     45        lookup_kwargs = {}
     46        if object_pk:
     47            lookup_kwargs['pk'] = object_pk
     48        elif slug and self.slug_field:
     49            lookup_kwargs[self.slug_field] = slug
     50        else:
     51            raise AttributeError("Generic view must be called with either an object_pk or a slug/slug_field")
     52        try:
     53            return self.get_query_set(request).get(**lookup_kwargs)
     54        except ObjectDoesNotExist:
     55            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..fc88f13 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            obj = None
     32        else:
     33            obj = 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=obj)
     37            if form.is_valid():
     38                new_obj = self.save_form(request, form)
     39                return self.on_success(request, obj, new_obj)
     40        else:
     41            form = Form()
     42        context_vars = {'object': obj, 'form': form}
     43        template = self.get_template(request, obj)
     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, obj, new_obj):
     67        # If the primary ke of the original object is None, we just created an
     68        # object, otherwise, we updated one.
     69        if obj.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 and returns the object represented by the given form. This
     76        method will only be called if the form is valid.
     77        """
     78        return form.save()
     79
     80    def on_success(self, request, obj, new_obj):
     81        """
     82        Returns an HttpResonse, generally an HttpResponse redirect. This will
     83        be the final return value of the view and will only be called if the
     84        object was saved successfuly.
     85        """
     86        # We can only do messaging for authenticated users right now
     87        if request.user.is_authenticated():
     88            message = self.get_message(request, new_obj)
     89            request.user.message_set.create(message=message)
     90        # Redirect to the new object: first by trying post_save_redirect,
     91        # then by obj.get_absolute_url; fail if neither works.
     92        if self.post_save_redirect:
     93            return HttpResponseRedirect(self.post_save_redirect % new_obj.__dict__)
     94        elif hasattr(new_obj, 'get_absolute_url'):
     95            return HttpResponseRedirect(new_obj.get_absolute_url())
     96        else:
     97            raise ImproperlyConfigured("No URL to redirect to from generic create view.")
     98
     99class DeleteView(BaseDetailView):
     100    def __init__(self, queryset, slug_field='slug', post_save_redirect=None):
     101        self.post_save_redirect = post_save_redirect
     102        super(DeleteView, self).__init__(queryset, slug_field)
     103
     104    def __call__(self, request, object_pk=None, slug=None):
     105        obj = self.get_object(request, object_pk, slug)
     106        if request.method == 'POST':
     107            self.delete(obj)
     108            return self.on_success(request, obj)
     109        context_vars = {'object': obj}
     110        template = self.get_template(request, obj)
     111        response = self.render_response(request, template, context_vars)
     112        populate_xheaders(request, response, self.model, obj.pk)
     113        return response
     114
     115    def get_template(self, request, obj=None):
     116        opts = self.model._meta
     117        template_name = "%s/%s_confirm_delete.html" % (opts.app_label, opts.object_name.lower())
     118        return loader.get_template(template_name)
     119
     120    def delete(request, obj):
     121        """
     122        Deletes the given instance. Subclasses that wish to veto deletion
     123        should do so here.
     124        """
     125        obj.delete()
     126
     127    def on_success(self, request, obj):
     128        """
     129        Redirects to self.post_save_redirect after setting a message if the
     130        user is logged in.
     131       
     132        This method is only called if saving the object was successful.
     133        """
     134        if request.user.is_authenticated():
     135            message = self.get_message(request, obj)
     136            request.user.message_set.create(message=message)
     137        return HttpResponseRedirect(self.post_save_redirect)
     138
     139    def get_message(self, request, new_object):
     140        return ugettext("The %s was deleted.") % self.model._meta.verbose_name
     141
     142
     143# Classic generic views ######################################################
    10144
    11145def create_object(request, model, template_name=None,
    12146        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..2029a8a 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, page=None):
     27        queryset = self.get_query_set(request)
     28        if self.paginate_by:
     29            paginator = QuerySetPaginator(queryset, self.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