callisto_core/delivery/view_partials.py
"""
View partials provide all the callisto-core front-end functionality.
Subclass these partials with your own views if you are implementing
callisto-core. Many of the view partials only provide a subset of the
functionality required for a full HTML view.
docs / reference:
- https://docs.djangoproject.com/en/1.11/topics/class-based-views/
- https://github.com/project-callisto/callisto-core/blob/master/callisto_core/wizard_builder/view_partials.py
view_partials should define:
- forms
- models
- helper classes
- access checks
- redirect handlers
and should not define:
- templates
- url names
"""
import logging
import re
import ratelimit.mixins
from nacl.exceptions import CryptoError
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.views import generic as views
from django.utils.http import is_safe_url
from callisto_core.evaluation.view_partials import EvalDataMixin
from callisto_core.reporting import report_delivery
from callisto_core.wizard_builder import (
data_helper,
view_partials as wizard_builder_partials,
)
from . import forms, models, view_helpers
logger = logging.getLogger(__name__)
#######################
# secret key partials #
#######################
class _PassphrasePartial(views.base.TemplateView):
storage_helper = view_helpers.ReportStorageHelper
@property
def storage(self):
return self.storage_helper(self)
class _PassphraseClearingPartial(EvalDataMixin, _PassphrasePartial):
def get(self, request, *args, **kwargs):
self.storage.clear_passphrases()
return super().get(request, *args, **kwargs)
class DashboardPartial(_PassphraseClearingPartial):
EVAL_ACTION_TYPE = "DASHBOARD"
###################
# report partials #
###################
class ReportBasePartial(EvalDataMixin, wizard_builder_partials.WizardFormPartial):
model = models.Report
storage_helper = view_helpers.EncryptedReportStorageHelper
EVAL_ACTION_TYPE = "VIEW"
@property
def site_id(self):
# TODO: remove
return self.request.site.id
@property
def decrypted_report(self):
return self.report.decrypt_record(self.storage.passphrase)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({"view": self}) # TODO: remove
return kwargs
class ReportCreatePartial(ReportBasePartial, views.edit.CreateView):
form_class = forms.ReportCreateForm
EVAL_ACTION_TYPE = "CREATE"
def get_success_url(self):
return reverse(self.success_url, kwargs={"step": 0, "uuid": self.object.uuid})
class _ReportDetailPartial(ReportBasePartial, views.detail.DetailView):
context_object_name = "report"
slug_field = "uuid"
slug_url_kwarg = "uuid"
@property
def report(self):
# TODO: remove, use self.object
return self.get_object()
class _ReportLimitedDetailPartial(
_ReportDetailPartial, ratelimit.mixins.RatelimitMixin
):
ratelimit_key = "user"
ratelimit_rate = settings.DECRYPT_THROTTLE_RATE
class _ReportAccessPartial(_ReportLimitedDetailPartial):
invalid_access_key_message = "Invalid key in access request"
invalid_access_user_message = "Invalid user in access request"
invalid_access_no_key_message = "No key in access request"
form_class = forms.ReportAccessForm
access_form_class = forms.ReportAccessForm
@property
def access_granted(self):
self._check_report_owner()
try:
passphrase = self.request.POST["key"]
except Exception:
return False
if passphrase:
try:
self.storage.report.decrypt_record(passphrase)
return True
except CryptoError:
logger.warn(self.invalid_access_key_message)
return False
else:
logger.info(self.invalid_access_no_key_message)
return False
@property
def access_form_valid(self):
form = self._get_access_form()
if form.is_valid():
form.save()
return True
else:
return False
def _passphrase_next_url(self, request):
next_url = None
if "next" in request.GET:
if re.search(r"^/[\W/-]*", request.GET["next"]):
if is_safe_url(request.GET["next"]):
next_url = request.GET["next"]
return next_url
def dispatch(self, request, *args, **kwargs):
logger.debug(f"{self.__class__.__name__} access check")
if (
self.access_granted or self.access_form_valid
) and self._passphrase_next_url(request):
return self._redirect_from_passphrase(request)
elif self.access_granted or self.access_form_valid:
return super().dispatch(request, *args, **kwargs)
else:
return self._render_access_form()
def _get_access_form(self):
form_kwargs = self.get_form_kwargs()
form_kwargs.update({"instance": self.get_object()})
return self.access_form_class(**form_kwargs)
def _render_access_form(self):
self.object = self.report
self.template_name = self.access_template_name
context = self.get_context_data(form=self._get_access_form())
return self.render_to_response(context)
def _redirect_from_passphrase(self, request):
return redirect(self._passphrase_next_url(request))
def _check_report_owner(self):
if not self.report.owner == self.request.user:
logger.warn(self.invalid_access_user_message)
raise PermissionDenied
class _ReportUpdatePartial(_ReportAccessPartial, views.edit.UpdateView):
back_url = None
@property
def report(self):
# TODO: remove, use self.object
return self.get_object()
###################
# wizard partials #
###################
class EncryptedWizardPartial(
_ReportUpdatePartial, wizard_builder_partials.WizardPartial
):
steps_helper = view_helpers.ReportStepsHelper
EVAL_ACTION_TYPE = "EDIT"
def dispatch(self, request, *args, **kwargs):
self._dispatch_processing()
return super().dispatch(request, *args, **kwargs)
def _rendering_done_hook(self):
self.eval_action("REVIEW")
###################
# report actions #
###################
class _ReportActionPartial(_ReportUpdatePartial):
success_url = reverse_lazy("dashboard")
def form_valid(self, form):
logger.debug(f"{self.__class__.__name__} form valid")
output = super().form_valid(form)
self.view_action()
return output
def form_invalid(self, form):
return super().form_invalid(form)
def view_action(self):
pass
class ReportDeletePartial(_ReportActionPartial):
EVAL_ACTION_TYPE = "DELETE"
def view_action(self):
self.report.delete()
class WizardPDFPartial(_ReportActionPartial):
EVAL_ACTION_TYPE = "ACCESS_PDF"
def form_valid(self, form):
# remove the old PDF generator completely.
# this should be generated via JS now.
pass
class ViewPDFPartial(WizardPDFPartial):
content_disposition = "inline"
EVAL_ACTION_TYPE = "VIEW_PDF"
class DownloadPDFPartial(WizardPDFPartial):
content_disposition = "attachment"
EVAL_ACTION_TYPE = "DOWNLOAD_PDF"