"""
OT views for the Tenhal Multidisciplinary Healthcare Platform.
This module contains views for Occupational Therapy documentation including:
- OT consultations (OT-F-1)
- OT session notes (OT-F-3)
- Target skills tracking with 0-10 scoring
- Progress visualization
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.db.models import Q, Avg
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.views.generic import ListView, DetailView, CreateView, UpdateView, View
from django.urls import reverse_lazy
from core.mixins import (
TenantFilterMixin,
RolePermissionMixin,
AuditLogMixin,
HTMXResponseMixin,
SuccessMessageMixin,
PaginationMixin,
ConsentRequiredMixin,
SignedDocumentEditPreventionMixin,
)
from core.models import User, Patient
from appointments.models import Appointment
from .models import OTConsult, OTSession, OTTargetSkill
from .forms import (
OTConsultForm, OTSessionForm,
OTDifficultyAreaFormSet, OTMilestoneFormSet, OTSelfHelpSkillFormSet,
OTInfantBehaviorFormSet, OTCurrentBehaviorFormSet
)
from .scoring_service import OTScoringService, initialize_consultation_data
class OTConsultSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
"""
Sign an OT consultation.
Features:
- Only the provider or admin can sign
- Records signature timestamp and user
- Prevents re-signing already signed consultations
"""
allowed_roles = [User.Role.ADMIN, User.Role.OT]
def post(self, request, pk):
"""Sign the consultation."""
consult = get_object_or_404(
OTConsult,
pk=pk,
tenant=request.user.tenant
)
# Check if already signed
if consult.signed_by:
messages.warning(
request,
"This consultation has already been signed."
)
return HttpResponseRedirect(
reverse_lazy('ot:consult_detail', kwargs={'pk': pk})
)
# Check if user is the provider or admin
if consult.provider != request.user and request.user.role != User.Role.ADMIN:
messages.error(
request,
"Only the consultation provider or an administrator can sign this consultation."
)
return HttpResponseRedirect(
reverse_lazy('ot:consult_detail', kwargs={'pk': pk})
)
# Sign the consultation
consult.signed_by = request.user
consult.signed_at = timezone.now()
consult.save()
messages.success(
request,
"Consultation signed successfully! This document can no longer be edited."
)
return HttpResponseRedirect(
reverse_lazy('ot:consult_detail', kwargs={'pk': pk})
)
class OTSessionSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
"""
Sign an OT session.
Features:
- Only the provider or admin can sign
- Records signature timestamp and user
- Prevents re-signing already signed sessions
"""
allowed_roles = [User.Role.ADMIN, User.Role.OT]
def post(self, request, pk):
"""Sign the session."""
session = get_object_or_404(
OTSession,
pk=pk,
tenant=request.user.tenant
)
# Check if already signed
if session.signed_by:
messages.warning(
request,
"This session has already been signed."
)
return HttpResponseRedirect(
reverse_lazy('ot:session_detail', kwargs={'pk': pk})
)
# Check if user is the provider or admin
if session.provider != request.user and request.user.role != User.Role.ADMIN:
messages.error(
request,
"Only the session provider or an administrator can sign this session."
)
return HttpResponseRedirect(
reverse_lazy('ot:session_detail', kwargs={'pk': pk})
)
# Sign the session
session.signed_by = request.user
session.signed_at = timezone.now()
session.save()
messages.success(
request,
"Session signed successfully! This document can no longer be edited."
)
return HttpResponseRedirect(
reverse_lazy('ot:session_detail', kwargs={'pk': pk})
)
class OTConsultListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
HTMXResponseMixin, ListView):
"""
OT consultation list view (OT-F-1).
Features:
- Filter by patient, provider, date range
- Search by patient name/MRN
- Role-based filtering
- Shows unsigned consultations notification
"""
model = OTConsult
template_name = 'ot/consult_list.html'
htmx_template_name = 'ot/partials/consult_list_partial.html'
context_object_name = 'consults'
paginate_by = 25
def get_queryset(self):
"""Get filtered queryset."""
queryset = super().get_queryset()
user = self.request.user
# Role-based filtering
if user.role == User.Role.OT:
queryset = queryset.filter(provider=user)
# Apply search
search_query = self.request.GET.get('search', '').strip()
if search_query:
queryset = queryset.filter(
Q(patient__first_name_en__icontains=search_query) |
Q(patient__last_name_en__icontains=search_query) |
Q(patient__mrn__icontains=search_query)
)
# Apply filters
patient_id = self.request.GET.get('patient')
if patient_id:
queryset = queryset.filter(patient_id=patient_id)
date_from = self.request.GET.get('date_from')
if date_from:
queryset = queryset.filter(consultation_date__gte=date_from)
date_to = self.request.GET.get('date_to')
if date_to:
queryset = queryset.filter(consultation_date__lte=date_to)
return queryset.select_related('patient', 'provider', 'appointment', 'signed_by').order_by('-consultation_date')
def get_context_data(self, **kwargs):
"""Add unsigned consultations count for current user."""
context = super().get_context_data(**kwargs)
user = self.request.user
# Get unsigned consultations for current user
unsigned_query = OTConsult.objects.filter(
tenant=user.tenant,
signed_by__isnull=True
)
# Filter by provider if OT role
if user.role == User.Role.OT:
unsigned_query = unsigned_query.filter(provider=user)
context['unsigned_count'] = unsigned_query.count()
context['unsigned_items'] = unsigned_query.select_related(
'patient', 'provider'
).order_by('-consultation_date')[:10]
return context
class OTConsultDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
"""
OT consultation detail view (OT-F-1).
Features:
- Full consultation details
- Reasons for referral (multi-select)
- Top 3 difficulty areas
- Developmental motor milestones
- Self-help skills matrix
- Feeding participation
- Infant/now behavior descriptors
- Recommendation (continue/discharge/referral with rules)
"""
model = OTConsult
template_name = 'ot/consult_detail.html'
context_object_name = 'consult'
def get_context_data(self, **kwargs):
"""Add related sessions."""
context = super().get_context_data(**kwargs)
# Get related sessions
context['sessions'] = OTSession.objects.filter(
patient=self.object.patient
).order_by('-session_date')[:10]
return context
class OTConsultCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin,
AuditLogMixin, SuccessMessageMixin, CreateView):
"""
OT consultation creation view (OT-F-1).
Features:
- Comprehensive form with formsets for all related data
- Automatic initialization of milestones, skills, and behaviors
- Automatic score calculation on save
- Consent verification enforced before creation
"""
model = OTConsult
form_class = OTConsultForm
template_name = 'ot/consult_form.html'
success_message = "OT consultation recorded successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.OT]
# Consent enforcement
consent_service_type = 'OT'
consent_error_message = (
"Patient must sign OT therapy consent before consultation can be documented."
)
def get_success_url(self):
"""Redirect to edit page to complete sections 4-5."""
messages.info(self.request, "Consultation saved! Please complete sections 4-5 (Developmental History & Self-Help Skills) by clicking Edit.")
return reverse_lazy('ot:consult_update', kwargs={'pk': self.object.pk})
def get_patient(self):
"""Get patient for consent verification."""
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
def get_context_data(self, **kwargs):
"""Add formsets and patient/appointment info."""
context = super().get_context_data(**kwargs)
context['form_title'] = 'OT Consultation (OT-F-1)'
context['submit_text'] = 'Save & Continue to Sections 4-5'
# Add formsets (will be empty for new consultations - that's OK)
if self.request.POST:
context['difficulty_formset'] = OTDifficultyAreaFormSet(self.request.POST, instance=self.object)
context['milestone_formset'] = OTMilestoneFormSet(self.request.POST, instance=self.object)
context['selfhelp_formset'] = OTSelfHelpSkillFormSet(self.request.POST, instance=self.object)
context['infant_behavior_formset'] = OTInfantBehaviorFormSet(self.request.POST, instance=self.object)
context['current_behavior_formset'] = OTCurrentBehaviorFormSet(self.request.POST, instance=self.object)
else:
context['difficulty_formset'] = OTDifficultyAreaFormSet(instance=self.object)
context['milestone_formset'] = OTMilestoneFormSet(instance=self.object)
context['selfhelp_formset'] = OTSelfHelpSkillFormSet(instance=self.object)
context['infant_behavior_formset'] = OTInfantBehaviorFormSet(instance=self.object)
context['current_behavior_formset'] = OTCurrentBehaviorFormSet(instance=self.object)
# Get patient if provided
patient_id = self.request.GET.get('patient')
if patient_id:
try:
context['patient'] = Patient.objects.get(
pk=patient_id,
tenant=self.request.user.tenant
)
except Patient.DoesNotExist:
pass
# Get appointment if provided
appointment_id = self.request.GET.get('appointment_id')
if appointment_id:
try:
context['appointment'] = Appointment.objects.get(
pk=appointment_id,
tenant=self.request.user.tenant
)
context['patient'] = context['appointment'].patient
except Appointment.DoesNotExist:
pass
return context
def form_valid(self, form):
"""Set tenant, provider, and handle formsets."""
context = self.get_context_data()
difficulty_formset = context['difficulty_formset']
milestone_formset = context['milestone_formset']
selfhelp_formset = context['selfhelp_formset']
infant_behavior_formset = context['infant_behavior_formset']
current_behavior_formset = context['current_behavior_formset']
# Validate all formsets
if not all([
difficulty_formset.is_valid(),
milestone_formset.is_valid(),
selfhelp_formset.is_valid(),
infant_behavior_formset.is_valid(),
current_behavior_formset.is_valid()
]):
return self.form_invalid(form)
# Set tenant and provider
form.instance.tenant = self.request.user.tenant
form.instance.provider = self.request.user
# Set consultation date if not provided
if not form.instance.consultation_date:
form.instance.consultation_date = timezone.now().date()
# Save consultation
self.object = form.save()
# Initialize consultation data if this is a new consultation
initialize_consultation_data(self.object)
# Save formsets
difficulty_formset.instance = self.object
difficulty_formset.save()
milestone_formset.instance = self.object
milestone_formset.save()
selfhelp_formset.instance = self.object
selfhelp_formset.save()
infant_behavior_formset.instance = self.object
infant_behavior_formset.save()
current_behavior_formset.instance = self.object
current_behavior_formset.save()
# Calculate and save scores
scoring_service = OTScoringService(self.object)
scoring_service.save_scores()
messages.success(self.request, self.success_message)
return HttpResponseRedirect(self.get_success_url())
class OTConsultUpdateView(SignedDocumentEditPreventionMixin, LoginRequiredMixin,
RolePermissionMixin, TenantFilterMixin,
AuditLogMixin, SuccessMessageMixin, UpdateView):
"""
OT consultation update view (OT-F-1).
Features:
- Update consultation details with formsets
- Automatic score recalculation on save
- Version history
- Audit trail
- Prevents editing of signed documents
"""
model = OTConsult
form_class = OTConsultForm
template_name = 'ot/consult_form.html'
success_message = "OT consultation updated successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.OT]
def get_success_url(self):
"""Redirect to consult detail."""
return reverse_lazy('ot:consult_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
"""Add formsets, form title and history."""
context = super().get_context_data(**kwargs)
context['form_title'] = f'Update OT Consultation - {self.object.patient.mrn}'
context['submit_text'] = 'Update Consultation'
# Add formsets
if self.request.POST:
context['difficulty_formset'] = OTDifficultyAreaFormSet(self.request.POST, instance=self.object)
context['milestone_formset'] = OTMilestoneFormSet(self.request.POST, instance=self.object)
context['selfhelp_formset'] = OTSelfHelpSkillFormSet(self.request.POST, instance=self.object)
context['infant_behavior_formset'] = OTInfantBehaviorFormSet(self.request.POST, instance=self.object)
context['current_behavior_formset'] = OTCurrentBehaviorFormSet(self.request.POST, instance=self.object)
else:
context['difficulty_formset'] = OTDifficultyAreaFormSet(instance=self.object)
context['milestone_formset'] = OTMilestoneFormSet(instance=self.object)
context['selfhelp_formset'] = OTSelfHelpSkillFormSet(instance=self.object)
context['infant_behavior_formset'] = OTInfantBehaviorFormSet(instance=self.object)
context['current_behavior_formset'] = OTCurrentBehaviorFormSet(instance=self.object)
# Add version history if available
if hasattr(self.object, 'history'):
context['history'] = self.object.history.all()[:10]
# Add patient info
context['patient'] = self.object.patient
return context
def form_valid(self, form):
"""Handle formsets and recalculate scores."""
context = self.get_context_data()
difficulty_formset = context['difficulty_formset']
milestone_formset = context['milestone_formset']
selfhelp_formset = context['selfhelp_formset']
infant_behavior_formset = context['infant_behavior_formset']
current_behavior_formset = context['current_behavior_formset']
# Validate all formsets
if not all([
difficulty_formset.is_valid(),
milestone_formset.is_valid(),
selfhelp_formset.is_valid(),
infant_behavior_formset.is_valid(),
current_behavior_formset.is_valid()
]):
return self.form_invalid(form)
# Save consultation
self.object = form.save()
# Save formsets
difficulty_formset.instance = self.object
difficulty_formset.save()
milestone_formset.instance = self.object
milestone_formset.save()
selfhelp_formset.instance = self.object
selfhelp_formset.save()
infant_behavior_formset.instance = self.object
infant_behavior_formset.save()
current_behavior_formset.instance = self.object
current_behavior_formset.save()
# Recalculate and save scores
scoring_service = OTScoringService(self.object)
scoring_service.save_scores()
messages.success(self.request, self.success_message)
return HttpResponseRedirect(self.get_success_url())
class OTSessionListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
HTMXResponseMixin, ListView):
"""
OT session list view (OT-F-3).
Features:
- Filter by patient, provider, date range, session type
- Search by patient name/MRN
- Role-based filtering
- Shows unsigned sessions notification
"""
model = OTSession
template_name = 'ot/session_list.html'
htmx_template_name = 'ot/partials/session_list_partial.html'
context_object_name = 'sessions'
paginate_by = 25
def get_queryset(self):
"""Get filtered queryset."""
queryset = super().get_queryset()
user = self.request.user
# Role-based filtering
if user.role == User.Role.OT:
queryset = queryset.filter(provider=user)
# Apply search
search_query = self.request.GET.get('search', '').strip()
if search_query:
queryset = queryset.filter(
Q(patient__first_name_en__icontains=search_query) |
Q(patient__last_name_en__icontains=search_query) |
Q(patient__mrn__icontains=search_query)
)
# Apply filters
patient_id = self.request.GET.get('patient')
if patient_id:
queryset = queryset.filter(patient_id=patient_id)
session_type = self.request.GET.get('session_type')
if session_type:
queryset = queryset.filter(session_type=session_type)
date_from = self.request.GET.get('date_from')
if date_from:
queryset = queryset.filter(session_date__gte=date_from)
date_to = self.request.GET.get('date_to')
if date_to:
queryset = queryset.filter(session_date__lte=date_to)
return queryset.select_related('patient', 'provider', 'appointment', 'signed_by').order_by('-session_date')
def get_context_data(self, **kwargs):
"""Add unsigned sessions count for current user."""
context = super().get_context_data(**kwargs)
user = self.request.user
# Get unsigned sessions for current user
unsigned_query = OTSession.objects.filter(
tenant=user.tenant,
signed_by__isnull=True
)
# Filter by provider if OT role
if user.role == User.Role.OT:
unsigned_query = unsigned_query.filter(provider=user)
context['unsigned_count'] = unsigned_query.count()
context['unsigned_items'] = unsigned_query.select_related(
'patient', 'provider'
).order_by('-session_date')[:10]
return context
class OTSessionDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
"""
OT session detail view (OT-F-3).
Features:
- Full session details
- Session type (Consult/Individual/Group/ParentTraining)
- Cooperative level (1-4)
- Distraction tolerance (1-4)
- "Today we work on..." checklist
- Target skills with 0-10 scoring
- Observations, activities, recommendations
"""
model = OTSession
template_name = 'ot/session_detail.html'
context_object_name = 'session'
def get_context_data(self, **kwargs):
"""Add target skills and progress data."""
context = super().get_context_data(**kwargs)
# Get target skills
context['target_skills'] = self.object.target_skills.all()
# Get previous sessions for comparison
context['previous_sessions'] = OTSession.objects.filter(
patient=self.object.patient,
session_date__lt=self.object.session_date
).order_by('-session_date')[:5]
return context
class OTSessionCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin,
AuditLogMixin, SuccessMessageMixin, CreateView):
"""
OT session creation view (OT-F-3).
Features:
- Session date, type (Consult/Individual/Group/ParentTraining)
- Cooperative level (1-4)
- Distraction tolerance (1-4)
- "Today we work on..." checklist
- Target skills with 0-10 scoring
- Observations, activities, recommendations
- Consent verification enforced before creation
"""
model = OTSession
form_class = OTSessionForm
template_name = 'ot/session_form.html'
success_message = "OT session recorded successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.OT]
# Consent enforcement
consent_service_type = 'OT'
consent_error_message = (
"Patient must sign OT therapy consent before session can be documented."
)
def get_success_url(self):
"""Redirect to session detail."""
return reverse_lazy('ot:session_detail', kwargs={'pk': self.object.pk})
def get_patient(self):
"""Get patient for consent verification."""
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
def form_valid(self, form):
"""Set tenant and provider."""
# Set tenant
form.instance.tenant = self.request.user.tenant
# Set provider
form.instance.provider = self.request.user
# Set session date if not provided
if not form.instance.session_date:
form.instance.session_date = timezone.now().date()
# Save session
response = super().form_valid(form)
return response
def get_context_data(self, **kwargs):
"""Add form title and patient/appointment info."""
context = super().get_context_data(**kwargs)
context['form_title'] = 'OT Session Note (OT-F-3)'
context['submit_text'] = 'Save Session'
# Get patient if provided
patient_id = self.request.GET.get('patient')
if patient_id:
try:
context['patient'] = Patient.objects.get(
pk=patient_id,
tenant=self.request.user.tenant
)
except Patient.DoesNotExist:
pass
# Get appointment if provided
appointment_id = self.request.GET.get('appointment_id')
if appointment_id:
try:
context['appointment'] = Appointment.objects.get(
pk=appointment_id,
tenant=self.request.user.tenant
)
context['patient'] = context['appointment'].patient
except Appointment.DoesNotExist:
pass
return context
class OTSessionUpdateView(SignedDocumentEditPreventionMixin, LoginRequiredMixin,
RolePermissionMixin, TenantFilterMixin,
AuditLogMixin, SuccessMessageMixin, UpdateView):
"""
OT session update view (OT-F-3).
Features:
- Update session details
- Version history
- Audit trail
- Prevents editing of signed documents
"""
model = OTSession
form_class = OTSessionForm
template_name = 'ot/session_form.html'
success_message = "OT session updated successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.OT]
def get_success_url(self):
"""Redirect to session detail."""
return reverse_lazy('ot:session_detail', kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
"""Add form title and history."""
context = super().get_context_data(**kwargs)
context['form_title'] = f'Update OT Session - {self.object.patient.mrn}'
context['submit_text'] = 'Update Session'
# Add version history if available
if hasattr(self.object, 'history'):
context['history'] = self.object.history.all()[:10]
return context
class PatientOTProgressView(LoginRequiredMixin, TenantFilterMixin, ListView):
"""
Patient OT progress view.
Features:
- All OT sessions for specific patient
- Target skills progress tracking (0-10 scoring graph)
- Cooperative level trends
- Distraction tolerance trends
"""
model = OTSession
template_name = 'ot/patient_progress.html'
context_object_name = 'sessions'
paginate_by = 50
def get_queryset(self):
"""Get all sessions for patient."""
patient_id = self.kwargs.get('patient_id')
return OTSession.objects.filter(
patient_id=patient_id,
tenant=self.request.user.tenant
).order_by('-session_date')
def get_context_data(self, **kwargs):
"""Add patient and progress data."""
context = super().get_context_data(**kwargs)
patient_id = self.kwargs.get('patient_id')
context['patient'] = get_object_or_404(
Patient,
pk=patient_id,
tenant=self.request.user.tenant
)
# Get progress data for charts
sessions = self.get_queryset()
context['progress_data'] = self._prepare_progress_data(sessions)
# Get statistics
context['stats'] = self._calculate_statistics(sessions)
return context
def _prepare_progress_data(self, sessions):
"""Prepare data for progress charts."""
data = {
'dates': [],
'cooperative_level': [],
'distraction_tolerance': [],
'target_skills': {},
}
for session in sessions.order_by('session_date'):
data['dates'].append(session.session_date.strftime('%Y-%m-%d'))
data['cooperative_level'].append(session.cooperative_level)
data['distraction_tolerance'].append(session.distraction_tolerance)
# Track target skills
for skill in session.target_skills.all():
if skill.skill_name not in data['target_skills']:
data['target_skills'][skill.skill_name] = []
data['target_skills'][skill.skill_name].append(skill.score)
return data
def _calculate_statistics(self, sessions):
"""Calculate statistics from sessions."""
if not sessions:
return {}
return {
'total_sessions': sessions.count(),
'avg_cooperative_level': sessions.aggregate(Avg('cooperative_level'))['cooperative_level__avg'],
'avg_distraction_tolerance': sessions.aggregate(Avg('distraction_tolerance'))['distraction_tolerance__avg'],
'latest_session': sessions.first(),
}
class TargetSkillProgressView(LoginRequiredMixin, TenantFilterMixin, ListView):
"""
Target skill progress view across all patients.
Features:
- Track specific target skills
- 0-10 scoring trends
- Skill mastery analysis
"""
model = OTTargetSkill
template_name = 'ot/target_skill_progress.html'
context_object_name = 'target_skills'
paginate_by = 50
def get_queryset(self):
"""Get target skills with filters."""
queryset = OTTargetSkill.objects.filter(
session__tenant=self.request.user.tenant
).select_related('session', 'session__patient')
# Apply filters
skill_name = self.request.GET.get('skill_name')
if skill_name:
queryset = queryset.filter(skill_name__icontains=skill_name)
patient_id = self.request.GET.get('patient')
if patient_id:
queryset = queryset.filter(session__patient_id=patient_id)
return queryset.order_by('-session__session_date')
def get_context_data(self, **kwargs):
"""Add statistics."""
context = super().get_context_data(**kwargs)
queryset = self.get_queryset()
# Add statistics
context['stats'] = {
'total_skills': queryset.count(),
'avg_score': queryset.aggregate(Avg('score'))['score__avg'],
'mastered_skills': queryset.filter(score__gte=8).count(),
}
return context
class SkillAssessmentView(LoginRequiredMixin, TenantFilterMixin, ListView):
"""
Skill assessment view with enhanced analytics.
Features:
- Detailed skill tracking across patients
- Achievement level categorization
- Score percentage visualization
- Search and filter capabilities
"""
model = OTTargetSkill
template_name = 'ot/skill_assessment.html'
context_object_name = 'target_skills'
paginate_by = 50
def get_queryset(self):
"""Get target skills with filters and annotations."""
queryset = OTTargetSkill.objects.filter(
session__tenant=self.request.user.tenant
).select_related('session', 'session__patient', 'session__provider')
# Apply search filters
skill_name = self.request.GET.get('skill_name', '').strip()
if skill_name:
queryset = queryset.filter(skill_name__icontains=skill_name)
patient_search = self.request.GET.get('patient', '').strip()
if patient_search:
queryset = queryset.filter(
Q(session__patient__first_name_en__icontains=patient_search) |
Q(session__patient__last_name_en__icontains=patient_search) |
Q(session__patient__mrn__icontains=patient_search)
)
return queryset.order_by('-session__session_date')
def get_context_data(self, **kwargs):
"""Add statistics."""
context = super().get_context_data(**kwargs)
queryset = self.get_queryset()
# Calculate statistics
total_skills = queryset.count()
avg_score = queryset.aggregate(Avg('score'))['score__avg'] or 0
mastered_skills = queryset.filter(score__gte=8).count()
context['stats'] = {
'total_skills': total_skills,
'avg_score': avg_score,
'mastered_skills': mastered_skills,
}
# Note: score_percentage and achievement_level are already properties on the model
# They will be automatically available in the template
return context
# ============================================================================
# PDF Generation Views
# ============================================================================
from core.pdf_service import BasePDFGenerator
from django.shortcuts import redirect
class OTConsultPDFGenerator(BasePDFGenerator):
"""PDF generator for OT Consultation (OT-F-1)."""
def get_document_title(self):
"""Return document title in English and Arabic."""
consult = self.document
return (
f"OT Consultation (OT-F-1) - {consult.patient.mrn}",
"استشارة العلاج الوظيفي"
)
def get_pdf_filename(self):
"""Return PDF filename."""
consult = self.document
date_str = consult.consultation_date.strftime('%Y%m%d')
return f"ot_consultation_{consult.patient.mrn}_{date_str}.pdf"
def get_document_sections(self):
"""Return document sections to render."""
consult = self.document
patient = consult.patient
sections = []
# Patient Information Section
patient_name_ar = f"{patient.first_name_ar} {patient.last_name_ar}" if patient.first_name_ar else ""
sections.append({
'heading_en': 'Patient Information',
'heading_ar': 'معلومات المريض',
'type': 'table',
'content': [
('Name', 'الاسم', f"{patient.first_name_en} {patient.last_name_en}", patient_name_ar),
('MRN', 'رقم السجل الطبي', patient.mrn, ""),
('Date of Birth', 'تاريخ الميلاد', patient.date_of_birth.strftime('%Y-%m-%d'), ""),
('Age', 'العمر', f"{patient.age} years", ""),
]
})
# Consultation Details Section
sections.append({
'heading_en': 'Consultation Details',
'heading_ar': 'تفاصيل الاستشارة',
'type': 'table',
'content': [
('Date', 'التاريخ', consult.consultation_date.strftime('%Y-%m-%d'), ""),
('Provider', 'مقدم الخدمة', consult.provider.get_full_name() if consult.provider else 'N/A', ""),
('Recommendation', 'التوصية', consult.get_recommendation_display() if consult.recommendation else 'N/A', ""),
('Signed By', 'موقع من قبل', consult.signed_by.get_full_name() if consult.signed_by else 'Not signed', ""),
]
})
# Referral Reason
if consult.referral_reason:
sections.append({
'heading_en': 'Reason for Referral',
'heading_ar': 'سبب الإحالة',
'type': 'text',
'content': [consult.get_referral_reason_display()]
})
# Difficulty Areas
if consult.difficulty_areas.exists():
difficulty_content = []
for area in consult.difficulty_areas.all():
text = f"• {area.get_area_display()}"
if area.details:
text += f": {area.details}"
difficulty_content.append(text)
sections.append({
'heading_en': 'Areas of Difficulty',
'heading_ar': 'مجالات الصعوبة',
'type': 'text',
'content': difficulty_content
})
# Developmental Milestones
if consult.milestones.exists():
milestone_content = []
for milestone in consult.milestones.all():
if milestone.age_achieved:
required_badge = " (Required)" if milestone.is_required else ""
milestone_content.append(f"• {milestone.get_milestone_display()}{required_badge}: {milestone.age_achieved}")
if milestone_content:
sections.append({
'heading_en': 'Developmental Motor Milestones',
'heading_ar': 'المعالم الحركية التنموية',
'type': 'text',
'content': milestone_content
})
# Motor Learning & Regression
if consult.motor_learning_difficulty is not None or consult.motor_skill_regression is not None:
motor_content = []
if consult.motor_learning_difficulty is not None:
motor_content.append(f"Difficulty learning new motor skills: {'Yes' if consult.motor_learning_difficulty else 'No'}")
if consult.motor_learning_details:
motor_content.append(f" Details: {consult.motor_learning_details}")
if consult.motor_skill_regression is not None:
motor_content.append(f"Lost previously gained motor skills: {'Yes' if consult.motor_skill_regression else 'No'}")
if consult.regression_details:
motor_content.append(f" Details: {consult.regression_details}")
sections.append({
'heading_en': 'Motor Learning & Regression',
'heading_ar': 'التعلم الحركي والتراجع',
'type': 'text',
'content': motor_content
})
# Self-Help Skills
if consult.self_help_skills.exists():
skill_content = []
for skill in consult.self_help_skills.all():
if skill.response:
response_text = 'Yes' if skill.response == 'yes' else 'No'
text = f"• {skill.skill_name} ({skill.get_age_range_display()}): {response_text}"
if skill.comments:
text += f" - {skill.comments}"
skill_content.append(text)
if skill_content:
sections.append({
'heading_en': 'Self-Help Skills',
'heading_ar': 'مهارات المساعدة الذاتية',
'type': 'text',
'content': skill_content
})
# Eating/Feeding
eating_content = []
if consult.eats_healthy_variety is not None:
eating_content.append(f"Eats healthy variety: {'Yes' if consult.eats_healthy_variety else 'No'}")
if consult.eats_variety_textures is not None:
eating_content.append(f"Eats variety of textures: {'Yes' if consult.eats_variety_textures else 'No'}")
if consult.participates_family_meals is not None:
eating_content.append(f"Participates in family meals: {'Yes' if consult.participates_family_meals else 'No'}")
if consult.eating_comments:
eating_content.append(f"Comments: {consult.eating_comments}")
if eating_content:
sections.append({
'heading_en': 'Eating / Feeding',
'heading_ar': 'الأكل / التغذية',
'type': 'text',
'content': eating_content
})
# Behavior Comments
behavior_content = []
if consult.infant_behavior_comments:
behavior_content.append(f"Infant Behavior: {consult.infant_behavior_comments}")
if consult.current_behavior_comments:
behavior_content.append(f"Current Behavior: {consult.current_behavior_comments}")
if behavior_content:
sections.append({
'heading_en': 'Behavior Comments',
'heading_ar': 'تعليقات السلوك',
'type': 'text',
'content': behavior_content
})
# Scoring Results
if consult.total_score > 0:
score_content = [
f"Self-Help Score: {consult.self_help_score}/24",
f"Behavior Score: {consult.behavior_score}/48",
f"Developmental Score: {consult.developmental_score}/6",
f"Eating Score: {consult.eating_score}/6",
f"Total Score: {consult.total_score}/84",
f"Interpretation: {consult.score_interpretation}"
]
sections.append({
'heading_en': 'Scoring Results',
'heading_ar': 'نتائج التقييم',
'type': 'text',
'content': score_content
})
# Recommendation Notes
if consult.recommendation_notes:
sections.append({
'heading_en': 'Recommendation Notes',
'heading_ar': 'ملاحظات التوصية',
'type': 'text',
'content': [consult.recommendation_notes]
})
return sections
class OTConsultPDFView(LoginRequiredMixin, TenantFilterMixin, View):
"""Generate PDF for OT consultation."""
def get(self, request, pk):
"""Generate and return PDF."""
consult = get_object_or_404(
OTConsult.objects.select_related(
'patient', 'provider', 'tenant', 'signed_by'
),
pk=pk,
tenant=request.user.tenant
)
pdf_generator = OTConsultPDFGenerator(consult, request)
view_mode = request.GET.get('view', 'download')
return pdf_generator.generate_pdf(view_mode=view_mode)
class OTConsultEmailPDFView(LoginRequiredMixin, TenantFilterMixin, View):
"""Email OT consultation PDF to patient."""
def post(self, request, pk):
"""Send PDF via email."""
consult = get_object_or_404(
OTConsult.objects.select_related(
'patient', 'provider', 'tenant'
),
pk=pk,
tenant=request.user.tenant
)
email_address = request.POST.get('email_address', '').strip()
custom_message = request.POST.get('email_message', '').strip()
if not email_address:
messages.error(request, _('Email address is required.'))
return redirect('ot:consult_detail', pk=pk)
pdf_generator = OTConsultPDFGenerator(consult, request)
subject = f"OT Consultation - {consult.patient.mrn}"
body = f"""
Dear {consult.patient.first_name_en} {consult.patient.last_name_en},
Please find attached your Occupational Therapy consultation details.
Consultation Date: {consult.consultation_date.strftime('%Y-%m-%d')}
Provider: {consult.provider.get_full_name() if consult.provider else 'N/A'}
Best regards,
{consult.tenant.name}
"""
success, message = pdf_generator.send_email(
email_address=email_address,
subject=subject,
body=body,
custom_message=custom_message
)
if success:
messages.success(request, _('PDF sent to %(email)s successfully!') % {'email': email_address})
else:
messages.error(request, _('Failed to send email: %(error)s') % {'error': message})
return redirect('ot:consult_detail', pk=pk)
class OTSessionPDFGenerator(BasePDFGenerator):
"""PDF generator for OT Session (OT-F-3)."""
def get_document_title(self):
"""Return document title in English and Arabic."""
session = self.document
return (
f"OT Session (OT-F-3) - {session.patient.mrn}",
"جلسة العلاج الوظيفي"
)
def get_pdf_filename(self):
"""Return PDF filename."""
session = self.document
date_str = session.session_date.strftime('%Y%m%d')
return f"ot_session_{session.patient.mrn}_{date_str}.pdf"
def get_document_sections(self):
"""Return document sections to render."""
session = self.document
patient = session.patient
sections = []
# Patient Information Section
patient_name_ar = f"{patient.first_name_ar} {patient.last_name_ar}" if patient.first_name_ar else ""
sections.append({
'heading_en': 'Patient Information',
'heading_ar': 'معلومات المريض',
'type': 'table',
'content': [
('Name', 'الاسم', f"{patient.first_name_en} {patient.last_name_en}", patient_name_ar),
('MRN', 'رقم السجل الطبي', patient.mrn, ""),
('Age', 'العمر', f"{patient.age} years", ""),
]
})
# Session Details Section
sections.append({
'heading_en': 'Session Details',
'heading_ar': 'تفاصيل الجلسة',
'type': 'table',
'content': [
('Date', 'التاريخ', session.session_date.strftime('%Y-%m-%d'), ""),
('Provider', 'مقدم الخدمة', session.provider.get_full_name() if session.provider else 'N/A', ""),
('Session Type', 'نوع الجلسة', session.get_session_type_display(), ""),
('Cooperative Level', 'مستوى التعاون', f"{session.cooperative_level}/4 - {session.cooperative_level_display}" if session.cooperative_level else 'N/A', ""),
('Distraction Tolerance', 'تحمل التشتت', f"{session.distraction_tolerance}/4 - {session.distraction_tolerance_display}" if session.distraction_tolerance else 'N/A', ""),
('Signed By', 'موقع من قبل', session.signed_by.get_full_name() if session.signed_by else 'Not signed', ""),
]
})
# Activities Checklist
if session.activities_checklist:
sections.append({
'heading_en': 'Activities Worked On',
'heading_ar': 'الأنشطة التي تم العمل عليها',
'type': 'text',
'content': [session.activities_checklist]
})
# Target Skills
target_skills = session.target_skills.all()
if target_skills:
skill_content = []
for skill in target_skills:
skill_text = f"• {skill.skill_name} - Score: {skill.score}/10 ({skill.achievement_level})"
if skill.notes:
skill_text += f"
Notes: {skill.notes}"
skill_content.append(skill_text)
sections.append({
'heading_en': 'Target Skills Progress',
'heading_ar': 'تقدم المهارات المستهدفة',
'type': 'text',
'content': skill_content
})
# Observations
if session.observations:
sections.append({
'heading_en': 'Observations',
'heading_ar': 'الملاحظات',
'type': 'text',
'content': [session.observations]
})
# Activities Performed
if session.activities_performed:
sections.append({
'heading_en': 'Activities Performed',
'heading_ar': 'الأنشطة المنفذة',
'type': 'text',
'content': [session.activities_performed]
})
# Recommendations
if session.recommendations:
sections.append({
'heading_en': 'Recommendations',
'heading_ar': 'التوصيات',
'type': 'text',
'content': [session.recommendations]
})
return sections
class OTSessionPDFView(LoginRequiredMixin, TenantFilterMixin, View):
"""Generate PDF for OT session."""
def get(self, request, pk):
"""Generate and return PDF."""
session = get_object_or_404(
OTSession.objects.select_related(
'patient', 'provider', 'tenant', 'signed_by'
).prefetch_related('target_skills'),
pk=pk,
tenant=request.user.tenant
)
pdf_generator = OTSessionPDFGenerator(session, request)
view_mode = request.GET.get('view', 'download')
return pdf_generator.generate_pdf(view_mode=view_mode)
class OTSessionEmailPDFView(LoginRequiredMixin, TenantFilterMixin, View):
"""Email OT session PDF to patient."""
def post(self, request, pk):
"""Send PDF via email."""
session = get_object_or_404(
OTSession.objects.select_related(
'patient', 'provider', 'tenant'
),
pk=pk,
tenant=request.user.tenant
)
email_address = request.POST.get('email_address', '').strip()
custom_message = request.POST.get('email_message', '').strip()
if not email_address:
messages.error(request, _('Email address is required.'))
return redirect('ot:session_detail', pk=pk)
pdf_generator = OTSessionPDFGenerator(session, request)
subject = f"OT Session - {session.patient.mrn}"
body = f"""
Dear {session.patient.first_name_en} {session.patient.last_name_en},
Please find attached your Occupational Therapy session details.
Session Date: {session.session_date.strftime('%Y-%m-%d')}
Provider: {session.provider.get_full_name() if session.provider else 'N/A'}
Best regards,
{session.tenant.name}
"""
success, message = pdf_generator.send_email(
email_address=email_address,
subject=subject,
body=body,
custom_message=custom_message
)
if success:
messages.success(request, _('PDF sent to %(email)s successfully!') % {'email': email_address})
else:
messages.error(request, _('Failed to send email: %(error)s') % {'error': message})
return redirect('ot:session_detail', pk=pk)