feat(research): add research study management functionality

- Implemented models for ResearchStudy, ResearchStudyArm, and ResearchParticipant.
- Created views for listing, creating, updating, and exporting research studies.
- Added bulk participant generation feature with CSV and JSON support.
- Developed templates for study management, participant entry, and bulk generation.
- Introduced URL routing for research study operations.
- Added utility functions for randomisation and participant assignment.
- Implemented participant intake process with demographic data collection.
- Ensured proper handling of participant CIDs and assignment methods.
This commit is contained in:
Ross
2026-05-12 21:37:57 +01:00
parent e28bf641b7
commit c94d2fb1a7
28 changed files with 2186 additions and 3 deletions
+73
View File
@@ -1458,6 +1458,47 @@ class CaseCollection(ExamOrCollectionGenericBase):
default=VIEWER_MODE_CHOICES.BUILT_IN,
)
# -----------------------------------------------------------------------
# Marking configuration
# These fields can be defined here so that the same CaseCollection can be
# reused in multiple research studies or standalone with consistent marking.
# -----------------------------------------------------------------------
marking_scheme_name = models.CharField(
max_length=255,
blank=True,
help_text="Human-readable mark scheme name shown to markers and in exports.",
)
marking_guidance = models.TextField(
blank=True,
help_text="Plain-language marking guidance displayed to markers.",
)
case_question_max_score = models.PositiveIntegerField(
null=True,
blank=True,
help_text="Maximum score per case used for normalisation. Defaults to 10 when not set.",
)
# -----------------------------------------------------------------------
# Case question template
# When set, this JSON schema is used as a default for all CaseDetail entries
# in the collection that do not have their own question_schema.
# -----------------------------------------------------------------------
case_question_template = models.JSONField(
null=True,
blank=True,
help_text=(
"JSON schema applied as a default question template for all cases in this collection. "
"Individual cases can override via CaseDetail.question_schema."
),
)
auto_apply_question_template = models.BooleanField(
default=False,
help_text=(
"If enabled, the case_question_template is automatically applied to new cases "
"added to this collection that do not have an existing question_schema."
),
)
def get_app_name(self):
return "atlas"
@@ -1546,6 +1587,38 @@ class CaseCollection(ExamOrCollectionGenericBase):
else:
return cases[new_index]
def apply_question_template_to_cases(self, overwrite_existing=False):
"""
Apply case_question_template to all CaseDetail entries in this collection.
Args:
overwrite_existing: If True, overwrite existing question_schema on CaseDetails.
If False (default), only apply to cases without a schema.
Returns:
int: Number of CaseDetail entries updated.
"""
if not self.case_question_template:
return 0
casedetails = self.casedetail_set.all()
updated = 0
for cd in casedetails:
if overwrite_existing or not cd.question_schema:
cd.question_schema = self.case_question_template
cd.save(update_fields=["question_schema"])
updated += 1
return updated
def get_effective_question_schema(self, casedetail):
"""
Return the effective question schema for a CaseDetail.
Uses the individual CaseDetail schema if set, otherwise falls back to the collection template.
"""
if casedetail.question_schema:
return casedetail.question_schema
return self.case_question_template
def get_index_of_case(self, case, case_count=False):
cases = list(self.get_cases())