-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathanswers.py
More file actions
757 lines (578 loc) · 22.1 KB
/
Copy pathanswers.py
File metadata and controls
757 lines (578 loc) · 22.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
import datetime
import enum
import uuid
from copy import deepcopy
from typing import Any, Generic
from pydantic import BaseModel, Field, root_validator, validator
from pydantic.generics import GenericModel
from apps.activities.domain.activity_full import ActivityFull, PublicActivityItemFull
from apps.activities.domain.activity_history import (
ActivityHistoryExport,
ActivityHistoryFull,
ActivityHistoryTranslatedExport,
)
from apps.activities.domain.response_type_config import ResponseType
from apps.activities.domain.scores_reports import SubscaleSetting
from apps.activity_flows.domain.flow_full import FlowFull, FlowHistoryWithActivityFlat, FlowHistoryWithActivityFull
from apps.answers.domain.answer_items import AnswerItem, ItemAnswerCreate
from apps.applets.domain.base import AppletBaseInfo
from apps.integrations.oneup_health.service.domain import EHRMetadata
from apps.integrations.prolific.domain import ProlificParamsActivityAnswer
from apps.shared.domain import InternalModel, PublicModel, Response
from apps.shared.domain.custom_validations import datetime_from_ms
from apps.shared.domain.types import _BaseModel
from apps.shared.locale import I18N
from apps.subjects.domain import SubjectReadResponse
class ClientMeta(InternalModel):
app_id: str
app_version: str
width: int | None = None
height: int | None = None
class Answer(InternalModel):
id: uuid.UUID
applet_id: uuid.UUID
version: str
submit_id: uuid.UUID
client: ClientMeta | None
applet_history_id: str
flow_history_id: str | None
activity_history_id: str
respondent_id: uuid.UUID | None
is_flow_completed: bool | None = False
target_subject_id: uuid.UUID | None = None
source_subject_id: uuid.UUID | None = None
input_subject_id: uuid.UUID | None = None
relation: str | None = None
migrated_data: dict | None = None
created_at: datetime.datetime
updated_at: datetime.datetime
migrated_date: datetime.datetime | None = None
migrated_updated: datetime.datetime | None = None
is_deleted: bool
answer_item: AnswerItem
class ParagraphText(InternalModel):
value: str
should_identify_response: bool = False
class Text(InternalModel):
value: str
should_identify_response: bool = False
class SingleSelection(InternalModel):
value: uuid.UUID
additional_text: str | None
class MultipleSelection(InternalModel):
value: list[uuid.UUID]
additional_text: str | None
class Slider(InternalModel):
value: float
additional_text: str | None
AnswerTypes = SingleSelection | Slider | MultipleSelection | Text | ParagraphText
ANSWER_TYPE_MAP: dict[ResponseType, Any] = {
ResponseType.TEXT: Text,
ResponseType.PARAGRAPHTEXT: ParagraphText,
ResponseType.SINGLESELECT: SingleSelection,
ResponseType.MULTISELECT: MultipleSelection,
ResponseType.SLIDER: Slider,
}
class AnswerAlert(InternalModel):
activity_item_id: uuid.UUID
message: str
class AppletAnswerCreate(InternalModel):
applet_id: uuid.UUID
version: str
submit_id: uuid.UUID
flow_id: uuid.UUID | None = None
is_flow_completed: bool | None = None
activity_id: uuid.UUID
answer: ItemAnswerCreate
created_at: datetime.datetime | None = None
alerts: list[AnswerAlert] = Field(default_factory=list)
client: ClientMeta
target_subject_id: uuid.UUID | None = None
source_subject_id: uuid.UUID | None = None
input_subject_id: uuid.UUID | None = None
consent_to_share: bool | None = False
prolific_params: ProlificParamsActivityAnswer | None = None
event_history_id: str | None = None
allowed_ehr_ingest: bool | None = False
_dates_from_ms = validator("created_at", pre=True, allow_reuse=True)(datetime_from_ms)
class AssessmentAnswerCreate(InternalModel):
answer: str
item_ids: list[uuid.UUID]
reviewer_public_key: str
assessment_version_id: str
reviewed_flow_submit_id: uuid.UUID | None = None
class AnswerDate(InternalModel):
created_at: datetime.datetime
answer_id: uuid.UUID
end_datetime: datetime.datetime
class ReviewActivity(InternalModel):
id: uuid.UUID
name: str
answer_dates: list[AnswerDate] = Field(default_factory=list)
class SummaryActivityFlow(InternalModel):
id: uuid.UUID
name: str
has_answer: bool
last_answer_date: datetime.datetime | None
class SummaryActivity(SummaryActivityFlow):
is_performance_task: bool
class PublicAnswerDate(PublicModel):
created_at: datetime.datetime
answer_id: uuid.UUID
end_datetime: datetime.datetime
class ReviewItem(PublicModel, GenericModel, Generic[_BaseModel]):
id: uuid.UUID
name: str
answer_dates: list[_BaseModel] = Field(default_factory=list)
class PublicReviewActivity(ReviewItem[PublicAnswerDate]):
last_answer_date: datetime.datetime | None
@root_validator
def calculate_last_answer_date(cls, values):
answer_dates = values.get("answer_dates", [])
if answer_dates:
last_date = max(ad.created_at for ad in answer_dates)
values["last_answer_date"] = last_date
else:
values["last_answer_date"] = None
return values
class SubmissionDate(PublicModel):
submit_id: uuid.UUID
created_at: datetime.datetime
end_datetime: datetime.datetime
class ReviewFlow(ReviewItem[SubmissionDate]): ...
class PublicReviewFlow(ReviewFlow):
last_answer_date: datetime.datetime | None
@validator("last_answer_date", always=True)
def calculate_last_answer_date(cls, value, values):
answer_dates = values.get("answer_dates", [])
if answer_dates:
value = max(v.created_at for v in answer_dates)
return value
class FlowSubmissionInfo(PublicModel):
submit_id: uuid.UUID
flow_history_id: str
applet_id: uuid.UUID
version: str
created_at: datetime.datetime
end_datetime: datetime.datetime
class PublicSummaryActivityFlow(InternalModel):
id: uuid.UUID
name: str
has_answer: bool
last_answer_date: datetime.datetime | None
class PublicSummaryActivity(PublicSummaryActivityFlow):
is_performance_task: bool
class PublicAnswerDates(PublicModel):
dates: list[datetime.date]
class IdentifierData(InternalModel):
identifier: str
user_public_key: str
is_encrypted: bool
last_answer_date: datetime.datetime
class Identifier(InternalModel):
identifier: str
user_public_key: str | None = None
last_answer_date: datetime.datetime
class ActivityAnswer(PublicModel):
id: uuid.UUID
submit_id: uuid.UUID
version: str
activity_history_id: str
activity_id: uuid.UUID | None = None
flow_history_id: str | None
user_public_key: str | None
answer: str | None
events: str | None
item_ids: list[str] = Field(default_factory=list)
identifier: str | None = None
migrated_data: dict | None = None
end_datetime: datetime.datetime
created_at: datetime.datetime
source_subject: SubjectReadResponse | None
@validator("activity_id", always=True)
def extract_activity_id(cls, value, values):
if val := values.get("activity_history_id"):
return val[:36]
class SubmissionSummary(PublicModel):
end_datetime: datetime.datetime
created_at: datetime.datetime
identifier: Identifier | None = None
version: str
class ActivitySubmission(PublicModel):
activity: ActivityHistoryFull
answer: ActivityAnswer
class ActivitySubmissionResponse(ActivitySubmission):
summary: SubmissionSummary | None = None
@validator("summary", always=True)
def generate_summary(cls, value, values) -> list[Any]:
if not value:
answer: ActivityAnswer = values["answer"]
if answer:
value = SubmissionSummary(
end_datetime=answer.end_datetime,
created_at=answer.created_at,
version=answer.version,
)
if answer.identifier:
if answer.migrated_data and answer.migrated_data.get("is_identifier_encrypted") is False:
value.identifier = Identifier(identifier=answer.identifier, last_answer_date=answer.created_at)
else:
value.identifier = Identifier(
identifier=answer.identifier,
last_answer_date=answer.created_at,
user_public_key=answer.user_public_key,
)
return value
class ReviewsCount(PublicModel):
mine: int = 0
other: int = 0
class AppletSubmission(PublicModel):
applet_id: uuid.UUID
respondent_subject_id: uuid.UUID
respondent_subject_tag: str | None
respondent_secret_user_id: str | None
respondent_nickname: str | None
target_subject_id: uuid.UUID
target_subject_tag: str | None
target_secret_user_id: str | None
target_nickname: str | None
source_subject_id: uuid.UUID | None
source_subject_tag: str | None
source_secret_user_id: str | None
source_nickname: str | None
created_at: datetime.datetime
updated_at: datetime.datetime
activity_name: str
activity_id: uuid.UUID
class FlowSubmission(PublicModel):
submit_id: uuid.UUID
flow_history_id: str
applet_id: uuid.UUID
version: str
created_at: datetime.datetime
end_datetime: datetime.datetime | None = None
is_completed: bool | None = None
answers: list[ActivityAnswer]
review_count: ReviewsCount = ReviewsCount()
class FlowSubmissionsDetails(PublicModel):
submissions: list[FlowSubmission]
flows: list[FlowHistoryWithActivityFull]
class FlowSubmissionsResponse(PublicModel):
submissions: list[FlowSubmission]
flows: list[FlowHistoryWithActivityFlat]
@validator("flows", pre=True)
def format_flows(cls, value, values):
if value:
if isinstance(value[0], dict) and "items" in value[0]:
_values = []
for _value in value:
data = deepcopy(_value)
del data["items"]
data["activities"] = [item["activity"] for item in _value["items"]]
_values.append(data)
value = _values
elif isinstance(value[0], FlowHistoryWithActivityFull):
_values = []
for _value in value:
data = _value.dict(exclude={"items"})
data["activities"] = [item.activity for item in _value.items]
_values.append(data)
value = _values
return value
class PublicFlowSubmissionsResponse(Response[FlowSubmissionsResponse]):
count: int = 0
class FlowSubmissionDetails(PublicModel):
submission: FlowSubmission
flow: FlowHistoryWithActivityFull
class FlowSubmissionResponse(PublicModel):
submission: FlowSubmission
flow: FlowHistoryWithActivityFlat
summary: SubmissionSummary | None = None
@validator("flow", pre=True)
def format_flow(cls, value, values):
if isinstance(value, dict) and "items" in value:
data = deepcopy(value)
del data["items"]
data["activities"] = [item["activity"] for item in value["items"]]
value = data
elif isinstance(value, FlowHistoryWithActivityFull):
data = value.dict(exclude={"items"})
data["activities"] = [item.activity for item in value.items]
value = data
return value
@validator("summary", always=True)
def generate_summary(cls, value, values) -> list[Any]:
if not value:
answers: list[ActivityAnswer] = values["submission"].answers
if answers:
value = SubmissionSummary(
end_datetime=answers[0].end_datetime,
created_at=answers[0].created_at,
version=answers[0].version,
)
for answer in answers:
if identifier := answer.identifier:
if answer.migrated_data and answer.migrated_data.get("is_identifier_encrypted") is False:
value.identifier = Identifier(identifier=identifier, last_answer_date=answer.created_at)
else:
value.identifier = Identifier(
identifier=identifier,
last_answer_date=answer.created_at,
user_public_key=answer.user_public_key,
)
break
return value
class AppletActivityAnswer(InternalModel):
answer_id: uuid.UUID
version: str | None
user_public_key: str | None
answer: str | None
events: str | None
item_ids: list[str] = Field(default_factory=list)
items: list[PublicActivityItemFull] = Field(default_factory=list)
start_datetime: datetime.datetime | None
end_datetime: datetime.datetime | None
subscale_setting: SubscaleSetting | None
class AssessmentAnswer(InternalModel):
reviewer_public_key: str | None
answer: str | None
item_ids: list[str] = Field(default_factory=list)
items: list[PublicActivityItemFull] = Field(default_factory=list)
items_last: list[PublicActivityItemFull] | None = Field(default_factory=list)
is_edited: bool = False
versions: list[str] = []
class Reviewer(InternalModel):
id: uuid.UUID
first_name: str
last_name: str
class AnswerReview(InternalModel):
id: uuid.UUID
reviewer_public_key: str | None
answer: str | None
item_ids: list[str] = Field(default_factory=list)
items: list[PublicActivityItemFull] = Field(default_factory=list)
reviewer: Reviewer
created_at: datetime.datetime
updated_at: datetime.datetime
class AppletActivityAnswerPublic(PublicModel):
answer_id: uuid.UUID
version: str
user_public_key: str | None
answer: str | None
events: str | None
item_ids: list[str] = Field(default_factory=list)
items: list[PublicActivityItemFull] = Field(default_factory=list)
start_datetime: datetime.datetime
end_datetime: datetime.datetime
subscale_setting: SubscaleSetting | None
review_count: ReviewsCount
class ReviewerPublic(PublicModel):
id: uuid.UUID
first_name: str
last_name: str
class AnswerReviewPublic(PublicModel):
id: uuid.UUID
reviewer_public_key: str | None
answer: str | None
item_ids: list[str] = Field(default_factory=list)
items: list[PublicActivityItemFull] = Field(default_factory=list)
reviewer: ReviewerPublic
created_at: datetime.datetime
updated_at: datetime.datetime
class AssessmentAnswerPublic(PublicModel):
reviewer_public_key: str | None
answer: str | None
item_ids: list[str] = Field(default_factory=list)
items: list[PublicActivityItemFull] = Field(default_factory=list)
items_last: list[PublicActivityItemFull] | None = Field(default_factory=list)
versions: list[str]
class AnswerNote(InternalModel):
note: str
class NoteOwner(InternalModel):
id: uuid.UUID
first_name: str
last_name: str
class AnswerNoteDetail(InternalModel):
id: uuid.UUID
user: NoteOwner
note: str
created_at: datetime.datetime
class NoteOwnerPublic(PublicModel):
id: uuid.UUID
first_name: str
last_name: str
class AnswerNoteDetailPublic(PublicModel):
id: uuid.UUID
user: NoteOwnerPublic
note: str
created_at: datetime.datetime
class UserAnswerDataBase(BaseModel):
id: uuid.UUID
submit_id: uuid.UUID
version: str
respondent_id: uuid.UUID | str | None = None
respondent_secret_id: str | None = None
source_subject_id: uuid.UUID | str | None = None
source_secret_id: uuid.UUID | str | None = None
source_user_nickname: str | None = None
source_user_tag: str | None = None
target_subject_id: uuid.UUID | str | None = None
target_secret_id: uuid.UUID | str | None = None
target_user_nickname: str | None = None
target_user_tag: str | None = None
input_subject_id: uuid.UUID | str | None = None
input_secret_id: uuid.UUID | str | None = None
input_user_nickname: str | None = None
relation: str | None = None
legacy_profile_id: str | None = None
user_public_key: str | None
answer: str | None = None
item_ids: list[str] = Field(default_factory=list)
events: str | None = None
scheduled_datetime: datetime.datetime | None = None
start_datetime: datetime.datetime | None = None
end_datetime: datetime.datetime | None = None
migrated_date: datetime.datetime | None = None
tz_offset: int | None = None
scheduled_event_id: uuid.UUID | str | None = None
scheduled_event_history_id: str | None = None
applet_history_id: str
activity_history_id: str | None
flow_history_id: str | None
flow_name: str | None
reviewed_answer_id: uuid.UUID | str | None
reviewed_flow_submit_id: uuid.UUID | str | None
created_at: datetime.datetime
migrated_data: dict | None = None
client: ClientMeta | None = None
ehr_data_file: str | None = None
class RespondentAnswerData(UserAnswerDataBase, InternalModel):
is_manager: bool = False
respondent_email: str | None = None
class RespondentAnswerDataPublic(UserAnswerDataBase, PublicModel):
applet_id: str | None
activity_id: str | None
flow_id: str | None
@validator("applet_id", always=True)
def extract_applet_id(cls, value, values):
return values["applet_history_id"][:36]
@validator("activity_id", always=True)
def extract_activity_id(cls, value, values):
if val := values.get("activity_history_id"):
return val[:36]
@validator("flow_id", always=True)
def extract_flow_id(cls, value, values):
if val := values.get("flow_history_id"):
return val[:36]
@validator("start_datetime", "end_datetime", "scheduled_datetime")
def convert_to_timestamp(cls, value: datetime.datetime):
if value:
return value.replace(tzinfo=datetime.timezone.utc).timestamp()
return None
class AnswerExport(InternalModel):
answers: list[RespondentAnswerData] = Field(default_factory=list)
activities: list[ActivityHistoryFull] = Field(default_factory=list)
total_answers: int = 0
class PublicAnswerExportTranslated(PublicModel):
answers: list[RespondentAnswerDataPublic] = Field(default_factory=list)
activities: list[ActivityHistoryTranslatedExport] = Field(default_factory=list)
class PublicAnswerExport(PublicModel):
answers: list[RespondentAnswerDataPublic] = Field(default_factory=list)
activities: list[ActivityHistoryExport] = Field(default_factory=list)
def translate(self, i18n: I18N) -> PublicAnswerExportTranslated:
return PublicAnswerExportTranslated(
answers=self.answers,
activities=[activity.translate(i18n) for activity in self.activities],
)
class PublicAnswerExportResponse(Response[PublicAnswerExportTranslated]):
count: int = 0
class SafeApplet(AppletBaseInfo, InternalModel):
id: uuid.UUID
version: str
created_at: datetime.datetime | None
updated_at: datetime.datetime | None
activities: list[ActivityFull] = Field(default_factory=list)
activity_flows: list[FlowFull] = Field(default_factory=list)
class ReportServerEmail(InternalModel):
body: str
subject: str
attachment: str
email_recipients: list[str]
class ReportServerResponse(InternalModel):
pdf: str
email: ReportServerEmail
class CompletedEntity(PublicModel):
id: uuid.UUID
answer_id: uuid.UUID
submit_id: uuid.UUID
target_subject_id: uuid.UUID | None
scheduled_event_id: str | None = None
local_end_date: datetime.date
local_end_time: datetime.time
@validator("id", pre=True)
def id_from_history_id(cls, value):
return uuid.UUID(str(value)[:36])
class AppletCompletedEntities(InternalModel):
id: uuid.UUID
version: str
activities: list[CompletedEntity]
activity_flows: list[CompletedEntity]
class AnswersCheck(PublicModel):
applet_id: uuid.UUID
created_at: int
activity_id: str
submit_id: uuid.UUID | None = None
@validator("created_at")
def convert_time_to_unix_timestamp(cls, value: int):
if value:
return value / 1000 # wtf, rework this
return value
class AnswerExistenceResponse(PublicModel):
exists: bool
class ArbitraryPreprocessor(PublicModel):
applet_id: uuid.UUID | None = None
class IdentifiersQueryParams(InternalModel):
respondent_id: uuid.UUID | None = None
target_subject_id: uuid.UUID | None = None
answer_id: uuid.UUID | None = None
class MultiinformantAssessmentValidationResponse(PublicModel):
valid: bool
message: str | None = None
code: str | None = None
class PublicSubmissionsResponse(PublicModel):
submissions: list[AppletSubmission] = Field(default_factory=list)
submissions_count: int = 0
participants_count: int = 0
class AnswersCopyCheckResult(InternalModel):
total_answers: int
not_copied_answers: set[uuid.UUID]
answers_to_remove: set[uuid.UUID]
total_answer_items: int
not_copied_answer_items: set[uuid.UUID]
class FilesCopyCheckResult(InternalModel):
total_files: int
not_copied_files: set[str]
files_to_remove: set[str]
class SubmissionsSubjectCounters(InternalModel):
respondents: set[uuid.UUID] = Field(default_factory=set)
subjects: set[uuid.UUID] = Field(default_factory=set)
subject_submissions_count: int = 0
subject_last_submission_date: datetime.datetime | None = None
respondent_submissions_count: int = 0
respondent_last_submission_date: datetime.datetime | None = None
class SubmissionsActivityMetadataBySubject(InternalModel):
subject_id: uuid.UUID
activities: dict[uuid.UUID, SubmissionsSubjectCounters] = Field(default_factory=dict)
class EHRIngestionStatus(enum.StrEnum):
COMPLETED = "completed"
IN_PROGRESS = "in_progress"
FAILED = "failed"
class AnswerEHR(InternalModel):
submit_id: uuid.UUID
ehr_ingestion_status: EHRIngestionStatus
activity_id: uuid.UUID
ehr_storage_uri: str | None
meta: EHRMetadata | None
class AnswerEHRFull(AnswerEHR):
target_subject_id: uuid.UUID
date: datetime.datetime