From ea6ef050284dc9c68416785686f618409ef821a0 Mon Sep 17 00:00:00 2001
From: Todd Dembrey <todd.dembrey@torchbox.com>
Date: Thu, 2 Aug 2018 16:05:10 +0100
Subject: [PATCH] Add logic for viewing sealed round

---
 opentech/apply/activity/messaging.py          | 15 ++++-
 .../templates/funds/submission_sealed.html    |  4 +-
 .../apply/funds/tests/factories/models.py     | 22 +++++++
 opentech/apply/funds/tests/test_views.py      | 60 ++++++++++++++++++-
 opentech/apply/funds/views.py                 | 55 +++++++++++++++--
 opentech/apply/users/tests/factories.py       |  5 ++
 opentech/apply/utils/testing/tests.py         |  3 +-
 7 files changed, 153 insertions(+), 11 deletions(-)

diff --git a/opentech/apply/activity/messaging.py b/opentech/apply/activity/messaging.py
index 815828de2..ac07d77b0 100644
--- a/opentech/apply/activity/messaging.py
+++ b/opentech/apply/activity/messaging.py
@@ -23,6 +23,7 @@ class MESSAGES(Enum):
     NEW_REVIEW = 'New Review'
     COMMENT = 'Comment'
     PROPOSAL_SUBMITTED = 'Proposal Submitted'
+    OPENED_SEALED = 'Opened sealed application'
 
     @classmethod
     def choices(cls):
@@ -110,12 +111,19 @@ class ActivityAdapter(AdapterBase):
         MESSAGES.DETERMINATION_OUTCOME: 'Sent a determination. Outcome: {submission.determination.clean_outcome}',
         MESSAGES.INVITED_TO_PROPOSAL: 'Invited to submit a proposal',
         MESSAGES.REVIEWERS_UPDATED: 'reviewers_updated',
-        MESSAGES.NEW_REVIEW: '{user} submitted a review'
+        MESSAGES.NEW_REVIEW: '{user} submitted a review',
+        MESSAGES.OPENED_SEALED: '{user} opened the application while still sealed',
     }
 
     def recipients(self, message_type, **kwargs):
         return [None]
 
+    def extra_kwargs(self, message_type, **kwargs):
+        if message_type == MESSAGES.OPENED_SEALED:
+            from .models import INTERNAL
+            return {'visibility': INTERNAL}
+        return {}
+
     def reviewers_updated(self, added, removed, **kwargs):
         message = ['Reviewers updated.']
         if added:
@@ -129,11 +137,13 @@ class ActivityAdapter(AdapterBase):
         return ' '.join(message)
 
     def send_message(self, message, user, submission, **kwargs):
-        from .models import Activity
+        from .models import Activity, PUBLIC
+        visibility = kwargs.get('visibility', PUBLIC)
         Activity.actions.create(
             user=user,
             submission=submission,
             message=message,
+            visibility=visibility,
         )
 
 
@@ -151,6 +161,7 @@ class SlackAdapter(AdapterBase):
         MESSAGES.INVITED_TO_PROPOSAL: '<{link}|{submission.title}> by {submission.user} has been invited to submit a proposal',
         MESSAGES.NEW_REVIEW: '{user} has submitted a review for <{link}|{submission.title}>. Outcome: {review.outcome} Score: {review.score}',
         MESSAGES.READY_FOR_REVIEW: 'notify_reviewers',
+        MESSAGES.OPENED_SEALED: '{user} has opened the sealed application: <{link}|{submission.title}>'
     }
 
     def __init__(self):
diff --git a/opentech/apply/funds/templates/funds/submission_sealed.html b/opentech/apply/funds/templates/funds/submission_sealed.html
index 5f10d2286..57342bf92 100644
--- a/opentech/apply/funds/templates/funds/submission_sealed.html
+++ b/opentech/apply/funds/templates/funds/submission_sealed.html
@@ -17,8 +17,8 @@
     <h2 class="heading">This application is sealed until the round is closed</h2>
     <h3>The round ends on: {{ object.open_round.end_date }}</h3>
     <a class="button button--primary" href="{% url 'apply:submissions:list' %}">Go back</a>
-    {% if request.user.is_superuser %}
-        <p>As an admin you are allowed to access the application. However this action will be recorded.</p>
+    {% if can_view_sealed %}
+        <p>As an admin you are allowed to access the application. However, this action will be recorded.</p>
         <form method="post">
             {% csrf_token %}
             <input class="button button--warning" type="submit" value="View application"></input>
diff --git a/opentech/apply/funds/tests/factories/models.py b/opentech/apply/funds/tests/factories/models.py
index 8a2854669..c8ff5de93 100644
--- a/opentech/apply/funds/tests/factories/models.py
+++ b/opentech/apply/funds/tests/factories/models.py
@@ -9,7 +9,9 @@ from opentech.apply.funds.models import (
     ApplicationRevision,
     FundType,
     LabType,
+    RequestForPartners,
     Round,
+    SealedRound,
 )
 from opentech.apply.funds.models.forms import (
     ApplicationForm,
@@ -33,6 +35,7 @@ __all__ = [
     'RoundBaseFormFactory',
     'LabFactory',
     'LabBaseFormFactory',
+    'SealedSubmissionFactory',
 ]
 
 
@@ -80,6 +83,11 @@ class FundTypeFactory(wagtail_factories.PageFactory):
                 ReviewFormFactory(**review_fields)
 
 
+class RequestForPartnersFactory(FundTypeFactory):
+    class Meta:
+        model = RequestForPartners
+
+
 class AbstractRelatedFormFactory(factory.DjangoModelFactory):
     class Meta:
         abstract = True
@@ -126,6 +134,10 @@ class RoundFactory(wagtail_factories.PageFactory):
                     **fields,
                 )
 
+class SealedRoundFactory(RoundFactory):
+    class Meta:
+        model = SealedRound
+
 
 class TodayRoundFactory(RoundFactory):
     start_date = factory.LazyFunction(datetime.date.today)
@@ -211,6 +223,16 @@ class ApplicationSubmissionFactory(factory.DjangoModelFactory):
         return super()._generate(strat, params)
 
 
+class SealedSubmissionFactory(ApplicationSubmissionFactory):
+    page = factory.SubFactory(RequestForPartnersFactory)
+    round = factory.SubFactory(
+        SealedRoundFactory,
+        workflow_name=factory.SelfAttribute('..workflow_name'),
+        lead=factory.SelfAttribute('..lead'),
+        now=True,
+    )
+
+
 class ApplicationRevisionFactory(factory.DjangoModelFactory):
     class Meta:
         model = ApplicationRevision
diff --git a/opentech/apply/funds/tests/test_views.py b/opentech/apply/funds/tests/test_views.py
index 65fa09bf9..78357c90f 100644
--- a/opentech/apply/funds/tests/test_views.py
+++ b/opentech/apply/funds/tests/test_views.py
@@ -1,7 +1,11 @@
 from datetime import datetime, timedelta
 
-from opentech.apply.funds.tests.factories import ApplicationSubmissionFactory, ApplicationRevisionFactory
-from opentech.apply.users.tests.factories import UserFactory, StaffFactory
+from opentech.apply.funds.tests.factories import (
+    ApplicationSubmissionFactory,
+    ApplicationRevisionFactory,
+    SealedSubmissionFactory,
+)
+from opentech.apply.users.tests.factories import UserFactory, StaffFactory, SuperUserFactory
 from opentech.apply.utils.testing.tests import BaseViewTestCase
 
 from ..models import ApplicationRevision
@@ -205,3 +209,55 @@ class TestRevisionList(BaseSubmissionViewTestCase):
             response.context['object_list'],
             [submission.live_revision, revision, revision_older],
         )
+
+
+class TestStaffSealedView(BaseSubmissionViewTestCase):
+    user_factory = StaffFactory
+
+    def test_redirected_to_sealed(self):
+        submission = SealedSubmissionFactory()
+        response = self.get_page(submission)
+        url = self.url_from_pattern('funds:submissions:sealed', kwargs={'pk': submission.id})
+        self.assertRedirects(response, url)
+
+    def test_cant_post_to_sealed(self):
+        submission = SealedSubmissionFactory()
+        response = self.post_page(submission, {'some': 'data'}, 'sealed')
+        url = self.url_from_pattern('funds:submissions:sealed', kwargs={'pk': submission.id})
+        self.assertRedirects(response, url)
+
+    def test_non_sealed_unaffected(self):
+        submission = ApplicationSubmissionFactory()
+        response = self.get_page(submission)
+        self.assertEqual(response.status_code, 200)
+
+    def test_non_sealed_redirected_away(self):
+        submission = ApplicationSubmissionFactory()
+        response = self.get_page(submission, 'sealed')
+        url = self.url_from_pattern('funds:submissions:detail', kwargs={'pk': submission.id})
+        self.assertRedirects(response, url)
+
+
+class TestSuperUserSealedView(BaseSubmissionViewTestCase):
+    user_factory = SuperUserFactory
+
+    def test_redirected_to_sealed(self):
+        submission = SealedSubmissionFactory()
+        response = self.get_page(submission)
+        url = self.url_from_pattern('funds:submissions:sealed', kwargs={'pk': submission.id})
+        self.assertRedirects(response, url)
+
+    def test_can_post_to_sealed(self):
+        submission = SealedSubmissionFactory()
+        response = self.post_page(submission, {}, 'sealed')
+        url = self.url_from_pattern('funds:submissions:detail', kwargs={'pk': submission.id})
+        self.assertRedirects(response, url)
+
+    def test_not_asked_again(self):
+        submission = SealedSubmissionFactory()
+
+        self.post_page(submission, {}, 'sealed')
+
+        # Now request the page again
+        response = self.get_page(submission)
+        self.assertEqual(response.status_code, 200)
diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py
index 613bb1a12..05980acf8 100644
--- a/opentech/apply/funds/views.py
+++ b/opentech/apply/funds/views.py
@@ -7,7 +7,9 @@ from django.shortcuts import get_object_or_404
 from django.urls import reverse_lazy
 from django.utils.decorators import method_decorator
 from django.utils.text import mark_safe
-from django.views.generic import DetailView, TemplateView, ListView, UpdateView
+from django.views.generic import DetailView, ListView, UpdateView
+from django.views.generic.detail import SingleObjectMixin
+from django.views.generic.edit import ProcessFormView
 
 from django_filters.views import FilterView
 from django_tables2.views import SingleTableMixin
@@ -152,9 +154,8 @@ class AdminSubmissionDetailView(ReviewContextMixin, ActivityContextMixin, Delega
 
     def dispatch(self, request, *args, **kwargs):
         submission = self.get_object()
-        if submission.round.specific.is_sealed:
-            return HttpResponseRedirect(reverse_lazy('funds:submissions:sealed', args=(submission.id,)))
-        return super().dispatch(request, *args, **kwargs)
+        redirect = SubmissionSealedView.should_redirect(request, submission)
+        return redirect or super().dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
         other_submissions = self.model.objects.filter(user=self.object.user).current().exclude(id=self.object.id)
@@ -172,6 +173,52 @@ class SubmissionSealedView(DetailView):
     template_name = 'funds/submission_sealed.html'
     model = ApplicationSubmission
 
+    def get(self, request, *args, **kwargs):
+        submission = self.get_object()
+        if not self.round_is_sealed(submission):
+            return self.redirect_detail(submission)
+        return super().get(request, *args, **kwargs)
+
+    def post(self, request, *args, **kwargs):
+        submission = self.get_object()
+        if self.can_view_sealed(request.user):
+            self.peeked(submission)
+        return self.redirect_detail(submission)
+
+    def redirect_detail(self, submission):
+        return HttpResponseRedirect(reverse_lazy('funds:submissions:detail', args=(submission.id,)))
+
+    def peeked(self, submission):
+        messenger(
+            MESSAGES.OPENED_SEALED,
+            request=self.request,
+            user=self.request.user,
+            submission=submission,
+        )
+        self.request.session.setdefault('peeked', {})[str(submission.id)] = True
+
+    def can_view_sealed(self, user):
+        return user.is_superuser
+
+    def get_context_data(self, **kwargs):
+        return super().get_context_data(
+            can_view_sealed=self.can_view_sealed(self.request.user),
+            **kwargs,
+        )
+
+    @classmethod
+    def round_is_sealed(cls, submission):
+        return submission.round.specific.is_sealed
+
+    @classmethod
+    def has_peeked(cls, request, submission):
+        return str(submission.id) in request.session.get('peeked', {})
+
+    @classmethod
+    def should_redirect(cls, request, submission):
+        if cls.round_is_sealed(submission) and not cls.has_peeked(request, submission):
+            return HttpResponseRedirect(reverse_lazy('funds:submissions:sealed', args=(submission.id,)))
+
 
 class ApplicantSubmissionDetailView(ActivityContextMixin, DelegateableView):
     model = ApplicationSubmission
diff --git a/opentech/apply/users/tests/factories.py b/opentech/apply/users/tests/factories.py
index 1ee80d85f..e474e940e 100644
--- a/opentech/apply/users/tests/factories.py
+++ b/opentech/apply/users/tests/factories.py
@@ -44,6 +44,7 @@ class AdminFactory(UserFactory):
 class StaffFactory(UserFactory):
     class Meta:
         exclude = ('slack_temp', )
+    is_staff = True
 
     # Required to generate the fake data add pass to LazyAttribute
     slack_temp = factory.Faker('word')
@@ -56,6 +57,10 @@ class StaffFactory(UserFactory):
             self.groups.add(GroupFactory(name=STAFF_GROUP_NAME))
 
 
+class SuperUserFactory(StaffFactory):
+    is_superuser = True
+
+
 class ReviewerFactory(UserFactory):
     @factory.post_generation
     def groups(self, create, extracted, **kwargs):
diff --git a/opentech/apply/utils/testing/tests.py b/opentech/apply/utils/testing/tests.py
index 2c85f1be5..9d3bad6ac 100644
--- a/opentech/apply/utils/testing/tests.py
+++ b/opentech/apply/utils/testing/tests.py
@@ -37,6 +37,7 @@ class BaseViewTestCase(TestCase):
             return url
 
         request = self.factory.get(url, secure=True)
+        # request._session = {}
         return request.build_absolute_uri()
 
     def url_from_pattern(self, pattern, kwargs=None):
@@ -44,7 +45,7 @@ class BaseViewTestCase(TestCase):
         request = self.factory.get(url, secure=True)
         return request.build_absolute_uri()
 
-    def get_page(self, instance=None, view_name=None):
+    def get_page(self, instance=None, view_name=None,):
         return self.client.get(self.url(instance, view_name), secure=True, follow=True)
 
     def post_page(self, instance=None, data=dict(), view_name=None):
-- 
GitLab