diff --git a/django/views/generic/__init__.py b/django/views/generic/__init__.py index 95c5fa9c50b..818454b367c 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 00000000000..d9d24a39de5 --- /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 76575f9f891..8cf29eed4b4 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 726df59654e..9c5bb270c62 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 00000000000..fe40b15580d --- /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 00000000000..55b8e6055e5 --- /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 /_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 00000000000..d2774936914 --- /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 00000000000..d01bacc9562 --- /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 1e39c4d9805..b876098a975 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 7c38f076884..2e2c4b638ff 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 b743176a84f..e456d047ec6 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -103,8 +103,8 @@ The view layer :doc:`Custom storage ` * **Generic views:** - :doc:`Overview` | - :doc:`Built-in generic views` + :doc:`Overview` | + :doc:`Built-in generic views` * **Advanced:** :doc:`Generating CSV ` | @@ -189,6 +189,8 @@ Other batteries included * :doc:`Unicode in Django ` * :doc:`Web design helpers ` * :doc:`Validators ` + * Function-based generic views (Deprecated) :doc:`Overview` | :doc:`Built-in generic views` | :doc:`Migration guide` + The Django open-source project ============================== diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index c227f9ab5cd..c1341e03fa4 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 dfbd82df554..95685462911 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\d+)/$', 'django.views.generic.list_detail.object_detail', info_dict), - url(r'^(?P\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\d+)/$', + DetailView.as_view( + models=Poll, + template_name='polls/detail.html')), + url(r'^(?P\d+)/results/$', + DetailView.as_view( + models=Poll, + template_name='polls/results.html'), + 'poll_results'), (r'^(?P\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 ` 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 + ` 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 ``/_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 ``/_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 ``/_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 ``/_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 - `. - -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 00000000000..4f800e1fd98 --- /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 ` and + :doc:`detailed reference `. + +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 +`. + +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) + * ``/.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[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 +`. + +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) + * ``/.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 64d0d68739b..c09cbca164c 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 -`. +`. 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 09194178afc..7b59589e74f 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 d681c4dbd3b..8f722f6cbc6 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` +for more details. There is also a document to help you :doc:`convert +your function-based generic views to class-based +views`. + 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 `. +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 00000000000..1f5421ab250 --- /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 ` and + :doc:`detailed reference `. + +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 %} +

Publishers

+ + {% 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` 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` 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` 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\\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 00000000000..3ac42044139 --- /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` +that existed in Django 1.2 have analogs as :doc:`class-based generic +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 c9c2f2d0336..1f680a16511 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 00000000000..e69de29bb2d diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py new file mode 100644 index 00000000000..a1da98690bd --- /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, '

About

') + + 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 00000000000..c730c763b95 --- /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 00000000000..91cacf65bac --- /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 00000000000..87233ec357f --- /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(), ['']) + + 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(), ['']) + + 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(), ['']) + + 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(), ['']) + + 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(), ['']) + + @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(), ['']) + + 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(), ['']) + + 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(), ['']) + + 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(), ['']) + + 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(), ['']) + + 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 00000000000..8ecfe5e1e2d --- /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 00000000000..72009477815 --- /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 00000000000..5d62aa8fb8f --- /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 00000000000..5a8577d0f64 --- /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 00000000000..7d877fdd78f --- /dev/null +++ b/tests/regressiontests/generic_views/templates/generic_views/about.html @@ -0,0 +1 @@ +

About

\ 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 00000000000..4fd0bd933b9 --- /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 00000000000..9ea13fed83a --- /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 00000000000..114e9bc5ccb --- /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 00000000000..d6ed99846e9 --- /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 00000000000..b6a60fd4922 --- /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 00000000000..114e9bc5ccb --- /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 00000000000..42ba3318026 --- /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 00000000000..42ba3318026 --- /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 00000000000..dcd9d6b3a64 --- /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 00000000000..5fc11149bd9 --- /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 00000000000..4a7b5027b59 --- /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 00000000000..4e1e52b35cf --- /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 00000000000..0ddcbc98a96 --- /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 00000000000..7705914e97e --- /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 00000000000..c40af704264 --- /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 00000000000..42ba3318026 --- /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 00000000000..87288c4495d --- /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 00000000000..1b6b9d946bd --- /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 00000000000..7249998643f --- /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 00000000000..42ba3318026 --- /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 00000000000..88e20c8b504 --- /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 00000000000..cac78221e2c --- /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 00000000000..40eec72b251 --- /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 00000000000..548d10d1641 --- /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\w+)/$', + TemplateView.as_view(template_name='generic_views/about.html')), + (r'^template/custom/(?P\w+)/$', + views.CustomTemplateView.as_view(template_name='generic_views/about.html')), + + # DetailView + (r'^detail/obj/$', + views.ObjectDetail.as_view()), + url(r'^detail/artist/(?P\d+)/$', + views.ArtistDetail.as_view(), + name="artist_detail"), + url(r'^detail/author/(?P\d+)/$', + views.AuthorDetail.as_view(), + name="author_detail"), + (r'^detail/author/byslug/(?P[\w-]+)/$', + views.AuthorDetail.as_view()), + (r'^detail/author/(?P\d+)/template_name_suffix/$', + views.AuthorDetail.as_view(template_name_suffix='_view')), + (r'^detail/author/(?P\d+)/template_name/$', + views.AuthorDetail.as_view(template_name='generic_views/about.html')), + (r'^detail/author/(?P\d+)/context_object_name/$', + views.AuthorDetail.as_view(context_object_name='thingy')), + (r'^detail/author/(?P\d+)/dupe_context_object_name/$', + views.AuthorDetail.as_view(context_object_name='object')), + (r'^detail/page/(?P\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\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\d+)/update/naive/$', + views.NaiveAuthorUpdate.as_view()), + (r'^edit/author/(?P\d+)/update/redirect/$', + views.NaiveAuthorUpdate.as_view(success_url='/edit/authors/create/')), + (r'^edit/author/(?P\d+)/update/$', + views.AuthorUpdate.as_view()), + (r'^edit/author/(?P\d+)/update/special/$', + views.SpecializedAuthorUpdate.as_view()), + (r'^edit/author/(?P\d+)/delete/naive/$', + views.NaiveAuthorDelete.as_view()), + (r'^edit/author/(?P\d+)/delete/redirect/$', + views.NaiveAuthorDelete.as_view(success_url='/edit/authors/create/')), + (r'^edit/author/(?P\d+)/delete/$', + views.AuthorDelete.as_view()), + (r'^edit/author/(?P\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\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\d{4})/$', + views.BookYearArchive.as_view()), + (r'^dates/books/(?P\d{4})/make_object_list/$', + views.BookYearArchive.as_view(make_object_list=True)), + (r'^dates/books/(?P\d{4})/allow_empty/$', + views.BookYearArchive.as_view(allow_empty=True)), + (r'^dates/books/(?P\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\d{4})/(?P[a-z]{3})/$', + views.BookMonthArchive.as_view()), + (r'^dates/books/(?P\d{4})/(?P\d{1,2})/$', + views.BookMonthArchive.as_view(month_format='%m')), + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/allow_empty/$', + views.BookMonthArchive.as_view(allow_empty=True)), + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/allow_future/$', + views.BookMonthArchive.as_view(allow_future=True)), + (r'^dates/books/(?P\d{4})/no_month/$', + views.BookMonthArchive.as_view()), + + # WeekArchiveView + (r'^dates/books/(?P\d{4})/week/(?P\d{1,2})/$', + views.BookWeekArchive.as_view()), + (r'^dates/books/(?P\d{4})/week/(?P\d{1,2})/allow_empty/$', + views.BookWeekArchive.as_view(allow_empty=True)), + (r'^dates/books/(?P\d{4})/week/(?P\d{1,2})/allow_future/$', + views.BookWeekArchive.as_view(allow_future=True)), + (r'^dates/books/(?P\d{4})/week/no_week/$', + views.BookWeekArchive.as_view()), + + # DayArchiveView + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/$', + views.BookDayArchive.as_view()), + (r'^dates/books/(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/$', + views.BookDayArchive.as_view(month_format='%m')), + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/allow_empty/$', + views.BookDayArchive.as_view(allow_empty=True)), + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/allow_future/$', + views.BookDayArchive.as_view(allow_future=True)), + (r'^dates/books/(?P\d{4})/(?P[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\d{4})/(?P[a-z]{3})/(?P\d{1,2})/(?P\d+)/$', + views.BookDetail.as_view()), + (r'^dates/books/(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/(?P\d+)/$', + views.BookDetail.as_view(month_format='%m')), + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/(?P\d+)/allow_future/$', + views.BookDetail.as_view(allow_future=True)), + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/nopk/$', + views.BookDetail.as_view()), + + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/byslug/(?P[\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 00000000000..eacfb717e2d --- /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