From 3b5133471baf8d5f98733f08ac290aa16769b05f Mon Sep 17 00:00:00 2001 From: Wes Appler <145372368+wes-otf@users.noreply.github.com> Date: Mon, 6 May 2024 08:21:20 -0400 Subject: [PATCH] Visibility fixes for partners that are also applicants (#3893) Fixes #3892. This solution was a bit unruly (especially ugly after linting) but it seemed like the logical approach without refactoring the whole `visibility_for` to be a normal method rather than a class method. This fix evaluates if the partner/reviewer is the author of the source submission first before evaluating the their role. The reviewer role was also lumped in here because even though I know we advise against a reviewer also being an applicant, it still could happen without oversight. I tried to keep the code as clean as I could and reuse QuerySets, but let me know if y'all see any way I can optimize! --- hypha/apply/activity/models.py | 76 ++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/hypha/apply/activity/models.py b/hypha/apply/activity/models.py index 016b3d311..d91371ae6 100644 --- a/hypha/apply/activity/models.py +++ b/hypha/apply/activity/models.py @@ -2,6 +2,7 @@ import os import uuid from typing import List, Optional, Tuple +from django.apps import apps from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType @@ -64,18 +65,58 @@ class BaseActivityQuerySet(models.QuerySet): messages = ActivityAdapter.messages + user_qs = Q(user=user) + # There are scenarios where users will have activities in which they # wouldn't have visibility just using Activity.visibility_for. Thus, - # the queryset should include activity in which they author - # (ie. A comment made only to staff from a partner). + # the queryset should include activity in which they author via + # `user_qs` (ie. A comment made only to staff from a partner). if user.is_applicant: - return self.exclude(message=messages.get(MESSAGES.NEW_REVIEW)).filter( - Q(visibility__in=self.model.visibility_for(user)) | Q(user=user) - ) + # Handle the edge case where a partner or reviewer is also an + # applicant. Ensures that any applications/projects the user + # authored will have comment visibility of applicant while others + # will get the appropriate role. + if user.is_partner or user.is_reviewer: + ApplicationSubmission = apps.get_model("funds", "ApplicationSubmission") + Project = apps.get_model("application_projects", "Project") + + app_content_type = ContentType.objects.get_for_model( + ApplicationSubmission + ) + proj_content_type = ContentType.objects.get_for_model(Project) + + authored_apps = ApplicationSubmission.objects.filter(user_qs).values( + "id" + ) + authored_projs = Project.objects.filter(user_qs).values("id") + + proj_app_qs = ( + Q(source_content_type=app_content_type) + & Q(source_object_id__in=authored_apps) + ) | ( + Q(source_content_type=proj_content_type) + & Q(source_object_id__in=authored_projs) + ) + + # Activities the user is the author of the source submission + applicant_activity = self.filter( + proj_app_qs + & Q( + Q(visibility__in=self.model.visibility_for(user, True)) + | user_qs + ) + ) + # All other activities + other_activity = self.exclude( + Q(message=messages.get(MESSAGES.NEW_REVIEW)) | proj_app_qs + ).filter(Q(visibility__in=self.model.visibility_for(user)) | user_qs) + return applicant_activity | other_activity + else: + return self.exclude(message=messages.get(MESSAGES.NEW_REVIEW)).filter( + Q(visibility__in=self.model.visibility_for(user)) | user_qs + ) - return self.filter( - Q(visibility__in=self.model.visibility_for(user)) | Q(user=user) - ) + return self.filter(Q(visibility__in=self.model.visibility_for(user)) | user_qs) def newer(self, activity): return self.filter(timestamp__gt=activity.timestamp) @@ -213,27 +254,38 @@ class Activity(models.Model): return '{}: for "{}"'.format(self.get_type_display(), self.source) @classmethod - def visibility_for(cls, user) -> List[str]: + def visibility_for( + cls, user, is_submission_author: Optional[bool] = False + ) -> List[str]: """Gets activity visibility for a specified user + Takes an optional boolean that is used to determine the visibility of + an application comment. This was mainly implemented to allow partners + also holding the role of applicant to have a proper visibility. + + ie. Prevent someone with the role of partner & applicant looking at + comments on their own application and seeing partner visibility + Args: user: [`User`][hypha.apply.users.models.User] to get visibility for + is_submission_author: + boolean used when the `user` is the applicant of the source activity Returns: A list of visibility strings """ if user.is_apply_staff: return [TEAM, APPLICANT, REVIEWER, APPLICANT_PARTNERS, PARTNER, ALL] - if user.is_reviewer: + if user.is_reviewer and not is_submission_author: return [REVIEWER, ALL] if user.is_finance or user.is_contracting: # for project part return [TEAM, APPLICANT, REVIEWER, PARTNER, ALL] + if user.is_partner and not is_submission_author: + return [PARTNER, ALL, APPLICANT_PARTNERS] if user.is_applicant: return [APPLICANT, ALL, APPLICANT_PARTNERS] - if user.is_partner: - return [PARTNER, ALL, APPLICANT_PARTNERS] return [ALL] -- GitLab