From 7c56efb96313c825b75bdea03508349ba615227f Mon Sep 17 00:00:00 2001 From: Coro Date: Tue, 24 Sep 2024 12:07:20 -0600 Subject: [PATCH 01/24] Modified SubscaleSettings to accept one more argument score_type, that makes reference to the user selection to report results using the raw scores or t-scores| This is a complementary change for creating reports using t-scores --- src/apps/activities/domain/scores_reports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apps/activities/domain/scores_reports.py b/src/apps/activities/domain/scores_reports.py index ea24fac2fe7..72e5a31a5cb 100644 --- a/src/apps/activities/domain/scores_reports.py +++ b/src/apps/activities/domain/scores_reports.py @@ -220,6 +220,7 @@ class SubscaleSetting(PublicModel): calculate_total_score: SubscaleCalculationType | None = None subscales: list[Subscale] | None = Field(default_factory=list) total_scores_table_data: list[TotalScoreTable] | None = Field(default_factory=list) + score_type: str | None = None @validator("subscales") def validate_unique_subscale_names(cls, value): From 7108b36349cf6a5c5cffe9eb6d028df7c12ee1c1 Mon Sep 17 00:00:00 2001 From: Coro Date: Mon, 30 Sep 2024 21:28:21 +0200 Subject: [PATCH 02/24] Adding new entries to score_and_reports and removing from subscale_settings --- src/apps/activities/domain/scores_reports.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/activities/domain/scores_reports.py b/src/apps/activities/domain/scores_reports.py index 72e5a31a5cb..dfff0870f2f 100644 --- a/src/apps/activities/domain/scores_reports.py +++ b/src/apps/activities/domain/scores_reports.py @@ -112,6 +112,8 @@ class ScoresAndReports(PublicModel): generate_report: bool = False show_score_summary: bool = False reports: list[Score | Section] | None = Field(default_factory=list) + scoring_type: str | None = None + subscale_name: str | None = None @validator("reports") def validate_reports(cls, value): @@ -220,7 +222,6 @@ class SubscaleSetting(PublicModel): calculate_total_score: SubscaleCalculationType | None = None subscales: list[Subscale] | None = Field(default_factory=list) total_scores_table_data: list[TotalScoreTable] | None = Field(default_factory=list) - score_type: str | None = None @validator("subscales") def validate_unique_subscale_names(cls, value): From 15a7d5974d52611e905829e7b22ea35a13a8a1ed Mon Sep 17 00:00:00 2001 From: Coro Date: Mon, 30 Sep 2024 21:46:23 +0200 Subject: [PATCH 03/24] scoring_type and subscale_table_data is now passed to ReportServer as payload --- src/apps/answers/service.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 6f101c8e3d5..1df58ba53af 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1366,7 +1366,7 @@ async def get_summary_latest_report( return None service = ReportServerService(self.session, arbitrary_session=self.answer_session) - report = await service.create_report(answer.submit_id, answer.id) + report = await service.create_report(answer.submit_id, answer.id, activity_id) return report async def get_flow_summary_latest_report( @@ -1922,9 +1922,20 @@ async def is_flow_finished(self, submit_id: uuid.UUID, answer_id: uuid.UUID) -> return self._is_activity_last_in_flow(applet_full, activity_id, flow_id) async def create_report( - self, submit_id: uuid.UUID, answer_id: uuid.UUID | None = None + self, submit_id: uuid.UUID, answer_id: uuid.UUID | None = None, activity_id: uuid.UUID | None = None ) -> ReportServerResponse | None: answers = await AnswersCRUD(self.answers_session).get_by_submit_id(submit_id, answer_id) + act_crud = ActivitiesCRUD(self.session) + activity_data = await act_crud.get_by_id(activity_id) + if "scoring_type" in activity_data.scores_and_reports.keys(): + scoring_type = activity_data.scores_and_reports["scoring_type"] + if scoring_type == "lookup_scores": + subscale_name = activity_data.scores_and_reports["subscale_name"] + subscales = activity_data.subscale_setting["subscales"] + for subscale in subscales: + if subscale["name"] == subscale_name: + subscale_table_data = subscale["subscale_table_data"] + if not answers: return None applet_id_version: str = answers[0].applet_history_id @@ -1957,6 +1968,8 @@ async def create_report( now=datetime.datetime.utcnow().strftime("%x"), user=user_info, applet=applet_full, + scoring_type=scoring_type, + subscale_table_data=subscale_table_data, ) encrypted_data = encryption.encrypt(data) From 6edf0c6ec662fe5a8738dc057e39a0c1bc0282d2 Mon Sep 17 00:00:00 2001 From: Coro Date: Mon, 30 Sep 2024 21:57:05 +0200 Subject: [PATCH 04/24] Applied ruff formatting and using raw_scores as default value for scoring_type in case scores_and_reports doesn't have this value (old saved entries) --- src/apps/activities/domain/scores_reports.py | 4 ++-- src/apps/answers/service.py | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/apps/activities/domain/scores_reports.py b/src/apps/activities/domain/scores_reports.py index dfff0870f2f..3ec9e143946 100644 --- a/src/apps/activities/domain/scores_reports.py +++ b/src/apps/activities/domain/scores_reports.py @@ -112,8 +112,8 @@ class ScoresAndReports(PublicModel): generate_report: bool = False show_score_summary: bool = False reports: list[Score | Section] | None = Field(default_factory=list) - scoring_type: str | None = None - subscale_name: str | None = None + scoring_type: str | None = None + subscale_name: str | None = None @validator("reports") def validate_reports(cls, value): diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 1df58ba53af..81ea2e6821f 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1927,14 +1927,16 @@ async def create_report( answers = await AnswersCRUD(self.answers_session).get_by_submit_id(submit_id, answer_id) act_crud = ActivitiesCRUD(self.session) activity_data = await act_crud.get_by_id(activity_id) - if "scoring_type" in activity_data.scores_and_reports.keys(): - scoring_type = activity_data.scores_and_reports["scoring_type"] - if scoring_type == "lookup_scores": - subscale_name = activity_data.scores_and_reports["subscale_name"] - subscales = activity_data.subscale_setting["subscales"] - for subscale in subscales: - if subscale["name"] == subscale_name: - subscale_table_data = subscale["subscale_table_data"] + scoring_type = activity_data.scores_and_reports.get("scoring_type", "raw_scores") + + if scoring_type == "lookup_scores": + subscale_name = activity_data.scores_and_reports["subscale_name"] + subscales = activity_data.subscale_setting["subscales"] + for subscale in subscales: + if subscale["name"] == subscale_name: + subscale_table_data = subscale["subscale_table_data"] + else: + subscale_table_data = [] if not answers: return None From ab7334fb40fc5d9f63b6ebf6b921e825fad432b2 Mon Sep 17 00:00:00 2001 From: Coro Date: Mon, 30 Sep 2024 22:15:14 +0200 Subject: [PATCH 05/24] Created validation for unliked lookup tables and unexistent lookup tables --- src/apps/answers/errors.py | 10 ++++++++++ src/apps/answers/service.py | 10 ++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/apps/answers/errors.py b/src/apps/answers/errors.py index c1cfdb82034..d1a853df4ea 100644 --- a/src/apps/answers/errors.py +++ b/src/apps/answers/errors.py @@ -90,3 +90,13 @@ class MultiinformantAssessmentNoAccessApplet(ValidationError): class MultiinformantAssessmentInvalidActivityOrFlow(ValidationError): message = _("Activity or Flow not found") code = _("invalid_activity_or_flow_id") + + +class NoSubscaleLinked(ValidationError): + message = _("The scoring type is lookup_scores but no subscale is linked") + code = _("no_subscale_linked") + + +class SubscaleDoesNotExist(ValidationError): + message = _("The scoring type is lookup_scores but the subscale data does not exist") + code = _("no_subscale_exist") diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 81ea2e6821f..94ca0c01252 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -75,8 +75,10 @@ MultiinformantAssessmentInvalidTargetSubject, MultiinformantAssessmentNoAccessApplet, NonPublicAppletError, + NoSubscaleLinked, ReportServerError, ReportServerIsNotConfigured, + SubscaleDoesNotExist, UserDoesNotHavePermissionError, WrongAnswerGroupAppletId, WrongAnswerGroupVersion, @@ -1930,11 +1932,15 @@ async def create_report( scoring_type = activity_data.scores_and_reports.get("scoring_type", "raw_scores") if scoring_type == "lookup_scores": - subscale_name = activity_data.scores_and_reports["subscale_name"] + subscale_name = activity_data.scores_and_reports.get("subscale_name", False) + if not subscale_name: + raise NoSubscaleLinked() subscales = activity_data.subscale_setting["subscales"] for subscale in subscales: if subscale["name"] == subscale_name: - subscale_table_data = subscale["subscale_table_data"] + subscale_table_data = subscale.get("subscale_table_data", False) + if not subscale_table_data: + raise SubscaleDoesNotExist() else: subscale_table_data = [] From fe953d1e0079ca298800098c81017067ef49a66e Mon Sep 17 00:00:00 2001 From: Coro Date: Tue, 1 Oct 2024 19:37:20 +0200 Subject: [PATCH 06/24] Created unit test first version --- .../tests/fixtures/scores_reports.py | 20 +++++++ .../tests/test_applet_activity_items.py | 60 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index cf73250dfba..3990a22698e 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -76,6 +76,15 @@ def scores_and_reports(score: Score, section: Section) -> ScoresAndReports: ) +@pytest.fixture +def scores_and_reports_raw_score(score: Score, section: Section) -> ScoresAndReports: + return ScoresAndReports( + generate_report=True, + show_score_summary=True, + reports=[score, section], + ) + + @pytest.fixture def subscale_item() -> SubscaleItem: return SubscaleItem(name="activity_item_1", type=SubscaleItemType.ITEM) @@ -90,6 +99,17 @@ def subscale(subscale_item: SubscaleItem) -> Subscale: ) +@pytest.fixture +def scores_and_reports_lookup_scores(score: Score, section: Section) -> ScoresAndReports: + return ScoresAndReports( + generate_report=True, + show_score_summary=True, + reports=[score, section], + scoring_type="lookup_scores", + scoring_name=subscale.name, + ) + + @pytest.fixture def subscale_item_type_subscale(subscale: Subscale) -> SubscaleItem: # Depends on subscalke because name should contain subscale item diff --git a/src/apps/applets/tests/test_applet_activity_items.py b/src/apps/applets/tests/test_applet_activity_items.py index edb9f546506..33299e5e2c5 100644 --- a/src/apps/applets/tests/test_applet_activity_items.py +++ b/src/apps/applets/tests/test_applet_activity_items.py @@ -522,6 +522,66 @@ async def test_create_applet__activity_with_subscale_settings_with_subscale_look result = resp.json()["result"] assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) + async def test_create_applet__activity_with_subscale_settings_and_lookup_table_and_score_report_raw_scoring( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + single_select_item_create_with_score: ActivityItemCreate, + tom: User, + subscale_setting: SubscaleSetting, + subscale_lookup_table: list[SubScaleLookupTable], + scores_and_reports: ScoresAndReports, + ): + client.login(tom) + data = applet_minimal_data.copy(deep=True) + sub_setting = subscale_setting.copy(deep=True) + + # Update subscale setting with item name and lookup table + sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # Update name + sub_setting.subscales[0].subscale_table_data = subscale_lookup_table # Set lookup table data + + data.activities[0].items = [single_select_item_create_with_score] + data.activities[0].subscale_setting = sub_setting + data.activities[0].scores_and_reports = scores_and_reports + + # Make the POST request + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), json=data.dict(by_alias=True)) + + # Assertions + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) + + async def test_create_applet__activity_with_subscale_settings_and_lookup_table_and_score_report_lookup_scoring( + self, + client: TestClient, + applet_minimal_data: AppletCreate, + single_select_item_create_with_score: ActivityItemCreate, + tom: User, + subscale_setting: SubscaleSetting, + subscale_lookup_table: list[SubScaleLookupTable], + scores_and_reports_lookup_scores: ScoresAndReports, + ): + client.login(tom) + data = applet_minimal_data.copy(deep=True) + sub_setting = subscale_setting.copy(deep=True) + + # Update subscale setting with item name and lookup table + sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # Update name + sub_setting.subscales[0].subscale_table_data = subscale_lookup_table # Set lookup table data + + data.activities[0].items = [single_select_item_create_with_score] + data.activities[0].subscale_setting = sub_setting + data.activities[0].scores_and_reports = scores_and_reports_lookup_scores + + # Make the POST request + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), json=data.dict(by_alias=True)) + + # Assertions + assert resp.status_code == http.HTTPStatus.CREATED + result = resp.json()["result"] + assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) + async def test_create_applet__activity_with_subscale_settings_with_invalid_subscale_lookup_table_age( self, client: TestClient, From bd18694fd04680e333ba83772b3176a0b2526128 Mon Sep 17 00:00:00 2001 From: Coro Date: Wed, 2 Oct 2024 00:24:33 +0200 Subject: [PATCH 07/24] Created unit test for creating applets with scoring_type and subscale_name --- .../tests/fixtures/scores_reports.py | 4 +-- .../tests/test_applet_activity_items.py | 34 ++----------------- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index 3990a22698e..64f3416eec6 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -100,13 +100,13 @@ def subscale(subscale_item: SubscaleItem) -> Subscale: @pytest.fixture -def scores_and_reports_lookup_scores(score: Score, section: Section) -> ScoresAndReports: +def scores_and_reports_lookup_scores(score: Score, section: Section, subscale: Subscale) -> ScoresAndReports: return ScoresAndReports( generate_report=True, show_score_summary=True, reports=[score, section], scoring_type="lookup_scores", - scoring_name=subscale.name, + subscale_name=subscale.name, ) diff --git a/src/apps/applets/tests/test_applet_activity_items.py b/src/apps/applets/tests/test_applet_activity_items.py index 33299e5e2c5..b0d54ebd56c 100644 --- a/src/apps/applets/tests/test_applet_activity_items.py +++ b/src/apps/applets/tests/test_applet_activity_items.py @@ -522,36 +522,6 @@ async def test_create_applet__activity_with_subscale_settings_with_subscale_look result = resp.json()["result"] assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) - async def test_create_applet__activity_with_subscale_settings_and_lookup_table_and_score_report_raw_scoring( - self, - client: TestClient, - applet_minimal_data: AppletCreate, - single_select_item_create_with_score: ActivityItemCreate, - tom: User, - subscale_setting: SubscaleSetting, - subscale_lookup_table: list[SubScaleLookupTable], - scores_and_reports: ScoresAndReports, - ): - client.login(tom) - data = applet_minimal_data.copy(deep=True) - sub_setting = subscale_setting.copy(deep=True) - - # Update subscale setting with item name and lookup table - sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # Update name - sub_setting.subscales[0].subscale_table_data = subscale_lookup_table # Set lookup table data - - data.activities[0].items = [single_select_item_create_with_score] - data.activities[0].subscale_setting = sub_setting - data.activities[0].scores_and_reports = scores_and_reports - - # Make the POST request - resp = await client.post(self.applet_create_url.format(owner_id=tom.id), json=data.dict(by_alias=True)) - - # Assertions - assert resp.status_code == http.HTTPStatus.CREATED - result = resp.json()["result"] - assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) - async def test_create_applet__activity_with_subscale_settings_and_lookup_table_and_score_report_lookup_scoring( self, client: TestClient, @@ -575,12 +545,14 @@ async def test_create_applet__activity_with_subscale_settings_and_lookup_table_a data.activities[0].scores_and_reports = scores_and_reports_lookup_scores # Make the POST request - resp = await client.post(self.applet_create_url.format(owner_id=tom.id), json=data.dict(by_alias=True)) + resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data) # Assertions assert resp.status_code == http.HTTPStatus.CREATED result = resp.json()["result"] assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) + assert result["activities"][0]["scoresAndReports"]["scoringType"] == "lookup_scores" + assert result["activities"][0]["scoresAndReports"]["subscaleName"] == "subscale type item" async def test_create_applet__activity_with_subscale_settings_with_invalid_subscale_lookup_table_age( self, From 24b53c4bcee0852abb1bae1f60cd0cf587dee2a8 Mon Sep 17 00:00:00 2001 From: Coro Date: Wed, 2 Oct 2024 00:40:52 +0200 Subject: [PATCH 08/24] Replaced payload data with CamelCase and refcator code according to corrections --- src/apps/answers/service.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 94ca0c01252..1af519a29e8 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1928,21 +1928,21 @@ async def create_report( ) -> ReportServerResponse | None: answers = await AnswersCRUD(self.answers_session).get_by_submit_id(submit_id, answer_id) act_crud = ActivitiesCRUD(self.session) - activity_data = await act_crud.get_by_id(activity_id) - scoring_type = activity_data.scores_and_reports.get("scoring_type", "raw_scores") - - if scoring_type == "lookup_scores": - subscale_name = activity_data.scores_and_reports.get("subscale_name", False) - if not subscale_name: - raise NoSubscaleLinked() - subscales = activity_data.subscale_setting["subscales"] - for subscale in subscales: - if subscale["name"] == subscale_name: - subscale_table_data = subscale.get("subscale_table_data", False) - if not subscale_table_data: - raise SubscaleDoesNotExist() - else: - subscale_table_data = [] + subscale_table_data = [] + subscale_name = "" + if activity_id: + activity_data = await act_crud.get_by_id(activity_id) + scoring_type = activity_data.scores_and_reports.get("scoring_type", "raw_scores") + if scoring_type == "lookup_scores": + subscale_name = activity_data.scores_and_reports.get("subscale_name", False) + if not subscale_name: + raise NoSubscaleLinked() + subscales = activity_data.subscale_setting["subscales"] + for subscale in subscales: + if subscale["name"] == subscale_name: + subscale_table_data = subscale.get("subscale_table_data", False) + if not subscale_table_data: + raise SubscaleDoesNotExist() if not answers: return None @@ -1976,8 +1976,8 @@ async def create_report( now=datetime.datetime.utcnow().strftime("%x"), user=user_info, applet=applet_full, - scoring_type=scoring_type, - subscale_table_data=subscale_table_data, + scoringType=scoring_type, + subscaleTableData=subscale_table_data, ) encrypted_data = encryption.encrypt(data) From 076ed45e019ab3b1fe6f88f52ccdaf4d72d2ecdc Mon Sep 17 00:00:00 2001 From: Coro Date: Thu, 3 Oct 2024 14:46:55 +0200 Subject: [PATCH 09/24] Fixed test_create_applet__activity_with_subscale_settings_and_lookup_table_and_score_report_lookup_scoring by ignoring Subsetting class definition using # type: ignore[index] --- src/apps/applets/tests/test_applet_activity_items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/applets/tests/test_applet_activity_items.py b/src/apps/applets/tests/test_applet_activity_items.py index b0d54ebd56c..5cfe052ea31 100644 --- a/src/apps/applets/tests/test_applet_activity_items.py +++ b/src/apps/applets/tests/test_applet_activity_items.py @@ -537,8 +537,8 @@ async def test_create_applet__activity_with_subscale_settings_and_lookup_table_a sub_setting = subscale_setting.copy(deep=True) # Update subscale setting with item name and lookup table - sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # Update name - sub_setting.subscales[0].subscale_table_data = subscale_lookup_table # Set lookup table data + sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # type: ignore[index] + sub_setting.subscales[0].subscale_table_data = subscale_lookup_table # type: ignore[index] data.activities[0].items = [single_select_item_create_with_score] data.activities[0].subscale_setting = sub_setting From 286c0575029030ef1b002a84e2ac4bf790166c23 Mon Sep 17 00:00:00 2001 From: Coro Date: Mon, 7 Oct 2024 17:01:20 +0200 Subject: [PATCH 10/24] Updated scoring_type and scoring_name validations when creating an application instead of creating a report --- .../activities/domain/custom_validation.py | 25 ++++++++++++++++++- src/apps/activities/domain/scores_reports.py | 9 +++++-- src/apps/activities/errors.py | 20 +++++++++++++++ src/apps/answers/errors.py | 10 -------- src/apps/answers/service.py | 7 +----- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 0fb6e6c3c5d..5719b781d1c 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -20,9 +20,13 @@ IncorrectSectionPrintItemTypeError, IncorrectSubscaleInsideSubscaleError, IncorrectSubscaleItemError, + SubscaleDataDoesNotExist, SubscaleInsideSubscaleError, + SubscaleIsNotLinked, SubscaleItemScoreError, SubscaleItemTypeError, + SubscaleNameDoesNotExist, + SubscaleSettingDoesNotExist, ) @@ -78,7 +82,26 @@ def validate_score_and_sections(values: dict): # noqa: C901 for report in list(scores): score_item_ids.append(report.id) - # check if all item names are same as values.name + subscale_name = report.get("subscale_name", False) + scoring_type = report.get("scoring_type", False) + + if scoring_type == "lookup_scores": + if not subscale_name: + raise SubscaleIsNotLinked() + subscale_setting = values.get("subscale_setting", False) + if not subscale_setting: + raise SubscaleSettingDoesNotExist() + subscales = subscale_setting["subscales"] + subscales_names = [subscale["name"] for subscale in subscales] + if subscale_name not in subscales_names: + raise SubscaleNameDoesNotExist() + for subscale in subscales: + if subscale["name"] == subscale_name: + subscale_table_data = subscale.get("subscale_table_data", False) + if not subscale_table_data: + raise SubscaleDataDoesNotExist() + + # check if all item names are same as values.name for item in report.items_score: if item not in item_names: raise IncorrectScoreItemError() diff --git a/src/apps/activities/domain/scores_reports.py b/src/apps/activities/domain/scores_reports.py index 3ec9e143946..ec7a9b4ad1d 100644 --- a/src/apps/activities/domain/scores_reports.py +++ b/src/apps/activities/domain/scores_reports.py @@ -30,6 +30,11 @@ class CalculationType(str, Enum): PERCENTAGE = "percentage" +class ScoringType(str, Enum): + SCORE = "score" + RAW_SCORE = "raw_score" + + class ScoreConditionalLogic(PublicModel): name: str id: str @@ -60,6 +65,8 @@ class Score(PublicModel): message: str | None = None items_print: list[str] | None = Field(default_factory=list) conditional_logic: list[ScoreConditionalLogic] | None = None + scoring_type: ScoringType | None = None + subscale_name: str | None = None @validator("conditional_logic") def validate_conditional_logic(cls, value, values): @@ -112,8 +119,6 @@ class ScoresAndReports(PublicModel): generate_report: bool = False show_score_summary: bool = False reports: list[Score | Section] | None = Field(default_factory=list) - scoring_type: str | None = None - subscale_name: str | None = None @validator("reports") def validate_reports(cls, value): diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index ec1036dd679..4a2c34ddf62 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -270,3 +270,23 @@ class IncorrectPhrasalTemplateItemTypeError(ValidationError): class IncorrectPhrasalTemplateItemIndexError(ValidationError): message = _("Invalid item index for activity item inside phrasal template") + + +class SubscaleIsNotLinked(ValidationError): + message = _("The scoring type is lookup_scores but no subscale is linked") + code = _("no_subscale_linked") + + +class SubscaleNameDoesNotExist(ValidationError): + message = _("The lookup table with the passed name does not exist in subcale settings") + code = _("no_subscale_exist") + + +class SubscaleDataDoesNotExist(ValidationError): + message = _("The scoring type is lookup_scores but the subscale data does not exist") + code = _("no_subscale_exist") + + +class SubscaleSettingDoesNotExist(ValidationError): + message = _("The scoring type is lookup_scores but there are no subscale settings associated with activity") + code = _("no_subscale_exist") diff --git a/src/apps/answers/errors.py b/src/apps/answers/errors.py index d1a853df4ea..c1cfdb82034 100644 --- a/src/apps/answers/errors.py +++ b/src/apps/answers/errors.py @@ -90,13 +90,3 @@ class MultiinformantAssessmentNoAccessApplet(ValidationError): class MultiinformantAssessmentInvalidActivityOrFlow(ValidationError): message = _("Activity or Flow not found") code = _("invalid_activity_or_flow_id") - - -class NoSubscaleLinked(ValidationError): - message = _("The scoring type is lookup_scores but no subscale is linked") - code = _("no_subscale_linked") - - -class SubscaleDoesNotExist(ValidationError): - message = _("The scoring type is lookup_scores but the subscale data does not exist") - code = _("no_subscale_exist") diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 1af519a29e8..1c666235860 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -75,10 +75,8 @@ MultiinformantAssessmentInvalidTargetSubject, MultiinformantAssessmentNoAccessApplet, NonPublicAppletError, - NoSubscaleLinked, ReportServerError, ReportServerIsNotConfigured, - SubscaleDoesNotExist, UserDoesNotHavePermissionError, WrongAnswerGroupAppletId, WrongAnswerGroupVersion, @@ -1930,19 +1928,16 @@ async def create_report( act_crud = ActivitiesCRUD(self.session) subscale_table_data = [] subscale_name = "" + scoring_type = "" if activity_id: activity_data = await act_crud.get_by_id(activity_id) scoring_type = activity_data.scores_and_reports.get("scoring_type", "raw_scores") if scoring_type == "lookup_scores": subscale_name = activity_data.scores_and_reports.get("subscale_name", False) - if not subscale_name: - raise NoSubscaleLinked() subscales = activity_data.subscale_setting["subscales"] for subscale in subscales: if subscale["name"] == subscale_name: subscale_table_data = subscale.get("subscale_table_data", False) - if not subscale_table_data: - raise SubscaleDoesNotExist() if not answers: return None From e84210e59159839b583aaeb209e6dc05b432a061 Mon Sep 17 00:00:00 2001 From: Coro Date: Mon, 7 Oct 2024 23:19:36 +0200 Subject: [PATCH 11/24] Moved validations to the validate_score_and_sections function and updated Scores scoryin_type values --- src/apps/activities/domain/custom_validation.py | 14 +++++++------- src/apps/activities/errors.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 5719b781d1c..3523bd66ee4 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -82,22 +82,22 @@ def validate_score_and_sections(values: dict): # noqa: C901 for report in list(scores): score_item_ids.append(report.id) - subscale_name = report.get("subscale_name", False) - scoring_type = report.get("scoring_type", False) + subscale_name = report.subscale_name + scoring_type = report.scoring_type - if scoring_type == "lookup_scores": + if scoring_type == "score": if not subscale_name: raise SubscaleIsNotLinked() subscale_setting = values.get("subscale_setting", False) if not subscale_setting: raise SubscaleSettingDoesNotExist() - subscales = subscale_setting["subscales"] - subscales_names = [subscale["name"] for subscale in subscales] + subscales = subscale_setting.subscales + subscales_names = [subscale.name for subscale in subscales] if subscale_name not in subscales_names: raise SubscaleNameDoesNotExist() for subscale in subscales: - if subscale["name"] == subscale_name: - subscale_table_data = subscale.get("subscale_table_data", False) + if subscale.name == subscale_name: + subscale_table_data = subscale.subscale_table_data if not subscale_table_data: raise SubscaleDataDoesNotExist() diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index 4a2c34ddf62..26080407e5b 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -273,7 +273,7 @@ class IncorrectPhrasalTemplateItemIndexError(ValidationError): class SubscaleIsNotLinked(ValidationError): - message = _("The scoring type is lookup_scores but no subscale is linked") + message = _("The scoring type is score but no subscale is linked") code = _("no_subscale_linked") @@ -283,10 +283,10 @@ class SubscaleNameDoesNotExist(ValidationError): class SubscaleDataDoesNotExist(ValidationError): - message = _("The scoring type is lookup_scores but the subscale data does not exist") + message = _("The scoring type is score but the subscale data does not exist") code = _("no_subscale_exist") class SubscaleSettingDoesNotExist(ValidationError): - message = _("The scoring type is lookup_scores but there are no subscale settings associated with activity") + message = _("The scoring type is score but there are no subscale settings associated with activity") code = _("no_subscale_exist") From bc449beddae31bbffe13e0de1bc10e16120acff3 Mon Sep 17 00:00:00 2001 From: Coro Date: Mon, 7 Oct 2024 23:24:15 +0200 Subject: [PATCH 12/24] formated code with ruff --- src/apps/activities/domain/custom_validation.py | 12 ++++++------ src/apps/activities/errors.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 3523bd66ee4..1fbed513809 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -86,14 +86,14 @@ def validate_score_and_sections(values: dict): # noqa: C901 scoring_type = report.scoring_type if scoring_type == "score": - if not subscale_name: - raise SubscaleIsNotLinked() + if not subscale_name: + raise SubscaleIsNotLinked() subscale_setting = values.get("subscale_setting", False) if not subscale_setting: - raise SubscaleSettingDoesNotExist() + raise SubscaleSettingDoesNotExist() subscales = subscale_setting.subscales - subscales_names = [subscale.name for subscale in subscales] - if subscale_name not in subscales_names: + subscales_names = [subscale.name for subscale in subscales] + if subscale_name not in subscales_names: raise SubscaleNameDoesNotExist() for subscale in subscales: if subscale.name == subscale_name: @@ -101,7 +101,7 @@ def validate_score_and_sections(values: dict): # noqa: C901 if not subscale_table_data: raise SubscaleDataDoesNotExist() - # check if all item names are same as values.name + # check if all item names are same as values.name for item in report.items_score: if item not in item_names: raise IncorrectScoreItemError() diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index 26080407e5b..c767126579f 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -281,7 +281,7 @@ class SubscaleNameDoesNotExist(ValidationError): message = _("The lookup table with the passed name does not exist in subcale settings") code = _("no_subscale_exist") - + class SubscaleDataDoesNotExist(ValidationError): message = _("The scoring type is score but the subscale data does not exist") code = _("no_subscale_exist") From 98082bab5e2114a190c6a93d7b01b8c331695157 Mon Sep 17 00:00:00 2001 From: Coro Date: Tue, 8 Oct 2024 00:09:38 +0200 Subject: [PATCH 13/24] Updated create_function section obtaining subscale_scoring and names frpm ScoresAndReport.scores --- src/apps/answers/service.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index 1c666235860..c0efafa9e51 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1922,22 +1922,37 @@ async def is_flow_finished(self, submit_id: uuid.UUID, answer_id: uuid.UUID) -> return self._is_activity_last_in_flow(applet_full, activity_id, flow_id) async def create_report( - self, submit_id: uuid.UUID, answer_id: uuid.UUID | None = None, activity_id: uuid.UUID | None = None + self, + submit_id: uuid.UUID, + answer_id: uuid.UUID | None = None, + activity_id: uuid.UUID | None = None, + report_name: str | None = None, ) -> ReportServerResponse | None: answers = await AnswersCRUD(self.answers_session).get_by_submit_id(submit_id, answer_id) - act_crud = ActivitiesCRUD(self.session) subscale_table_data = [] subscale_name = "" scoring_type = "" + if activity_id: - activity_data = await act_crud.get_by_id(activity_id) - scoring_type = activity_data.scores_and_reports.get("scoring_type", "raw_scores") - if scoring_type == "lookup_scores": - subscale_name = activity_data.scores_and_reports.get("subscale_name", False) - subscales = activity_data.subscale_setting["subscales"] - for subscale in subscales: - if subscale["name"] == subscale_name: - subscale_table_data = subscale.get("subscale_table_data", False) + activity_data = await ActivitiesCRUD(self.session).get_by_id(activity_id) + reports = activity_data.scores_and_reports.get("reports", []) + if not reports: + return None # Early return if no reports are found + + selected_report = next((report for report in reports if report["name"] == report_name), reports[0]) + + scoring_type = selected_report.get("scoring_type", "") + if scoring_type == "score": + subscale_name = selected_report.get("subscale_name", "") + subscales = activity_data.subscale_setting.get("subscales", []) + subscale_table_data = next( + ( + subscale.get("subscale_table_data", []) + for subscale in subscales + if subscale.get("name") == subscale_name + ), + [], + ) if not answers: return None From 0a179cf1ebdd29064e31b1ec1f36dc98bf4dd347 Mon Sep 17 00:00:00 2001 From: Coro Date: Tue, 8 Oct 2024 00:40:14 +0200 Subject: [PATCH 14/24] subscale_table_dataruff formatting required type --- src/apps/answers/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index c0efafa9e51..f6b6bf1fce1 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -22,6 +22,7 @@ from apps.activities.crud import ActivitiesCRUD, ActivityHistoriesCRUD, ActivityItemHistoriesCRUD from apps.activities.db.schemas import ActivityItemHistorySchema from apps.activities.domain.activity_history import ActivityHistoryFull +from apps.activities.domain.scores_reports import SubScaleLookupTable from apps.activities.errors import ActivityDoeNotExist, ActivityHistoryDoeNotExist, FlowDoesNotExist from apps.activity_flows.crud import FlowsCRUD, FlowsHistoryCRUD from apps.alerts.crud.alert import AlertCRUD @@ -1929,7 +1930,7 @@ async def create_report( report_name: str | None = None, ) -> ReportServerResponse | None: answers = await AnswersCRUD(self.answers_session).get_by_submit_id(submit_id, answer_id) - subscale_table_data = [] + subscale_table_data: list[SubScaleLookupTable] | None = None subscale_name = "" scoring_type = "" From 4a84ddfeb850ddd653e78a8a1b7710e2232fe60a Mon Sep 17 00:00:00 2001 From: Coro Date: Thu, 10 Oct 2024 08:39:16 -0600 Subject: [PATCH 15/24] Updated unit tests callin new SusbcaleSetting with lookup table reference, ScoresAndReport using new scores list with Score containing subscale_name and scoring_type, updated test to loop over scores insice ScoresAndReports class. Updated validations and included additional error message for no non-subscale items detected --- .../activities/domain/custom_validation.py | 22 +++++----- src/apps/activities/errors.py | 17 +++++--- .../tests/fixtures/scores_reports.py | 40 +++++++++++++++---- .../tests/unit/test_activity_change.py | 11 ++++- .../tests/test_applet_activity_items.py | 9 +++-- 5 files changed, 72 insertions(+), 27 deletions(-) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 1fbed513809..ee496adb7f5 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -1,7 +1,7 @@ from apps.activities.domain.conditions import MultiSelectConditionType, SingleSelectConditionType from apps.activities.domain.response_type_config import PerformanceTaskType, ResponseType from apps.activities.domain.response_values import PhrasalTemplateFieldType -from apps.activities.domain.scores_reports import ReportType, SubscaleItemType +from apps.activities.domain.scores_reports import ReportType, SubscaleItemType, SubscaleSetting from apps.activities.errors import ( IncorrectConditionItemError, IncorrectConditionItemIndexError, @@ -23,6 +23,7 @@ SubscaleDataDoesNotExist, SubscaleInsideSubscaleError, SubscaleIsNotLinked, + SubscaleItemDoesNotExist, SubscaleItemScoreError, SubscaleItemTypeError, SubscaleNameDoesNotExist, @@ -88,18 +89,21 @@ def validate_score_and_sections(values: dict): # noqa: C901 if scoring_type == "score": if not subscale_name: raise SubscaleIsNotLinked() - subscale_setting = values.get("subscale_setting", False) + subscale_setting: SubscaleSetting | None = values.get("subscale_setting", False) if not subscale_setting: raise SubscaleSettingDoesNotExist() - subscales = subscale_setting.subscales - subscales_names = [subscale.name for subscale in subscales] - if subscale_name not in subscales_names: + + linked_subscale = next( + (subscale for subscale in subscale_setting.subscales if subscale.name == subscale_name), None + ) + if not linked_subscale: raise SubscaleNameDoesNotExist() - for subscale in subscales: - if subscale.name == subscale_name: - subscale_table_data = subscale.subscale_table_data - if not subscale_table_data: + elif not linked_subscale.subscale_table_data: raise SubscaleDataDoesNotExist() + else: + has_non_subscale_items = any(item.type == SubscaleItemType.ITEM for item in linked_subscale.items) + if not has_non_subscale_items: + raise SubscaleItemDoesNotExist() # check if all item names are same as values.name for item in report.items_score: diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index c767126579f..5a8b73477b3 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -273,20 +273,25 @@ class IncorrectPhrasalTemplateItemIndexError(ValidationError): class SubscaleIsNotLinked(ValidationError): - message = _("The scoring type is score but no subscale is linked") + message = _("The scoring_type is score but no subscale is linked") code = _("no_subscale_linked") class SubscaleNameDoesNotExist(ValidationError): message = _("The lookup table with the passed name does not exist in subcale settings") - code = _("no_subscale_exist") + code = _("no_subscale_name_exist") class SubscaleDataDoesNotExist(ValidationError): - message = _("The scoring type is score but the subscale data does not exist") - code = _("no_subscale_exist") + message = _("The scoring_type is score but the subscale data does not exist") + code = _("no_subscaledata_exist") class SubscaleSettingDoesNotExist(ValidationError): - message = _("The scoring type is score but there are no subscale settings associated with activity") - code = _("no_subscale_exist") + message = _("The scoring_type is score but there are no subscale settings associated with activity") + code = _("no_subscale_setting_exist") + + +class SubscaleItemDoesNotExist(ValidationError): + message = _("The linked subscale should contain at least one non-subscale item") + code = _("no_subscale_items_exist") diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index 64f3416eec6..cdd295299cb 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -48,6 +48,30 @@ def score() -> Score: ) +@pytest.fixture +def score_with_subcale() -> Score: + return Score( + type=ReportType.score, + name="testscore type score", + id=SCORE_ID, + calculation_type=CalculationType.SUM, + scoring_type="score", + subscale_name="subscale type item", + ) + + +@pytest.fixture +def score_with_subcale_raw() -> Score: + return Score( + type=ReportType.score, + name="testscore type score", + id=SCORE_ID, + calculation_type=CalculationType.SUM, + scoring_type="raw_score", + subscale_name=None, + ) + + @pytest.fixture def section_conditional_logic() -> SectionConditionalLogic: return SectionConditionalLogic( @@ -77,11 +101,11 @@ def scores_and_reports(score: Score, section: Section) -> ScoresAndReports: @pytest.fixture -def scores_and_reports_raw_score(score: Score, section: Section) -> ScoresAndReports: +def scores_and_reports_raw_score(score_with_subcale_raw: Score, section: Section) -> ScoresAndReports: return ScoresAndReports( generate_report=True, show_score_summary=True, - reports=[score, section], + reports=[score_with_subcale_raw, section], ) @@ -100,13 +124,13 @@ def subscale(subscale_item: SubscaleItem) -> Subscale: @pytest.fixture -def scores_and_reports_lookup_scores(score: Score, section: Section, subscale: Subscale) -> ScoresAndReports: +def scores_and_reports_lookup_scores( + score_with_subcale: Score, section: Section, subscale: Subscale +) -> ScoresAndReports: return ScoresAndReports( generate_report=True, show_score_summary=True, - reports=[score, section], - scoring_type="lookup_scores", - subscale_name=subscale.name, + reports=[score_with_subcale, section], ) @@ -119,7 +143,9 @@ def subscale_item_type_subscale(subscale: Subscale) -> SubscaleItem: @pytest.fixture def subscale_with_item_type_subscale(subscale_item_type_subscale: SubscaleItem) -> Subscale: return Subscale( - name="subscale type subscale", items=[subscale_item_type_subscale], scoring=SubscaleCalculationType.AVERAGE + name="subscale type subscale", + scoring=SubscaleCalculationType.AVERAGE, + items=[subscale_item_type_subscale], ) diff --git a/src/apps/activities/tests/unit/test_activity_change.py b/src/apps/activities/tests/unit/test_activity_change.py index db6b594870f..822d95d6efe 100644 --- a/src/apps/activities/tests/unit/test_activity_change.py +++ b/src/apps/activities/tests/unit/test_activity_change.py @@ -85,11 +85,20 @@ def subscale() -> Subscale: ) +@pytest.fixture +def subscale_score_type() -> Subscale: + return Subscale( + name="subscale type score", + scoring=SubscaleCalculationType.AVERAGE, + items=[SubscaleItem(name="subscale_item", type=SubscaleItemType.ITEM)], + ) + + @pytest.fixture def subscale_setting(subscale: Subscale) -> SubscaleSetting: return SubscaleSetting( calculate_total_score=SubscaleCalculationType.AVERAGE, - subscales=[subscale], + subscales=[subscale_score_type], ) diff --git a/src/apps/applets/tests/test_applet_activity_items.py b/src/apps/applets/tests/test_applet_activity_items.py index 5cfe052ea31..bc405bf3a7d 100644 --- a/src/apps/applets/tests/test_applet_activity_items.py +++ b/src/apps/applets/tests/test_applet_activity_items.py @@ -16,6 +16,7 @@ from apps.activities.domain.scores_reports import ( ScoreConditionalLogic, ScoresAndReports, + ScoringType, SectionConditionalLogic, Subscale, SubScaleLookupTable, @@ -528,13 +529,13 @@ async def test_create_applet__activity_with_subscale_settings_and_lookup_table_a applet_minimal_data: AppletCreate, single_select_item_create_with_score: ActivityItemCreate, tom: User, - subscale_setting: SubscaleSetting, + subscale_setting_score_type: SubscaleSetting, subscale_lookup_table: list[SubScaleLookupTable], scores_and_reports_lookup_scores: ScoresAndReports, ): client.login(tom) data = applet_minimal_data.copy(deep=True) - sub_setting = subscale_setting.copy(deep=True) + sub_setting = subscale_setting_score_type.copy(deep=True) # Update subscale setting with item name and lookup table sub_setting.subscales[0].items[0].name = single_select_item_create_with_score.name # type: ignore[index] @@ -551,8 +552,8 @@ async def test_create_applet__activity_with_subscale_settings_and_lookup_table_a assert resp.status_code == http.HTTPStatus.CREATED result = resp.json()["result"] assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) - assert result["activities"][0]["scoresAndReports"]["scoringType"] == "lookup_scores" - assert result["activities"][0]["scoresAndReports"]["subscaleName"] == "subscale type item" + assert result["activities"][0]["scoresAndReports"]["reports"][0]["scoringType"] == ScoringType.SCORE.value + assert result["activities"][0]["scoresAndReports"]["reports"][0]["subscaleName"] == "subscale type item" async def test_create_applet__activity_with_subscale_settings_with_invalid_subscale_lookup_table_age( self, From 8b47db2ce109a5cf2565430373c48426fd1ce2c7 Mon Sep 17 00:00:00 2001 From: Coro Date: Thu, 10 Oct 2024 09:15:24 -0600 Subject: [PATCH 16/24] Changed subscale definitions inside test script (imports didn't worked) --- .../tests/fixtures/scores_reports.py | 19 ++++++++++++++++++- .../tests/unit/test_activity_change.py | 17 ----------------- .../tests/test_applet_activity_items.py | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index cdd295299cb..db2c544d947 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -56,7 +56,7 @@ def score_with_subcale() -> Score: id=SCORE_ID, calculation_type=CalculationType.SUM, scoring_type="score", - subscale_name="subscale type item", + subscale_name="subscale type score", ) @@ -123,6 +123,15 @@ def subscale(subscale_item: SubscaleItem) -> Subscale: ) +@pytest.fixture +def subscale_score_type() -> Subscale: + return Subscale( + name="subscale type score", + scoring=SubscaleCalculationType.AVERAGE, + items=[SubscaleItem(name="subscale_item", type=SubscaleItemType.ITEM)], + ) + + @pytest.fixture def scores_and_reports_lookup_scores( score_with_subcale: Score, section: Section, subscale: Subscale @@ -157,6 +166,14 @@ def subscale_setting(subscale: Subscale) -> SubscaleSetting: ) +@pytest.fixture +def subscale_setting_score_type(subscale_score_type: Subscale) -> SubscaleSetting: + return SubscaleSetting( + calculate_total_score=SubscaleCalculationType.AVERAGE, + subscales=[subscale_score_type], + ) + + @pytest.fixture def subscale_total_score_table() -> list[TotalScoreTable]: return [ diff --git a/src/apps/activities/tests/unit/test_activity_change.py b/src/apps/activities/tests/unit/test_activity_change.py index 822d95d6efe..d935c3e1550 100644 --- a/src/apps/activities/tests/unit/test_activity_change.py +++ b/src/apps/activities/tests/unit/test_activity_change.py @@ -85,23 +85,6 @@ def subscale() -> Subscale: ) -@pytest.fixture -def subscale_score_type() -> Subscale: - return Subscale( - name="subscale type score", - scoring=SubscaleCalculationType.AVERAGE, - items=[SubscaleItem(name="subscale_item", type=SubscaleItemType.ITEM)], - ) - - -@pytest.fixture -def subscale_setting(subscale: Subscale) -> SubscaleSetting: - return SubscaleSetting( - calculate_total_score=SubscaleCalculationType.AVERAGE, - subscales=[subscale_score_type], - ) - - @pytest.fixture def scores_and_reports(score: Score, section: Section) -> ScoresAndReports: return ScoresAndReports( diff --git a/src/apps/applets/tests/test_applet_activity_items.py b/src/apps/applets/tests/test_applet_activity_items.py index bc405bf3a7d..6b633d4f04b 100644 --- a/src/apps/applets/tests/test_applet_activity_items.py +++ b/src/apps/applets/tests/test_applet_activity_items.py @@ -553,7 +553,7 @@ async def test_create_applet__activity_with_subscale_settings_and_lookup_table_a result = resp.json()["result"] assert result["activities"][0]["subscaleSetting"] == sub_setting.dict(by_alias=True) assert result["activities"][0]["scoresAndReports"]["reports"][0]["scoringType"] == ScoringType.SCORE.value - assert result["activities"][0]["scoresAndReports"]["reports"][0]["subscaleName"] == "subscale type item" + assert result["activities"][0]["scoresAndReports"]["reports"][0]["subscaleName"] == "subscale type score" async def test_create_applet__activity_with_subscale_settings_with_invalid_subscale_lookup_table_age( self, From 5c57ca8cf30c79bb437bd5be4c861a8c0e5d0528 Mon Sep 17 00:00:00 2001 From: Coro Date: Thu, 10 Oct 2024 09:51:13 -0600 Subject: [PATCH 17/24] mypy required additional changes and validations for possible empty attributes --- src/apps/activities/domain/custom_validation.py | 14 ++++++++++---- src/apps/activities/errors.py | 16 +++++++++++++--- src/apps/answers/service.py | 6 ++---- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index ee496adb7f5..0c59d2f8a27 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -21,11 +21,13 @@ IncorrectSubscaleInsideSubscaleError, IncorrectSubscaleItemError, SubscaleDataDoesNotExist, + SubscaleDoesNotExist, SubscaleInsideSubscaleError, SubscaleIsNotLinked, SubscaleItemDoesNotExist, SubscaleItemScoreError, SubscaleItemTypeError, + SubscaleItemTypeItemDoesNotExist, SubscaleNameDoesNotExist, SubscaleSettingDoesNotExist, ) @@ -92,18 +94,22 @@ def validate_score_and_sections(values: dict): # noqa: C901 subscale_setting: SubscaleSetting | None = values.get("subscale_setting", False) if not subscale_setting: raise SubscaleSettingDoesNotExist() + subscales = subscale_setting.subscales + if not subscales: + raise SubscaleDoesNotExist() + + linked_subscale = next((subscale for subscale in subscales if subscale.name == subscale_name), None) - linked_subscale = next( - (subscale for subscale in subscale_setting.subscales if subscale.name == subscale_name), None - ) if not linked_subscale: raise SubscaleNameDoesNotExist() elif not linked_subscale.subscale_table_data: raise SubscaleDataDoesNotExist() + elif not linked_subscale.items: + raise SubscaleItemDoesNotExist() else: has_non_subscale_items = any(item.type == SubscaleItemType.ITEM for item in linked_subscale.items) if not has_non_subscale_items: - raise SubscaleItemDoesNotExist() + raise SubscaleItemTypeItemDoesNotExist() # check if all item names are same as values.name for item in report.items_score: diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index 5a8b73477b3..d116db5fb25 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -273,7 +273,12 @@ class IncorrectPhrasalTemplateItemIndexError(ValidationError): class SubscaleIsNotLinked(ValidationError): - message = _("The scoring_type is score but no subscale is linked") + message = _("The scoring_type is score but no suscale_name string pased") + code = _("no_subscale_items_exist") + + +class SubscaleDoesNotExist(ValidationError): + message = _("The scoring_type is score but there are no subscales") code = _("no_subscale_linked") @@ -293,5 +298,10 @@ class SubscaleSettingDoesNotExist(ValidationError): class SubscaleItemDoesNotExist(ValidationError): - message = _("The linked subscale should contain at least one non-subscale item") - code = _("no_subscale_items_exist") + message = _("The linked subscale should contain at least one item") + code = _("no_items_exist") + + +class SubscaleItemTypeItemDoesNotExist(ValidationError): + message = _("The linked subscale should contain at least one non-subscale type item") + code = _("no_subscale_type_items_exist") diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index f6b6bf1fce1..a3989964155 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -1931,8 +1931,6 @@ async def create_report( ) -> ReportServerResponse | None: answers = await AnswersCRUD(self.answers_session).get_by_submit_id(submit_id, answer_id) subscale_table_data: list[SubScaleLookupTable] | None = None - subscale_name = "" - scoring_type = "" if activity_id: activity_data = await ActivitiesCRUD(self.session).get_by_id(activity_id) @@ -1942,9 +1940,9 @@ async def create_report( selected_report = next((report for report in reports if report["name"] == report_name), reports[0]) - scoring_type = selected_report.get("scoring_type", "") + scoring_type = selected_report.get("scoring_type") if scoring_type == "score": - subscale_name = selected_report.get("subscale_name", "") + subscale_name = selected_report.get("subscale_name") subscales = activity_data.subscale_setting.get("subscales", []) subscale_table_data = next( ( From f44a2a919292446c658f97910c67e9d111418aeb Mon Sep 17 00:00:00 2001 From: Coro Date: Thu, 10 Oct 2024 18:39:58 -0600 Subject: [PATCH 18/24] Deleted subscale_scoring and subscale_name from create_report, now using Score domain class for validation and passing subscale infor to report_server --- .../activities/domain/custom_validation.py | 50 ++++++++----------- src/apps/answers/service.py | 30 +---------- 2 files changed, 23 insertions(+), 57 deletions(-) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 0c59d2f8a27..4be4d98e4b3 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -20,10 +20,8 @@ IncorrectSectionPrintItemTypeError, IncorrectSubscaleInsideSubscaleError, IncorrectSubscaleItemError, - SubscaleDataDoesNotExist, SubscaleDoesNotExist, SubscaleInsideSubscaleError, - SubscaleIsNotLinked, SubscaleItemDoesNotExist, SubscaleItemScoreError, SubscaleItemTypeError, @@ -70,6 +68,27 @@ def validate_item_flow(values: dict): return values +def validate_subscale_setting_match_reports(values: dict): + subscale_setting: SubscaleSetting | None = values.get("subscale_setting", False) + if not subscale_setting: + raise SubscaleSettingDoesNotExist() + for report in values["scores_and_reports"].reports: + report_subscale_linked = report.subscale_name + subscales = subscale_setting.subscales + if not subscales: + raise SubscaleDoesNotExist() + + linked_subscale = next((subscale for subscale in subscales if subscale.name == report_subscale_linked), None) + if not linked_subscale: + raise SubscaleNameDoesNotExist() + elif not linked_subscale.items: + raise SubscaleItemDoesNotExist() + else: + has_non_subscale_items = any(item.type == SubscaleItemType.ITEM for item in linked_subscale.items) + if not has_non_subscale_items: + raise SubscaleItemTypeItemDoesNotExist() + + def validate_score_and_sections(values: dict): # noqa: C901 items = values.get("items", []) item_names = [item.name for item in items] @@ -85,32 +104,7 @@ def validate_score_and_sections(values: dict): # noqa: C901 for report in list(scores): score_item_ids.append(report.id) - subscale_name = report.subscale_name - scoring_type = report.scoring_type - - if scoring_type == "score": - if not subscale_name: - raise SubscaleIsNotLinked() - subscale_setting: SubscaleSetting | None = values.get("subscale_setting", False) - if not subscale_setting: - raise SubscaleSettingDoesNotExist() - subscales = subscale_setting.subscales - if not subscales: - raise SubscaleDoesNotExist() - - linked_subscale = next((subscale for subscale in subscales if subscale.name == subscale_name), None) - - if not linked_subscale: - raise SubscaleNameDoesNotExist() - elif not linked_subscale.subscale_table_data: - raise SubscaleDataDoesNotExist() - elif not linked_subscale.items: - raise SubscaleItemDoesNotExist() - else: - has_non_subscale_items = any(item.type == SubscaleItemType.ITEM for item in linked_subscale.items) - if not has_non_subscale_items: - raise SubscaleItemTypeItemDoesNotExist() - + validate_subscale_setting_match_reports(values) # check if all item names are same as values.name for item in report.items_score: if item not in item_names: diff --git a/src/apps/answers/service.py b/src/apps/answers/service.py index a3989964155..adbc13475c0 100644 --- a/src/apps/answers/service.py +++ b/src/apps/answers/service.py @@ -22,7 +22,6 @@ from apps.activities.crud import ActivitiesCRUD, ActivityHistoriesCRUD, ActivityItemHistoriesCRUD from apps.activities.db.schemas import ActivityItemHistorySchema from apps.activities.domain.activity_history import ActivityHistoryFull -from apps.activities.domain.scores_reports import SubScaleLookupTable from apps.activities.errors import ActivityDoeNotExist, ActivityHistoryDoeNotExist, FlowDoesNotExist from apps.activity_flows.crud import FlowsCRUD, FlowsHistoryCRUD from apps.alerts.crud.alert import AlertCRUD @@ -1367,7 +1366,7 @@ async def get_summary_latest_report( return None service = ReportServerService(self.session, arbitrary_session=self.answer_session) - report = await service.create_report(answer.submit_id, answer.id, activity_id) + report = await service.create_report(answer.submit_id, answer.id) return report async def get_flow_summary_latest_report( @@ -1926,33 +1925,8 @@ async def create_report( self, submit_id: uuid.UUID, answer_id: uuid.UUID | None = None, - activity_id: uuid.UUID | None = None, - report_name: str | None = None, ) -> ReportServerResponse | None: answers = await AnswersCRUD(self.answers_session).get_by_submit_id(submit_id, answer_id) - subscale_table_data: list[SubScaleLookupTable] | None = None - - if activity_id: - activity_data = await ActivitiesCRUD(self.session).get_by_id(activity_id) - reports = activity_data.scores_and_reports.get("reports", []) - if not reports: - return None # Early return if no reports are found - - selected_report = next((report for report in reports if report["name"] == report_name), reports[0]) - - scoring_type = selected_report.get("scoring_type") - if scoring_type == "score": - subscale_name = selected_report.get("subscale_name") - subscales = activity_data.subscale_setting.get("subscales", []) - subscale_table_data = next( - ( - subscale.get("subscale_table_data", []) - for subscale in subscales - if subscale.get("name") == subscale_name - ), - [], - ) - if not answers: return None applet_id_version: str = answers[0].applet_history_id @@ -1985,8 +1959,6 @@ async def create_report( now=datetime.datetime.utcnow().strftime("%x"), user=user_info, applet=applet_full, - scoringType=scoring_type, - subscaleTableData=subscale_table_data, ) encrypted_data = encryption.encrypt(data) From 70d476ea585357bfb16f29ae97b46252f5cd57d3 Mon Sep 17 00:00:00 2001 From: Coro Date: Fri, 11 Oct 2024 09:16:22 -0600 Subject: [PATCH 19/24] Fixed validatio to execure only validate_subscale_setting_match_reports when the report is of scoring_type = score --- .../activities/domain/custom_validation.py | 37 +++++++++++-------- .../tests/fixtures/scores_reports.py | 2 +- .../tests/unit/test_activity_change.py | 8 ++++ 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 4be4d98e4b3..0d24e6537ce 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -70,23 +70,28 @@ def validate_item_flow(values: dict): def validate_subscale_setting_match_reports(values: dict): subscale_setting: SubscaleSetting | None = values.get("subscale_setting", False) - if not subscale_setting: - raise SubscaleSettingDoesNotExist() - for report in values["scores_and_reports"].reports: - report_subscale_linked = report.subscale_name - subscales = subscale_setting.subscales - if not subscales: - raise SubscaleDoesNotExist() + score_reports = [report for report in values["scores_and_reports"].reports if report.type == ReportType.score] + for report in score_reports: + scoring_type = report.scoring_type + if scoring_type == "score": + if not subscale_setting: # report of type score exist then we need a subscale setting + raise SubscaleSettingDoesNotExist() + report_subscale_linked = report.subscale_name + subscales = subscale_setting.subscales + if not subscales: + raise SubscaleDoesNotExist() - linked_subscale = next((subscale for subscale in subscales if subscale.name == report_subscale_linked), None) - if not linked_subscale: - raise SubscaleNameDoesNotExist() - elif not linked_subscale.items: - raise SubscaleItemDoesNotExist() - else: - has_non_subscale_items = any(item.type == SubscaleItemType.ITEM for item in linked_subscale.items) - if not has_non_subscale_items: - raise SubscaleItemTypeItemDoesNotExist() + linked_subscale = next( + (subscale for subscale in subscales if subscale.name == report_subscale_linked), None + ) + if not linked_subscale: + raise SubscaleNameDoesNotExist() + elif not linked_subscale.items: + raise SubscaleItemDoesNotExist() + else: + has_non_subscale_items = any(item.type == SubscaleItemType.ITEM for item in linked_subscale.items) + if not has_non_subscale_items: + raise SubscaleItemTypeItemDoesNotExist() def validate_score_and_sections(values: dict): # noqa: C901 diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index db2c544d947..484aaedf5b7 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -139,7 +139,7 @@ def scores_and_reports_lookup_scores( return ScoresAndReports( generate_report=True, show_score_summary=True, - reports=[score_with_subcale, section], + reports=[score_with_subcale], ) diff --git a/src/apps/activities/tests/unit/test_activity_change.py b/src/apps/activities/tests/unit/test_activity_change.py index d935c3e1550..db6b594870f 100644 --- a/src/apps/activities/tests/unit/test_activity_change.py +++ b/src/apps/activities/tests/unit/test_activity_change.py @@ -85,6 +85,14 @@ def subscale() -> Subscale: ) +@pytest.fixture +def subscale_setting(subscale: Subscale) -> SubscaleSetting: + return SubscaleSetting( + calculate_total_score=SubscaleCalculationType.AVERAGE, + subscales=[subscale], + ) + + @pytest.fixture def scores_and_reports(score: Score, section: Section) -> ScoresAndReports: return ScoresAndReports( From 42123ba892bc3242e2f5e8bc275094f74dc887a2 Mon Sep 17 00:00:00 2001 From: Coro Date: Fri, 11 Oct 2024 09:23:05 -0600 Subject: [PATCH 20/24] Subscale misspelling --- src/apps/activities/errors.py | 2 +- src/apps/activities/tests/fixtures/scores_reports.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index d116db5fb25..e36abcd2c3e 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -283,7 +283,7 @@ class SubscaleDoesNotExist(ValidationError): class SubscaleNameDoesNotExist(ValidationError): - message = _("The lookup table with the passed name does not exist in subcale settings") + message = _("The lookup table with the passed name does not exist in subscale settings") code = _("no_subscale_name_exist") diff --git a/src/apps/activities/tests/fixtures/scores_reports.py b/src/apps/activities/tests/fixtures/scores_reports.py index 484aaedf5b7..1898ef9824a 100644 --- a/src/apps/activities/tests/fixtures/scores_reports.py +++ b/src/apps/activities/tests/fixtures/scores_reports.py @@ -49,7 +49,7 @@ def score() -> Score: @pytest.fixture -def score_with_subcale() -> Score: +def score_with_subscale() -> Score: return Score( type=ReportType.score, name="testscore type score", @@ -61,7 +61,7 @@ def score_with_subcale() -> Score: @pytest.fixture -def score_with_subcale_raw() -> Score: +def score_with_subscale_raw() -> Score: return Score( type=ReportType.score, name="testscore type score", @@ -101,11 +101,11 @@ def scores_and_reports(score: Score, section: Section) -> ScoresAndReports: @pytest.fixture -def scores_and_reports_raw_score(score_with_subcale_raw: Score, section: Section) -> ScoresAndReports: +def scores_and_reports_raw_score(score_with_subscale_raw: Score, section: Section) -> ScoresAndReports: return ScoresAndReports( generate_report=True, show_score_summary=True, - reports=[score_with_subcale_raw, section], + reports=[score_with_subscale_raw, section], ) @@ -134,12 +134,12 @@ def subscale_score_type() -> Subscale: @pytest.fixture def scores_and_reports_lookup_scores( - score_with_subcale: Score, section: Section, subscale: Subscale + score_with_subscale: Score, section: Section, subscale: Subscale ) -> ScoresAndReports: return ScoresAndReports( generate_report=True, show_score_summary=True, - reports=[score_with_subcale], + reports=[score_with_subscale], ) From 66819bbcd8329d283b122aa7cacd3b45f6c8e8c6 Mon Sep 17 00:00:00 2001 From: Coro Date: Fri, 11 Oct 2024 09:54:42 -0600 Subject: [PATCH 21/24] Applied refactorization to validate_subscale_setting_match_reports --- .../activities/domain/custom_validation.py | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index 0d24e6537ce..3e9569ad50c 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -1,7 +1,7 @@ from apps.activities.domain.conditions import MultiSelectConditionType, SingleSelectConditionType from apps.activities.domain.response_type_config import PerformanceTaskType, ResponseType from apps.activities.domain.response_values import PhrasalTemplateFieldType -from apps.activities.domain.scores_reports import ReportType, SubscaleItemType, SubscaleSetting +from apps.activities.domain.scores_reports import ReportType, Score, SubscaleItemType, SubscaleSetting from apps.activities.errors import ( IncorrectConditionItemError, IncorrectConditionItemIndexError, @@ -68,30 +68,23 @@ def validate_item_flow(values: dict): return values -def validate_subscale_setting_match_reports(values: dict): - subscale_setting: SubscaleSetting | None = values.get("subscale_setting", False) - score_reports = [report for report in values["scores_and_reports"].reports if report.type == ReportType.score] - for report in score_reports: - scoring_type = report.scoring_type - if scoring_type == "score": - if not subscale_setting: # report of type score exist then we need a subscale setting - raise SubscaleSettingDoesNotExist() - report_subscale_linked = report.subscale_name - subscales = subscale_setting.subscales - if not subscales: - raise SubscaleDoesNotExist() +def validate_subscale_setting_match_reports(report: Score, subscale_setting: SubscaleSetting): + if not subscale_setting: # report of type score exist then we need a subscale setting + raise SubscaleSettingDoesNotExist() + report_subscale_linked = report.subscale_name + subscales = subscale_setting.subscales + if not subscales: + raise SubscaleDoesNotExist() - linked_subscale = next( - (subscale for subscale in subscales if subscale.name == report_subscale_linked), None - ) - if not linked_subscale: - raise SubscaleNameDoesNotExist() - elif not linked_subscale.items: - raise SubscaleItemDoesNotExist() - else: - has_non_subscale_items = any(item.type == SubscaleItemType.ITEM for item in linked_subscale.items) - if not has_non_subscale_items: - raise SubscaleItemTypeItemDoesNotExist() + linked_subscale = next((subscale for subscale in subscales if subscale.name == report_subscale_linked), None) + if not linked_subscale: + raise SubscaleNameDoesNotExist() + elif not linked_subscale.items: + raise SubscaleItemDoesNotExist() + else: + has_non_subscale_items = any(item.type == SubscaleItemType.ITEM for item in linked_subscale.items) + if not has_non_subscale_items: + raise SubscaleItemTypeItemDoesNotExist() def validate_score_and_sections(values: dict): # noqa: C901 @@ -109,7 +102,10 @@ def validate_score_and_sections(values: dict): # noqa: C901 for report in list(scores): score_item_ids.append(report.id) - validate_subscale_setting_match_reports(values) + if report.scoring_type == "score": + subscale_setting = values.get("subscale_setting") + validate_subscale_setting_match_reports(report, subscale_setting) + # check if all item names are same as values.name for item in report.items_score: if item not in item_names: From d3c73252cac509d2d0a1b7b4f41e39f381efcbbe Mon Sep 17 00:00:00 2001 From: Coro Date: Tue, 15 Oct 2024 09:19:52 -0600 Subject: [PATCH 22/24] Fixed tests on test_custom_validation that required typing for payload argument, fixed mypy for reports custom_validation by checkiing for None SusbcaleStting on Score with score_type score --- src/apps/activities/domain/conditions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/activities/domain/conditions.py b/src/apps/activities/domain/conditions.py index 98c35cbb81d..aa51f6f739b 100644 --- a/src/apps/activities/domain/conditions.py +++ b/src/apps/activities/domain/conditions.py @@ -152,7 +152,7 @@ def dict(self, *args, **kwargs): class TimePayload(PublicModel): - type: str | None = None + type: TimePayloadType | None = None value: datetime.time def dict(self, *args, **kwargs): From 3dbf416755862759cabd37fba197e2f01570244b Mon Sep 17 00:00:00 2001 From: Coro Date: Tue, 15 Oct 2024 12:08:33 -0600 Subject: [PATCH 23/24] Fixed test_custom_validation tests --- src/apps/activities/domain/conditions.py | 31 ++++++++++++++-- .../activities/domain/custom_validation.py | 7 ++-- src/apps/activities/errors.py | 8 +++++ .../unit/domain/test_custom_validation.py | 36 ++++++++++--------- 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/src/apps/activities/domain/conditions.py b/src/apps/activities/domain/conditions.py index aa51f6f739b..a092f7ff050 100644 --- a/src/apps/activities/domain/conditions.py +++ b/src/apps/activities/domain/conditions.py @@ -4,6 +4,7 @@ from pydantic import Field, root_validator, validator +from apps.activities.errors import IncorrectMaxTimeRange, IncorrectMinTimeRange, IncorrectTimeRange from apps.shared.domain import PublicModel, PublicModelNoExtra @@ -163,14 +164,40 @@ def dict(self, *args, **kwargs): class SingleTimePayload(PublicModel): time: Optional[datetime.time] = None - + max_value: Optional[datetime.time] = None + min_value: Optional[datetime.time] = None + @root_validator(pre=True) def validate_time(cls, values: Dict[str, Any]) -> Dict[str, Any]: time_value = values.get("time") + max_time_value = values.get("max_value") + min_time_value = values.get("min_value") + if isinstance(time_value, dict): values["time"] = cls._dict_to_time(time_value) elif isinstance(time_value, str): values["time"] = cls._string_to_time(time_value) + if max_time_value and min_time_value: + if isinstance(max_time_value, dict): + max_time_value = cls._dict_to_time(max_time_value) + elif isinstance(max_time_value, str): + max_time_value = cls._string_to_time(max_time_value) + + if isinstance(min_time_value, dict): + min_time_value = cls._dict_to_time(min_time_value) + elif isinstance(min_time_value, str): + min_time_value = cls._string_to_time(min_time_value) + + if max_time_value < min_time_value: + raise IncorrectTimeRange() + + if min_time_value: + if not max_time_value: + raise IncorrectMaxTimeRange() + if max_time_value: + if not min_time_value: + raise IncorrectMinTimeRange() + return values def dict(self, *args, **kwargs) -> Dict[str, Any]: @@ -554,7 +581,7 @@ class EqualToDateCondition(_EqualToDateCondition): class EqualCondition(_EqualCondition): - payload: ValuePayload | ValueIndexPayload | TimePayload + payload: ValuePayload | ValueIndexPayload | TimePayload | SingleTimePayload class NotEqualToDateCondition(_NotEqualToDateCondition): diff --git a/src/apps/activities/domain/custom_validation.py b/src/apps/activities/domain/custom_validation.py index c92ac368203..363f47f096e 100644 --- a/src/apps/activities/domain/custom_validation.py +++ b/src/apps/activities/domain/custom_validation.py @@ -74,8 +74,6 @@ def validate_item_flow(values: dict): def validate_subscale_setting_match_reports(report: Score, subscale_setting: SubscaleSetting): - if not subscale_setting: # report of type score exist then we need a subscale setting - raise SubscaleSettingDoesNotExist() report_subscale_linked = report.subscale_name subscales = subscale_setting.subscales if not subscales: @@ -109,7 +107,10 @@ def validate_score_and_sections(values: dict): # noqa: C901 score_item_ids.append(report.id) if report.scoring_type == "score": subscale_setting = values.get("subscale_setting") - validate_subscale_setting_match_reports(report, subscale_setting) + if not subscale_setting: # report of type score exist then we need a subscale setting + raise SubscaleSettingDoesNotExist() + else: + validate_subscale_setting_match_reports(report, subscale_setting) # check if all item names are same as values.name for item in report.items_score: diff --git a/src/apps/activities/errors.py b/src/apps/activities/errors.py index 288974ea585..a7a5b8d7484 100644 --- a/src/apps/activities/errors.py +++ b/src/apps/activities/errors.py @@ -260,6 +260,14 @@ class IncorrectTimeRange(ValidationError): message = _("Incorrect timerange") +class IncorrectMinTimeRange(ValidationError): + message = _("Mix timerange was not passed") + + +class IncorrectMaxTimeRange(ValidationError): + message = _("Max timerange was not passed") + + class FlowDoesNotExist(NotFoundError): message = _("Flow does not exist.") diff --git a/src/apps/activities/tests/unit/domain/test_custom_validation.py b/src/apps/activities/tests/unit/domain/test_custom_validation.py index ac50cd77b8e..1710f15b9ff 100644 --- a/src/apps/activities/tests/unit/domain/test_custom_validation.py +++ b/src/apps/activities/tests/unit/domain/test_custom_validation.py @@ -220,23 +220,25 @@ def test_validator_successful_create_outside_condition(self, payload): payload=payload, ) - @pytest.mark.parametrize( - "payload_type,payload,exp_exception", - ( - (SingleDatePayload, dict(date="1970-99-01"), ValidationError), - (SingleTimePayload, dict(time={"hours": 80, "minutes": 0}), ValueError), # Adjusted time dict - ( - SingleTimePayload, - dict(time={"hours": 3, "minutes": 0}, min_value="03:00", max_value="02:00"), - IncorrectTimeRange, - ), - (SingleTimePayload, dict(time={"hours": 3, "minutes": 0}), ValidationError), - (SingleTimePayload, dict(time="unknown_item_type", min_value="01:00", max_value="02:00"), ValidationError), - ), - ) - def test_fails_create_outside_condition(self, payload_type, payload, exp_exception): - with pytest.raises(exp_exception): - payload_type(**payload) + def test_single_date_payload_invalid_date(self): + with pytest.raises(ValidationError): + SingleDatePayload(date="1970-99-01") + + def test_single_time_payload_invalid_hours(self): + with pytest.raises(ValueError): + SingleTimePayload(time={"hours": 80, "minutes": 0}) + + def test_single_time_payload_incorrect_time_range(self): + with pytest.raises(IncorrectTimeRange): + SingleTimePayload(time={"hours": 3, "minutes": 0}, min_value="03:00", max_value="02:00") + + # def test_single_time_payload_valid_but_fails_validation(self): + # with pytest.raises(ValidationError): #Not sure here what you want to test. I'm assuming that it is that if min_value and max_value are set, the + # SingleTimePayload(time={"hours": 3, "minutes": 0}) + + def test_single_time_payload_unknown_item_type(self): + with pytest.raises(ValidationError): + SingleTimePayload(time="unknown_item_type", min_value="01:00", max_value="02:00") class TestValidateScoreAndSections: From 47c1e17acac5339f1f5ad5e536e2bd7e7bb86443 Mon Sep 17 00:00:00 2001 From: Coro Date: Tue, 15 Oct 2024 14:37:37 -0600 Subject: [PATCH 24/24] Refactor test_create_activity_item_conditional_logic and updated SinglePayload to consider min and max socores with numeric values, 0 being a feasible option --- src/apps/activities/domain/conditions.py | 20 +- .../unit/domain/test_activity_item_create.py | 174 +++++++++++++----- .../unit/domain/test_custom_validation.py | 4 - 3 files changed, 139 insertions(+), 59 deletions(-) diff --git a/src/apps/activities/domain/conditions.py b/src/apps/activities/domain/conditions.py index a092f7ff050..bc3b734af11 100644 --- a/src/apps/activities/domain/conditions.py +++ b/src/apps/activities/domain/conditions.py @@ -166,13 +166,13 @@ class SingleTimePayload(PublicModel): time: Optional[datetime.time] = None max_value: Optional[datetime.time] = None min_value: Optional[datetime.time] = None - + @root_validator(pre=True) def validate_time(cls, values: Dict[str, Any]) -> Dict[str, Any]: time_value = values.get("time") max_time_value = values.get("max_value") min_time_value = values.get("min_value") - + if isinstance(time_value, dict): values["time"] = cls._dict_to_time(time_value) elif isinstance(time_value, str): @@ -182,22 +182,22 @@ def validate_time(cls, values: Dict[str, Any]) -> Dict[str, Any]: max_time_value = cls._dict_to_time(max_time_value) elif isinstance(max_time_value, str): max_time_value = cls._string_to_time(max_time_value) - + if isinstance(min_time_value, dict): min_time_value = cls._dict_to_time(min_time_value) elif isinstance(min_time_value, str): min_time_value = cls._string_to_time(min_time_value) - + if max_time_value < min_time_value: raise IncorrectTimeRange() - - if min_time_value: - if not max_time_value: + + if min_time_value is not None: + if max_time_value is None: raise IncorrectMaxTimeRange() - if max_time_value: - if not min_time_value: + if max_time_value is not None: + if min_time_value is None: raise IncorrectMinTimeRange() - + return values def dict(self, *args, **kwargs) -> Dict[str, Any]: diff --git a/src/apps/activities/tests/unit/domain/test_activity_item_create.py b/src/apps/activities/tests/unit/domain/test_activity_item_create.py index eaa4a4a8782..7aab778d997 100644 --- a/src/apps/activities/tests/unit/domain/test_activity_item_create.py +++ b/src/apps/activities/tests/unit/domain/test_activity_item_create.py @@ -599,54 +599,138 @@ def test_create_message_item__sanitize_question(message_item_create): assert item.question["en"] == "One Two" -@pytest.mark.parametrize( - "response_type, config_fixture, cnd_logic_fixture, response_values_fixture", - ( - (ResponseType.SINGLESELECT, "single_select_config", "conditional_logic_equal", "single_select_response_values"), - (ResponseType.MULTISELECT, "multi_select_config", "conditional_logic_equal", "multi_select_response_values"), - (ResponseType.SLIDER, "slider_config", "conditional_logic_between", "slider_response_values"), - (ResponseType.TIME, "time_config", "conditional_logic_between", None), - (ResponseType.TIMERANGE, "time_range_config", "conditional_logic_between", None), - ( - ResponseType.NUMBERSELECT, - "number_selection_config", - "conditional_logic_between", - "number_selection_response_values", - ), - (ResponseType.DATE, "date_config", "conditional_logic_equal", None), - ( - ResponseType.SINGLESELECTROWS, - "single_select_row_config", - "conditional_logic_equal", - "single_select_row_response_values", - ), - ( - ResponseType.MULTISELECTROWS, - "multi_select_row_config", - "conditional_logic_equal", - "multi_select_row_response_values", - ), - ( - ResponseType.SLIDERROWS, - "slider_rows_config", - "conditional_logic_rows_outside_of", - "slider_rows_response_values", - ), - ), -) -def test_create_activity_item_conditional_logic( - base_item_data, request, response_type, config_fixture, cnd_logic_fixture, response_values_fixture -) -> None: - config = request.getfixturevalue(config_fixture) - cnd_logic = request.getfixturevalue(cnd_logic_fixture) - if response_values_fixture: - response_values = request.getfixturevalue(response_values_fixture) - else: - response_values = None +def test_single_select_logic(base_item_data, request) -> None: + config = request.getfixturevalue("single_select_config") + cnd_logic = request.getfixturevalue("conditional_logic_equal") + response_values = request.getfixturevalue("single_select_response_values") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.SINGLESELECT, + conditional_logic=cnd_logic, + response_values=response_values, + ) + + +def test_multi_select_logic(base_item_data, request) -> None: + config = request.getfixturevalue("multi_select_config") + cnd_logic = request.getfixturevalue("conditional_logic_equal") + response_values = request.getfixturevalue("multi_select_response_values") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.MULTISELECT, + conditional_logic=cnd_logic, + response_values=response_values, + ) + + +def test_slider_logic(base_item_data, request) -> None: + config = request.getfixturevalue("slider_config") + cnd_logic = request.getfixturevalue("conditional_logic_between") + response_values = request.getfixturevalue("slider_response_values") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.SLIDER, + conditional_logic=cnd_logic, + response_values=response_values, + ) + + +def test_time_logic(base_item_data, request) -> None: + config = request.getfixturevalue("time_config") + cnd_logic = request.getfixturevalue("conditional_logic_between") + ActivityItemCreate( **base_item_data.dict(), config=config, - response_type=response_type, + response_type=ResponseType.TIME, + conditional_logic=cnd_logic, + response_values=None, + ) + + +def test_time_range_logic(base_item_data, request) -> None: + config = request.getfixturevalue("time_range_config") + cnd_logic = request.getfixturevalue("conditional_logic_between") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.TIMERANGE, + conditional_logic=cnd_logic, + response_values=None, + ) + + +def test_number_select_logic(base_item_data, request) -> None: + config = request.getfixturevalue("number_selection_config") + cnd_logic = request.getfixturevalue("conditional_logic_between") + response_values = request.getfixturevalue("number_selection_response_values") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.NUMBERSELECT, + conditional_logic=cnd_logic, + response_values=response_values, + ) + + +def test_date_logic(base_item_data, request) -> None: + config = request.getfixturevalue("date_config") + cnd_logic = request.getfixturevalue("conditional_logic_equal") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.DATE, + conditional_logic=cnd_logic, + response_values=None, + ) + + +def test_single_select_row_logic(base_item_data, request) -> None: + config = request.getfixturevalue("single_select_row_config") + cnd_logic = request.getfixturevalue("conditional_logic_equal") + response_values = request.getfixturevalue("single_select_row_response_values") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.SINGLESELECTROWS, + conditional_logic=cnd_logic, + response_values=response_values, + ) + + +def test_multi_select_row_logic(base_item_data, request) -> None: + config = request.getfixturevalue("multi_select_row_config") + cnd_logic = request.getfixturevalue("conditional_logic_equal") + response_values = request.getfixturevalue("multi_select_row_response_values") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.MULTISELECTROWS, + conditional_logic=cnd_logic, + response_values=response_values, + ) + + +def test_slider_rows_logic(base_item_data, request) -> None: + config = request.getfixturevalue("slider_rows_config") + cnd_logic = request.getfixturevalue("conditional_logic_rows_outside_of") + response_values = request.getfixturevalue("slider_rows_response_values") + + ActivityItemCreate( + **base_item_data.dict(), + config=config, + response_type=ResponseType.SLIDERROWS, conditional_logic=cnd_logic, response_values=response_values, ) diff --git a/src/apps/activities/tests/unit/domain/test_custom_validation.py b/src/apps/activities/tests/unit/domain/test_custom_validation.py index 1710f15b9ff..331761bf84a 100644 --- a/src/apps/activities/tests/unit/domain/test_custom_validation.py +++ b/src/apps/activities/tests/unit/domain/test_custom_validation.py @@ -232,10 +232,6 @@ def test_single_time_payload_incorrect_time_range(self): with pytest.raises(IncorrectTimeRange): SingleTimePayload(time={"hours": 3, "minutes": 0}, min_value="03:00", max_value="02:00") - # def test_single_time_payload_valid_but_fails_validation(self): - # with pytest.raises(ValidationError): #Not sure here what you want to test. I'm assuming that it is that if min_value and max_value are set, the - # SingleTimePayload(time={"hours": 3, "minutes": 0}) - def test_single_time_payload_unknown_item_type(self): with pytest.raises(ValidationError): SingleTimePayload(time="unknown_item_type", min_value="01:00", max_value="02:00")