summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/views/generic/__init__.py9
-rw-r--r--django/views/generic/base.py190
-rw-r--r--django/views/generic/create_update.py8
-rw-r--r--django/views/generic/date_based.py7
-rw-r--r--django/views/generic/dates.py595
-rw-r--r--django/views/generic/detail.py142
-rw-r--r--django/views/generic/edit.py249
-rw-r--r--django/views/generic/list.py138
-rw-r--r--django/views/generic/list_detail.py7
-rw-r--r--django/views/generic/simple.py6
-rw-r--r--docs/index.txt6
-rw-r--r--docs/internals/deprecation.txt9
-rw-r--r--docs/intro/tutorial04.txt140
-rw-r--r--docs/ref/class-based-views.txt1391
-rw-r--r--docs/ref/generic-views.txt2
-rw-r--r--docs/ref/index.txt10
-rw-r--r--docs/releases/1.3.txt29
-rw-r--r--docs/topics/class-based-views.txt535
-rw-r--r--docs/topics/generic-views-migration.txt127
-rw-r--r--docs/topics/index.txt10
-rw-r--r--tests/regressiontests/generic_views/__init__.py0
-rw-r--r--tests/regressiontests/generic_views/base.py233
-rw-r--r--tests/regressiontests/generic_views/dates.py352
-rw-r--r--tests/regressiontests/generic_views/detail.py71
-rw-r--r--tests/regressiontests/generic_views/edit.py233
-rw-r--r--tests/regressiontests/generic_views/fixtures/generic-views-test-data.json47
-rw-r--r--tests/regressiontests/generic_views/forms.py11
-rw-r--r--tests/regressiontests/generic_views/list.py129
-rw-r--r--tests/regressiontests/generic_views/models.py41
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/about.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/apple_detail.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/artist_detail.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/artist_form.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/author_confirm_delete.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/author_detail.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/author_form.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/author_list.html3
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/author_objects.html3
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/author_view.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/book_archive.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/book_archive_day.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/book_archive_month.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/book_archive_week.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/book_detail.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/book_list.html3
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/confirm_delete.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/detail.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/form.html1
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/list.html3
-rw-r--r--tests/regressiontests/generic_views/templates/generic_views/page_template.html1
-rw-r--r--tests/regressiontests/generic_views/templates/registration/login.html1
-rw-r--r--tests/regressiontests/generic_views/tests.py5
-rw-r--r--tests/regressiontests/generic_views/urls.py186
-rw-r--r--tests/regressiontests/generic_views/views.py145
55 files changed, 5020 insertions, 74 deletions
diff --git a/django/views/generic/__init__.py b/django/views/generic/__init__.py
index 95c5fa9c50..818454b367 100644
--- a/django/views/generic/__init__.py
+++ b/django/views/generic/__init__.py
@@ -1,3 +1,12 @@
+from django.views.generic.base import View, TemplateView, RedirectView
+from django.views.generic.dates import (ArchiveIndexView, YearArchiveView, MonthArchiveView,
+ WeekArchiveView, DayArchiveView, TodayArchiveView,
+ DateDetailView)
+from django.views.generic.detail import DetailView
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
+from django.views.generic.list import ListView
+
+
class GenericViewError(Exception):
"""A problem in a generic view."""
pass
diff --git a/django/views/generic/base.py b/django/views/generic/base.py
new file mode 100644
index 0000000000..d9d24a39de
--- /dev/null
+++ b/django/views/generic/base.py
@@ -0,0 +1,190 @@
+import copy
+from django import http
+from django.core.exceptions import ImproperlyConfigured
+from django.template import RequestContext, loader
+from django.utils.translation import ugettext_lazy as _
+from django.utils.functional import update_wrapper
+from django.utils.log import getLogger
+
+logger = getLogger('django.request')
+
+class classonlymethod(classmethod):
+ def __get__(self, instance, owner):
+ if instance is not None:
+ raise AttributeError("This method is available only on the view class.")
+ return super(classonlymethod, self).__get__(instance, owner)
+
+class View(object):
+ """
+ Intentionally simple parent class for all views. Only implements
+ dispatch-by-method and simple sanity checking.
+ """
+
+ http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
+
+ def __init__(self, **kwargs):
+ """
+ Constructor. Called in the URLconf; can contain helpful extra
+ keyword arguments, and other things.
+ """
+ # Go through keyword arguments, and either save their values to our
+ # instance, or raise an error.
+ for key, value in kwargs.iteritems():
+ setattr(self, key, value)
+
+ @classonlymethod
+ def as_view(cls, **initkwargs):
+ """
+ Main entry point for a request-response process.
+ """
+ # sanitize keyword arguments
+ for key in initkwargs:
+ if key in cls.http_method_names:
+ raise TypeError(u"You tried to pass in the %s method name as a "
+ u"keyword argument to %s(). Don't do that."
+ % (key, cls.__name__))
+ if not hasattr(cls, key):
+ raise TypeError(u"%s() received an invalid keyword %r" % (
+ cls.__name__, key))
+
+ def view(request, *args, **kwargs):
+ self = cls(**initkwargs)
+ return self.dispatch(request, *args, **kwargs)
+
+ # take name and docstring from class
+ update_wrapper(view, cls, updated=())
+
+ # and possible attributes set by decorators
+ # like csrf_exempt from dispatch
+ update_wrapper(view, cls.dispatch, assigned=())
+ return view
+
+ def dispatch(self, request, *args, **kwargs):
+ # Try to dispatch to the right method for that; if it doesn't exist,
+ # defer to the error handler. Also defer to the error handler if the
+ # request method isn't on the approved list.
+ if request.method.lower() in self.http_method_names:
+ handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
+ else:
+ handler = self.http_method_not_allowed
+ self.request = request
+ self.args = args
+ self.kwargs = kwargs
+ return handler(request, *args, **kwargs)
+
+ def http_method_not_allowed(self, request, *args, **kwargs):
+ allowed_methods = [m for m in self.http_method_names if hasattr(self, m)]
+ return http.HttpResponseNotAllowed(allowed_methods)
+
+
+class TemplateResponseMixin(object):
+ """
+ A mixin that can be used to render a template.
+ """
+ template_name = None
+
+ def render_to_response(self, context):
+ """
+ Returns a response with a template rendered with the given context.
+ """
+ return self.get_response(self.render_template(context))
+
+ def get_response(self, content, **httpresponse_kwargs):
+ """
+ Construct an `HttpResponse` object.
+ """
+ return http.HttpResponse(content, **httpresponse_kwargs)
+
+ def render_template(self, context):
+ """
+ Render the template with a given context.
+ """
+ context_instance = self.get_context_instance(context)
+ return self.get_template().render(context_instance)
+
+ def get_context_instance(self, context):
+ """
+ Get the template context instance. Must return a Context (or subclass)
+ instance.
+ """
+ return RequestContext(self.request, context)
+
+ def get_template(self):
+ """
+ Get a ``Template`` object for the given request.
+ """
+ names = self.get_template_names()
+ if not names:
+ raise ImproperlyConfigured(u"'%s' must provide template_name."
+ % self.__class__.__name__)
+ return self.load_template(names)
+
+ def get_template_names(self):
+ """
+ Return a list of template names to be used for the request. Must return
+ a list. May not be called if get_template is overridden.
+ """
+ if self.template_name is None:
+ return []
+ else:
+ return [self.template_name]
+
+ def load_template(self, names):
+ """
+ Load a list of templates using the default template loader.
+ """
+ return loader.select_template(names)
+
+
+class TemplateView(TemplateResponseMixin, View):
+ """
+ A view that renders a template.
+ """
+ def get_context_data(self, **kwargs):
+ return {
+ 'params': kwargs
+ }
+
+ def get(self, request, *args, **kwargs):
+ context = self.get_context_data(**kwargs)
+ return self.render_to_response(context)
+
+
+class RedirectView(View):
+ """
+ A view that provides a redirect on any GET request.
+ """
+ permanent = True
+ url = None
+ query_string = False
+
+ def get_redirect_url(self, **kwargs):
+ """
+ Return the URL redirect to. Keyword arguments from the
+ URL pattern match generating the redirect request
+ are provided as kwargs to this method.
+ """
+ if self.url:
+ args = self.request.META["QUERY_STRING"]
+ if args and self.query_string:
+ url = "%s?%s" % (self.url, args)
+ else:
+ url = self.url
+ return url % kwargs
+ else:
+ return None
+
+ def get(self, request, *args, **kwargs):
+ url = self.get_redirect_url(**kwargs)
+ if url:
+ if self.permanent:
+ return http.HttpResponsePermanentRedirect(url)
+ else:
+ return http.HttpResponseRedirect(url)
+ else:
+ logger.warning('Gone: %s' % self.request.path,
+ extra={
+ 'status_code': 410,
+ 'request': self.request
+ })
+ return http.HttpResponseGone()
diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py
index 76575f9f89..8cf29eed4b 100644
--- a/django/views/generic/create_update.py
+++ b/django/views/generic/create_update.py
@@ -8,6 +8,12 @@ from django.contrib.auth.views import redirect_to_login
from django.views.generic import GenericViewError
from django.contrib import messages
+import warnings
+warnings.warn(
+ 'Function-based generic views have been deprecated; use class-based views instead.',
+ PendingDeprecationWarning
+)
+
def apply_extra_context(extra_context, context):
"""
@@ -111,7 +117,7 @@ def create_object(request, model=None, template_name=None,
form = form_class(request.POST, request.FILES)
if form.is_valid():
new_object = form.save()
-
+
msg = ugettext("The %(verbose_name)s was created successfully.") %\
{"verbose_name": model._meta.verbose_name}
messages.success(request, msg, fail_silently=True)
diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py
index 726df59654..9c5bb270c6 100644
--- a/django/views/generic/date_based.py
+++ b/django/views/generic/date_based.py
@@ -7,6 +7,13 @@ from django.core.xheaders import populate_xheaders
from django.db.models.fields import DateTimeField
from django.http import Http404, HttpResponse
+import warnings
+warnings.warn(
+ 'Function-based generic views have been deprecated; use class-based views instead.',
+ PendingDeprecationWarning
+)
+
+
def archive_index(request, queryset, date_field, num_latest=15,
template_name=None, template_loader=loader,
extra_context=None, allow_empty=True, context_processors=None,
diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py
new file mode 100644
index 0000000000..fe40b15580
--- /dev/null
+++ b/django/views/generic/dates.py
@@ -0,0 +1,595 @@
+import time
+import datetime
+from django.db import models
+from django.core.exceptions import ImproperlyConfigured
+from django.http import Http404
+from django.views.generic.base import View
+from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin
+from django.views.generic.list import MultipleObjectMixin, MultipleObjectTemplateResponseMixin
+
+
+class YearMixin(object):
+ year_format = '%Y'
+ year = None
+
+ def get_year_format(self):
+ """
+ Get a year format string in strptime syntax to be used to parse the
+ year from url variables.
+ """
+ return self.year_format
+
+ def get_year(self):
+ "Return the year for which this view should display data"
+ year = self.year
+ if year is None:
+ try:
+ year = self.kwargs['year']
+ except KeyError:
+ try:
+ year = self.request.GET['year']
+ except KeyError:
+ raise Http404("No year specified")
+ return year
+
+
+class MonthMixin(object):
+ month_format = '%b'
+ month = None
+
+ def get_month_format(self):
+ """
+ Get a month format string in strptime syntax to be used to parse the
+ month from url variables.
+ """
+ return self.month_format
+
+ def get_month(self):
+ "Return the month for which this view should display data"
+ month = self.month
+ if month is None:
+ try:
+ month = self.kwargs['month']
+ except KeyError:
+ try:
+ month = self.request.GET['month']
+ except KeyError:
+ raise Http404("No month specified")
+ return month
+
+ def get_next_month(self, date):
+ """
+ Get the next valid month.
+ """
+ first_day, last_day = _month_bounds(date)
+ next = (last_day + datetime.timedelta(days=1)).replace(day=1)
+ return _get_next_prev_month(self, next, is_previous=False, use_first_day=True)
+
+ def get_previous_month(self, date):
+ """
+ Get the previous valid month.
+ """
+ first_day, last_day = _month_bounds(date)
+ prev = (first_day - datetime.timedelta(days=1)).replace(day=1)
+ return _get_next_prev_month(self, prev, is_previous=True, use_first_day=True)
+
+
+class DayMixin(object):
+ day_format = '%d'
+ day = None
+
+ def get_day_format(self):
+ """
+ Get a month format string in strptime syntax to be used to parse the
+ month from url variables.
+ """
+ return self.day_format
+
+ def get_day(self):
+ "Return the day for which this view should display data"
+ day = self.day
+ if day is None:
+ try:
+ day = self.kwargs['day']
+ except KeyError:
+ try:
+ day = self.request.GET['day']
+ except KeyError:
+ raise Http404("No day specified")
+ return day
+
+ def get_next_day(self, date):
+ """
+ Get the next valid day.
+ """
+ next = date + datetime.timedelta(days=1)
+ return _get_next_prev_month(self, next, is_previous=False, use_first_day=False)
+
+ def get_previous_day(self, date):
+ """
+ Get the previous valid day.
+ """
+ prev = date - datetime.timedelta(days=1)
+ return _get_next_prev_month(self, prev, is_previous=True, use_first_day=False)
+
+
+class WeekMixin(object):
+ week_format = '%U'
+ week = None
+
+ def get_week_format(self):
+ """
+ Get a week format string in strptime syntax to be used to parse the
+ week from url variables.
+ """
+ return self.week_format
+
+ def get_week(self):
+ "Return the week for which this view should display data"
+ week = self.week
+ if week is None:
+ try:
+ week = self.kwargs['week']
+ except KeyError:
+ try:
+ week = self.request.GET['week']
+ except KeyError:
+ raise Http404("No week specified")
+ return week
+
+
+class DateMixin(object):
+ """
+ Mixin class for views manipulating date-based data.
+ """
+ date_field = None
+ allow_future = False
+
+ def get_date_field(self):
+ """
+ Get the name of the date field to be used to filter by.
+ """
+ if self.date_field is None:
+ raise ImproperlyConfigured(u"%s.date_field is required." % self.__class__.__name__)
+ return self.date_field
+
+ def get_allow_future(self):
+ """
+ Returns `True` if the view should be allowed to display objects from
+ the future.
+ """
+ return self.allow_future
+
+
+class BaseDateListView(MultipleObjectMixin, DateMixin, View):
+ """
+ Abstract base class for date-based views display a list of objects.
+ """
+ allow_empty = False
+
+ def get(self, request, *args, **kwargs):
+ self.date_list, self.object_list, extra_context = self.get_dated_items()
+ context = self.get_context_data(object_list=self.object_list,
+ date_list=self.date_list)
+ context.update(extra_context)
+ return self.render_to_response(context)
+
+ def get_dated_items(self):
+ """
+ Obtain the list of dates and itesm
+ """
+ raise NotImplemented('A DateView must provide an implementaiton of get_dated_items()')
+
+ def get_dated_queryset(self, **lookup):
+ """
+ Get a queryset properly filtered according to `allow_future` and any
+ extra lookup kwargs.
+ """
+ qs = self.get_queryset().filter(**lookup)
+ date_field = self.get_date_field()
+ allow_future = self.get_allow_future()
+ allow_empty = self.get_allow_empty()
+
+ if not allow_future:
+ qs = qs.filter(**{'%s__lte' % date_field: datetime.datetime.now()})
+
+ if not allow_empty and not qs:
+ raise Http404(u"No %s available" % unicode(qs.model._meta.verbose_name_plural))
+
+ return qs
+
+ def get_date_list(self, queryset, date_type):
+ """
+ Get a date list by calling `queryset.dates()`, checking along the way
+ for empty lists that aren't allowed.
+ """
+ date_field = self.get_date_field()
+ allow_empty = self.get_allow_empty()
+
+ date_list = queryset.dates(date_field, date_type)[::-1]
+ if date_list is not None and not date_list and not allow_empty:
+ raise Http404(u"No %s available" % unicode(qs.model._meta.verbose_name_plural))
+
+ return date_list
+
+
+
+ def get_context_data(self, **kwargs):
+ """
+ Get the context. Must return a Context (or subclass) instance.
+ """
+ items = kwargs.pop('object_list')
+ context = super(BaseDateListView, self).get_context_data(object_list=items)
+ context.update(kwargs)
+ return context
+
+
+class BaseArchiveIndexView(BaseDateListView):
+ """
+ Base class for archives of date-based items.
+
+ Requires a response mixin.
+ """
+ context_object_name = 'latest'
+
+ def get_dated_items(self):
+ """
+ Return (date_list, items, extra_context) for this request.
+ """
+ qs = self.get_dated_queryset()
+ date_list = self.get_date_list(qs, 'year')
+
+ if date_list:
+ object_list = qs.order_by('-'+self.get_date_field())
+ else:
+ object_list = qs.none()
+
+ return (date_list, object_list, {})
+
+
+class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView):
+ """
+ Top-level archive of date-based items.
+ """
+ template_name_suffix = '_archive'
+
+
+class BaseYearArchiveView(YearMixin, BaseDateListView):
+ """
+ List of objects published in a given year.
+ """
+ make_object_list = False
+
+ def get_dated_items(self):
+ """
+ Return (date_list, items, extra_context) for this request.
+ """
+ # Yes, no error checking: the URLpattern ought to validate this; it's
+ # an error if it doesn't.
+ year = self.get_year()
+ date_field = self.get_date_field()
+ qs = self.get_dated_queryset(**{date_field+'__year': year})
+ date_list = self.get_date_list(qs, 'month')
+
+ if self.get_make_object_list():
+ object_list = qs.order_by('-'+date_field)
+ else:
+ # We need this to be a queryset since parent classes introspect it
+ # to find information about the model.
+ object_list = qs.none()
+
+ return (date_list, object_list, {'year': year})
+
+ def get_make_object_list(self):
+ """
+ Return `True` if this view should contain the full list of objects in
+ the given year.
+ """
+ return self.make_object_list
+
+
+class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView):
+ """
+ List of objects published in a given year.
+ """
+ template_name_suffix = '_archive_year'
+
+
+class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView):
+ """
+ List of objects published in a given year.
+ """
+ def get_dated_items(self):
+ """
+ Return (date_list, items, extra_context) for this request.
+ """
+ year = self.get_year()
+ month = self.get_month()
+
+ date_field = self.get_date_field()
+ date = _date_from_string(year, self.get_year_format(),
+ month, self.get_month_format())
+
+ # Construct a date-range lookup.
+ first_day, last_day = _month_bounds(date)
+ lookup_kwargs = {
+ '%s__gte' % date_field: first_day,
+ '%s__lt' % date_field: last_day,
+ }
+
+ qs = self.get_dated_queryset(**lookup_kwargs)
+ date_list = self.get_date_list(qs, 'day')
+
+ return (date_list, qs, {
+ 'month': date,
+ 'next_month': self.get_next_month(date),
+ 'previous_month': self.get_previous_month(date),
+ })
+
+
+
+class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView):
+ """
+ List of objects published in a given year.
+ """
+ template_name_suffix = '_archive_month'
+
+
+class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView):
+ """
+ List of objects published in a given week.
+ """
+
+ def get_dated_items(self):
+ """
+ Return (date_list, items, extra_context) for this request.
+ """
+ year = self.get_year()
+ week = self.get_week()
+
+ date_field = self.get_date_field()
+ date = _date_from_string(year, self.get_year_format(),
+ '0', '%w',
+ week, self.get_week_format())
+
+ # Construct a date-range lookup.
+ first_day = date
+ last_day = date + datetime.timedelta(days=7)
+ lookup_kwargs = {
+ '%s__gte' % date_field: first_day,
+ '%s__lt' % date_field: last_day,
+ }
+
+ qs = self.get_dated_queryset(**lookup_kwargs)
+
+ return (None, qs, {'week': date})
+
+
+class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView):
+ """
+ List of objects published in a given week.
+ """
+ template_name_suffix = '_archive_week'
+
+
+class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView):
+ """
+ List of objects published on a given day.
+ """
+ def get_dated_items(self):
+ """
+ Return (date_list, items, extra_context) for this request.
+ """
+ year = self.get_year()
+ month = self.get_month()
+ day = self.get_day()
+
+ date = _date_from_string(year, self.get_year_format(),
+ month, self.get_month_format(),
+ day, self.get_day_format())
+
+ return self._get_dated_items(date)
+
+ def _get_dated_items(self, date):
+ """
+ Do the actual heavy lifting of getting the dated items; this accepts a
+ date object so that TodayArchiveView can be trivial.
+ """
+ date_field = self.get_date_field()
+
+ field = self.get_queryset().model._meta.get_field(date_field)
+ lookup_kwargs = _date_lookup_for_field(field, date)
+
+ qs = self.get_dated_queryset(**lookup_kwargs)
+
+ return (None, qs, {
+ 'day': date,
+ 'previous_day': self.get_previous_day(date),
+ 'next_day': self.get_next_day(date),
+ 'previous_month': self.get_previous_month(date),
+ 'next_month': self.get_next_month(date)
+ })
+
+
+
+class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView):
+ """
+ List of objects published on a given day.
+ """
+ template_name_suffix = "_archive_day"
+
+
+class BaseTodayArchiveView(BaseDayArchiveView):
+ """
+ List of objects published today.
+ """
+
+ def get_dated_items(self):
+ """
+ Return (date_list, items, extra_context) for this request.
+ """
+ return self._get_dated_items(datetime.date.today())
+
+
+class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView):
+ """
+ List of objects published today.
+ """
+ template_name_suffix = "_archive_day"
+
+
+class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailView):
+ """
+ Detail view of a single object on a single date; this differs from the
+ standard DetailView by accepting a year/month/day in the URL.
+ """
+ def get_object(self, queryset=None, **kwargs):
+ """
+ Get the object this request displays.
+ """
+ year = self.get_year()
+ month = self.get_month()
+ day = self.get_day()
+ date = _date_from_string(year, self.get_year_format(),
+ month, self.get_month_format(),
+ day, self.get_day_format())
+
+ qs = self.get_queryset()
+
+ if not self.get_allow_future() and date > datetime.date.today():
+ raise Http404("Future %s not available because %s.allow_future is False." % (
+ qs.model._meta.verbose_name_plural, self.__class__.__name__)
+ )
+
+ # Filter down a queryset from self.queryset using the date from the
+ # URL. This'll get passed as the queryset to DetailView.get_object,
+ # which'll handle the 404
+ date_field = self.get_date_field()
+ field = qs.model._meta.get_field(date_field)
+ lookup = _date_lookup_for_field(field, date)
+ qs = qs.filter(**lookup)
+
+ return super(BaseDetailView, self).get_object(queryset=qs, **kwargs)
+
+
+
+class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView):
+ """
+ Detail view of a single object on a single date; this differs from the
+ standard DetailView by accepting a year/month/day in the URL.
+ """
+ template_name_suffix = '_detail'
+
+
+def _date_from_string(year, year_format, month, month_format, day='', day_format='', delim='__'):
+ """
+ Helper: get a datetime.date object given a format string and a year,
+ month, and possibly day; raise a 404 for an invalid date.
+ """
+ format = delim.join((year_format, month_format, day_format))
+ datestr = delim.join((year, month, day))
+ try:
+ return datetime.date(*time.strptime(datestr, format)[:3])
+ except ValueError:
+ raise Http404(u"Invalid date string '%s' given format '%s'" % (datestr, format))
+
+def _month_bounds(date):
+ """
+ Helper: return the first and last days of the month for the given date.
+ """
+ first_day = date.replace(day=1)
+ if first_day.month == 12:
+ last_day = first_day.replace(year=first_day.year + 1, month=1)
+ else:
+ last_day = first_day.replace(month=first_day.month + 1)
+
+ return first_day, last_day
+
+def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day):
+ """
+ Helper: Get the next or the previous valid date. The idea is to allow
+ links on month/day views to never be 404s by never providing a date
+ that'll be invalid for the given view.
+
+ This is a bit complicated since it handles both next and previous months
+ and days (for MonthArchiveView and DayArchiveView); hence the coupling to generic_view.
+
+ However in essance the logic comes down to:
+
+ * If allow_empty and allow_future are both true, this is easy: just
+ return the naive result (just the next/previous day or month,
+ reguardless of object existence.)
+
+ * If allow_empty is true, allow_future is false, and the naive month
+ isn't in the future, then return it; otherwise return None.
+
+ * If allow_empty is false and allow_future is true, return the next
+ date *that contains a valid object*, even if it's in the future. If
+ there are no next objects, return None.
+
+ * If allow_empty is false and allow_future is false, return the next
+ date that contains a valid object. If that date is in the future, or
+ if there are no next objects, return None.
+
+ """
+ date_field = generic_view.get_date_field()
+ allow_empty = generic_view.get_allow_empty()
+ allow_future = generic_view.get_allow_future()
+
+ # If allow_empty is True the naive value will be valid
+ if allow_empty:
+ result = naive_result
+
+ # Otherwise, we'll need to go to the database to look for an object
+ # whose date_field is at least (greater than/less than) the given
+ # naive result
+ else:
+ # Construct a lookup and an ordering depending on weather we're doing
+ # a previous date or a next date lookup.
+ if is_previous:
+ lookup = {'%s__lte' % date_field: naive_result}
+ ordering = '-%s' % date_field
+ else:
+ lookup = {'%s__gte' % date_field: naive_result}
+ ordering = date_field
+
+ qs = generic_view.get_queryset().filter(**lookup).order_by(ordering)
+
+ # Snag the first object from the queryset; if it doesn't exist that
+ # means there's no next/previous link available.
+ try:
+ result = getattr(qs[0], date_field)
+ except IndexError:
+ result = None
+
+ # Convert datetimes to a dates
+ if hasattr(result, 'date'):
+ result = result.date()
+
+ # For month views, we always want to have a date that's the first of the
+ # month for consistancy's sake.
+ if result and use_first_day:
+ result = result.replace(day=1)
+
+ # Check against future dates.
+ if result and (allow_future or result < datetime.date.today()):
+ return result
+ else:
+ return None
+
+def _date_lookup_for_field(field, date):
+ """
+ Get the lookup kwargs for looking up a date against a given Field. If the
+ date field is a DateTimeField, we can't just do filter(df=date) because
+ that doesn't take the time into account. So we need to make a range lookup
+ in those cases.
+ """
+ if isinstance(field, models.DateTimeField):
+ date_range = (
+ datetime.datetime.combine(date, datetime.time.min),
+ datetime.datetime.combine(date, datetime.time.max)
+ )
+ return {'%s__range' % field.name: date_range}
+ else:
+ return {field.name: date}
+
diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py
new file mode 100644
index 0000000000..55b8e6055e
--- /dev/null
+++ b/django/views/generic/detail.py
@@ -0,0 +1,142 @@
+import re
+
+from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
+from django.http import Http404
+from django.views.generic.base import TemplateResponseMixin, View
+
+
+class SingleObjectMixin(object):
+ """
+ Provides the ability to retrieve a single object for further manipulation.
+ """
+ model = None
+ queryset = None
+ slug_field = 'slug'
+ context_object_name = None
+
+ def get_object(self, pk=None, slug=None, queryset=None, **kwargs):
+ """
+ Returns the object the view is displaying.
+
+ By default this requires `self.queryset` and a `pk` or `slug` argument
+ in the URLconf, but subclasses can override this to return any object.
+ """
+ # Use a custom queryset if provided; this is required for subclasses
+ # like DateDetailView
+ if queryset is None:
+ queryset = self.get_queryset()
+
+ # Next, try looking up by primary key.
+ if pk is not None:
+ queryset = queryset.filter(pk=pk)
+
+ # Next, try looking up by slug.
+ elif slug is not None:
+ slug_field = self.get_slug_field()
+ queryset = queryset.filter(**{slug_field: slug})
+
+ # If none of those are defined, it's an error.
+ else:
+ raise AttributeError(u"Generic detail view %s must be called with "
+ u"either an object id or a slug."
+ % self.__class__.__name__)
+
+ try:
+ obj = queryset.get()
+ except ObjectDoesNotExist:
+ raise Http404(u"No %s found matching the query" %
+ (queryset.model._meta.verbose_name))
+ return obj
+
+ def get_queryset(self):
+ """
+ Get the queryset to look an object up against. May not be called if
+ `get_object` is overridden.
+ """
+ if self.queryset is None:
+ if self.model:
+ return self.model._default_manager.all()
+ else:
+ raise ImproperlyConfigured(u"%(cls)s is missing a queryset. Define "
+ u"%(cls)s.model, %(cls)s.queryset, or override "
+ u"%(cls)s.get_object()." % {
+ 'cls': self.__class__.__name__
+ })
+ return self.queryset._clone()
+
+ def get_slug_field(self):
+ """
+ Get the name of a slug field to be used to look up by slug.
+ """
+ return self.slug_field
+
+ def get_context_object_name(self, obj):
+ """
+ Get the name to use for the object.
+ """
+ if self.context_object_name:
+ return self.context_object_name
+ elif hasattr(obj, '_meta'):
+ return re.sub('[^a-zA-Z0-9]+', '_',
+ obj._meta.verbose_name.lower())
+ else:
+ return None
+
+ def get_context_data(self, **kwargs):
+ context = kwargs
+ context_object_name = self.get_context_object_name(self.object)
+ if context_object_name:
+ context[context_object_name] = self.object
+ return context
+
+
+class BaseDetailView(SingleObjectMixin, View):
+ def get(self, request, **kwargs):
+ self.object = self.get_object(**kwargs)
+ context = self.get_context_data(object=self.object)
+ return self.render_to_response(context)
+
+
+class SingleObjectTemplateResponseMixin(TemplateResponseMixin):
+ template_name_field = None
+ template_name_suffix = '_detail'
+
+ def get_template_names(self):
+ """
+ Return a list of template names to be used for the request. Must return
+ a list. May not be called if get_template is overridden.
+ """
+ names = super(SingleObjectTemplateResponseMixin, self).get_template_names()
+
+ # If self.template_name_field is set, grab the value of the field
+ # of that name from the object; this is the most specific template
+ # name, if given.
+ if self.object and self.template_name_field:
+ name = getattr(self.object, self.template_name_field, None)
+ if name:
+ names.insert(0, name)
+
+ # The least-specific option is the default <app>/<model>_detail.html;
+ # only use this if the object in question is a model.
+ if hasattr(self.object, '_meta'):
+ names.append("%s/%s%s.html" % (
+ self.object._meta.app_label,
+ self.object._meta.object_name.lower(),
+ self.template_name_suffix
+ ))
+ elif hasattr(self, 'model') and hasattr(self.model, '_meta'):
+ names.append("%s/%s%s.html" % (
+ self.model._meta.app_label,
+ self.model._meta.object_name.lower(),
+ self.template_name_suffix
+ ))
+ return names
+
+
+class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView):
+ """
+ Render a "detail" view of an object.
+
+ By default this is a model instance looked up from `self.queryset`, but the
+ view will support display of *any* object by overriding `self.get_object()`.
+ """
diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py
new file mode 100644
index 0000000000..d277493691
--- /dev/null
+++ b/django/views/generic/edit.py
@@ -0,0 +1,249 @@
+from django.forms import models as model_forms
+from django.core.exceptions import ImproperlyConfigured
+from django.http import HttpResponseRedirect
+from django.views.generic.base import TemplateResponseMixin, View
+from django.views.generic.detail import (SingleObjectMixin,
+ SingleObjectTemplateResponseMixin, BaseDetailView)
+
+
+class FormMixin(object):
+ """
+ A mixin that provides a way to show and handle a form in a request.
+ """
+
+ initial = {}
+ form_class = None
+ success_url = None
+
+ def get_initial(self):
+ """
+ Returns the initial data to use for forms on this view.
+ """
+ return self.initial
+
+ def get_form_class(self):
+ """
+ Returns the form class to use in this view
+ """
+ return self.form_class
+
+ def get_form(self, form_class):
+ """
+ Returns an instance of the form to be used in this view.
+ """
+ if self.request.method in ('POST', 'PUT'):
+ return form_class(
+ self.request.POST,
+ self.request.FILES,
+ initial=self.get_initial()
+ )
+ else:
+ return form_class(
+ initial=self.get_initial()
+ )
+
+ def get_context_data(self, **kwargs):
+ return kwargs
+
+ def get_success_url(self):
+ if self.success_url:
+ url = self.success_url
+ else:
+ raise ImproperlyConfigured(
+ "No URL to redirect to. Either provide a url or define"
+ " a get_absolute_url method on the Model.")
+ return url
+
+ def form_valid(self, form):
+ return HttpResponseRedirect(self.get_success_url())
+
+ def form_invalid(self, form):
+ return self.render_to_response(self.get_context_data(form=form))
+
+
+class ModelFormMixin(FormMixin, SingleObjectMixin):
+ """
+ A mixin that provides a way to show and handle a modelform in a request.
+ """
+
+ def get_form_class(self):
+ """
+ Returns the form class to use in this view
+ """
+ if self.form_class:
+ return self.form_class
+ else:
+ if self.model is None:
+ model = self.queryset.model
+ else:
+ model = self.model
+ return model_forms.modelform_factory(model)
+
+ def get_form(self, form_class):
+ """
+ Returns a form instantiated with the model instance from get_object().
+ """
+ if self.request.method in ('POST', 'PUT'):
+ return form_class(
+ self.request.POST,
+ self.request.FILES,
+ initial=self.get_initial(),
+ instance=self.object,
+ )
+ else:
+ return form_class(
+ initial=self.get_initial(),
+ instance=self.object,
+ )
+
+ def get_success_url(self):
+ if self.success_url:
+ url = self.success_url
+ else:
+ try:
+ url = self.object.get_absolute_url()
+ except AttributeError:
+ raise ImproperlyConfigured(
+ "No URL to redirect to. Either provide a url or define"
+ " a get_absolute_url method on the Model.")
+ return url
+
+ def form_valid(self, form):
+ self.object = form.save()
+ return super(ModelFormMixin, self).form_valid(form)
+
+ def form_invalid(self, form):
+ return self.render_to_response(self.get_context_data(form=form))
+
+ def get_context_data(self, **kwargs):
+ context = kwargs
+ if self.object:
+ context['object'] = self.object
+ context_object_name = self.get_context_object_name(self.object)
+ if context_object_name:
+ context[context_object_name] = self.object
+ return context
+
+
+class ProcessFormView(View):
+ """
+ A mixin that processes a form on POST.
+ """
+ def get(self, request, *args, **kwargs):
+ form_class = self.get_form_class()
+ form = self.get_form(form_class)
+ return self.render_to_response(self.get_context_data(form=form))
+
+ def post(self, request, *args, **kwargs):
+ form_class = self.get_form_class()
+ form = self.get_form(form_class)
+ if form.is_valid():
+ return self.form_valid(form)
+ else:
+ return self.form_invalid(form)
+
+ # PUT is a valid HTTP verb for creating (with a known URL) or editing an
+ # object, note that browsers only support POST for now.
+ put = post
+
+
+class BaseFormView(FormMixin, ProcessFormView):
+ """
+ A base view for displaying a form
+ """
+
+
+class FormView(TemplateResponseMixin, BaseFormView):
+ """
+ A view for displaying a form, and rendering a template response.
+ """
+
+
+class BaseCreateView(ModelFormMixin, ProcessFormView):
+ """
+ Base view for creating an new object instance.
+
+ Using this base class requires subclassing to provide a response mixin.
+ """
+ def get(self, request, *args, **kwargs):
+ self.object = None
+ return super(BaseCreateView, self).get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ self.object = None
+ return super(BaseCreateView, self).post(request, *args, **kwargs)
+
+ # PUT is a valid HTTP verb for creating (with a known URL) or editing an
+ # object, note that browsers only support POST for now.
+ put = post
+
+class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
+ """
+ View for creating an new object instance,
+ with a response rendered by template.
+ """
+ template_name_suffix = '_form'
+
+
+class BaseUpdateView(ModelFormMixin, ProcessFormView):
+ """
+ Base view for updating an existing object.
+
+ Using this base class requires subclassing to provide a response mixin.
+ """
+ def get(self, request, *args, **kwargs):
+ self.object = self.get_object(**kwargs)
+ return super(BaseUpdateView, self).get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ self.object = self.get_object(**kwargs)
+ return super(BaseUpdateView, self).post(request, *args, **kwargs)
+
+ # PUT is a valid HTTP verb for creating (with a known URL) or editing an
+ # object, note that browsers only support POST for now.
+ put = post
+
+
+class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView):
+ """
+ View for updating an object,
+ with a response rendered by template..
+ """
+ template_name_suffix = '_form'
+
+
+class DeletionMixin(object):
+ """
+ A mixin providing the ability to delete objects
+ """
+ success_url = None
+
+ def delete(self, request, *args, **kwargs):
+ self.object = self.get_object(**kwargs)
+ self.object.delete()
+ return HttpResponseRedirect(self.get_success_url())
+
+ # Add support for browsers which only accept GET and POST for now.
+ post = delete
+
+ def get_success_url(self):
+ if self.success_url:
+ return self.success_url
+ else:
+ raise ImproperlyConfigured(
+ "No URL to redirect to. Either provide a url or define"
+ " a get_absolute_url method on the Model.")
+
+class BaseDeleteView(DeletionMixin, BaseDetailView):
+ """
+ Base view for deleting an object.
+
+ Using this base class requires subclassing to provide a response mixin.
+ """
+
+class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
+ """
+ View for deleting an object retrieved with `self.get_object()`,
+ with a response rendered by template.
+ """
+ template_name_suffix = '_confirm_delete'
diff --git a/django/views/generic/list.py b/django/views/generic/list.py
new file mode 100644
index 0000000000..d01bacc956
--- /dev/null
+++ b/django/views/generic/list.py
@@ -0,0 +1,138 @@
+from django.core.paginator import Paginator, InvalidPage
+from django.core.exceptions import ImproperlyConfigured
+from django.http import Http404
+from django.utils.encoding import smart_str
+from django.views.generic.base import TemplateResponseMixin, View
+
+class MultipleObjectMixin(object):
+ allow_empty = True
+ queryset = None
+ model = None
+ paginate_by = None
+ context_object_name = None
+
+ def get_queryset(self):
+ """
+ Get the list of items for this view. This must be an interable, and may
+ be a queryset (in which qs-specific behavior will be enabled).
+ """
+ if self.queryset is not None:
+ queryset = self.queryset
+ if hasattr(queryset, '_clone'):
+ queryset = queryset._clone()
+ elif self.model is not None:
+ queryset = self.model._default_manager.all()
+ else:
+ raise ImproperlyConfigured(u"'%s' must define 'queryset' or 'model'"
+ % self.__class__.__name__)
+ return queryset
+
+ def paginate_queryset(self, queryset, page_size):
+ """
+ Paginate the queryset, if needed.
+ """
+ if queryset.count() > page_size:
+ paginator = Paginator(queryset, page_size, allow_empty_first_page=self.get_allow_empty())
+ page = self.kwargs.get('page', None) or self.request.GET.get('page', 1)
+ try:
+ page_number = int(page)
+ except ValueError:
+ if page == 'last':
+ page_number = paginator.num_pages
+ else:
+ raise Http404("Page is not 'last', nor can it be converted to an int.")
+ try:
+ page = paginator.page(page_number)
+ return (paginator, page, page.object_list, True)
+ except InvalidPage:
+ raise Http404(u'Invalid page (%s)' % page_number)
+ else:
+ return (None, None, queryset, False)
+
+ def get_paginate_by(self, queryset):
+ """
+ Get the number of items to paginate by, or ``None`` for no pagination.
+ """
+ return self.paginate_by
+
+ def get_allow_empty(self):
+ """
+ Returns ``True`` if the view should display empty lists, and ``False``
+ if a 404 should be raised instead.
+ """
+ return self.allow_empty
+
+ def get_context_object_name(self, object_list):
+ """
+ Get the name of the item to be used in the context.
+ """
+ if self.context_object_name:
+ return self.context_object_name
+ elif hasattr(object_list, 'model'):
+ return smart_str(object_list.model._meta.verbose_name_plural)
+ else:
+ return None
+
+ def get_context_data(self, **kwargs):
+ """
+ Get the context for this view.
+ """
+ queryset = kwargs.get('object_list')
+ page_size = self.get_paginate_by(queryset)
+ if page_size:
+ paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
+ context = {
+ 'paginator': paginator,
+ 'page_obj': page,
+ 'is_paginated': is_paginated,
+ 'object_list': queryset
+ }
+ else:
+ context = {
+ 'paginator': None,
+ 'page_obj': None,
+ 'is_paginated': False,
+ 'object_list': queryset
+ }
+ context.update(kwargs)
+ context_object_name = self.get_context_object_name(queryset)
+ if context_object_name is not None:
+ context[context_object_name] = queryset
+ return context
+
+
+class BaseListView(MultipleObjectMixin, View):
+ def get(self, request, *args, **kwargs):
+ self.object_list = self.get_queryset()
+ allow_empty = self.get_allow_empty()
+ if not allow_empty and len(self.object_list) == 0:
+ raise Http404(u"Empty list and '%s.allow_empty' is False."
+ % self.__class__.__name__)
+ context = self.get_context_data(object_list=self.object_list)
+ return self.render_to_response(context)
+
+class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
+ template_name_suffix = '_list'
+
+ def get_template_names(self):
+ """
+ Return a list of template names to be used for the request. Must return
+ a list. May not be called if get_template is overridden.
+ """
+ names = super(MultipleObjectTemplateResponseMixin, self).get_template_names()
+
+ # If the list is a queryset, we'll invent a template name based on the
+ # app and model name. This name gets put at the end of the template
+ # name list so that user-supplied names override the automatically-
+ # generated ones.
+ if hasattr(self.object_list, 'model'):
+ opts = self.object_list.model._meta
+ names.append("%s/%s%s.html" % (opts.app_label, opts.object_name.lower(), self.template_name_suffix))
+
+ return names
+
+class ListView(MultipleObjectTemplateResponseMixin, BaseListView):
+ """
+ Render some list of objects, set by `self.model` or `self.queryset`.
+ `self.queryset` can actually be any iterable of items, not just a queryset.
+ """
diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py
index 1e39c4d980..b876098a97 100644
--- a/django/views/generic/list_detail.py
+++ b/django/views/generic/list_detail.py
@@ -4,6 +4,13 @@ from django.core.xheaders import populate_xheaders
from django.core.paginator import Paginator, InvalidPage
from django.core.exceptions import ObjectDoesNotExist
+import warnings
+warnings.warn(
+ 'Function-based generic views have been deprecated; use class-based views instead.',
+ PendingDeprecationWarning
+)
+
+
def object_list(request, queryset, paginate_by=None, page=None,
allow_empty=True, template_name=None, template_loader=loader,
extra_context=None, context_processors=None, template_object_name='object',
diff --git a/django/views/generic/simple.py b/django/views/generic/simple.py
index 7c38f07688..2e2c4b638f 100644
--- a/django/views/generic/simple.py
+++ b/django/views/generic/simple.py
@@ -2,6 +2,12 @@ from django.template import loader, RequestContext
from django.http import HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseGone
from django.utils.log import getLogger
+import warnings
+warnings.warn(
+ 'Function-based generic views have been deprecated; use class-based views instead.',
+ PendingDeprecationWarning
+)
+
logger = getLogger('django.request')
diff --git a/docs/index.txt b/docs/index.txt
index b743176a84..e456d047ec 100644
--- a/docs/index.txt
+++ b/docs/index.txt
@@ -103,8 +103,8 @@ The view layer
:doc:`Custom storage <howto/custom-file-storage>`
* **Generic views:**
- :doc:`Overview<topics/generic-views>` |
- :doc:`Built-in generic views<ref/generic-views>`
+ :doc:`Overview<topics/class-based-views>` |
+ :doc:`Built-in generic views<ref/class-based-views>`
* **Advanced:**
:doc:`Generating CSV <howto/outputting-csv>` |
@@ -189,6 +189,8 @@ Other batteries included
* :doc:`Unicode in Django <ref/unicode>`
* :doc:`Web design helpers <ref/contrib/webdesign>`
* :doc:`Validators <ref/validators>`
+ * Function-based generic views (Deprecated) :doc:`Overview<topics/generic-views>` | :doc:`Built-in generic views<ref/generic-views>` | :doc:`Migration guide<topics/generic-views-migration>`
+
The Django open-source project
==============================
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index c227f9ab5c..c1341e03fa 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -118,6 +118,15 @@ their deprecation, as per the :ref:`Django deprecation policy
:func:`django.contrib.formtools.utils.security_hash`
is deprecated, in favour of :func:`django.contrib.formtools.utils.form_hmac`
+ * The function-based generic views have been deprecated in
+ favor of their class-based cousins. The following modules
+ will be removed:
+
+ * :mod:`django.views.generic.create_update`
+ * :mod:`django.views.generic.date_based`
+ * :mod:`django.views.generic.list_detail`
+ * :mod:`django.views.generic.simple`
+
* 2.0
* ``django.views.defaults.shortcut()``. This function has been moved
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt
index dfbd82df55..9568546291 100644
--- a/docs/intro/tutorial04.txt
+++ b/docs/intro/tutorial04.txt
@@ -232,6 +232,7 @@ tutorial so far::
Change it like so::
from django.conf.urls.defaults import *
+ from django.views.generic import DetailView, ListView
from polls.models import Poll
info_dict = {
@@ -239,88 +240,91 @@ Change it like so::
}
urlpatterns = patterns('',
- (r'^$', 'django.views.generic.list_detail.object_list', info_dict),
- (r'^(?P<object_id>\d+)/$', 'django.views.generic.list_detail.object_detail', info_dict),
- url(r'^(?P<object_id>\d+)/results/$', 'django.views.generic.list_detail.object_detail', dict(info_dict, template_name='polls/results.html'), 'poll_results'),
+ (r'^$',
+ ListView.as_view(
+ models=Poll,
+ context_object_name='latest_poll_list'
+ template_name='polls/index.html')),
+ (r'^(?P<pk>\d+)/$',
+ DetailView.as_view(
+ models=Poll,
+ template_name='polls/detail.html')),
+ url(r'^(?P<pk>\d+)/results/$',
+ DetailView.as_view(
+ models=Poll,
+ template_name='polls/results.html'),
+ 'poll_results'),
(r'^(?P<poll_id>\d+)/vote/$', 'polls.views.vote'),
)
We're using two generic views here:
-:func:`~django.views.generic.list_detail.object_list` and
-:func:`~django.views.generic.list_detail.object_detail`. Respectively, those two
-views abstract the concepts of "display a list of objects" and "display a detail
-page for a particular type of object."
+:class:`~django.views.generic.list.ListView` and
+:class:`~django.views.generic.detail.DetailView`. Respectively, those
+two views abstract the concepts of "display a list of objects" and
+"display a detail page for a particular type of object."
- * Each generic view needs to know what data it will be acting upon. This
- data is provided in a dictionary. The ``queryset`` key in this dictionary
- points to the list of objects to be manipulated by the generic view.
+ * Each generic view needs to know what model it will be acting
+ upon. This is provided using the ``model`` parameter.
- * The :func:`~django.views.generic.list_detail.object_detail` generic view
- expects the ID value captured from the URL to be called ``"object_id"``,
- so we've changed ``poll_id`` to ``object_id`` for the generic views.
+ * The :class:`~django.views.generic.list.DetailView` generic view
+ expects the primary key value captured from the URL to be called
+ ``"pk"``, so we've changed ``poll_id`` to ``pk`` for the generic
+ views.
- * We've added a name, ``poll_results``, to the results view so that we have
- a way to refer to its URL later on (see the documentation about
- :ref:`naming URL patterns <naming-url-patterns>` for information). We're
- also using the :func:`~django.conf.urls.default.url` function from
+ * We've added a name, ``poll_results``, to the results view so
+ that we have a way to refer to its URL later on (see the
+ documentation about :ref:`naming URL patterns
+ <naming-url-patterns>` for information). We're also using the
+ :func:`~django.conf.urls.default.url` function from
:mod:`django.conf.urls.defaults` here. It's a good habit to use
- :func:`~django.conf.urls.defaults.url` when you are providing a pattern
- name like this.
+ :func:`~django.conf.urls.defaults.url` when you are providing a
+ pattern name like this.
-By default, the :func:`~django.views.generic.list_detail.object_detail` generic
-view uses a template called ``<app name>/<model name>_detail.html``. In our
-case, it'll use the template ``"polls/poll_detail.html"``. Thus, rename your
-``polls/detail.html`` template to ``polls/poll_detail.html``, and change the
-:func:`~django.shortcuts.render_to_response` line in ``vote()``.
+By default, the :class:`~django.views.generic.list.DetailView` generic
+view uses a template called ``<app name>/<model name>_detail.html``.
+In our case, it'll use the template ``"polls/poll_detail.html"``. The
+``template_name`` argument is used to tell Django to use a specific
+template name instead of the autogenerated default template name. We
+also specify the ``template_name`` for the ``results`` list view --
+this ensures that the results view and the detail view have a
+different appearance when rendered, even though they're both a
+:class:`~django.views.generic.list.DetailView` behind the scenes.
-Similarly, the :func:`~django.views.generic.list_detail.object_list` generic
-view uses a template called ``<app name>/<model name>_list.html``. Thus, rename
-``polls/index.html`` to ``polls/poll_list.html``.
+Similarly, the :class:`~django.views.generic.list.ListView` generic
+view uses a default template called ``<app name>/<model
+name>_list.html``; we use ``template_name`` to tell
+:class:`~django.views.generic.list.ListView` to use our existing
+``"polls/index.html"`` template.
-Because we have more than one entry in the URLconf that uses
-:func:`~django.views.generic.list_detail.object_detail` for the polls app, we
-manually specify a template name for the results view:
-``template_name='polls/results.html'``. Otherwise, both views would use the same
-template. Note that we use ``dict()`` to return an altered dictionary in place.
+In previous parts of the tutorial, the templates have been provided
+with a context that contains the ``poll`` and ``latest_poll_list``
+context variables. For DetailView the ``poll`` variable is provided
+automatically -- since we're using a Django model (``Poll``), Django
+is able to determine an appropriate name for the context variable.
+However, for ListView, the automatically generated context variable is
+``poll_list``. To override this we provide the ``context_object_name``
+option, specifying that we want to use ``latest_poll_list`` instead.
+As an alternative approach, you could change your templates to match
+the new default context variables -- but it's a lot easier to just
+tell Django to use the variable you want.
-.. note:: :meth:`django.db.models.QuerySet.all` is lazy
+You can now delete the ``index()``, ``detail()`` and ``results()``
+views from ``polls/views.py``. We don't need them anymore -- they have
+been replaced by generic views.
- It might look a little frightening to see ``Poll.objects.all()`` being used
- in a detail view which only needs one ``Poll`` object, but don't worry;
- ``Poll.objects.all()`` is actually a special object called a
- :class:`~django.db.models.QuerySet`, which is "lazy" and doesn't hit your
- database until it absolutely has to. By the time the database query happens,
- the :func:`~django.views.generic.list_detail.object_detail` generic view
- will have narrowed its scope down to a single object, so the eventual query
- will only select one row from the database.
+The ``vote()`` view is still required. However, it must be modified to
+match the new context variables. In the
+:func:`~django.shortcuts.render_to_response` call, rename the ``poll``
+context variable to ``object``.
- If you'd like to know more about how that works, The Django database API
- documentation :ref:`explains the lazy nature of QuerySet objects
- <querysets-are-lazy>`.
-
-In previous parts of the tutorial, the templates have been provided with a
-context that contains the ``poll`` and ``latest_poll_list`` context variables.
-However, the generic views provide the variables ``object`` and ``object_list``
-as context. Therefore, you need to change your templates to match the new
-context variables. Go through your templates, and modify any reference to
-``latest_poll_list`` to ``object_list``, and change any reference to ``poll``
-to ``object``.
-
-You can now delete the ``index()``, ``detail()`` and ``results()`` views
-from ``polls/views.py``. We don't need them anymore -- they have been replaced
-by generic views.
-
-The ``vote()`` view is still required. However, it must be modified to match the
-new context variables. In the :func:`~django.shortcuts.render_to_response` call,
-rename the ``poll`` context variable to ``object``.
-
-The last thing to do is fix the URL handling to account for the use of generic
-views. In the vote view above, we used the
-:func:`~django.core.urlresolvers.reverse` function to avoid hard-coding our
-URLs. Now that we've switched to a generic view, we'll need to change the
-:func:`~django.core.urlresolvers.reverse` call to point back to our new generic
-view. We can't simply use the view function anymore -- generic views can be (and
-are) used multiple times -- but we can use the name we've given::
+The last thing to do is fix the URL handling to account for the use of
+generic views. In the vote view above, we used the
+:func:`~django.core.urlresolvers.reverse` function to avoid
+hard-coding our URLs. Now that we've switched to a generic view, we'll
+need to change the :func:`~django.core.urlresolvers.reverse` call to
+point back to our new generic view. We can't simply use the view
+function anymore -- generic views can be (and are) used multiple times
+-- but we can use the name we've given::
return HttpResponseRedirect(reverse('poll_results', args=(p.id,)))
diff --git a/docs/ref/class-based-views.txt b/docs/ref/class-based-views.txt
new file mode 100644
index 0000000000..4f800e1fd9
--- /dev/null
+++ b/docs/ref/class-based-views.txt
@@ -0,0 +1,1391 @@
+=========================
+Class-Based Generic views
+=========================
+
+.. versionadded:: 1.3
+
+.. note::
+ Prior to Django 1.3, generic views were implemented as functions. The
+ function-based implementation has been deprecated in favor of the
+ class-based approach described here.
+
+ For the reference to the old on details on the old implementation,
+ see the :doc:`topic guide </topics/generic-views>` and
+ :doc:`detailed reference </topics/generic-views>`.
+
+Writing Web applications can be monotonous, because we repeat certain patterns
+again and again. In Django, the most common of these patterns have been
+abstracted into "generic views" that let you quickly provide common views of
+an object without actually needing to write any Python code.
+
+A general introduction to generic views can be found in the :doc:`topic guide
+</topics/class-based-views>`.
+
+This reference contains details of Django's built-in generic views, along with
+a list of all keyword arguments that a generic view expects. Remember that
+arguments may either come from the URL pattern or from the ``extra_context``
+additional-information dictionary.
+
+Most generic views require the ``queryset`` key, which is a ``QuerySet``
+instance; see :doc:`/topics/db/queries` for more information about ``QuerySet``
+objects.
+
+Mixins
+======
+
+A mixin class is a way of using the inheritance capabilities of
+classes to compose a class out of smaller pieces of behavior. Django's
+class-based generic views are constructed by composing a mixins into
+usable generic views.
+
+For example, the :class:`~django.views.generic.base.detail.DetailView`
+is composed from:
+
+ * :class:`~django.db.views.generic.base.View`, which provides the
+ basic class-based behavior
+ * :class:`~django.db.views.generic.detail.SingleObjectMixin`, which
+ provides the utilities for retrieving and displaying a single object
+ * :class:`~django.db.views.generic.detail.SingleObjectTemplateResponseMixin`,
+ which provides the tools for rendering a single object into a
+ template-based response.
+
+When combined, these mixins provide all the pieces necessary to
+provide a view over a single object that renders a template to produce
+a response.
+
+When the documentation for a view gives the list of mixins, that view
+inherits all the properties and methods of that mixin.
+
+Django provides a range of mixins. If you want to write your own
+generic views, you can build classes that compose these mixins in
+interesting ways. Alternatively, you can just use the pre-mixed
+`Generic views`_ that Django provides.
+
+Simple mixins
+-------------
+
+.. currentmodule:: django.views.generic.base
+
+TemplateResponseMixin
+~~~~~~~~~~~~~~~~~~~~~
+.. class:: TemplateResponseMixin()
+
+**Attributes**
+
+.. attribute:: TemplateResponseMixin.template_name
+
+ The path to the template to use when rendering the view.
+
+**Methods**
+
+.. method:: TemplateResponseMixin.render_to_response(context)
+
+ Returns a full composed HttpResponse instance, ready to be
+ returned to the user.
+
+ Calls, :meth:`~TemplateResponseMixin.render_template()` to build
+ the content of the response, and
+ :meth:`~TemplateResponseMixin.get_response()` to construct the
+ :class:`~django.http.HttpResponse` object.
+
+.. method:: TemplateResponseMixin.get_response(content, **httpresponse_kwargs)
+
+ Constructs the :class:`~django.http.HttpResponse` object around
+ the given content. If any keyword arguments are provided, they
+ will be passed to the constructor of the
+ :class:`~django.http.HttpResponse` instance.
+
+.. method:: TemplateResponseMixin.render_template(context)
+
+ Calls :meth:`~TemplateResponseMixin.get_context_instance()` to
+ obtain the :class:`Context` instance to use for rendering, and
+ calls :meth:`TemplateReponseMixin.get_template()` to load the
+ template that will be used to render the final content.
+
+.. method:: TemplateResponseMixin.get_context_instance(context)
+
+ Turns the data dictionary ``context`` into an actual context
+ instance that can be used for rendering.
+
+ By default, constructs a :class:`~django.template.RequestContext`
+ instance.
+
+.. method:: TemplateResponseMixin.get_template()
+
+ Calls :meth:`~TemplateResponseMixin.get_template_names()` to
+ obtain the list of template names that will be searched looking
+ for an existent template.
+
+.. method:: TemplateResponseMixin.get_template_names()
+
+ The list of template names to search for when rendering the
+ template.
+
+ If :attr:`TemplateResponseMixin.template_name` is specified, the
+ default implementation will return a list containing
+ :attr:`TemplateResponseMixin.template_name` (if it is specified).
+
+.. method:: TemplateResponseMixin.load_template(names)
+
+ Loads and returns a template found by searching the list of
+ ``names`` for a match. Uses Django's default template loader.
+
+Single object mixins
+--------------------
+
+.. currentmodule:: django.views.generic.detail
+
+SingleObjectMixin
+~~~~~~~~~~~~~~~~~
+.. class:: SingleObjectMixin()
+
+**Attributes**
+
+.. attribute:: SingleObjectMixin.model
+
+ The model that this view will display data for. Specifying ``model
+ = Foo`` is effectively the same as specifying ``queryset =
+ Foo.objects.all()``.
+
+.. attribute:: SingleObjectMixin.queryset
+
+ A ``QuerySet`` that represents the objects. If provided, the
+ value of :attr:`SingleObjectMixin.queryset` supersedes the
+ value provided for :attr:`SingleObjectMixin.model`.
+
+.. attribute:: SingleObjectMixin.slug_field
+
+ The name of the field on the model that contains the slug. By
+ default, ``slug_field`` is ``'slug'``.
+
+.. attribute:: SingleObjectMixin.context_object_name
+
+ Designates the name of the variable to use in the context.
+
+**Methods**
+
+.. method:: SingleObjectMixin.get_queryset()
+
+ Returns the queryset that will be used to retrieve the object that
+ this view will display.
+
+.. method:: SingleObjectMixin.get_context_object_name(object_list)
+
+ Return the context variable name that will be used to contain the
+ list of data that this view is manipulating. If object_list is a
+ queryset of Django objects, the context name will be verbose
+ plural name of the model that the queryset is composed from.
+
+.. method:: SingleObjectMixin.get_context_data(**kwargs)
+
+ Returns context data for displaying the list of objects.
+
+**Context**
+
+ * ``object``: The object that this view is displaying. If
+ ``context_object_name`` is specified, that variable will also be
+ set in the context, with the same value as ``object``.
+
+SingleObjectTemplateResponseMixin
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: SingleObjectTemplateResponseMixin()
+
+A mixin class that performs template-based response rendering for
+views that operate upon a single object instance. Requires that the
+view it is mixed with provides ``self.object``, the object instance
+that the view is operating on. ``self.object`` will usually be, but is
+not required to be, an instance of a Django model. It may be ``None``
+if the view is in the process of constructing a new instance.
+
+**Extends**
+
+ * :class:`~django.views.generic.base.TemplateResponseMixin`
+
+**Attributes**
+
+.. attribute:: SingleObjectTemplateResponseMixin.template_name_field
+
+ The field on the current object instance that can be used to
+ determine the name of a candidate template. If either
+ ``template_name_field`` or the value of the
+ ``template_name_field`` on the current object instance is
+ ``None``, the object will not be interrogated for a candidate
+ template name.
+
+.. attribute:: SingleObjectTemplateResponseMixin.template_name_suffix
+
+ The suffix to append to the auto-generated candidate template name.
+ Default suffix is ``_detail``.
+
+**Methods**
+
+.. method:: SingleObjectTemplateResponseMixin.get_template_names()
+
+ Returns a list of candidate template names. Returns the following
+ list:
+
+ * the value of ``template_name`` on the view (if provided)
+ * the contents of the ``template_name_field`` field on the
+ object instance that the view is operating upon (if available)
+ * ``<app_label>/<object_name><template_name_suffix>.html``
+
+Multiple object mixins
+----------------------
+
+.. currentmodule:: django.views.generic.list
+
+MultipleObjectMixin
+~~~~~~~~~~~~~~~~~~~
+.. class:: MultipleObjectMixin()
+
+A mixin that can be used to display a list of objects.
+
+If ``paginate_by`` is specified, Django will paginate the results
+returned by this. You can specify the page number in the URL in one of
+two ways:
+
+ * Use the ``page`` parameter in the URLconf. For example, this is
+ what your URLconf might look like::
+
+ (r'^objects/page(?P<page>[0-9]+)/$', PaginatedView.as_view())
+
+ * Pass the page number via the ``page`` query-string parameter. For
+ example, a URL would look like this::
+
+ /objects/?page=3
+
+These values and lists are 1-based, not 0-based, so the first page
+would be represented as page ``1``.
+
+For more on pagination, read the :doc:`pagination documentation
+</topics/pagination>`.
+
+As a special case, you are also permitted to use ``last`` as a value
+for ``page``::
+
+ /objects/?page=last
+
+This allows you to access the final page of results without first
+having to determine how many pages there are.
+
+Note that ``page`` *must* be either a valid page number or the value
+``last``; any other value for ``page`` will result in a 404 error.
+
+**Attributes**
+
+.. attribute:: MultipleObjectMixin.allow_empty
+
+ A boolean specifying whether to display the page if no objects are
+ available. If this is ``False`` and no objects are available, the
+ view will raise a 404 instead of displaying an empty page. By
+ default, this is ``True``.
+
+.. attribute:: MultipleObjectMixin.model
+
+ The model that this view will display data for. Specifying ``model
+ = Foo`` is effectively the same as specifying ``queryset =
+ Foo.objects.all()``.
+
+.. attribute:: MultipleObjectMixin.queryset
+
+ A ``QuerySet`` that represents the objects. If provided, the
+ value of :attr:`MultipleObjectMixin.queryset` supersedes the
+ value provided for :attr:`MultipleObjectMixin.model`.
+
+.. attribute:: MultipleObjectMixin.paginate_by
+
+ An integer specifying how many objects should be displayed per
+ page. If this is given, the view will paginate objects with
+ :attr:`MultipleObjectMixin.paginate_by` objects per page. The view
+ will expect either a ``page`` query string parameter (via ``GET``)
+ or a ``page`` variable specified in the URLconf.
+
+.. attribute:: MultipleObjectMixin.context_object_name
+
+ Designates the name of the variable to use in the context.
+
+**Methods**
+
+.. method:: MultipleObjectMixin.get_queryset()
+
+ Returns the queryset that represents the data this view will display.
+
+.. method:: MultipleObjectMixin.paginate_queryset(queryset, page_size)
+
+ Returns a 4-tuple containing::
+
+ (``paginator``, ``page``, ``object_list``, ``is_paginated``)
+
+ constructed by paginating ``queryset`` into pages of size ``page_size``.
+ If the request contains a ``page`` argument, either as a captured
+ URL argument or as a GET argument, ``object_list`` will correspond
+ to the objects from that page.
+
+.. method:: MultipleObjectMixin.get_paginate_by(queryset)
+
+.. method:: MultipleObjectMixin.get_allow_empty()
+
+ Return a boolean specifying whether to display the page if no objects are
+ available. If this method returns ``False`` and no objects are available, the
+ view will raise a 404 instead of displaying an empty page. By
+ default, this is ``True``.
+
+.. method:: MultipleObjectMixin.get_context_object_name(object_list)
+
+ Return the context variable name that will be used to contain the
+ list of data that this view is manipulating. If object_list is a
+ queryset of Django objects, the context name will be verbose
+ plural name of the model that the queryset is composed from.
+
+.. method:: MultipleObjectMixin.get_context_data(**kwargs)
+
+ Returns context data for displaying the list of objects.
+
+**Context**
+
+ * ``object_list``: The list of object that this view is
+ displaying. If ``context_object_name`` is specified, that
+ variable will also be set in the context, with the same value as
+ ``object_list``.
+
+ * ``is_paginated``: A boolean representing whether the results are
+ paginated. Specifically, this is set to ``False`` if no page
+ size has been specified, or if the number of available objects
+ is less than or equal to ``paginate_by``.
+
+ * ``paginator``: An instance of
+ :class:`django.core.paginator.Paginator`. If the page is not
+ paginated, this context variable will be ``None``
+
+ * ``page_obj``: An instance of
+ :class:`django.core.paginator.Page`. If the page is not
+ paginated, this context variable will be ``None``
+
+MultipleObjectTemplateResponseMixin
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. class:: MultipleObjectTemplateResponseMixin()
+
+A mixin class that performs template-based response rendering for
+views that operate upon a list of object instances. Requires that the
+view it is mixed with provides ``self.object_list``, the list of
+object instances that the view is operating on. ``self.object_list``
+may be, but is not required to be, a
+:class:`~django.db.models.Queryset`.
+
+**Extends**
+
+ * :class:`~django.views.generic.base.TemplateResponseMixin`
+
+**Attributes**
+
+.. attribute:: MultipleObjectTemplateResponseMixin.template_name_suffix
+
+ The suffix to append to the auto-generated candidate template name.
+ Default suffix is ``_list``.
+
+**Methods**
+
+.. method:: MultipleObjectTemplateResponseMixin.get_template_names()
+
+ Returns a list of candidate template names. Returns the following
+ list:
+
+ * the value of ``template_name`` on the view (if provided)
+ * ``<app_label>/<object_name><template_name_suffix>.html``
+
+Editing mixins
+--------------
+
+.. currentmodule:: django.views.generic.edit
+
+FormMixin
+~~~~~~~~~
+.. class:: FormMixin()
+
+A mixin class that provides facilities for creating and displaying forms.
+
+**Attributes**
+
+.. attribute:: FormMixin.initial
+
+ A dictionary containing initial data for the form.
+
+.. attribute:: FormMixin.form_class
+
+ The form class to instantiate.
+
+.. attribute:: FormMixin.success_url
+
+ The URL to redirect to when the form is successfully processed.
+
+**Methods**
+
+.. method:: FormMixin.get_initial()
+
+ Retrieve initial data for the form. By default, returns
+ :attr:`FormMixin.initial`.
+
+.. method:: FormMixin.get_form_class()
+
+ Retrieve the form class to instantiate. By default,
+ :attr:`FormMixin.form_class`.
+
+.. method:: FormMixin.get_form(form_class)
+
+ Instantiate an instance of ``form_class``. If the request is a
+ ``POST`` or ``PUT``, the request data (``request.POST`` and
+ ``request.FILES``) will be provided to the form at time of
+ construction
+
+.. method:: FormMixin.get_success_url()
+
+ Determine the URL to redirect to when the form is successfully
+ validated. Returns :attr:`FormMixin.success_url` by default.
+
+.. method:: FormMixin.form_valid()
+
+ Redirects to :attr:`ModelFormMixin.success_url`.
+
+.. method:: FormMixin.form_invalid()
+
+ Renders a response, providing the invalid form as context.
+
+.. method:: FormMixin.get_context_data(**kwargs)
+
+ Populates a context containing the contents of ``kwargs``.
+
+**Context**
+
+ * ``form``: The form instance that was generated for the view.
+
+**Notes**
+
+ * Views mixing :class:`~django.views.generic.edit.FormMixin` must
+ provide an implementation of :meth:`~FormMixin.form_valid()` and
+ :meth:`~FormMixin.form_invalid()`.
+
+ModelFormMixin
+~~~~~~~~~~~~~~
+.. class:: ModelFormMixin()
+
+A form mixin that works on ModelForms, rather than a standalone form.
+
+Since this is a subclass of
+:class:`~django.views.generic.detail.SingleObjectMixin`, instances of
+this mixin have access to the :attr:`~SingleObjectMixin.model`` and
+:attr:`~SingleObjectMixin.queryset`` attributes, describing the type of
+object that the ModelForm is manipulating. The view also provides
+``self.object``, the instance being manipulated. If the instance is
+being created, ``self.object`` will be ``None``
+
+**Mixins**
+
+ * :class:`django.views.generic.forms.FormMixin`
+ * :class:`django.views.generic.detail.SingleObjectMixin`
+
+**Attributes**
+
+.. attribute:: ModelFormMixin.success_url
+
+ The URL to redirect to when the form is successfully processed.
+
+**Methods**
+
+.. method:: ModelFormMixin.get_form_class()
+
+ Retrieve the form class to instantiate. If
+ :attr:`FormMixin.form_class` is provided, that class will be used.
+ Otherwise, a ModelForm will be instantiated using the model
+ associated with the :attr:`~SingleObjectMixin.queryset``, or with
+ the :attr:`~SingleObjectMixin.model``, depending on which
+ attribute is provided.
+
+.. method:: FormMixin.get_form(form_class)
+
+ Instantiate an instance of ``form_class``. If the request is a
+ ``POST`` or ``PUT``, the request data (``request.POST`` and
+ ``request.FILES``) will be provided to the form at time of
+ construction. The current instance (``self.object``) will also
+ be provided.
+
+.. method:: ModelFormMixin.get_success_url()
+
+ Determine the URL to redirect to when the form is successfully
+ validated. Returns :attr:`FormMixin.success_url` if it is
+ provided; otherwise, attempts to use the ``get_absolute_url()``
+ of the object.
+
+.. method:: ModelFormMixin.form_valid()
+
+ Saves the form instance, sets the current object for the view,
+ and redirects to :attr:`ModelFormMixin.success_url`.
+
+.. method:: ModelFormMixin.form_invalid()
+
+ Renders a response, providing the invalid form as context.
+
+ProcessFormView
+~~~~~~~~~~~~~~~
+.. class:: ProcessFormView()
+
+A mixin that provides basic HTTP GET and POST workflow.
+
+On GET:
+ * Construct a form
+ * Render a response using a context that contains that form
+
+On POST:
+ * Construct a form
+ * Check the form for validity, and handle accordingly.
+
+The PUT action is also handled, as an analog of POST.
+
+DeletionMixin
+~~~~~~~~~~~~~
+.. class:: DeletionMixin()
+
+Enables handling of the ``DELETE`` http action.
+
+**Attributes**
+
+.. attribute:: DeletionMixin.success_url
+
+ The url to redirect to when the nominated object has been
+ successfully deleted.
+
+**Methods**
+
+.. attribute:: DeletionMixin.get_success_url(obj)
+
+ Returns the url to redirect to when the nominated object has been
+ successfully deleted. Returns
+ :attr:`~django.views.generic.edit.DeletionMixin.success_url` by
+ default.
+
+Date-based mixins
+-----------------
+
+.. currentmodule:: django.views.generic.dates
+
+YearMixin
+~~~~~~~~~
+.. class:: YearMixin()
+
+A mixin that can be used to retrieve and provide parsing information
+for a year component of a date.
+
+**Attributes**
+
+.. attribute:: YearMixin.year_format
+
+ The strftime_ format to use when parsing the year. By default,
+ this is ``'%Y'``.
+
+.. _strftime: http://docs.python.org/library/time.html#time.strftime
+
+.. attribute:: YearMixin.year
+
+ **Optional** The value for the year (as a string). By default,
+ set to ``None``, which means the year will be determined using
+ other means.
+
+**Methods**
+
+.. method:: YearMixin.get_year_format()
+
+ Returns the strftime_ format to use when parsing the year. Returns
+ :attr:`YearMixin.year_format` by default.
+
+.. method:: YearMixin.get_year()
+
+ Returns the year for which this view will display data. Tries the
+ following sources, in order:
+
+ * The value of the :attr:`YearMixin.year` attribute.
+ * The value of the `year` argument captured in the URL pattern
+ * The value of the `year` GET query argument.
+
+ Raises a 404 if no valid year specification can be found.
+
+MonthMixin
+~~~~~~~~~~
+.. class:: MonthMixin()
+
+A mixin that can be used to retrieve and provide parsing information
+for a month component of a date.
+
+**Attributes**
+
+.. attribute:: MonthMixin.month_format
+
+ The strftime_ format to use when parsing the month. By default,
+ this is ``'%b'``.
+
+.. attribute:: MonthMixin.month
+
+ **Optional** The value for the month (as a string). By default,
+ set to ``None``, which means the month will be determined using
+ other means.
+
+**Methods**
+
+.. method:: MonthMixin.get_month_format()
+
+ Returns the strftime_ format to use when parsing the month. Returns
+ :attr:`MonthMixin.month_format` by default.
+
+.. method:: MonthMixin.get_month()
+
+ Returns the month for which this view will display data. Tries the
+ following sources, in order:
+
+ * The value of the :attr:`MonthMixin.month` attribute.
+ * The value of the `month` argument captured in the URL pattern
+ * The value of the `month` GET query argument.
+
+ Raises a 404 if no valid month specification can be found.
+
+.. method:: MonthMixin.get_next_month(date)
+
+ Returns a date object containing the first day of the month after
+ the date provided. Returns `None`` if mixed with a view that sets
+ ``allow_future = False``, and the next month is in the future.
+ If ``allow_empty = False``, returns the next month that contains
+ data.
+
+.. method:: MonthMixin.get_prev_month(date)
+
+ Returns a date object containing the first day of the month before
+ the date provided. If ``allow_empty = False``, returns the previous
+ month that contained data.
+
+DayMixin
+~~~~~~~~~
+.. class:: DayMixin()
+
+A mixin that can be used to retrieve and provide parsing information
+for a day component of a date.
+
+**Attributes**
+
+.. attribute:: DayMixin.day_format
+
+ The strftime_ format to use when parsing the day. By default,
+ this is ``'%d'``.
+
+.. attribute:: DayMixin.day
+
+ **Optional** The value for the day (as a string). By default,
+ set to ``None``, which means the day will be determined using
+ other means.
+
+**Methods**
+
+.. method:: DayMixin.get_day_format()
+
+ Returns the strftime_ format to use when parsing the day. Returns
+ :attr:`DayMixin.day_format` by default.
+
+.. method:: DayMixin.get_day()
+
+ Returns the day for which this view will display data. Tries the
+ following sources, in order:
+
+ * The value of the :attr:`DayMixin.day` attribute.
+ * The value of the `day` argument captured in the URL pattern
+ * The value of the `day` GET query argument.
+
+ Raises a 404 if no valid day specification can be found.
+
+.. method:: MonthMixin.get_next_day(date)
+
+ Returns a date object containing the next day after the date
+ provided. Returns `None`` if mixed with a view that sets
+ ``allow_future = False``, and the next day is in the future. If
+ ``allow_empty = False``, returns the next day that contains
+ data.
+
+.. method:: MonthMixin.get_prev_day(date)
+
+ Returns a date object containing the previous day. If
+ ``allow_empty = False``, returns the previous day that contained
+ data.
+
+WeekMixin
+~~~~~~~~~
+.. class:: WeekMixin()
+
+A mixin that can be used to retrieve and provide parsing information
+for a week component of a date.
+
+**Attributes**
+
+.. attribute:: WeekMixin.week_format
+
+ The strftime_ format to use when parsing the week. By default,
+ this is ``'%U'``.
+
+.. attribute:: WeekMixin.week
+
+ **Optional** The value for the week (as a string). By default,
+ set to ``None``, which means the week will be determined using
+ other means.
+
+**Methods**
+
+.. method:: WeekMixin.get_week_format()
+
+ Returns the strftime_ format to use when parsing the week. Returns
+ :attr:`WeekMixin.week_format` by default.
+
+.. method:: WeekMixin.get_week()
+
+ Returns the week for which this view will display data. Tries the
+ following sources, in order:
+
+ * The value of the :attr:`WeekMixin.week` attribute.
+ * The value of the `week` argument captured in the URL pattern
+ * The value of the `week` GET query argument.
+
+ Raises a 404 if no valid week specification can be found.
+
+
+DateMixin
+~~~~~~~~~
+.. class:: DateMixin()
+
+A mixin class providing common behavior for all date-based views.
+
+**Attributes**
+
+.. attribute:: BaseDateListView.date_field
+
+ The name of the ``DateField`` or ``DateTimeField`` in the
+ ``QuerySet``'s model that the date-based archive should use to
+ determine the objects on the page.
+
+.. attribute:: BaseDateListView.allow_future
+
+ A boolean specifying whether to include "future" objects on this
+ page, where "future" means objects in which the field specified in
+ ``date_field`` is greater than the current date/time. By default,
+ this is ``False``.
+
+**Methods**
+
+.. method:: BaseDateListView.get_date_field()
+
+ Returns the name of the field that contains the date data that
+ this view will operate on. Returns :attr:`DateMixin.date_field` by
+ default.
+
+.. method:: BaseDateListView.get_allow_future()
+
+ Determine whether to include "future" objects on this page, where
+ "future" means objects in which the field specified in
+ ``date_field`` is greater than the current date/time. Returns
+ :attr:`DateMixin.date_field` by default.
+
+BaseDateListView
+~~~~~~~~~~~~~~~~
+.. class:: BaseDateListView()
+
+A base class that provides common behavior for all date-based views.
+There won't normally be a reason to instantiate
+:class:`~django.views.generic.dates.BaseDateListView`; instantiate one of
+the subclasses instead.
+
+While this view (and it's subclasses) are executing,
+``self.object_list`` will contain the list of objects that the view is
+operating upon, and ``self.date_list`` will contain the list of dates
+for which data is available.
+
+**Mixins**
+
+ * :class:`~django.views.generic.dates.DateMixin`
+ * :class:`~django.views.generic.list.MultipleObjectMixin`
+
+**Attributes**
+
+.. attribute:: BaseDateListView.allow_empty
+
+ A boolean specifying whether to display the page if no objects are
+ available. If this is ``False`` and no objects are available, the
+ view will raise a 404 instead of displaying an empty page. By
+ default, this is ``True``.
+
+**Methods**
+
+.. method:: ArchiveView.get_dated_items():
+
+ Returns a 3-tuple containing::
+
+ (date_list, latest, extra_context)
+
+ ``date_list`` is the list of dates for which data is available.
+ ``object_list`` is the list of objects ``extra_context`` is a
+ dictionary of context data that will be added to any context data
+ provided by the
+ :class:`~django.db.views.generic.list.MultiplObjectMixin`.
+
+.. method:: BaseDateListView.get_dated_queryset(**lookup)
+
+ Returns a queryset, filtered using the query arguments defined by
+ ``lookup``. Enforces any restrictions on the queryset, such as
+ ``allow_empty`` and ``allow_future``.
+
+.. method:: BaseDateListView.get_date_list(queryset, date_type)
+
+ Returns the list of dates of type ``date_type`` for which
+ ``queryset`` contains entries. For example, ``get_date_list(qs,
+ 'year')`` will return the list of years for which ``qs`` has
+ entries. See :meth:``~django.db.models.QuerySet.dates()` for the
+ ways that the ``date_type`` argument can be used.
+
+
+Generic views
+=============
+
+Simple generic views
+--------------------
+
+.. currentmodule:: django.views.generic.base
+
+View
+~~~~
+.. class:: View()
+
+The master class-based base view. All other generic class-based views
+inherit from this base class.
+
+Each request served by a :class:`~django.views.generic.base.View` has
+an independent state; therefore, it is safe to store state variables
+on the instance (i.e., ``self.foo = 3`` is a thread-safe operation).
+
+A class-based view is deployed into a URL pattern using the
+:meth:`~View.as_view()` classmethod::
+
+ urlpatterns = patterns('',
+ (r'^view/$', MyView.as_view(size=42)),
+ )
+
+Any argument passed into :meth:`~View.as_view()` will be assigned onto
+the instance that is used to service a request. Using the previous
+example, this means that every request on ``MyView`` is able to
+interrogate ``self.size``.
+
+.. admonition:: Thread safety with view arguments
+
+ Arguments passed to a view are shared between every instance of a
+ view. This means that you shoudn't use a list, dictionary, or any
+ other variable object as an argument to a view. If you did, the
+ actions of one user visiting your view could have an effect on
+ subsequent users visiting the same view.
+
+**Methods**
+
+.. method:: View.dispatch(request, *args, **kwargs)
+
+ The ``view`` part of the view -- the method that accepts a
+ ``request`` argument plus arguments, and returns a HTTP response.
+
+ The default implementation will inspect the HTTP method and
+ attempt to delegate to a method that matches the HTTP method; a
+ ``GET`` will be delegated to :meth:`~View.get()`, a ``POST`` to
+ :meth:`~View.post()`, and so on.
+
+ The default implementation also sets ``request``, ``args`` and
+ ``kwargs`` as instance variables, so any method on the view can
+ know the full details of the request that was made to invoke the
+ view.
+
+.. method:: View.http_method_not_allowed(request, *args, **kwargs)
+
+ If the view was called with HTTP method it doesn't support, this
+ method is called instead.
+
+ The default implementation returns ``HttpResponseNotAllowed``
+ with list of allowed methods in plain text.
+
+TemplateView
+~~~~~~~~~~~~
+.. class:: TemplateView()
+
+Renders a given template, passing it a ``{{ params }}`` template
+variable, which is a dictionary of the parameters captured in the URL.
+
+**Mixins**
+
+ * :class:`django.views.generic.base.TemplateResponseMixin`
+
+**Attributes**
+
+.. attribute:: TemplateView.template_name
+
+ The full name of a template to use.
+
+**Methods**
+
+.. method:: TemplateView.get_context_data(**kwargs)
+
+ Return a context data dictionary consisting of the contents of
+ ``kwargs`` stored in the context variable ``params``.
+
+**Context**
+
+ * ``params``: The dictionary of keyword arguments captured from
+ the URL pattern that served the view.
+
+RedirectView
+~~~~~~~~~~~~
+.. class:: RedirectView()
+
+Redirects to a given URL.
+
+The given URL may contain dictionary-style string formatting, which
+will be interpolated against the parameters captured in the URL.
+Because keyword interpolation is *always* done (even if no arguments
+are passed in), any ``"%"`` characters in the URL must be written as
+``"%%"`` so that Python will convert them to a single percent sign on
+output.
+
+If the given URL is ``None``, Django will return an
+``HttpResponseGone`` (410).
+
+**Mixins**
+
+None.
+
+**Attributes**
+
+.. attribute:: RedirectView.url
+
+ The URL to redirect to, as a string. Or ``None`` to raise a 410
+ (Gone) HTTP error.
+
+.. attribute:: RedirectView.permanent
+
+ Whether the redirect should be permanent. The only difference here
+ is the HTTP status code returned. If ``True``, then the redirect
+ will use status code 301. If ``False``, then the redirect will use
+ status code 302. By default, ``permanent`` is ``True``.
+
+.. attribute:: RedirectView.query_string
+
+ Whether to pass along the GET query string to the new location. If
+ ``True``, then the query string is appended to the URL. If
+ ``False``, then the query string is discarded. By default,
+ ``query_string`` is ``False``.
+
+**Methods**
+
+.. method:: RedirectView.get_redirect_url(**kwargs)
+
+ Constructs the target URL for redirection.
+
+ The default implementation uses :attr:`~RedirectView.url` as a
+ starting string, performs expansion of ``%`` parameters in that
+ string, as well as the appending of query string if requested by
+ :attr:`~RedirectView.query_string`. Subclasses may implement any
+ behavior they wish, as long as the method returns a redirect-ready
+ URL string.
+
+Detail views
+------------
+
+.. currentmodule:: django.views.generic.detail
+
+DetailView
+~~~~~~~~~~
+.. class:: BaseDetailView()
+.. class:: DetailView()
+
+A page representing an individual object.
+
+While this view is executing, ``self.object`` will contain the object that
+the view is operating upon.
+
+:class:`~django.views.generic.base.BaseDetailView` implements the same
+behavior as :class:`~django.views.generic.base.DetailView`, but doesn't
+include the
+:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.detail.SingleObjectMixin`
+ * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin`
+
+List views
+----------
+
+.. currentmodule:: django.views.generic.list
+
+ListView
+~~~~~~~~
+.. class:: BaseListView()
+.. class:: ListView()
+
+A page representing a list of objects.
+
+While this view is executing, ``self.object_list`` will contain the
+list of objects (usually, but not necessarily a queryset) that the
+view is operating upon.
+
+:class:`~django.views.generic.list.BaseListView` implements the same
+behavior as :class:`~django.views.generic.list.ListView`, but doesn't
+include the
+:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.base.MultipleObjectMixin`
+ * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin`
+
+
+Editing views
+-------------
+
+.. currentmodule:: django.views.generic.edit
+
+FormView
+~~~~~~~~
+.. class:: BaseFormView()
+.. class:: FormView()
+
+A view that displays a form. On error, redisplays the form with
+validation errors; on success, redirects to a new URL.
+
+:class:`~django.views.generic.edit.BaseFormView` implements the same
+behavior as :class:`~django.views.generic.edit.FormView`, but doesn't
+include the :class:`~django.views.generic.base.TemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.edit.FormMixin`
+ * :class:`django.views.generic.edit.ProcessFormView`
+
+CreateView
+~~~~~~~~~~
+.. class:: BaseCreateView()
+.. class:: CreateView()
+
+A view that displays a form for creating an object, redisplaying the
+form with validation errors (if there are any) and saving the object.
+
+:class:`~django.views.generic.edit.BaseCreateView` implements the same
+behavior as :class:`~django.views.generic.edit.CreateView`, but
+doesn't include the
+:class:`~django.views.generic.base.TemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.edit.ModelFormMixin`
+ * :class:`django.views.generic.edit.ProcessFormView`
+
+UpdateView
+~~~~~~~~~~
+.. class:: BaseUpdateView()
+.. class:: UpdateView()
+
+A view that displays a form for editing an existing object,
+redisplaying the form with validation errors (if there are any) and
+saving changes to the object. This uses a form automatically generated
+from the object's model class (unless a form class is manually
+specified).
+
+:class:`~django.views.generic.edit.BaseUpdateView` implements the same
+behavior as :class:`~django.views.generic.edit.UpdateView`, but
+doesn't include the
+:class:`~django.views.generic.base.TemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.edit.ModelFormMixin`
+ * :class:`django.views.generic.edit.ProcessFormView`
+
+DeleteView
+~~~~~~~~~~
+.. class:: BaseDeleteView()
+.. class:: DeleteView()
+
+A view that displays a confirmation page and deletes an existing object. The
+given object will only be deleted if the request method is ``POST``. If this
+view is fetched via ``GET``, it will display a confirmation page that should
+contain a form that POSTs to the same URL.
+
+:class:`~django.views.generic.edit.BaseDeleteView` implements the same
+behavior as :class:`~django.views.generic.edit.DeleteView`, but
+doesn't include the
+:class:`~django.views.generic.base.TemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.edit.ModelFormMixin`
+ * :class:`django.views.generic.edit.ProcessFormView`
+
+**Notes**
+
+ * The delete confirmation page displayed to a GET request uses a
+ ``template_name_suffix`` of ``'_confirm_delete'``.
+
+Date-based views
+----------------
+
+Date-based generic views (in the module :mod:`django.views.generic.dates`)
+are views for displaying drilldown pages for date-based data.
+
+.. currentmodule:: django.views.generic.dates
+
+ArchiveIndexView
+~~~~~~~~~~~~~~~~
+.. class:: BaseArchiveIndexView()
+.. class:: ArchiveIndexView()
+
+A top-level index page showing the "latest" objects, by date. Objects
+with a date in the *future* are not included unless you set
+``allow_future`` to ``True``.
+
+:class:`~django.views.generic.dates.BaseArchiveIndexView` implements
+the same behavior as
+:class:`~django.views.generic.dates.ArchiveIndexView`, but doesn't
+include the
+:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.dates.BaseDateListView`
+ * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin`
+
+**Notes**
+
+ * Uses a default ``context_object_name`` of ``latest``.
+
+ * Uses a default ``template_name_suffix`` of ``_archive``.
+
+YearArchiveView
+~~~~~~~~~~~~~~~
+.. class:: BaseYearArchiveView()
+.. class:: YearArchiveView()
+
+A yearly archive page showing all available months in a given year.
+Objects with a date in the *future* are not displayed unless you set
+``allow_future`` to ``True``.
+
+:class:`~django.views.generic.dates.BaseYearArchiveView` implements the
+same behavior as :class:`~django.views.generic.dates.YearArchiveView`,
+but doesn't include the
+:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin`
+ * :class:`django.views.generic.dates.YearMixin`
+ * :class:`django.views.generic.dates.BaseDateListView`
+
+**Attributes**
+
+.. attribute:: YearArchiveView.make_object_list
+
+ A boolean specifying whether to retrieve the full list of objects
+ for this year and pass those to the template. If ``True``, the
+ list of objects will be made available to the context. By default,
+ this is ``False``.
+
+**Methods**
+
+.. method:: YearArchiveView.get_make_object_list()
+
+ Determine if an object list will be returned as part of the context.
+ If ``False``, the ``None`` queryset will be used as the object list.
+
+**Context**
+
+In addition to the context provided by
+:class:`django.views.generic.list.MultipleObjectMixin` (via
+:class:`django.views.generic.dates.BaseDateListView`), the template's
+context will be:
+
+ * ``date_list``: A ``DateQuerySet`` object containing all months that have
+ have objects available according to ``queryset``, represented as
+ ``datetime.datetime`` objects, in ascending order.
+
+ * ``year``: The given year, as a four-character string.
+
+**Notes**
+
+ * Uses a default ``template_name_suffix`` of ``_archive_year``.
+
+MonthArchiveView
+~~~~~~~~~~~~~~~~
+.. class:: BaseMonthArchiveView()
+.. class:: MonthArchiveView()
+
+A monthly archive page showing all objects in a given month. Objects with a
+date in the *future* are not displayed unless you set ``allow_future`` to
+``True``.
+
+:class:`~django.views.generic.dates.BaseMonthArchiveView` implements
+the same behavior as
+:class:`~django.views.generic.dates.MonthArchiveView`, but doesn't
+include the
+:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin`
+ * :class:`django.views.generic.dates.YearMixin`
+ * :class:`django.views.generic.dates.MonthMixin`
+ * :class:`django.views.generic.dates.BaseDateListView`
+
+**Attributes**
+
+**Methods**
+
+**Context**
+
+In addition to the context provided by
+:class:`~django.views.generic.list.MultipleObjectMixin` (via
+:class:`~django.views.generic.dates.BaseDateListView`), the template's
+context will be:
+
+ * ``date_list``: A ``DateQuerySet`` object containing all days that have
+ have objects available in the given month, according to ``queryset``,
+ represented as ``datetime.datetime`` objects, in ascending order.
+
+ * ``month``: A ``datetime.date`` object representing the given month.
+
+ * ``next_month``: A ``datetime.date`` object representing the first day of
+ the next month. If the next month is in the future, this will be
+ ``None``.
+
+ * ``previous_month``: A ``datetime.date`` object representing the first day
+ of the previous month. Unlike ``next_month``, this will never be
+ ``None``.
+
+**Notes**
+
+ * Uses a default ``template_name_suffix`` of ``_archive_month``.
+
+WeekArchiveView
+~~~~~~~~~~~~~~~
+.. class:: BaseWeekArchiveView()
+.. class:: WeekArchiveView()
+
+A weekly archive page showing all objects in a given week. Objects with a date
+in the *future* are not displayed unless you set ``allow_future`` to ``True``.
+
+:class:`~django.views.generic.dates.BaseWeekArchiveView` implements the
+same behavior as :class:`~django.views.generic.dates.WeekArchiveView`,
+but doesn't include the
+:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin`
+ * :class:`django.views.generic.dates.YearMixin`
+ * :class:`django.views.generic.dates.MonthMixin`
+ * :class:`django.views.generic.dates.BaseDateListView`
+
+**Context**
+
+In addition to the context provided by
+:class:`~django.views.generic.list.MultipleObjectMixin` (via
+:class:`~django.views.generic.dates.BaseDateListView`), the template's
+context will be:
+
+ * ``week``: A ``datetime.date`` object representing the first day of the
+ given week.
+
+**Notes**
+
+ * Uses a default ``template_name_suffix`` of ``_archive_week``.
+
+DayArchiveView
+~~~~~~~~~~~~~~
+.. class:: BaseDayArchiveView()
+.. class:: DayArchiveView()
+
+A day archive page showing all objects in a given day. Days in the future throw
+a 404 error, regardless of whether any objects exist for future days, unless
+you set ``allow_future`` to ``True``.
+
+:class:`~django.views.generic.dates.BaseDayArchiveView` implements the
+same behavior as :class:`~django.views.generic.dates.DayArchiveView`,
+but doesn't include the
+:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin`
+ * :class:`django.views.generic.dates.YearMixin`
+ * :class:`django.views.generic.dates.MonthMixin`
+ * :class:`django.views.generic.dates.DayMixin`
+ * :class:`django.views.generic.dates.BaseDateListView`
+
+**Context**
+
+In addition to the context provided by
+:class:`~django.views.generic.list.MultipleObjectMixin` (via
+:class:`~django.views.generic.dates.BaseDateListView`), the template's
+context will be:
+
+ * ``day``: A ``datetime.date`` object representing the given day.
+
+ * ``next_day``: A ``datetime.date`` object representing the next day. If
+ the next day is in the future, this will be ``None``.
+
+ * ``previous_day``: A ``datetime.date`` object representing the previous day.
+ Unlike ``next_day``, this will never be ``None``.
+
+ * ``next_month``: A ``datetime.date`` object representing the first day of
+ the next month. If the next month is in the future, this will be
+ ``None``.
+
+ * ``previous_month``: A ``datetime.date`` object representing the first day
+ of the previous month. Unlike ``next_month``, this will never be
+ ``None``.
+
+**Notes**
+
+ * Uses a default ``template_name_suffix`` of ``_archive_day``.
+
+TodayArchiveView
+~~~~~~~~~~~~~~~~
+.. class:: BaseTodayArchiveView()
+.. class:: TodayArchiveView()
+
+A day archive page showing all objects for *today*. This is exactly the same as
+``archive_day``, except the ``year``/``month``/``day`` arguments are not used,
+
+:class:`~django.views.generic.dates.BaseTodayArchiveView` implements
+the same behavior as
+:class:`~django.views.generic.dates.TodayArchiveView`, but doesn't
+include the
+:class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.dates.DayArchiveView`
+
+DateDetailView
+~~~~~~~~~~~~~~
+.. class:: BaseDateDetailView()
+.. class:: DateDetailView()
+
+A page representing an individual object. If the object has a date value in the
+future, the view will throw a 404 error by default, unless you set
+``allow_future`` to ``True``.
+
+:class:`~django.views.generic.dates.BaseDateDetailView` implements the
+same behavior as :class:`~django.views.generic.dates.DateDetailView`,
+but doesn't include the
+:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`.
+
+**Mixins**
+
+ * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin`
+ * :class:`django.views.generic.dates.YearMixin`
+ * :class:`django.views.generic.dates.MonthMixin`
+ * :class:`django.views.generic.dates.DayMixin`
+ * :class:`django.views.generic.dates.BaseDateListView`
diff --git a/docs/ref/generic-views.txt b/docs/ref/generic-views.txt
index 64d0d68739..c09cbca164 100644
--- a/docs/ref/generic-views.txt
+++ b/docs/ref/generic-views.txt
@@ -8,7 +8,7 @@ abstracted into "generic views" that let you quickly provide common views of
an object without actually needing to write any Python code.
A general introduction to generic views can be found in the :doc:`topic guide
-</topics/http/generic-views>`.
+</topics/generic-views>`.
This reference contains details of Django's built-in generic views, along with
a list of all keyword arguments that a generic view expects. Remember that
diff --git a/docs/ref/index.txt b/docs/ref/index.txt
index 09194178af..7b59589e74 100644
--- a/docs/ref/index.txt
+++ b/docs/ref/index.txt
@@ -12,7 +12,7 @@ API Reference
exceptions
files/index
forms/index
- generic-views
+ class-based-views
middleware
models/index
request-response
@@ -22,3 +22,11 @@ API Reference
unicode
utils
validators
+
+Deprecated features
+-------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ generic-views
diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt
index d681c4dbd3..8f722f6cbc 100644
--- a/docs/releases/1.3.txt
+++ b/docs/releases/1.3.txt
@@ -17,6 +17,23 @@ upgrade path from Django 1.2.
What's new in Django 1.3
========================
+Class-based views
+~~~~~~~~~~~~~~~~~
+
+Django 1.3 adds a framework that allows you to use a class as a view.
+This means you can compose a view out of a collection of methods that
+can be subclassed and overridden to provide
+
+Analogs of all the old function-based generic views have been
+provided, along with a completely generic view base class that can be
+used as the basis for reusable applications that can be easily
+extended.
+
+See :doc:`the documentation on Generic Views</topics/generic-views>`
+for more details. There is also a document to help you :doc:`convert
+your function-based generic views to class-based
+views</topics/generic-views-migration>`.
+
Logging
~~~~~~~
@@ -174,6 +191,18 @@ If you are currently using the ``mod_python`` request handler, it is strongly
encouraged you redeploy your Django instances using :doc:`mod_wsgi
</howto/deployment/modwsgi>`.
+Function-based generic views
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+As a result of the introduction of class-based generic views, the
+function-based generic views provided by Django have been deprecated.
+The following modules and the views they contain have been deprecated:
+
+ * :mod:`django.views.generic.create_update`
+ * :mod:`django.views.generic.date_based`
+ * :mod:`django.views.generic.list_detail`
+ * :mod:`django.views.generic.simple`
+
Test client response ``template`` attribute
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt
new file mode 100644
index 0000000000..1f5421ab25
--- /dev/null
+++ b/docs/topics/class-based-views.txt
@@ -0,0 +1,535 @@
+=========================
+Class-based generic views
+=========================
+
+.. versionadded:: 1.3
+
+.. note::
+ Prior to Django 1.3, generic views were implemented as functions. The
+ function-based implementation has been deprecated in favor of the
+ class-based approach described here.
+
+ For the reference to the old on details on the old implementation,
+ see the :doc:`topic guide </topics/generic-views>` and
+ :doc:`detailed reference </topics/generic-views>`.
+
+Writing Web applications can be monotonous, because we repeat certain patterns
+again and again. Django tries to take away some of that monotony at the model
+and template layers, but Web developers also experience this boredom at the view
+level.
+
+Django's *generic views* were developed to ease that pain. They take certain
+common idioms and patterns found in view development and abstract them so that
+you can quickly write common views of data without having to write too much
+code.
+
+We can recognize certain common tasks, like displaying a list of objects, and
+write code that displays a list of *any* object. Then the model in question can
+be passed as an extra argument to the URLconf.
+
+Django ships with generic views to do the following:
+
+ * Perform common "simple" tasks: redirect to a different page and
+ render a given template.
+
+ * Display list and detail pages for a single object. If we were creating an
+ application to manage conferences then a ``TalkListView`` and a
+ ``RegisteredUserListView`` would be examples of list views. A single
+ talk page is an example of what we call a "detail" view.
+
+ * Present date-based objects in year/month/day archive pages,
+ associated detail, and "latest" pages. The Django Weblog's
+ (http://www.djangoproject.com/weblog/) year, month, and
+ day archives are built with these, as would be a typical
+ newspaper's archives.
+
+ * Allow users to create, update, and delete objects -- with or
+ without authorization.
+
+Taken together, these views provide easy interfaces to perform the most common
+tasks developers encounter.
+
+
+Simple usage
+============
+
+Class-based generic views (and indeed any class-based views that are
+based on the base classes Django provides) can be configured in two
+ways: subclassing, or passing in arguments directly in the URLconf.
+
+When you subclass a class-based view, you can override attributes
+(such as the template name, ``template_name``) or methods (such as
+``get_context_data``) in your subclass to provide new values or
+methods. Consider, for example, a view that just displays one
+template, ``about.html``. Django has a generic view to do this -
+:class:`~django.views.generic.base.TemplateView` - so we can just
+subclass it, and override the template name::
+
+ # some_app/views.py
+ from django.views.generic import TemplateView
+
+ class AboutView(TemplateView):
+ template_name = "about.html"
+
+Then, we just need to add this new view into our URLconf. As the class-based
+views themselves are classes, we point the URL to the as_view class method
+instead, which is the entrypoint for class-based views::
+
+ # urls.py
+ from django.conf.urls.defaults import *
+ from some_app.views import AboutView
+
+ urlpatterns = patterns('',
+ (r'^about/', AboutView.as_view()),
+ )
+
+Alternatively, if you're only changing a few simple attributes on a
+class-based view, you can simply pass the new attributes into the as_view
+method call itself::
+
+ from django.conf.urls.defaults import *
+ from django.views.generic import TemplateView
+
+ urlpatterns = patterns('',
+ (r'^about/', TemplateView.as_view(template_name="about.html")),
+ )
+
+A similar overriding pattern can be used for the ``url`` attribute on
+:class:`~django.views.generic.base.RedirectView`, another simple
+generic view.
+
+
+Generic views of objects
+========================
+
+:class:`~django.views.generic.base.TemplateView` certainly is useful,
+but Django's generic views really shine when it comes to presenting
+views on your database content. Because it's such a common task,
+Django comes with a handful of built-in generic views that make
+generating list and detail views of objects incredibly easy.
+
+Let's take a look at one of these generic views: the "object list" view. We'll
+be using these models::
+
+ # models.py
+ from django.db import models
+
+ class Publisher(models.Model):
+ name = models.CharField(max_length=30)
+ address = models.CharField(max_length=50)
+ city = models.CharField(max_length=60)
+ state_province = models.CharField(max_length=30)
+ country = models.CharField(max_length=50)
+ website = models.URLField()
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ ordering = ["-name"]
+
+ class Book(models.Model):
+ title = models.CharField(max_length=100)
+ authors = models.ManyToManyField('Author')
+ publisher = models.ForeignKey(Publisher)
+ publication_date = models.DateField()
+
+To build a list page of all publishers, we'd use a URLconf along these lines::
+
+ from django.conf.urls.defaults import *
+ from django.views.generic import ListView
+ from books.models import Publisher
+
+ urlpatterns = patterns('',
+ (r'^publishers/$', ListView.as_view(
+ model=Publisher,
+ )),
+ )
+
+That's all the Python code we need to write. We still need to write a template,
+however. We could explicitly tell the view which template to use
+by including a ``template_name`` key in the arguments to as_view, but in
+the absence of an explicit template Django will infer one from the object's
+name. In this case, the inferred template will be
+``"books/publisher_list.html"`` -- the "books" part comes from the name of the
+app that defines the model, while the "publisher" bit is just the lowercased
+version of the model's name.
+
+.. highlightlang:: html+django
+
+This template will be rendered against a context containing a variable called
+``object_list`` that contains all the publisher objects. A very simple template
+might look like the following::
+
+ {% extends "base.html" %}
+
+ {% block content %}
+ <h2>Publishers</h2>
+ <ul>
+ {% for publisher in object_list %}
+ <li>{{ publisher.name }}</li>
+ {% endfor %}
+ </ul>
+ {% endblock %}
+
+That's really all there is to it. All the cool features of generic views come
+from changing the "info" dictionary passed to the generic view. The
+:doc:`generic views reference</ref/class-based-views>` documents all the generic
+views and all their options in detail; the rest of this document will consider
+some of the common ways you might customize and extend generic views.
+
+
+Extending generic views
+=======================
+
+.. highlightlang:: python
+
+There's no question that using generic views can speed up development
+substantially. In most projects, however, there comes a moment when the
+generic views no longer suffice. Indeed, the most common question asked by new
+Django developers is how to make generic views handle a wider array of
+situations.
+
+This is one of the reasons generic views were redesigned for the 1.3 release -
+previously, they were just view functions with a bewildering array of options;
+now, rather than passing in a large amount of configuration in the URLconf,
+the recommended way to extend generic views is to subclass them, and override
+their attributes or methods.
+
+
+Making "friendly" template contexts
+-----------------------------------
+
+You might have noticed that our sample publisher list template stores all the
+books in a variable named ``object_list``. While this works just fine, it isn't
+all that "friendly" to template authors: they have to "just know" that they're
+dealing with publishers here. A better name for that variable would be
+``publisher_list``; that variable's content is pretty obvious.
+
+We can change the name of that variable easily with the ``context_object_name``
+attribute - here, we'll override it in the URLconf, since it's a simple change:
+
+.. parsed-literal::
+
+ urlpatterns = patterns('',
+ (r'^publishers/$', ListView.as_view(
+ model=Publisher,
+ **context_object_name = "publisher_list",**
+ )),
+ )
+
+Providing a useful ``context_object_name`` is always a good idea. Your
+coworkers who design templates will thank you.
+
+
+Adding extra context
+--------------------
+
+Often you simply need to present some extra information beyond that
+provided by the generic view. For example, think of showing a list of
+all the books on each publisher detail page. The
+:class:`~django.views.generic.detail.DetailView` generic view provides
+the publisher to the context, but it seems there's no way to get
+additional information in that template.
+
+However, there is; you can subclass
+:class:`~django.views.generic.detail.DetailView` and provide your own
+implementation of the ``get_context_data`` method. The default
+implementation of this that comes with
+:class:`~django.views.generic.detail.DetailView` simply adds in the
+object being displayed to the template, but we can override it to show
+more::
+
+ from django.views.generic import DetailView
+ from some_app.models import Publisher, Book
+
+ class PublisherDetailView(DetailView):
+
+ context_object_name = "publisher"
+ model = Publisher
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = DetailView.get_context_data(self, **kwargs)
+ # Add in a QuerySet of all the books
+ context['book_list'] = Book.objects.all()
+ return context
+
+
+Viewing subsets of objects
+--------------------------
+
+Now let's take a closer look at the ``model`` argument we've been
+using all along. The ``model`` argument, which specifies the database
+model that the view will operate upon, is available on all the
+generic views that operate on a single object or a collection of
+objects. However, the ``model`` argument is not the only way to
+specify the objects that the view will operate upon -- you can also
+specify the list of objects using the ``queryset`` argument::
+
+ from django.views.generic import DetailView
+ from some_app.models import Publisher, Book
+
+ class PublisherDetailView(DetailView):
+
+ context_object_name = "publisher"
+ queryset = Publisher.object.all()
+
+Specifying ``mode = Publisher`` is really just shorthand for saying
+``queryset = Publisher.objects.all()``. However, by using ``queryset``
+to define a filtered list of objects you can be more specific about the
+objects that will be visible in the view (see :doc:`/topics/db/queries`
+for more information about ``QuerySet`` objects, and see the
+:doc:`generic views reference</ref/generic-views>` for the complete details).
+
+To pick a simple example, we might want to order a list of books by
+publication date, with the most recent first::
+
+ urlpatterns = patterns('',
+ (r'^publishers/$', ListView.as_view(
+ queryset = Publisher.objects.all(),
+ context_object_name = "publisher_list",
+ )),
+ (r'^publishers/$', ListView.as_view(
+ queryset = Book.objects.order_by("-publication_date"),
+ context_object_name = "book_list",
+ )),
+ )
+
+
+That's a pretty simple example, but it illustrates the idea nicely. Of course,
+you'll usually want to do more than just reorder objects. If you want to
+present a list of books by a particular publisher, you can use the same
+technique (here, illustrated using subclassing rather than by passing arguments
+in the URLconf)::
+
+ from django.views.generic import ListView
+ from some_app.models import Book
+
+ class AcmeBookListView(ListView):
+
+ context_object_name = "book_list"
+ queryset = Book.objects.filter(publisher__name="Acme Publishing")
+ template_name = "books/acme_list.html"
+
+Notice that along with a filtered ``queryset``, we're also using a custom
+template name. If we didn't, the generic view would use the same template as the
+"vanilla" object list, which might not be what we want.
+
+Also notice that this isn't a very elegant way of doing publisher-specific
+books. If we want to add another publisher page, we'd need another handful of
+lines in the URLconf, and more than a few publishers would get unreasonable.
+We'll deal with this problem in the next section.
+
+.. note::
+
+ If you get a 404 when requesting ``/books/acme/``, check to ensure you
+ actually have a Publisher with the name 'ACME Publishing'. Generic
+ views have an ``allow_empty`` parameter for this case. See the
+ :doc:`generic views reference</ref/class-based-views>` for more details.
+
+
+Dynamic filtering
+-----------------
+
+Another common need is to filter down the objects given in a list page by some
+key in the URL. Earlier we hard-coded the publisher's name in the URLconf, but
+what if we wanted to write a view that displayed all the books by some arbitrary
+publisher?
+
+Handily, the ListView has a ``get_queryset`` method we can override. Previously,
+it has just been returning the value of the ``queryset`` attribute, but now we
+can add more logic.
+
+The key part to making this work is that when class-based views are called,
+various useful things are stored on ``self``; as well as the request
+(``self.request``) this includes the positional (``self.args``) and name-based
+(``self.kwargs``) arguments captured according to the URLconf.
+
+Here, we have a URLconf with a single captured group::
+
+ from some_app.views import PublisherBookListView
+
+ urlpatterns = patterns('',
+ (r'^books/(\w+)/$', PublisherBookListView.as_view()),
+ )
+
+Next, we'll write the ``PublisherBookListView`` view itself::
+
+ from django.shortcuts import get_object_or_404
+ from django.views.generic import ListView
+ from some_app.models import Book, Publisher
+
+ class PublisherBookListView(ListView):
+
+ context_object_name = "book_list"
+ template_name = "books/books_by_publisher.html",
+
+ def get_queryset(self):
+ publisher = get_object_or_404(Publisher, name__iexact=self.args[0])
+ return Book.objects.filter(publisher=publisher)
+
+As you can see, it's quite easy to add more logic to the queryset selection;
+if we wanted, we could use ``self.request.user`` to filter using the current
+user, or other more complex logic.
+
+We can also add the publisher into the context at the same time, so we can
+use it in the template::
+
+ class PublisherBookListView(ListView):
+
+ context_object_name = "book_list"
+ template_name = "books/books_by_publisher.html",
+
+ def get_queryset(self):
+ self.publisher = get_object_or_404(Publisher, name__iexact=self.args[0])
+ return Book.objects.filter(publisher=self.publisher)
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = ListView.get_context_data(self, **kwargs)
+ # Add in the publisher
+ context['publisher'] = self.publisher
+ return context
+
+Performing extra work
+---------------------
+
+The last common pattern we'll look at involves doing some extra work before
+or after calling the generic view.
+
+Imagine we had a ``last_accessed`` field on our ``Author`` object that we were
+using to keep track of the last time anybody looked at that author::
+
+ # models.py
+
+ class Author(models.Model):
+ salutation = models.CharField(max_length=10)
+ first_name = models.CharField(max_length=30)
+ last_name = models.CharField(max_length=40)
+ email = models.EmailField()
+ headshot = models.ImageField(upload_to='/tmp')
+ last_accessed = models.DateTimeField()
+
+The generic ``DetailView`` class, of course, wouldn't know anything about this
+field, but once again we could easily write a custom view to keep that field
+updated.
+
+First, we'd need to add an author detail bit in the URLconf to point to a
+custom view:
+
+.. parsed-literal::
+
+ from some_app.views import AuthorDetailView
+
+ urlpatterns = patterns('',
+ #...
+ **(r'^authors/(?P<pk>\\d+)/$', AuthorDetailView.as_view()),**
+ )
+
+Then we'd write our new view - ``get_object`` is the method that retrieves the
+object, so we simply override it and wrap the call::
+
+ import datetime
+ from some_app.models import Author
+ from django.views.generic import DetailView
+ from django.shortcuts import get_object_or_404
+
+ class AuthorDetailView(DetailView):
+
+ queryset = Author.objects.all()
+
+ def get_object(self, **kwargs):
+ # Call the superclass
+ object = DetailView.get_object(self, **kwargs)
+ # Record the lass accessed date
+ object.last_accessed = datetime.datetime.now()
+ object.save()
+ # Return the object
+ return object
+
+.. note::
+
+ This code won't actually work unless you create a
+ ``books/author_detail.html`` template.
+
+.. note::
+
+ The URLconf here uses the named group ``pk`` - this name is the default
+ name that DetailView uses to find the value of the primary key used to
+ filter the queryset.
+
+ If you want to change it, you'll need to do your own ``get()`` call
+ on ``self.queryset`` using the new named parameter from ``self.kwargs``.
+
+More than just HTML
+-------------------
+
+So far, we've been focusing on rendering templates to generate
+responses. However, that's not all generic views can do.
+
+Each generic view is composed out of a series of mixins, and each
+mixin contributes a little piece of the entire view. Some of these
+mixins -- such as
+:class:`~django.views.generic.base.TemplateResponseMixin` -- are
+specifically designed for rendering content to a HTML response using a
+template. However, you can write your own mixins that perform
+different rendering behavior.
+
+For example, you a simple JSON mixin might look something like this::
+
+ class JSONResponseMixin(object):
+ def render_to_response(self, context):
+ "Returns a JSON response containing 'context' as payload"
+ return self.get_json_response(self.convert_context_to_json(context))
+
+ def get_json_response(self, content, **httpresponse_kwargs):
+ "Construct an `HttpResponse` object."
+ return http.HttpResponse(content,
+ content_type='application/json',
+ **httpresponse_kwargs)
+
+ def convert_context_to_json(self, context):
+ "Convert the context dictionary into a JSON object"
+ # Note: This is *EXTREMELY* naive; in reality, you'll need
+ # to do much more complex handling to ensure that arbitrary
+ # objects -- such as Django model instances or querysets
+ # -- can be serialized as JSON.
+ return json.dumps(content)
+
+Then, you could build a JSON-returning
+:class:`~django.views.generic.detail.DetailView` by mixing your
+:class:`JSONResponseMixin` with the
+:class:`~django.views.generic.detail.BaseDetailView` -- (the
+:class:`~django.views.generic.detail.DetailView` before template
+rendering behavior has been mixed in)::
+
+ class JSONDetailView(JSONResponseMixin, BaseDetailView):
+ pass
+
+This view can then be deployed in the same way as any other
+:class:`~django.views.generic.detail.DetailView`, with exactly the
+same behavior -- except for the format of the response.
+
+If you want to be really adventurous, you could even mix a
+:class:`~django.views.generic.detail.DetailView` subclass that is able
+to return *both* HTML and JSON content, depending on some property of
+the HTTP request, such as a query argument or a HTTP header. Just mix
+in both the :class:`JSONResponseMixin` and a
+:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`,
+and override the implementation of :func:`render_to_response()` to defer
+to the appropriate subclass depending on the type of response that the user
+requested::
+
+ class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
+ def render_to_response(self, context):
+ # Look for a 'format=json' GET argument
+ if self.request.GET.get('format','html') == 'json':
+ return JSONResponseMixin.render_to_response(self, context)
+ else:
+ return SingleObjectTemplateResponseMixin.render_to_response(self, context)
+
+Because of the way that Python resolves method overloading, the local
+:func:``render_to_response()`` implementation will override the
+versions provided by :class:`JSONResponseMixin` and
+:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`.
diff --git a/docs/topics/generic-views-migration.txt b/docs/topics/generic-views-migration.txt
new file mode 100644
index 0000000000..3ac4204413
--- /dev/null
+++ b/docs/topics/generic-views-migration.txt
@@ -0,0 +1,127 @@
+======================================
+Migrating function-based generic views
+======================================
+
+All the :doc:`function-based generic views</ref/generic-views>`
+that existed in Django 1.2 have analogs as :doc:`class-based generic
+views</ref/class-based-views>` in Django 1.3. The feature set
+exposed in those function-based views can be replicated in a
+class-based way.
+
+How to migrate
+==============
+
+Replace generic views with generic classes
+------------------------------------------
+
+Existing usage of function-based generic views should be replaced with
+their class-based analogs:
+
+ ==================================================== ====================================================
+ Old function-based generic view New class-based generic view
+ ==================================================== ====================================================
+ ``django.views.generic.simple.direct_to_template`` :class:`django.views.generic.base.TemplateView`
+ ``django.views.generic.simple.redirect_to`` :class:`django.views.generic.base.RedirectView`
+ ``django.views.generic.list_detail.object_list`` :class:`django.views.generic.list.ListView`
+ ``django.views.generic.list_detail.object_detail`` :class:`django.views.generic.detail.DetailView`
+ ``django.views.generic.create_update.create_object`` :class:`django.views.generic.edit.CreateView`
+ ``django.views.generic.create_update.update_object`` :class:`django.views.generic.edit.UpdateView`
+ ``django.views.generic.create_update.delete_object`` :class:`django.views.generic.edit.DeleteView`
+ ``django.views.generic.date_based.archive_index`` :class:`django.views.generic.dates.ArchiveIndexView`
+ ``django.views.generic.date_based.archive_year`` :class:`django.views.generic.dates.YearArchiveView`
+ ``django.views.generic.date_based.archive_month`` :class:`django.views.generic.dates.MonthArchiveView`
+ ``django.views.generic.date_based.archive_week`` :class:`django.views.generic.dates.WeekArchiveView`
+ ``django.views.generic.date_based.archive_day`` :class:`django.views.generic.dates.DayArchiveView`
+ ``django.views.generic.date_based.archive_today`` :class:`django.views.generic.dates.TodayArchiveView`
+ ``django.views.generic.date_based.object_detail`` :class:`django.views.generic.dates.DateDetailView`
+ ==================================================== ====================================================
+
+To do this, replace the reference to the generic view function with
+a ``as_view()`` instantiation of the class-based view. For example,
+the old-style ``direct_to_template`` pattern::
+
+ ('^about/$', direct_to_template, {'template': 'about.html'})
+
+can be replaced with an instance of
+:class:`~django.views.generic.base.TemplateView`::
+
+ ('^about/$', TemplateView.as_view(template_name='about.html'))
+
+``template`` argument to ``direct_to_template`` views
+-----------------------------------------------------
+
+The ``template`` argument to the ``direct_to_template`` view has been renamed
+``template_name``. This has ben done to maintain consistency with other views.
+
+``object_id`` argument to detail views
+--------------------------------------
+
+The object_id argument to the ``object_detail`` view has been renamed
+``pk`` on the :class:`~django.views.generic.detail.DetailView`.
+
+``template_object_name``
+------------------------
+
+``template_object_name`` has been renamed ``context_object_name``,
+reflecting the fact that the context data can be used for purposes
+other than template rendering (e.g., to populate JSON output).
+
+The ``_list`` suffix on list views
+----------------------------------
+
+In a function-based :class:`ListView`, the ``template_object_name``
+was appended with the suffix ``'_list'`` to yield the final context
+variable name. In a class-based ``ListView``, the
+``context_object_name`` is used verbatim.
+
+``extra_context``
+-----------------
+
+Function-based generic views provided an ``extra_context`` argument
+as way to insert extra items into the context at time of rendering.
+
+Class-based views don't provide an ``extra_context`` argument.
+Instead, you subclass the view, overriding :meth:`get_context_data()`.
+For example::
+
+ class MyListView(ListView):
+ def get_context_data(self, **kwargs):
+ context = super(MyListView, self).get_context_data(**kwargs)
+ context.update({
+ 'foo': 42,
+ 'bar': 37
+ })
+ return context
+
+``mimetype``
+------------
+
+Some function-based generic views provided a ``mimetype`` argument
+as way to control the mimetype of the response.
+
+Class-based views don't provide a ``mimetype`` argument. Instead, you
+subclass the view, overriding
+:meth:`TemplateResponseMixin.get_response()` and pass in arguments for
+the HttpResponse constructor. For example::
+
+ class MyListView(ListView):
+ def get_response(self, content, **kwargs):
+ return super(MyListView, self).get_response(content,
+ content_type='application/json', **kwargs)
+
+``context_processors``
+----------------------
+
+Some function-based generic views provided a ``context_processors``
+argument that could be used to force the use of specialized context
+processors when rendering template content.
+
+Class-based views don't provide a ``context_processors`` argument.
+Instead, you subclass the view, overriding
+:meth:`TemplateResponseMixin.get_context_instance()`. For example::
+
+ class MyListView(ListView):
+ def get_context_instance(self, context):
+ return RequestContext(self.request,
+ context,
+ processors=[custom_processor])
diff --git a/docs/topics/index.txt b/docs/topics/index.txt
index c9c2f2d033..1f680a1651 100644
--- a/docs/topics/index.txt
+++ b/docs/topics/index.txt
@@ -12,7 +12,8 @@ Introductions to all the key parts of Django you'll need to know:
forms/index
forms/modelforms
templates
- generic-views
+ class-based-views
+ generic-views-migration
files
testing
auth
@@ -26,3 +27,10 @@ Introductions to all the key parts of Django you'll need to know:
settings
signals
+Deprecated features
+-------------------
+
+.. toctree::
+ :maxdepth: 1
+
+ generic-views
diff --git a/tests/regressiontests/generic_views/__init__.py b/tests/regressiontests/generic_views/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/generic_views/__init__.py
diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py
new file mode 100644
index 0000000000..a1da98690b
--- /dev/null
+++ b/tests/regressiontests/generic_views/base.py
@@ -0,0 +1,233 @@
+import unittest
+
+from django.core.exceptions import ImproperlyConfigured
+from django.http import HttpResponse
+from django.test import TestCase, RequestFactory
+from django.utils import simplejson
+from django.views.generic import View, TemplateView, RedirectView
+
+
+class SimpleView(View):
+ """
+ A simple view with a docstring.
+ """
+ def get(self, request):
+ return HttpResponse('This is a simple view')
+
+
+class SimplePostView(SimpleView):
+ post = SimpleView.get
+
+
+class CustomizableView(SimpleView):
+ parameter = {}
+
+def decorator(view):
+ view.is_decorated = True
+ return view
+
+
+class DecoratedDispatchView(SimpleView):
+
+ @decorator
+ def dispatch(self, request, *args, **kwargs):
+ return super(DecoratedDispatchView, self).dispatch(request, *args, **kwargs)
+
+
+class AboutTemplateView(TemplateView):
+ def get(self, request):
+ return self.render_to_response({})
+
+ def get_template_names(self):
+ return ['generic_views/about.html']
+
+
+class AboutTemplateAttributeView(TemplateView):
+ template_name = 'generic_views/about.html'
+
+ def get(self, request):
+ return self.render_to_response(context={})
+
+
+class InstanceView(View):
+
+ def get(self, request):
+ return self
+
+
+class ViewTest(unittest.TestCase):
+ rf = RequestFactory()
+
+ def _assert_simple(self, response):
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, 'This is a simple view')
+
+ def test_no_init_kwargs(self):
+ """
+ Test that a view can't be accidentally instantiated before deployment
+ """
+ try:
+ view = SimpleView(key='value').as_view()
+ self.fail('Should not be able to instantiate a view')
+ except AttributeError:
+ pass
+
+ def test_no_init_args(self):
+ """
+ Test that a view can't be accidentally instantiated before deployment
+ """
+ try:
+ view = SimpleView.as_view('value')
+ self.fail('Should not be able to use non-keyword arguments instantiating a view')
+ except TypeError:
+ pass
+
+ def test_pathological_http_method(self):
+ """
+ The edge case of a http request that spoofs an existing method name is caught.
+ """
+ self.assertEqual(SimpleView.as_view()(
+ self.rf.get('/', REQUEST_METHOD='DISPATCH')
+ ).status_code, 405)
+
+ def test_get_only(self):
+ """
+ Test a view which only allows GET doesn't allow other methods.
+ """
+ self._assert_simple(SimpleView.as_view()(self.rf.get('/')))
+ self.assertEqual(SimpleView.as_view()(self.rf.post('/')).status_code, 405)
+ self.assertEqual(SimpleView.as_view()(
+ self.rf.get('/', REQUEST_METHOD='FAKE')
+ ).status_code, 405)
+
+ def test_get_and_post(self):
+ """
+ Test a view which only allows both GET and POST.
+ """
+ self._assert_simple(SimplePostView.as_view()(self.rf.get('/')))
+ self._assert_simple(SimplePostView.as_view()(self.rf.post('/')))
+ self.assertEqual(SimplePostView.as_view()(
+ self.rf.get('/', REQUEST_METHOD='FAKE')
+ ).status_code, 405)
+
+ def test_invalid_keyword_argument(self):
+ """
+ Test that view arguments must be predefined on the class and can't
+ be named like a HTTP method.
+ """
+ # Check each of the allowed method names
+ for method in SimpleView.http_method_names:
+ kwargs = dict(((method, "value"),))
+ self.assertRaises(TypeError, SimpleView.as_view, **kwargs)
+
+ # Check the case view argument is ok if predefined on the class...
+ CustomizableView.as_view(parameter="value")
+ # ...but raises errors otherwise.
+ self.assertRaises(TypeError, CustomizableView.as_view, foobar="value")
+
+ def test_calling_more_than_once(self):
+ """
+ Test a view can only be called once.
+ """
+ request = self.rf.get('/')
+ view = InstanceView.as_view()
+ self.assertNotEqual(view(request), view(request))
+
+ def test_class_attributes(self):
+ """
+ Test that the callable returned from as_view() has proper
+ docstring, name and module.
+ """
+ self.assertEqual(SimpleView.__doc__, SimpleView.as_view().__doc__)
+ self.assertEqual(SimpleView.__name__, SimpleView.as_view().__name__)
+ self.assertEqual(SimpleView.__module__, SimpleView.as_view().__module__)
+
+ def test_dispatch_decoration(self):
+ """
+ Test that attributes set by decorators on the dispatch method
+ are also present on the closure.
+ """
+ self.assertTrue(DecoratedDispatchView.as_view().is_decorated)
+
+
+class TemplateViewTest(TestCase):
+ urls = 'regressiontests.generic_views.urls'
+
+ rf = RequestFactory()
+
+ def _assert_about(self, response):
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, '<h1>About</h1>')
+
+ def test_get(self):
+ """
+ Test a view that simply renders a template on GET
+ """
+ self._assert_about(AboutTemplateView.as_view()(self.rf.get('/about/')))
+
+ def test_get_template_attribute(self):
+ """
+ Test a view that renders a template on GET with the template name as
+ an attribute on the class.
+ """
+ self._assert_about(AboutTemplateAttributeView.as_view()(self.rf.get('/about/')))
+
+ def test_get_generic_template(self):
+ """
+ Test a completely generic view that renders a template on GET
+ with the template name as an argument at instantiation.
+ """
+ self._assert_about(TemplateView.as_view(template_name='generic_views/about.html')(self.rf.get('/about/')))
+
+ def test_template_params(self):
+ """
+ A generic template view passes kwargs as context.
+ """
+ response = self.client.get('/template/simple/bar/')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['params'], {'foo': 'bar'})
+
+ def test_extra_template_params(self):
+ """
+ A template view can be customized to return extra context.
+ """
+ response = self.client.get('/template/custom/bar/')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context['params'], {'foo': 'bar'})
+ self.assertEqual(response.context['key'], 'value')
+
+class RedirectViewTest(unittest.TestCase):
+ rf = RequestFactory()
+
+ def test_no_url(self):
+ "Without any configuration, returns HTTP 410 GONE"
+ response = RedirectView.as_view()(self.rf.get('/foo/'))
+ self.assertEquals(response.status_code, 410)
+
+ def test_permanaent_redirect(self):
+ "Default is a permanent redirect"
+ response = RedirectView.as_view(url='/bar/')(self.rf.get('/foo/'))
+ self.assertEquals(response.status_code, 301)
+ self.assertEquals(response['Location'], '/bar/')
+
+ def test_temporary_redirect(self):
+ "Permanent redirects are an option"
+ response = RedirectView.as_view(url='/bar/', permanent=False)(self.rf.get('/foo/'))
+ self.assertEquals(response.status_code, 302)
+ self.assertEquals(response['Location'], '/bar/')
+
+ def test_include_args(self):
+ "GET arguments can be included in the redirected URL"
+ response = RedirectView.as_view(url='/bar/')(self.rf.get('/foo/'))
+ self.assertEquals(response.status_code, 301)
+ self.assertEquals(response['Location'], '/bar/')
+
+ response = RedirectView.as_view(url='/bar/', query_string=True)(self.rf.get('/foo/?pork=spam'))
+ self.assertEquals(response.status_code, 301)
+ self.assertEquals(response['Location'], '/bar/?pork=spam')
+
+ def test_parameter_substitution(self):
+ "Redirection URLs can be parameterized"
+ response = RedirectView.as_view(url='/bar/%(object_id)d/')(self.rf.get('/foo/42/'), object_id=42)
+ self.assertEquals(response.status_code, 301)
+ self.assertEquals(response['Location'], '/bar/42/')
diff --git a/tests/regressiontests/generic_views/dates.py b/tests/regressiontests/generic_views/dates.py
new file mode 100644
index 0000000000..c730c763b9
--- /dev/null
+++ b/tests/regressiontests/generic_views/dates.py
@@ -0,0 +1,352 @@
+import datetime
+import random
+
+from django.core.exceptions import ImproperlyConfigured
+from django.test import TestCase
+
+from regressiontests.generic_views.models import Book
+
+class ArchiveIndexViewTests(TestCase):
+ fixtures = ['generic-views-test-data.json']
+ urls = 'regressiontests.generic_views.urls'
+
+ def _make_books(self, n, base_date):
+ for i in range(n):
+ b = Book.objects.create(
+ name='Book %d' % i,
+ slug='book-%d' % i,
+ pages=100+i,
+ pubdate=base_date - datetime.timedelta(days=1))
+
+ def test_archive_view(self):
+ res = self.client.get('/dates/books/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1])
+ self.assertEqual(list(res.context['latest']), list(Book.objects.all()))
+ self.assertTemplateUsed(res, 'generic_views/book_archive.html')
+
+ def test_archive_view_context_object_name(self):
+ res = self.client.get('/dates/books/context_object_name/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1])
+ self.assertEqual(list(res.context['thingies']), list(Book.objects.all()))
+ self.assertFalse('latest' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/book_archive.html')
+
+ def test_empty_archive_view(self):
+ Book.objects.all().delete()
+ res = self.client.get('/dates/books/')
+ self.assertEqual(res.status_code, 404)
+
+ def test_allow_empty_archive_view(self):
+ Book.objects.all().delete()
+ res = self.client.get('/dates/books/allow_empty/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['date_list']), [])
+ self.assertEqual(list(res.context['date_list']), [])
+ self.assertTemplateUsed(res, 'generic_views/book_archive.html')
+
+ def test_archive_view_template(self):
+ res = self.client.get('/dates/books/template_name/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1])
+ self.assertEqual(list(res.context['latest']), list(Book.objects.all()))
+ self.assertTemplateUsed(res, 'generic_views/list.html')
+
+ def test_archive_view_template_suffix(self):
+ res = self.client.get('/dates/books/template_name_suffix/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1])
+ self.assertEqual(list(res.context['latest']), list(Book.objects.all()))
+ self.assertTemplateUsed(res, 'generic_views/book_detail.html')
+
+ def test_archive_view_invalid(self):
+ self.assertRaises(ImproperlyConfigured, self.client.get, '/dates/books/invalid/')
+
+ def test_paginated_archive_view(self):
+ self._make_books(20, base_date=datetime.date.today())
+ res = self.client.get('/dates/books/paginated/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['date_list'], Book.objects.dates('pubdate', 'year')[::-1])
+ self.assertEqual(list(res.context['latest']), list(Book.objects.all()[0:10]))
+ self.assertTemplateUsed(res, 'generic_views/book_archive.html')
+
+ res = self.client.get('/dates/books/paginated/?page=2')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['page_obj'].number, 2)
+ self.assertEqual(list(res.context['latest']), list(Book.objects.all()[10:20]))
+
+
+class YearArchiveViewTests(TestCase):
+ fixtures = ['generic-views-test-data.json']
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_year_view(self):
+ res = self.client.get('/dates/books/2008/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['date_list']), [datetime.datetime(2008, 10, 1)])
+ self.assertEqual(res.context['year'], '2008')
+ self.assertTemplateUsed(res, 'generic_views/book_archive_year.html')
+
+ def test_year_view_make_object_list(self):
+ res = self.client.get('/dates/books/2006/make_object_list/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['date_list']), [datetime.datetime(2006, 5, 1)])
+ self.assertEqual(list(res.context['books']), list(Book.objects.filter(pubdate__year=2006)))
+ self.assertEqual(list(res.context['object_list']), list(Book.objects.filter(pubdate__year=2006)))
+ self.assertTemplateUsed(res, 'generic_views/book_archive_year.html')
+
+ def test_year_view_empty(self):
+ res = self.client.get('/dates/books/1999/')
+ self.assertEqual(res.status_code, 404)
+ res = self.client.get('/dates/books/1999/allow_empty/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['date_list']), [])
+ self.assertEqual(list(res.context['books']), [])
+
+ def test_year_view_allow_future(self):
+ # Create a new book in the future
+ year = datetime.date.today().year + 1
+ b = Book.objects.create(name="The New New Testement", pages=600, pubdate=datetime.date(year, 1, 1))
+ res = self.client.get('/dates/books/%s/' % year)
+ self.assertEqual(res.status_code, 404)
+
+ res = self.client.get('/dates/books/%s/allow_empty/' % year)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['books']), [])
+
+ res = self.client.get('/dates/books/%s/allow_future/' % year)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['date_list']), [datetime.datetime(year, 1, 1)])
+
+ def test_year_view_invalid_pattern(self):
+ res = self.client.get('/dates/books/no_year/')
+ self.assertEqual(res.status_code, 404)
+
+class MonthArchiveViewTests(TestCase):
+ fixtures = ['generic-views-test-data.json']
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_month_view(self):
+ res = self.client.get('/dates/books/2008/oct/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/book_archive_month.html')
+ self.assertEqual(list(res.context['date_list']), [datetime.datetime(2008, 10, 1)])
+ self.assertEqual(list(res.context['books']),
+ list(Book.objects.filter(pubdate=datetime.date(2008, 10, 1))))
+ self.assertEqual(res.context['month'], datetime.date(2008, 10, 1))
+
+ # Since allow_empty=False, next/prev months must be valid (#7164)
+ self.assertEqual(res.context['next_month'], None)
+ self.assertEqual(res.context['previous_month'], datetime.date(2006, 5, 1))
+
+ def test_month_view_allow_empty(self):
+ # allow_empty = False, empty month
+ res = self.client.get('/dates/books/2000/jan/')
+ self.assertEqual(res.status_code, 404)
+
+ # allow_empty = True, empty month
+ res = self.client.get('/dates/books/2000/jan/allow_empty/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['date_list']), [])
+ self.assertEqual(list(res.context['books']), [])
+ self.assertEqual(res.context['month'], datetime.date(2000, 1, 1))
+
+ # Since it's allow empty, next/prev are allowed to be empty months (#7164)
+ self.assertEqual(res.context['next_month'], datetime.date(2000, 2, 1))
+ self.assertEqual(res.context['previous_month'], datetime.date(1999, 12, 1))
+
+ # allow_empty but not allow_future: next_month should be empty (#7164)
+ url = datetime.date.today().strftime('/dates/books/%Y/%b/allow_empty/').lower()
+ res = self.client.get(url)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['next_month'], None)
+
+ def test_month_view_allow_future(self):
+ future = (datetime.date.today() + datetime.timedelta(days=60)).replace(day=1)
+ urlbit = future.strftime('%Y/%b').lower()
+ b = Book.objects.create(name="The New New Testement", pages=600, pubdate=future)
+
+ # allow_future = False, future month
+ res = self.client.get('/dates/books/%s/' % urlbit)
+ self.assertEqual(res.status_code, 404)
+
+ # allow_future = True, valid future month
+ res = self.client.get('/dates/books/%s/allow_future/' % urlbit)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['date_list'][0].date(), b.pubdate)
+ self.assertEqual(list(res.context['books']), [b])
+ self.assertEqual(res.context['month'], future)
+
+ # Since it's allow_future but not allow_empty, next/prev are not
+ # allowed to be empty months (#7164)
+ self.assertEqual(res.context['next_month'], None)
+ self.assertEqual(res.context['previous_month'], datetime.date(2008, 10, 1))
+
+ # allow_future, but not allow_empty, with a current month. So next
+ # should be in the future (yup, #7164, again)
+ res = self.client.get('/dates/books/2008/oct/allow_future/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['next_month'], future)
+ self.assertEqual(res.context['previous_month'], datetime.date(2006, 5, 1))
+
+ def test_custom_month_format(self):
+ res = self.client.get('/dates/books/2008/10/')
+ self.assertEqual(res.status_code, 200)
+
+ def test_month_view_invalid_pattern(self):
+ res = self.client.get('/dates/books/2007/no_month/')
+ self.assertEqual(res.status_code, 404)
+
+class WeekArchiveViewTests(TestCase):
+ fixtures = ['generic-views-test-data.json']
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_week_view(self):
+ res = self.client.get('/dates/books/2008/week/39/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/book_archive_week.html')
+ self.assertEqual(res.context['books'][0], Book.objects.get(pubdate=datetime.date(2008, 10, 1)))
+ self.assertEqual(res.context['week'], datetime.date(2008, 9, 28))
+
+ def test_week_view_allow_empty(self):
+ res = self.client.get('/dates/books/2008/week/12/')
+ self.assertEqual(res.status_code, 404)
+
+ res = self.client.get('/dates/books/2008/week/12/allow_empty/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['books']), [])
+
+ def test_week_view_allow_future(self):
+ future = datetime.date(datetime.date.today().year + 1, 1, 1)
+ b = Book.objects.create(name="The New New Testement", pages=600, pubdate=future)
+
+ res = self.client.get('/dates/books/%s/week/0/' % future.year)
+ self.assertEqual(res.status_code, 404)
+
+ res = self.client.get('/dates/books/%s/week/0/allow_future/' % future.year)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['books']), [b])
+
+ def test_week_view_invalid_pattern(self):
+ res = self.client.get('/dates/books/2007/week/no_week/')
+ self.assertEqual(res.status_code, 404)
+
+class DayArchiveViewTests(TestCase):
+ fixtures = ['generic-views-test-data.json']
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_day_view(self):
+ res = self.client.get('/dates/books/2008/oct/01/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/book_archive_day.html')
+ self.assertEqual(list(res.context['books']),
+ list(Book.objects.filter(pubdate=datetime.date(2008, 10, 1))))
+ self.assertEqual(res.context['day'], datetime.date(2008, 10, 1))
+
+ # Since allow_empty=False, next/prev days must be valid.
+ self.assertEqual(res.context['next_day'], None)
+ self.assertEqual(res.context['previous_day'], datetime.date(2006, 5, 1))
+
+ def test_day_view_allow_empty(self):
+ # allow_empty = False, empty month
+ res = self.client.get('/dates/books/2000/jan/1/')
+ self.assertEqual(res.status_code, 404)
+
+ # allow_empty = True, empty month
+ res = self.client.get('/dates/books/2000/jan/1/allow_empty/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['books']), [])
+ self.assertEqual(res.context['day'], datetime.date(2000, 1, 1))
+
+ # Since it's allow empty, next/prev are allowed to be empty months (#7164)
+ self.assertEqual(res.context['next_day'], datetime.date(2000, 1, 2))
+ self.assertEqual(res.context['previous_day'], datetime.date(1999, 12, 31))
+
+ # allow_empty but not allow_future: next_month should be empty (#7164)
+ url = datetime.date.today().strftime('/dates/books/%Y/%b/%d/allow_empty/').lower()
+ res = self.client.get(url)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['next_day'], None)
+
+ def test_day_view_allow_future(self):
+ future = (datetime.date.today() + datetime.timedelta(days=60))
+ urlbit = future.strftime('%Y/%b/%d').lower()
+ b = Book.objects.create(name="The New New Testement", pages=600, pubdate=future)
+
+ # allow_future = False, future month
+ res = self.client.get('/dates/books/%s/' % urlbit)
+ self.assertEqual(res.status_code, 404)
+
+ # allow_future = True, valid future month
+ res = self.client.get('/dates/books/%s/allow_future/' % urlbit)
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['books']), [b])
+ self.assertEqual(res.context['day'], future)
+
+ # allow_future but not allow_empty, next/prev amust be valid
+ self.assertEqual(res.context['next_day'], None)
+ self.assertEqual(res.context['previous_day'], datetime.date(2008, 10, 1))
+
+ # allow_future, but not allow_empty, with a current month.
+ res = self.client.get('/dates/books/2008/oct/01/allow_future/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['next_day'], future)
+ self.assertEqual(res.context['previous_day'], datetime.date(2006, 5, 1))
+
+ def test_next_prev_context(self):
+ res = self.client.get('/dates/books/2008/oct/01/')
+ self.assertEqual(res.content, "Archive for Oct. 1, 2008. Previous day is May 1, 2006")
+
+ def test_custom_month_format(self):
+ res = self.client.get('/dates/books/2008/10/01/')
+ self.assertEqual(res.status_code, 200)
+
+ def test_day_view_invalid_pattern(self):
+ res = self.client.get('/dates/books/2007/oct/no_day/')
+ self.assertEqual(res.status_code, 404)
+
+ def test_today_view(self):
+ res = self.client.get('/dates/books/today/')
+ self.assertEqual(res.status_code, 404)
+ res = self.client.get('/dates/books/today/allow_empty/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['day'], datetime.date.today())
+
+class DateDetailViewTests(TestCase):
+ fixtures = ['generic-views-test-data.json']
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_date_detail_by_pk(self):
+ res = self.client.get('/dates/books/2008/oct/01/1/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Book.objects.get(pk=1))
+ self.assertEqual(res.context['book'], Book.objects.get(pk=1))
+ self.assertTemplateUsed(res, 'generic_views/book_detail.html')
+
+ def test_date_detail_by_slug(self):
+ res = self.client.get('/dates/books/2006/may/01/byslug/dreaming-in-code/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['book'], Book.objects.get(slug='dreaming-in-code'))
+
+ def test_date_detail_custom_month_format(self):
+ res = self.client.get('/dates/books/2008/10/01/1/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['book'], Book.objects.get(pk=1))
+
+ def test_date_detail_allow_future(self):
+ future = (datetime.date.today() + datetime.timedelta(days=60))
+ urlbit = future.strftime('%Y/%b/%d').lower()
+ b = Book.objects.create(name="The New New Testement", slug="new-new", pages=600, pubdate=future)
+
+ res = self.client.get('/dates/books/%s/new-new/' % urlbit)
+ self.assertEqual(res.status_code, 404)
+
+ res = self.client.get('/dates/books/%s/%s/allow_future/' % (urlbit, b.id))
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['book'], b)
+ self.assertTemplateUsed(res, 'generic_views/book_detail.html')
+
+ def test_invalid_url(self):
+ self.assertRaises(AttributeError, self.client.get, "/dates/books/2008/oct/01/nopk/")
+
diff --git a/tests/regressiontests/generic_views/detail.py b/tests/regressiontests/generic_views/detail.py
new file mode 100644
index 0000000000..91cacf65ba
--- /dev/null
+++ b/tests/regressiontests/generic_views/detail.py
@@ -0,0 +1,71 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.test import TestCase
+
+from regressiontests.generic_views.models import Author, Page
+
+
+class DetailViewTest(TestCase):
+ fixtures = ['generic-views-test-data.json']
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_simple_object(self):
+ res = self.client.get('/detail/obj/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], {'foo': 'bar'})
+ self.assertTemplateUsed(res, 'generic_views/detail.html')
+
+ def test_detail_by_pk(self):
+ res = self.client.get('/detail/author/1/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=1))
+ self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+
+ def test_detail_by_slug(self):
+ res = self.client.get('/detail/author/byslug/scott-rosenberg/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(slug='scott-rosenberg'))
+ self.assertEqual(res.context['author'], Author.objects.get(slug='scott-rosenberg'))
+ self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+
+ def test_template_name(self):
+ res = self.client.get('/detail/author/1/template_name/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=1))
+ self.assertTemplateUsed(res, 'generic_views/about.html')
+
+ def test_template_name_suffix(self):
+ res = self.client.get('/detail/author/1/template_name_suffix/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=1))
+ self.assertTemplateUsed(res, 'generic_views/author_view.html')
+
+ def test_template_name_field(self):
+ res = self.client.get('/detail/page/1/field/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Page.objects.get(pk=1))
+ self.assertEqual(res.context['page'], Page.objects.get(pk=1))
+ self.assertTemplateUsed(res, 'generic_views/page_template.html')
+
+ def test_context_object_name(self):
+ res = self.client.get('/detail/author/1/context_object_name/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertEqual(res.context['thingy'], Author.objects.get(pk=1))
+ self.assertFalse('author' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+
+ def test_duplicated_context_object_name(self):
+ res = self.client.get('/detail/author/1/dupe_context_object_name/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertFalse('author' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/author_detail.html')
+
+ def test_invalid_url(self):
+ self.assertRaises(AttributeError, self.client.get, '/detail/author/invalid/url/')
+
+ def test_invalid_queryset(self):
+ self.assertRaises(ImproperlyConfigured, self.client.get, '/detail/author/invalid/qs/')
diff --git a/tests/regressiontests/generic_views/edit.py b/tests/regressiontests/generic_views/edit.py
new file mode 100644
index 0000000000..87233ec357
--- /dev/null
+++ b/tests/regressiontests/generic_views/edit.py
@@ -0,0 +1,233 @@
+from django.core.exceptions import ImproperlyConfigured
+from django import forms
+from django.test import TestCase
+from django.utils.unittest import expectedFailure
+
+from regressiontests.generic_views.models import Artist, Author
+from regressiontests.generic_views import views
+
+
+class CreateViewTests(TestCase):
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_create(self):
+ res = self.client.get('/edit/authors/create/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(isinstance(res.context['form'], forms.ModelForm))
+ self.assertFalse('object' in res.context)
+ self.assertFalse('author' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/author_form.html')
+
+ res = self.client.post('/edit/authors/create/',
+ {'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/list/authors/')
+ self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe>'])
+
+ def test_create_invalid(self):
+ res = self.client.post('/edit/authors/create/',
+ {'name': 'A' * 101, 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_form.html')
+ self.assertEqual(len(res.context['form'].errors), 1)
+ self.assertEqual(Author.objects.count(), 0)
+
+ def test_create_with_object_url(self):
+ res = self.client.post('/edit/artists/create/',
+ {'name': 'Rene Magritte'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/detail/artist/1/')
+ self.assertQuerysetEqual(Artist.objects.all(), ['<Artist: Rene Magritte>'])
+
+ def test_create_with_redirect(self):
+ res = self.client.post('/edit/authors/create/redirect/',
+ {'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/edit/authors/create/')
+ self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe>'])
+
+ def test_create_with_special_properties(self):
+ res = self.client.get('/edit/authors/create/special/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(isinstance(res.context['form'], views.AuthorForm))
+ self.assertFalse('object' in res.context)
+ self.assertFalse('author' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/form.html')
+
+ res = self.client.post('/edit/authors/create/special/',
+ {'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/detail/author/1/')
+ self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe>'])
+
+ def test_create_without_redirect(self):
+ try:
+ res = self.client.post('/edit/authors/create/naive/',
+ {'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ self.fail('Should raise exception -- No redirect URL provided, and no get_absolute_url provided')
+ except ImproperlyConfigured:
+ pass
+
+ def test_create_restricted(self):
+ res = self.client.post('/edit/authors/create/restricted/',
+ {'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/accounts/login/?next=/edit/authors/create/restricted/')
+
+class UpdateViewTests(TestCase):
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_update_post(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+ res = self.client.get('/edit/author/1/update/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(isinstance(res.context['form'], forms.ModelForm))
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=1))
+ self.assertTemplateUsed(res, 'generic_views/author_form.html')
+
+ # Modification with both POST and PUT (browser compatible)
+ res = self.client.post('/edit/author/1/update/',
+ {'name': 'Randall Munroe (xkcd)', 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/list/authors/')
+ self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe (xkcd)>'])
+
+ @expectedFailure
+ def test_update_put(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+ res = self.client.get('/edit/author/1/update/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_form.html')
+
+ res = self.client.put('/edit/author/1/update/',
+ {'name': 'Randall Munroe (author of xkcd)', 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/list/authors/')
+ self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe (author of xkcd)>'])
+
+ def test_update_invalid(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+ res = self.client.post('/edit/author/1/update/',
+ {'name': 'A' * 101, 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_form.html')
+ self.assertEqual(len(res.context['form'].errors), 1)
+ self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe>'])
+
+ def test_update_with_object_url(self):
+ a = Artist.objects.create(name='Rene Magritte')
+ res = self.client.post('/edit/artists/1/update/',
+ {'name': 'Rene Magritte'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/detail/artist/1/')
+ self.assertQuerysetEqual(Artist.objects.all(), ['<Artist: Rene Magritte>'])
+
+ def test_update_with_redirect(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+ res = self.client.post('/edit/author/1/update/redirect/',
+ {'name': 'Randall Munroe (author of xkcd)', 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/edit/authors/create/')
+ self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe (author of xkcd)>'])
+
+ def test_update_with_special_properties(self):
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+ res = self.client.get('/edit/author/1/update/special/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(isinstance(res.context['form'], views.AuthorForm))
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertEqual(res.context['thingy'], Author.objects.get(pk=1))
+ self.assertFalse('author' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/form.html')
+
+ res = self.client.post('/edit/author/1/update/special/',
+ {'name': 'Randall Munroe (author of xkcd)', 'slug': 'randall-munroe'})
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/detail/author/1/')
+ self.assertQuerysetEqual(Author.objects.all(), ['<Author: Randall Munroe (author of xkcd)>'])
+
+ def test_update_without_redirect(self):
+ try:
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+ res = self.client.post('/edit/author/1/update/naive/',
+ {'name': 'Randall Munroe (author of xkcd)', 'slug': 'randall-munroe'})
+ self.fail('Should raise exception -- No redirect URL provided, and no get_absolute_url provided')
+ except ImproperlyConfigured:
+ pass
+
+class DeleteViewTests(TestCase):
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_delete_by_post(self):
+ Author.objects.create(**{'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ res = self.client.get('/edit/author/1/delete/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertEqual(res.context['author'], Author.objects.get(pk=1))
+ self.assertTemplateUsed(res, 'generic_views/author_confirm_delete.html')
+
+ # Deletion with POST
+ res = self.client.post('/edit/author/1/delete/')
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/list/authors/')
+ self.assertQuerysetEqual(Author.objects.all(), [])
+
+ def test_delete_by_delete(self):
+ # Deletion with browser compatible DELETE method
+ Author.objects.create(**{'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ res = self.client.delete('/edit/author/1/delete/')
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/list/authors/')
+ self.assertQuerysetEqual(Author.objects.all(), [])
+
+ def test_delete_with_redirect(self):
+ Author.objects.create(**{'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ res = self.client.post('/edit/author/1/delete/redirect/')
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/edit/authors/create/')
+ self.assertQuerysetEqual(Author.objects.all(), [])
+
+ def test_delete_with_special_properties(self):
+ Author.objects.create(**{'name': 'Randall Munroe', 'slug': 'randall-munroe'})
+ res = self.client.get('/edit/author/1/delete/special/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['object'], Author.objects.get(pk=1))
+ self.assertEqual(res.context['thingy'], Author.objects.get(pk=1))
+ self.assertFalse('author' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/confirm_delete.html')
+
+ res = self.client.post('/edit/author/1/delete/special/')
+ self.assertEqual(res.status_code, 302)
+ self.assertRedirects(res, 'http://testserver/list/authors/')
+ self.assertQuerysetEqual(Author.objects.all(), [])
+
+ def test_delete_without_redirect(self):
+ try:
+ a = Author.objects.create(
+ name='Randall Munroe',
+ slug='randall-munroe',
+ )
+ res = self.client.post('/edit/author/1/delete/naive/')
+ self.fail('Should raise exception -- No redirect URL provided, and no get_absolute_url provided')
+ except ImproperlyConfigured:
+ pass
+
diff --git a/tests/regressiontests/generic_views/fixtures/generic-views-test-data.json b/tests/regressiontests/generic_views/fixtures/generic-views-test-data.json
new file mode 100644
index 0000000000..8ecfe5e1e2
--- /dev/null
+++ b/tests/regressiontests/generic_views/fixtures/generic-views-test-data.json
@@ -0,0 +1,47 @@
+[
+ {
+ "model": "generic_views.author",
+ "pk": 1,
+ "fields": {
+ "name": "Roberto BolaƱo",
+ "slug": "roberto-bolano"
+ }
+ },
+ {
+ "model": "generic_views.author",
+ "pk": 2,
+ "fields": {
+ "name": "Scott Rosenberg",
+ "slug": "scott-rosenberg"
+ }
+ },
+ {
+ "model": "generic_views.book",
+ "pk": 1,
+ "fields": {
+ "name": "2066",
+ "slug": "2066",
+ "pages": "800",
+ "authors": [1],
+ "pubdate": "2008-10-01"
+ }
+ },
+ {
+ "model": "generic_views.book",
+ "pk": 2,
+ "fields": {
+ "name": "Dreaming in Code",
+ "slug": "dreaming-in-code",
+ "pages": "300",
+ "pubdate": "2006-05-01"
+ }
+ },
+ {
+ "model": "generic_views.page",
+ "pk": 1,
+ "fields": {
+ "template": "generic_views/page_template.html",
+ "content": "I was once bitten by a moose."
+ }
+ }
+] \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/forms.py b/tests/regressiontests/generic_views/forms.py
new file mode 100644
index 0000000000..7200947781
--- /dev/null
+++ b/tests/regressiontests/generic_views/forms.py
@@ -0,0 +1,11 @@
+from django import forms
+
+from regressiontests.generic_views.models import Author
+
+
+class AuthorForm(forms.ModelForm):
+ name = forms.CharField()
+ slug = forms.SlugField()
+
+ class Meta:
+ model = Author
diff --git a/tests/regressiontests/generic_views/list.py b/tests/regressiontests/generic_views/list.py
new file mode 100644
index 0000000000..5d62aa8fb8
--- /dev/null
+++ b/tests/regressiontests/generic_views/list.py
@@ -0,0 +1,129 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.test import TestCase
+
+from regressiontests.generic_views.models import Author
+
+
+class ListViewTests(TestCase):
+ fixtures = ['generic-views-test-data.json']
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_items(self):
+ res = self.client.get('/list/dict/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/list.html')
+ self.assertEqual(res.context['object_list'][0]['first'], 'John')
+
+ def test_queryset(self):
+ res = self.client.get('/list/authors/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
+ self.assertEqual(list(res.context['authors']), list(Author.objects.all()))
+ self.assertEqual(res.context['paginator'], None)
+ self.assertEqual(res.context['page_obj'], None)
+ self.assertEqual(res.context['is_paginated'], False)
+
+ def test_paginated_queryset(self):
+ self._make_authors(100)
+ res = self.client.get('/list/authors/paginated/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ self.assertEqual(len(res.context['authors']), 30)
+ self.assertEqual(res.context['is_paginated'], True)
+ self.assertEqual(res.context['page_obj'].number, 1)
+ self.assertEqual(res.context['paginator'].num_pages, 4)
+ self.assertEqual(res.context['authors'][0].name, 'Author 00')
+ self.assertEqual(list(res.context['authors'])[-1].name, 'Author 29')
+
+ def test_paginated_queryset_shortdata(self):
+ res = self.client.get('/list/authors/paginated/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
+ self.assertEqual(list(res.context['authors']), list(Author.objects.all()))
+ self.assertEqual(res.context['paginator'], None)
+ self.assertEqual(res.context['page_obj'], None)
+ self.assertEqual(res.context['is_paginated'], False)
+
+ def test_paginated_get_page_by_query_string(self):
+ self._make_authors(100)
+ res = self.client.get('/list/authors/paginated/', {'page': '2'})
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ self.assertEqual(len(res.context['authors']), 30)
+ self.assertEqual(res.context['authors'][0].name, 'Author 30')
+ self.assertEqual(res.context['page_obj'].number, 2)
+
+ def test_paginated_get_last_page_by_query_string(self):
+ self._make_authors(100)
+ res = self.client.get('/list/authors/paginated/', {'page': 'last'})
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(len(res.context['authors']), 10)
+ self.assertEqual(res.context['authors'][0].name, 'Author 90')
+ self.assertEqual(res.context['page_obj'].number, 4)
+
+ def test_paginated_get_page_by_urlvar(self):
+ self._make_authors(100)
+ res = self.client.get('/list/authors/paginated/3/')
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ self.assertEqual(len(res.context['authors']), 30)
+ self.assertEqual(res.context['authors'][0].name, 'Author 60')
+ self.assertEqual(res.context['page_obj'].number, 3)
+
+ def test_paginated_page_out_of_range(self):
+ self._make_authors(100)
+ res = self.client.get('/list/authors/paginated/42/')
+ self.assertEqual(res.status_code, 404)
+
+ def test_paginated_invalid_page(self):
+ self._make_authors(100)
+ res = self.client.get('/list/authors/paginated/?page=frog')
+ self.assertEqual(res.status_code, 404)
+
+ def test_allow_empty_false(self):
+ res = self.client.get('/list/authors/notempty/')
+ self.assertEqual(res.status_code, 200)
+ Author.objects.all().delete()
+ res = self.client.get('/list/authors/notempty/')
+ self.assertEqual(res.status_code, 404)
+
+ def test_template_name(self):
+ res = self.client.get('/list/authors/template_name/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
+ self.assertEqual(list(res.context['authors']), list(Author.objects.all()))
+ self.assertTemplateUsed(res, 'generic_views/list.html')
+
+ def test_template_name_suffix(self):
+ res = self.client.get('/list/authors/template_name_suffix/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
+ self.assertEqual(list(res.context['authors']), list(Author.objects.all()))
+ self.assertTemplateUsed(res, 'generic_views/author_objects.html')
+
+ def test_context_object_name(self):
+ res = self.client.get('/list/authors/context_object_name/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
+ self.assertEqual(list(res.context['author_list']), list(Author.objects.all()))
+ self.assertFalse('authors' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+
+ def test_duplicate_context_object_name(self):
+ res = self.client.get('/list/authors/dupe_context_object_name/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(list(res.context['object_list']), list(Author.objects.all()))
+ self.assertFalse('author_list' in res.context)
+ self.assertFalse('authors' in res.context)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+
+ def test_missing_items(self):
+ self.assertRaises(ImproperlyConfigured, self.client.get, '/list/authors/invalid/')
+
+ def _make_authors(self, n):
+ Author.objects.all().delete()
+ for i in range(n):
+ Author.objects.create(name='Author %02i' % i, slug='a%s' % i)
+
diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py
new file mode 100644
index 0000000000..5a8577d0f6
--- /dev/null
+++ b/tests/regressiontests/generic_views/models.py
@@ -0,0 +1,41 @@
+from django.db import models
+
+class Artist(models.Model):
+ name = models.CharField(max_length=100)
+
+ class Meta:
+ ordering = ['name']
+
+ def __unicode__(self):
+ return self.name
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('artist_detail', (), {'pk': self.id})
+
+class Author(models.Model):
+ name = models.CharField(max_length=100)
+ slug = models.SlugField()
+
+ class Meta:
+ ordering = ['name']
+
+ def __unicode__(self):
+ return self.name
+
+class Book(models.Model):
+ name = models.CharField(max_length=300)
+ slug = models.SlugField()
+ pages = models.IntegerField()
+ authors = models.ManyToManyField(Author)
+ pubdate = models.DateField()
+
+ class Meta:
+ ordering = ['-pubdate']
+
+ def __unicode__(self):
+ return self.name
+
+class Page(models.Model):
+ content = models.TextField()
+ template = models.CharField(max_length=300)
diff --git a/tests/regressiontests/generic_views/templates/generic_views/about.html b/tests/regressiontests/generic_views/templates/generic_views/about.html
new file mode 100644
index 0000000000..7d877fdd78
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/about.html
@@ -0,0 +1 @@
+<h1>About</h1> \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/apple_detail.html b/tests/regressiontests/generic_views/templates/generic_views/apple_detail.html
new file mode 100644
index 0000000000..4fd0bd933b
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/apple_detail.html
@@ -0,0 +1 @@
+This is a {% if tasty %}tasty {% endif %}{{ apple.color }} apple{{ extra }} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/artist_detail.html b/tests/regressiontests/generic_views/templates/generic_views/artist_detail.html
new file mode 100644
index 0000000000..9ea13fed83
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/artist_detail.html
@@ -0,0 +1 @@
+This is an {{ artist }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/artist_form.html b/tests/regressiontests/generic_views/templates/generic_views/artist_form.html
new file mode 100644
index 0000000000..114e9bc5cc
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/artist_form.html
@@ -0,0 +1 @@
+A form: {{ form }} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/author_confirm_delete.html b/tests/regressiontests/generic_views/templates/generic_views/author_confirm_delete.html
new file mode 100644
index 0000000000..d6ed99846e
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/author_confirm_delete.html
@@ -0,0 +1 @@
+Are you sure? \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/author_detail.html b/tests/regressiontests/generic_views/templates/generic_views/author_detail.html
new file mode 100644
index 0000000000..b6a60fd492
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/author_detail.html
@@ -0,0 +1 @@
+This is an {{ author }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/author_form.html b/tests/regressiontests/generic_views/templates/generic_views/author_form.html
new file mode 100644
index 0000000000..114e9bc5cc
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/author_form.html
@@ -0,0 +1 @@
+A form: {{ form }} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/author_list.html b/tests/regressiontests/generic_views/templates/generic_views/author_list.html
new file mode 100644
index 0000000000..42ba331802
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/author_list.html
@@ -0,0 +1,3 @@
+{% for item in object_list %}
+ {{ item }}
+{% endfor %} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/author_objects.html b/tests/regressiontests/generic_views/templates/generic_views/author_objects.html
new file mode 100644
index 0000000000..42ba331802
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/author_objects.html
@@ -0,0 +1,3 @@
+{% for item in object_list %}
+ {{ item }}
+{% endfor %} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/author_view.html b/tests/regressiontests/generic_views/templates/generic_views/author_view.html
new file mode 100644
index 0000000000..dcd9d6b3a6
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/author_view.html
@@ -0,0 +1 @@
+This is an alternate template_name_suffix for an {{ author }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_archive.html b/tests/regressiontests/generic_views/templates/generic_views/book_archive.html
new file mode 100644
index 0000000000..5fc11149bd
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/book_archive.html
@@ -0,0 +1 @@
+Archive of books from {{ date_list }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_archive_day.html b/tests/regressiontests/generic_views/templates/generic_views/book_archive_day.html
new file mode 100644
index 0000000000..4a7b5027b5
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/book_archive_day.html
@@ -0,0 +1 @@
+Archive for {{ day }}. Previous day is {{ previous_day }} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_archive_month.html b/tests/regressiontests/generic_views/templates/generic_views/book_archive_month.html
new file mode 100644
index 0000000000..4e1e52b35c
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/book_archive_month.html
@@ -0,0 +1 @@
+Books in {{ month }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_archive_week.html b/tests/regressiontests/generic_views/templates/generic_views/book_archive_week.html
new file mode 100644
index 0000000000..0ddcbc98a9
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/book_archive_week.html
@@ -0,0 +1 @@
+Archive for {{ week }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html b/tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html
new file mode 100644
index 0000000000..7705914e97
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html
@@ -0,0 +1 @@
+Archive of books from {{ year }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_detail.html b/tests/regressiontests/generic_views/templates/generic_views/book_detail.html
new file mode 100644
index 0000000000..c40af70426
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/book_detail.html
@@ -0,0 +1 @@
+This is {{ book }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_list.html b/tests/regressiontests/generic_views/templates/generic_views/book_list.html
new file mode 100644
index 0000000000..42ba331802
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/book_list.html
@@ -0,0 +1,3 @@
+{% for item in object_list %}
+ {{ item }}
+{% endfor %} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/confirm_delete.html b/tests/regressiontests/generic_views/templates/generic_views/confirm_delete.html
new file mode 100644
index 0000000000..87288c4495
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/confirm_delete.html
@@ -0,0 +1 @@
+Generic: Are you sure? \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/detail.html b/tests/regressiontests/generic_views/templates/generic_views/detail.html
new file mode 100644
index 0000000000..1b6b9d946b
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/detail.html
@@ -0,0 +1 @@
+Look, an {{ object }}. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/form.html b/tests/regressiontests/generic_views/templates/generic_views/form.html
new file mode 100644
index 0000000000..7249998643
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/form.html
@@ -0,0 +1 @@
+A generic form: {{ form }} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/list.html b/tests/regressiontests/generic_views/templates/generic_views/list.html
new file mode 100644
index 0000000000..42ba331802
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/list.html
@@ -0,0 +1,3 @@
+{% for item in object_list %}
+ {{ item }}
+{% endfor %} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/generic_views/page_template.html b/tests/regressiontests/generic_views/templates/generic_views/page_template.html
new file mode 100644
index 0000000000..88e20c8b50
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/generic_views/page_template.html
@@ -0,0 +1 @@
+This is some content: {{ content }} \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/templates/registration/login.html b/tests/regressiontests/generic_views/templates/registration/login.html
new file mode 100644
index 0000000000..cac78221e2
--- /dev/null
+++ b/tests/regressiontests/generic_views/templates/registration/login.html
@@ -0,0 +1 @@
+An empty login template. \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/tests.py b/tests/regressiontests/generic_views/tests.py
new file mode 100644
index 0000000000..40eec72b25
--- /dev/null
+++ b/tests/regressiontests/generic_views/tests.py
@@ -0,0 +1,5 @@
+from regressiontests.generic_views.base import ViewTest, TemplateViewTest, RedirectViewTest
+from regressiontests.generic_views.dates import ArchiveIndexViewTests, YearArchiveViewTests, MonthArchiveViewTests, WeekArchiveViewTests, DayArchiveViewTests, DateDetailViewTests
+from regressiontests.generic_views.detail import DetailViewTest
+from regressiontests.generic_views.edit import CreateViewTests, UpdateViewTests, DeleteViewTests
+from regressiontests.generic_views.list import ListViewTests \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/urls.py b/tests/regressiontests/generic_views/urls.py
new file mode 100644
index 0000000000..548d10d164
--- /dev/null
+++ b/tests/regressiontests/generic_views/urls.py
@@ -0,0 +1,186 @@
+from django.conf.urls.defaults import *
+from django.views.generic import TemplateView
+
+import views
+
+
+urlpatterns = patterns('',
+ # base
+ #(r'^about/login-required/$',
+ # views.DecoratedAboutView()),
+
+ # TemplateView
+ (r'^template/simple/(?P<foo>\w+)/$',
+ TemplateView.as_view(template_name='generic_views/about.html')),
+ (r'^template/custom/(?P<foo>\w+)/$',
+ views.CustomTemplateView.as_view(template_name='generic_views/about.html')),
+
+ # DetailView
+ (r'^detail/obj/$',
+ views.ObjectDetail.as_view()),
+ url(r'^detail/artist/(?P<pk>\d+)/$',
+ views.ArtistDetail.as_view(),
+ name="artist_detail"),
+ url(r'^detail/author/(?P<pk>\d+)/$',
+ views.AuthorDetail.as_view(),
+ name="author_detail"),
+ (r'^detail/author/byslug/(?P<slug>[\w-]+)/$',
+ views.AuthorDetail.as_view()),
+ (r'^detail/author/(?P<pk>\d+)/template_name_suffix/$',
+ views.AuthorDetail.as_view(template_name_suffix='_view')),
+ (r'^detail/author/(?P<pk>\d+)/template_name/$',
+ views.AuthorDetail.as_view(template_name='generic_views/about.html')),
+ (r'^detail/author/(?P<pk>\d+)/context_object_name/$',
+ views.AuthorDetail.as_view(context_object_name='thingy')),
+ (r'^detail/author/(?P<pk>\d+)/dupe_context_object_name/$',
+ views.AuthorDetail.as_view(context_object_name='object')),
+ (r'^detail/page/(?P<pk>\d+)/field/$',
+ views.PageDetail.as_view()),
+ (r'^detail/author/invalid/url/$',
+ views.AuthorDetail.as_view()),
+ (r'^detail/author/invalid/qs/$',
+ views.AuthorDetail.as_view(queryset=None)),
+
+ # Create/UpdateView
+ (r'^edit/artists/create/$',
+ views.ArtistCreate.as_view()),
+ (r'^edit/artists/(?P<pk>\d+)/update/$',
+ views.ArtistUpdate.as_view()),
+
+ (r'^edit/authors/create/naive/$',
+ views.NaiveAuthorCreate.as_view()),
+ (r'^edit/authors/create/redirect/$',
+ views.NaiveAuthorCreate.as_view(success_url='/edit/authors/create/')),
+ (r'^edit/authors/create/restricted/$',
+ views.AuthorCreateRestricted.as_view()),
+ (r'^edit/authors/create/$',
+ views.AuthorCreate.as_view()),
+ (r'^edit/authors/create/special/$',
+ views.SpecializedAuthorCreate.as_view()),
+
+ (r'^edit/author/(?P<pk>\d+)/update/naive/$',
+ views.NaiveAuthorUpdate.as_view()),
+ (r'^edit/author/(?P<pk>\d+)/update/redirect/$',
+ views.NaiveAuthorUpdate.as_view(success_url='/edit/authors/create/')),
+ (r'^edit/author/(?P<pk>\d+)/update/$',
+ views.AuthorUpdate.as_view()),
+ (r'^edit/author/(?P<pk>\d+)/update/special/$',
+ views.SpecializedAuthorUpdate.as_view()),
+ (r'^edit/author/(?P<pk>\d+)/delete/naive/$',
+ views.NaiveAuthorDelete.as_view()),
+ (r'^edit/author/(?P<pk>\d+)/delete/redirect/$',
+ views.NaiveAuthorDelete.as_view(success_url='/edit/authors/create/')),
+ (r'^edit/author/(?P<pk>\d+)/delete/$',
+ views.AuthorDelete.as_view()),
+ (r'^edit/author/(?P<pk>\d+)/delete/special/$',
+ views.SpecializedAuthorDelete.as_view()),
+
+ # ArchiveIndexView
+ (r'^dates/books/$',
+ views.BookArchive.as_view()),
+ (r'^dates/books/context_object_name/$',
+ views.BookArchive.as_view(context_object_name='thingies')),
+ (r'^dates/books/allow_empty/$',
+ views.BookArchive.as_view(allow_empty=True)),
+ (r'^dates/books/template_name/$',
+ views.BookArchive.as_view(template_name='generic_views/list.html')),
+ (r'^dates/books/template_name_suffix/$',
+ views.BookArchive.as_view(template_name_suffix='_detail')),
+ (r'^dates/books/invalid/$',
+ views.BookArchive.as_view(queryset=None)),
+ (r'^dates/books/paginated/$',
+ views.BookArchive.as_view(paginate_by=10)),
+
+ # ListView
+ (r'^list/dict/$',
+ views.DictList.as_view()),
+ url(r'^list/authors/$',
+ views.AuthorList.as_view(),
+ name="authors_list"),
+ (r'^list/authors/paginated/$',
+ views.AuthorList.as_view(paginate_by=30)),
+ (r'^list/authors/paginated/(?P<page>\d+)/$',
+ views.AuthorList.as_view(paginate_by=30)),
+ (r'^list/authors/notempty/$',
+ views.AuthorList.as_view(allow_empty=False)),
+ (r'^list/authors/template_name/$',
+ views.AuthorList.as_view(template_name='generic_views/list.html')),
+ (r'^list/authors/template_name_suffix/$',
+ views.AuthorList.as_view(template_name_suffix='_objects')),
+ (r'^list/authors/context_object_name/$',
+ views.AuthorList.as_view(context_object_name='author_list')),
+ (r'^list/authors/dupe_context_object_name/$',
+ views.AuthorList.as_view(context_object_name='object_list')),
+ (r'^list/authors/invalid/$',
+ views.AuthorList.as_view(queryset=None)),
+
+ # YearArchiveView
+ # Mixing keyword and possitional captures below is intentional; the views
+ # ought to be able to accept either.
+ (r'^dates/books/(?P<year>\d{4})/$',
+ views.BookYearArchive.as_view()),
+ (r'^dates/books/(?P<year>\d{4})/make_object_list/$',
+ views.BookYearArchive.as_view(make_object_list=True)),
+ (r'^dates/books/(?P<year>\d{4})/allow_empty/$',
+ views.BookYearArchive.as_view(allow_empty=True)),
+ (r'^dates/books/(?P<year>\d{4})/allow_future/$',
+ views.BookYearArchive.as_view(allow_future=True)),
+ (r'^dates/books/no_year/$',
+ views.BookYearArchive.as_view()),
+
+ # MonthArchiveView
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/$',
+ views.BookMonthArchive.as_view()),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>\d{1,2})/$',
+ views.BookMonthArchive.as_view(month_format='%m')),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/allow_empty/$',
+ views.BookMonthArchive.as_view(allow_empty=True)),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/allow_future/$',
+ views.BookMonthArchive.as_view(allow_future=True)),
+ (r'^dates/books/(?P<year>\d{4})/no_month/$',
+ views.BookMonthArchive.as_view()),
+
+ # WeekArchiveView
+ (r'^dates/books/(?P<year>\d{4})/week/(?P<week>\d{1,2})/$',
+ views.BookWeekArchive.as_view()),
+ (r'^dates/books/(?P<year>\d{4})/week/(?P<week>\d{1,2})/allow_empty/$',
+ views.BookWeekArchive.as_view(allow_empty=True)),
+ (r'^dates/books/(?P<year>\d{4})/week/(?P<week>\d{1,2})/allow_future/$',
+ views.BookWeekArchive.as_view(allow_future=True)),
+ (r'^dates/books/(?P<year>\d{4})/week/no_week/$',
+ views.BookWeekArchive.as_view()),
+
+ # DayArchiveView
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/$',
+ views.BookDayArchive.as_view()),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$',
+ views.BookDayArchive.as_view(month_format='%m')),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/allow_empty/$',
+ views.BookDayArchive.as_view(allow_empty=True)),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/allow_future/$',
+ views.BookDayArchive.as_view(allow_future=True)),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/no_day/$',
+ views.BookDayArchive.as_view()),
+
+ # TodayArchiveView
+ (r'dates/books/today/$',
+ views.BookTodayArchive.as_view()),
+ (r'dates/books/today/allow_empty/$',
+ views.BookTodayArchive.as_view(allow_empty=True)),
+
+ # DateDetailView
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/(?P<pk>\d+)/$',
+ views.BookDetail.as_view()),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<pk>\d+)/$',
+ views.BookDetail.as_view(month_format='%m')),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/(?P<pk>\d+)/allow_future/$',
+ views.BookDetail.as_view(allow_future=True)),
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/nopk/$',
+ views.BookDetail.as_view()),
+
+ (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/byslug/(?P<slug>[\w-]+)/$',
+ views.BookDetail.as_view()),
+
+ # Useful for testing redirects
+ (r'^accounts/login/$', 'django.contrib.auth.views.login')
+) \ No newline at end of file
diff --git a/tests/regressiontests/generic_views/views.py b/tests/regressiontests/generic_views/views.py
new file mode 100644
index 0000000000..eacfb717e2
--- /dev/null
+++ b/tests/regressiontests/generic_views/views.py
@@ -0,0 +1,145 @@
+from django.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.utils.decorators import method_decorator
+from django.views import generic
+
+from regressiontests.generic_views.models import Artist, Author, Book, Page
+from regressiontests.generic_views.forms import AuthorForm
+
+
+class CustomTemplateView(generic.TemplateView):
+ template_name = 'generic_views/about.html'
+
+ def get_context_data(self, **kwargs):
+ return {
+ 'params': kwargs,
+ 'key': 'value'
+ }
+
+
+class ObjectDetail(generic.DetailView):
+ template_name = 'generic_views/detail.html'
+
+ def get_object(self):
+ return {'foo': 'bar'}
+
+
+class ArtistDetail(generic.DetailView):
+ queryset = Artist.objects.all()
+
+
+class AuthorDetail(generic.DetailView):
+ queryset = Author.objects.all()
+
+
+class PageDetail(generic.DetailView):
+ queryset = Page.objects.all()
+ template_name_field = 'template'
+
+
+class DictList(generic.ListView):
+ """A ListView that doesn't use a model."""
+ queryset = [
+ {'first': 'John', 'last': 'Lennon'},
+ {'last': 'Yoko', 'last': 'Ono'}
+ ]
+ template_name = 'generic_views/list.html'
+
+
+class AuthorList(generic.ListView):
+ queryset = Author.objects.all()
+
+
+
+class ArtistCreate(generic.CreateView):
+ model = Artist
+
+
+class NaiveAuthorCreate(generic.CreateView):
+ queryset = Author.objects.all()
+
+
+class AuthorCreate(generic.CreateView):
+ model = Author
+ success_url = '/list/authors/'
+
+
+class SpecializedAuthorCreate(generic.CreateView):
+ model = Author
+ form_class = AuthorForm
+ template_name = 'generic_views/form.html'
+ context_object_name = 'thingy'
+
+ def get_success_url(self):
+ return reverse('author_detail', args=[self.object.id,])
+
+
+class AuthorCreateRestricted(AuthorCreate):
+ post = method_decorator(login_required)(AuthorCreate.post)
+
+
+class ArtistUpdate(generic.UpdateView):
+ model = Artist
+
+
+class NaiveAuthorUpdate(generic.UpdateView):
+ queryset = Author.objects.all()
+
+
+class AuthorUpdate(generic.UpdateView):
+ model = Author
+ success_url = '/list/authors/'
+
+
+class SpecializedAuthorUpdate(generic.UpdateView):
+ model = Author
+ form_class = AuthorForm
+ template_name = 'generic_views/form.html'
+ context_object_name = 'thingy'
+
+ def get_success_url(self):
+ return reverse('author_detail', args=[self.object.id,])
+
+
+class NaiveAuthorDelete(generic.DeleteView):
+ queryset = Author.objects.all()
+
+
+class AuthorDelete(generic.DeleteView):
+ model = Author
+ success_url = '/list/authors/'
+
+
+class SpecializedAuthorDelete(generic.DeleteView):
+ queryset = Author.objects.all()
+ template_name = 'generic_views/confirm_delete.html'
+ context_object_name = 'thingy'
+
+ def get_success_url(self):
+ return reverse('authors_list')
+
+
+class BookConfig(object):
+ queryset = Book.objects.all()
+ date_field = 'pubdate'
+
+class BookArchive(BookConfig, generic.ArchiveIndexView):
+ pass
+
+class BookYearArchive(BookConfig, generic.YearArchiveView):
+ pass
+
+class BookMonthArchive(BookConfig, generic.MonthArchiveView):
+ pass
+
+class BookWeekArchive(BookConfig, generic.WeekArchiveView):
+ pass
+
+class BookDayArchive(BookConfig, generic.DayArchiveView):
+ pass
+
+class BookTodayArchive(BookConfig, generic.TodayArchiveView):
+ pass
+
+class BookDetail(BookConfig, generic.DateDetailView):
+ pass