Data Integration Guide¶
Guide for integrating and extending SATHI’s data models, particularly the date reference system.
Overview¶
SATHI uses a flexible date reference system that allows users to select different starting points for calculating time intervals in PROM data visualization. This system is crucial for:
Time-based analysis of patient outcomes
Calculating intervals from significant clinical events
Flexible reporting relative to different milestones
Date Reference System¶
Current Date Types¶
The system supports three types of date references:
Patient Registration Date (
date_of_registration)Field:
Patient.date_of_registrationReference key:
'date_of_registration'When patient was registered in the system
Diagnosis Dates (
date_of_diagnosis_<id>)Field:
Diagnosis.date_of_diagnosisReference key pattern:
'date_of_diagnosis_{diagnosis.id}'When specific diagnosis was made
Treatment Start Dates (
date_of_start_of_treatment_<id>)Field:
Treatment.date_of_start_of_treatmentReference key pattern:
'date_of_start_of_treatment_{treatment.id}'When specific treatment began
Key Functions¶
Located in patientapp/utils.py:
get_patient_available_start_dates(patient)
Collects all available date references for a patient.
from patientapp.utils import get_patient_available_start_dates
available_dates = get_patient_available_start_dates(patient)
# Returns: [(reference_key, display_name, date_value), ...]
# Example output:
# [
# ('date_of_registration', 'Date of Registration', datetime.date(2024, 1, 15)),
# ('date_of_diagnosis_123', 'Diagnosis: Cancer', datetime.date(2024, 2, 1)),
# ('date_of_start_of_treatment_456', 'Start of Treatment: Chemotherapy', datetime.date(2024, 2, 15))
# ]
get_patient_start_date(patient, start_date_reference)
Retrieves the actual date value for a given reference key.
from patientapp.utils import get_patient_start_date
start_date = get_patient_start_date(patient, 'date_of_diagnosis_123')
# Returns: datetime.date(2024, 2, 1)
Adding New Date Fields¶
This section explains how to add new date fields to existing models and integrate them into the date reference system.
Step 1: Add Date Field to Model¶
Add your new date field to the model:
# In patientapp/models.py
class Treatment(models.Model):
# ... existing fields ...
date_of_start_of_treatment = models.DateField(
null=True,
blank=True,
verbose_name="Date of Start of Treatment"
)
# NEW: Add your new date field
date_of_follow_up = models.DateField(
null=True,
blank=True,
verbose_name="Date of Follow-up"
)
Create Migration:
python manage.py makemigrations
python manage.py migrate
Step 2: Update get_patient_available_start_dates()¶
Add logic to collect your new date field:
def get_patient_available_start_dates(patient):
"""Get all available start dates for a patient."""
available_dates = []
try:
# Registration date
if patient.date_of_registration:
available_dates.append((
'date_of_registration',
'Date of Registration',
patient.date_of_registration
))
# Diagnosis dates
for diagnosis in patient.diagnosis_set.all():
if diagnosis.date_of_diagnosis:
diagnosis_name = diagnosis.diagnosis.diagnosis if diagnosis.diagnosis else "Unknown"
available_dates.append((
f'date_of_diagnosis_{diagnosis.id}',
f'Diagnosis: {diagnosis_name}',
diagnosis.date_of_diagnosis
))
# Treatment dates (existing + new)
for diagnosis in patient.diagnosis_set.all():
diagnosis_name = diagnosis.diagnosis.diagnosis if diagnosis.diagnosis else "Unknown"
treatments = diagnosis.treatment_set.filter(
models.Q(date_of_start_of_treatment__isnull=False) |
models.Q(date_of_follow_up__isnull=False) # NEW: Include follow-up dates
).order_by('date_of_start_of_treatment')
for i, treatment in enumerate(treatments):
treatment_types = ", ".join([
tt.treatment_type
for tt in treatment.treatment_type.all()
]) if treatment.treatment_type.exists() else f"Treatment {i+1}"
# Existing start date
if treatment.date_of_start_of_treatment:
available_dates.append((
f'date_of_start_of_treatment_{treatment.id}',
f'Start of Treatment: {treatment_types} ({diagnosis_name})',
treatment.date_of_start_of_treatment
))
# NEW: Add follow-up date
if treatment.date_of_follow_up:
available_dates.append((
f'date_of_follow_up_{treatment.id}',
f'Follow-up: {treatment_types} ({diagnosis_name})',
treatment.date_of_follow_up
))
# Sort by date
available_dates.sort(key=lambda x: x[2])
except Exception as e:
logger.error(f"Error getting dates for patient {patient.id}: {e}")
return available_dates
Step 3: Update get_patient_start_date()¶
Add parsing logic for your new date field:
def get_patient_start_date(patient, start_date_reference):
"""Retrieve the actual date value for a given reference key."""
if not start_date_reference:
return patient.date_of_registration
# Registration date
if start_date_reference == 'date_of_registration':
return patient.date_of_registration
# Diagnosis date
if start_date_reference.startswith('date_of_diagnosis_'):
diagnosis_id = start_date_reference.replace('date_of_diagnosis_', '')
try:
diagnosis = patient.diagnosis_set.get(id=diagnosis_id)
return diagnosis.date_of_diagnosis
except Diagnosis.DoesNotExist:
return patient.date_of_registration
# Treatment start date
if start_date_reference.startswith('date_of_start_of_treatment_'):
treatment_id = start_date_reference.replace('date_of_start_of_treatment_', '')
try:
treatment = Treatment.objects.get(id=treatment_id)
return treatment.date_of_start_of_treatment
except Treatment.DoesNotExist:
return patient.date_of_registration
# NEW: Follow-up date
if start_date_reference.startswith('date_of_follow_up_'):
treatment_id = start_date_reference.replace('date_of_follow_up_', '')
try:
treatment = Treatment.objects.get(id=treatment_id)
return treatment.date_of_follow_up
except Treatment.DoesNotExist:
return patient.date_of_registration
# Default fallback
return patient.date_of_registration
Step 4: Update Forms¶
If your new date field should be editable, add it to the relevant forms:
# In patientapp/forms.py
class TreatmentForm(forms.ModelForm):
class Meta:
model = Treatment
fields = [
'treatment_type',
'date_of_start_of_treatment',
'date_of_follow_up', # NEW
# ... other fields
]
Step 5: Update Templates¶
Add the new field to display templates:
<!-- In templates/patientapp/treatment_detail.html -->
<c-field_display
label="Start Date"
value="{{ treatment.date_of_start_of_treatment }}" />
<!-- NEW -->
<c-field_display
label="Follow-up Date"
value="{{ treatment.date_of_follow_up }}" />
Adding New Models with Dates¶
If you’re adding a completely new model with date fields:
Step 1: Create the Model¶
# In patientapp/models.py
class Procedure(models.Model):
"""New model for tracking medical procedures."""
patient = models.ForeignKey(
Patient,
on_delete=models.CASCADE,
related_name='procedures'
)
procedure_type = models.CharField(max_length=255)
date_of_procedure = models.DateField(
null=True,
blank=True,
verbose_name="Date of Procedure"
)
class Meta:
ordering = ['-date_of_procedure']
def __str__(self):
return f"{self.procedure_type} - {self.date_of_procedure}"
Step 2: Integrate into Date Reference System¶
def get_patient_available_start_dates(patient):
"""Get all available start dates for a patient."""
available_dates = []
# ... existing code ...
# NEW: Add procedure dates
for procedure in patient.procedures.filter(date_of_procedure__isnull=False):
available_dates.append((
f'date_of_procedure_{procedure.id}',
f'Procedure: {procedure.procedure_type}',
procedure.date_of_procedure
))
available_dates.sort(key=lambda x: x[2])
return available_dates
def get_patient_start_date(patient, start_date_reference):
"""Retrieve the actual date value for a given reference key."""
# ... existing code ...
# NEW: Procedure date
if start_date_reference.startswith('date_of_procedure_'):
procedure_id = start_date_reference.replace('date_of_procedure_', '')
try:
procedure = patient.procedures.get(id=procedure_id)
return procedure.date_of_procedure
except Procedure.DoesNotExist:
return patient.date_of_registration
return patient.date_of_registration
Time Interval Calculations¶
How Time Intervals Work¶
The system calculates time intervals between:
Start Date: Selected reference date (e.g., diagnosis date)
Submission Date: When questionnaire was completed
Example:
from datetime import datetime, timedelta
start_date = datetime(2024, 1, 15).date() # Diagnosis date
submission_date = datetime(2024, 4, 15).date() # Questionnaire date
# Calculate weeks
days_diff = (submission_date - start_date).days
weeks = days_diff / 7
# Result: 13 weeks
Using Time Intervals in Views¶
from patientapp.utils import get_patient_start_date
def prom_review(request, pk):
patient = get_object_or_404(Patient, pk=pk)
# Get selected start date reference
start_date_ref = request.GET.get('start_date_reference', 'date_of_registration')
start_date = get_patient_start_date(patient, start_date_ref)
# Filter submissions by time interval
max_weeks = int(request.GET.get('max_time_interval', 52))
cutoff_date = start_date + timedelta(weeks=max_weeks)
submissions = QuestionnaireSubmission.objects.filter(
patient_questionnaire__patient=patient,
submission_date__gte=start_date,
submission_date__lte=cutoff_date
)
Data Import/Export¶
CSV Import for Items¶
SATHI supports bulk import of questionnaire items with translations.
CSV Format:
id,construct_scale,item_number,response_type,language_code,name,likert_response,range_response,is_required,item_missing_value,item_better_score_direction,item_threshold_score,item_minimum_clinical_important_difference,item_normative_score_mean,item_normative_score_standard_deviation,discrimination_parameter,difficulty_parameter,pseudo_guessing_parameter
550e8400-e29b-41d4-a716-446655440000,123e4567-e89b-12d3-a456-426614174000,1,Likert,en,"How are you feeling?",789e4567-e89b-12d3-a456-426614174000,,true,0,"Higher is Better",5,2,10,2,1.5,0.5,0.1
Import Process:
from django.core.management import call_command
# Import items from CSV
call_command('import_items', 'path/to/items.csv')
Field Descriptions:
id: UUID for the itemconstruct_scale: UUID of the construct scaleitem_number: Order in questionnaireresponse_type: Likert, Range, Number, or Textlanguage_code: Language for this translationname: Question textlikert_response: UUID of Likert scale (if applicable)range_response: UUID of Range scale (if applicable)
CSV Export for Responses¶
Export patient questionnaire responses:
from promapp.views import export_questionnaire_responses
# Export all responses for a questionnaire
response = export_questionnaire_responses(
request,
questionnaire_id=questionnaire.id
)
# Returns CSV file download
Export Includes:
Patient ID (encrypted)
Institution name
Submission date and time
All item responses
Response values for each question type
Best Practices¶
Date Field Naming¶
DO:
Use descriptive names:
date_of_diagnosis,date_of_follow_upInclude
date_of_prefix for consistencyUse snake_case naming convention
DON’T:
Use ambiguous names:
date,whenMix naming conventions
Omit the
date_of_prefix
Data Validation¶
Always validate dates:
from django.core.exceptions import ValidationError
from datetime import date
def clean_date_of_follow_up(self):
follow_up_date = self.cleaned_data.get('date_of_follow_up')
start_date = self.cleaned_data.get('date_of_start_of_treatment')
if follow_up_date and start_date:
if follow_up_date < start_date:
raise ValidationError(
"Follow-up date cannot be before treatment start date"
)
if follow_up_date and follow_up_date > date.today():
raise ValidationError(
"Follow-up date cannot be in the future"
)
return follow_up_date
Error Handling¶
Graceful fallbacks:
def get_patient_start_date(patient, start_date_reference):
"""Always return a valid date, even if reference is invalid."""
try:
# Attempt to parse reference
if start_date_reference.startswith('date_of_diagnosis_'):
diagnosis_id = start_date_reference.replace('date_of_diagnosis_', '')
diagnosis = patient.diagnosis_set.get(id=diagnosis_id)
return diagnosis.date_of_diagnosis
except (Diagnosis.DoesNotExist, ValueError) as e:
logger.warning(f"Invalid date reference: {start_date_reference}, {e}")
# Always fall back to registration date
return patient.date_of_registration or date.today()
Testing¶
Test Date Reference System¶
from django.test import TestCase
from patientapp.models import Patient, Diagnosis, Treatment
from patientapp.utils import get_patient_available_start_dates, get_patient_start_date
class DateReferenceTestCase(TestCase):
def setUp(self):
self.patient = Patient.objects.create(
name="Test Patient",
date_of_registration=date(2024, 1, 1)
)
self.diagnosis = Diagnosis.objects.create(
patient=self.patient,
date_of_diagnosis=date(2024, 2, 1)
)
self.treatment = Treatment.objects.create(
diagnosis=self.diagnosis,
date_of_start_of_treatment=date(2024, 3, 1)
)
def test_available_dates_includes_all_types(self):
"""Test that all date types are included."""
dates = get_patient_available_start_dates(self.patient)
# Should have registration, diagnosis, and treatment dates
self.assertEqual(len(dates), 3)
# Check reference keys
keys = [d[0] for d in dates]
self.assertIn('date_of_registration', keys)
self.assertIn(f'date_of_diagnosis_{self.diagnosis.id}', keys)
self.assertIn(f'date_of_start_of_treatment_{self.treatment.id}', keys)
def test_get_start_date_returns_correct_date(self):
"""Test that correct date is returned for each reference."""
# Registration date
date_val = get_patient_start_date(self.patient, 'date_of_registration')
self.assertEqual(date_val, date(2024, 1, 1))
# Diagnosis date
date_val = get_patient_start_date(
self.patient,
f'date_of_diagnosis_{self.diagnosis.id}'
)
self.assertEqual(date_val, date(2024, 2, 1))
# Treatment date
date_val = get_patient_start_date(
self.patient,
f'date_of_start_of_treatment_{self.treatment.id}'
)
self.assertEqual(date_val, date(2024, 3, 1))
def test_invalid_reference_returns_fallback(self):
"""Test that invalid references fall back to registration date."""
date_val = get_patient_start_date(self.patient, 'invalid_reference')
self.assertEqual(date_val, self.patient.date_of_registration)
Troubleshooting¶
Common Issues¶
Date Not Appearing in Dropdown:
Check date field is not null in database
Verify
get_patient_available_start_dates()includes the fieldCheck filter query in the function
Ensure date is properly formatted
Wrong Date Being Used:
Verify reference key format matches pattern
Check
get_patient_start_date()parsing logicEnsure model relationships are correct
Check for typos in reference key
Time Interval Calculations Incorrect:
Verify start date is correct
Check submission dates are valid
Ensure timezone handling is consistent
Validate date arithmetic logic