Files
penracourses/research/migrations/0001_initial.py
T
Ross c94d2fb1a7 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.
2026-05-12 21:37:57 +01:00

96 lines
7.0 KiB
Python

# 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')},
},
),
]