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:
@@ -0,0 +1,95 @@
|
||||
# Generated by Django 6.0.1 on 2026-05-12 20:35
|
||||
|
||||
import django.db.models.deletion
|
||||
import generic.mixins
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('atlas', '0103_casecollection_auto_apply_question_template_and_more'),
|
||||
('generic', '0032_ciduserexam_shared_with_users'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ResearchStudy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('slug', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('randomisation_mode', models.CharField(choices=[('RANDOM', 'Completely random'), ('BALANCED', 'Balanced (equal-fill)'), ('BALANCED_GROUP', 'Balanced within candidate group'), ('TARGET_BASED', 'Target-based allocation')], default='BALANCED', help_text='How participants are assigned to packets.', max_length=20)),
|
||||
('generation_mode', models.CharField(choices=[('AUTO', 'Auto-create on first access'), ('BULK', 'Bulk generation only')], default='AUTO', help_text='How participants are created for this study.', max_length=20)),
|
||||
('balance_within_candidate_group', models.BooleanField(default=True, help_text='If enabled, balanced randomisation is performed independently per candidate group.')),
|
||||
('collect_demographics', models.BooleanField(default=True, help_text='If enabled, participants are asked for simple demographic fields before starting.')),
|
||||
('allocation_targets', models.JSONField(blank=True, default=dict, help_text="For target-based allocation: maps arm_id or group_name to target count. E.g. {'arm_1': 30, 'arm_2': 30}")),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('author', models.ManyToManyField(blank=True, help_text='Users allowed to manage this study.', related_name='research_studies', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Research Study',
|
||||
'verbose_name_plural': 'Research Studies',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
bases=(models.Model, generic.mixins.AuthorMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ResearchStudyArm',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('allocation_weight', models.PositiveIntegerField(default=1, help_text='Relative weighting used for completely-random assignment.')),
|
||||
('sort_order', models.PositiveIntegerField(default=100)),
|
||||
('order_sequence', models.PositiveIntegerField(default=0, help_text='Order in which this arm should be completed (0 = no ordering). Used with prerequisite support.')),
|
||||
('packet', models.ForeignKey(help_text='CaseCollection used as this packet.', on_delete=django.db.models.deletion.PROTECT, related_name='research_arms', to='atlas.casecollection')),
|
||||
('required_previous_arm', models.ForeignKey(blank=True, help_text='If set, participant must complete this arm before the dependent arm.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dependent_arms', to='research.researchstudyarm')),
|
||||
('study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='arms', to='research.researchstudy')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Research Study Arm',
|
||||
'verbose_name_plural': 'Research Study Arms',
|
||||
'ordering': ('sort_order', 'id'),
|
||||
'unique_together': {('study', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ResearchParticipant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('pseudo_id', models.CharField(help_text='Pseudo-anonymised ID used in participant URL.', max_length=100)),
|
||||
('candidate_group', models.CharField(blank=True, max_length=100)),
|
||||
('age_band', models.CharField(blank=True, max_length=100)),
|
||||
('sex', models.CharField(blank=True, max_length=50)),
|
||||
('training_grade', models.CharField(blank=True, max_length=100)),
|
||||
('years_experience', models.CharField(blank=True, max_length=50)),
|
||||
('demographics_extra', models.JSONField(blank=True, default=dict)),
|
||||
('email', models.EmailField(blank=True, help_text='Email address for participant result reaccess.', max_length=254)),
|
||||
('consented', models.BooleanField(default=False)),
|
||||
('assignment_method', models.CharField(blank=True, choices=[('RANDOM', 'Completely random'), ('BALANCED', 'Balanced'), ('BALANCED_GROUP', 'Balanced within group'), ('TARGET_BASED', 'Target-based'), ('MANUAL', 'Manual')], max_length=20)),
|
||||
('assigned_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('cid_user', models.OneToOneField(blank=True, help_text='CidUser for this participant (1-1 mapping; assigned at intake).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='research_participant', to='generic.ciduser')),
|
||||
('user_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='research_participants', to=settings.AUTH_USER_MODEL)),
|
||||
('study', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='research.researchstudy')),
|
||||
('assigned_arm', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='participants', to='research.researchstudyarm')),
|
||||
('completed_arms', models.ManyToManyField(blank=True, help_text='Arms this participant has completed.', related_name='completed_by_participants', to='research.researchstudyarm')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Research Participant',
|
||||
'verbose_name_plural': 'Research Participants',
|
||||
'ordering': ('-created_at',),
|
||||
'constraints': [models.UniqueConstraint(fields=('study', 'cid_user'), name='unique_cid_per_study')],
|
||||
'unique_together': {('study', 'pseudo_id')},
|
||||
},
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user