From 86afb713144eb8ea26f872b6fd398ad5a45fcdba Mon Sep 17 00:00:00 2001 From: Simone Federici Date: Sun, 15 Jun 2014 10:30:04 +0200 Subject: [PATCH 1/4] backport the Django 1.4 generic view in common.backport.generic --- apps/common/backport/__init__.py | 1 + apps/common/backport/generic/__init__.py | 12 + apps/common/backport/generic/base.py | 178 +++++ apps/common/backport/generic/create_update.py | 221 +++++++ apps/common/backport/generic/date_based.py | 376 +++++++++++ apps/common/backport/generic/dates.py | 607 ++++++++++++++++++ apps/common/backport/generic/detail.py | 150 +++++ apps/common/backport/generic/edit.py | 242 +++++++ apps/common/backport/generic/list.py | 153 +++++ apps/common/backport/generic/list_detail.py | 152 +++++ apps/common/backport/generic/simple.py | 68 ++ apps/common/urls.py | 4 +- apps/documents/views.py | 2 +- apps/ocr/views.py | 2 +- apps/permissions/views.py | 4 +- apps/user_management/views.py | 2 +- requirements/common.txt | 2 +- settings.py | 2 +- 18 files changed, 2169 insertions(+), 9 deletions(-) create mode 100644 apps/common/backport/__init__.py create mode 100644 apps/common/backport/generic/__init__.py create mode 100644 apps/common/backport/generic/base.py create mode 100644 apps/common/backport/generic/create_update.py create mode 100644 apps/common/backport/generic/date_based.py create mode 100644 apps/common/backport/generic/dates.py create mode 100644 apps/common/backport/generic/detail.py create mode 100644 apps/common/backport/generic/edit.py create mode 100644 apps/common/backport/generic/list.py create mode 100644 apps/common/backport/generic/list_detail.py create mode 100644 apps/common/backport/generic/simple.py diff --git a/apps/common/backport/__init__.py b/apps/common/backport/__init__.py new file mode 100644 index 0000000000..9e1bb9575e --- /dev/null +++ b/apps/common/backport/__init__.py @@ -0,0 +1 @@ +__author__ = 'aldaran' diff --git a/apps/common/backport/generic/__init__.py b/apps/common/backport/generic/__init__.py new file mode 100644 index 0000000000..799ea2b36b --- /dev/null +++ b/apps/common/backport/generic/__init__.py @@ -0,0 +1,12 @@ +from .base import View, TemplateView, RedirectView +from .dates import (ArchiveIndexView, YearArchiveView, MonthArchiveView, + WeekArchiveView, DayArchiveView, TodayArchiveView, + DateDetailView) +from .detail import DetailView +from .edit import FormView, CreateView, UpdateView, DeleteView +from .list import ListView + + +class GenericViewError(Exception): + """A problem in a generic view.""" + pass diff --git a/apps/common/backport/generic/base.py b/apps/common/backport/generic/base.py new file mode 100644 index 0000000000..fcdc7c785b --- /dev/null +++ b/apps/common/backport/generic/base.py @@ -0,0 +1,178 @@ +from functools import update_wrapper +from django import http +from django.core.exceptions import ImproperlyConfigured +from django.template.response import TemplateResponse +from django.utils.log import getLogger +from django.utils.decorators import classonlymethod + +logger = getLogger('django.request') + + +class View(object): + """ + Intentionally simple parent class for all views. Only implements + dispatch-by-method and simple sanity checking. + """ + + http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + + def __init__(self, **kwargs): + """ + Constructor. Called in the URLconf; can contain helpful extra + keyword arguments, and other things. + """ + # Go through keyword arguments, and either save their values to our + # instance, or raise an error. + for key, value in kwargs.iteritems(): + setattr(self, key, value) + + @classonlymethod + def as_view(cls, **initkwargs): + """ + Main entry point for a request-response process. + """ + # sanitize keyword arguments + for key in initkwargs: + if key in cls.http_method_names: + raise TypeError(u"You tried to pass in the %s method name as a " + u"keyword argument to %s(). Don't do that." + % (key, cls.__name__)) + if not hasattr(cls, key): + raise TypeError(u"%s() received an invalid keyword %r" % ( + cls.__name__, key)) + + def view(request, *args, **kwargs): + self = cls(**initkwargs) + if hasattr(self, 'get') and not hasattr(self, 'head'): + self.head = self.get + return self.dispatch(request, *args, **kwargs) + + # take name and docstring from class + update_wrapper(view, cls, updated=()) + + # and possible attributes set by decorators + # like csrf_exempt from dispatch + update_wrapper(view, cls.dispatch, assigned=()) + return view + + def dispatch(self, request, *args, **kwargs): + # Try to dispatch to the right method; 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 + self.request = request + self.args = args + self.kwargs = kwargs + return handler(request, *args, **kwargs) + + def http_method_not_allowed(self, request, *args, **kwargs): + allowed_methods = [m for m in self.http_method_names if hasattr(self, m)] + logger.warning('Method Not Allowed (%s): %s', request.method, request.path, + extra={ + 'status_code': 405, + 'request': self.request + } + ) + return http.HttpResponseNotAllowed(allowed_methods) + + +class TemplateResponseMixin(object): + """ + A mixin that can be used to render a template. + """ + template_name = None + response_class = TemplateResponse + + def render_to_response(self, context, **response_kwargs): + """ + Returns a response with a template rendered with the given context. + """ + return self.response_class( + request = self.request, + template = self.get_template_names(), + context = context, + **response_kwargs + ) + + def get_template_names(self): + """ + Returns 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, View): + """ + A view that renders a template. + """ + def get_context_data(self, **kwargs): + return { + 'params': kwargs + } + + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return self.render_to_response(context) + + +class RedirectView(View): + """ + A view that provides a redirect on any GET request. + """ + permanent = True + url = None + query_string = False + + def get_redirect_url(self, **kwargs): + """ + Return the URL redirect to. Keyword arguments from the + URL pattern match generating the redirect request + are provided as kwargs to this method. + """ + if self.url: + url = self.url % kwargs + args = self.request.META.get('QUERY_STRING', '') + if args and self.query_string: + url = "%s?%s" % (url, args) + return url + else: + return None + + def get(self, request, *args, **kwargs): + url = self.get_redirect_url(**kwargs) + if url: + if self.permanent: + return http.HttpResponsePermanentRedirect(url) + else: + return http.HttpResponseRedirect(url) + else: + logger.warning('Gone: %s', self.request.path, + extra={ + 'status_code': 410, + 'request': self.request + }) + return http.HttpResponseGone() + + 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) diff --git a/apps/common/backport/generic/create_update.py b/apps/common/backport/generic/create_update.py new file mode 100644 index 0000000000..7ea171f42b --- /dev/null +++ b/apps/common/backport/generic/create_update.py @@ -0,0 +1,221 @@ +from django.forms.models import ModelFormMetaclass, ModelForm +from django.template import RequestContext, loader +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.core.xheaders import populate_xheaders +from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured +from django.utils.translation import ugettext +from django.contrib.auth.views import redirect_to_login +from . import GenericViewError +from django.contrib import messages + +import warnings +warnings.warn( + 'Function-based generic views have been deprecated; use class-based views instead.', + DeprecationWarning +) + + +def apply_extra_context(extra_context, context): + """ + Adds items from extra_context dict to context. If a value in extra_context + is callable, then it is called and the result is added to context. + """ + for key, value in extra_context.iteritems(): + if callable(value): + context[key] = value() + else: + context[key] = value + +def get_model_and_form_class(model, form_class): + """ + Returns a model and form class based on the model and form_class + parameters that were passed to the generic view. + + If ``form_class`` is given then its associated model will be returned along + with ``form_class`` itself. Otherwise, if ``model`` is given, ``model`` + itself will be returned along with a ``ModelForm`` class created from + ``model``. + """ + if form_class: + return form_class._meta.model, form_class + if model: + # The inner Meta class fails if model = model is used for some reason. + tmp_model = model + # TODO: we should be able to construct a ModelForm without creating + # and passing in a temporary inner class. + class Meta: + model = tmp_model + class_name = model.__name__ + 'Form' + form_class = ModelFormMetaclass(class_name, (ModelForm,), {'Meta': Meta}) + return model, form_class + raise GenericViewError("Generic view must be called with either a model or" + " form_class argument.") + +def redirect(post_save_redirect, obj): + """ + Returns a HttpResponseRedirect to ``post_save_redirect``. + + ``post_save_redirect`` should be a string, and can contain named string- + substitution place holders of ``obj`` field names. + + If ``post_save_redirect`` is None, then redirect to ``obj``'s URL returned + by ``get_absolute_url()``. If ``obj`` has no ``get_absolute_url`` method, + then raise ImproperlyConfigured. + + This function is meant to handle the post_save_redirect parameter to the + ``create_object`` and ``update_object`` views. + """ + if post_save_redirect: + return HttpResponseRedirect(post_save_redirect % obj.__dict__) + elif hasattr(obj, 'get_absolute_url'): + return HttpResponseRedirect(obj.get_absolute_url()) + else: + raise ImproperlyConfigured( + "No URL to redirect to. Either pass a post_save_redirect" + " parameter to the generic view or define a get_absolute_url" + " method on the Model.") + +def lookup_object(model, object_id, slug, slug_field): + """ + Return the ``model`` object with the passed ``object_id``. If + ``object_id`` is None, then return the object whose ``slug_field`` + equals the passed ``slug``. If ``slug`` and ``slug_field`` are not passed, + then raise Http404 exception. + """ + lookup_kwargs = {} + if object_id: + lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id + elif slug and slug_field: + lookup_kwargs['%s__exact' % slug_field] = slug + else: + raise GenericViewError( + "Generic view must be called with either an object_id or a" + " slug/slug_field.") + try: + return model.objects.get(**lookup_kwargs) + except ObjectDoesNotExist: + raise Http404("No %s found for %s" + % (model._meta.verbose_name, lookup_kwargs)) + +def create_object(request, model=None, template_name=None, + template_loader=loader, extra_context=None, post_save_redirect=None, + login_required=False, context_processors=None, form_class=None): + """ + Generic object-creation function. + + Templates: ``/_form.html`` + Context: + form + the form for the object + """ + if extra_context is None: extra_context = {} + if login_required and not request.user.is_authenticated(): + return redirect_to_login(request.path) + + model, form_class = get_model_and_form_class(model, form_class) + if request.method == 'POST': + form = form_class(request.POST, request.FILES) + if form.is_valid(): + new_object = form.save() + + msg = ugettext("The %(verbose_name)s was created successfully.") %\ + {"verbose_name": model._meta.verbose_name} + messages.success(request, msg, fail_silently=True) + return redirect(post_save_redirect, new_object) + else: + form = form_class() + + # Create the template, context, response + if not template_name: + template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + 'form': form, + }, context_processors) + apply_extra_context(extra_context, c) + return HttpResponse(t.render(c)) + +def update_object(request, model=None, object_id=None, slug=None, + slug_field='slug', template_name=None, template_loader=loader, + extra_context=None, post_save_redirect=None, login_required=False, + context_processors=None, template_object_name='object', + form_class=None): + """ + Generic object-update function. + + Templates: ``/_form.html`` + Context: + form + the form for the object + object + the original object being edited + """ + if extra_context is None: extra_context = {} + if login_required and not request.user.is_authenticated(): + return redirect_to_login(request.path) + + model, form_class = get_model_and_form_class(model, form_class) + obj = lookup_object(model, object_id, slug, slug_field) + + if request.method == 'POST': + form = form_class(request.POST, request.FILES, instance=obj) + if form.is_valid(): + obj = form.save() + msg = ugettext("The %(verbose_name)s was updated successfully.") %\ + {"verbose_name": model._meta.verbose_name} + messages.success(request, msg, fail_silently=True) + return redirect(post_save_redirect, obj) + else: + form = form_class(instance=obj) + + if not template_name: + template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + 'form': form, + template_object_name: obj, + }, context_processors) + apply_extra_context(extra_context, c) + response = HttpResponse(t.render(c)) + populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname)) + return response + +def delete_object(request, model, post_delete_redirect, object_id=None, + slug=None, slug_field='slug', template_name=None, + template_loader=loader, extra_context=None, login_required=False, + context_processors=None, template_object_name='object'): + """ + Generic object-delete function. + + The given template will be used to confirm deletetion if this view is + fetched using GET; for safty, deletion will only be performed if this + view is POSTed. + + Templates: ``/_confirm_delete.html`` + Context: + object + the original object being deleted + """ + if extra_context is None: extra_context = {} + if login_required and not request.user.is_authenticated(): + return redirect_to_login(request.path) + + obj = lookup_object(model, object_id, slug, slug_field) + + if request.method == 'POST': + obj.delete() + msg = ugettext("The %(verbose_name)s was deleted.") %\ + {"verbose_name": model._meta.verbose_name} + messages.success(request, msg, fail_silently=True) + return HttpResponseRedirect(post_delete_redirect) + else: + if not template_name: + template_name = "%s/%s_confirm_delete.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + template_object_name: obj, + }, context_processors) + apply_extra_context(extra_context, c) + response = HttpResponse(t.render(c)) + populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.attname)) + return response diff --git a/apps/common/backport/generic/date_based.py b/apps/common/backport/generic/date_based.py new file mode 100644 index 0000000000..75094aa775 --- /dev/null +++ b/apps/common/backport/generic/date_based.py @@ -0,0 +1,376 @@ +import datetime +import time + +from django.template import loader, RequestContext +from django.core.exceptions import ObjectDoesNotExist +from django.core.xheaders import populate_xheaders +from django.db.models.fields import DateTimeField +from django.http import Http404, HttpResponse +from django.utils import timezone + +import warnings +warnings.warn( + 'Function-based generic views have been deprecated; use class-based views instead.', + DeprecationWarning +) + + +def archive_index(request, queryset, date_field, num_latest=15, + template_name=None, template_loader=loader, + extra_context=None, allow_empty=True, context_processors=None, + mimetype=None, allow_future=False, template_object_name='latest'): + """ + Generic top-level archive of date-based objects. + + Templates: ``/_archive.html`` + Context: + date_list + List of years + latest + Latest N (defaults to 15) objects by date + """ + if extra_context is None: extra_context = {} + model = queryset.model + if not allow_future: + queryset = queryset.filter(**{'%s__lte' % date_field: timezone.now()}) + date_list = queryset.dates(date_field, 'year')[::-1] + if not date_list and not allow_empty: + raise Http404("No %s available" % model._meta.verbose_name) + + if date_list and num_latest: + latest = queryset.order_by('-'+date_field)[:num_latest] + else: + latest = None + + if not template_name: + template_name = "%s/%s_archive.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + 'date_list' : date_list, + template_object_name : latest, + }, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + return HttpResponse(t.render(c), mimetype=mimetype) + +def archive_year(request, year, queryset, date_field, template_name=None, + template_loader=loader, extra_context=None, allow_empty=False, + context_processors=None, template_object_name='object', mimetype=None, + make_object_list=False, allow_future=False): + """ + Generic yearly archive view. + + Templates: ``/_archive_year.html`` + Context: + date_list + List of months in this year with objects + year + This year + object_list + List of objects published in the given month + (Only available if make_object_list argument is True) + """ + if extra_context is None: extra_context = {} + model = queryset.model + now = timezone.now() + + lookup_kwargs = {'%s__year' % date_field: year} + + # Only bother to check current date if the year isn't in the past and future objects aren't requested. + if int(year) >= now.year and not allow_future: + lookup_kwargs['%s__lte' % date_field] = now + date_list = queryset.filter(**lookup_kwargs).dates(date_field, 'month') + if not date_list and not allow_empty: + raise Http404 + if make_object_list: + object_list = queryset.filter(**lookup_kwargs) + else: + object_list = [] + if not template_name: + template_name = "%s/%s_archive_year.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + 'date_list': date_list, + 'year': year, + '%s_list' % template_object_name: object_list, + }, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + return HttpResponse(t.render(c), mimetype=mimetype) + +def archive_month(request, year, month, queryset, date_field, + month_format='%b', template_name=None, template_loader=loader, + extra_context=None, allow_empty=False, context_processors=None, + template_object_name='object', mimetype=None, allow_future=False): + """ + Generic monthly archive view. + + Templates: ``/_archive_month.html`` + Context: + date_list: + List of days in this month with objects + month: + (date) this month + next_month: + (date) the first day of the next month, or None if the next month is in the future + previous_month: + (date) the first day of the previous month + object_list: + list of objects published in the given month + """ + if extra_context is None: extra_context = {} + try: + tt = time.strptime("%s-%s" % (year, month), '%s-%s' % ('%Y', month_format)) + date = datetime.date(*tt[:3]) + except ValueError: + raise Http404 + + model = queryset.model + now = timezone.now() + + # Calculate first and last day of month, for use in a date-range lookup. + first_day = date.replace(day=1) + if first_day.month == 12: + last_day = first_day.replace(year=first_day.year + 1, month=1) + else: + last_day = first_day.replace(month=first_day.month + 1) + lookup_kwargs = { + '%s__gte' % date_field: first_day, + '%s__lt' % date_field: last_day, + } + + # Only bother to check current date if the month isn't in the past and future objects are requested. + if last_day >= now.date() and not allow_future: + lookup_kwargs['%s__lte' % date_field] = now + object_list = queryset.filter(**lookup_kwargs) + date_list = object_list.dates(date_field, 'day') + if not object_list and not allow_empty: + raise Http404 + + # Calculate the next month, if applicable. + if allow_future: + next_month = last_day + elif last_day <= datetime.date.today(): + next_month = last_day + else: + next_month = None + + # Calculate the previous month + if first_day.month == 1: + previous_month = first_day.replace(year=first_day.year-1,month=12) + else: + previous_month = first_day.replace(month=first_day.month-1) + + if not template_name: + template_name = "%s/%s_archive_month.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + 'date_list': date_list, + '%s_list' % template_object_name: object_list, + 'month': date, + 'next_month': next_month, + 'previous_month': previous_month, + }, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + return HttpResponse(t.render(c), mimetype=mimetype) + +def archive_week(request, year, week, queryset, date_field, + template_name=None, template_loader=loader, + extra_context=None, allow_empty=True, context_processors=None, + template_object_name='object', mimetype=None, allow_future=False): + """ + Generic weekly archive view. + + Templates: ``/_archive_week.html`` + Context: + week: + (date) this week + object_list: + list of objects published in the given week + """ + if extra_context is None: extra_context = {} + try: + tt = time.strptime(year+'-0-'+week, '%Y-%w-%U') + date = datetime.date(*tt[:3]) + except ValueError: + raise Http404 + + model = queryset.model + now = timezone.now() + + # Calculate first and last day of week, for use in a date-range lookup. + first_day = date + last_day = date + datetime.timedelta(days=7) + lookup_kwargs = { + '%s__gte' % date_field: first_day, + '%s__lt' % date_field: last_day, + } + + # Only bother to check current date if the week isn't in the past and future objects aren't requested. + if last_day >= now.date() and not allow_future: + lookup_kwargs['%s__lte' % date_field] = now + object_list = queryset.filter(**lookup_kwargs) + if not object_list and not allow_empty: + raise Http404 + if not template_name: + template_name = "%s/%s_archive_week.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + '%s_list' % template_object_name: object_list, + 'week': date, + }) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + return HttpResponse(t.render(c), mimetype=mimetype) + +def archive_day(request, year, month, day, queryset, date_field, + month_format='%b', day_format='%d', template_name=None, + template_loader=loader, extra_context=None, allow_empty=False, + context_processors=None, template_object_name='object', + mimetype=None, allow_future=False): + """ + Generic daily archive view. + + Templates: ``/_archive_day.html`` + Context: + object_list: + list of objects published that day + day: + (datetime) the day + previous_day + (datetime) the previous day + next_day + (datetime) the next day, or None if the current day is today + """ + if extra_context is None: extra_context = {} + try: + tt = time.strptime('%s-%s-%s' % (year, month, day), + '%s-%s-%s' % ('%Y', month_format, day_format)) + date = datetime.date(*tt[:3]) + except ValueError: + raise Http404 + + model = queryset.model + now = timezone.now() + + if isinstance(model._meta.get_field(date_field), DateTimeField): + lookup_kwargs = {'%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max))} + else: + lookup_kwargs = {date_field: date} + + # Only bother to check current date if the date isn't in the past and future objects aren't requested. + if date >= now.date() and not allow_future: + lookup_kwargs['%s__lte' % date_field] = now + object_list = queryset.filter(**lookup_kwargs) + if not allow_empty and not object_list: + raise Http404 + + # Calculate the next day, if applicable. + if allow_future: + next_day = date + datetime.timedelta(days=1) + elif date < datetime.date.today(): + next_day = date + datetime.timedelta(days=1) + else: + next_day = None + + if not template_name: + template_name = "%s/%s_archive_day.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + c = RequestContext(request, { + '%s_list' % template_object_name: object_list, + 'day': date, + 'previous_day': date - datetime.timedelta(days=1), + 'next_day': next_day, + }, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + return HttpResponse(t.render(c), mimetype=mimetype) + +def archive_today(request, **kwargs): + """ + Generic daily archive view for today. Same as archive_day view. + """ + today = datetime.date.today() + kwargs.update({ + 'year': str(today.year), + 'month': today.strftime('%b').lower(), + 'day': str(today.day), + }) + return archive_day(request, **kwargs) + +def object_detail(request, year, month, day, queryset, date_field, + month_format='%b', day_format='%d', object_id=None, slug=None, + slug_field='slug', template_name=None, template_name_field=None, + template_loader=loader, extra_context=None, context_processors=None, + template_object_name='object', mimetype=None, allow_future=False): + """ + Generic detail view from year/month/day/slug or year/month/day/id structure. + + Templates: ``/_detail.html`` + Context: + object: + the object to be detailed + """ + if extra_context is None: extra_context = {} + try: + tt = time.strptime('%s-%s-%s' % (year, month, day), + '%s-%s-%s' % ('%Y', month_format, day_format)) + date = datetime.date(*tt[:3]) + except ValueError: + raise Http404 + + model = queryset.model + now = timezone.now() + + if isinstance(model._meta.get_field(date_field), DateTimeField): + lookup_kwargs = {'%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max))} + else: + lookup_kwargs = {date_field: date} + + # Only bother to check current date if the date isn't in the past and future objects aren't requested. + if date >= now.date() and not allow_future: + lookup_kwargs['%s__lte' % date_field] = now + if object_id: + lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id + elif slug and slug_field: + lookup_kwargs['%s__exact' % slug_field] = slug + else: + raise AttributeError("Generic detail view must be called with either an object_id or a slug/slugfield") + try: + obj = queryset.get(**lookup_kwargs) + except ObjectDoesNotExist: + raise Http404("No %s found for" % model._meta.verbose_name) + if not template_name: + template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower()) + if template_name_field: + template_name_list = [getattr(obj, template_name_field), template_name] + t = template_loader.select_template(template_name_list) + else: + t = template_loader.get_template(template_name) + c = RequestContext(request, { + template_object_name: obj, + }, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + response = HttpResponse(t.render(c), mimetype=mimetype) + populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name)) + return response diff --git a/apps/common/backport/generic/dates.py b/apps/common/backport/generic/dates.py new file mode 100644 index 0000000000..42036c427f --- /dev/null +++ b/apps/common/backport/generic/dates.py @@ -0,0 +1,607 @@ +import datetime +from django.db import models +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404 +from django.utils.encoding import force_unicode +from django.utils.translation import ugettext as _ +from django.utils import timezone +from .base import View +from .detail import BaseDetailView, SingleObjectTemplateResponseMixin +from .list import MultipleObjectMixin, MultipleObjectTemplateResponseMixin + +class YearMixin(object): + year_format = '%Y' + year = None + + def get_year_format(self): + """ + Get a year format string in strptime syntax to be used to parse the + year from url variables. + """ + return self.year_format + + def get_year(self): + "Return the year for which this view should display data" + year = self.year + if year is None: + try: + year = self.kwargs['year'] + except KeyError: + try: + year = self.request.GET['year'] + except KeyError: + raise Http404(_(u"No year specified")) + return year + + +class MonthMixin(object): + month_format = '%b' + month = None + + def get_month_format(self): + """ + Get a month format string in strptime syntax to be used to parse the + month from url variables. + """ + return self.month_format + + def get_month(self): + "Return the month for which this view should display data" + month = self.month + if month is None: + try: + month = self.kwargs['month'] + except KeyError: + try: + month = self.request.GET['month'] + except KeyError: + raise Http404(_(u"No month specified")) + return month + + def get_next_month(self, date): + """ + Get the next valid month. + """ + first_day, last_day = _month_bounds(date) + next = (last_day + datetime.timedelta(days=1)).replace(day=1) + return _get_next_prev_month(self, next, is_previous=False, use_first_day=True) + + def get_previous_month(self, date): + """ + Get the previous valid month. + """ + first_day, last_day = _month_bounds(date) + prev = (first_day - datetime.timedelta(days=1)) + return _get_next_prev_month(self, prev, is_previous=True, use_first_day=True) + + +class DayMixin(object): + day_format = '%d' + day = None + + def get_day_format(self): + """ + Get a 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(_(u"No day specified")) + return day + + def get_next_day(self, date): + """ + Get the next valid day. + """ + next = date + datetime.timedelta(days=1) + return _get_next_prev_month(self, next, is_previous=False, use_first_day=False) + + def get_previous_day(self, date): + """ + Get the previous valid day. + """ + prev = date - datetime.timedelta(days=1) + return _get_next_prev_month(self, prev, is_previous=True, use_first_day=False) + + +class WeekMixin(object): + week_format = '%U' + week = None + + def get_week_format(self): + """ + Get a week format string in strptime syntax to be used to parse the + week from url variables. + """ + return self.week_format + + def get_week(self): + "Return the week for which this view should display data" + week = self.week + if week is None: + try: + week = self.kwargs['week'] + except KeyError: + try: + week = self.request.GET['week'] + except KeyError: + raise Http404(_(u"No week specified")) + return week + + +class DateMixin(object): + """ + Mixin class for views manipulating date-based data. + """ + date_field = None + allow_future = False + + def get_date_field(self): + """ + Get the name of the date field to be used to filter by. + """ + if self.date_field is None: + raise ImproperlyConfigured(u"%s.date_field is required." % self.__class__.__name__) + return self.date_field + + def get_allow_future(self): + """ + Returns `True` if the view should be allowed to display objects from + the future. + """ + return self.allow_future + + +class BaseDateListView(MultipleObjectMixin, DateMixin, View): + """ + Abstract base class for date-based views display a list of objects. + """ + allow_empty = False + + def get(self, request, *args, **kwargs): + self.date_list, self.object_list, extra_context = self.get_dated_items() + context = self.get_context_data(object_list=self.object_list, + date_list=self.date_list) + context.update(extra_context) + return self.render_to_response(context) + + def get_dated_items(self): + """ + Obtain the list of dates and itesm + """ + raise NotImplementedError('A DateView must provide an implementation of get_dated_items()') + + def get_dated_queryset(self, **lookup): + """ + Get a queryset properly filtered according to `allow_future` and any + extra lookup kwargs. + """ + qs = self.get_queryset().filter(**lookup) + date_field = self.get_date_field() + allow_future = self.get_allow_future() + allow_empty = self.get_allow_empty() + + if not allow_future: + qs = qs.filter(**{'%s__lte' % date_field: timezone.now()}) + + if not allow_empty and not qs: + raise Http404(_(u"No %(verbose_name_plural)s available") % { + 'verbose_name_plural': force_unicode(qs.model._meta.verbose_name_plural) + }) + + return qs + + def get_date_list(self, queryset, date_type): + """ + Get a date list by calling `queryset.dates()`, checking along the way + for empty lists that aren't allowed. + """ + date_field = self.get_date_field() + allow_empty = self.get_allow_empty() + + date_list = queryset.dates(date_field, date_type)[::-1] + if date_list is not None and not date_list and not allow_empty: + name = force_unicode(queryset.model._meta.verbose_name_plural) + raise Http404(_(u"No %(verbose_name_plural)s available") % + {'verbose_name_plural': name}) + + return date_list + + def get_context_data(self, **kwargs): + """ + Get the context. Must return a Context (or subclass) instance. + """ + items = kwargs.pop('object_list') + context = super(BaseDateListView, self).get_context_data(object_list=items) + context.update(kwargs) + return context + + +class BaseArchiveIndexView(BaseDateListView): + """ + Base class for archives of date-based items. + + Requires a response mixin. + """ + context_object_name = 'latest' + + def get_dated_items(self): + """ + Return (date_list, items, extra_context) for this request. + """ + qs = self.get_dated_queryset() + date_list = self.get_date_list(qs, 'year') + + if date_list: + object_list = qs.order_by('-' + self.get_date_field()) + else: + object_list = qs.none() + + return (date_list, object_list, {}) + + +class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView): + """ + Top-level archive of date-based items. + """ + template_name_suffix = '_archive' + + +class BaseYearArchiveView(YearMixin, BaseDateListView): + """ + List of objects published in a given year. + """ + make_object_list = False + + def get_dated_items(self): + """ + Return (date_list, items, extra_context) for this request. + """ + # Yes, no error checking: the URLpattern ought to validate this; it's + # an error if it doesn't. + year = self.get_year() + date_field = self.get_date_field() + qs = self.get_dated_queryset(**{date_field+'__year': year}) + date_list = self.get_date_list(qs, 'month') + + if self.get_make_object_list(): + object_list = qs.order_by('-'+date_field) + else: + # We need this to be a queryset since parent classes introspect it + # to find information about the model. + object_list = qs.none() + + return (date_list, object_list, {'year': year}) + + def get_make_object_list(self): + """ + Return `True` if this view should contain the full list of objects in + the given year. + """ + return self.make_object_list + + +class YearArchiveView(MultipleObjectTemplateResponseMixin, BaseYearArchiveView): + """ + List of objects published in a given year. + """ + template_name_suffix = '_archive_year' + + +class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView): + """ + List of objects published in a given year. + """ + def get_dated_items(self): + """ + Return (date_list, items, extra_context) for this request. + """ + year = self.get_year() + month = self.get_month() + + date_field = self.get_date_field() + date = _date_from_string(year, self.get_year_format(), + month, self.get_month_format()) + + # Construct a date-range lookup. + first_day, last_day = _month_bounds(date) + lookup_kwargs = { + '%s__gte' % date_field: first_day, + '%s__lt' % date_field: last_day, + } + + qs = self.get_dated_queryset(**lookup_kwargs) + date_list = self.get_date_list(qs, 'day') + + return (date_list, qs, { + 'month': date, + 'next_month': self.get_next_month(date), + 'previous_month': self.get_previous_month(date), + }) + + +class MonthArchiveView(MultipleObjectTemplateResponseMixin, BaseMonthArchiveView): + """ + List of objects published in a given year. + """ + template_name_suffix = '_archive_month' + + +class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView): + """ + List of objects published in a given week. + """ + + def get_dated_items(self): + """ + Return (date_list, items, extra_context) for this request. + """ + year = self.get_year() + week = self.get_week() + + date_field = self.get_date_field() + week_format = self.get_week_format() + week_start = { + '%W': '1', + '%U': '0', + }[week_format] + date = _date_from_string(year, self.get_year_format(), + week_start, '%w', + week, week_format) + + # Construct a date-range lookup. + first_day = date + last_day = date + datetime.timedelta(days=7) + lookup_kwargs = { + '%s__gte' % date_field: first_day, + '%s__lt' % date_field: last_day, + } + + qs = self.get_dated_queryset(**lookup_kwargs) + + return (None, qs, {'week': date}) + + +class WeekArchiveView(MultipleObjectTemplateResponseMixin, BaseWeekArchiveView): + """ + List of objects published in a given week. + """ + template_name_suffix = '_archive_week' + + +class BaseDayArchiveView(YearMixin, MonthMixin, DayMixin, BaseDateListView): + """ + List of objects published on a given day. + """ + def get_dated_items(self): + """ + Return (date_list, items, extra_context) for this request. + """ + year = self.get_year() + month = self.get_month() + day = self.get_day() + + date = _date_from_string(year, self.get_year_format(), + month, self.get_month_format(), + day, self.get_day_format()) + + return self._get_dated_items(date) + + def _get_dated_items(self, date): + """ + Do the actual heavy lifting of getting the dated items; this accepts a + date object so that TodayArchiveView can be trivial. + """ + date_field = self.get_date_field() + + field = self.get_queryset().model._meta.get_field(date_field) + lookup_kwargs = _date_lookup_for_field(field, date) + + qs = self.get_dated_queryset(**lookup_kwargs) + + return (None, qs, { + 'day': date, + 'previous_day': self.get_previous_day(date), + 'next_day': self.get_next_day(date), + 'previous_month': self.get_previous_month(date), + 'next_month': self.get_next_month(date) + }) + + +class DayArchiveView(MultipleObjectTemplateResponseMixin, BaseDayArchiveView): + """ + List of objects published on a given day. + """ + template_name_suffix = "_archive_day" + + +class BaseTodayArchiveView(BaseDayArchiveView): + """ + List of objects published today. + """ + + def get_dated_items(self): + """ + Return (date_list, items, extra_context) for this request. + """ + return self._get_dated_items(datetime.date.today()) + + +class TodayArchiveView(MultipleObjectTemplateResponseMixin, BaseTodayArchiveView): + """ + List of objects published today. + """ + template_name_suffix = "_archive_day" + + +class BaseDateDetailView(YearMixin, MonthMixin, DayMixin, DateMixin, BaseDetailView): + """ + Detail view of a single object on a single date; this differs from the + standard DetailView by accepting a year/month/day in the URL. + """ + def get_object(self, queryset=None): + """ + 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 = queryset or self.get_queryset() + + if not self.get_allow_future() and date > datetime.date.today(): + raise Http404(_(u"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 + date_field = self.get_date_field() + field = qs.model._meta.get_field(date_field) + lookup = _date_lookup_for_field(field, date) + qs = qs.filter(**lookup) + + return super(BaseDetailView, self).get_object(queryset=qs) + + +class DateDetailView(SingleObjectTemplateResponseMixin, BaseDateDetailView): + """ + Detail view of a single object on a single date; this differs from the + standard DetailView by accepting a year/month/day in the URL. + """ + template_name_suffix = '_detail' + + +def _date_from_string(year, year_format, month, month_format, day='', day_format='', delim='__'): + """ + Helper: get a datetime.date object given a format string and a year, + month, and possibly day; raise a 404 for an invalid date. + """ + format = delim.join((year_format, month_format, day_format)) + datestr = delim.join((year, month, day)) + try: + return datetime.datetime.strptime(datestr, format).date() + except ValueError: + raise Http404(_(u"Invalid date string '%(datestr)s' given format '%(format)s'") % { + 'datestr': datestr, + 'format': format, + }) + + +def _month_bounds(date): + """ + Helper: return the first and last days of the month for the given date. + """ + first_day = date.replace(day=1) + if first_day.month == 12: + last_day = first_day.replace(year=first_day.year + 1, month=1) + else: + last_day = first_day.replace(month=first_day.month + 1) + + return first_day, last_day + + +def _get_next_prev_month(generic_view, naive_result, is_previous, use_first_day): + """ + Helper: Get the next or the previous valid date. The idea is to allow + links on month/day views to never be 404s by never providing a date + that'll be invalid for the given view. + + This is a bit complicated since it handles both next and previous months + and days (for MonthArchiveView and DayArchiveView); hence the coupling to generic_view. + + However in 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 or month, + reguardless of object existence.) + + * If allow_empty is true, allow_future is false, and the naive month + isn't in the future, then return it; otherwise return None. + + * If allow_empty is false and allow_future is true, return the next + date *that contains a valid object*, even if it's in the future. If + there are no next objects, return None. + + * If allow_empty is false and allow_future is false, return the next + date that contains a valid object. If that date is in the future, or + if there are no next objects, return None. + + """ + date_field = generic_view.get_date_field() + allow_empty = generic_view.get_allow_empty() + allow_future = generic_view.get_allow_future() + + # If allow_empty is True the naive value will be valid + if allow_empty: + result = naive_result + + # Otherwise, we'll need to go to the database to look for an object + # whose date_field is at least (greater than/less than) the given + # naive result + else: + # Construct a lookup and an ordering depending on whether we're doing + # a previous date or a next date lookup. + if is_previous: + lookup = {'%s__lte' % date_field: naive_result} + ordering = '-%s' % date_field + else: + lookup = {'%s__gte' % date_field: naive_result} + ordering = date_field + + qs = generic_view.get_queryset().filter(**lookup).order_by(ordering) + + # Snag the first object from the queryset; if it doesn't exist that + # means there's no next/previous link available. + try: + result = getattr(qs[0], date_field) + except IndexError: + result = None + + # Convert datetimes to a dates + if hasattr(result, 'date'): + result = result.date() + + # For month views, we always want to have a date that's the first of the + # month for consistency's sake. + if result and use_first_day: + result = result.replace(day=1) + + # Check against future dates. + if result and (allow_future or result < datetime.date.today()): + return result + else: + return None + + +def _date_lookup_for_field(field, date): + """ + Get the lookup kwargs for looking up a date against a given Field. If the + date field is a DateTimeField, we can't just do filter(df=date) because + that doesn't take the time into account. So we need to make a range lookup + in those cases. + """ + if isinstance(field, models.DateTimeField): + date_range = ( + datetime.datetime.combine(date, datetime.time.min), + datetime.datetime.combine(date, datetime.time.max) + ) + return {'%s__range' % field.name: date_range} + else: + return {field.name: date} diff --git a/apps/common/backport/generic/detail.py b/apps/common/backport/generic/detail.py new file mode 100644 index 0000000000..c9aa588fb9 --- /dev/null +++ b/apps/common/backport/generic/detail.py @@ -0,0 +1,150 @@ +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.http import Http404 +from django.utils.encoding import smart_str +from django.utils.translation import ugettext as _ +from .base import TemplateResponseMixin, View + + +class SingleObjectMixin(object): + """ + Provides the ability to retrieve a single object for further manipulation. + """ + model = None + queryset = None + slug_field = 'slug' + context_object_name = None + slug_url_kwarg = 'slug' + pk_url_kwarg = 'pk' + + def get_object(self, queryset=None): + """ + Returns the object the view is displaying. + + By default this requires `self.queryset` and a `pk` or `slug` argument + in the URLconf, but subclasses can override this to return any object. + """ + # Use a custom queryset if provided; this is required for subclasses + # like DateDetailView + if queryset is None: + queryset = self.get_queryset() + + # Next, try looking up by primary key. + pk = self.kwargs.get(self.pk_url_kwarg, None) + slug = self.kwargs.get(self.slug_url_kwarg, None) + if pk is not None: + queryset = queryset.filter(pk=pk) + + # Next, try looking up by slug. + elif slug is not None: + slug_field = self.get_slug_field() + queryset = queryset.filter(**{slug_field: slug}) + + # If none of those are defined, it's an error. + else: + raise AttributeError(u"Generic detail view %s must be called with " + u"either an object pk or a slug." + % self.__class__.__name__) + + try: + obj = queryset.get() + except ObjectDoesNotExist: + raise Http404(_(u"No %(verbose_name)s found matching the query") % + {'verbose_name': queryset.model._meta.verbose_name}) + return obj + + def get_queryset(self): + """ + Get the queryset to look an object up against. May not be called if + `get_object` is overridden. + """ + if self.queryset is None: + if self.model: + return self.model._default_manager.all() + else: + raise ImproperlyConfigured(u"%(cls)s is missing a queryset. Define " + u"%(cls)s.model, %(cls)s.queryset, or override " + u"%(cls)s.get_object()." % { + 'cls': self.__class__.__name__ + }) + return self.queryset._clone() + + def get_slug_field(self): + """ + Get the name of a slug field to be used to look up by slug. + """ + return self.slug_field + + def get_context_object_name(self, obj): + """ + Get the name to use for the object. + """ + if self.context_object_name: + return self.context_object_name + elif hasattr(obj, '_meta'): + return smart_str(obj._meta.object_name.lower()) + else: + return None + + def get_context_data(self, **kwargs): + context = kwargs + context_object_name = self.get_context_object_name(self.object) + if context_object_name: + context[context_object_name] = self.object + return context + + +class BaseDetailView(SingleObjectMixin, View): + def get(self, request, *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. Must return + a list. May not be called if get_template is overridden. + """ + try: + names = super(SingleObjectTemplateResponseMixin, self).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 /_detail.html; + # only use this if the object in question is a model. + if hasattr(self.object, '_meta'): + names.append("%s/%s%s.html" % ( + self.object._meta.app_label, + self.object._meta.object_name.lower(), + self.template_name_suffix + )) + elif hasattr(self, 'model') and hasattr(self.model, '_meta'): + names.append("%s/%s%s.html" % ( + self.model._meta.app_label, + self.model._meta.object_name.lower(), + self.template_name_suffix + )) + return names + + +class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView): + """ + Render a "detail" view of an object. + + By default this is a model instance looked up from `self.queryset`, but the + view will support display of *any* object by overriding `self.get_object()`. + """ diff --git a/apps/common/backport/generic/edit.py b/apps/common/backport/generic/edit.py new file mode 100644 index 0000000000..e1be9a88ac --- /dev/null +++ b/apps/common/backport/generic/edit.py @@ -0,0 +1,242 @@ +from django.forms import models as model_forms +from django.core.exceptions import ImproperlyConfigured +from django.http import HttpResponseRedirect +from .base import TemplateResponseMixin, View +from .detail import (SingleObjectMixin, + SingleObjectTemplateResponseMixin, BaseDetailView) + + +class FormMixin(object): + """ + A mixin that provides a way to show and handle a form in a request. + """ + + initial = {} + form_class = None + success_url = None + + def get_initial(self): + """ + Returns the initial data to use for forms on this view. + """ + return self.initial.copy() + + def get_form_class(self): + """ + Returns the form class to use in this view + """ + return self.form_class + + def get_form(self, form_class): + """ + Returns an instance of the form to be used in this view. + """ + return form_class(**self.get_form_kwargs()) + + def get_form_kwargs(self): + """ + Returns the keyword arguments for instanciating the form. + """ + kwargs = {'initial': self.get_initial()} + if self.request.method in ('POST', 'PUT'): + kwargs.update({ + 'data': self.request.POST, + 'files': self.request.FILES, + }) + return kwargs + + def get_context_data(self, **kwargs): + return kwargs + + def get_success_url(self): + if self.success_url: + url = self.success_url + else: + raise ImproperlyConfigured( + "No URL to redirect to. Provide a success_url.") + return url + + def form_valid(self, form): + return HttpResponseRedirect(self.get_success_url()) + + def form_invalid(self, form): + return self.render_to_response(self.get_context_data(form=form)) + + +class ModelFormMixin(FormMixin, SingleObjectMixin): + """ + A mixin that provides a way to show and handle a modelform in a request. + """ + + def get_form_class(self): + """ + Returns the form class to use in this view + """ + if self.form_class: + return self.form_class + else: + if self.model is not None: + # If a model has been explicitly provided, use it + model = self.model + elif hasattr(self, 'object') and self.object 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 + return model_forms.modelform_factory(model) + + def get_form_kwargs(self): + """ + Returns the keyword arguments for instanciating the form. + """ + kwargs = super(ModelFormMixin, self).get_form_kwargs() + kwargs.update({'instance': self.object}) + return kwargs + + def get_success_url(self): + if self.success_url: + url = self.success_url % 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): + self.object = form.save() + return super(ModelFormMixin, self).form_valid(form) + + def get_context_data(self, **kwargs): + context = kwargs + if self.object: + context['object'] = self.object + context_object_name = self.get_context_object_name(self.object) + if context_object_name: + context[context_object_name] = self.object + return context + + +class ProcessFormView(View): + """ + A mixin that processes a form on POST. + """ + def get(self, request, *args, **kwargs): + form_class = self.get_form_class() + form = self.get_form(form_class) + return self.render_to_response(self.get_context_data(form=form)) + + def post(self, request, *args, **kwargs): + form_class = self.get_form_class() + form = self.get_form(form_class) + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + # PUT is a valid HTTP verb for creating (with a known URL) or editing an + # object, note that browsers only support POST for now. + 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 an new object instance. + + Using this base class requires subclassing to provide a response mixin. + """ + def get(self, request, *args, **kwargs): + self.object = None + return super(BaseCreateView, self).get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.object = None + return super(BaseCreateView, self).post(request, *args, **kwargs) + + +class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView): + """ + View for creating an new object instance, + with a response rendered by template. + """ + template_name_suffix = '_form' + + +class BaseUpdateView(ModelFormMixin, ProcessFormView): + """ + Base view for updating an existing object. + + Using this base class requires subclassing to provide a response mixin. + """ + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return super(BaseUpdateView, self).get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + return super(BaseUpdateView, self).post(request, *args, **kwargs) + + +class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView): + """ + View for updating an object, + with a response rendered by template.. + """ + template_name_suffix = '_form' + + +class DeletionMixin(object): + """ + A mixin providing the ability to delete objects + """ + success_url = None + + def delete(self, request, *args, **kwargs): + self.object = self.get_object() + self.object.delete() + return HttpResponseRedirect(self.get_success_url()) + + # Add support for browsers which only accept GET and POST for now. + def post(self, *args, **kwargs): + return self.delete(*args, **kwargs) + + def get_success_url(self): + if self.success_url: + return self.success_url + else: + raise ImproperlyConfigured( + "No URL to redirect to. Provide a success_url.") + + +class BaseDeleteView(DeletionMixin, BaseDetailView): + """ + Base view for deleting an object. + + Using this base class requires subclassing to provide a response mixin. + """ + + +class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView): + """ + View for deleting an object retrieved with `self.get_object()`, + with a response rendered by template. + """ + template_name_suffix = '_confirm_delete' diff --git a/apps/common/backport/generic/list.py b/apps/common/backport/generic/list.py new file mode 100644 index 0000000000..9731e6a909 --- /dev/null +++ b/apps/common/backport/generic/list.py @@ -0,0 +1,153 @@ +from django.core.paginator import Paginator, InvalidPage +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404 +from django.utils.encoding import smart_str +from django.utils.translation import ugettext as _ +from .base import TemplateResponseMixin, View + + +class MultipleObjectMixin(object): + allow_empty = True + queryset = None + model = None + paginate_by = None + context_object_name = None + paginator_class = Paginator + + def get_queryset(self): + """ + Get the list of items for this view. This must be an interable, and may + be a queryset (in which qs-specific behavior will be enabled). + """ + if self.queryset is not None: + queryset = self.queryset + if hasattr(queryset, '_clone'): + queryset = queryset._clone() + elif self.model is not None: + queryset = self.model._default_manager.all() + else: + raise ImproperlyConfigured(u"'%s' must define 'queryset' or 'model'" + % self.__class__.__name__) + return queryset + + def paginate_queryset(self, queryset, page_size): + """ + Paginate the queryset, if needed. + """ + paginator = self.get_paginator(queryset, page_size, allow_empty_first_page=self.get_allow_empty()) + page = self.kwargs.get('page') or self.request.GET.get('page') or 1 + try: + page_number = int(page) + except ValueError: + if page == 'last': + page_number = paginator.num_pages + else: + raise Http404(_(u"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: + raise Http404(_(u'Invalid page (%(page_number)s)') % { + 'page_number': page_number + }) + + 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): + """ + 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) + + def get_allow_empty(self): + """ + Returns ``True`` if the view should display empty lists, and ``False`` + if a 404 should be raised instead. + """ + return self.allow_empty + + def get_context_object_name(self, object_list): + """ + Get the name of the item to be used in the context. + """ + if self.context_object_name: + return self.context_object_name + elif hasattr(object_list, 'model'): + return smart_str('%s_list' % object_list.model._meta.object_name.lower()) + else: + return None + + def get_context_data(self, **kwargs): + """ + Get the context for this view. + """ + queryset = kwargs.pop('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 + } + context.update(kwargs) + if context_object_name is not None: + context[context_object_name] = queryset + return context + + +class BaseListView(MultipleObjectMixin, View): + def get(self, request, *args, **kwargs): + self.object_list = self.get_queryset() + allow_empty = self.get_allow_empty() + if not allow_empty and len(self.object_list) == 0: + raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.") + % {'class_name': self.__class__.__name__}) + context = self.get_context_data(object_list=self.object_list) + return self.render_to_response(context) + + +class MultipleObjectTemplateResponseMixin(TemplateResponseMixin): + template_name_suffix = '_list' + + def get_template_names(self): + """ + Return a list of template names to be used for the request. Must return + a list. May not be called if get_template is overridden. + """ + try: + names = super(MultipleObjectTemplateResponseMixin, self).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.object_name.lower(), self.template_name_suffix)) + + return names + + +class ListView(MultipleObjectTemplateResponseMixin, BaseListView): + """ + Render some list of objects, set by `self.model` or `self.queryset`. + `self.queryset` can actually be any iterable of items, not just a queryset. + """ diff --git a/apps/common/backport/generic/list_detail.py b/apps/common/backport/generic/list_detail.py new file mode 100644 index 0000000000..22414ae216 --- /dev/null +++ b/apps/common/backport/generic/list_detail.py @@ -0,0 +1,152 @@ +from django.template import loader, RequestContext +from django.http import Http404, HttpResponse +from django.core.xheaders import populate_xheaders +from django.core.paginator import Paginator, InvalidPage +from django.core.exceptions import ObjectDoesNotExist + +import warnings +warnings.warn( + 'Function-based generic views have been deprecated; use class-based views instead.', + DeprecationWarning +) + + +def object_list(request, queryset, paginate_by=None, page=None, + allow_empty=True, template_name=None, template_loader=loader, + extra_context=None, context_processors=None, template_object_name='object', + mimetype=None): + """ + Generic list of objects. + + Templates: ``/_list.html`` + Context: + object_list + list of objects + is_paginated + are the results paginated? + results_per_page + number of objects per page (if paginated) + has_next + is there a next page? + has_previous + is there a prev page? + page + the current page + next + the next page + previous + the previous page + pages + number of pages, total + hits + number of objects, total + last_on_page + the result number of the last of object in the + object_list (1-indexed) + first_on_page + the result number of the first object in the + object_list (1-indexed) + page_range: + A list of the page numbers (1-indexed). + """ + if extra_context is None: extra_context = {} + queryset = queryset._clone() + if paginate_by: + paginator = Paginator(queryset, paginate_by, allow_empty_first_page=allow_empty) + if not page: + page = request.GET.get('page', 1) + try: + page_number = int(page) + except ValueError: + if page == 'last': + page_number = paginator.num_pages + else: + # Page is not 'last', nor can it be converted to an int. + raise Http404 + try: + page_obj = paginator.page(page_number) + except InvalidPage: + raise Http404 + c = RequestContext(request, { + '%s_list' % template_object_name: page_obj.object_list, + 'paginator': paginator, + 'page_obj': page_obj, + 'is_paginated': page_obj.has_other_pages(), + + # Legacy template context stuff. New templates should use page_obj + # to access this instead. + 'results_per_page': paginator.per_page, + 'has_next': page_obj.has_next(), + 'has_previous': page_obj.has_previous(), + 'page': page_obj.number, + 'next': page_obj.next_page_number(), + 'previous': page_obj.previous_page_number(), + 'first_on_page': page_obj.start_index(), + 'last_on_page': page_obj.end_index(), + 'pages': paginator.num_pages, + 'hits': paginator.count, + 'page_range': paginator.page_range, + }, context_processors) + else: + c = RequestContext(request, { + '%s_list' % template_object_name: queryset, + 'paginator': None, + 'page_obj': None, + 'is_paginated': False, + }, context_processors) + if not allow_empty and len(queryset) == 0: + raise Http404 + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + if not template_name: + model = queryset.model + template_name = "%s/%s_list.html" % (model._meta.app_label, model._meta.object_name.lower()) + t = template_loader.get_template(template_name) + return HttpResponse(t.render(c), mimetype=mimetype) + +def object_detail(request, queryset, object_id=None, slug=None, + slug_field='slug', template_name=None, template_name_field=None, + template_loader=loader, extra_context=None, + context_processors=None, template_object_name='object', + mimetype=None): + """ + Generic detail of an object. + + Templates: ``/_detail.html`` + Context: + object + the object + """ + if extra_context is None: extra_context = {} + model = queryset.model + if object_id: + queryset = queryset.filter(pk=object_id) + elif slug and slug_field: + queryset = queryset.filter(**{slug_field: slug}) + else: + raise AttributeError("Generic detail view must be called with either an object_id or a slug/slug_field.") + try: + obj = queryset.get() + except ObjectDoesNotExist: + raise Http404("No %s found matching the query" % (model._meta.verbose_name)) + if not template_name: + template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower()) + if template_name_field: + template_name_list = [getattr(obj, template_name_field), template_name] + t = template_loader.select_template(template_name_list) + else: + t = template_loader.get_template(template_name) + c = RequestContext(request, { + template_object_name: obj, + }, context_processors) + for key, value in extra_context.items(): + if callable(value): + c[key] = value() + else: + c[key] = value + response = HttpResponse(t.render(c), mimetype=mimetype) + populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name)) + return response diff --git a/apps/common/backport/generic/simple.py b/apps/common/backport/generic/simple.py new file mode 100644 index 0000000000..dd2f5277d5 --- /dev/null +++ b/apps/common/backport/generic/simple.py @@ -0,0 +1,68 @@ +from django.template import loader, RequestContext +from django.http import HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseGone +from django.utils.log import getLogger + +import warnings +warnings.warn( + 'Function-based generic views have been deprecated; use class-based views instead.', + DeprecationWarning +) + +logger = getLogger('django.request') + + +def direct_to_template(request, template, extra_context=None, mimetype=None, **kwargs): + """ + Render a given template with any extra URL parameters in the context as + ``{{ params }}``. + """ + if extra_context is None: extra_context = {} + dictionary = {'params': kwargs} + for key, value in extra_context.items(): + if callable(value): + dictionary[key] = value() + else: + dictionary[key] = value + c = RequestContext(request, dictionary) + t = loader.get_template(template) + return HttpResponse(t.render(c), content_type=mimetype) + +def redirect_to(request, url, permanent=True, query_string=False, **kwargs): + """ + Redirect to a given URL. + + The given url may contain dict-style string formatting, which will be + interpolated against the params in the URL. For example, to redirect from + ``/foo//`` to ``/bar//``, you could use the following URLconf:: + + urlpatterns = patterns('', + ('^foo/(?P\d+)/$', 'common.backport.generic.simple.redirect_to', {'url' : '/bar/%(id)s/'}), + ) + + If the given url is ``None``, a HttpResponseGone (410) will be issued. + + If the ``permanent`` argument is False, then the response will have a 302 + HTTP status code. Otherwise, the status code will be 301. + + If the ``query_string`` argument is True, then the GET query string + from the request is appended to the URL. + + """ + args = request.META.get('QUERY_STRING', '') + + if url is not None: + if kwargs: + url = url % kwargs + + if args and query_string: + url = "%s?%s" % (url, args) + + klass = permanent and HttpResponsePermanentRedirect or HttpResponseRedirect + return klass(url) + else: + logger.warning('Gone: %s', request.path, + extra={ + 'status_code': 410, + 'request': request + }) + return HttpResponseGone() diff --git a/apps/common/urls.py b/apps/common/urls.py index 2a74759a75..ce9503454d 100644 --- a/apps/common/urls.py +++ b/apps/common/urls.py @@ -1,5 +1,5 @@ from django.conf.urls.defaults import patterns, url -from django.views.generic.simple import direct_to_template +from common.backport.generic.simple import direct_to_template from django.conf import settings urlpatterns = patterns('common.views', @@ -23,7 +23,7 @@ urlpatterns += patterns('', url(r'^password/reset/complete/$', 'django.contrib.auth.views.password_reset_complete', {'template_name': 'password_reset_complete.html'}, name='password_reset_complete_view'), url(r'^password/reset/done/$', 'django.contrib.auth.views.password_reset_done', {'template_name': 'password_reset_done.html'}, name='password_reset_done_view'), - (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '%s%s' % (settings.STATIC_URL, 'images/favicon.ico')}), + (r'^favicon\.ico$', 'common.backport.generic.simple.redirect_to', {'url': '%s%s' % (settings.STATIC_URL, 'images/favicon.ico')}), ) urlpatterns += patterns('', diff --git a/apps/documents/views.py b/apps/documents/views.py index 59521762e6..7ba6f6421e 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -13,7 +13,7 @@ from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ -from django.views.generic.list_detail import object_list +from common.backport.generic.list_detail import object_list import sendfile diff --git a/apps/ocr/views.py b/apps/ocr/views.py index 92772ad03e..001052d3d0 100644 --- a/apps/ocr/views.py +++ b/apps/ocr/views.py @@ -4,7 +4,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.contrib import messages -from django.views.generic.list_detail import object_list +from common.backport.generic.list_detail import object_list from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.core.exceptions import PermissionDenied diff --git a/apps/permissions/views.py b/apps/permissions/views.py index 6e8673de12..0f1d1087a7 100644 --- a/apps/permissions/views.py +++ b/apps/permissions/views.py @@ -8,9 +8,9 @@ from django.http import HttpResponseRedirect, Http404 from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.contrib import messages -from django.views.generic.list_detail import object_list +from common.backport.generic.list_detail import object_list from django.core.urlresolvers import reverse -from django.views.generic.create_update import create_object, delete_object, update_object +from common.backport.generic.create_update import create_object, delete_object, update_object from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import User, Group from django.utils.simplejson import loads diff --git a/apps/user_management/views.py b/apps/user_management/views.py index 74b058cf51..bdd220c977 100644 --- a/apps/user_management/views.py +++ b/apps/user_management/views.py @@ -5,7 +5,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext from django.contrib import messages -from django.views.generic.list_detail import object_list +from common.backport.generic.list_detail import object_list from django.core.urlresolvers import reverse from django.contrib.auth.models import User, Group diff --git a/requirements/common.txt b/requirements/common.txt index 6a6413a352..23aee8e96b 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -2,7 +2,7 @@ APScheduler==2.0.3 cssmin==0.1.4 -Django==1.4.13 +#Django<1.6 django-filetransfers==0.1.0 django-pagination==1.0.7 django-compressor==1.1.1 diff --git a/settings.py b/settings.py index 0c95aef0f3..09e081c025 100644 --- a/settings.py +++ b/settings.py @@ -114,7 +114,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - 'common.middleware.login_required_middleware.LoginRequiredMiddleware', +# 'common.middleware.login_required_middleware.LoginRequiredMiddleware', 'permissions.middleware.permission_denied_middleware.PermissionDeniedMiddleware', 'pagination.middleware.PaginationMiddleware', ) From 320c701d126adefa356502d78089847976a970eb Mon Sep 17 00:00:00 2001 From: Simone Federici Date: Sun, 15 Jun 2014 10:38:26 +0200 Subject: [PATCH 2/4] upgrade template to django 1.5 url syntax --- .../templates/generic_list_horizontal_subtemplate.html | 2 +- apps/common/templates/generic_list_subtemplate.html | 2 +- apps/documents/templates/document_print.html | 4 ++-- apps/main/templates/base.html | 6 +++--- apps/web_theme/templates/web_theme_base.html | 2 +- settings.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/common/templates/generic_list_horizontal_subtemplate.html b/apps/common/templates/generic_list_horizontal_subtemplate.html index 8b62536db7..625209595b 100644 --- a/apps/common/templates/generic_list_horizontal_subtemplate.html +++ b/apps/common/templates/generic_list_horizontal_subtemplate.html @@ -29,7 +29,7 @@
{% endif %} - {#
#} + {##} {% if object_list %} {% if multi_select or multi_select_as_buttons %} diff --git a/apps/common/templates/generic_list_subtemplate.html b/apps/common/templates/generic_list_subtemplate.html index 6d826f3f07..5ee8cc0ccf 100644 --- a/apps/common/templates/generic_list_subtemplate.html +++ b/apps/common/templates/generic_list_subtemplate.html @@ -30,7 +30,7 @@
{% endif %} - + {% if object_list %} {% if multi_select or multi_select_as_buttons %} diff --git a/apps/documents/templates/document_print.html b/apps/documents/templates/document_print.html index 35f08abaf8..66841fd6e6 100644 --- a/apps/documents/templates/document_print.html +++ b/apps/documents/templates/document_print.html @@ -50,8 +50,8 @@ {% for page in pages %} {#{% get_document_size object %}#}
- {# page_aspect %}width="97%"{% else %}height="97%"{% endif %} />#} - + {# page_aspect %}width="97%"{% else %}height="97%"{% endif %} />#} +
{% endfor %} diff --git a/apps/main/templates/base.html b/apps/main/templates/base.html index 4fb55da75d..91a10c831a 100644 --- a/apps/main/templates/base.html +++ b/apps/main/templates/base.html @@ -195,14 +195,14 @@ {% trans "Anonymous" %} {% else %} {{ user.get_full_name|default:user }} - + {% endif %} {% get_setting "MIDDLEWARE_CLASSES" as middleware_classes %} {% if "django.middleware.locale.LocaleMiddleware" in middleware_classes %}
  • - {% csrf_token %} + {% csrf_token %}