docker setup
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 | ||||
| @ -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,294 @@ | ||||
| import warnings | ||||
|  | ||||
| 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.") | ||||
|  | ||||
|  | ||||
| # RemovedInDjango50Warning. | ||||
| class DeleteViewCustomDeleteWarning(Warning): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| 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 __init__(self, *args, **kwargs): | ||||
|         # RemovedInDjango50Warning. | ||||
|         if self.__class__.delete is not DeletionMixin.delete: | ||||
|             warnings.warn( | ||||
|                 f"DeleteView uses FormMixin to handle POST requests. As a " | ||||
|                 f"consequence, any custom deletion logic in " | ||||
|                 f"{self.__class__.__name__}.delete() handler should be moved " | ||||
|                 f"to form_valid().", | ||||
|                 DeleteViewCustomDeleteWarning, | ||||
|                 stacklevel=2, | ||||
|             ) | ||||
|         super().__init__(*args, **kwargs) | ||||
|  | ||||
|     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