Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #6735 -- Added class-based views.

This patch is the result of the work of many people, over many years.
To try and thank individuals would inevitably lead to many people
being left out or forgotten -- so rather than try to give a list that
will inevitably be incomplete, I'd like to thank *everybody* who
contributed in any way, big or small, with coding, testing, feedback
and/or documentation over the multi-year process of getting this into
trunk.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14254 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 0fcb09455729113f64a9873ca40bffd009b9bc5f 1 parent fa2159f
Russell Keith-Magee authored October 18, 2010

Showing 55 changed files with 5,029 additions and 83 deletions. Show diff stats Hide diff stats

  1. 9  django/views/generic/__init__.py
  2. 190  django/views/generic/base.py
  3. 8  django/views/generic/create_update.py
  4. 7  django/views/generic/date_based.py
  5. 595  django/views/generic/dates.py
  6. 142  django/views/generic/detail.py
  7. 249  django/views/generic/edit.py
  8. 138  django/views/generic/list.py
  9. 7  django/views/generic/list_detail.py
  10. 6  django/views/generic/simple.py
  11. 6  docs/index.txt
  12. 9  docs/internals/deprecation.txt
  13. 158  docs/intro/tutorial04.txt
  14. 1,391  docs/ref/class-based-views.txt
  15. 2  docs/ref/generic-views.txt
  16. 10  docs/ref/index.txt
  17. 29  docs/releases/1.3.txt
  18. 535  docs/topics/class-based-views.txt
  19. 127  docs/topics/generic-views-migration.txt
  20. 10  docs/topics/index.txt
  21. 0  tests/regressiontests/generic_views/__init__.py
  22. 233  tests/regressiontests/generic_views/base.py
  23. 352  tests/regressiontests/generic_views/dates.py
  24. 71  tests/regressiontests/generic_views/detail.py
  25. 233  tests/regressiontests/generic_views/edit.py
  26. 47  tests/regressiontests/generic_views/fixtures/generic-views-test-data.json
  27. 11  tests/regressiontests/generic_views/forms.py
  28. 129  tests/regressiontests/generic_views/list.py
  29. 41  tests/regressiontests/generic_views/models.py
  30. 1  tests/regressiontests/generic_views/templates/generic_views/about.html
  31. 1  tests/regressiontests/generic_views/templates/generic_views/apple_detail.html
  32. 1  tests/regressiontests/generic_views/templates/generic_views/artist_detail.html
  33. 1  tests/regressiontests/generic_views/templates/generic_views/artist_form.html
  34. 1  tests/regressiontests/generic_views/templates/generic_views/author_confirm_delete.html
  35. 1  tests/regressiontests/generic_views/templates/generic_views/author_detail.html
  36. 1  tests/regressiontests/generic_views/templates/generic_views/author_form.html
  37. 3  tests/regressiontests/generic_views/templates/generic_views/author_list.html
  38. 3  tests/regressiontests/generic_views/templates/generic_views/author_objects.html
  39. 1  tests/regressiontests/generic_views/templates/generic_views/author_view.html
  40. 1  tests/regressiontests/generic_views/templates/generic_views/book_archive.html
  41. 1  tests/regressiontests/generic_views/templates/generic_views/book_archive_day.html
  42. 1  tests/regressiontests/generic_views/templates/generic_views/book_archive_month.html
  43. 1  tests/regressiontests/generic_views/templates/generic_views/book_archive_week.html
  44. 1  tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html
  45. 1  tests/regressiontests/generic_views/templates/generic_views/book_detail.html
  46. 3  tests/regressiontests/generic_views/templates/generic_views/book_list.html
  47. 1  tests/regressiontests/generic_views/templates/generic_views/confirm_delete.html
  48. 1  tests/regressiontests/generic_views/templates/generic_views/detail.html
  49. 1  tests/regressiontests/generic_views/templates/generic_views/form.html
  50. 3  tests/regressiontests/generic_views/templates/generic_views/list.html
  51. 1  tests/regressiontests/generic_views/templates/generic_views/page_template.html
  52. 1  tests/regressiontests/generic_views/templates/registration/login.html
  53. 5  tests/regressiontests/generic_views/tests.py
  54. 186  tests/regressiontests/generic_views/urls.py
  55. 145  tests/regressiontests/generic_views/views.py
9  django/views/generic/__init__.py
... ...
@@ -1,3 +1,12 @@
  1
+from django.views.generic.base import View, TemplateView, RedirectView
  2
+from django.views.generic.dates import (ArchiveIndexView, YearArchiveView, MonthArchiveView,
  3
+                                     WeekArchiveView, DayArchiveView, TodayArchiveView,
  4
+                                     DateDetailView)
  5
+from django.views.generic.detail import DetailView
  6
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
  7
+from django.views.generic.list import ListView
  8
+
  9
+
1 10
 class GenericViewError(Exception):
2 11
     """A problem in a generic view."""
3 12
     pass
190  django/views/generic/base.py
... ...
@@ -0,0 +1,190 @@
  1
+import copy
  2
+from django import http
  3
+from django.core.exceptions import ImproperlyConfigured
  4
+from django.template import RequestContext, loader
  5
+from django.utils.translation import ugettext_lazy as _
  6
+from django.utils.functional import update_wrapper
  7
+from django.utils.log import getLogger
  8
+
  9
+logger = getLogger('django.request')
  10
+
  11
+class classonlymethod(classmethod):
  12
+    def __get__(self, instance, owner):
  13
+        if instance is not None:
  14
+            raise AttributeError("This method is available only on the view class.")
  15
+        return super(classonlymethod, self).__get__(instance, owner)
  16
+
  17
+class View(object):
  18
+    """
  19
+    Intentionally simple parent class for all views. Only implements
  20
+    dispatch-by-method and simple sanity checking.
  21
+    """
  22
+
  23
+    http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
  24
+
  25
+    def __init__(self, **kwargs):
  26
+        """
  27
+        Constructor. Called in the URLconf; can contain helpful extra
  28
+        keyword arguments, and other things.
  29
+        """
  30
+        # Go through keyword arguments, and either save their values to our
  31
+        # instance, or raise an error.
  32
+        for key, value in kwargs.iteritems():
  33
+            setattr(self, key, value)
  34
+
  35
+    @classonlymethod
  36
+    def as_view(cls, **initkwargs):
  37
+        """
  38
+        Main entry point for a request-response process.
  39
+        """
  40
+        # sanitize keyword arguments
  41
+        for key in initkwargs:
  42
+            if key in cls.http_method_names:
  43
+                raise TypeError(u"You tried to pass in the %s method name as a "
  44
+                                u"keyword argument to %s(). Don't do that."
  45
+                                % (key, cls.__name__))
  46
+            if not hasattr(cls, key):
  47
+                raise TypeError(u"%s() received an invalid keyword %r" % (
  48
+                    cls.__name__, key))
  49
+
  50
+        def view(request, *args, **kwargs):
  51
+            self = cls(**initkwargs)
  52
+            return self.dispatch(request, *args, **kwargs)
  53
+
  54
+        # take name and docstring from class
  55
+        update_wrapper(view, cls, updated=())
  56
+
  57
+        # and possible attributes set by decorators
  58
+        # like csrf_exempt from dispatch
  59
+        update_wrapper(view, cls.dispatch, assigned=())
  60
+        return view
  61
+
  62
+    def dispatch(self, request, *args, **kwargs):
  63
+        # Try to dispatch to the right method for that; if it doesn't exist,
  64
+        # defer to the error handler. Also defer to the error handler if the
  65
+        # request method isn't on the approved list.
  66
+        if request.method.lower() in self.http_method_names:
  67
+            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
  68
+        else:
  69
+            handler = self.http_method_not_allowed
  70
+        self.request = request
  71
+        self.args = args
  72
+        self.kwargs = kwargs
  73
+        return handler(request, *args, **kwargs)
  74
+
  75
+    def http_method_not_allowed(self, request, *args, **kwargs):
  76
+        allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
  77
+        return http.HttpResponseNotAllowed(allowed_methods)
  78
+
  79
+
  80
+class TemplateResponseMixin(object):
  81
+    """
  82
+    A mixin that can be used to render a template.
  83
+    """
  84
+    template_name = None
  85
+
  86
+    def render_to_response(self, context):
  87
+        """
  88
+        Returns a response with a template rendered with the given context.
  89
+        """
  90
+        return self.get_response(self.render_template(context))
  91
+
  92
+    def get_response(self, content, **httpresponse_kwargs):
  93
+        """
  94
+        Construct an `HttpResponse` object.
  95
+        """
  96
+        return http.HttpResponse(content, **httpresponse_kwargs)
  97
+
  98
+    def render_template(self, context):
  99
+        """
  100
+        Render the template with a given context.
  101
+        """
  102
+        context_instance = self.get_context_instance(context)
  103
+        return self.get_template().render(context_instance)
  104
+
  105
+    def get_context_instance(self, context):
  106
+        """
  107
+        Get the template context instance. Must return a Context (or subclass)
  108
+        instance.
  109
+        """
  110
+        return RequestContext(self.request, context)
  111
+
  112
+    def get_template(self):
  113
+        """
  114
+        Get a ``Template`` object for the given request.
  115
+        """
  116
+        names = self.get_template_names()
  117
+        if not names:
  118
+            raise ImproperlyConfigured(u"'%s' must provide template_name."
  119
+                                       % self.__class__.__name__)
  120
+        return self.load_template(names)
  121
+
  122
+    def get_template_names(self):
  123
+        """
  124
+        Return a list of template names to be used for the request. Must return
  125
+        a list. May not be called if get_template is overridden.
  126
+        """
  127
+        if self.template_name is None:
  128
+            return []
  129
+        else:
  130
+            return [self.template_name]
  131
+
  132
+    def load_template(self, names):
  133
+        """
  134
+        Load a list of templates using the default template loader.
  135
+        """
  136
+        return loader.select_template(names)
  137
+
  138
+
  139
+class TemplateView(TemplateResponseMixin, View):
  140
+    """
  141
+    A view that renders a template.
  142
+    """
  143
+    def get_context_data(self, **kwargs):
  144
+        return {
  145
+            'params': kwargs
  146
+        }
  147
+
  148
+    def get(self, request, *args, **kwargs):
  149
+        context = self.get_context_data(**kwargs)
  150
+        return self.render_to_response(context)
  151
+
  152
+
  153
+class RedirectView(View):
  154
+    """
  155
+    A view that provides a redirect on any GET request.
  156
+    """
  157
+    permanent = True
  158
+    url = None
  159
+    query_string = False
  160
+
  161
+    def get_redirect_url(self, **kwargs):
  162
+        """
  163
+        Return the URL redirect to. Keyword arguments from the
  164
+        URL pattern match generating the redirect request
  165
+        are provided as kwargs to this method.
  166
+        """
  167
+        if self.url:
  168
+            args = self.request.META["QUERY_STRING"]
  169
+            if args and self.query_string:
  170
+                url = "%s?%s" % (self.url, args)
  171
+            else:
  172
+                url = self.url
  173
+            return url % kwargs
  174
+        else:
  175
+            return None
  176
+
  177
+    def get(self, request, *args, **kwargs):
  178
+        url = self.get_redirect_url(**kwargs)
  179
+        if url:
  180
+            if self.permanent:
  181
+                return http.HttpResponsePermanentRedirect(url)
  182
+            else:
  183
+                return http.HttpResponseRedirect(url)
  184
+        else:
  185
+            logger.warning('Gone: %s' % self.request.path,
  186
+                        extra={
  187
+                            'status_code': 410,
  188
+                            'request': self.request
  189
+                        })
  190
+            return http.HttpResponseGone()
8  django/views/generic/create_update.py
@@ -8,6 +8,12 @@
8 8
 from django.views.generic import GenericViewError
9 9
 from django.contrib import messages
10 10
 
  11
+import warnings
  12
+warnings.warn(
  13
+    'Function-based generic views have been deprecated; use class-based views instead.',
  14
+    PendingDeprecationWarning
  15
+)
  16
+
11 17
 
12 18
 def apply_extra_context(extra_context, context):
13 19
     """
@@ -111,7 +117,7 @@ def create_object(request, model=None, template_name=None,
111 117
         form = form_class(request.POST, request.FILES)
112 118
         if form.is_valid():
113 119
             new_object = form.save()
114  
-            
  120
+
115 121
             msg = ugettext("The %(verbose_name)s was created successfully.") %\
116 122
                                     {"verbose_name": model._meta.verbose_name}
117 123
             messages.success(request, msg, fail_silently=True)
7  django/views/generic/date_based.py
@@ -7,6 +7,13 @@
7 7
 from django.db.models.fields import DateTimeField
8 8
 from django.http import Http404, HttpResponse
9 9
 
  10
+import warnings
  11
+warnings.warn(
  12
+    'Function-based generic views have been deprecated; use class-based views instead.',
  13
+    PendingDeprecationWarning
  14
+)
  15
+
  16
+
10 17
 def archive_index(request, queryset, date_field, num_latest=15,
11 18
         template_name=None, template_loader=loader,
12 19
         extra_context=None, allow_empty=True, context_processors=None,
595  django/views/generic/dates.py
... ...
@@ -0,0 +1,595 @@
  1
+import time
  2
+import datetime
  3
+from django.db import models
  4
+from django.core.exceptions import ImproperlyConfigured
  5
+from django.http import Http404
  6
+from django.views.generic.base import View
  7
+from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin
  8
+from django.views.generic.list import MultipleObjectMixin, MultipleObjectTemplateResponseMixin
  9
+
  10
+
  11
+class YearMixin(object):
  12
+    year_format = '%Y'
  13
+    year = None
  14
+
  15
+    def get_year_format(self):
  16
+        """
  17
+        Get a year format string in strptime syntax to be used to parse the
  18
+        year from url variables.
  19
+        """
  20
+        return self.year_format
  21
+
  22
+    def get_year(self):
  23
+        "Return the year for which this view should display data"
  24
+        year = self.year
  25
+        if year is None:
  26
+            try:
  27
+                year = self.kwargs['year']
  28
+            except KeyError:
  29
+                try:
  30
+                    year = self.request.GET['year']
  31
+                except KeyError:
  32
+                    raise Http404("No year specified")
  33
+        return year
  34
+
  35
+
  36
+class MonthMixin(object):
  37
+    month_format = '%b'
  38
+    month = None
  39
+
  40
+    def get_month_format(self):
  41
+        """
  42
+        Get a month format string in strptime syntax to be used to parse the
  43
+        month from url variables.
  44
+        """
  45
+        return self.month_format
  46
+
  47
+    def get_month(self):
  48
+        "Return the month for which this view should display data"
  49
+        month = self.month
  50
+        if month is None:
  51
+            try:
  52
+                month = self.kwargs['month']
  53
+            except KeyError:
  54
+                try:
  55
+                    month = self.request.GET['month']
  56
+                except KeyError:
  57
+                    raise Http404("No month specified")
  58
+        return month
  59
+
  60
+    def get_next_month(self, date):
  61
+        """
  62
+        Get the next valid month.
  63
+        """
  64
+        first_day, last_day = _month_bounds(date)
  65
+        next = (last_day + datetime.timedelta(days=1)).replace(day=1)
  66
+        return _get_next_prev_month(self, next, is_previous=False, use_first_day=True)
  67
+
  68
+    def get_previous_month(self, date):
  69
+        """
  70
+        Get the previous valid month.
  71
+        """
  72
+        first_day, last_day = _month_bounds(date)
  73
+        prev = (first_day - datetime.timedelta(days=1)).replace(day=1)
  74
+        return _get_next_prev_month(self, prev, is_previous=True, use_first_day=True)
  75
+
  76
+
  77
+class DayMixin(object):
  78
+    day_format = '%d'
  79
+    day = None
  80
+
  81
+    def get_day_format(self):
  82
+        """
  83
+        Get a month format string in strptime syntax to be used to parse the
  84
+        month from url variables.
  85
+        """
  86
+        return self.day_format
  87
+
  88
+    def get_day(self):
  89
+        "Return the day for which this view should display data"
  90
+        day = self.day
  91
+        if day is None:
  92
+            try:
  93
+                day = self.kwargs['day']
  94
+            except KeyError:
  95
+                try:
  96
+                    day = self.request.GET['day']
  97
+                except KeyError:
  98
+                    raise Http404("No day specified")
  99
+        return day
  100
+
  101
+    def get_next_day(self, date):
  102
+        """
  103
+        Get the next valid day.
  104
+        """
  105
+        next = date + datetime.timedelta(days=1)
  106
+        return _get_next_prev_month(self, next, is_previous=False, use_first_day=False)
  107
+
  108
+    def get_previous_day(self, date):
  109
+        """
  110
+        Get the previous valid day.
  111
+        """
  112
+        prev = date - datetime.timedelta(days=1)
  113
+        return _get_next_prev_month(self, prev, is_previous=True, use_first_day=False)
  114
+
  115
+
  116
+class WeekMixin(object):
  117
+    week_format = '%U'
  118
+    week = None
  119
+
  120
+    def get_week_format(self):
  121
+        """
  122
+        Get a week format string in strptime syntax to be used to parse the
  123
+        week from url variables.
  124
+        """
  125
+        return self.week_format
  126
+
  127
+    def get_week(self):
  128
+        "Return the week for which this view should display data"
  129
+        week = self.week
  130
+        if week is None:
  131
+            try:
  132
+                week = self.kwargs['week']
  133
+            except KeyError:
  134
+                try:
  135
+                    week = self.request.GET['week']
  136
+                except KeyError:
  137
+                    raise Http404("No week specified")
  138
+        return week
  139
+
  140
+
  141
+class DateMixin(object):
  142
+    """
  143
+    Mixin class for views manipulating date-based data.
  144
+    """
  145
+    date_field = None
  146
+    allow_future = False
  147
+
  148
+    def get_date_field(self):
  149
+        """
  150
+        Get the name of the date field to be used to filter by.
  151
+        """
  152
+        if self.date_field is None:
  153
+            raise ImproperlyConfigured(u"%s.date_field is required." % self.__class__.__name__)
  154
+        return self.date_field
  155
+
  156
+    def get_allow_future(self):
  157
+        """
  158
+        Returns `True` if the view should be allowed to display objects from
  159
+        the future.
  160
+        """
  161
+        return self.allow_future
  162
+
  163
+
  164
+class BaseDateListView(MultipleObjectMixin, DateMixin, View):
  165
+    """
  166
+    Abstract base class for date-based views display a list of objects.
  167
+    """
  168
+    allow_empty = False
  169
+
  170
+    def get(self, request, *args, **kwargs):
  171
+        self.date_list, self.object_list, extra_context = self.get_dated_items()
  172
+        context = self.get_context_data(object_list=self.object_list,
  173
+                                        date_list=self.date_list)
  174
+        context.update(extra_context)
  175
+        return self.render_to_response(context)
  176
+
  177
+    def get_dated_items(self):
  178
+        """
  179
+        Obtain the list of dates and itesm
  180
+        """
  181
+        raise NotImplemented('A DateView must provide an implementaiton of get_dated_items()')
  182
+
  183
+    def get_dated_queryset(self, **lookup):
  184
+        """
  185
+        Get a queryset properly filtered according to `allow_future` and any
  186
+        extra lookup kwargs.
  187
+        """
  188
+        qs = self.get_queryset().filter(**lookup)
  189
+        date_field = self.get_date_field()
  190
+        allow_future = self.get_allow_future()
  191
+        allow_empty = self.get_allow_empty()
  192
+
  193
+        if not allow_future:
  194
+            qs = qs.filter(**{'%s__lte' % date_field: datetime.datetime.now()})
  195
+
  196
+        if not allow_empty and not qs:
  197
+            raise Http404(u"No %s available" % unicode(qs.model._meta.verbose_name_plural))
  198
+
  199
+        return qs
  200
+
  201
+    def get_date_list(self, queryset, date_type):
  202
+        """
  203
+        Get a date list by calling `queryset.dates()`, checking along the way
  204
+        for empty lists that aren't allowed.
  205
+        """
  206
+        date_field = self.get_date_field()
  207
+        allow_empty = self.get_allow_empty()
  208
+
  209
+        date_list = queryset.dates(date_field, date_type)[::-1]
  210
+        if date_list is not None and not date_list and not allow_empty:
  211
+            raise Http404(u"No %s available" % unicode(qs.model._meta.verbose_name_plural))
  212
+
  213
+        return date_list
  214
+
  215
+
  216
+
  217
+    def get_context_data(self, **kwargs):
  218
+        """
  219
+        Get the context. Must return a Context (or subclass) instance.
  220
+        """
  221
+        items = kwargs.pop('object_list')
  222
+        context = super(BaseDateListView, self).get_context_data(object_list=items)
  223
+        context.update(kwargs)
  224
+        return context
  225
+
  226
+
  227
+class BaseArchiveIndexView(BaseDateListView):
  228
+    """
  229
+    Base class for archives of date-based items.
  230
+
  231
+    Requires a response mixin.
  232
+    """
  233
+    context_object_name = 'latest'
  234
+
  235
+    def get_dated_items(self):
  236
+        """
  237
+        Return (date_list, items, extra_context) for this request.
  238
+        """
  239
+        qs = self.get_dated_queryset()
  240
+        date_list = self.get_date_list(qs, 'year')
  241
+
  242
+        if date_list:
  243
+            object_list = qs.order_by('-'+self.get_date_field())
  244
+        else:
  245
+            object_list = qs.none()
  246
+
  247
+        return (date_list, object_list, {})
  248
+
  249
+
  250
+class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView):
  251
+    """
  252
+    Top-level archive of date-based items.
  253
+    """
  254
+    template_name_suffix = '_archive'
  255
+
  256
+
  257
+class BaseYearArchiveView(YearMixin, BaseDateListView):
  258
+    """
  259
+    List of objects published in a given year.
  260
+    """
  261
+    make_object_list = False
  262
+
  263
+    def get_dated_items(self):
  264
+        """
  265
+        Return (date_list, items, extra_context) for this request.
  266
+        """
  267
+        # Yes, no error checking: the URLpattern ought to validate this; it's
  268
+        # an error if it doesn't.
  269
+        year = self.get_year()
  270
+        date_field = self.get_date_field()
  271
+        qs = self.get_dated_queryset(**{date_field+'__year': year})
  272
+        date_list = self.get_date_list(qs, 'month')
  273
+
  274
+        if self.get_make_object_list():
  275
+            object_list = qs.order_by('-'+date_field)
  276
+        else:
  277
+            # We need this to be a queryset since parent classes introspect it
  278
+            # to find information about the model.
  279
+            object_list = qs.none()
  280
+
  281
+        return (date_list, object_list, {'year': year})
  282
+
  283
+    def get_make_object_list(self):
  284
+        """
  285
+        Return `True` if this view should contain the full list of objects in
  286
+        the given year.
  287
+        """
  288
+        return self.make_object_list
  289
+
  290
+
  291
+class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView):
  292
+    """
  293
+    List of objects published in a given year.
  294
+    """
  295
+    template_name_suffix = '_archive_year'
  296
+
  297
+
  298
+class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
  299
+    """
  300
+    List of objects published in a given year.
  301
+    """
  302
+    def get_dated_items(self):
  303
+        """
  304
+        Return (date_list, items, extra_context) for this request.
  305
+        """
  306
+        year = self.get_year()
  307
+        month = self.get_month()
  308
+
  309
+        date_field = self.get_date_field()
  310
+        date = _date_from_string(year, self.get_year_format(),
  311
+                                 month, self.get_month_format())
  312
+
  313
+        # Construct a date-range lookup.
  314
+        first_day, last_day = _month_bounds(date)
  315
+        lookup_kwargs = {
  316
+            '%s__gte' % date_field: first_day,
  317
+            '%s__lt' % date_field: last_day,
  318
+        }
  319
+
  320
+        qs = self.get_dated_queryset(**lookup_kwargs)
  321
+        date_list = self.get_date_list(qs, 'day')
  322
+
  323
+        return (date_list, qs, {
  324
+            'month': date,
  325
+            'next_month': self.get_next_month(date),
  326
+            'previous_month': self.get_previous_month(date),
  327
+        })
  328
+
  329
+
  330
+
  331
+class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView):
  332
+    """
  333
+    List of objects published in a given year.
  334
+    """
  335
+    template_name_suffix = '_archive_month'
  336
+
  337
+
  338
+class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
  339
+    """
  340
+    List of objects published in a given week.
  341
+    """
  342
+
  343
+    def get_dated_items(self):
  344
+        """
  345
+        Return (date_list, items, extra_context) for this request.
  346
+        """
  347
+        year = self.get_year()
  348
+        week = self.get_week()
  349
+
  350
+        date_field = self.get_date_field()
  351
+        date = _date_from_string(year, self.get_year_format(),
  352
+                                 '0', '%w',
  353
+                                 week, self.get_week_format())
  354
+
  355
+        # Construct a date-range lookup.
  356
+        first_day = date
  357
+        last_day = date + datetime.timedelta(days=7)
  358
+        lookup_kwargs = {
  359
+            '%s__gte' % date_field: first_day,
  360
+            '%s__lt' % date_field: last_day,
  361
+        }
  362
+
  363
+        qs = self.get_dated_queryset(**lookup_kwargs)
  364
+
  365
+        return (None, qs, {'week': date})
  366
+
  367
+
  368
+class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView):
  369
+    """
  370
+    List of objects published in a given week.
  371
+    """
  372
+    template_name_suffix = '_archive_week'
  373
+
  374
+
  375
+class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
  376
+    """
  377
+    List of objects published on a given day.
  378
+    """
  379
+    def get_dated_items(self):
  380
+        """
  381
+        Return (date_list, items, extra_context) for this request.
  382
+        """
  383
+        year = self.get_year()
  384
+        month = self.get_month()
  385
+        day = self.get_day()
  386
+
  387
+        date = _date_from_string(year, self.get_year_format(),
  388
+                                 month, self.get_month_format(),
  389
+                                 day, self.get_day_format())
  390
+
  391
+        return self._get_dated_items(date)
  392
+
  393
+    def _get_dated_items(self, date):
  394
+        """
  395
+        Do the actual heavy lifting of getting the dated items; this accepts a
  396
+        date object so that TodayArchiveView can be trivial.
  397
+        """
  398
+        date_field = self.get_date_field()
  399
+
  400
+        field = self.get_queryset().model._meta.get_field(date_field)
  401
+        lookup_kwargs = _date_lookup_for_field(field, date)
  402
+
  403
+        qs = self.get_dated_queryset(**lookup_kwargs)
  404
+
  405
+        return (None, qs, {
  406
+            'day': date,
  407
+            'previous_day': self.get_previous_day(date),
  408
+            'next_day': self.get_next_day(date),
  409
+            'previous_month': self.get_previous_month(date),
  410
+            'next_month': self.get_next_month(date)
  411
+        })
  412
+
  413
+
  414
+
  415
+class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView):
  416
+    """
  417
+    List of objects published on a given day.
  418
+    """
  419
+    template_name_suffix = "_archive_day"
  420
+
  421
+
  422
+class BaseTodayArchiveView(BaseDayArchiveView):
  423
+    """
  424
+    List of objects published today.
  425
+    """
  426
+
  427
+    def get_dated_items(self):
  428
+        """
  429
+        Return (date_list, items, extra_context) for this request.
  430
+        """
  431
+        return self._get_dated_items(datetime.date.today())
  432
+
  433
+
  434
+class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView):
  435
+    """
  436
+    List of objects published today.
  437
+    """
  438
+    template_name_suffix = "_archive_day"
  439
+
  440
+
  441
+class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailView):
  442
+    """
  443
+    Detail view of a single object on a single date; this differs from the
  444
+    standard DetailView by accepting a year/month/day in the URL.
  445
+    """
  446
+    def get_object(self, queryset=None, **kwargs):
  447
+        """
  448
+        Get the object this request displays.
  449
+        """
  450
+        year = self.get_year()
  451
+        month = self.get_month()
  452
+        day = self.get_day()
  453
+        date = _date_from_string(year, self.get_year_format(),
  454
+                                 month, self.get_month_format(),
  455
+                                 day, self.get_day_format())
  456
+
  457
+        qs = self.get_queryset()
  458
+
  459
+        if not self.get_allow_future() and date > datetime.date.today():
  460
+            raise Http404("Future %s not available because %s.allow_future is False." % (
  461
+                qs.model._meta.verbose_name_plural, self.__class__.__name__)
  462
+            )
  463
+
  464
+        # Filter down a queryset from self.queryset using the date from the
  465
+        # URL. This'll get passed as the queryset to DetailView.get_object,
  466
+        # which'll handle the 404
  467
+        date_field = self.get_date_field()
  468
+        field = qs.model._meta.get_field(date_field)
  469
+        lookup = _date_lookup_for_field(field, date)
  470
+        qs = qs.filter(**lookup)
  471
+
  472
+        return super(BaseDetailView, self).get_object(queryset=qs, **kwargs)
  473
+
  474
+
  475
+
  476
+class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView):
  477
+    """
  478
+    Detail view of a single object on a single date; this differs from the
  479
+    standard DetailView by accepting a year/month/day in the URL.
  480
+    """
  481
+    template_name_suffix = '_detail'
  482
+
  483
+
  484
+def _date_from_string(year, year_format, month, month_format, day='', day_format='', delim='__'):
  485
+    """
  486
+    Helper: get a datetime.date object given a format string and a year,
  487
+    month, and possibly day; raise a 404 for an invalid date.
  488
+    """
  489
+    format = delim.join((year_format, month_format, day_format))
  490
+    datestr = delim.join((year, month, day))
  491
+    try:
  492
+        return datetime.date(*time.strptime(datestr, format)[:3])
  493
+    except ValueError:
  494
+        raise Http404(u"Invalid date string '%s' given format '%s'" % (datestr, format))
  495
+
  496
+def _month_bounds(date):
  497
+    """
  498
+    Helper: return the first and last days of the month for the given date.
  499
+    """
  500
+    first_day = date.replace(day=1)
  501
+    if first_day.month == 12:
  502
+        last_day = first_day.replace(year=first_day.year + 1, month=1)
  503
+    else:
  504
+        last_day = first_day.replace(month=first_day.month + 1)
  505
+
  506
+    return first_day, last_day
  507
+
  508
+def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day):
  509
+    """
  510
+    Helper: Get the next or the previous valid date. The idea is to allow
  511
+    links on month/day views to never be 404s by never providing a date
  512
+    that'll be invalid for the given view.
  513
+
  514
+    This is a bit complicated since it handles both next and previous months
  515
+    and days (for MonthArchiveView and DayArchiveView); hence the coupling to generic_view.
  516
+
  517
+    However in essance the logic comes down to:
  518
+
  519
+        * If allow_empty and allow_future are both true, this is easy: just
  520
+          return the naive result (just the next/previous day or month,
  521
+          reguardless of object existence.)
  522
+
  523
+        * If allow_empty is true, allow_future is false, and the naive month
  524
+          isn't in the future, then return it; otherwise return None.
  525
+
  526
+        * If allow_empty is false and allow_future is true, return the next
  527
+          date *that contains a valid object*, even if it's in the future. If
  528
+          there are no next objects, return None.
  529
+
  530
+        * If allow_empty is false and allow_future is false, return the next
  531
+          date that contains a valid object. If that date is in the future, or
  532
+          if there are no next objects, return None.
  533
+
  534
+    """
  535
+    date_field = generic_view.get_date_field()
  536
+    allow_empty = generic_view.get_allow_empty()
  537
+    allow_future = generic_view.get_allow_future()
  538
+
  539
+    # If allow_empty is True the naive value will be valid
  540
+    if allow_empty:
  541
+        result = naive_result
  542
+
  543
+    # Otherwise, we'll need to go to the database to look for an object
  544
+    # whose date_field is at least (greater than/less than) the given
  545
+    # naive result
  546
+    else:
  547
+        # Construct a lookup and an ordering depending on weather we're doing
  548
+        # a previous date or a next date lookup.
  549
+        if is_previous:
  550
+            lookup = {'%s__lte' % date_field: naive_result}
  551
+            ordering = '-%s' % date_field
  552
+        else:
  553
+            lookup = {'%s__gte' % date_field: naive_result}
  554
+            ordering = date_field
  555
+
  556
+        qs = generic_view.get_queryset().filter(**lookup).order_by(ordering)
  557
+
  558
+        # Snag the first object from the queryset; if it doesn't exist that
  559
+        # means there's no next/previous link available.
  560
+        try:
  561
+            result = getattr(qs[0], date_field)
  562
+        except IndexError:
  563
+            result = None
  564
+
  565
+    # Convert datetimes to a dates
  566
+    if hasattr(result, 'date'):
  567
+        result = result.date()
  568
+
  569
+    # For month views, we always want to have a date that's the first of the
  570
+    # month for consistancy's sake.
  571
+    if result and use_first_day:
  572
+        result = result.replace(day=1)
  573
+
  574
+    # Check against future dates.
  575
+    if result and (allow_future or result < datetime.date.today()):
  576
+        return result
  577
+    else:
  578
+        return None
  579
+
  580
+def _date_lookup_for_field(field, date):
  581
+    """
  582
+    Get the lookup kwargs for looking up a date against a given Field. If the
  583
+    date field is a DateTimeField, we can't just do filter(df=date) because
  584
+    that doesn't take the time into account. So we need to make a range lookup
  585
+    in those cases.
  586
+    """
  587
+    if isinstance(field, models.DateTimeField):
  588
+        date_range = (
  589
+            datetime.datetime.combine(date, datetime.time.min),
  590
+            datetime.datetime.combine(date, datetime.time.max)
  591
+        )
  592
+        return {'%s__range' % field.name: date_range}
  593
+    else:
  594
+        return {field.name: date}
  595
+
142  django/views/generic/detail.py
... ...
@@ -0,0 +1,142 @@
  1
+import re
  2
+
  3
+from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
  4
+from django.http import Http404
  5
+from django.views.generic.base import TemplateResponseMixin, View
  6
+
  7
+
  8
+class SingleObjectMixin(object):
  9
+    """
  10
+    Provides the ability to retrieve a single object for further manipulation.
  11
+    """
  12
+    model = None
  13
+    queryset = None
  14
+    slug_field = 'slug'
  15
+    context_object_name = None
  16
+
  17
+    def get_object(self, pk=None, slug=None, queryset=None, **kwargs):
  18
+        """
  19
+        Returns the object the view is displaying.
  20
+
  21
+        By default this requires `self.queryset` and a `pk` or `slug` argument
  22
+        in the URLconf, but subclasses can override this to return any object.
  23
+        """
  24
+        # Use a custom queryset if provided; this is required for subclasses
  25
+        # like DateDetailView
  26
+        if queryset is None:
  27
+            queryset = self.get_queryset()
  28
+
  29
+        # Next, try looking up by primary key.
  30
+        if pk is not None:
  31
+            queryset = queryset.filter(pk=pk)
  32
+
  33
+        # Next, try looking up by slug.
  34
+        elif slug is not None:
  35
+            slug_field = self.get_slug_field()
  36
+            queryset = queryset.filter(**{slug_field: slug})
  37
+
  38
+        # If none of those are defined, it's an error.
  39
+        else:
  40
+            raise AttributeError(u"Generic detail view %s must be called with "
  41
+                                 u"either an object id or a slug."
  42
+                                 % self.__class__.__name__)
  43
+
  44
+        try:
  45
+            obj = queryset.get()
  46
+        except ObjectDoesNotExist:
  47
+            raise Http404(u"No %s found matching the query" %
  48
+                          (queryset.model._meta.verbose_name))
  49
+        return obj
  50
+
  51
+    def get_queryset(self):
  52
+        """
  53
+        Get the queryset to look an object up against. May not be called if
  54
+        `get_object` is overridden.
  55
+        """
  56
+        if self.queryset is None:
  57
+            if self.model:
  58
+                return self.model._default_manager.all()
  59
+            else:
  60
+                raise ImproperlyConfigured(u"%(cls)s is missing a queryset. Define "
  61
+                                           u"%(cls)s.model, %(cls)s.queryset, or override "
  62
+                                           u"%(cls)s.get_object()." % {
  63
+                                                'cls': self.__class__.__name__
  64
+                                        })
  65
+        return self.queryset._clone()
  66
+
  67
+    def get_slug_field(self):
  68
+        """
  69
+        Get the name of a slug field to be used to look up by slug.
  70
+        """
  71
+        return self.slug_field
  72
+
  73
+    def get_context_object_name(self, obj):
  74
+        """
  75
+        Get the name to use for the object.
  76
+        """
  77
+        if self.context_object_name:
  78
+            return self.context_object_name
  79
+        elif hasattr(obj, '_meta'):
  80
+            return re.sub('[^a-zA-Z0-9]+', '_',
  81
+                    obj._meta.verbose_name.lower())
  82
+        else:
  83
+            return None
  84
+
  85
+    def get_context_data(self, **kwargs):
  86
+        context = kwargs
  87
+        context_object_name = self.get_context_object_name(self.object)
  88
+        if context_object_name:
  89
+            context[context_object_name] = self.object
  90
+        return context
  91
+
  92
+
  93
+class BaseDetailView(SingleObjectMixin, View):
  94
+    def get(self, request, **kwargs):
  95
+        self.object = self.get_object(**kwargs)
  96
+        context = self.get_context_data(object=self.object)
  97
+        return self.render_to_response(context)
  98
+
  99
+
  100
+class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
  101
+    template_name_field = None
  102
+    template_name_suffix = '_detail'
  103
+
  104
+    def get_template_names(self):
  105
+        """
  106
+        Return a list of template names to be used for the request. Must return
  107
+        a list. May not be called if get_template is overridden.
  108
+        """
  109
+        names = super(SingleObjectTemplateResponseMixin, self).get_template_names()
  110
+
  111
+        # If self.template_name_field is set, grab the value of the field
  112
+        # of that name from the object; this is the most specific template
  113
+        # name, if given.
  114
+        if self.object and self.template_name_field:
  115
+            name = getattr(self.object, self.template_name_field, None)
  116
+            if name:
  117
+                names.insert(0, name)
  118
+
  119
+        # The least-specific option is the default <app>/<model>_detail.html;
  120
+        # only use this if the object in question is a model.
  121
+        if hasattr(self.object, '_meta'):
  122
+            names.append("%s/%s%s.html" % (
  123
+                self.object._meta.app_label,
  124
+                self.object._meta.object_name.lower(),
  125
+                self.template_name_suffix
  126
+            ))
  127
+        elif hasattr(self, 'model') and hasattr(self.model, '_meta'):
  128
+            names.append("%s/%s%s.html" % (
  129
+                self.model._meta.app_label,
  130
+                self.model._meta.object_name.lower(),
  131
+                self.template_name_suffix
  132
+            ))
  133
+        return names
  134
+
  135
+
  136
+class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView):
  137
+    """
  138
+    Render a "detail" view of an object.
  139
+
  140
+    By default this is a model instance looked up from `self.queryset`, but the
  141
+    view will support display of *any* object by overriding `self.get_object()`.
  142
+    """
249  django/views/generic/edit.py
... ...
@@ -0,0 +1,249 @@
  1
+from django.forms import models as model_forms
  2
+from django.core.exceptions import ImproperlyConfigured
  3
+from django.http import HttpResponseRedirect
  4
+from django.views.generic.base import TemplateResponseMixin, View
  5
+from django.views.generic.detail import (SingleObjectMixin, 
  6
+                        SingleObjectTemplateResponseMixin, BaseDetailView) 
  7
+
  8
+
  9
+class FormMixin(object):
  10
+    """
  11
+    A mixin that provides a way to show and handle a form in a request.
  12
+    """
  13
+
  14
+    initial = {}
  15
+    form_class = None
  16
+    success_url = None
  17
+
  18
+    def get_initial(self):
  19
+        """
  20
+        Returns the initial data to use for forms on this view.
  21
+        """
  22
+        return self.initial
  23
+
  24
+    def get_form_class(self):
  25
+        """
  26
+        Returns the form class to use in this view
  27
+        """
  28
+        return self.form_class
  29
+
  30
+    def get_form(self, form_class):
  31
+        """
  32
+        Returns an instance of the form to be used in this view.
  33
+        """
  34
+        if self.request.method in ('POST', 'PUT'):
  35
+            return form_class(
  36
+                self.request.POST,
  37
+                self.request.FILES,
  38
+                initial=self.get_initial()
  39
+            )
  40
+        else:
  41
+            return form_class(
  42
+                initial=self.get_initial()
  43
+            )
  44
+
  45
+    def get_context_data(self, **kwargs):
  46
+        return kwargs
  47
+
  48
+    def get_success_url(self):
  49
+        if self.success_url:
  50
+            url = self.success_url
  51
+        else:
  52
+            raise ImproperlyConfigured(
  53
+                "No URL to redirect to.  Either provide a url or define"
  54
+                " a get_absolute_url method on the Model.")
  55
+        return url
  56
+
  57
+    def form_valid(self, form):
  58
+        return HttpResponseRedirect(self.get_success_url())
  59
+
  60
+    def form_invalid(self, form):
  61
+        return self.render_to_response(self.get_context_data(form=form))
  62
+
  63
+
  64
+class ModelFormMixin(FormMixin, SingleObjectMixin):
  65
+    """
  66
+    A mixin that provides a way to show and handle a modelform in a request.
  67
+    """
  68
+
  69
+    def get_form_class(self):
  70
+        """
  71
+        Returns the form class to use in this view
  72
+        """
  73
+        if self.form_class:
  74
+            return self.form_class
  75
+        else:
  76
+            if self.model is None:
  77
+                model = self.queryset.model
  78
+            else:
  79
+                model = self.model
  80
+            return model_forms.modelform_factory(model)
  81
+
  82
+    def get_form(self, form_class):
  83
+        """
  84
+        Returns a form instantiated with the model instance from get_object().
  85
+        """
  86
+        if self.request.method in ('POST', 'PUT'):
  87
+            return form_class(
  88
+                self.request.POST,
  89
+                self.request.FILES,
  90
+                initial=self.get_initial(),
  91
+                instance=self.object,
  92
+            )
  93
+        else:
  94
+            return form_class(
  95
+                initial=self.get_initial(),
  96
+                instance=self.object,
  97
+            )
  98
+
  99
+    def get_success_url(self):
  100
+        if self.success_url:
  101
+            url = self.success_url
  102
+        else:
  103
+            try:
  104
+                url = self.object.get_absolute_url()
  105
+            except AttributeError:
  106
+                raise ImproperlyConfigured(
  107
+                    "No URL to redirect to.  Either provide a url or define"
  108
+                    " a get_absolute_url method on the Model.")
  109
+        return url
  110
+
  111
+    def form_valid(self, form):
  112
+        self.object = form.save()
  113
+        return super(ModelFormMixin, self).form_valid(form)
  114
+
  115
+    def form_invalid(self, form):
  116
+        return self.render_to_response(self.get_context_data(form=form))
  117
+
  118
+    def get_context_data(self, **kwargs):
  119
+        context = kwargs
  120
+        if self.object:
  121
+            context['object'] = self.object
  122
+            context_object_name = self.get_context_object_name(self.object)
  123
+            if context_object_name:
  124
+                context[context_object_name] = self.object
  125
+        return context
  126
+
  127
+
  128
+class ProcessFormView(View):
  129
+    """
  130
+    A mixin that processes a form on POST.
  131
+    """
  132
+    def get(self, request, *args, **kwargs):
  133
+        form_class = self.get_form_class()
  134
+        form = self.get_form(form_class)
  135
+        return self.render_to_response(self.get_context_data(form=form))
  136
+
  137
+    def post(self, request, *args, **kwargs):
  138
+        form_class = self.get_form_class()
  139
+        form = self.get_form(form_class)
  140
+        if form.is_valid():
  141
+            return self.form_valid(form)
  142
+        else:
  143
+            return self.form_invalid(form)
  144
+
  145
+    # PUT is a valid HTTP verb for creating (with a known URL) or editing an
  146
+    # object, note that browsers only support POST for now.
  147
+    put = post
  148
+