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