Skip to content

Commit e0bff6f

Browse files
authored
Merge pull request #1804 from ChildMindInstitute/release/2025.04.0
Release/2025.04.0 [main]
2 parents 0fd6a4e + 15117d0 commit e0bff6f

16 files changed

Lines changed: 185 additions & 2 deletions

File tree

src/apps/activities/domain/activity_create.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
validate_item_flow,
1010
validate_performance_task_type,
1111
validate_phrasal_templates,
12+
validate_request_health_record_data,
1213
validate_score_and_sections,
1314
validate_subscales,
1415
)
@@ -60,3 +61,7 @@ def validate_performance_task_type(cls, values):
6061
@root_validator()
6162
def validate_phrasal_templates(cls, values):
6263
return validate_phrasal_templates(values)
64+
65+
@root_validator()
66+
def validate_request_health_record_data(cls, values):
67+
return validate_request_health_record_data(values)

src/apps/activities/domain/activity_update.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
validate_item_flow,
1010
validate_performance_task_type,
1111
validate_phrasal_templates,
12+
validate_request_health_record_data,
1213
validate_score_and_sections,
1314
validate_subscales,
1415
)
@@ -61,6 +62,10 @@ def validate_performance_task_type(cls, values):
6162
def validate_phrasal_templates(cls, values):
6263
return validate_phrasal_templates(values)
6364

65+
@root_validator()
66+
def validate_request_health_record_data(cls, values):
67+
return validate_request_health_record_data(values)
68+
6469

6570
class ActivityReportConfiguration(PublicModel):
6671
report_included_item_name: str | None

src/apps/activities/domain/custom_validation.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from apps.activities.domain.response_type_config import PerformanceTaskType, ResponseType
2-
from apps.activities.domain.response_values import PhrasalTemplateFieldType
2+
from apps.activities.domain.response_values import PhrasalTemplateFieldType, RequestHealthRecordDataOptType
33
from apps.activities.domain.scores_reports import ReportType, Score, SubscaleItemType, SubscaleSetting
44
from apps.activities.errors import (
55
IncorrectConditionItemError,
@@ -274,3 +274,33 @@ def validate_phrasal_templates(values: dict):
274274
raise IncorrectPhrasalTemplateItemIndexError()
275275

276276
return values
277+
278+
279+
def validate_request_health_record_data(values: dict):
280+
items = values.get("items", [])
281+
for item in items:
282+
if item.response_type == ResponseType.REQUEST_HEALTH_RECORD_DATA:
283+
if item.response_values.opt_in_out_options is None or len(item.response_values.opt_in_out_options) != 2:
284+
raise ValueError("Request Health Record Data item must have 2 opt-in/out options")
285+
opt_in_item = next(
286+
(
287+
option
288+
for option in item.response_values.opt_in_out_options
289+
if option.id == RequestHealthRecordDataOptType.OPT_IN
290+
),
291+
None,
292+
)
293+
if opt_in_item is None:
294+
raise ValueError("Request Health Record Data item must have opt-in option")
295+
opt_out_item = next(
296+
(
297+
option
298+
for option in item.response_values.opt_in_out_options
299+
if option.id == RequestHealthRecordDataOptType.OPT_OUT
300+
),
301+
None,
302+
)
303+
if opt_out_item is None:
304+
raise ValueError("Request Health Record Data item must have opt-out option")
305+
306+
return values

src/apps/activities/domain/response_type_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class ResponseType(StrEnum):
5656
ABTRAILS = "ABTrails"
5757
UNITY = "unity"
5858
PHRASAL_TEMPLATE = "phrasalTemplate"
59+
REQUEST_HEALTH_RECORD_DATA = "requestHealthRecordData"
5960

6061
@classmethod
6162
def get_non_response_types(cls):
@@ -263,6 +264,11 @@ class PhrasalTemplateConfig(PublicModel):
263264
remove_back_button: bool
264265

265266

267+
class RequestHealthRecordDataConfig(PublicModel):
268+
type: Literal[ResponseType.REQUEST_HEALTH_RECORD_DATA] | None
269+
remove_back_button: bool
270+
271+
266272
class UnityConfig(PublicModel):
267273
type: Literal[ResponseType.UNITY] | None
268274
device_type: str | None
@@ -472,4 +478,5 @@ def get_values(cls) -> list[str]:
472478
| ABTrailsConfig
473479
| PhrasalTemplateConfig
474480
| UnityConfig
481+
| RequestHealthRecordDataConfig
475482
)

src/apps/activities/domain/response_values.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
ParagraphTextConfig,
2020
PhotoConfig,
2121
PhrasalTemplateConfig,
22+
RequestHealthRecordDataConfig,
2223
ResponseType,
2324
SingleSelectionConfig,
2425
SingleSelectionRowsConfig,
@@ -41,6 +42,11 @@
4142
from apps.shared.domain import PublicModel, validate_color, validate_image, validate_uuid
4243

4344

45+
class RequestHealthRecordDataOptType(StrEnum):
46+
OPT_IN = "opt_in"
47+
OPT_OUT = "opt_out"
48+
49+
4450
class PhrasalTemplateFieldType(StrEnum):
4551
SENTENCE = "sentence"
4652
ITEM_RESPONSE = "item_response"
@@ -388,6 +394,16 @@ class PhrasalTemplateValues(PublicModel):
388394
phrases: list[PhrasalTemplatePhrase]
389395

390396

397+
class RequestHealthRecordDataOption(PublicModel):
398+
id: RequestHealthRecordDataOptType = Field(default=...) # ellipsis indicates that the field is required
399+
label: str = Field(default=...)
400+
401+
402+
class RequestHealthRecordDataValues(PublicModel):
403+
type: Literal[ResponseType.REQUEST_HEALTH_RECORD_DATA] | None
404+
opt_in_out_options: list[RequestHealthRecordDataOption]
405+
406+
391407
ResponseValueConfigOptions = [
392408
TextValues,
393409
ParagraphTextValues,
@@ -413,6 +429,7 @@ class PhrasalTemplateValues(PublicModel):
413429
ABTrailsValues,
414430
UnityValues,
415431
PhrasalTemplateValues,
432+
RequestHealthRecordDataValues,
416433
]
417434

418435

@@ -430,6 +447,7 @@ class PhrasalTemplateValues(PublicModel):
430447
| TimeValues
431448
| UnityValues
432449
| PhrasalTemplateValues
450+
| RequestHealthRecordDataValues
433451
)
434452

435453

@@ -486,6 +504,7 @@ def validate_none_option_flag(options):
486504
ABTrailsConfig,
487505
UnityConfig,
488506
PhrasalTemplateConfig,
507+
RequestHealthRecordDataConfig,
489508
]
490509

491510
ResponseTypeValueConfig = {}

src/apps/activities/tests/fixtures/configs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
ParagraphTextConfig,
1616
PhotoConfig,
1717
PhrasalTemplateConfig,
18+
RequestHealthRecordDataConfig,
1819
ResponseType,
1920
SingleSelectionConfig,
2021
SingleSelectionRowsConfig,
@@ -186,3 +187,8 @@ def phrasal_template_config(default_config: DefaultConfig) -> PhrasalTemplateCon
186187
@pytest.fixture
187188
def unity_config(default_config: DefaultConfig) -> UnityConfig:
188189
return UnityConfig(**default_config.dict(), type=ResponseType.UNITY)
190+
191+
192+
@pytest.fixture
193+
def request_health_record_data_config(default_config: DefaultConfig) -> RequestHealthRecordDataConfig:
194+
return RequestHealthRecordDataConfig(**default_config.dict(), type=ResponseType.REQUEST_HEALTH_RECORD_DATA)

src/apps/activities/tests/fixtures/items.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ParagraphTextConfig,
1515
PhotoConfig,
1616
PhrasalTemplateConfig,
17+
RequestHealthRecordDataConfig,
1718
ResponseType,
1819
SingleSelectionConfig,
1920
SingleSelectionRowsConfig,
@@ -32,6 +33,7 @@
3233
MultiSelectionValues,
3334
NumberSelectionValues,
3435
PhrasalTemplateValues,
36+
RequestHealthRecordDataValues,
3537
SingleSelectionRowsValues,
3638
SingleSelectionValues,
3739
SliderRowsValues,
@@ -334,3 +336,17 @@ def phrasal_template_with_paragraph_create(
334336
)
335337

336338
return [paragraph_text_item_create, phrasal_item]
339+
340+
341+
@pytest.fixture
342+
def request_health_record_data_create(
343+
request_health_record_data_config: RequestHealthRecordDataConfig,
344+
request_health_record_data_response_values: RequestHealthRecordDataValues,
345+
base_item_data: BaseItemData,
346+
) -> ActivityItemCreate:
347+
return ActivityItemCreate(
348+
**base_item_data.dict(),
349+
response_type=ResponseType.REQUEST_HEALTH_RECORD_DATA,
350+
config=request_health_record_data_config,
351+
response_values=request_health_record_data_response_values,
352+
)

src/apps/activities/tests/fixtures/response_values.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
PhrasalTemplateField,
1818
PhrasalTemplatePhrase,
1919
PhrasalTemplateValues,
20+
RequestHealthRecordDataOption,
21+
RequestHealthRecordDataOptType,
22+
RequestHealthRecordDataValues,
2023
SingleSelectionRowsValues,
2124
SingleSelectionValues,
2225
SliderRowsValue,
@@ -263,3 +266,21 @@ def phrasal_template_with_paragraph_response_values(
263266
card_title="test paragraph card title",
264267
type=ResponseType.PHRASAL_TEMPLATE,
265268
)
269+
270+
271+
@pytest.fixture
272+
def request_health_record_data_response_values() -> RequestHealthRecordDataValues:
273+
opt_in_out_options = [
274+
RequestHealthRecordDataOption(
275+
id=RequestHealthRecordDataOptType.OPT_IN,
276+
label="Opt In label",
277+
),
278+
RequestHealthRecordDataOption(
279+
id=RequestHealthRecordDataOptType.OPT_OUT,
280+
label="Opt Out label",
281+
),
282+
]
283+
284+
return RequestHealthRecordDataValues(
285+
type=ResponseType.REQUEST_HEALTH_RECORD_DATA, opt_in_out_options=opt_in_out_options
286+
)

src/apps/applets/tests/test_applet_activity_items.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class TestActivityItems:
9494
"message_item_create",
9595
"audio_player_item_create",
9696
"paragraph_text_item_create",
97+
"request_health_record_data_create",
9798
),
9899
)
99100
async def test_create_applet_with_each_activity_item(
@@ -211,6 +212,38 @@ async def test_create_applet_with_performance_task(
211212
assert resp.json()["result"]["activities"][0]["isPerformanceTask"]
212213
assert resp.json()["result"]["activities"][0]["performanceTaskType"] == performance_task_type
213214

215+
async def test_create_applet_with_request_health_record_data_invalid(
216+
self,
217+
client: TestClient,
218+
tom: User,
219+
applet_minimal_data: AppletCreate,
220+
request_health_record_data_create: ActivityItemCreate,
221+
):
222+
client.login(tom)
223+
data = applet_minimal_data.copy(deep=True)
224+
data.activities[0].items = [request_health_record_data_create]
225+
226+
data_dict = data.dict(by_alias=True)
227+
data_dict["activities"][0]["items"][0]["responseValues"]["optInOutOptions"][1]["id"] = "opt_in"
228+
229+
resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data_dict)
230+
assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY
231+
assert resp.json()["result"][0]["message"] == "Request Health Record Data item must have opt-out option"
232+
233+
data_dict = data.dict(by_alias=True)
234+
data_dict["activities"][0]["items"][0]["responseValues"]["optInOutOptions"][0]["id"] = "opt_out"
235+
236+
resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data_dict)
237+
assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY
238+
assert resp.json()["result"][0]["message"] == "Request Health Record Data item must have opt-in option"
239+
240+
data_dict = data.dict(by_alias=True)
241+
data_dict["activities"][0]["items"][0]["responseValues"]["optInOutOptions"] = []
242+
243+
resp = await client.post(self.applet_create_url.format(owner_id=tom.id), data=data_dict)
244+
assert resp.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY
245+
assert resp.json()["result"][0]["message"] == "Request Health Record Data item must have 2 opt-in/out options"
246+
214247
async def test_creating_applet_with_activity_items_condition(
215248
self,
216249
client: TestClient,

src/apps/schedule/api/schedule.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from apps.workspaces.service.check_access import CheckAccessService
2626
from infrastructure.database import atomic
2727
from infrastructure.database.deps import get_session
28+
from infrastructure.http import get_local_tz
2829
from infrastructure.logger import logger
2930
from infrastructure.utility import FirebaseNotificationType
3031

@@ -275,6 +276,7 @@ async def schedule_get_all_by_user(
275276
async def schedule_get_all_by_respondent_user(
276277
user: User = Depends(get_current_user),
277278
session=Depends(get_session),
279+
time_zone: str | None = Depends(get_local_tz()),
278280
device_id: Annotated[str | None, Header()] = None,
279281
os_name: Annotated[str | None, Header()] = None,
280282
os_version: Annotated[str | None, Header()] = None,
@@ -307,6 +309,7 @@ async def schedule_get_all_by_respondent_user(
307309
applet_ids=applet_ids,
308310
min_end_date=min_end_date,
309311
max_start_date=max_start_date,
312+
time_zone=time_zone,
310313
device_id=device_id,
311314
os_name=os_name,
312315
os_version=os_version,

0 commit comments

Comments
 (0)