From a7cfa5f902f97789164031da87ce3fb4733dfa6d Mon Sep 17 00:00:00 2001 From: Kenroy Gobourne Date: Mon, 4 Nov 2024 09:57:04 -0500 Subject: [PATCH] feat: Add Source subject Details to endpoints that return individual answer submissions (M2-8140) (#1645) This PR updates the following endpoints to add a nullable `sourceSubject` property to their `answer` objects - Get activity answer: `GET /answers/applet/{applet_id}/activities/{activity_id}/answers/{answer_id}` - Get flow submission: `GET /answers/applet/{applet_id}/flows/{flow_id}/submissions/{submit_id}` The structure of the `sourceSubject` is the same `SubjectReadResponse` as that returned from get subject endpoint (`GET /subjects/{subject_id}`). The only exception being that the `lastSeen` property will always be null in these cases. --- src/apps/answers/domain/answers.py | 2 + src/apps/answers/service.py | 44 ++++++++++- src/apps/answers/tests/test_answers.py | 102 ++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 5 deletions(-) diff --git a/src/apps/answers/domain/answers.py b/src/apps/answers/domain/answers.py index 0139168d4bb..0a0309d9491 100644 --- a/src/apps/answers/domain/answers.py +++ b/src/apps/answers/domain/answers.py @@ -21,6 +21,7 @@ from apps.shared.domain.custom_validations import datetime_from_ms from apps.shared.domain.types import _BaseModel from apps.shared.locale import I18N +from apps.subjects.domain import SubjectReadResponse class ClientMeta(InternalModel): @@ -244,6 +245,7 @@ class ActivityAnswer(PublicModel): migrated_data: dict | None = None end_datetime: datetime.datetime created_at: datetime.datetime + source_subject: SubjectReadResponse | None @validator("activity_id", always=True) def extract_activity_id(cls, value, values): diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index b1da5e9e139..238a00a9540 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -99,6 +99,7 @@ from apps.subjects.constants import Relation from apps.subjects.crud import SubjectsCrud from apps.subjects.db.schemas import SubjectSchema +from apps.subjects.domain import SubjectReadResponse from apps.users import User, UserSchema, UsersCRUD from apps.workspaces.crud.applet_access import AppletAccessCRUD from apps.workspaces.crud.user_applet_access import UserAppletAccessCRUD @@ -627,6 +628,25 @@ async def get_activity_answer( raise AnswerNotFoundError() answer = answers[0] + source_subject = None + + if answer.source_subject_id: + source_subject_schema = await SubjectsCrud(self.session).get_by_id(answer.source_subject_id) + source_subject = ( + SubjectReadResponse( + id=source_subject_schema.id, + first_name=source_subject_schema.first_name, + last_name=source_subject_schema.last_name, + nickname=source_subject_schema.nickname, + secret_user_id=source_subject_schema.secret_user_id, + tag=source_subject_schema.tag, + applet_id=source_subject_schema.applet_id, + user_id=source_subject_schema.user_id, + ) + if source_subject_schema + else None + ) + answer_result = ActivityAnswer( **answer.dict(exclude={"migrated_data"}), **answer.answer_item.dict( @@ -640,6 +660,7 @@ async def get_activity_answer( "end_datetime", } ), + source_subject=source_subject, ) activities = await ActivityHistoriesCRUD(self.session).load_full([answer.activity_history_id]) @@ -744,8 +765,12 @@ async def get_flow_submission( answer_result: list[ActivityAnswer] = [] + source_subject_id_answer_index_map: dict[uuid.UUID, list[int]] = defaultdict(list) + is_flow_completed = False - for answer in answers: + for i, answer in enumerate(answers): + if answer.source_subject_id: + source_subject_id_answer_index_map[answer.source_subject_id].append(i) if answer.flow_history_id and answer.is_flow_completed: is_completed = True answer_result.append( @@ -777,6 +802,23 @@ async def get_flow_submission( flows = await FlowsHistoryCRUD(self.session).load_full([flow_history_id]) assert flows + source_subject_ids = list(source_subject_id_answer_index_map.keys()) + source_subjects = await SubjectsCrud(self.session).get_by_ids(source_subject_ids) + for source_subject_schema in source_subjects: + answer_indexes = source_subject_id_answer_index_map[source_subject_schema.id] + source_subject = SubjectReadResponse( + id=source_subject_schema.id, + first_name=source_subject_schema.first_name, + last_name=source_subject_schema.last_name, + nickname=source_subject_schema.nickname, + secret_user_id=source_subject_schema.secret_user_id, + tag=source_subject_schema.tag, + applet_id=source_subject_schema.applet_id, + user_id=source_subject_schema.user_id, + ) + for answer_index in answer_indexes: + answer_result[answer_index].source_subject = source_subject + submission = FlowSubmissionDetails( submission=FlowSubmission( submit_id=submit_id, diff --git a/src/apps/answers/tests/test_answers.py b/src/apps/answers/tests/test_answers.py index a8c621c3d87..0e3928e37d6 100644 --- a/src/apps/answers/tests/test_answers.py +++ b/src/apps/answers/tests/test_answers.py @@ -1471,7 +1471,9 @@ async def test_answered_applet_activities_1( assert set(data["summary"]["identifier"]) == {"lastAnswerDate", "identifier", "userPublicKey"} assert data["summary"]["identifier"]["identifier"] == "encrypted_identifier" - async def test_get_answer_activity(self, client: TestClient, tom: User, applet: AppletFull, answer: AnswerSchema): + async def test_get_answer_activity( + self, client: TestClient, tom: User, tom_applet_subject: SubjectSchema, applet: AppletFull, answer: AnswerSchema + ): client.login(tom) response = await client.get( self.activity_answer_url.format( @@ -1480,7 +1482,79 @@ async def test_get_answer_activity(self, client: TestClient, tom: User, applet: activity_id=applet.activities[0].id, ) ) - assert response.status_code == http.HTTPStatus.OK # TODO: Check response + assert response.status_code == http.HTTPStatus.OK + response_json = response.json() + + result = response_json["result"] + + assert set(result.keys()) == {"activity", "answer", "summary"} + + result_answer = result["answer"] + assert set(result_answer.keys()) == { + "activityHistoryId", + "activityId", + "answer", + "createdAt", + "endDatetime", + "events", + "flowHistoryId", + "id", + "identifier", + "itemIds", + "migratedData", + "submitId", + "userPublicKey", + "version", + "sourceSubject", + } + assert result_answer["submitId"] == str(answer.submit_id) + assert ( + result_answer["flowHistoryId"] == answer.flow_history_id + if answer.flow_history_id + else str(answer.flow_history_id) + ) + + assert set(result_answer.keys()) == { + "activityHistoryId", + "activityId", + "answer", + "createdAt", + "endDatetime", + "events", + "flowHistoryId", + "id", + "identifier", + "itemIds", + "migratedData", + "submitId", + "userPublicKey", + "version", + "sourceSubject", + } + + assert result_answer["id"] == str(answer.id) + + source_subject = result_answer["sourceSubject"] + + assert set(source_subject.keys()) == { + "secretUserId", + "nickname", + "tag", + "id", + "lastSeen", + "appletId", + "userId", + "firstName", + "lastName", + } + assert source_subject["id"] == str(tom_applet_subject.id) + assert source_subject["userId"] == str(tom.id) + assert source_subject["secretUserId"] == tom_applet_subject.secret_user_id + assert source_subject["nickname"] == tom_applet_subject.nickname + assert source_subject["firstName"] == tom_applet_subject.first_name + assert source_subject["lastName"] == tom_applet_subject.last_name + assert source_subject["tag"] == tom_applet_subject.tag + assert source_subject["lastSeen"] is None async def test_fail_answered_applet_not_existed_activities( self, client: TestClient, tom: User, applet: AppletFull, uuid_zero: uuid.UUID, answer: AnswerSchema @@ -2506,7 +2580,14 @@ async def test_review_flows_multiple_answers( assert len(data[0]["answerDates"]) == 2 assert len(data[1]["answerDates"]) == 1 - async def test_flow_submission(self, client, tom: User, applet_with_flow: AppletFull, tom_answer_activity_flow): + async def test_flow_submission( + self, + client: TestClient, + tom: User, + applet_with_flow: AppletFull, + tom_answer_activity_flow: AnswerSchema, + tom_applet_with_flow_subject: Subject, + ): client.login(tom) url = self.flow_submission_url.format( applet_id=applet_with_flow.id, @@ -2525,11 +2606,24 @@ async def test_flow_submission(self, client, tom: User, applet_with_flow: Applet # fmt: off assert set(answer_data.keys()) == { "activityHistoryId", "activityId", "answer", "createdAt", "endDatetime", "events", "flowHistoryId", "id", - "identifier", "itemIds", "migratedData", "submitId", "userPublicKey", "version" + "identifier", "itemIds", "migratedData", "submitId", "userPublicKey", "version", "sourceSubject" } assert answer_data["submitId"] == str(tom_answer_activity_flow.submit_id) assert answer_data["flowHistoryId"] == str(tom_answer_activity_flow.flow_history_id) + source_subject = answer_data["sourceSubject"] + + assert set(source_subject.keys()) == {"secretUserId", "nickname", "tag", "id", "lastSeen", "appletId", "userId", + "firstName", "lastName"} + assert source_subject["id"] == str(tom_applet_with_flow_subject.id) + assert source_subject["userId"] == str(tom.id) + assert source_subject["secretUserId"] == tom_applet_with_flow_subject.secret_user_id + assert source_subject["nickname"] == tom_applet_with_flow_subject.nickname + assert source_subject["firstName"] == tom_applet_with_flow_subject.first_name + assert source_subject["lastName"] == tom_applet_with_flow_subject.last_name + assert source_subject["tag"] == tom_applet_with_flow_subject.tag + assert source_subject["lastSeen"] is None + assert set(data["flow"].keys()) == { "id", "activities", "autoAssign", "createdAt", "description", "hideBadge", "idVersion", "isHidden", "isSingleReport",