agdar/ot/views.py
2025-11-02 14:35:35 +03:00

586 lines
19 KiB
Python

"""
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.db.models import Q, Avg
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.urls import reverse_lazy
from core.mixins import (
TenantFilterMixin,
RolePermissionMixin,
AuditLogMixin,
HTMXResponseMixin,
SuccessMessageMixin,
PaginationMixin,
ConsentRequiredMixin,
)
from core.models import User, Patient
from appointments.models import Appointment
from .models import OTConsult, OTSession, OTTargetSkill
from .forms import OTConsultForm, OTSessionForm
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
"""
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=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').order_by('-consultation_date')
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:
- Reasons for referral (multi-select)
- Top 3 difficulty areas (text)
- Developmental motor milestones
- Self-help skills matrix
- Feeding participation
- Infant/now behavior descriptors
- Recommendation (continue/discharge/referral with rules)
- 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 consult detail."""
return reverse_lazy('ot:consult_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 consultation date if not provided
if not form.instance.consultation_date:
form.instance.consultation_date = timezone.now().date()
# Save consultation
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 Consultation (OT-F-1)'
context['submit_text'] = 'Save Consultation'
# 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 OTConsultUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
AuditLogMixin, SuccessMessageMixin, UpdateView):
"""
OT consultation update view (OT-F-1).
Features:
- Update consultation details
- Version history
- Audit trail
"""
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 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 version history if available
if hasattr(self.object, 'history'):
context['history'] = self.object.history.all()[:10]
return context
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
"""
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=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').order_by('-session_date')
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(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
AuditLogMixin, SuccessMessageMixin, UpdateView):
"""
OT session update view (OT-F-3).
Features:
- Update session details
- Version history
- Audit trail
"""
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