diff --git a/src/apps/activity_flows/crud/flow_history.py b/src/apps/activity_flows/crud/flow_history.py index 647e97852c7..bddd122f86f 100644 --- a/src/apps/activity_flows/crud/flow_history.py +++ b/src/apps/activity_flows/crud/flow_history.py @@ -39,17 +39,20 @@ async def get_by_id_versions(self, id_versions: list[str]) -> list[ActivityFlowH result = await self._execute(query) return result.scalars().all() - async def load_full(self, id_versions: list[str]) -> list[FlowHistoryWithActivityFull]: + async def load_full( + self, id_versions: list[str], *, load_activities: bool = True, load_activity_items: bool = True + ) -> list[FlowHistoryWithActivityFull]: if not id_versions: return [] + opts = joinedload(ActivityFlowHistoriesSchema.items, innerjoin=True) + if load_activities: + opts = opts.joinedload(ActivityFlowItemHistorySchema.activity, innerjoin=True) + if load_activity_items: + opts = opts.joinedload(ActivityHistorySchema.items, innerjoin=True) query = ( select(ActivityFlowHistoriesSchema) - .options( - joinedload(ActivityFlowHistoriesSchema.items, innerjoin=True) - .joinedload(ActivityFlowItemHistorySchema.activity, innerjoin=True) - .joinedload(ActivityHistorySchema.items, innerjoin=True) - ) + .options(opts) .where(ActivityFlowHistoriesSchema.id_version.in_(id_versions)) ) res = await self._execute(query) diff --git a/src/apps/activity_flows/domain/flow_full.py b/src/apps/activity_flows/domain/flow_full.py index 6a83706d25a..519ba5af486 100644 --- a/src/apps/activity_flows/domain/flow_full.py +++ b/src/apps/activity_flows/domain/flow_full.py @@ -24,7 +24,7 @@ class FlowItemHistoryFull(InternalModel): class FlowItemHistoryWithActivityFull(FlowItemHistoryFull): - activity: ActivityHistoryFull + activity: ActivityHistoryFull | None = None class FlowFull(FlowBase, InternalModel): diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 3c4576d11c8..545f130d008 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -133,10 +133,35 @@ async def _validate_anonymous_answer(self, activity_answer: AppletAnswerCreate) await self._validate_applet_for_anonymous_response(activity_answer.applet_id, activity_answer.version) await self._validate_answer(activity_answer) - async def _validate_answer(self, applet_answer: AppletAnswerCreate) -> None: - existed_answers = await AnswersCRUD(self.session).get_by_submit_id(applet_answer.submit_id) + async def _validate_answer(self, applet_answer: AppletAnswerCreate) -> None: # noqa: C901 + pk = self._generate_history_id(applet_answer.version) + existed_answers = await AnswersCRUD(self.answer_session).get_by_submit_id(applet_answer.submit_id) + + activity_history_id = pk(applet_answer.activity_id) + flow_history_id = pk(applet_answer.flow_id) if applet_answer.flow_id else None + + activity_index = None + if flow_history_id: + flow_histories = await FlowsHistoryCRUD(self.session).load_full( + [pk(applet_answer.flow_id)], load_activities=False + ) + if not flow_histories: + raise ValidationError("Flow not found") + flow_history = next(iter(flow_histories)) + + # check activity in the flow + for i, item in enumerate(flow_history.items): + if item.activity_id == activity_history_id: + activity_index = i + break + if activity_index is None: + raise ValidationError("Activity not found in the flow") if existed_answers: + # check uniqueness for activities (duplicated for flow submission only) + if not flow_history_id: + raise ValidationError("Submit id duplicate error") + existed_answer = existed_answers[0] if existed_answer.applet_id != applet_answer.applet_id: raise WrongAnswerGroupAppletId() @@ -145,8 +170,23 @@ async def _validate_answer(self, applet_answer: AppletAnswerCreate) -> None: elif existed_answer.respondent_id != self.user_id: raise WrongRespondentForAnswerGroup() - pk = self._generate_history_id(applet_answer.version) - activity_history = await ActivityHistoriesCRUD(self.session).get_by_id(pk(applet_answer.activity_id)) + # check uniqueness in flow submisssions + if flow_history_id != existed_answer.flow_history_id: + raise ValidationError("Submit id duplicate error") + + # check current answer is provided in right order in the flow, so prev activities already answered + prev_answers_count = len(existed_answers) + if prev_answers_count != activity_index: + assert activity_index is not None + if prev_answers_count < activity_index: + raise ValidationError("Wrong activity order in the flow") + raise ValidationError("Wrong activity order in the flow: previous activity answer missed") + + elif flow_history_id and activity_index != 0: + # check first flow answer + raise ValidationError("Wrong activity order in the flow") + + activity_history = await ActivityHistoriesCRUD(self.session).get_by_id(activity_history_id) if not activity_history.applet_id.startswith(f"{applet_answer.applet_id}"): raise ActivityHistoryDoeNotExist() diff --git a/src/apps/answers/tests/conftest.py b/src/apps/answers/tests/conftest.py index bf1a1a72c76..d01c7463a85 100644 --- a/src/apps/answers/tests/conftest.py +++ b/src/apps/answers/tests/conftest.py @@ -313,6 +313,7 @@ def answer_reviewable_activity_with_tz_offset_create( answer_reviewable_activity_create: AppletAnswerCreate, ) -> AppletAnswerCreate: data = answer_reviewable_activity_create.copy(deep=True) + data.submit_id = uuid.uuid4() # US/Pacific tz_offset = -420 # To minutes like in api diff --git a/src/apps/answers/tests/test_answer_for_cases.py b/src/apps/answers/tests/test_answer_for_cases.py index af9286c690d..c8c00a41295 100644 --- a/src/apps/answers/tests/test_answer_for_cases.py +++ b/src/apps/answers/tests/test_answer_for_cases.py @@ -20,16 +20,20 @@ async def applet_data_with_flow(applet_minimal_data: AppletCreate) -> AppletCrea data = applet_minimal_data.copy(deep=True) data.display_name = "schedule" # By some reasons for test need ActivityFlow + second_activity = data.activities[0].copy(deep=True) + second_activity.name = data.activities[0].name + " second" + second_activity.key = uuid.uuid4() + data.activities.append(second_activity) data.activity_flows = [ FlowCreate( name="flow", description={Language.ENGLISH: "description"}, - items=[FlowItemCreate(activity_key=data.activities[0].key)], + items=[ + FlowItemCreate(activity_key=data.activities[0].key), + FlowItemCreate(activity_key=data.activities[1].key), + ], ) ] - second_activity = data.activities[0].copy(deep=True) - second_activity.name = data.activities[0].name + " second" - data.activities.append(second_activity) return AppletCreate(**data.dict()) diff --git a/src/apps/answers/tests/test_answers.py b/src/apps/answers/tests/test_answers.py index 86c1c946f08..3cf3e50aa14 100644 --- a/src/apps/answers/tests/test_answers.py +++ b/src/apps/answers/tests/test_answers.py @@ -475,6 +475,120 @@ async def test_create_answer__wrong_applet_version( assert response.status_code == http.HTTPStatus.BAD_REQUEST assert response.json()["result"][0]["message"] == InvalidVersionError.message + async def test_create_activity_answers__submit_duplicate( + self, + client: TestClient, + tom: User, + answer_create: AppletAnswerCreate, + ): + client.login(tom) + data = answer_create.copy(deep=True) + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.CREATED + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + data.submit_id = uuid.uuid4() + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.CREATED + + async def test_create_activity_answer_flow_answer__submit_duplicate( + self, + client: TestClient, + tom: User, + answer_create: AppletAnswerCreate, + applet_with_flow: AppletFull, + ): + client.login(tom) + data: AppletAnswerCreate = answer_create.copy(deep=True) + data.submit_id = uuid.uuid4() + data.applet_id = applet_with_flow.id + data.activity_id = applet_with_flow.activities[0].id + data.flow_id = None + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.CREATED + + data.flow_id = applet_with_flow.activity_flows[0].id + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + data.submit_id = uuid.uuid4() + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.CREATED + + async def test_create_flow_answer__submit_duplicate( + self, + client: TestClient, + tom: User, + answer_create: AppletAnswerCreate, + applet_with_flow: AppletFull, + ): + client.login(tom) + data: AppletAnswerCreate = answer_create.copy(deep=True) + data.submit_id = uuid.uuid4() + data.applet_id = applet_with_flow.id + data.flow_id = applet_with_flow.activity_flows[0].id + data.activity_id = applet_with_flow.activity_flows[0].items[0].activity_id + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.CREATED + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + async def test_create_flow_answer__correct_order( + self, + client: TestClient, + tom: User, + answer_create: AppletAnswerCreate, + applet_with_flow: AppletFull, + ): + client.login(tom) + data: AppletAnswerCreate = answer_create.copy(deep=True) + data.submit_id = uuid.uuid4() + data.applet_id = applet_with_flow.id + data.flow_id = applet_with_flow.activity_flows[1].id + data.activity_id = applet_with_flow.activity_flows[1].items[0].activity_id + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.CREATED + + data.activity_id = applet_with_flow.activity_flows[1].items[1].activity_id + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.CREATED + + async def test_create_flow_answer__wrong_order( + self, + client: TestClient, + tom: User, + answer_create: AppletAnswerCreate, + applet_with_flow: AppletFull, + ): + client.login(tom) + data: AppletAnswerCreate = answer_create.copy(deep=True) + data.submit_id = uuid.uuid4() + data.applet_id = applet_with_flow.id + data.flow_id = applet_with_flow.activity_flows[1].id + data.activity_id = applet_with_flow.activity_flows[1].items[1].activity_id + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + + # different submit_id for second activity + data.activity_id = applet_with_flow.activity_flows[1].items[0].activity_id + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.CREATED + + data.submit_id = uuid.uuid4() + data.activity_id = applet_with_flow.activity_flows[1].items[1].activity_id + + response = await client.post(self.answer_url, data=data) + assert response.status_code == http.HTTPStatus.BAD_REQUEST + @pytest.mark.usefixtures("mock_report_server_response", "answer") async def test_get_latest_summary( self, client: TestClient, tom: User, applet: AppletFull, tom_applet_subject: Subject