24 KiB
Consent Implementation Analysis Report
Date: October 30, 2025
Project: AgdarCentre - Tenhal Multidisciplinary Healthcare Platform
Analysis Scope: Consent enforcement across all clinical applications
Executive Summary
The consent implementation in AgdarCentre is NOT PERFECT. While the core infrastructure is robust, there are critical gaps in enforcement across clinical applications. Consent verification is only enforced at appointment check-in, but NOT when creating clinical documentation (consultations, sessions, assessments).
Risk Level: 🔴 HIGH - Providers can document services without verifying patient consent, creating legal and compliance risks.
Current Implementation Status
✅ Strengths
1. Core Infrastructure (core app)
-
Models:
Consentmodel with comprehensive fieldsConsentTemplatemodel for reusable templates- E-signature support (drawn, typed, uploaded, external)
- Version control via
HistoricalRecords - Audit trail with IP address, user agent tracking
- Signature hash for integrity verification
-
Service Layer:
ConsentServiceclass with verification methodsverify_consent_for_service()- checks general + service-specific + photo/video consentsget_missing_consents()- identifies what's missingget_active_consents()- retrieves patient's active consentssign_consent()- handles consent signing workflowcreate_consent()- creates new consent forms
-
API & Views:
- RESTful API endpoints via
ConsentViewSet - CRUD views for consent templates
- CRUD views for patient consents
- Serializers for API responses
- RESTful API endpoints via
2. Appointment Integration
-
Enforcement at Check-in:
AppointmentService.mark_arrival()enforces consent verification- State machine prevents check-in without consent
check_arrival_prerequisites()provides clear feedback- Blocks patient arrival if consents missing
-
Prerequisites Checking:
# From appointments/services.py consent_verified, consent_message = ConsentService.verify_consent_for_service( appointment.patient, appointment.service_type ) if not consent_verified: raise ValueError(f"Consent verification required: {consent_message}")
❌ Critical Gaps Identified
1. NO Consent Enforcement in Clinical Apps
ABA App (aba/)
Files Examined: aba/views.py, aba/forms.py, aba/models.py
Findings:
- ❌
ABAConsultCreateView- No consent verification - ❌
ABASessionCreateView- No consent verification - ❌
ABAConsultUpdateView- No consent verification - ❌
ABASessionUpdateView- No consent verification
Impact: ABA therapists can create consultations and sessions without verifying:
- General treatment consent
- ABA-specific consent (if required)
- Photo/Video consent (ABA often involves recording)
Medical App (medical/)
Files Examined: medical/views.py, medical/forms.py, medical/models.py
Findings:
- ❌ No consent verification found in any views
- Medical consultations can be created without consent checks
OT App (ot/)
Files Examined: Search results showed no consent references
Findings:
- ❌ No consent verification implemented
- OT sessions can be documented without consent
SLP App (slp/)
Files Examined: Search results showed no consent references
Findings:
- ❌ No consent verification implemented
- SLP sessions can be documented without consent
Nursing App (nursing/)
Status: Not examined but likely missing based on pattern
2. Incomplete Service Type Coverage
Current Implementation in ConsentService:
# From core/services.py
@staticmethod
def _requires_service_specific_consent(service_type: str) -> bool:
specific_consent_services = [
'SURGERY',
'PROCEDURE',
'ANESTHESIA',
'BLOOD_TRANSFUSION',
'EXPERIMENTAL_TREATMENT',
]
return service_type.upper() in specific_consent_services
@staticmethod
def _requires_photo_video_consent(service_type: str) -> bool:
recording_services = [
'ABA', # ABA therapy often involves video recording
'BEHAVIORAL_THERAPY',
'RESEARCH',
]
return service_type.upper() in recording_services
Missing Service Types:
- ❌ MEDICAL (general medical consultations)
- ❌ OT (occupational therapy)
- ❌ SLP (speech-language pathology)
- ❌ NURSING (nursing procedures)
- ❌ PHYSIOTHERAPY
- ❌ PSYCHOLOGY
- ❌ NUTRITION
Problem: The service type mapping is hardcoded and incomplete. Most clinical services are not recognized.
3. No Consent Checks Before Clinical Documentation
Current Flow:
- Patient books appointment ✅
- Patient arrives → Consent checked ✅
- Provider creates consultation/session → NO CONSENT CHECK ❌
Expected Flow:
- Patient books appointment ✅
- Patient arrives → Consent checked ✅
- Provider creates consultation/session → CONSENT VERIFIED ✅
Gap: Providers can bypass the appointment system and create documentation directly without consent verification.
4. Limited Consent Types
Current Consent Types:
class ConsentType(models.TextChoices):
GENERAL_TREATMENT = 'GENERAL_TREATMENT', _('General Treatment')
SERVICE_SPECIFIC = 'SERVICE_SPECIFIC', _('Service Specific')
PHOTO_VIDEO = 'PHOTO_VIDEO', _('Photo/Video')
DATA_SHARING = 'DATA_SHARING', _('Data Sharing')
Missing Consent Types:
- ❌ ABA_THERAPY - Specific consent for ABA services
- ❌ OCCUPATIONAL_THERAPY - Specific consent for OT services
- ❌ SPEECH_THERAPY - Specific consent for SLP services
- ❌ MEDICAL_PROCEDURE - Specific consent for medical procedures
- ❌ TELEHEALTH - Consent for remote services
- ❌ RESEARCH_PARTICIPATION - Research consent
- ❌ STUDENT_OBSERVATION - Consent for student observers
- ❌ EMERGENCY_TREATMENT - Emergency consent
5. No Consent Expiration Logic
Current Implementation:
- Consents have no expiration date
- Once signed, valid indefinitely
- No mechanism to require renewal
Missing Fields:
expires_at- When consent expiresrenewal_required- Flag for renewalrenewal_period_days- How often to renew
Risk: Outdated consents remain valid, potentially violating regulations requiring periodic renewal.
6. No Consent Withdrawal Mechanism
Current Implementation:
- Patients cannot withdraw consent
- No workflow for consent revocation
is_activeflag exists but no withdrawal process
Missing Features:
- Consent withdrawal workflow
withdrawn_attimestampwithdrawn_byuser referencewithdrawal_reasontext field- Audit trail for withdrawals
Risk: Violates patient rights to withdraw consent at any time.
7. No Consent Dependency Management
Problem: Some consents depend on others:
- Service-specific consent requires general treatment consent
- Photo/video consent may require general consent
- Research consent may require treatment consent
Missing:
- Dependency validation
- Cascade withdrawal (withdrawing general consent should affect specific consents)
- Prerequisite checking
8. No Consent Compliance Reporting
Missing Features:
- Dashboard showing consent compliance rates
- Alerts for missing consents
- Reports on unsigned consents
- Expiring consent notifications
- Consent coverage by service type
Detailed Recommendations
🔴 HIGH PRIORITY (Immediate Action Required)
1. Create ConsentRequiredMixin for Clinical Views
File: core/mixins.py
Purpose: Reusable mixin to enforce consent verification in all clinical CreateViews
Implementation:
class ConsentRequiredMixin:
"""
Mixin to enforce consent verification before creating clinical documentation.
Usage:
class MyConsultCreateView(ConsentRequiredMixin, CreateView):
consent_service_type = 'ABA' # Required
consent_types_required = ['GENERAL_TREATMENT', 'ABA_THERAPY'] # Optional
"""
consent_service_type = None # Must be set by subclass
consent_types_required = None # Optional specific consent types
def dispatch(self, request, *args, **kwargs):
# Get patient from form or URL
patient = self.get_patient()
if patient:
# Verify consent
from core.services import ConsentService
has_consent, message = ConsentService.verify_consent_for_service(
patient,
self.consent_service_type
)
if not has_consent:
messages.error(request, f"Cannot create documentation: {message}")
return redirect('core:consent_create', patient_id=patient.id)
return super().dispatch(request, *args, **kwargs)
def get_patient(self):
"""Override to specify how to get patient."""
raise NotImplementedError("Subclass must implement get_patient()")
2. Update ConsentService with Complete Service Type Coverage
File: core/services.py
Changes:
# Service type configuration
SERVICE_CONSENT_REQUIREMENTS = {
'MEDICAL': {
'requires_specific': False,
'requires_photo_video': False,
},
'ABA': {
'requires_specific': True,
'requires_photo_video': True, # Often involves recording
},
'OT': {
'requires_specific': True,
'requires_photo_video': False,
},
'SLP': {
'requires_specific': True,
'requires_photo_video': False,
},
'NURSING': {
'requires_specific': False,
'requires_photo_video': False,
},
'SURGERY': {
'requires_specific': True,
'requires_photo_video': False,
},
'PROCEDURE': {
'requires_specific': True,
'requires_photo_video': False,
},
# ... add more as needed
}
@staticmethod
def _requires_service_specific_consent(service_type: str) -> bool:
"""Check if service requires service-specific consent."""
config = SERVICE_CONSENT_REQUIREMENTS.get(service_type.upper(), {})
return config.get('requires_specific', False)
@staticmethod
def _requires_photo_video_consent(service_type: str) -> bool:
"""Check if service requires photo/video consent."""
config = SERVICE_CONSENT_REQUIREMENTS.get(service_type.upper(), {})
return config.get('requires_photo_video', False)
3. Add Consent Verification to ABA Views
File: aba/views.py
Changes:
class ABAConsultCreateView(ConsentRequiredMixin, LoginRequiredMixin,
RolePermissionMixin, AuditLogMixin,
SuccessMessageMixin, CreateView):
"""ABA consultation creation with consent enforcement."""
consent_service_type = 'ABA'
def get_patient(self):
"""Get patient from form or URL."""
patient_id = self.request.GET.get('patient')
if patient_id:
return Patient.objects.get(pk=patient_id, tenant=self.request.user.tenant)
return None
# ... rest of implementation
class ABASessionCreateView(ConsentRequiredMixin, LoginRequiredMixin,
RolePermissionMixin, AuditLogMixin,
SuccessMessageMixin, CreateView):
"""ABA session creation with consent enforcement."""
consent_service_type = 'ABA'
def get_patient(self):
"""Get patient from form or URL."""
patient_id = self.request.GET.get('patient')
if patient_id:
return Patient.objects.get(pk=patient_id, tenant=self.request.user.tenant)
return None
# ... rest of implementation
4. Apply Same Pattern to Medical, OT, SLP Apps
Files: medical/views.py, ot/views.py, slp/views.py
Pattern: Add ConsentRequiredMixin to all consultation/session CreateViews with appropriate consent_service_type.
🟡 MEDIUM PRIORITY (Next Sprint)
5. Add Consent Expiration Support
File: core/models.py
Migration Required: Yes
Changes to Consent Model:
class Consent(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
# ... existing fields ...
# NEW FIELDS
expires_at = models.DateField(
null=True,
blank=True,
verbose_name=_("Expires At"),
help_text=_("Date when this consent expires and requires renewal")
)
renewal_required = models.BooleanField(
default=False,
verbose_name=_("Renewal Required"),
help_text=_("Whether this consent requires periodic renewal")
)
renewal_period_days = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name=_("Renewal Period (Days)"),
help_text=_("Number of days before consent expires and requires renewal")
)
def is_expired(self):
"""Check if consent has expired."""
if self.expires_at:
from django.utils import timezone
return timezone.now().date() > self.expires_at
return False
def days_until_expiration(self):
"""Get days until expiration."""
if self.expires_at:
from django.utils import timezone
delta = self.expires_at - timezone.now().date()
return delta.days
return None
Update ConsentService:
@staticmethod
def verify_consent_for_service(patient: Patient, service_type: str) -> Tuple[bool, str]:
"""Check if patient has valid (non-expired) consent for service."""
# Check general consent
general_consent = Consent.objects.filter(
patient=patient,
consent_type='GENERAL_TREATMENT',
is_active=True,
signed_at__isnull=False
).first()
if not general_consent:
return False, "General treatment consent required."
# Check if expired
if general_consent.is_expired():
return False, "General treatment consent has expired. Please renew."
# ... rest of verification logic
6. Add Consent Withdrawal Workflow
File: core/models.py
Migration Required: Yes
Changes to Consent Model:
class Consent(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
# ... existing fields ...
# NEW FIELDS
withdrawn_at = models.DateTimeField(
null=True,
blank=True,
verbose_name=_("Withdrawn At")
)
withdrawn_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='withdrawn_consents',
verbose_name=_("Withdrawn By")
)
withdrawal_reason = models.TextField(
blank=True,
verbose_name=_("Withdrawal Reason")
)
def withdraw(self, user, reason):
"""Withdraw this consent."""
from django.utils import timezone
self.withdrawn_at = timezone.now()
self.withdrawn_by = user
self.withdrawal_reason = reason
self.is_active = False
self.save()
Add Withdrawal View:
class ConsentWithdrawView(LoginRequiredMixin, RolePermissionMixin, UpdateView):
"""Withdraw a consent."""
model = Consent
template_name = 'clinic/consent_withdraw.html'
fields = ['withdrawal_reason']
def form_valid(self, form):
self.object.withdraw(
user=self.request.user,
reason=form.cleaned_data['withdrawal_reason']
)
messages.success(self.request, "Consent withdrawn successfully.")
return redirect('core:patient_detail', pk=self.object.patient.id)
7. Expand Consent Types
File: core/models.py
Migration Required: Yes
Changes:
class ConsentType(models.TextChoices):
GENERAL_TREATMENT = 'GENERAL_TREATMENT', _('General Treatment')
SERVICE_SPECIFIC = 'SERVICE_SPECIFIC', _('Service Specific')
PHOTO_VIDEO = 'PHOTO_VIDEO', _('Photo/Video')
DATA_SHARING = 'DATA_SHARING', _('Data Sharing')
# NEW TYPES
ABA_THERAPY = 'ABA_THERAPY', _('ABA Therapy')
OCCUPATIONAL_THERAPY = 'OCCUPATIONAL_THERAPY', _('Occupational Therapy')
SPEECH_THERAPY = 'SPEECH_THERAPY', _('Speech-Language Therapy')
MEDICAL_PROCEDURE = 'MEDICAL_PROCEDURE', _('Medical Procedure')
TELEHEALTH = 'TELEHEALTH', _('Telehealth Services')
RESEARCH = 'RESEARCH', _('Research Participation')
STUDENT_OBSERVATION = 'STUDENT_OBSERVATION', _('Student Observation')
EMERGENCY_TREATMENT = 'EMERGENCY_TREATMENT', _('Emergency Treatment')
🟢 LOW PRIORITY (Future Enhancement)
8. Consent Compliance Dashboard
Features:
- Overall consent compliance rate
- Missing consents by patient
- Expiring consents (next 30 days)
- Consent coverage by service type
- Withdrawal statistics
9. Automated Consent Reminders
Features:
- Email/SMS reminders for expiring consents
- Notifications for missing consents
- Scheduled tasks to check compliance
10. Bulk Consent Operations
Features:
- Sign multiple consents at once
- Family consent management
- Batch consent renewal
11. Consent Dependency Management
Features:
- Define consent prerequisites
- Cascade withdrawal logic
- Dependency validation
Implementation Timeline
Week 1: Critical Fixes
- Create
ConsentRequiredMixin - Update
ConsentServicewith complete service type coverage - Add consent verification to ABA views
- Add consent verification to Medical views
Week 2: Remaining Apps
- Add consent verification to OT views
- Add consent verification to SLP views
- Add consent verification to Nursing views
- Testing and bug fixes
Week 3: Expiration & Withdrawal
- Add expiration fields to Consent model
- Implement expiration checking logic
- Add withdrawal workflow
- Create withdrawal views and templates
Week 4: Enhanced Types & Reporting
- Expand consent types
- Create consent compliance dashboard
- Add automated reminders
- Documentation and training
Testing Requirements
Unit Tests
- Test
ConsentRequiredMixinblocks unauthorized access - Test
ConsentService.verify_consent_for_service()with all service types - Test consent expiration logic
- Test consent withdrawal workflow
Integration Tests
- Test ABA consultation creation with/without consent
- Test Medical consultation creation with/without consent
- Test appointment check-in with expired consent
- Test consent withdrawal cascade effects
User Acceptance Tests
- Provider attempts to create consultation without consent
- Patient signs consent and provider creates consultation
- Patient withdraws consent and provider cannot create documentation
- Admin views consent compliance dashboard
Compliance & Legal Considerations
Regulatory Requirements
- HIPAA: Consent for treatment and data sharing
- Saudi MOH: Informed consent requirements
- GDPR (if applicable): Right to withdraw consent
Audit Trail
- All consent actions must be logged
- IP address and user agent tracking
- Historical records via
simple-history
Documentation
- Consent templates must be reviewed by legal
- Multi-language support required
- Clear withdrawal process documented
Conclusion
The consent implementation has a solid foundation but critical enforcement gaps. The highest priority is adding ConsentRequiredMixin to all clinical CreateViews to prevent documentation without consent. This can be accomplished in 1-2 weeks with proper testing.
Recommendation: Implement HIGH PRIORITY items immediately to close legal and compliance gaps.
Appendix: Code Examples
Example: Complete ConsentRequiredMixin Implementation
# core/mixins.py
from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse
from core.services import ConsentService
class ConsentRequiredMixin:
"""
Mixin to enforce consent verification before creating clinical documentation.
Attributes:
consent_service_type (str): Required. The service type to check consent for.
consent_redirect_url (str): Optional. URL to redirect to if consent missing.
consent_error_message (str): Optional. Custom error message.
Usage:
class ABAConsultCreateView(ConsentRequiredMixin, CreateView):
consent_service_type = 'ABA'
def get_patient(self):
patient_id = self.request.GET.get('patient')
return Patient.objects.get(pk=patient_id)
"""
consent_service_type = None
consent_redirect_url = None
consent_error_message = None
def dispatch(self, request, *args, **kwargs):
"""Check consent before allowing access."""
# Validate configuration
if not self.consent_service_type:
raise ImproperlyConfigured(
f"{self.__class__.__name__} must define consent_service_type"
)
# Get patient
try:
patient = self.get_patient()
except Exception as e:
messages.error(request, f"Error retrieving patient: {e}")
return redirect('core:patient_list')
if patient:
# Verify consent
has_consent, message = ConsentService.verify_consent_for_service(
patient,
self.consent_service_type
)
if not has_consent:
# Get missing consents
missing = ConsentService.get_missing_consents(
patient,
self.consent_service_type
)
# Custom or default error message
error_msg = self.consent_error_message or (
f"Cannot create {self.consent_service_type} documentation: {message}. "
f"Missing consents: {', '.join(missing)}"
)
messages.error(request, error_msg)
# Redirect to consent creation or custom URL
redirect_url = self.consent_redirect_url or reverse(
'core:consent_create'
) + f'?patient={patient.id}&service_type={self.consent_service_type}'
return redirect(redirect_url)
return super().dispatch(request, *args, **kwargs)
def get_patient(self):
"""
Get the patient for consent verification.
Must be implemented by subclass.
Returns:
Patient: The patient instance
Raises:
NotImplementedError: If not implemented by subclass
"""
raise NotImplementedError(
f"{self.__class__.__name__} must implement get_patient()"
)
Example: Updated ABA Views
# aba/views.py
class ABAConsultCreateView(ConsentRequiredMixin, LoginRequiredMixin,
RolePermissionMixin, AuditLogMixin,
SuccessMessageMixin, CreateView):
"""ABA consultation creation with consent enforcement."""
model = ABAConsult
form_class = ABAConsultForm
template_name = 'aba/consult_form.html'
success_message = "ABA consultation recorded successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.ABA]
# Consent enforcement
consent_service_type = 'ABA'
consent_error_message = (
"Patient must sign ABA therapy consent before consultation can be documented."
)
def get_patient(self):
"""Get patient from URL parameter or appointment."""
patient_id = self.request.GET.get('patient')
appointment_id = self.request.GET.get('appointment_id')
if patient_id:
return Patient.objects.get(
pk=patient_id,
tenant=self.request.user.tenant
)
elif appointment_id:
appointment = Appointment.objects.get(
pk=appointment_id,
tenant=self.request.user.tenant
)
return appointment.patient
return None
# ... rest of implementation
End of Report