Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/setup/administrators/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ Should Applicant identities be obscured from External Reviewers

----

Should applicants be able to see the detailed answers of the determinations on their own applications. The determination message is always shown to them.

DETERMINATION_DETAILS_ACCESS_APPLICANT = env.bool('DETERMINATION_DETAILS_ACCESS_APPLICANT', True)

----

Should staff be able to access/see draft submissions.

SUBMISSIONS_DRAFT_ACCESS_STAFF = env.bool('SUBMISSIONS_DRAFT_ACCESS_STAFF', False)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,30 @@ <h2 class="pb-1 mb-2 font-medium border-b text-h3 border-base-300 question">
</div>
</section>

{% for group in determination.detailed_data.values %}
<section>
{% if group.title %}
<h2 class="section-header">{{ group.title|nh3 }}</h2>
{% endif %}
{% if show_detailed_data %}
{% for group in determination.detailed_data.values %}
<section>
{% if group.title %}
<h2 class="section-header">{{ group.title|nh3 }}</h2>
{% endif %}

{% for question, answer in group.questions %}
<h3 class="pb-1 mb-2 font-medium border-b text-h3 border-base-300 question">{{ question }}</h3>
{% if answer %}
{% if answer == True or answer == False %}
{{ answer|yesno:_("Agree,Disagree") }}
{% for question, answer in group.questions %}
<h3 class="pb-1 mb-2 font-medium border-b text-h3 border-base-300 question">{{ question }}</h3>
{% if answer %}
{% if answer == True or answer == False %}
{{ answer|yesno:_("Agree,Disagree") }}
{% else %}
<div class="max-w-none prose">
{{ answer|nh3 }}
</div>
{% endif %}
{% else %}
<div class="max-w-none prose">
{{ answer|nh3 }}
</div>
-
{% endif %}
{% else %}
-
{% endif %}
{% endfor %}
<section>
{% endfor %}
{% endfor %}
</section>
{% endfor %}
{% endif %}

</div>
{% endblock %}
115 changes: 114 additions & 1 deletion hypha/apply/determinations/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,36 @@
from hypha.apply.activity.models import Activity
from hypha.apply.determinations.options import ACCEPTED, NEEDS_MORE_INFO, REJECTED
from hypha.apply.determinations.views import BatchDeterminationCreateView
from hypha.apply.funds.models.co_applicants import (
CoApplicant,
CoApplicantInvite,
CoApplicantInviteStatus,
)
from hypha.apply.funds.tests.factories import ApplicationSubmissionFactory
from hypha.apply.projects.models.project import CONTRACTING, DRAFT
from hypha.apply.users.tests.factories import StaffFactory, UserFactory
from hypha.apply.users.roles import APPLICANT_GROUP_NAME
from hypha.apply.users.tests.factories import (
ApplicantFactory,
GroupFactory,
ReviewerFactory,
StaffFactory,
UserFactory,
)
from hypha.apply.utils.testing import BaseViewTestCase

from .factories import DeterminationFactory


def make_co_applicant(submission, user):
"""Create a CoApplicant on the submission."""
invite = CoApplicantInvite.objects.create(
submission=submission,
invited_user_email=user.email,
status=CoApplicantInviteStatus.ACCEPTED,
)
return CoApplicant.objects.create(submission=submission, user=user, invite=invite)


class StaffDeterminationsTestCase(BaseViewTestCase):
user_factory = StaffFactory
url_name = "funds:submissions:determinations:{}"
Expand All @@ -34,6 +56,16 @@ def test_can_access_determination(self):
self.assertContains(response, self.user.full_name)
self.assertContains(response, submission.get_absolute_url())

@override_settings(DETERMINATION_DETAILS_ACCESS_APPLICANT=False)
def test_can_see_detailed_data_when_hidden_from_applicants(self):
submission = ApplicationSubmissionFactory(status="in_discussion")
determination = DeterminationFactory(
submission=submission, author=self.user, submitted=True
)
response = self.get_page(determination)
self.assertTrue(response.context["show_detailed_data"])
self.assertContains(response, "Goals and principles")

def test_lead_can_access_determination(self):
submission = ApplicationSubmissionFactory(
status="in_discussion", lead=self.user
Expand Down Expand Up @@ -562,6 +594,87 @@ def test_message_created_if_determination_exists(self):
self.assertEqual(len(response.context["messages"]), 5)


class ApplicantDeterminationDetailTestCase(BaseViewTestCase):
user_factory = ApplicantFactory
url_name = "funds:submissions:determinations:{}"
base_view_name = "detail"

def get_kwargs(self, instance):
return {"submission_pk": instance.submission.id, "pk": instance.pk}

def determination_for_user(self):
submission = ApplicationSubmissionFactory(
status="in_discussion", user=self.user
)
return DeterminationFactory(submission=submission, submitted=True)

def test_can_see_detailed_data_by_default(self):
determination = self.determination_for_user()
response = self.get_page(determination)
self.assertTrue(response.context["show_detailed_data"])
self.assertContains(response, "Goals and principles")

@override_settings(DETERMINATION_DETAILS_ACCESS_APPLICANT=False)
def test_cant_see_detailed_data_when_disabled(self):
determination = self.determination_for_user()
response = self.get_page(determination)
self.assertFalse(response.context["show_detailed_data"])
self.assertNotContains(response, "Goals and principles")
# The determination message is always shown to the applicant.
self.assertContains(response, determination.message)


class ReviewerDeterminationDetailTestCase(BaseViewTestCase):
user_factory = ReviewerFactory
url_name = "funds:submissions:determinations:{}"
base_view_name = "detail"

def get_kwargs(self, instance):
return {"submission_pk": instance.submission.id, "pk": instance.pk}

def determination_for_user(self):
"""A determination on the reviewer's own application.

Users can hold both roles; ViewDispatcher routes them to the reviewer
view because it checks `is_reviewer` before `is_applicant`.
"""
self.user.groups.add(GroupFactory(name=APPLICANT_GROUP_NAME))
submission = ApplicationSubmissionFactory(
status="in_discussion", user=self.user
)
return DeterminationFactory(submission=submission, submitted=True)

def test_can_see_detailed_data_on_other_submissions(self):
determination = DeterminationFactory(
submission=ApplicationSubmissionFactory(status="in_discussion"),
submitted=True,
)
response = self.get_page(determination)
self.assertTrue(response.context["show_detailed_data"])

def test_can_see_detailed_data_on_own_submission_by_default(self):
determination = self.determination_for_user()
response = self.get_page(determination)
self.assertTrue(response.context["show_detailed_data"])

@override_settings(DETERMINATION_DETAILS_ACCESS_APPLICANT=False)
def test_cant_see_detailed_data_on_own_submission_when_disabled(self):
determination = self.determination_for_user()
response = self.get_page(determination)
self.assertFalse(response.context["show_detailed_data"])
self.assertNotContains(response, "Goals and principles")
# The determination message is always shown to the applicant.
self.assertContains(response, determination.message)

@override_settings(DETERMINATION_DETAILS_ACCESS_APPLICANT=False)
def test_cant_see_detailed_data_as_co_applicant_when_disabled(self):
submission = ApplicationSubmissionFactory(status="in_discussion")
make_co_applicant(submission, self.user)
determination = DeterminationFactory(submission=submission, submitted=True)
response = self.get_page(determination)
self.assertFalse(response.context["show_detailed_data"])


class UserDeterminationFormTestCase(BaseViewTestCase):
user_factory = UserFactory
url_name = "funds:submissions:determinations:{}"
Expand Down
47 changes: 35 additions & 12 deletions hypha/apply/determinations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from hypha.apply.activity.messaging import MESSAGES, messenger
from hypha.apply.activity.models import Activity
from hypha.apply.funds.models import ApplicationSubmission
from hypha.apply.funds.permissions import is_submission_applicant
from hypha.apply.funds.workflows import DETERMINATION_OUTCOMES
from hypha.apply.funds.workflows.models.stage import Concept
from hypha.apply.projects.models import Project
Expand Down Expand Up @@ -527,8 +528,23 @@ def should_redirect(cls, request, submission, action):
)


class DeterminationDetailedDataMixin:
"""Controls whether the determination's detailed answers are rendered.

The determination message is always shown, the answers to the individual
determination form questions are opt-out for applicants.
"""

show_detailed_data = True

def get_context_data(self, **kwargs):
return super().get_context_data(
show_detailed_data=self.show_detailed_data, **kwargs
)


@method_decorator(staff_required, name="dispatch")
class AdminDeterminationDetailView(DetailView):
class AdminDeterminationDetailView(DeterminationDetailedDataMixin, DetailView):
model = Determination

def get_object(self, queryset=None):
Expand Down Expand Up @@ -556,9 +572,18 @@ def dispatch(self, request, *args, **kwargs):


@method_decorator(login_required, name="dispatch")
class ReviewerDeterminationDetailView(DetailView):
class ReviewerDeterminationDetailView(DeterminationDetailedDataMixin, DetailView):
model = Determination

@property
def show_detailed_data(self):
# Reviewers are routed here ahead of the applicant view, so a reviewer
# looking at a determination on their own application is still subject
# to the applicant setting.
if is_submission_applicant(self.request.user, self.submission):
return settings.DETERMINATION_DETAILS_ACCESS_APPLICANT
return True

def get_object(self, queryset=None):
return get_object_or_404(
self.model, submission=self.submission, id=self.kwargs["pk"]
Expand All @@ -579,7 +604,7 @@ def dispatch(self, request, *args, **kwargs):


@method_decorator(login_required, name="dispatch")
class CommunityDeterminationDetailView(DetailView):
class CommunityDeterminationDetailView(DeterminationDetailedDataMixin, DetailView):
model = Determination

def get_queryset(self):
Expand All @@ -601,9 +626,13 @@ def dispatch(self, request, *args, **kwargs):


@method_decorator(login_required, name="dispatch")
class ApplicantDeterminationDetailView(DetailView):
class ApplicantDeterminationDetailView(DeterminationDetailedDataMixin, DetailView):
model = Determination

@property
def show_detailed_data(self):
return settings.DETERMINATION_DETAILS_ACCESS_APPLICANT

def get_object(self, queryset=None):
return get_object_or_404(
self.model, submission=self.submission, id=self.kwargs["pk"]
Expand All @@ -615,18 +644,12 @@ def dispatch(self, request, *args, **kwargs):
)
determination = self.get_object()

if (
request.user != self.submission.user
and not self.submission.co_applicants.filter(user=request.user).exists
):
if not is_submission_applicant(request.user, self.submission):
raise PermissionDenied

if determination.is_draft:
return HttpResponseRedirect(
reverse_lazy(
"apply:submissions:determinations:detail",
args=(self.submission.id,),
)
reverse_lazy("apply:submissions:detail", args=(self.submission.id,))
)

return super().dispatch(request, *args, **kwargs)
Expand Down
13 changes: 8 additions & 5 deletions hypha/apply/funds/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ def has_permission(action, user, object=None, raise_exception=True):
return value, reason


def is_submission_applicant(user, submission) -> bool:
"""Is the user the applicant, or a co-applicant, on this submission."""
return (
user == submission.user or submission.co_applicants.filter(user=user).exists()
)


def can_take_submission_actions(user, submission):
if not user.is_authenticated:
return False, _("Login Required")
Expand Down Expand Up @@ -214,11 +221,7 @@ def can_view_submission(user, submission):
if submission.is_archive and not can_view_archived_submissions(user):
return False, _("Archived Submission")

if (
user.is_apply_staff
or submission.user == user
or submission.co_applicants.filter(user=user).exists()
):
if user.is_apply_staff or is_submission_applicant(user, submission):
return True, ""

# By default, reviewers can see all submissions. This can be configured in Wagtail Admin > Apply > Reviewer Settings
Expand Down
16 changes: 4 additions & 12 deletions hypha/apply/funds/views/submission_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
can_alter_archived_submissions,
get_archive_view_groups,
has_permission,
is_submission_applicant,
)
from ..workflows import DRAFT_STATE

Expand Down Expand Up @@ -129,10 +130,7 @@ def dispatch(self, request, *args, **kwargs):
# If the requesting user submitted the application, return the Applicant view.
# Reviewers may sometimes be applicants as well.
# or if requesting user is a co-applicant to application, return the Applicant view.
if (
submission.user == request.user
or submission.co_applicants.filter(user=request.user).exists()
):
if is_submission_applicant(request.user, submission):
return ApplicantSubmissionDetailView.as_view()(request, *args, **kwargs)
if submission.status == DRAFT_STATE:
raise Http404
Expand Down Expand Up @@ -161,10 +159,7 @@ def dispatch(self, request, *args, **kwargs):
# If the requesting user submitted the application, return the Applicant view.
# Reviewers may sometimes be applicants as well.
# or if requesting user is a co-applicant to application, return the Applicant view.
if (
submission.user == request.user
or submission.co_applicants.filter(user=request.user).exists()
):
if is_submission_applicant(request.user, submission):
return ApplicantSubmissionDetailView.as_view()(request, *args, **kwargs)
# Only allow community reviewers in submission with a community review state.
if not submission.community_review:
Expand All @@ -188,10 +183,7 @@ def dispatch(self, request, *args, **kwargs):
"submission_view", request.user, object=submission, raise_exception=True
)
# This view is only for applicants and co-applicants.
if (
submission.user != request.user
and not submission.co_applicants.filter(user=request.user).exists()
):
if not is_submission_applicant(request.user, submission):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)

Expand Down
6 changes: 2 additions & 4 deletions hypha/apply/funds/views/submission_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from ..models import ApplicationSubmission
from ..permissions import (
has_permission,
is_submission_applicant,
)
from ..workflows.constants import (
DRAFT_STATE,
Expand Down Expand Up @@ -324,10 +325,7 @@ def buttons( # type: ignore[return]
class ApplicantSubmissionEditView(BaseSubmissionEditView):
def dispatch(self, request, *args, **kwargs):
submission = self.get_object()
if (
request.user != submission.user
and not submission.co_applicants.filter(user=request.user).exists()
):
if not is_submission_applicant(request.user, submission):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)

Expand Down
Loading