project-callisto/callisto-core

View on GitHub
callisto_core/reporting/view_partials.py

Summary

Maintainability
A
3 hrs
Test Coverage
"""

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/

view_partials should define:
    - forms
    - models
    - helper classes
    - access checks
    - redirect handlers

and should not define:
    - templates
    - url names

"""
from django.contrib.auth.views import PasswordResetView
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.views.generic.edit import FormView

from callisto_core.accounts import forms as account_forms, tokens as account_tokens
from callisto_core.delivery import view_partials as delivery_partials
from callisto_core.utils.api import MatchingApi, NotificationApi, TenantApi

from . import forms, view_helpers


class _SubmissionPartial(
    view_helpers.ReportingSuccessUrlMixin, delivery_partials._ReportUpdatePartial
):
    back_url = None

    @property
    def all_user_emails(self):
        return [
            self.report.owner.email,
            self.report.owner.account.school_email,
            self.report.contact_email,
        ]

    @property
    def in_demo_mode(self):
        return TenantApi.site_settings("DEMO_MODE", request=self.request, cast=bool)

    @property
    def coordinator_emails(self):
        return TenantApi.site_settings("COORDINATOR_EMAIL", request=self.request)

    @property
    def coordinator_public_key(self):
        return TenantApi.site_settings("COORDINATOR_PUBLIC_KEY", request=self.request)

    @property
    def school_name(self):
        return TenantApi.site_settings("SCHOOL_SHORTNAME", request=self.request)

    @property
    def school_email_domain(self):
        return TenantApi.site_settings("SCHOOL_EMAIL_DOMAIN", request=self.request)


class CallistoPasswordResetView(PasswordResetView, FormView):
    """
    Workaround for bug in PasswordResetView's form_valid() function
    where it calls get_current_site() from its save() method. To
    workaround this we overload PasswordResetView's form_valid()
    method and set save()'s 'domain_override' option in order to
    bypass calls to get_current_site().
    """

    def form_valid(self, form):
        opts = {
            "use_https": self.request.is_secure(),
            "token_generator": self.token_generator,
            "from_email": self.from_email,
            "email_template_name": self.email_template_name,
            "subject_template_name": self.subject_template_name,
            "request": self.request,
            "domain_override": self.request.site.domain,
            "html_email_template_name": self.html_email_template_name,
            "extra_email_context": self.extra_email_context,
        }
        form.save(**opts)

        # Do not call super() for our parents. They will all call save()
        # wrong way again and cause us much sadness (broken notifications).
        # Instead, we must call "HttpResponseRedirect" as per the parent
        # Mixin class (see: django/views/generic/edit.py).
        return HttpResponseRedirect(self.get_success_url())


class SchoolEmailFormPartial(_SubmissionPartial, CallistoPasswordResetView):
    form_class = account_forms.ReportingVerificationEmailForm
    token_generator = account_tokens.StudentVerificationTokenGenerator()
    # success_url is used for inputting a valid school email address
    success_url = None
    # next_url is used for having your account already verified,
    # and inputting a correct account verification token
    next_url = None
    EVAL_ACTION_TYPE = "SCHOOL_EMAIL_ENTRY"

    @property
    def student_confirmation_url(self):
        uidb64 = urlsafe_base64_encode(force_bytes(self.request.user.pk)).decode(
            "utf-8"
        )
        token = self.token_generator.make_token(self.request.user)
        return reverse(
            self.request.resolver_match.view_name,
            kwargs={"uuid": self.report.uuid, "uidb64": uidb64, "token": token},
        )

    def dispatch(self, request, *args, **kwargs):
        if self.request.user.account.is_verified:
            return self._redirect_to_next()
        else:
            return super().dispatch(request, *args, **kwargs)

    def get_success_url(self):
        return reverse(self.success_url)

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs.update({"school_email_domain": self.school_email_domain})
        return kwargs

    def form_valid(self, form):
        self._update_email(form)
        self._send_mail(form)
        return super().form_valid(form)

    def _update_email(self, form):
        self.request.user.account.school_email = form.cleaned_data.get("email")
        self.request.user.account.save()

    def _send_mail(self, form):
        NotificationApi.send_with_kwargs(
            site_id=self.request.user.account.site_id,
            to_addresses=[form.cleaned_data.get("email")],
            email_subject="Verify your student email",
            email_name="student_verification_email",
            redirect_url=self.student_confirmation_url,
            email_template_name=self.email_template_name,
            user=self.request.user,
        )

    def _redirect_to_next(self):
        next_url = reverse(self.next_url, kwargs={"uuid": self.report.uuid})
        return redirect(next_url)


class SchoolEmailConfirmationPartial(SchoolEmailFormPartial):
    EVAL_ACTION_TYPE = "SCHOOL_EMAIL_CONFIRMATION"

    def verify_email(self):
        self.request.user.account.is_verified = True
        self.request.user.account.save()

    def dispatch(self, request, token=None, uidb64=None, *args, **kwargs):
        if self.token_generator.check_token(self.request.user, token):
            self.verify_email()
            return self._redirect_to_next()
        else:
            return super().dispatch(self.request, *args, **kwargs)


class PrepPartial(_SubmissionPartial):
    form_class = forms.PrepForm
    EVAL_ACTION_TYPE = "CONTACT_INFO_PREPERATION"


class ResubmitPrepPartial(PrepPartial):
    EVAL_ACTION_TYPE = "RESUBMIT_CONTACT_INFO_PREPERATION"


class _ReportSubclassPartial(_SubmissionPartial):
    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()
        kwargs.update({"instance": None})  # TODO: remove
        return kwargs


class _MatchingPartial(_ReportSubclassPartial):
    def form_valid(self, form):
        response = super().form_valid(form)
        identifiers = form.cleaned_data.get("identifiers")

        self._notify_owner_of_submission(identifiers)
        for identifier in identifiers:
            matches = self._get_matches(identifier)

            if matches:
                self._notify_authority_of_matches(matches, identifier)
                self._notify_owners_of_matches(matches)
                self._slack_match_notification()
                self._match_confirmation_email_to_callisto(matches)

        return response

    def _get_matches(self, identifier):
        return MatchingApi.find_matches(identifier)

    def _slack_match_notification(self):
        if not self.in_demo_mode:
            NotificationApi.slack_notification(
                msg="New Callisto Matches (details will be sent via email)",
                type="match_confirmation",
            )

    def _match_confirmation_email_to_callisto(self, matches):
        if not self.in_demo_mode:
            NotificationApi.send_with_kwargs(
                site_id=self.site_id,  # required in general
                email_template_name=self.admin_email_template_name,  # the email template
                to_addresses=NotificationApi.ALERT_LIST,  # addresses to send to
                matches=matches,  # used in the email body
                email_subject="New Callisto Matches",  # rendered as the email subject
                email_name="match_confirmation_callisto_team",  # used in test assertions
            )

    def _notify_owner_of_submission(self, identifier):
        if identifier:
            NotificationApi.send_confirmation(
                email_type="match_confirmation",
                to_addresses=[self.report.contact_email],
                site_id=self.site_id,
            )

    def _notify_authority_of_matches(self, matches, identifier):
        NotificationApi.send_matching_report_to_authority(
            matches=matches,
            identifier=identifier,
            to_addresses=self.coordinator_emails,
            public_key=self.coordinator_public_key,
        )

    def _notify_owners_of_matches(self, matches):
        for match in matches:
            NotificationApi.send_match_notification(match_report=match)


class OptionalMatchingPartial(_MatchingPartial):
    EVAL_ACTION_TYPE = "ENTER_MATCHING_OPTIONAL"
    form_class = forms.MatchingOptionalForm


class RequiredMatchingPartial(_MatchingPartial):
    EVAL_ACTION_TYPE = "ENTER_MATCHING_REQUIRED"
    form_class = forms.MatchingRequiredForm


class ConfirmationPartial(_ReportSubclassPartial):
    form_class = forms.ConfirmationForm
    EVAL_ACTION_TYPE = "DIRECT_REPORTING_FINAL_CONFIRMATION"
    SLACK_ALERT_TEXT = (
        "New Callisto Report at {school_name} (details will be sent via email)"
    )

    def form_valid(self, form):
        output = super().form_valid(form)
        self._save_to_address(form)
        self._send_report_alerts()
        return output

    def _send_report_to_authority(self, report):
        kwargs = {
            "sent_report": report,
            "report_data": self.storage.cleaned_form_data,
            "site_id": self.site_id,
            "to_addresses": [self.coordinator_emails],
            "public_key": self.coordinator_public_key,
        }
        print(kwargs)
        if self.in_demo_mode:
            kwargs["DEMO_MODE"] = True
            kwargs["to_addresses"] += self.all_user_emails
        NotificationApi.send_report_to_authority(**kwargs)

    def _send_confirmation_email(self):
        kwargs = {
            "email_type": "submit_confirmation",
            "to_addresses": [self.report.contact_email],
            "site_id": self.site_id,
        }
        if self.in_demo_mode:
            kwargs["DEMO_MODE"] = True
            kwargs["to_addresses"] += self.all_user_emails
        NotificationApi.send_confirmation(**kwargs)

    def _send_confirmation_email_to_callisto(self):
        if not self.in_demo_mode:
            NotificationApi.send_with_kwargs(
                site_id=self.site_id,  # required in general
                email_template_name=self.admin_email_template_name,  # the email template
                to_addresses=NotificationApi.ALERT_LIST,  # addresses to send to
                report=self.report,  # used in the email body
                email_subject="New Callisto Report",  # rendered as the email subject
            )

    def _send_confirmation_slack_notification(self):
        if not self.in_demo_mode:
            NotificationApi.slack_notification(
                msg=self.SLACK_ALERT_TEXT.format(school_name=self.school_name),
                type="submit_confirmation",
            )

    def _save_to_address(self, form):
        sent_report = form.instance
        sent_report.to_address = self.coordinator_emails
        sent_report.save()

    def _send_report_alerts(self):
        for sent_full_report in self.report.sentfullreport_set.all():
            self._send_report_to_authority(sent_full_report)
        self._send_confirmation_email()
        self._send_confirmation_email_to_callisto()
        self._send_confirmation_slack_notification()


class ResubmitConfirmationPartial(ConfirmationPartial):
    form_class = forms.ConfirmedConfirmationForm
    EVAL_ACTION_TYPE = "RESUBMIT_FINAL_CONFIRMATION"
    SLACK_ALERT_TEXT = (
        "Resubmitted Callisto Report at {school_name} (details will be sent via email)"
    )


class MatchingWithdrawPartial(
    view_helpers.ReportingSuccessUrlMixin, delivery_partials._ReportActionPartial
):
    EVAL_ACTION_TYPE = "MATCHING_WITHDRAW"

    def view_action(self):
        self.report.withdraw_from_matching()