Source code for document_catalogue.views

from functools import partial
from importlib import import_module
from itertools import groupby

from django.apps import apps
from django.db.models import Q
from django.http import Http404, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.views import generic

from . import forms, plugins
from .decorators import permission_required
from .models import Document, DocumentCategory
from .views_generic import AjaxOnlyViewMixin

appConfig = apps.get_app_config("document_catalogue")

# import Plugin Permissions module
permissions = import_module(appConfig.settings.PERMISSIONS)

# import Plugin classes
list_view_plugin_classes = tuple(
    import_string(plugin) for plugin in appConfig.settings.LIST_VIEW_PLUGINS
)


[docs]def get_permissions_context(view): """Return a dictionary of permissions (partials that can be called with no arguments)""" context = {} for name in dir(permissions): fn = getattr(permissions, name) if callable(fn): context[name] = partial(fn, view.request.user, **view.kwargs) return context
[docs]@permission_required(permissions.user_can_view_document_catalogue) class CatalogueViewMixin(generic.base.ContextMixin, generic.View): """Mixin for all Document Views""" def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx.update( { "show_edit_links": True if appConfig.settings.ENABLE_EDIT_URLS else False, **get_permissions_context(self), } ) return ctx
[docs]class CategorySlugViewMixin: """Mixin for views that take a category slug as a URL arg""" @property def category_slug(self): return self.kwargs.get("slug", None) @cached_property def category(self): return get_object_or_404(DocumentCategory, slug=self.category_slug)
[docs]class DocumentPkMixin: """Mixins for views that take a document pk as a URL arg""" @property def document_pk(self): return self.kwargs.get("pk", None) @cached_property def document(self): try: return Document.published.get(pk=self.document_pk) except Document.DoesNotExist: raise Http404
[docs]class DocumentCatalogueListView(CatalogueViewMixin, generic.ListView): """List all categories in the Catalogue""" template_name = "document_catalogue/categories_list.html" queryset = DocumentCategory.objects.all()
[docs]class CategoryContextViewMixin(generic.base.ContextMixin, CategorySlugViewMixin): """Mixing for views that supply context about a category""" # here to serve as a base class so consitent MRO can be created pass
[docs]class CategoryListViewMixin(CategoryContextViewMixin): """Mixin for views that navigate categories or display a list of categories""" def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx.update( { "category": self.category, } ) return ctx
[docs]class BaseDocumentListView( plugins.ViewPluginManager, CatalogueViewMixin, CategoryContextViewMixin, generic.ListView, ): """ Base class / mixin for views that list documents. Plugin architecture used to inject custom view logic. See plugins. """ queryset = Document.published.all()
[docs] def dispatch(self, request, *args, **kwargs): """Apply any plugins""" self.apply_plugins(lambda plugin: plugin.apply(request)) return super().dispatch(request, *args, **kwargs)
def get_document_queryset(self): qs = super().get_queryset() if self.category_slug: qs = qs.filter(category__slug=self.category_slug) return self.plugins_extend_qs(self.request, qs)
[docs] def get_queryset(self): return self.get_document_queryset()
[docs] def get_context_data(self, **kwargs): plugin_ctx = self.plugins_get_context(self.request) return super().get_context_data(**plugin_ctx)
[docs]@plugins.RegisterPlugins(*(plugin() for plugin in list_view_plugin_classes)) class CategoryDocumentListView(CategoryListViewMixin, BaseDocumentListView): """List all documents in a given category""" template_name = "document_catalogue/category_document_list.html"
[docs]class DocumentViewMixin(generic.base.ContextMixin, DocumentPkMixin): """Mixins for views that display a document""" def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx.update( { "document": self.document, "category": self.document.category, } ) return ctx def get_document_absolute_uri(self): return self.request.build_absolute_uri(self.document.get_download_url())
[docs]class DocumentDetailView(CatalogueViewMixin, DocumentViewMixin, generic.DetailView): """Display detailed information about a single document""" template_name = "document_catalogue/document_detail.html" model = Document
[docs]@permission_required(permissions.user_can_view_document_catalogue) class DocumentDownloadView(DocumentPkMixin, generic.RedirectView): """Redirect to the file URL"""
[docs] def get_redirect_url(self, *args, **kwargs): """Return the document's file download URL""" return self.document.file.url
[docs]@permission_required(permissions.user_can_edit_document) class DocumentEditView(CatalogueViewMixin, DocumentViewMixin, generic.UpdateView): """Display detailed information about a single document""" template_name = "document_catalogue/document_edit.html" model = Document form_class = forms.DocumentEditForm
[docs]@permission_required(permissions.user_can_delete_document) class DocumentDeleteView(CatalogueViewMixin, DocumentViewMixin, generic.DeleteView): """Delete a single document""" model = Document @property def document(self): return self.object def get_success_url(self): return reverse( "document_catalogue:category_list", kwargs={"slug": self.document.category.slug}, )
[docs]class DocumentAjaxAPI( CatalogueViewMixin, CategorySlugViewMixin, DocumentPkMixin, AjaxOnlyViewMixin ): """ Async API for document actions Plays nice with document_catalogue.js and dropzone """ def save_document(self): file = self.request.FILES["file"] document = Document( user=self.request.user, title=file.name, category=self.category, is_published=True, file=file, ) document.save() return document def post(self, request, *args, **kwargs): if not permissions.user_can_post_document(request.user, **self.kwargs): return HttpResponseForbidden("Permission Denied") form_class = forms.DocumentUploadForm document_template = get_template("document_catalogue/include/documents.html") def get_form(): return form_class(data=request.POST, files=request.FILES) # Use the Upload Form to validate the file (mime type and size) form = get_form() if form.is_valid(): document = self.save_document() html = ( document_template.render( {"document_list": (document,), **get_permissions_context(self)} ), ) return self.render_to_json_response( { "success": True, "document_item": html, } ) else: # Common error handling is completed by dropzone -- this is a hard-fail fallback. return HttpResponseForbidden( "Invalid request: Form errors %s" % ", ".join(e.as_text() for e in form.errors.values()) ) def delete(self, request, *args, **kwargs): if not permissions.user_can_delete_document(request.user, **self.kwargs): return HttpResponseForbidden("Permission Denied") if self.document: self.document.delete() json_context = {"success": True} return self.render_to_json_response(json_context) else: return HttpResponseForbidden("Invalid request. Document NOT deleted.")
[docs] def get(self, request, *args, **kwargs): """Ajax Search for documents matching search term in ?q= request param""" search_term = request.GET.get("q", None) def format_select2(document): return {"id": document.get_absolute_url(), "text": document.title} search_options = [] # Format options as select2 data objects if search_term: # retrieve search results, if a search_term is given filter = Q(title__icontains=search_term) | Q( category__name__icontains=search_term ) docs = Document.published.filter(filter) search_options = [ { "text": category.name, "children": [format_select2(doc) for doc in group], } for category, group in groupby(docs, lambda d: d.category) ] else: # Recently updated documents. recently_updated = Document.published.order_by("-update_date")[:10] if recently_updated: options = [format_select2(doc) for doc in recently_updated] search_options = [ {"text": "Recently Updated", "children": options}, ] return self.render_to_json_response({"options": search_options})