Skip to content

Commit

Permalink
feat: Update "Get target subjects by respondent" endpoint to include …
Browse files Browse the repository at this point in the history
…`teamMemberCanViewData` property (M2-8464) (#1702)

This PR updates the structure of the subjects being returned by the "Get target subjects by respondent" endpoint. Each subject now includes a `teamMemberCanViewData` property, which indicates whether the logged-in admin user calling the endpoint can view the data of that particular subject.
  • Loading branch information
sultanofcardio authored Jan 8, 2025
1 parent 80d54be commit adbe344
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 2 deletions.
15 changes: 14 additions & 1 deletion src/apps/subjects/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from apps.invitations.errors import NonUniqueValue
from apps.invitations.services import InvitationsService
from apps.shared.domain import Response, ResponseMulti
from apps.shared.exception import NotFoundError, ValidationError
from apps.shared.exception import AccessDeniedError, NotFoundError, ValidationError
from apps.shared.response import EmptyResponse
from apps.shared.subjects import is_take_now_relation, is_valid_take_now_relation
from apps.subjects.domain import (
Expand All @@ -33,6 +33,7 @@
from apps.users import User
from apps.users.services.user import UserService
from apps.workspaces.domain.constants import Role
from apps.workspaces.service.applet_access import AppletAccessService
from apps.workspaces.service.check_access import CheckAccessService
from apps.workspaces.service.user_access import UserAccessService
from apps.workspaces.service.user_applet_access import UserAppletAccessService
Expand Down Expand Up @@ -317,6 +318,12 @@ async def get_target_subjects_by_respondent(
respondent_subject.applet_id, respondent_subject.id
)

access = await AppletAccessService(session).get_priority_access(
applet_id=respondent_subject.applet_id, user_id=user.id
)
if not access:
raise AccessDeniedError()

assignment_service = ActivityAssignmentService(session)
assignment_subject_ids = await assignment_service.get_target_subject_ids_by_respondent(
respondent_subject_id=respondent_subject_id, activity_or_flow_ids=[activity_or_flow_id]
Expand Down Expand Up @@ -351,7 +358,12 @@ class SubjectInfo(TypedDict):

# Find the respondent subject in the list of subjects
respondent_target_subject: TargetSubjectByRespondentResponse | None = None
is_super_reviewer = access.role in Role.super_reviewers()
for subject in subjects:
can_view_data = is_super_reviewer or (
access.role == Role.REVIEWER and str(subject.id) in access.meta.get("subjects", [])
)

target_subject = TargetSubjectByRespondentResponse(
secret_user_id=subject.secret_user_id,
nickname=subject.nickname,
Expand All @@ -363,6 +375,7 @@ class SubjectInfo(TypedDict):
last_name=subject.last_name,
submission_count=subject_info[subject.id]["submission_count"],
currently_assigned=subject_info[subject.id]["currently_assigned"],
team_member_can_view_data=can_view_data,
)

if subject.id == respondent_subject_id:
Expand Down
1 change: 1 addition & 0 deletions src/apps/subjects/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class SubjectReadResponse(SubjectUpdateRequest):
class TargetSubjectByRespondentResponse(SubjectReadResponse):
submission_count: int = 0
currently_assigned: bool = False
team_member_can_view_data: bool = False


class SubjectRelation(InternalModel):
Expand Down
164 changes: 163 additions & 1 deletion src/apps/subjects/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,7 @@ async def test_get_target_subjects_by_respondent_no_assignments_or_submissions(
assert subject_result["id"] == str(lucy_applet_one_subject.id)
assert subject_result["submissionCount"] == 0
assert subject_result["currentlyAssigned"] is True
assert subject_result["teamMemberCanViewData"] is True

async def test_get_target_subjects_by_respondent_manual_assignment(
self,
Expand Down Expand Up @@ -944,6 +945,7 @@ async def test_get_target_subjects_by_respondent_manual_assignment(
assert subject_result["id"] == str(tom_applet_one_subject.id)
assert subject_result["submissionCount"] == 0
assert subject_result["currentlyAssigned"] is True
assert subject_result["teamMemberCanViewData"] is True

async def test_get_target_subjects_by_respondent_excludes_deleted_assignment(
self,
Expand Down Expand Up @@ -1067,15 +1069,17 @@ async def test_get_target_subjects_by_respondent_multiple_assignments(
assert tom_result["id"] == str(tom_applet_one_subject.id)
assert tom_result["submissionCount"] == 0
assert tom_result["currentlyAssigned"] is True
assert tom_result["teamMemberCanViewData"] is True

shell_account_result = result[1]

assert shell_account_result["id"] == str(applet_one_shell_account.id)
assert shell_account_result["submissionCount"] == 0
assert shell_account_result["currentlyAssigned"] is True
assert shell_account_result["teamMemberCanViewData"] is True

@pytest.mark.parametrize("subject_type", ["target", "respondent"])
async def test_get_target_subjects_by_respondent_via_submission(
async def test_get_target_subjects_by_respondent_via_submission_as_owner(
self,
client,
tom: User,
Expand Down Expand Up @@ -1130,3 +1134,161 @@ async def test_get_target_subjects_by_respondent_via_submission(
assert tom_result["id"] == str(tom_applet_one_subject.id)
assert tom_result["submissionCount"] == 1
assert tom_result["currentlyAssigned"] is False
assert tom_result["teamMemberCanViewData"] is True

async def test_get_target_subjects_by_respondent_via_submission_as_reviewer(
self,
client,
tom: User,
pit: User,
tom_applet_one_subject: Subject,
applet_one_shell_account: Subject,
applet_one_pit_reviewer: AppletFull,
answer_create_payload: dict,
session: AsyncSession,
):
activity = applet_one_pit_reviewer.activities[0]

# Turn off auto-assignment
activity_service = ActivityService(session, tom.id)
await activity_service.remove_applet_activities(applet_one_pit_reviewer.id)
await activity_service.update_create(
applet_one_pit_reviewer.id,
[
ActivityUpdate(
**activity.dict(exclude={"auto_assign"}),
auto_assign=False,
)
],
)

# Assign pit as a reviewer to tom
await UserAppletAccessService(session, tom.id, applet_one_pit_reviewer.id).set_subjects_for_review(
reviewer_id=pit.id, applet_id=applet_one_pit_reviewer.id, subjects=[tom_applet_one_subject.id]
)

filtered_answer_create_payload = {k: v for k, v in answer_create_payload.items() if k != "submit_id"}

# Self-report answer
await AnswerService(session, tom.id).create_answer(
AppletAnswerCreate(
**filtered_answer_create_payload,
submit_id=str(uuid.uuid4()),
input_subject_id=tom_applet_one_subject.id,
source_subject_id=tom_applet_one_subject.id,
target_subject_id=tom_applet_one_subject.id,
)
)

# Multi-informant answer
await AnswerService(session, tom.id).create_answer(
AppletAnswerCreate(
**filtered_answer_create_payload,
submit_id=str(uuid.uuid4()),
input_subject_id=tom_applet_one_subject.id,
source_subject_id=tom_applet_one_subject.id,
target_subject_id=applet_one_shell_account.id,
)
)

client.login(pit)

url = self.subject_target_by_respondent_url.format(
respondent_subject_id=tom_applet_one_subject.id, activity_or_flow_id=str(activity.id)
)
response = await client.get(url)
assert response.status_code == http.HTTPStatus.OK

result = response.json()["result"]

assert len(result) == 2

tom_result = result[0]

assert tom_result["id"] == str(tom_applet_one_subject.id)
assert tom_result["submissionCount"] == 1
assert tom_result["currentlyAssigned"] is False
assert tom_result["teamMemberCanViewData"] is True

shell_account_result = result[1]

assert shell_account_result["id"] == str(applet_one_shell_account.id)
assert shell_account_result["submissionCount"] == 1
assert shell_account_result["currentlyAssigned"] is False
assert shell_account_result["teamMemberCanViewData"] is False

async def test_get_target_subjects_by_respondent_via_submission_as_coordinator(
self,
client,
tom: User,
bob: User,
tom_applet_one_subject: Subject,
applet_one_shell_account: Subject,
applet_one_bob_coordinator: AppletFull,
answer_create_payload: dict,
session: AsyncSession,
):
activity = applet_one_bob_coordinator.activities[0]

# Turn off auto-assignment
activity_service = ActivityService(session, tom.id)
await activity_service.remove_applet_activities(applet_one_bob_coordinator.id)
await activity_service.update_create(
applet_one_bob_coordinator.id,
[
ActivityUpdate(
**activity.dict(exclude={"auto_assign"}),
auto_assign=False,
)
],
)

filtered_answer_create_payload = {k: v for k, v in answer_create_payload.items() if k != "submit_id"}

# Self-report answer
await AnswerService(session, tom.id).create_answer(
AppletAnswerCreate(
**filtered_answer_create_payload,
submit_id=str(uuid.uuid4()),
input_subject_id=tom_applet_one_subject.id,
source_subject_id=tom_applet_one_subject.id,
target_subject_id=tom_applet_one_subject.id,
)
)

# Multi-informant answer
await AnswerService(session, tom.id).create_answer(
AppletAnswerCreate(
**filtered_answer_create_payload,
submit_id=str(uuid.uuid4()),
input_subject_id=tom_applet_one_subject.id,
source_subject_id=tom_applet_one_subject.id,
target_subject_id=applet_one_shell_account.id,
)
)

client.login(bob)

url = self.subject_target_by_respondent_url.format(
respondent_subject_id=tom_applet_one_subject.id, activity_or_flow_id=str(activity.id)
)
response = await client.get(url)
assert response.status_code == http.HTTPStatus.OK

result = response.json()["result"]

assert len(result) == 2

tom_result = result[0]

assert tom_result["id"] == str(tom_applet_one_subject.id)
assert tom_result["submissionCount"] == 1
assert tom_result["currentlyAssigned"] is False
assert tom_result["teamMemberCanViewData"] is False

shell_account_result = result[1]

assert shell_account_result["id"] == str(applet_one_shell_account.id)
assert shell_account_result["submissionCount"] == 1
assert shell_account_result["currentlyAssigned"] is False
assert shell_account_result["teamMemberCanViewData"] is False
30 changes: 30 additions & 0 deletions src/apps/workspaces/service/applet_access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import uuid

from apps.applets.domain import UserAppletAccess
from apps.workspaces.crud.applet_access import AppletAccessCRUD

__all__ = ["AppletAccessService"]


class AppletAccessService:
def __init__(self, session):
self.session = session

async def get_priority_access(self, applet_id: uuid.UUID, user_id: uuid.UUID) -> UserAppletAccess | None:
"""
Get the user's access to an applet with the most permissions. Returns accesses in this order:
1. Owner
2. Manager
3. Coordinator
4. Editor
5. Reviewer
6. Respondent
:param applet_id:
:param user_id:
:return:
"""
schema = await AppletAccessCRUD(self.session).get_priority_access(applet_id, user_id)
if not schema:
return None

return UserAppletAccess.from_orm(schema)

0 comments on commit adbe344

Please sign in to comment.