""" ABA views for the Tenhal Multidisciplinary Healthcare Platform. This module contains views for ABA (Applied Behavior Analysis) documentation including: - ABA consultations (ABA-F-1) - Functional behavior interviews - Behavior tracking - Antecedents and consequences analysis - Recommendations """ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages from django.db.models import Q from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import gettext_lazy as _ 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, ) from core.models import User, Patient from appointments.models import Appointment from .models import ABAConsult, ABABehavior, ABASession, ABASkillTarget from .forms import ABAConsultForm, ABABehaviorFormSet, ABASessionForm, ABASkillTargetFormSet class ABAConsultListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin, HTMXResponseMixin, ListView): """ ABA consultation list view (ABA-F-1). Features: - Filter by patient, provider, date range - Search by patient name/MRN - Role-based filtering """ model = ABAConsult template_name = 'aba/consult_list.html' htmx_template_name = 'aba/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.ABA: 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').order_by('-consultation_date') class ABAConsultDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView): """ ABA consultation detail view (ABA-F-1). Features: - Full consultation details - Reason of referral - Parental and school concerns - Respondents and interviewer info - Diagnosed conditions - Interaction hours - Physiological/medical factors - Behaviors with frequency, duration, intensity - Antecedents (likely/least likely contexts) - Recommendations """ model = ABAConsult template_name = 'aba/consult_detail.html' context_object_name = 'consult' def get_context_data(self, **kwargs): """Add related behaviors.""" context = super().get_context_data(**kwargs) # Get behaviors context['behaviors'] = self.object.behaviors.all() return context class ABAConsultCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin, AuditLogMixin, SuccessMessageMixin, CreateView): """ ABA consultation creation view (ABA-F-1). Features: - Complete functional interview form - Reason of referral (radio options) - Parental concern, school concern - Respondents, interviewer, diagnosed - Interaction hours - Physiological/medical factors (booleans + text) - Behaviors (1..n) with frequency, duration, intensity - Antecedents (likely/least likely contexts) - Recommendations - Consent verification enforced before creation """ 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 and photo/video consent " "before consultation can be documented." ) def get_success_url(self): """Redirect to consult detail.""" return reverse_lazy('aba: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 get_context_data(self, **kwargs): """Add form title, patient/appointment info, and behavior formset.""" context = super().get_context_data(**kwargs) context['form_title'] = _('ABA Consultation (ABA-F-1)') context['submit_text'] = _('Save Consultation') # Add behavior formset if self.request.POST: context['behavior_formset'] = ABABehaviorFormSet(self.request.POST) else: context['behavior_formset'] = ABABehaviorFormSet() # 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 and provider, handle behavior formset.""" # 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() # Get the formset context = self.get_context_data() behavior_formset = context['behavior_formset'] # Validate formset if behavior_formset.is_valid(): # Save the consultation first self.object = form.save() # Save the behaviors behavior_formset.instance = self.object behavior_formset.save() return super().form_valid(form) else: # Formset is invalid, return to form with errors return self.form_invalid(form) # ============================================================================ # PDF Generation Views # ============================================================================ from core.pdf_service import BasePDFGenerator from django.shortcuts import redirect class ABAConsultPDFGenerator(BasePDFGenerator): """PDF generator for ABA Consultation (ABA-F-1).""" def get_document_title(self): """Return document title in English and Arabic.""" consult = self.document return ( f"ABA Consultation (ABA-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"aba_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', ""), ('Reason of Referral', 'سبب الإحالة', consult.get_reason_of_referral_display(), ""), ('Diagnosed Condition', 'الحالة المشخصة', consult.diagnosed_condition or 'N/A', ""), ('Interaction Hours/Day', 'ساعات التفاعل/اليوم', str(consult.interaction_hours_per_day) if consult.interaction_hours_per_day else 'N/A', ""), ('Signed By', 'موقع من قبل', consult.signed_by.get_full_name() if consult.signed_by else 'Not signed', ""), ] }) # Interview Information if consult.respondents or consult.interviewer: sections.append({ 'heading_en': 'Interview Information', 'heading_ar': 'معلومات المقابلة', 'type': 'table', 'content': [ ('Respondents', 'المستجيبون', consult.respondents or 'N/A', ""), ('Interviewer', 'المحاور', consult.interviewer or 'N/A', ""), ] }) # Concerns if consult.parental_concern: sections.append({ 'heading_en': 'Parental Concern', 'heading_ar': 'قلق الوالدين', 'type': 'text', 'content': [consult.parental_concern] }) if consult.school_concern: sections.append({ 'heading_en': 'School Concern', 'heading_ar': 'قلق المدرسة', 'type': 'text', 'content': [consult.school_concern] }) # Factors if consult.physiological_factors: sections.append({ 'heading_en': 'Physiological Factors', 'heading_ar': 'العوامل الفسيولوجية', 'type': 'text', 'content': [consult.physiological_factors] }) if consult.medical_factors: sections.append({ 'heading_en': 'Medical Factors', 'heading_ar': 'العوامل الطبية', 'type': 'text', 'content': [consult.medical_factors] }) # Behaviors behaviors = consult.behaviors.all() if behaviors: behavior_content = [] for behavior in behaviors: behavior_text = f"{behavior.behavior_description}
" behavior_text += f"Frequency: {behavior.get_frequency_display()}, " behavior_text += f"Intensity: {behavior.get_intensity_display()}" if behavior.duration: behavior_text += f", Duration: {behavior.duration}" if behavior.antecedents_likely: behavior_text += f"
Most Likely Context: {behavior.antecedents_likely}" if behavior.antecedents_least_likely: behavior_text += f"
Least Likely Context: {behavior.antecedents_least_likely}" if behavior.consequences: behavior_text += f"
Consequences: {behavior.consequences}" behavior_content.append(behavior_text) sections.append({ 'heading_en': 'Behaviors', 'heading_ar': 'السلوكيات', 'type': 'text', 'content': behavior_content }) # Recommendations if consult.recommendations: sections.append({ 'heading_en': 'Recommendations', 'heading_ar': 'التوصيات', 'type': 'text', 'content': [consult.recommendations] }) return sections class ABAConsultPDFView(LoginRequiredMixin, TenantFilterMixin, View): """Generate PDF for ABA consultation.""" def get(self, request, pk): """Generate and return PDF.""" consult = get_object_or_404( ABAConsult.objects.select_related( 'patient', 'provider', 'tenant', 'signed_by' ).prefetch_related('behaviors'), pk=pk, tenant=request.user.tenant ) pdf_generator = ABAConsultPDFGenerator(consult, request) view_mode = request.GET.get('view', 'download') return pdf_generator.generate_pdf(view_mode=view_mode) class ABAConsultEmailPDFView(LoginRequiredMixin, TenantFilterMixin, View): """Email ABA consultation PDF to patient.""" def post(self, request, pk): """Send PDF via email.""" consult = get_object_or_404( ABAConsult.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('aba:consult_detail', pk=pk) pdf_generator = ABAConsultPDFGenerator(consult, request) subject = f"ABA Consultation - {consult.patient.mrn}" body = f""" Dear {consult.patient.first_name_en} {consult.patient.last_name_en}, Please find attached your ABA 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('aba:consult_detail', pk=pk) class ABASessionSignView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, View): """ Sign an ABA 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.ABA] def post(self, request, pk): """Sign the session.""" session = get_object_or_404( ABASession, 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('aba: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('aba: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!") ) return HttpResponseRedirect( reverse_lazy('aba:session_detail', kwargs={'pk': pk}) ) class ABAConsultUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, SuccessMessageMixin, UpdateView): """ ABA consultation update view (ABA-F-1). Features: - Update consultation details - Version history - Audit trail """ model = ABAConsult form_class = ABAConsultForm template_name = 'aba/consult_form.html' success_message = _("ABA consultation updated successfully!") allowed_roles = [User.Role.ADMIN, User.Role.ABA] def get_success_url(self): """Redirect to consult detail.""" return reverse_lazy('aba:consult_detail', kwargs={'pk': self.object.pk}) def get_context_data(self, **kwargs): """Add form title, behavior formset, and history.""" context = super().get_context_data(**kwargs) context['form_title'] = _('Update ABA Consultation - %(mrn)s') % {'mrn': self.object.patient.mrn} context['submit_text'] = _('Update Consultation') context['patient'] = self.object.patient # Add behavior formset if self.request.POST: context['behavior_formset'] = ABABehaviorFormSet(self.request.POST, instance=self.object) else: context['behavior_formset'] = ABABehaviorFormSet(instance=self.object) # Add version history if available if hasattr(self.object, 'history'): context['history'] = self.object.history.all()[:10] return context def form_valid(self, form): """Handle behavior formset.""" # Get the formset context = self.get_context_data() behavior_formset = context['behavior_formset'] # Validate formset if behavior_formset.is_valid(): # Save the consultation first self.object = form.save() # Save the behaviors behavior_formset.instance = self.object behavior_formset.save() return super().form_valid(form) else: # Formset is invalid, return to form with errors return self.form_invalid(form) class PatientABAHistoryView(LoginRequiredMixin, TenantFilterMixin, ListView): """ Patient ABA history view. Features: - All ABA consultations for specific patient - Behavior tracking over time - Progress visualization """ model = ABAConsult template_name = 'aba/patient_history.html' context_object_name = 'consults' paginate_by = 50 def get_queryset(self): """Get all consults for patient.""" patient_id = self.kwargs.get('patient_id') return ABAConsult.objects.filter( patient_id=patient_id, tenant=self.request.user.tenant ).order_by('-consultation_date') def get_context_data(self, **kwargs): """Add patient and behavior tracking data.""" import json 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 all behaviors across consultations for this patient consults = self.get_queryset() all_behaviors = [] high_intensity_count = 0 for consult in consults: behaviors = consult.behaviors.all() for behavior in behaviors: all_behaviors.append({ 'date': consult.consultation_date.strftime('%Y-%m-%d'), 'behavior': behavior.behavior_description, 'frequency': behavior.get_frequency_display(), 'intensity': behavior.get_intensity_display(), }) # Count high intensity behaviors if behavior.intensity == 'SEVERE': high_intensity_count += 1 # Add statistics context['total_behaviors_count'] = len(all_behaviors) context['high_intensity_count'] = high_intensity_count # Properly serialize to JSON for chart context['all_behaviors'] = json.dumps(all_behaviors) return context class BehaviorTrackingView(LoginRequiredMixin, TenantFilterMixin, ListView): """ Behavior tracking view across all patients. Features: - Track specific behaviors - Frequency analysis - Intensity trends - Intervention effectiveness """ model = ABABehavior template_name = 'aba/behavior_tracking.html' context_object_name = 'behaviors' paginate_by = 50 def get_queryset(self): """Get behaviors with filters.""" queryset = ABABehavior.objects.filter( consult__tenant=self.request.user.tenant ).select_related('consult', 'consult__patient') # Apply filters behavior_type = self.request.GET.get('behavior_type') if behavior_type: queryset = queryset.filter(behavior_description__icontains=behavior_type) frequency = self.request.GET.get('frequency') if frequency: queryset = queryset.filter(frequency=frequency) intensity = self.request.GET.get('intensity') if intensity: queryset = queryset.filter(intensity=intensity) return queryset.order_by('-consult__consultation_date') def get_context_data(self, **kwargs): """Add statistics.""" context = super().get_context_data(**kwargs) queryset = self.get_queryset() # Add statistics context['stats'] = { 'total_behaviors': queryset.count(), 'high_intensity': queryset.filter(intensity='SEVERE').count(), 'daily_frequency': queryset.filter(frequency='DAILY').count(), } context['frequency_choices'] = ABABehavior.Frequency.choices context['intensity_choices'] = ABABehavior.Intensity.choices return context class RecommendationTrackingView(LoginRequiredMixin, TenantFilterMixin, ListView): """ Recommendation tracking view. Features: - Track recommendations across consultations - Implementation status - Effectiveness analysis """ model = ABAConsult template_name = 'aba/recommendation_tracking.html' context_object_name = 'consults' paginate_by = 25 def get_queryset(self): """Get consults with recommendations.""" queryset = super().get_queryset() # Filter only consults with recommendations queryset = queryset.exclude(recommendations='').exclude(recommendations__isnull=True) # Apply filters patient_id = self.request.GET.get('patient') if patient_id: queryset = queryset.filter(patient_id=patient_id) return queryset.select_related('patient', 'provider').order_by('-consultation_date') class ABASessionListView(LoginRequiredMixin, TenantFilterMixin, PaginationMixin, HTMXResponseMixin, ListView): """ ABA session list view. Features: - Filter by patient, provider, date range, session type - Search by patient name/MRN - Role-based filtering - Shows unsigned sessions notification """ model = ABASession template_name = 'aba/session_list.html' htmx_template_name = 'aba/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.ABA: 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_sessions_query = ABASession.objects.filter( tenant=user.tenant, signed_by__isnull=True ) # Filter by provider if ABA role if user.role == User.Role.ABA: unsigned_sessions_query = unsigned_sessions_query.filter(provider=user) context['unsigned_sessions_count'] = unsigned_sessions_query.count() context['unsigned_sessions'] = unsigned_sessions_query.select_related( 'patient', 'provider' ).order_by('-session_date')[:10] # Show latest 10 unsigned sessions return context class ABASessionDetailView(LoginRequiredMixin, TenantFilterMixin, DetailView): """ ABA session detail view. Features: - Full session details - Session type, duration - Engagement and cooperation levels - Target behaviors and interventions - Skill targets with mastery tracking - Progress notes and recommendations """ model = ABASession template_name = 'aba/session_detail.html' context_object_name = 'session' def get_context_data(self, **kwargs): """Add skill targets and progress data.""" context = super().get_context_data(**kwargs) # Get skill targets context['skill_targets'] = self.object.skill_targets.all() # Get previous sessions for comparison context['previous_sessions'] = ABASession.objects.filter( patient=self.object.patient, session_date__lt=self.object.session_date ).order_by('-session_date')[:5] return context class ABASessionCreateView(ConsentRequiredMixin, LoginRequiredMixin, RolePermissionMixin, AuditLogMixin, SuccessMessageMixin, CreateView): """ ABA session creation view. Features: - Session date, type, duration - Engagement and cooperation levels (1-5) - Target behaviors and interventions - Activities performed - Behavioral observations - Progress notes - Recommendations and home program - Skill targets with mastery tracking - Consent verification enforced before creation """ model = ABASession form_class = ABASessionForm template_name = 'aba/session_form.html' success_message = _("ABA session 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 and photo/video consent " "before session can be documented." ) def get_success_url(self): """Redirect to session detail.""" return reverse_lazy('aba: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 get_context_data(self, **kwargs): """Add form title, patient/appointment info, and skill target formset.""" context = super().get_context_data(**kwargs) context['form_title'] = _('ABA Session Note') context['submit_text'] = _('Save Session') # Add skill target formset if self.request.POST: context['skill_target_formset'] = ABASkillTargetFormSet(self.request.POST) else: context['skill_target_formset'] = ABASkillTargetFormSet() # 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 and provider, handle skill target formset.""" # 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() # Get the formset context = self.get_context_data() skill_target_formset = context['skill_target_formset'] # Validate formset if skill_target_formset.is_valid(): # Save the session first self.object = form.save() # Save the skill targets skill_target_formset.instance = self.object skill_target_formset.save() return super().form_valid(form) else: # Formset is invalid, return to form with errors return self.form_invalid(form) class ABASessionUpdateView(LoginRequiredMixin, RolePermissionMixin, TenantFilterMixin, AuditLogMixin, SuccessMessageMixin, UpdateView): """ ABA session update view. Features: - Update session details - Version history - Audit trail """ model = ABASession form_class = ABASessionForm template_name = 'aba/session_form.html' success_message = _("ABA session updated successfully!") allowed_roles = [User.Role.ADMIN, User.Role.ABA] def get_success_url(self): """Redirect to session detail.""" return reverse_lazy('aba:session_detail', kwargs={'pk': self.object.pk}) def get_context_data(self, **kwargs): """Add form title, skill target formset, and history.""" context = super().get_context_data(**kwargs) context['form_title'] = _('Update ABA Session - %(mrn)s') % {'mrn': self.object.patient.mrn} context['submit_text'] = _('Update Session') context['patient'] = self.object.patient # Add skill target formset if self.request.POST: context['skill_target_formset'] = ABASkillTargetFormSet(self.request.POST, instance=self.object) else: context['skill_target_formset'] = ABASkillTargetFormSet(instance=self.object) # Add version history if available if hasattr(self.object, 'history'): context['history'] = self.object.history.all()[:10] return context def form_valid(self, form): """Handle skill target formset.""" # Get the formset context = self.get_context_data() skill_target_formset = context['skill_target_formset'] # Validate formset if skill_target_formset.is_valid(): # Save the session first self.object = form.save() # Save the skill targets skill_target_formset.instance = self.object skill_target_formset.save() return super().form_valid(form) else: # Formset is invalid, return to form with errors return self.form_invalid(form)