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,116 @@
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from research.models import ResearchStudy, ResearchStudyArm, ResearchParticipant
|
||||
|
||||
|
||||
@admin.register(ResearchStudy)
|
||||
class ResearchStudyAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'active', 'generation_mode', 'randomisation_mode', 'created_at']
|
||||
list_filter = ['active', 'generation_mode', 'randomisation_mode', 'created_at']
|
||||
search_fields = ['name', 'slug', 'description']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
fieldsets = (
|
||||
('Study Information', {
|
||||
'fields': ('name', 'slug', 'description', 'active')
|
||||
}),
|
||||
('Participant Creation', {
|
||||
'fields': ('generation_mode', 'collect_demographics'),
|
||||
'description': 'Configure how participants are created and what data is collected.'
|
||||
}),
|
||||
('Randomisation Strategy', {
|
||||
'fields': ('randomisation_mode', 'balance_within_candidate_group', 'allocation_targets'),
|
||||
'description': 'Configure how participants are assigned to arms.'
|
||||
}),
|
||||
('Authors', {
|
||||
'fields': ('author',),
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(ResearchStudyArm)
|
||||
class ResearchStudyArmAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'study', 'packet', 'active', 'sort_order', 'order_sequence']
|
||||
list_filter = ['study', 'active', 'order_sequence']
|
||||
search_fields = ['name', 'study__name', 'packet__name']
|
||||
fieldsets = (
|
||||
('Arm Information', {
|
||||
'fields': ('study', 'name', 'packet', 'active', 'sort_order')
|
||||
}),
|
||||
('Randomisation', {
|
||||
'fields': ('allocation_weight',),
|
||||
'description': 'Used for completely-random allocation.'
|
||||
}),
|
||||
('Ordering & Prerequisites', {
|
||||
'fields': ('order_sequence', 'required_previous_arm'),
|
||||
'description': 'Configure sequential completion requirements.'
|
||||
}),
|
||||
)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
"""Pre-filter related fields based on study."""
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
if obj and hasattr(obj, 'study'):
|
||||
form.base_fields['required_previous_arm'].queryset = ResearchStudyArm.objects.filter(
|
||||
study=obj.study
|
||||
).exclude(pk=obj.pk)
|
||||
return form
|
||||
|
||||
|
||||
@admin.register(ResearchParticipant)
|
||||
class ResearchParticipantAdmin(admin.ModelAdmin):
|
||||
list_display = ['pseudo_id', 'study', 'assigned_arm', 'cid_user_display', 'consented', 'assigned_at']
|
||||
list_filter = ['study', 'assigned_arm', 'consented', 'created_at']
|
||||
search_fields = ['pseudo_id', 'study__slug', 'cid_user__cid', 'email']
|
||||
readonly_fields = ['created_at', 'updated_at', 'cid_user_info']
|
||||
fieldsets = (
|
||||
('Study & ID', {
|
||||
'fields': ('study', 'pseudo_id')
|
||||
}),
|
||||
('CID User', {
|
||||
'fields': ('cid_user', 'cid_user_info'),
|
||||
'description': 'Associated CID user for study access.'
|
||||
}),
|
||||
('Demographics', {
|
||||
'fields': ('candidate_group', 'age_band', 'sex', 'training_grade', 'years_experience', 'email'),
|
||||
}),
|
||||
('Assignment', {
|
||||
'fields': ('assigned_arm', 'assignment_method', 'assigned_at'),
|
||||
}),
|
||||
('Consent & Status', {
|
||||
'fields': ('consented', 'completed_arms'),
|
||||
}),
|
||||
('User Link', {
|
||||
'fields': ('user_user',),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def cid_user_display(self, obj):
|
||||
"""Display CID number with link."""
|
||||
if obj.cid_user:
|
||||
return format_html(
|
||||
'<code>{}</code>',
|
||||
obj.cid_user.cid
|
||||
)
|
||||
return '-'
|
||||
cid_user_display.short_description = 'CID'
|
||||
|
||||
def cid_user_info(self, obj):
|
||||
"""Display full CID user info."""
|
||||
if obj.cid_user:
|
||||
return format_html(
|
||||
'<strong>CID:</strong> {}<br><strong>Passcode:</strong> <code>{}</code><br><strong>Name:</strong> {}',
|
||||
obj.cid_user.cid,
|
||||
obj.cid_user.passcode,
|
||||
obj.cid_user.name
|
||||
)
|
||||
return 'Not yet assigned'
|
||||
cid_user_info.short_description = 'CID User Information'
|
||||
Reference in New Issue
Block a user