agdar/medical/views.py
Marwan Alwali a04817ef6e update
2025-11-02 19:25:08 +03:00

792 lines
30 KiB
Python

"""
Medical views for the Tenhal Multidisciplinary Healthcare Platform.
This module contains views for medical documentation including:
- Medical consultations (MD-F-1)
- Follow-up visits (MD-F-2)
- Medication management
- Lab/radiology integration
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.utils import timezone
from django.views.generic import ListView, DetailView, CreateView, UpdateView, View
from django.urls import reverse_lazy
from django.contrib import messages
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 MedicalConsultation, MedicalFollowUp, MedicationPlan, ConsultationResponse, ConsultationFeedback
from .forms import (
MedicalConsultationForm, MedicalFollowUpForm, MedicationPlanFormSet,
ConsultationResponseForm, ConsultationFeedbackForm
)
# Sign Views
class MedicalConsultationSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
"""Sign a medical consultation."""
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
def post(self, request, pk):
consultation = get_object_or_404(MedicalConsultation, pk=pk, tenant=request.user.tenant)
if consultation.signed_by:
messages.warning(request, "This consultation has already been signed.")
return HttpResponseRedirect(reverse_lazy('medical:consultation_detail', kwargs={'pk': pk}))
if consultation.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('medical:consultation_detail', kwargs={'pk': pk}))
consultation.signed_by = request.user
consultation.signed_at = timezone.now()
consultation.save()
messages.success(request, "Consultation signed successfully!")
return HttpResponseRedirect(reverse_lazy('medical:consultation_detail', kwargs={'pk': pk}))
class MedicalFollowUpSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View):
"""Sign a medical follow-up."""
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
def post(self, request, pk):
followup = get_object_or_404(MedicalFollowUp, pk=pk, tenant=request.user.tenant)
if followup.signed_by:
messages.warning(request, "This follow-up has already been signed.")
return HttpResponseRedirect(reverse_lazy('medical:followup_detail', kwargs={'pk': pk}))
if followup.provider != request.user and request.user.role != User.Role.ADMIN:
messages.error(request, "Only the follow-up provider or an administrator can sign this follow-up.")
return HttpResponseRedirect(reverse_lazy('medical:followup_detail', kwargs={'pk': pk}))
followup.signed_by = request.user
followup.signed_at = timezone.now()
followup.save()
messages.success(request, "Follow-up signed successfully!")
return HttpResponseRedirect(reverse_lazy('medical:followup_detail', kwargs={'pk': pk}))
class MedicalConsultationListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
HTMXResponseMixin, ListView):
"""
Medical consultation list view (MD-F-1).
Features:
- Filter by patient, provider, date range
- Search by patient name/MRN
- Role-based filtering (doctors see their consultations)
"""
model = MedicalConsultation
template_name = 'medical/consultation_list.html'
htmx_template_name = 'medical/partials/consultation_list_partial.html'
context_object_name = 'consultations'
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.DOCTOR:
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)
provider_id = self.request.GET.get('provider')
if provider_id:
queryset = queryset.filter(provider_id=provider_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 filter options and unsigned counts."""
context = super().get_context_data(**kwargs)
user = self.request.user
# Add providers for filter
context['providers'] = User.objects.filter(
tenant=self.request.user.tenant,
role=User.Role.DOCTOR
)
# Get unsigned consultations
unsigned_query = MedicalConsultation.objects.filter(tenant=user.tenant, signed_by__isnull=True)
if user.role == User.Role.DOCTOR:
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]
# Add current filters
context['current_filters'] = {
'search': self.request.GET.get('search', ''),
'patient': self.request.GET.get('patient', ''),
'provider': self.request.GET.get('provider', ''),
'date_from': self.request.GET.get('date_from', ''),
'date_to': self.request.GET.get('date_to', ''),
}
return context
class MedicalConsultationDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
"""
Medical consultation detail view (MD-F-1).
Features:
- Full consultation details with all sections
- Medication plans
- Lab/radiology orders
- Follow-up consultations
- Behavioral symptoms checklist
"""
model = MedicalConsultation
template_name = 'medical/consultation_detail.html'
context_object_name = 'consultation'
def get_context_data(self, **kwargs):
"""Add related data."""
context = super().get_context_data(**kwargs)
consultation = self.object
# Get medication plans (medications is a JSONField, not a related manager)
context['medications'] = consultation.medications if consultation.medications else []
# Get follow-ups
context['followups'] = MedicalFollowUp.objects.filter(
patient=consultation.patient,
previous_consultation=consultation
).order_by('-followup_date')
# Get responses from other disciplines
context['responses'] = consultation.responses.select_related('responder').order_by('-response_date')
# Get feedback
context['feedback'] = consultation.feedback.select_related('submitted_by').order_by('-feedback_date')
# Calculate average feedback rating
feedback_list = list(context['feedback'])
if feedback_list:
avg_ratings = [f.average_rating for f in feedback_list if f.average_rating is not None]
context['average_feedback_rating'] = sum(avg_ratings) / len(avg_ratings) if avg_ratings else None
else:
context['average_feedback_rating'] = None
# Get lab/radiology orders (from integrations)
try:
from integrations.models import ExternalOrder
context['lab_orders'] = ExternalOrder.objects.filter(
patient=consultation.patient,
order_type='LAB',
created_at__gte=consultation.consultation_date
).order_by('-created_at')[:5]
context['radiology_orders'] = ExternalOrder.objects.filter(
patient=consultation.patient,
order_type='RADIOLOGY',
created_at__gte=consultation.consultation_date
).order_by('-created_at')[:5]
except:
context['lab_orders'] = []
context['radiology_orders'] = []
return context
class MedicalConsultationCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin,
AuditLogMixin, SuccessMessageMixin, CreateView):
"""
Medical consultation creation view (MD-F-1).
Features:
- Complete consultation form with all sections
- Chief complaint, present history, past history
- Vaccination, family history, social history
- Pregnancy/neonatal/developmental history
- Behavioral symptoms checklist
- Physical examination
- Summary and recommendations
- Medication formset
- Consent verification enforced before creation
"""
model = MedicalConsultation
form_class = MedicalConsultationForm
template_name = 'medical/consultation_form.html'
success_message = "Medical consultation recorded successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
# Consent enforcement
consent_service_type = 'MEDICAL'
consent_error_message = (
"Patient must sign general treatment consent before medical consultation can be documented."
)
def get_success_url(self):
"""Redirect to consultation detail."""
return reverse_lazy('medical:consultation_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, handle medications JSON."""
# 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()
# Handle medications JSON field
import json
medications_json = self.request.POST.get('medications', '[]')
try:
form.instance.medications = json.loads(medications_json)
except json.JSONDecodeError:
form.instance.medications = []
# Save consultation
self.object = form.save()
# Update appointment if linked
if self.object.appointment:
# Appointment remains IN_PROGRESS until completed
pass
messages.success(self.request, self.success_message)
return redirect(self.get_success_url())
def get_context_data(self, **kwargs):
"""Add form title, patient/appointment info, and medication formset."""
context = super().get_context_data(**kwargs)
context['form_title'] = 'Medical Consultation (MD-F-1)'
context['submit_text'] = 'Save Consultation'
# Add medication formset if not already in context
if 'medication_formset' not in context:
context['medication_formset'] = MedicationPlanFormSet()
# 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 MedicalConsultationUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
AuditLogMixin, SuccessMessageMixin, UpdateView):
"""
Medical consultation update view (MD-F-1).
Features:
- Update consultation details
- Medication formset
- Version history
- Audit trail
"""
model = MedicalConsultation
form_class = MedicalConsultationForm
template_name = 'medical/consultation_form.html'
success_message = "Medical consultation updated successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
def get_success_url(self):
"""Redirect to consultation detail."""
return reverse_lazy('medical:consultation_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
"""Save form and handle medications JSON."""
# Handle medications JSON field
import json
medications_json = self.request.POST.get('medications', '[]')
try:
form.instance.medications = json.loads(medications_json)
except json.JSONDecodeError:
form.instance.medications = []
# Save consultation
self.object = form.save()
messages.success(self.request, self.success_message)
return redirect(self.get_success_url())
def get_context_data(self, **kwargs):
"""Add form title, medication formset, and history."""
context = super().get_context_data(**kwargs)
context['form_title'] = f'Update Medical Consultation - {self.object.patient.mrn}'
context['submit_text'] = 'Update Consultation'
context['patient'] = self.object.patient
# Add medication formset if not already in context
if 'medication_formset' not in context:
context['medication_formset'] = MedicationPlanFormSet(instance=self.object)
# Add version history if available
if hasattr(self.object, 'history'):
context['history'] = self.object.history.all()[:10]
return context
class MedicalFollowUpListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin,
HTMXResponseMixin, ListView):
"""
Medical follow-up list view (MD-F-2).
Features:
- Filter by patient, provider, date range
- Search by patient name/MRN
- Link to previous consultations
"""
model = MedicalFollowUp
template_name = 'medical/followup_list.html'
htmx_template_name = 'medical/partials/followup_list_partial.html'
context_object_name = 'followups'
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.DOCTOR:
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(followup_date__gte=date_from)
date_to = self.request.GET.get('date_to')
if date_to:
queryset = queryset.filter(followup_date__lte=date_to)
return queryset.select_related(
'patient', 'provider', 'appointment', 'previous_consultation', 'signed_by'
).order_by('-followup_date')
def get_context_data(self, **kwargs):
"""Add unsigned follow-ups count."""
context = super().get_context_data(**kwargs)
user = self.request.user
unsigned_query = MedicalFollowUp.objects.filter(tenant=user.tenant, signed_by__isnull=True)
if user.role == User.Role.DOCTOR:
unsigned_query = unsigned_query.filter(provider=user)
context['unsigned_count'] = unsigned_query.count()
context['unsigned_items'] = unsigned_query.select_related('patient', 'provider').order_by('-followup_date')[:10]
return context
class MedicalFollowUpDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView):
"""
Medical follow-up detail view (MD-F-2).
Features:
- Full follow-up details
- Previous consultation reference
- Previous complaints status
- New complaints
- Vital signs link
- Assessment and recommendations
- Family satisfaction score
- Medication snapshot
"""
model = MedicalFollowUp
template_name = 'medical/followup_detail.html'
context_object_name = 'followup'
def get_context_data(self, **kwargs):
"""Add related data."""
context = super().get_context_data(**kwargs)
followup = self.object
# Get previous consultation
if followup.previous_consultation:
context['previous_consultation'] = followup.previous_consultation
# medications is a JSONField, not a related manager
context['previous_medications'] = followup.previous_consultation.medications if followup.previous_consultation.medications else []
# Get vital signs from nursing
from nursing.models import NursingEncounter
if followup.appointment:
context['vitals'] = NursingEncounter.objects.filter(
appointment=followup.appointment
).first()
return context
class MedicalFollowUpCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin,
AuditLogMixin, SuccessMessageMixin, CreateView):
"""
Medical follow-up creation view (MD-F-2).
Features:
- Link to previous consultation
- Previous complaints with status (Resolved/Static/Worse)
- New complaints
- Vital signs reference
- Assessment and recommendations
- Family satisfaction (0/50/100)
- Medication table snapshot
- Consent verification enforced before creation
"""
model = MedicalFollowUp
form_class = MedicalFollowUpForm
template_name = 'medical/followup_form.html'
success_message = "Medical follow-up recorded successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
# Consent enforcement
consent_service_type = 'MEDICAL'
consent_error_message = (
"Patient must sign general treatment consent before medical follow-up can be documented."
)
def get_success_url(self):
"""Redirect to follow-up detail."""
return reverse_lazy('medical:followup_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 get_initial(self):
"""Pre-populate form fields from appointment."""
initial = super().get_initial()
appointment_id = self.request.GET.get('appointment_id')
if appointment_id:
try:
appointment = Appointment.objects.get(pk=appointment_id, tenant=self.request.user.tenant)
initial['patient'] = appointment.patient.pk
initial['appointment'] = appointment.pk
initial['followup_date'] = appointment.appointment_date
initial['provider'] = self.request.user.pk
except Appointment.DoesNotExist:
pass
patient_id = self.request.GET.get('patient')
if patient_id and not appointment_id:
initial['patient'] = patient_id
initial['provider'] = self.request.user.pk
initial['followup_date'] = timezone.now().date()
return initial
def form_valid(self, form):
"""Set tenant and provider."""
# Set tenant
form.instance.tenant = self.request.user.tenant
# Set provider if not set
if not form.instance.provider:
form.instance.provider = self.request.user
# Set follow-up date if not provided
if not form.instance.followup_date:
form.instance.followup_date = timezone.now().date()
# Save follow-up
response = super().form_valid(form)
return response
def get_context_data(self, **kwargs):
"""Add form title and previous consultation info."""
context = super().get_context_data(**kwargs)
context['form_title'] = 'Medical Follow-up (MD-F-2)'
context['submit_text'] = 'Save Follow-up'
# Get patient if provided
patient_id = self.request.GET.get('patient')
if patient_id:
try:
patient = Patient.objects.get(
pk=patient_id,
tenant=self.request.user.tenant
)
context['patient'] = patient
# Get most recent consultation for this patient
context['recent_consultations'] = MedicalConsultation.objects.filter(
patient=patient
).order_by('-consultation_date')[:5]
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
# Get most recent consultations for this patient
context['recent_consultations'] = MedicalConsultation.objects.filter(
patient=context['appointment'].patient
).order_by('-consultation_date')[:5]
except Appointment.DoesNotExist:
pass
return context
class MedicalFollowUpUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin,
AuditLogMixin, SuccessMessageMixin, UpdateView):
"""
Medical follow-up update view (MD-F-2).
Features:
- Update follow-up details
- Version history
- Audit trail
"""
model = MedicalFollowUp
form_class = MedicalFollowUpForm
template_name = 'medical/followup_form.html'
success_message = "Medical follow-up updated successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR]
def get_success_url(self):
"""Redirect to follow-up detail."""
return reverse_lazy('medical:followup_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 Medical Follow-up - {self.object.patient.mrn}'
context['submit_text'] = 'Update Follow-up'
# Add version history if available
if hasattr(self.object, 'history'):
context['history'] = self.object.history.all()[:10]
return context
class ConsultationResponseCreateView(LoginRequiredMixin, RolePermissionMixin, AuditLogMixin,
SuccessMessageMixin, CreateView):
"""
Create response to medical consultation from other disciplines.
Features:
- OT, SLP, ABA, Nursing can respond to consultations
- Assessment and recommendations
- Follow-up needed flag
"""
model = ConsultationResponse
form_class = ConsultationResponseForm
template_name = 'medical/response_form.html'
success_message = "Response submitted successfully!"
allowed_roles = [User.Role.ADMIN, User.Role.DOCTOR, User.Role.NURSE,
User.Role.OT, User.Role.SLP, User.Role.ABA]
def get_success_url(self):
"""Redirect to consultation detail."""
return reverse_lazy('medical:consultation_detail',
kwargs={'pk': self.object.consultation.pk})
def get_initial(self):
"""Pre-populate consultation and responder."""
initial = super().get_initial()
consultation_id = self.kwargs.get('consultation_pk')
if consultation_id:
initial['consultation'] = consultation_id
initial['responder'] = self.request.user.pk
initial['response_date'] = timezone.now().date()
return initial
def form_valid(self, form):
"""Set tenant and responder."""
form.instance.tenant = self.request.user.tenant
form.instance.responder = self.request.user
if not form.instance.response_date:
form.instance.response_date = timezone.now().date()
return super().form_valid(form)
def get_context_data(self, **kwargs):
"""Add consultation info."""
context = super().get_context_data(**kwargs)
consultation_id = self.kwargs.get('consultation_pk')
if consultation_id:
try:
consultation = MedicalConsultation.objects.get(
pk=consultation_id,
tenant=self.request.user.tenant
)
context['consultation'] = consultation
context['form_title'] = f'Respond to Consultation - {consultation.patient.mrn}'
except MedicalConsultation.DoesNotExist:
pass
return context
class ConsultationFeedbackCreateView(LoginRequiredMixin, AuditLogMixin,
SuccessMessageMixin, CreateView):
"""
Create feedback for medical consultation.
Features:
- Family/caregiver feedback
- Team feedback
- Ratings and comments
"""
model = ConsultationFeedback
form_class = ConsultationFeedbackForm
template_name = 'medical/feedback_form.html'
success_message = "Feedback submitted successfully!"
def get_success_url(self):
"""Redirect to consultation detail."""
return reverse_lazy('medical:consultation_detail',
kwargs={'pk': self.object.consultation.pk})
def get_initial(self):
"""Pre-populate consultation."""
initial = super().get_initial()
consultation_id = self.kwargs.get('consultation_pk')
if consultation_id:
initial['consultation'] = consultation_id
initial['feedback_date'] = timezone.now().date()
# Set submitted_by if user is logged in
if self.request.user.is_authenticated:
initial['submitted_by'] = self.request.user.pk
return initial
def form_valid(self, form):
"""Set submitted_by if authenticated."""
if self.request.user.is_authenticated and not form.instance.submitted_by:
form.instance.submitted_by = self.request.user
if not form.instance.feedback_date:
form.instance.feedback_date = timezone.now().date()
return super().form_valid(form)
def get_context_data(self, **kwargs):
"""Add consultation info."""
context = super().get_context_data(**kwargs)
consultation_id = self.kwargs.get('consultation_pk')
if consultation_id:
try:
consultation = MedicalConsultation.objects.get(pk=consultation_id)
context['consultation'] = consultation
context['form_title'] = f'Provide Feedback - {consultation.patient.mrn}'
except MedicalConsultation.DoesNotExist:
pass
return context