import calendar
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, JsonResponse
from django.utils.cache import get_conditional_response, patch_vary_headers
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
from parasolr.django.queryset import SolrQuerySet
from parasolr.utils import solr_timestamp_to_datetime
import rdflib
from mep.common import SCHEMA_ORG
[docs]class LoginRequiredOr404Mixin(LoginRequiredMixin):
'''Extend :class:`~django.contrib.auth.mixins.LoginRequiredMixin`
to return a 404 if access is denied, rather than redirecting
to the login form.'''
[docs] def handle_no_permission(self):
'''If permission is denied, raise an :class:`~django.http.Http404`'''
raise Http404
[docs]class LabeledPagesMixin(ContextMixin):
'''View mixin to add labels for pages to a paginated view's context,
for use in rendering pagination controls.'''
[docs] def get_page_labels(self, paginator):
'''Generate labels for pages. Defaults to labeling pages using numeric
ranges, e.g. `50-100`.'''
page_labels = []
# if there's nothing to paginate, just return an empty list
if paginator.count == 0:
return page_labels
for page in paginator.page_range:
# item count starts at zero, goes up by page size
page_start = (page - 1) * paginator.per_page
# final page should end at number of the final item
page_end = min(page_start + paginator.per_page, paginator.count)
# first item on page is 1-based index, e.g. 51-100
page_labels.append((page, '%d – %d' % (page_start + 1, page_end)))
return page_labels
[docs] def get_context_data(self, **kwargs):
'''Add generated page labels to the view context.'''
context = super().get_context_data(**kwargs)
paginator = context['page_obj'].paginator
context['page_labels'] = self.get_page_labels(paginator)
# store paginator and generated labels for use in custom headers
# on ajax response
self._paginator = paginator
self._page_labels = context['page_labels']
return context
[docs] def dispatch(self, request, *args, **kwargs):
'''Wrap the dispatch method to patch in page label header for
ajax requests.'''
response = super(LabeledPagesMixin, self).dispatch(request, *args, **kwargs)
# NOTE we need to replace the en dashes in alpha pagelabels with a
# hyphen when requested via ajax because unicode can't be sent via the
# X-Page-Labels header. this needs to get converted back to an en dash
# on the client side.
if self.request.is_ajax():
response['X-Page-Labels'] = '|'.join(
[label.replace('–', '-') for index, label in self._page_labels])
return response
[docs]class RdfViewMixin(ContextMixin):
'''View mixin to add an RDF linked data graph to context for use in serializing
and embedding structured data in templates.'''
#: default schema.org type for a View
rdf_type = SCHEMA_ORG.WebPage
#: breadcrumbs, used to render breadcrumb navigation. they should be a list
#: of tuples like ('Title', '/url')
breadcrumbs = []
[docs] def get_context_data(self, *args, **kwargs):
'''Add generated breadcrumbs and an RDF graph to the view context.'''
context = super().get_context_data(*args, **kwargs)
return self.add_rdf_to_context(context)
[docs] def get_context(self, request, *args, **kwargs):
'''Add generated breadcrumbs and RDF graph to Wagtail page context.'''
context = super().get_context(request, *args, **kwargs)
return self.add_rdf_to_context(context)
[docs] def add_rdf_to_context(self, context):
'''add jsonld and breadcrumb list to context dictionary'''
context.update({
'page_jsonld': self.as_rdf().serialize(format='json-ld',
auto_compact=True).decode(),
'breadcrumbs': self.get_breadcrumbs()
})
return context
[docs] def get_absolute_url(self):
'''Get a URI for this page to use for making RDF assertions. Note that
this should return a full absolute path, e.g. with absolutize_url().'''
raise NotImplementedError
[docs] def as_rdf(self):
'''Generate an RDF graph representing the page.'''
# add the root node (this page)
graph = rdflib.ConjunctiveGraph()
# explicitly bind schema.org namespace
graph.bind('schema', SCHEMA_ORG)
page_uri = rdflib.URIRef(self.get_absolute_url())
graph.add((page_uri, rdflib.RDF.type, self.rdf_type))
# generate and add breadcrumbs, if any
breadcrumbs = self.get_breadcrumbs()
if breadcrumbs:
breadcrumbs_node = rdflib.BNode()
graph.set((page_uri, SCHEMA_ORG.breadcrumb, breadcrumbs_node))
graph.set((breadcrumbs_node, rdflib.RDF.type, SCHEMA_ORG.BreadcrumbList))
for pos, crumb in enumerate(breadcrumbs):
crumb_node = rdflib.BNode()
graph.add((breadcrumbs_node, SCHEMA_ORG.itemListElement, crumb_node))
graph.set((crumb_node, rdflib.RDF.type, SCHEMA_ORG.ListItem))
graph.set((crumb_node, SCHEMA_ORG.name, rdflib.Literal(crumb[0]))) # name/label
graph.set((crumb_node, SCHEMA_ORG.item, rdflib.Literal(crumb[1]))) # url
graph.set((crumb_node, SCHEMA_ORG.position, rdflib.Literal(pos + 1))) # position
# output full graph
return graph
[docs] def get_breadcrumbs(self):
'''Generate the breadcrumbs that lead to this page. Returns the value of
`breadcrumbs` set on the View by default.'''
return self.breadcrumbs
[docs]class AjaxTemplateMixin(TemplateResponseMixin, VaryOnHeadersMixin):
'''View mixin to use a different template when responding to an ajax
request.'''
#: name of the template to use for ajax request
ajax_template_name = None
#: vary on X-Request-With to avoid browsers caching and displaying
#: ajax response for the non-ajax response
vary_headers = ['X-Requested-With']
[docs] def get_template_names(self):
'''Return :attr:`ajax_template_name` if this is an ajax request;
otherwise return default template name.'''
if self.request.is_ajax():
return self.ajax_template_name
return super().get_template_names()
[docs] def dispatch(self, request, *args, **kwargs):
'''Set a total result header on the response'''
response = super(AjaxTemplateMixin, self).dispatch(request, *args, **kwargs)
response['X-Total-Results'] = self.get_queryset().count()
return response
[docs]class FacetJSONMixin(TemplateResponseMixin, VaryOnHeadersMixin):
'''View mixin to respond with JSON representation of Solr facets when the
HTTP Accept: header specifies application/json.'''
#: vary on Accept: so that facets and results are cached separately
vary_headers = ['Accept']
[docs] def render_to_response(self, request, *args, **kwargs):
'''Return a JsonResponse if the client asked for JSON, otherwise just
call dispatch(). NOTE that this isn't currently smart enough to detect
if the view's queryset is a SolrQuerySet; it will just break.'''
if self.request.META.get('HTTP_ACCEPT') == 'application/json':
return self.render_facets(request, *args, **kwargs)
return super(FacetJSONMixin, self).render_to_response(request, *args, **kwargs)
[docs] def render_facets(self, request, *args, **kwargs):
'''Construct a JsonResponse based on the already-populated queryset
data for the view.'''
return JsonResponse(self.object_list.get_facets())
# last modified view mixin adapted from ppa
[docs]class SolrLastModifiedMixin(View):
"""View mixin to add last modified headers based on Solr"""
#: solr query filter for getting last modified date
solr_lastmodified_filters = {} # by default, find all
[docs] def get_solr_lastmodified_filters(self):
'''Get filters for last modified Solr query. By default returns
:attr:`solr_lastmodified_filters`.'''
return self.solr_lastmodified_filters
[docs] def last_modified(self):
'''Return last modified :class:`datetime.datetime` from the
specified Solr query'''
filter_qs = self.get_solr_lastmodified_filters()
sqs = SolrQuerySet().filter(**filter_qs) \
.order_by('-last_modified').only('last_modified')
try:
# Solr stores date in isoformat; convert to datetime
return solr_timestamp_to_datetime(sqs[0]['last_modified'])
# skip extra call to Solr to check count and just grab the first
# item if it exists
except (IndexError, KeyError):
# if a syntax or other solr error happens, no date to return
pass
[docs] def dispatch(self, request, *args, **kwargs):
'''Wrap the dispatch method to add a last modified header if
one is available, then return a conditional response.'''
# NOTE: this doesn't actually skip view processing,
# but without it we could return a not modified for a non-200 response
response = super(SolrLastModifiedMixin, self) \
.dispatch(request, *args, **kwargs)
last_modified = self.last_modified()
if last_modified:
# remove microseconds so that comparison will pass,
# since microseconds are not included in the last-modified header
last_modified = last_modified.replace(microsecond=0)
response['Last-Modified'] = last_modified \
.strftime('%a, %d %b %Y %H:%M:%S GMT')
# convert the same way django does so that they will
# compare correctly
last_modified = calendar.timegm(last_modified.utctimetuple())
return get_conditional_response(request, last_modified=last_modified,
response=response)