""" RCA (Root Cause Analysis) views """ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.db.models import Q, Count from django.core.exceptions import PermissionDenied from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse_lazy from django.utils import timezone from django.views import View from django.views.generic import ( CreateView, DeleteView, DetailView, ListView, UpdateView, ) def _check_rca_access(request, rca): user = request.user if user.is_superuser: return if user.is_px_admin(): tenant = getattr(request, "tenant_hospital", None) if tenant and rca.hospital_id == tenant.id: return elif user.hospital and rca.hospital_id == user.hospital.id: return raise PermissionDenied("You don't have access to this RCA.") from .forms import ( RCAAttachmentForm, RCAClosureForm, RCACorrectiveActionForm, RCANoteForm, RCARootCauseForm, RCAApprovalForm, RCAStatusChangeForm, RootCauseAnalysisForm, ) from .models import ( RCAActionStatus, RCAStatus, RCACorrectiveAction, RCARootCause, RootCauseAnalysis, ) class RCAListView(LoginRequiredMixin, ListView): """List view for Root Cause Analyses""" model = RootCauseAnalysis template_name = "rca/rca_list.html" context_object_name = "rcas" paginate_by = 20 def get_queryset(self): queryset = ( RootCauseAnalysis.objects.filter(is_deleted=False) .select_related("hospital", "department", "assigned_to", "created_by") .prefetch_related("root_causes", "corrective_actions") ) # Get filter parameters status = self.request.GET.get("status") severity = self.request.GET.get("severity") priority = self.request.GET.get("priority") hospital = self.request.GET.get("hospital") search = self.request.GET.get("search") date_from = self.request.GET.get("date_from") date_to = self.request.GET.get("date_to") # Apply filters if status: queryset = queryset.filter(status=status) if severity: queryset = queryset.filter(severity=severity) if priority: queryset = queryset.filter(priority=priority) if hospital: queryset = queryset.filter(hospital_id=hospital) if search: queryset = queryset.filter(Q(title__icontains=search) | Q(description__icontains=search)) if date_from: queryset = queryset.filter(created_at__gte=date_from) if date_to: queryset = queryset.filter(created_at__lte=date_to) # Filter by user's hospital (if not admin) user = self.request.user if user.is_px_admin(): tenant = getattr(self.request, "tenant_hospital", None) if tenant: queryset = queryset.filter(hospital=tenant) elif user.hospital: queryset = queryset.filter(hospital=user.hospital) else: queryset = queryset.none() return queryset.order_by("-created_at") def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) # Add filter form from .forms import RCAFilterForm context["filter_form"] = RCAFilterForm(self.request.GET) # Add counts context["total_count"] = self.get_queryset().count() context["draft_count"] = self.get_queryset().filter(status=RCAStatus.DRAFT).count() context["in_progress_count"] = self.get_queryset().filter(status=RCAStatus.IN_PROGRESS).count() context["review_count"] = self.get_queryset().filter(status=RCAStatus.REVIEW).count() context["approved_count"] = self.get_queryset().filter(status=RCAStatus.APPROVED).count() context["closed_count"] = self.get_queryset().filter(status=RCAStatus.CLOSED).count() return context class RCADetailView(LoginRequiredMixin, DetailView): """Detail view for Root Cause Analysis""" model = RootCauseAnalysis template_name = "rca/rca_detail.html" context_object_name = "rca" def get_queryset(self): queryset = ( RootCauseAnalysis.objects.filter(is_deleted=False) .select_related("hospital", "department", "assigned_to", "created_by", "approved_by", "closed_by") .prefetch_related( "root_causes__verified_by", "corrective_actions__root_cause", "attachments", "notes__created_by", "status_logs__changed_by", ) ) user = self.request.user if user.is_px_admin(): tenant = getattr(self.request, "tenant_hospital", None) if tenant: return queryset.filter(hospital=tenant) elif user.hospital: return queryset.filter(hospital=user.hospital) return queryset.none() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["status_choices"] = RCAStatus.choices total_actions = self.object.corrective_actions.count() completed_actions = self.object.corrective_actions.filter(status=RCAActionStatus.COMPLETED).count() context["progress_percentage"] = (completed_actions / total_actions * 100) if total_actions > 0 else 0 if self.object.content_type and self.object.object_id: try: model_class = self.object.content_type.model_class() related_item = model_class.objects.filter(pk=self.object.object_id).first() context["related_item"] = related_item context["related_item_type"] = self.object.content_type.model except Exception: pass return context class RCACreateView(LoginRequiredMixin, CreateView): """Create view for Root Cause Analysis""" model = RootCauseAnalysis form_class = RootCauseAnalysisForm template_name = "rca/rca_form.html" success_url = reverse_lazy("rca:rca_list") def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["user"] = self.request.user kwargs["request"] = self.request return kwargs def get_initial(self): initial = super().get_initial() related_model = self.request.GET.get("related_model") or self.request.POST.get("related_model") related_id = self.request.GET.get("related_id") or self.request.POST.get("related_id") if related_model and related_id: initial["related_model"] = related_model initial["related_id"] = related_id try: content_type = ContentType.objects.get(model=related_model) initial["content_type"] = content_type.pk initial["object_id"] = related_id model_class = content_type.model_class() if model_class and hasattr(model_class, "objects"): obj = model_class.objects.filter(pk=related_id).first() if obj: if hasattr(obj, "hospital_id"): initial["hospital"] = obj.hospital_id if hasattr(obj, "department_id"): initial["department"] = obj.department_id if hasattr(obj, "title"): initial["title"] = f"RCA: {obj.title}" elif hasattr(obj, "subject"): initial["title"] = f"RCA: {obj.subject}" if hasattr(obj, "description"): initial["description"] = obj.description except ContentType.DoesNotExist: pass return initial def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) related_model = self.request.GET.get("related_model") related_id = self.request.GET.get("related_id") context["related_model"] = related_model context["related_id"] = related_id if related_model and related_id: try: content_type = ContentType.objects.get(model=related_model) model_class = content_type.model_class() if model_class: obj = model_class.objects.filter(pk=related_id).first() if obj: context["related_item"] = obj context["related_item_type"] = related_model except ContentType.DoesNotExist: pass return context def form_valid(self, form): rca = form.save(commit=False) rca.created_by = self.request.user related_model = self.request.POST.get("related_model") related_id = self.request.POST.get("related_id") if related_model and related_id: try: content_type = ContentType.objects.get(model=related_model) rca.content_type = content_type rca.object_id = related_id except ContentType.DoesNotExist: pass rca.save() rca.status_logs.create(old_status="", new_status=rca.status, changed_by=self.request.user, notes="RCA created") messages.success(self.request, "Root Cause Analysis created successfully!") return redirect("rca:rca_detail", pk=rca.pk) class RCAUpdateView(LoginRequiredMixin, UpdateView): """Update view for Root Cause Analysis""" model = RootCauseAnalysis form_class = RootCauseAnalysisForm template_name = "rca/rca_form.html" def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["user"] = self.request.user kwargs["request"] = self.request return kwargs def get_object(self, queryset=None): obj = super().get_object(queryset) _check_rca_access(self.request, obj) return obj def get_success_url(self): return reverse_lazy("rca:rca_detail", kwargs={"pk": self.object.pk}) def form_valid(self, form): rca = form.save() messages.success(self.request, "Root Cause Analysis updated successfully!") return redirect(self.get_success_url()) class RCADeleteView(LoginRequiredMixin, View): """Delete (soft delete) view for Root Cause Analysis""" def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) _check_rca_access(request, rca) rca.soft_delete(user=request.user) messages.success(request, "Root Cause Analysis deleted successfully!") return redirect("rca:rca_list") class RCAStatusChangeView(LoginRequiredMixin, View): """View to change RCA status""" def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) _check_rca_access(request, rca) form = RCAStatusChangeForm(request.POST) if form.is_valid(): old_status = rca.status new_status = form.cleaned_data["new_status"] notes = form.cleaned_data["notes"] rca.status = new_status # Set timestamps based on status if new_status == RCAStatus.IN_PROGRESS: rca.assigned_to = request.user rca.assigned_at = timezone.now() elif new_status == RCAStatus.APPROVED: rca.approved_by = request.user rca.approved_at = timezone.now() rca.save() # Create status log rca.status_logs.create(old_status=old_status, new_status=new_status, changed_by=request.user, notes=notes) messages.success(request, f"Status changed from {old_status} to {new_status}") else: messages.error(request, "Invalid status change") return redirect("rca:rca_detail", pk=rca.pk) class RCAApprovalView(LoginRequiredMixin, View): """View to approve RCA""" def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False, status=RCAStatus.REVIEW) _check_rca_access(request, rca) form = RCAApprovalForm(request.POST) if form.is_valid(): rca.status = RCAStatus.APPROVED rca.approved_by = request.user rca.approved_at = timezone.now() rca.approval_notes = form.cleaned_data["approval_notes"] rca.save() # Create status log rca.status_logs.create( old_status=RCAStatus.REVIEW, new_status=RCAStatus.APPROVED, changed_by=request.user, notes=rca.approval_notes, ) messages.success(request, "RCA approved successfully!") else: messages.error(request, "Invalid approval data") return redirect("rca:rca_detail", pk=rca.pk) class RCAClosureView(LoginRequiredMixin, View): """View to close RCA""" def post(self, request, pk): rca = get_object_or_404( RootCauseAnalysis, pk=pk, is_deleted=False, status__in=[RCAStatus.APPROVED, RCAStatus.IN_PROGRESS] ) _check_rca_access(request, rca) form = RCAClosureForm(request.POST) if form.is_valid(): rca.status = RCAStatus.CLOSED rca.closed_by = request.user rca.closed_at = timezone.now() rca.closure_notes = form.cleaned_data["closure_notes"] rca.actual_completion_date = form.cleaned_data["actual_completion_date"] rca.save() # Create status log rca.status_logs.create( old_status=RCAStatus.APPROVED, new_status=RCAStatus.CLOSED, changed_by=request.user, notes=rca.closure_notes, ) messages.success(request, "RCA closed successfully!") else: messages.error(request, "Invalid closure data") return redirect("rca:rca_detail", pk=rca.pk) class RCARootCauseCreateView(LoginRequiredMixin, View): """View to add a root cause to RCA""" def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) _check_rca_access(request, rca) form = RCARootCauseForm(request.POST) if form.is_valid(): root_cause = form.save(commit=False) root_cause.rca = rca root_cause.save() messages.success(request, "Root cause added successfully!") else: messages.error(request, "Invalid root cause data") return redirect("rca:rca_detail", pk=rca.pk) class RCARootCauseDeleteView(LoginRequiredMixin, View): """View to delete a root cause""" def post(self, request, pk, root_cause_pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) _check_rca_access(request, rca) root_cause = get_object_or_404(RCARootCause, pk=root_cause_pk, rca=rca) root_cause.delete() messages.success(request, "Root cause deleted successfully!") return redirect("rca:rca_detail", pk=rca.pk) class RCACorrectiveActionCreateView(LoginRequiredMixin, View): """View to add a corrective action to RCA""" def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) _check_rca_access(request, rca) form = RCACorrectiveActionForm(request.POST, rca=rca) if form.is_valid(): action = form.save(commit=False) action.rca = rca action.save() messages.success(request, "Corrective action added successfully!") else: messages.error(request, "Invalid corrective action data") return redirect("rca:rca_detail", pk=rca.pk) class RCACorrectiveActionDeleteView(LoginRequiredMixin, View): """View to delete a corrective action""" def post(self, request, pk, action_pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) _check_rca_access(request, rca) action = get_object_or_404(RCACorrectiveAction, pk=action_pk, rca=rca) action.delete() messages.success(request, "Corrective action deleted successfully!") return redirect("rca:rca_detail", pk=rca.pk) class RCAAttachmentCreateView(LoginRequiredMixin, View): """View to add an attachment to RCA""" def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) _check_rca_access(request, rca) form = RCAAttachmentForm(request.POST, request.FILES) if form.is_valid(): attachment = form.save(commit=False) attachment.rca = rca attachment.uploaded_by = request.user attachment.filename = request.FILES["file"].name attachment.file_type = request.FILES["file"].content_type attachment.file_size = request.FILES["file"].size attachment.save() messages.success(request, "Attachment added successfully!") else: messages.error(request, "Invalid attachment data") return redirect("rca:rca_detail", pk=rca.pk) class RCANoteCreateView(LoginRequiredMixin, View): """View to add a note to RCA""" def post(self, request, pk): rca = get_object_or_404(RootCauseAnalysis, pk=pk, is_deleted=False) _check_rca_access(request, rca) form = RCANoteForm(request.POST) if form.is_valid(): note = form.save(commit=False) note.rca = rca note.created_by = request.user note.save() messages.success(request, "Note added successfully!") else: messages.error(request, "Invalid note data") return redirect("rca:rca_detail", pk=rca.pk)