first comit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
from django.views.generic.base import RedirectView, TemplateView, View
|
||||
from django.views.generic.dates import (
|
||||
ArchiveIndexView,
|
||||
DateDetailView,
|
||||
DayArchiveView,
|
||||
MonthArchiveView,
|
||||
TodayArchiveView,
|
||||
WeekArchiveView,
|
||||
YearArchiveView,
|
||||
)
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||
from django.views.generic.list import ListView
|
||||
|
||||
__all__ = [
|
||||
"View",
|
||||
"TemplateView",
|
||||
"RedirectView",
|
||||
"ArchiveIndexView",
|
||||
"YearArchiveView",
|
||||
"MonthArchiveView",
|
||||
"WeekArchiveView",
|
||||
"DayArchiveView",
|
||||
"TodayArchiveView",
|
||||
"DateDetailView",
|
||||
"DetailView",
|
||||
"FormView",
|
||||
"CreateView",
|
||||
"UpdateView",
|
||||
"DeleteView",
|
||||
"ListView",
|
||||
"GenericViewError",
|
||||
]
|
||||
|
||||
|
||||
class GenericViewError(Exception):
|
||||
"""A problem in a generic view."""
|
||||
|
||||
pass
|
||||
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,285 @@
|
||||
import logging
|
||||
|
||||
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import (
|
||||
HttpResponse,
|
||||
HttpResponseGone,
|
||||
HttpResponseNotAllowed,
|
||||
HttpResponsePermanentRedirect,
|
||||
HttpResponseRedirect,
|
||||
)
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import classonlymethod
|
||||
from django.utils.functional import classproperty
|
||||
|
||||
logger = logging.getLogger("django.request")
|
||||
|
||||
|
||||
class ContextMixin:
|
||||
"""
|
||||
A default context mixin that passes the keyword arguments received by
|
||||
get_context_data() as the template context.
|
||||
"""
|
||||
|
||||
extra_context = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs.setdefault("view", self)
|
||||
if self.extra_context is not None:
|
||||
kwargs.update(self.extra_context)
|
||||
return kwargs
|
||||
|
||||
|
||||
class View:
|
||||
"""
|
||||
Intentionally simple parent class for all views. Only implements
|
||||
dispatch-by-method and simple sanity checking.
|
||||
"""
|
||||
|
||||
http_method_names = [
|
||||
"get",
|
||||
"post",
|
||||
"put",
|
||||
"patch",
|
||||
"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.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@classproperty
|
||||
def view_is_async(cls):
|
||||
handlers = [
|
||||
getattr(cls, method)
|
||||
for method in cls.http_method_names
|
||||
if (method != "options" and hasattr(cls, method))
|
||||
]
|
||||
if not handlers:
|
||||
return False
|
||||
is_async = iscoroutinefunction(handlers[0])
|
||||
if not all(iscoroutinefunction(h) == is_async for h in handlers[1:]):
|
||||
raise ImproperlyConfigured(
|
||||
f"{cls.__qualname__} HTTP handlers must either be all sync or all "
|
||||
"async."
|
||||
)
|
||||
return is_async
|
||||
|
||||
@classonlymethod
|
||||
def as_view(cls, **initkwargs):
|
||||
"""Main entry point for a request-response process."""
|
||||
for key in initkwargs:
|
||||
if key in cls.http_method_names:
|
||||
raise TypeError(
|
||||
"The method name %s is not accepted as a keyword argument "
|
||||
"to %s()." % (key, cls.__name__)
|
||||
)
|
||||
if not hasattr(cls, key):
|
||||
raise TypeError(
|
||||
"%s() received an invalid keyword %r. as_view "
|
||||
"only accepts arguments that are already "
|
||||
"attributes of the class." % (cls.__name__, key)
|
||||
)
|
||||
|
||||
def view(request, *args, **kwargs):
|
||||
self = cls(**initkwargs)
|
||||
self.setup(request, *args, **kwargs)
|
||||
if not hasattr(self, "request"):
|
||||
raise AttributeError(
|
||||
"%s instance has no 'request' attribute. Did you override "
|
||||
"setup() and forget to call super()?" % cls.__name__
|
||||
)
|
||||
return self.dispatch(request, *args, **kwargs)
|
||||
|
||||
view.view_class = cls
|
||||
view.view_initkwargs = initkwargs
|
||||
|
||||
# __name__ and __qualname__ are intentionally left unchanged as
|
||||
# view_class should be used to robustly determine the name of the view
|
||||
# instead.
|
||||
view.__doc__ = cls.__doc__
|
||||
view.__module__ = cls.__module__
|
||||
view.__annotations__ = cls.dispatch.__annotations__
|
||||
# Copy possible attributes set by decorators, e.g. @csrf_exempt, from
|
||||
# the dispatch method.
|
||||
view.__dict__.update(cls.dispatch.__dict__)
|
||||
|
||||
# Mark the callback if the view class is async.
|
||||
if cls.view_is_async:
|
||||
markcoroutinefunction(view)
|
||||
|
||||
return view
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
"""Initialize attributes shared by all view methods."""
|
||||
if hasattr(self, "get") and not hasattr(self, "head"):
|
||||
self.head = self.get
|
||||
self.request = request
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# Try to dispatch to the right method; if a method 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
|
||||
return handler(request, *args, **kwargs)
|
||||
|
||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||
logger.warning(
|
||||
"Method Not Allowed (%s): %s",
|
||||
request.method,
|
||||
request.path,
|
||||
extra={"status_code": 405, "request": request},
|
||||
)
|
||||
response = HttpResponseNotAllowed(self._allowed_methods())
|
||||
|
||||
if self.view_is_async:
|
||||
|
||||
async def func():
|
||||
return response
|
||||
|
||||
return func()
|
||||
else:
|
||||
return response
|
||||
|
||||
def options(self, request, *args, **kwargs):
|
||||
"""Handle responding to requests for the OPTIONS HTTP verb."""
|
||||
response = HttpResponse()
|
||||
response.headers["Allow"] = ", ".join(self._allowed_methods())
|
||||
response.headers["Content-Length"] = "0"
|
||||
|
||||
if self.view_is_async:
|
||||
|
||||
async def func():
|
||||
return response
|
||||
|
||||
return func()
|
||||
else:
|
||||
return response
|
||||
|
||||
def _allowed_methods(self):
|
||||
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
|
||||
|
||||
|
||||
class TemplateResponseMixin:
|
||||
"""A mixin that can be used to render a template."""
|
||||
|
||||
template_name = None
|
||||
template_engine = None
|
||||
response_class = TemplateResponse
|
||||
content_type = None
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
"""
|
||||
Return a response, using the `response_class` for this view, with a
|
||||
template rendered with the given context.
|
||||
|
||||
Pass response_kwargs to the constructor of the response class.
|
||||
"""
|
||||
response_kwargs.setdefault("content_type", self.content_type)
|
||||
return self.response_class(
|
||||
request=self.request,
|
||||
template=self.get_template_names(),
|
||||
context=context,
|
||||
using=self.template_engine,
|
||||
**response_kwargs,
|
||||
)
|
||||
|
||||
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 render_to_response() is overridden.
|
||||
"""
|
||||
if self.template_name is None:
|
||||
raise ImproperlyConfigured(
|
||||
"TemplateResponseMixin requires either a definition of "
|
||||
"'template_name' or an implementation of 'get_template_names()'"
|
||||
)
|
||||
else:
|
||||
return [self.template_name]
|
||||
|
||||
|
||||
class TemplateView(TemplateResponseMixin, ContextMixin, View):
|
||||
"""
|
||||
Render a template. Pass keyword arguments from the URLconf to the context.
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = self.get_context_data(**kwargs)
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
class RedirectView(View):
|
||||
"""Provide a redirect on any GET request."""
|
||||
|
||||
permanent = False
|
||||
url = None
|
||||
pattern_name = None
|
||||
query_string = False
|
||||
|
||||
def get_redirect_url(self, *args, **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:
|
||||
url = self.url % kwargs
|
||||
elif self.pattern_name:
|
||||
url = reverse(self.pattern_name, args=args, kwargs=kwargs)
|
||||
else:
|
||||
return None
|
||||
|
||||
args = self.request.META.get("QUERY_STRING", "")
|
||||
if args and self.query_string:
|
||||
url = "%s?%s" % (url, args)
|
||||
return url
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
url = self.get_redirect_url(*args, **kwargs)
|
||||
if url:
|
||||
if self.permanent:
|
||||
return HttpResponsePermanentRedirect(url)
|
||||
else:
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
logger.warning(
|
||||
"Gone: %s", request.path, extra={"status_code": 410, "request": request}
|
||||
)
|
||||
return HttpResponseGone()
|
||||
|
||||
def head(self, request, *args, **kwargs):
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def options(self, request, *args, **kwargs):
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def put(self, request, *args, **kwargs):
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
return self.get(request, *args, **kwargs)
|
||||
@@ -0,0 +1,795 @@
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.http import Http404
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext as _
|
||||
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:
|
||||
"""Mixin for views manipulating year-based data."""
|
||||
|
||||
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
|
||||
|
||||
def get_next_year(self, date):
|
||||
"""Get the next valid year."""
|
||||
return _get_next_prev(self, date, is_previous=False, period="year")
|
||||
|
||||
def get_previous_year(self, date):
|
||||
"""Get the previous valid year."""
|
||||
return _get_next_prev(self, date, is_previous=True, period="year")
|
||||
|
||||
def _get_next_year(self, date):
|
||||
"""
|
||||
Return the start date of the next interval.
|
||||
|
||||
The interval is defined by start date <= item date < next start date.
|
||||
"""
|
||||
try:
|
||||
return date.replace(year=date.year + 1, month=1, day=1)
|
||||
except ValueError:
|
||||
raise Http404(_("Date out of range"))
|
||||
|
||||
def _get_current_year(self, date):
|
||||
"""Return the start date of the current interval."""
|
||||
return date.replace(month=1, day=1)
|
||||
|
||||
|
||||
class MonthMixin:
|
||||
"""Mixin for views manipulating month-based data."""
|
||||
|
||||
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."""
|
||||
return _get_next_prev(self, date, is_previous=False, period="month")
|
||||
|
||||
def get_previous_month(self, date):
|
||||
"""Get the previous valid month."""
|
||||
return _get_next_prev(self, date, is_previous=True, period="month")
|
||||
|
||||
def _get_next_month(self, date):
|
||||
"""
|
||||
Return the start date of the next interval.
|
||||
|
||||
The interval is defined by start date <= item date < next start date.
|
||||
"""
|
||||
if date.month == 12:
|
||||
try:
|
||||
return date.replace(year=date.year + 1, month=1, day=1)
|
||||
except ValueError:
|
||||
raise Http404(_("Date out of range"))
|
||||
else:
|
||||
return date.replace(month=date.month + 1, day=1)
|
||||
|
||||
def _get_current_month(self, date):
|
||||
"""Return the start date of the previous interval."""
|
||||
return date.replace(day=1)
|
||||
|
||||
|
||||
class DayMixin:
|
||||
"""Mixin for views manipulating day-based data."""
|
||||
|
||||
day_format = "%d"
|
||||
day = None
|
||||
|
||||
def get_day_format(self):
|
||||
"""
|
||||
Get a day format string in strptime syntax to be used to parse the day
|
||||
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."""
|
||||
return _get_next_prev(self, date, is_previous=False, period="day")
|
||||
|
||||
def get_previous_day(self, date):
|
||||
"""Get the previous valid day."""
|
||||
return _get_next_prev(self, date, is_previous=True, period="day")
|
||||
|
||||
def _get_next_day(self, date):
|
||||
"""
|
||||
Return the start date of the next interval.
|
||||
|
||||
The interval is defined by start date <= item date < next start date.
|
||||
"""
|
||||
return date + datetime.timedelta(days=1)
|
||||
|
||||
def _get_current_day(self, date):
|
||||
"""Return the start date of the current interval."""
|
||||
return date
|
||||
|
||||
|
||||
class WeekMixin:
|
||||
"""Mixin for views manipulating week-based data."""
|
||||
|
||||
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
|
||||
|
||||
def get_next_week(self, date):
|
||||
"""Get the next valid week."""
|
||||
return _get_next_prev(self, date, is_previous=False, period="week")
|
||||
|
||||
def get_previous_week(self, date):
|
||||
"""Get the previous valid week."""
|
||||
return _get_next_prev(self, date, is_previous=True, period="week")
|
||||
|
||||
def _get_next_week(self, date):
|
||||
"""
|
||||
Return the start date of the next interval.
|
||||
|
||||
The interval is defined by start date <= item date < next start date.
|
||||
"""
|
||||
try:
|
||||
return date + datetime.timedelta(days=7 - self._get_weekday(date))
|
||||
except OverflowError:
|
||||
raise Http404(_("Date out of range"))
|
||||
|
||||
def _get_current_week(self, date):
|
||||
"""Return the start date of the current interval."""
|
||||
return date - datetime.timedelta(self._get_weekday(date))
|
||||
|
||||
def _get_weekday(self, date):
|
||||
"""
|
||||
Return the weekday for a given date.
|
||||
|
||||
The first day according to the week format is 0 and the last day is 6.
|
||||
"""
|
||||
week_format = self.get_week_format()
|
||||
if week_format in {"%W", "%V"}: # week starts on Monday
|
||||
return date.weekday()
|
||||
elif week_format == "%U": # week starts on Sunday
|
||||
return (date.weekday() + 1) % 7
|
||||
else:
|
||||
raise ValueError("unknown week format: %s" % week_format)
|
||||
|
||||
|
||||
class DateMixin:
|
||||
"""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(
|
||||
"%s.date_field is required." % self.__class__.__name__
|
||||
)
|
||||
return self.date_field
|
||||
|
||||
def get_allow_future(self):
|
||||
"""
|
||||
Return `True` if the view should be allowed to display objects from
|
||||
the future.
|
||||
"""
|
||||
return self.allow_future
|
||||
|
||||
# Note: the following three methods only work in subclasses that also
|
||||
# inherit SingleObjectMixin or MultipleObjectMixin.
|
||||
|
||||
@cached_property
|
||||
def uses_datetime_field(self):
|
||||
"""
|
||||
Return `True` if the date field is a `DateTimeField` and `False`
|
||||
if it's a `DateField`.
|
||||
"""
|
||||
model = self.get_queryset().model if self.model is None else self.model
|
||||
field = model._meta.get_field(self.get_date_field())
|
||||
return isinstance(field, models.DateTimeField)
|
||||
|
||||
def _make_date_lookup_arg(self, value):
|
||||
"""
|
||||
Convert a date into a datetime when the date field is a DateTimeField.
|
||||
|
||||
When time zone support is enabled, `date` is assumed to be in the
|
||||
current time zone, so that displayed items are consistent with the URL.
|
||||
"""
|
||||
if self.uses_datetime_field:
|
||||
value = datetime.datetime.combine(value, datetime.time.min)
|
||||
if settings.USE_TZ:
|
||||
value = timezone.make_aware(value)
|
||||
return value
|
||||
|
||||
def _make_single_date_lookup(self, date):
|
||||
"""
|
||||
Get the lookup kwargs for filtering on a single date.
|
||||
|
||||
If the date field is a DateTimeField, we can't just filter on
|
||||
date_field=date because that doesn't take the time into account.
|
||||
"""
|
||||
date_field = self.get_date_field()
|
||||
if self.uses_datetime_field:
|
||||
since = self._make_date_lookup_arg(date)
|
||||
until = self._make_date_lookup_arg(date + datetime.timedelta(days=1))
|
||||
return {
|
||||
"%s__gte" % date_field: since,
|
||||
"%s__lt" % date_field: until,
|
||||
}
|
||||
else:
|
||||
# Skip self._make_date_lookup_arg, it's a no-op in this branch.
|
||||
return {date_field: date}
|
||||
|
||||
|
||||
class BaseDateListView(MultipleObjectMixin, DateMixin, View):
|
||||
"""Abstract base class for date-based views displaying a list of objects."""
|
||||
|
||||
allow_empty = False
|
||||
date_list_period = "year"
|
||||
|
||||
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, **extra_context
|
||||
)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_dated_items(self):
|
||||
"""Obtain the list of dates and items."""
|
||||
raise NotImplementedError(
|
||||
"A DateView must provide an implementation of get_dated_items()"
|
||||
)
|
||||
|
||||
def get_ordering(self):
|
||||
"""
|
||||
Return the field or fields to use for ordering the queryset; use the
|
||||
date field by default.
|
||||
"""
|
||||
return "-%s" % self.get_date_field() if self.ordering is None else self.ordering
|
||||
|
||||
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()
|
||||
paginate_by = self.get_paginate_by(qs)
|
||||
|
||||
if not allow_future:
|
||||
now = timezone.now() if self.uses_datetime_field else timezone_today()
|
||||
qs = qs.filter(**{"%s__lte" % date_field: now})
|
||||
|
||||
if not allow_empty:
|
||||
# When pagination is enabled, it's better to do a cheap query
|
||||
# than to load the unpaginated queryset in memory.
|
||||
is_empty = not qs if paginate_by is None else not qs.exists()
|
||||
if is_empty:
|
||||
raise Http404(
|
||||
_("No %(verbose_name_plural)s available")
|
||||
% {
|
||||
"verbose_name_plural": qs.model._meta.verbose_name_plural,
|
||||
}
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
def get_date_list_period(self):
|
||||
"""
|
||||
Get the aggregation period for the list of dates: 'year', 'month', or
|
||||
'day'.
|
||||
"""
|
||||
return self.date_list_period
|
||||
|
||||
def get_date_list(self, queryset, date_type=None, ordering="ASC"):
|
||||
"""
|
||||
Get a date list by calling `queryset.dates/datetimes()`, checking
|
||||
along the way for empty lists that aren't allowed.
|
||||
"""
|
||||
date_field = self.get_date_field()
|
||||
allow_empty = self.get_allow_empty()
|
||||
if date_type is None:
|
||||
date_type = self.get_date_list_period()
|
||||
|
||||
if self.uses_datetime_field:
|
||||
date_list = queryset.datetimes(date_field, date_type, ordering)
|
||||
else:
|
||||
date_list = queryset.dates(date_field, date_type, ordering)
|
||||
if date_list is not None and not date_list and not allow_empty:
|
||||
raise Http404(
|
||||
_("No %(verbose_name_plural)s available")
|
||||
% {
|
||||
"verbose_name_plural": queryset.model._meta.verbose_name_plural,
|
||||
}
|
||||
)
|
||||
|
||||
return date_list
|
||||
|
||||
|
||||
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, ordering="DESC")
|
||||
|
||||
if not date_list:
|
||||
qs = qs.none()
|
||||
|
||||
return (date_list, qs, {})
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
date_list_period = "month"
|
||||
make_object_list = False
|
||||
|
||||
def get_dated_items(self):
|
||||
"""Return (date_list, items, extra_context) for this request."""
|
||||
year = self.get_year()
|
||||
|
||||
date_field = self.get_date_field()
|
||||
date = _date_from_string(year, self.get_year_format())
|
||||
|
||||
since = self._make_date_lookup_arg(date)
|
||||
until = self._make_date_lookup_arg(self._get_next_year(date))
|
||||
lookup_kwargs = {
|
||||
"%s__gte" % date_field: since,
|
||||
"%s__lt" % date_field: until,
|
||||
}
|
||||
|
||||
qs = self.get_dated_queryset(**lookup_kwargs)
|
||||
date_list = self.get_date_list(qs)
|
||||
|
||||
if not self.get_make_object_list():
|
||||
# We need this to be a queryset since parent classes introspect it
|
||||
# to find information about the model.
|
||||
qs = qs.none()
|
||||
|
||||
return (
|
||||
date_list,
|
||||
qs,
|
||||
{
|
||||
"year": date,
|
||||
"next_year": self.get_next_year(date),
|
||||
"previous_year": self.get_previous_year(date),
|
||||
},
|
||||
)
|
||||
|
||||
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 month."""
|
||||
|
||||
date_list_period = "day"
|
||||
|
||||
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()
|
||||
)
|
||||
|
||||
since = self._make_date_lookup_arg(date)
|
||||
until = self._make_date_lookup_arg(self._get_next_month(date))
|
||||
lookup_kwargs = {
|
||||
"%s__gte" % date_field: since,
|
||||
"%s__lt" % date_field: until,
|
||||
}
|
||||
|
||||
qs = self.get_dated_queryset(**lookup_kwargs)
|
||||
date_list = self.get_date_list(qs)
|
||||
|
||||
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 month."""
|
||||
|
||||
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()
|
||||
week_format = self.get_week_format()
|
||||
week_choices = {"%W": "1", "%U": "0", "%V": "1"}
|
||||
try:
|
||||
week_start = week_choices[week_format]
|
||||
except KeyError:
|
||||
raise ValueError(
|
||||
"Unknown week format %r. Choices are: %s"
|
||||
% (
|
||||
week_format,
|
||||
", ".join(sorted(week_choices)),
|
||||
)
|
||||
)
|
||||
year_format = self.get_year_format()
|
||||
if week_format == "%V" and year_format != "%G":
|
||||
raise ValueError(
|
||||
"ISO week directive '%s' is incompatible with the year "
|
||||
"directive '%s'. Use the ISO year '%%G' instead."
|
||||
% (
|
||||
week_format,
|
||||
year_format,
|
||||
)
|
||||
)
|
||||
date = _date_from_string(year, year_format, week_start, "%w", week, week_format)
|
||||
since = self._make_date_lookup_arg(date)
|
||||
until = self._make_date_lookup_arg(self._get_next_week(date))
|
||||
lookup_kwargs = {
|
||||
"%s__gte" % date_field: since,
|
||||
"%s__lt" % date_field: until,
|
||||
}
|
||||
|
||||
qs = self.get_dated_queryset(**lookup_kwargs)
|
||||
|
||||
return (
|
||||
None,
|
||||
qs,
|
||||
{
|
||||
"week": date,
|
||||
"next_week": self.get_next_week(date),
|
||||
"previous_week": self.get_previous_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.
|
||||
"""
|
||||
lookup_kwargs = self._make_single_date_lookup(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):
|
||||
"""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(),
|
||||
)
|
||||
|
||||
# Use a custom queryset if provided
|
||||
qs = self.get_queryset() if queryset is None else queryset
|
||||
|
||||
if not self.get_allow_future() and date > datetime.date.today():
|
||||
raise Http404(
|
||||
_(
|
||||
"Future %(verbose_name_plural)s not available because "
|
||||
"%(class_name)s.allow_future is False."
|
||||
)
|
||||
% {
|
||||
"verbose_name_plural": qs.model._meta.verbose_name_plural,
|
||||
"class_name": 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
|
||||
lookup_kwargs = self._make_single_date_lookup(date)
|
||||
qs = qs.filter(**lookup_kwargs)
|
||||
|
||||
return super().get_object(queryset=qs)
|
||||
|
||||
|
||||
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="__"
|
||||
):
|
||||
"""
|
||||
Get a datetime.date object given a format string and a year, month, and day
|
||||
(only year is mandatory). Raise a 404 for an invalid date.
|
||||
"""
|
||||
format = year_format + delim + month_format + delim + day_format
|
||||
datestr = str(year) + delim + str(month) + delim + str(day)
|
||||
try:
|
||||
return datetime.datetime.strptime(datestr, format).date()
|
||||
except ValueError:
|
||||
raise Http404(
|
||||
_("Invalid date string “%(datestr)s” given format “%(format)s”")
|
||||
% {
|
||||
"datestr": datestr,
|
||||
"format": format,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_next_prev(generic_view, date, is_previous, period):
|
||||
"""
|
||||
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 different intervals of time,
|
||||
hence the coupling to generic_view.
|
||||
|
||||
However in essence 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/week/month,
|
||||
regardless of object existence.)
|
||||
|
||||
* If allow_empty is true, allow_future is false, and the naive result
|
||||
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()
|
||||
|
||||
get_current = getattr(generic_view, "_get_current_%s" % period)
|
||||
get_next = getattr(generic_view, "_get_next_%s" % period)
|
||||
|
||||
# Bounds of the current interval
|
||||
start, end = get_current(date), get_next(date)
|
||||
|
||||
# If allow_empty is True, the naive result will be valid
|
||||
if allow_empty:
|
||||
if is_previous:
|
||||
result = get_current(start - datetime.timedelta(days=1))
|
||||
else:
|
||||
result = end
|
||||
|
||||
if allow_future or result <= timezone_today():
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
|
||||
# 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 whether we're doing
|
||||
# a previous date or a next date lookup.
|
||||
if is_previous:
|
||||
lookup = {"%s__lt" % date_field: generic_view._make_date_lookup_arg(start)}
|
||||
ordering = "-%s" % date_field
|
||||
else:
|
||||
lookup = {"%s__gte" % date_field: generic_view._make_date_lookup_arg(end)}
|
||||
ordering = date_field
|
||||
|
||||
# Filter out objects in the future if appropriate.
|
||||
if not allow_future:
|
||||
# Fortunately, to match the implementation of allow_future,
|
||||
# we need __lte, which doesn't conflict with __lt above.
|
||||
if generic_view.uses_datetime_field:
|
||||
now = timezone.now()
|
||||
else:
|
||||
now = timezone_today()
|
||||
lookup["%s__lte" % date_field] = now
|
||||
|
||||
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:
|
||||
return None
|
||||
|
||||
# Convert datetimes to dates in the current time zone.
|
||||
if generic_view.uses_datetime_field:
|
||||
if settings.USE_TZ:
|
||||
result = timezone.localtime(result)
|
||||
result = result.date()
|
||||
|
||||
# Return the first day of the period.
|
||||
return get_current(result)
|
||||
|
||||
|
||||
def timezone_today():
|
||||
"""Return the current date in the current time zone."""
|
||||
if settings.USE_TZ:
|
||||
return timezone.localdate()
|
||||
else:
|
||||
return datetime.date.today()
|
||||
@@ -0,0 +1,180 @@
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
|
||||
|
||||
|
||||
class SingleObjectMixin(ContextMixin):
|
||||
"""
|
||||
Provide the ability to retrieve a single object for further manipulation.
|
||||
"""
|
||||
|
||||
model = None
|
||||
queryset = None
|
||||
slug_field = "slug"
|
||||
context_object_name = None
|
||||
slug_url_kwarg = "slug"
|
||||
pk_url_kwarg = "pk"
|
||||
query_pk_and_slug = False
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
"""
|
||||
Return the object the view is displaying.
|
||||
|
||||
Require `self.queryset` and a `pk` or `slug` argument in the URLconf.
|
||||
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.
|
||||
pk = self.kwargs.get(self.pk_url_kwarg)
|
||||
slug = self.kwargs.get(self.slug_url_kwarg)
|
||||
if pk is not None:
|
||||
queryset = queryset.filter(pk=pk)
|
||||
|
||||
# Next, try looking up by slug.
|
||||
if slug is not None and (pk is None or self.query_pk_and_slug):
|
||||
slug_field = self.get_slug_field()
|
||||
queryset = queryset.filter(**{slug_field: slug})
|
||||
|
||||
# If none of those are defined, it's an error.
|
||||
if pk is None and slug is None:
|
||||
raise AttributeError(
|
||||
"Generic detail view %s must be called with either an object "
|
||||
"pk or a slug in the URLconf." % self.__class__.__name__
|
||||
)
|
||||
|
||||
try:
|
||||
# Get the single item from the filtered queryset
|
||||
obj = queryset.get()
|
||||
except queryset.model.DoesNotExist:
|
||||
raise Http404(
|
||||
_("No %(verbose_name)s found matching the query")
|
||||
% {"verbose_name": queryset.model._meta.verbose_name}
|
||||
)
|
||||
return obj
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Return the `QuerySet` that will be used to look up the object.
|
||||
|
||||
This method is called by the default implementation of get_object() and
|
||||
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(
|
||||
"%(cls)s is missing a QuerySet. Define "
|
||||
"%(cls)s.model, %(cls)s.queryset, or override "
|
||||
"%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
|
||||
)
|
||||
return self.queryset.all()
|
||||
|
||||
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 isinstance(obj, models.Model):
|
||||
return obj._meta.model_name
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Insert the single object into the context dict."""
|
||||
context = {}
|
||||
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
|
||||
context.update(kwargs)
|
||||
return super().get_context_data(**context)
|
||||
|
||||
|
||||
class BaseDetailView(SingleObjectMixin, View):
|
||||
"""A base view for displaying a single object."""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
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. May not be
|
||||
called if render_to_response() is overridden. Return the following list:
|
||||
|
||||
* the value of ``template_name`` on the view (if provided)
|
||||
* the contents of the ``template_name_field`` field on the
|
||||
object instance that the view is operating upon (if available)
|
||||
* ``<app_label>/<model_name><template_name_suffix>.html``
|
||||
"""
|
||||
try:
|
||||
names = super().get_template_names()
|
||||
except ImproperlyConfigured:
|
||||
# If template_name isn't specified, it's not a problem --
|
||||
# we just start with an empty list.
|
||||
names = []
|
||||
|
||||
# If self.template_name_field is set, grab the value of the field
|
||||
# of that name from the object; this is the most specific template
|
||||
# name, if given.
|
||||
if self.object and self.template_name_field:
|
||||
name = getattr(self.object, self.template_name_field, None)
|
||||
if name:
|
||||
names.insert(0, name)
|
||||
|
||||
# The least-specific option is the default <app>/<model>_detail.html;
|
||||
# only use this if the object in question is a model.
|
||||
if isinstance(self.object, models.Model):
|
||||
object_meta = self.object._meta
|
||||
names.append(
|
||||
"%s/%s%s.html"
|
||||
% (
|
||||
object_meta.app_label,
|
||||
object_meta.model_name,
|
||||
self.template_name_suffix,
|
||||
)
|
||||
)
|
||||
elif getattr(self, "model", None) is not None and issubclass(
|
||||
self.model, models.Model
|
||||
):
|
||||
names.append(
|
||||
"%s/%s%s.html"
|
||||
% (
|
||||
self.model._meta.app_label,
|
||||
self.model._meta.model_name,
|
||||
self.template_name_suffix,
|
||||
)
|
||||
)
|
||||
|
||||
# If we still haven't managed to find any template names, we should
|
||||
# re-raise the ImproperlyConfigured to alert the user.
|
||||
if not names:
|
||||
raise
|
||||
|
||||
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()`.
|
||||
"""
|
||||
@@ -0,0 +1,274 @@
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.forms import Form
|
||||
from django.forms import models as model_forms
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
|
||||
from django.views.generic.detail import (
|
||||
BaseDetailView,
|
||||
SingleObjectMixin,
|
||||
SingleObjectTemplateResponseMixin,
|
||||
)
|
||||
|
||||
|
||||
class FormMixin(ContextMixin):
|
||||
"""Provide a way to show and handle a form in a request."""
|
||||
|
||||
initial = {}
|
||||
form_class = None
|
||||
success_url = None
|
||||
prefix = None
|
||||
|
||||
def get_initial(self):
|
||||
"""Return the initial data to use for forms on this view."""
|
||||
return self.initial.copy()
|
||||
|
||||
def get_prefix(self):
|
||||
"""Return the prefix to use for forms."""
|
||||
return self.prefix
|
||||
|
||||
def get_form_class(self):
|
||||
"""Return the form class to use."""
|
||||
return self.form_class
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
"""Return an instance of the form to be used in this view."""
|
||||
if form_class is None:
|
||||
form_class = self.get_form_class()
|
||||
return form_class(**self.get_form_kwargs())
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Return the keyword arguments for instantiating the form."""
|
||||
kwargs = {
|
||||
"initial": self.get_initial(),
|
||||
"prefix": self.get_prefix(),
|
||||
}
|
||||
|
||||
if self.request.method in ("POST", "PUT"):
|
||||
kwargs.update(
|
||||
{
|
||||
"data": self.request.POST,
|
||||
"files": self.request.FILES,
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
"""Return the URL to redirect to after processing a valid form."""
|
||||
if not self.success_url:
|
||||
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
|
||||
return str(self.success_url) # success_url may be lazy
|
||||
|
||||
def form_valid(self, form):
|
||||
"""If the form is valid, redirect to the supplied URL."""
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def form_invalid(self, form):
|
||||
"""If the form is invalid, render the invalid form."""
|
||||
return self.render_to_response(self.get_context_data(form=form))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Insert the form into the context dict."""
|
||||
if "form" not in kwargs:
|
||||
kwargs["form"] = self.get_form()
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class ModelFormMixin(FormMixin, SingleObjectMixin):
|
||||
"""Provide a way to show and handle a ModelForm in a request."""
|
||||
|
||||
fields = None
|
||||
|
||||
def get_form_class(self):
|
||||
"""Return the form class to use in this view."""
|
||||
if self.fields is not None and self.form_class:
|
||||
raise ImproperlyConfigured(
|
||||
"Specifying both 'fields' and 'form_class' is not permitted."
|
||||
)
|
||||
if self.form_class:
|
||||
return self.form_class
|
||||
else:
|
||||
if self.model is not None:
|
||||
# If a model has been explicitly provided, use it
|
||||
model = self.model
|
||||
elif getattr(self, "object", None) is not None:
|
||||
# If this view is operating on a single object, use
|
||||
# the class of that object
|
||||
model = self.object.__class__
|
||||
else:
|
||||
# Try to get a queryset and extract the model class
|
||||
# from that
|
||||
model = self.get_queryset().model
|
||||
|
||||
if self.fields is None:
|
||||
raise ImproperlyConfigured(
|
||||
"Using ModelFormMixin (base class of %s) without "
|
||||
"the 'fields' attribute is prohibited." % self.__class__.__name__
|
||||
)
|
||||
|
||||
return model_forms.modelform_factory(model, fields=self.fields)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Return the keyword arguments for instantiating the form."""
|
||||
kwargs = super().get_form_kwargs()
|
||||
if hasattr(self, "object"):
|
||||
kwargs.update({"instance": self.object})
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
"""Return the URL to redirect to after processing a valid form."""
|
||||
if self.success_url:
|
||||
url = self.success_url.format(**self.object.__dict__)
|
||||
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):
|
||||
"""If the form is valid, save the associated model."""
|
||||
self.object = form.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ProcessFormView(View):
|
||||
"""Render a form on GET and processes it on POST."""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Handle GET requests: instantiate a blank version of the form."""
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handle POST requests: instantiate a form instance with the passed
|
||||
POST variables and then check if it's valid.
|
||||
"""
|
||||
form = self.get_form()
|
||||
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.
|
||||
def put(self, *args, **kwargs):
|
||||
return self.post(*args, **kwargs)
|
||||
|
||||
|
||||
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 a 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().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = None
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
|
||||
"""
|
||||
View for creating a new object, with a response rendered by a 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()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView):
|
||||
"""View for updating an object, with a response rendered by a template."""
|
||||
|
||||
template_name_suffix = "_form"
|
||||
|
||||
|
||||
class DeletionMixin:
|
||||
"""Provide the ability to delete objects."""
|
||||
|
||||
success_url = None
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
"""
|
||||
Call the delete() method on the fetched object and then redirect to the
|
||||
success URL.
|
||||
"""
|
||||
self.object = self.get_object()
|
||||
success_url = self.get_success_url()
|
||||
self.object.delete()
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
# Add support for browsers which only accept GET and POST for now.
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.delete(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
if self.success_url:
|
||||
return self.success_url.format(**self.object.__dict__)
|
||||
else:
|
||||
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
|
||||
|
||||
|
||||
class BaseDeleteView(DeletionMixin, FormMixin, BaseDetailView):
|
||||
"""
|
||||
Base view for deleting an object.
|
||||
|
||||
Using this base class requires subclassing to provide a response mixin.
|
||||
"""
|
||||
|
||||
form_class = Form
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# Set self.object before the usual form processing flow.
|
||||
# Inlined because having DeletionMixin as the first base, for
|
||||
# get_success_url(), makes leveraging super() with ProcessFormView
|
||||
# overly complex.
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
success_url = self.get_success_url()
|
||||
self.object.delete()
|
||||
return HttpResponseRedirect(success_url)
|
||||
|
||||
|
||||
class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
|
||||
"""
|
||||
View for deleting an object retrieved with self.get_object(), with a
|
||||
response rendered by a template.
|
||||
"""
|
||||
|
||||
template_name_suffix = "_confirm_delete"
|
||||
@@ -0,0 +1,220 @@
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.paginator import InvalidPage, Paginator
|
||||
from django.db.models import QuerySet
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
|
||||
|
||||
|
||||
class MultipleObjectMixin(ContextMixin):
|
||||
"""A mixin for views manipulating multiple objects."""
|
||||
|
||||
allow_empty = True
|
||||
queryset = None
|
||||
model = None
|
||||
paginate_by = None
|
||||
paginate_orphans = 0
|
||||
context_object_name = None
|
||||
paginator_class = Paginator
|
||||
page_kwarg = "page"
|
||||
ordering = None
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Return the list of items for this view.
|
||||
|
||||
The return value must be an iterable and may be an instance of
|
||||
`QuerySet` in which case `QuerySet` specific behavior will be enabled.
|
||||
"""
|
||||
if self.queryset is not None:
|
||||
queryset = self.queryset
|
||||
if isinstance(queryset, QuerySet):
|
||||
queryset = queryset.all()
|
||||
elif self.model is not None:
|
||||
queryset = self.model._default_manager.all()
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
"%(cls)s is missing a QuerySet. Define "
|
||||
"%(cls)s.model, %(cls)s.queryset, or override "
|
||||
"%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
|
||||
)
|
||||
ordering = self.get_ordering()
|
||||
if ordering:
|
||||
if isinstance(ordering, str):
|
||||
ordering = (ordering,)
|
||||
queryset = queryset.order_by(*ordering)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_ordering(self):
|
||||
"""Return the field or fields to use for ordering the queryset."""
|
||||
return self.ordering
|
||||
|
||||
def paginate_queryset(self, queryset, page_size):
|
||||
"""Paginate the queryset, if needed."""
|
||||
paginator = self.get_paginator(
|
||||
queryset,
|
||||
page_size,
|
||||
orphans=self.get_paginate_orphans(),
|
||||
allow_empty_first_page=self.get_allow_empty(),
|
||||
)
|
||||
page_kwarg = self.page_kwarg
|
||||
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 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, page.has_other_pages())
|
||||
except InvalidPage as e:
|
||||
raise Http404(
|
||||
_("Invalid page (%(page_number)s): %(message)s")
|
||||
% {"page_number": page_number, "message": str(e)}
|
||||
)
|
||||
|
||||
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_paginator(
|
||||
self, queryset, per_page, orphans=0, allow_empty_first_page=True, **kwargs
|
||||
):
|
||||
"""Return an instance of the paginator for this view."""
|
||||
return self.paginator_class(
|
||||
queryset,
|
||||
per_page,
|
||||
orphans=orphans,
|
||||
allow_empty_first_page=allow_empty_first_page,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def get_paginate_orphans(self):
|
||||
"""
|
||||
Return the maximum number of orphans extend the last page by when
|
||||
paginating.
|
||||
"""
|
||||
return self.paginate_orphans
|
||||
|
||||
def get_allow_empty(self):
|
||||
"""
|
||||
Return ``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 "%s_list" % object_list.model._meta.model_name
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_context_data(self, *, object_list=None, **kwargs):
|
||||
"""Get the context for this view."""
|
||||
queryset = object_list if object_list is not None else self.object_list
|
||||
page_size = self.get_paginate_by(queryset)
|
||||
context_object_name = self.get_context_object_name(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,
|
||||
}
|
||||
if context_object_name is not None:
|
||||
context[context_object_name] = queryset
|
||||
context.update(kwargs)
|
||||
return super().get_context_data(**context)
|
||||
|
||||
|
||||
class BaseListView(MultipleObjectMixin, View):
|
||||
"""A base view for displaying a list of objects."""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object_list = self.get_queryset()
|
||||
allow_empty = self.get_allow_empty()
|
||||
|
||||
if not allow_empty:
|
||||
# When pagination is enabled and object_list is a queryset,
|
||||
# it's better to do a cheap query than to load the unpaginated
|
||||
# queryset in memory.
|
||||
if self.get_paginate_by(self.object_list) is not None and hasattr(
|
||||
self.object_list, "exists"
|
||||
):
|
||||
is_empty = not self.object_list.exists()
|
||||
else:
|
||||
is_empty = not self.object_list
|
||||
if is_empty:
|
||||
raise Http404(
|
||||
_("Empty list and “%(class_name)s.allow_empty” is False.")
|
||||
% {
|
||||
"class_name": self.__class__.__name__,
|
||||
}
|
||||
)
|
||||
context = self.get_context_data()
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
class MultipleObjectTemplateResponseMixin(TemplateResponseMixin):
|
||||
"""Mixin for responding with a template and list of objects."""
|
||||
|
||||
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 render_to_response is overridden.
|
||||
"""
|
||||
try:
|
||||
names = super().get_template_names()
|
||||
except ImproperlyConfigured:
|
||||
# If template_name isn't specified, it's not a problem --
|
||||
# we just start with an empty list.
|
||||
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.model_name, self.template_name_suffix)
|
||||
)
|
||||
elif not names:
|
||||
raise ImproperlyConfigured(
|
||||
"%(cls)s requires either a 'template_name' attribute "
|
||||
"or a get_queryset() method that returns a QuerySet."
|
||||
% {
|
||||
"cls": self.__class__.__name__,
|
||||
}
|
||||
)
|
||||
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.
|
||||
"""
|
||||
Reference in New Issue
Block a user