700 lines
22 KiB
Python
700 lines
22 KiB
Python
from rest_framework import viewsets, status
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
|
|
from django.http import JsonResponse
|
|
from django.db.models import Count, Q
|
|
from django.utils import timezone
|
|
from datetime import datetime
|
|
import json
|
|
|
|
from apps.standards.models import StandardSource, StandardCategory, Standard, StandardCompliance, StandardAttachment
|
|
from apps.organizations.models import Department
|
|
from apps.standards.forms import (
|
|
StandardSourceForm,
|
|
StandardCategoryForm,
|
|
StandardForm,
|
|
StandardComplianceForm,
|
|
StandardAttachmentForm,
|
|
)
|
|
|
|
|
|
# ==================== API ViewSets ====================
|
|
|
|
|
|
class StandardSourceViewSet(viewsets.ModelViewSet):
|
|
queryset = StandardSource.objects.all()
|
|
filterset_fields = ["is_active"]
|
|
search_fields = ["name", "name_ar", "code"]
|
|
ordering = ["name"]
|
|
|
|
def get_serializer_class(self):
|
|
from apps.standards.serializers import StandardSourceSerializer
|
|
|
|
return StandardSourceSerializer
|
|
|
|
|
|
class StandardCategoryViewSet(viewsets.ModelViewSet):
|
|
queryset = StandardCategory.objects.all()
|
|
filterset_fields = ["is_active"]
|
|
search_fields = ["name", "name_ar"]
|
|
ordering = ["order", "name"]
|
|
|
|
def get_serializer_class(self):
|
|
from apps.standards.serializers import StandardCategorySerializer
|
|
|
|
return StandardCategorySerializer
|
|
|
|
|
|
class StandardViewSet(viewsets.ModelViewSet):
|
|
queryset = Standard.objects.all()
|
|
filterset_fields = ["source", "category", "department", "is_active"]
|
|
search_fields = ["code", "title", "title_ar", "description"]
|
|
ordering = ["source", "category", "code"]
|
|
|
|
def get_serializer_class(self):
|
|
from apps.standards.serializers import StandardSerializer
|
|
|
|
return StandardSerializer
|
|
|
|
|
|
class StandardComplianceViewSet(viewsets.ModelViewSet):
|
|
queryset = StandardCompliance.objects.all()
|
|
filterset_fields = ["department", "standard", "status"]
|
|
search_fields = ["department__name", "standard__code", "notes"]
|
|
ordering = ["-created_at"]
|
|
|
|
def get_serializer_class(self):
|
|
from apps.standards.serializers import StandardComplianceSerializer
|
|
|
|
return StandardComplianceSerializer
|
|
|
|
|
|
class StandardAttachmentViewSet(viewsets.ModelViewSet):
|
|
queryset = StandardAttachment.objects.all()
|
|
filterset_fields = ["compliance"]
|
|
search_fields = ["filename", "description"]
|
|
ordering = ["-uploaded_at"]
|
|
|
|
def get_serializer_class(self):
|
|
from apps.standards.serializers import StandardAttachmentSerializer
|
|
|
|
return StandardAttachmentSerializer
|
|
|
|
|
|
# ==================== UI Views ====================
|
|
|
|
|
|
@login_required
|
|
def standards_dashboard(request):
|
|
"""Standards dashboard with statistics"""
|
|
# Get current hospital from tenant_hospital (set by middleware)
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
if not hospital:
|
|
return render(request, "core/no_hospital_assigned.html")
|
|
|
|
departments = hospital.departments.filter(status="active")
|
|
|
|
# Get compliance statistics
|
|
compliance_records = StandardCompliance.objects.filter(department__hospital=hospital)
|
|
|
|
stats = {
|
|
"total_standards": Standard.objects.filter(is_active=True).count(),
|
|
"total_departments": departments.count(),
|
|
"met": compliance_records.filter(status="met").count(),
|
|
"partially_met": compliance_records.filter(status="partially_met").count(),
|
|
"not_met": compliance_records.filter(status="not_met").count(),
|
|
"not_assessed": compliance_records.filter(status="not_assessed").count(),
|
|
}
|
|
|
|
# Recent compliance updates
|
|
recent_updates = compliance_records.order_by("-updated_at")[:10]
|
|
|
|
# Check if user is PX admin or Hospital Admin
|
|
is_px_admin = request.user.is_px_admin() or request.user.is_hospital_admin()
|
|
|
|
context = {
|
|
"hospital": hospital,
|
|
"departments": departments,
|
|
"stats": stats,
|
|
"recent_updates": recent_updates,
|
|
"is_px_admin": is_px_admin,
|
|
}
|
|
|
|
return render(request, "standards/dashboard.html", context)
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def department_standards_view(request, pk):
|
|
"""View all standards for a department"""
|
|
department = get_object_or_404(Department, pk=pk)
|
|
|
|
# Check if user is PX admin
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
is_px_admin = (request.user.is_px_admin() or request.user.is_hospital_admin()) if hospital else False
|
|
|
|
# Get all active standards (both department-specific and general)
|
|
department_standards = (
|
|
Standard.objects.filter(is_active=True)
|
|
.filter(Q(department=department) | Q(department__isnull=True))
|
|
.order_by("source", "category", "code")
|
|
)
|
|
|
|
# Get compliance status for each standard
|
|
standards_data = []
|
|
for standard in department_standards:
|
|
compliance = StandardCompliance.objects.filter(department=department, standard=standard).first()
|
|
|
|
standards_data.append(
|
|
{
|
|
"standard": standard,
|
|
"compliance": compliance,
|
|
"attachment_count": compliance.attachments.count() if compliance else 0,
|
|
}
|
|
)
|
|
|
|
context = {
|
|
"department": department,
|
|
"standards_data": standards_data,
|
|
"is_px_admin": is_px_admin,
|
|
}
|
|
|
|
return render(request, "standards/department_standards.html", context)
|
|
|
|
|
|
@login_required
|
|
def standard_detail(request, pk):
|
|
"""View standard details and compliance history"""
|
|
standard = get_object_or_404(Standard, pk=pk)
|
|
|
|
# Get compliance records for all departments
|
|
compliance_records = (
|
|
StandardCompliance.objects.filter(standard=standard)
|
|
.select_related("department", "assessor")
|
|
.order_by("-created_at")
|
|
)
|
|
|
|
# Check if user is PX admin
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
is_px_admin = False
|
|
if hospital:
|
|
is_px_admin = request.user.is_px_admin() or request.user.is_hospital_admin()
|
|
|
|
context = {
|
|
"standard": standard,
|
|
"compliance_records": compliance_records,
|
|
"is_px_admin": is_px_admin,
|
|
}
|
|
|
|
return render(request, "standards/standard_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
def standard_compliance_update(request, compliance_id):
|
|
"""Update compliance status"""
|
|
compliance = get_object_or_404(StandardCompliance, pk=compliance_id)
|
|
|
|
if request.method == "POST":
|
|
form = StandardComplianceForm(request.POST, instance=compliance)
|
|
if form.is_valid():
|
|
form.save()
|
|
return redirect("standards:department_standards", pk=compliance.department.pk)
|
|
else:
|
|
form = StandardComplianceForm(instance=compliance)
|
|
|
|
context = {
|
|
"compliance": compliance,
|
|
"form": form,
|
|
}
|
|
|
|
return render(request, "standards/compliance_form.html", context)
|
|
|
|
|
|
@login_required
|
|
def standard_attachment_upload(request, compliance_id):
|
|
"""Upload attachment for compliance"""
|
|
compliance = get_object_or_404(StandardCompliance, pk=compliance_id)
|
|
|
|
if request.method == "POST":
|
|
form = StandardAttachmentForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
attachment = form.save(commit=False)
|
|
attachment.compliance = compliance
|
|
attachment.uploaded_by = request.user
|
|
attachment.filename = request.FILES["file"].name
|
|
attachment.save()
|
|
return redirect("standards:standard_compliance_update", compliance_id=compliance.pk)
|
|
else:
|
|
form = StandardAttachmentForm()
|
|
|
|
context = {
|
|
"compliance": compliance,
|
|
"form": form,
|
|
}
|
|
|
|
return render(request, "standards/attachment_upload.html", context)
|
|
|
|
|
|
@login_required
|
|
def standard_attachment_delete(request, pk):
|
|
"""Delete an attachment"""
|
|
attachment = get_object_or_404(StandardAttachment, pk=pk)
|
|
compliance_id = attachment.compliance.id
|
|
|
|
if request.method == "POST":
|
|
attachment.delete()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Attachment deleted successfully.")
|
|
return redirect("standards:standard_compliance_update", compliance_id=compliance_id)
|
|
|
|
context = {"attachment": attachment}
|
|
return render(request, "standards/attachment_confirm_delete.html", context)
|
|
|
|
|
|
@login_required
|
|
def standards_search(request):
|
|
"""Search standards"""
|
|
query = request.GET.get("q", "")
|
|
source_filter = request.GET.get("source", "")
|
|
category_filter = request.GET.get("category", "")
|
|
status_filter = request.GET.get("status", "")
|
|
|
|
# Get current hospital from tenant_hospital (set by middleware)
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
if not hospital:
|
|
return render(request, "core/no_hospital_assigned.html")
|
|
|
|
# Build queryset
|
|
standards = Standard.objects.filter(is_active=True)
|
|
|
|
if query:
|
|
standards = standards.filter(
|
|
Q(code__icontains=query)
|
|
| Q(title__icontains=query)
|
|
| Q(title_ar__icontains=query)
|
|
| Q(description__icontains=query)
|
|
)
|
|
|
|
if source_filter:
|
|
standards = standards.filter(source_id=source_filter)
|
|
|
|
if category_filter:
|
|
standards = standards.filter(category_id=category_filter)
|
|
|
|
standards = standards.select_related("source", "category").order_by("source", "category", "code")
|
|
|
|
# Get filters
|
|
sources = StandardSource.objects.filter(is_active=True)
|
|
categories = StandardCategory.objects.filter(is_active=True)
|
|
|
|
# Check if user is PX admin or Hospital Admin
|
|
is_px_admin = request.user.is_px_admin() or request.user.is_hospital_admin()
|
|
|
|
context = {
|
|
"hospital": hospital,
|
|
"standards": standards,
|
|
"query": query,
|
|
"source_filter": source_filter,
|
|
"category_filter": category_filter,
|
|
"sources": sources,
|
|
"categories": categories,
|
|
"is_px_admin": is_px_admin,
|
|
}
|
|
|
|
return render(request, "standards/search.html", context)
|
|
|
|
|
|
@login_required
|
|
def standard_create(request, department_id=None):
|
|
"""Create a new standard (PX Admin only)"""
|
|
# Check if user is PX admin
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
if not hospital:
|
|
return render(request, "core/no_hospital_assigned.html")
|
|
|
|
is_px_admin = request.user.is_px_admin() or request.user.is_hospital_admin()
|
|
|
|
if not is_px_admin:
|
|
from django.contrib import messages
|
|
|
|
messages.error(request, "You do not have permission to create standards.")
|
|
if department_id:
|
|
return redirect("standards:department_standards", pk=department_id)
|
|
return redirect("standards:dashboard")
|
|
|
|
if request.method == "POST":
|
|
form = StandardForm(request.POST)
|
|
if form.is_valid():
|
|
standard = form.save(commit=False)
|
|
standard.save()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Standard created successfully.")
|
|
if department_id:
|
|
return redirect("standards:department_standards", pk=department_id)
|
|
return redirect("standards:dashboard")
|
|
else:
|
|
form = StandardForm()
|
|
# If department_id is provided, pre-select that department
|
|
if department_id:
|
|
from apps.organizations.models import Department
|
|
|
|
department = Department.objects.filter(pk=department_id).first()
|
|
if department:
|
|
form.fields["department"].initial = department
|
|
|
|
# Get all departments for the hospital
|
|
departments = hospital.departments.filter(status="active")
|
|
|
|
context = {
|
|
"form": form,
|
|
"department_id": department_id,
|
|
"departments": departments,
|
|
"hospital": hospital,
|
|
}
|
|
|
|
return render(request, "standards/standard_form.html", context)
|
|
|
|
|
|
@login_required
|
|
def standard_update(request, pk):
|
|
"""Update a standard (PX Admin only)"""
|
|
standard = get_object_or_404(Standard, pk=pk)
|
|
|
|
# Check if user is PX admin
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
if not hospital:
|
|
return render(request, "core/no_hospital_assigned.html")
|
|
|
|
is_px_admin = request.user.is_px_admin() or request.user.is_hospital_admin()
|
|
|
|
if not is_px_admin:
|
|
from django.contrib import messages
|
|
|
|
messages.error(request, "You do not have permission to update standards.")
|
|
return redirect("standards:standard_detail", pk=pk)
|
|
|
|
if request.method == "POST":
|
|
form = StandardForm(request.POST, instance=standard)
|
|
if form.is_valid():
|
|
form.save()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Standard updated successfully.")
|
|
return redirect("standards:standard_detail", pk=pk)
|
|
else:
|
|
form = StandardForm(instance=standard)
|
|
|
|
context = {
|
|
"form": form,
|
|
"standard": standard,
|
|
"hospital": hospital,
|
|
}
|
|
|
|
return render(request, "standards/standard_form.html", context)
|
|
|
|
|
|
@login_required
|
|
def standard_delete(request, pk):
|
|
"""Delete a standard (PX Admin only)"""
|
|
standard = get_object_or_404(Standard, pk=pk)
|
|
|
|
# Check if user is PX admin
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
if not hospital:
|
|
return render(request, "core/no_hospital_assigned.html")
|
|
|
|
is_px_admin = request.user.is_px_admin() or request.user.is_hospital_admin()
|
|
|
|
if not is_px_admin:
|
|
from django.contrib import messages
|
|
|
|
messages.error(request, "You do not have permission to delete standards.")
|
|
return redirect("standards:standard_detail", pk=pk)
|
|
|
|
if request.method == "POST":
|
|
standard.delete()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Standard deleted successfully.")
|
|
return redirect("standards:dashboard")
|
|
|
|
context = {"standard": standard}
|
|
return render(request, "standards/standard_confirm_delete.html", context)
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def create_compliance_ajax(request):
|
|
"""Create compliance record via AJAX"""
|
|
if not request.user.is_authenticated:
|
|
return JsonResponse({"success": False, "error": "Authentication required"}, status=401)
|
|
|
|
if request.method != "POST":
|
|
return JsonResponse({"success": False, "error": "Invalid request method"})
|
|
|
|
# Parse JSON from request body
|
|
try:
|
|
data = json.loads(request.body)
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({"success": False, "error": "Invalid JSON"})
|
|
|
|
department_id = data.get("department_id")
|
|
standard_id = data.get("standard_id")
|
|
|
|
if not department_id or not standard_id:
|
|
return JsonResponse({"success": False, "error": "Missing required fields"})
|
|
|
|
try:
|
|
department = Department.objects.get(pk=department_id)
|
|
standard = Standard.objects.get(pk=standard_id)
|
|
|
|
# Check if compliance already exists
|
|
compliance, created = StandardCompliance.objects.get_or_create(
|
|
department=department,
|
|
standard=standard,
|
|
defaults={
|
|
"assessor": request.user,
|
|
"last_assessed_date": timezone.now().date(),
|
|
},
|
|
)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"success": True,
|
|
"compliance_id": compliance.id,
|
|
"status": compliance.status,
|
|
"created": created,
|
|
}
|
|
)
|
|
except Exception as e:
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
return JsonResponse({"success": False, "error": str(e)})
|
|
|
|
|
|
@ensure_csrf_cookie
|
|
@login_required
|
|
def update_compliance_ajax(request):
|
|
"""Update compliance record via AJAX"""
|
|
if not request.user.is_authenticated:
|
|
return JsonResponse({"success": False, "error": "Authentication required"}, status=401)
|
|
|
|
if request.method != "POST":
|
|
return JsonResponse({"success": False, "error": "Invalid request method"})
|
|
|
|
# Parse JSON from request body
|
|
try:
|
|
data = json.loads(request.body)
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({"success": False, "error": "Invalid JSON"})
|
|
|
|
compliance_id = data.get("compliance_id")
|
|
status = data.get("status")
|
|
notes = data.get("notes", "")
|
|
evidence_summary = data.get("evidence_summary", "")
|
|
last_assessed_date_str = data.get("last_assessed_date")
|
|
assessor_id = data.get("assessor_id")
|
|
|
|
if not compliance_id or not status:
|
|
return JsonResponse({"success": False, "error": "Missing required fields"})
|
|
|
|
try:
|
|
compliance = StandardCompliance.objects.get(pk=compliance_id)
|
|
compliance.status = status
|
|
compliance.notes = notes
|
|
compliance.evidence_summary = evidence_summary
|
|
|
|
# Set assessor - use logged-in user or provided ID
|
|
if assessor_id:
|
|
from apps.accounts.models import User
|
|
|
|
try:
|
|
assessor = User.objects.get(pk=assessor_id)
|
|
compliance.assessor = assessor
|
|
except User.DoesNotExist:
|
|
compliance.assessor = request.user
|
|
else:
|
|
compliance.assessor = request.user
|
|
|
|
# Set assessment date
|
|
if last_assessed_date_str:
|
|
compliance.last_assessed_date = datetime.strptime(last_assessed_date_str, "%Y-%m-%d").date()
|
|
else:
|
|
compliance.last_assessed_date = timezone.now().date()
|
|
|
|
compliance.save()
|
|
|
|
return JsonResponse(
|
|
{
|
|
"success": True,
|
|
"status": compliance.status,
|
|
"status_display": compliance.get_status_display(),
|
|
}
|
|
)
|
|
except Exception as e:
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
return JsonResponse({"success": False, "error": str(e)})
|
|
|
|
|
|
@login_required
|
|
def get_compliance_status(request, department_id, standard_id):
|
|
"""API endpoint to get compliance status"""
|
|
compliance = StandardCompliance.objects.filter(department_id=department_id, standard_id=standard_id).first()
|
|
|
|
if compliance:
|
|
data = {
|
|
"status": compliance.status,
|
|
"last_assessed_date": compliance.last_assessed_date,
|
|
"assessor": compliance.assessor.get_full_name() if compliance.assessor else None,
|
|
"notes": compliance.notes,
|
|
"attachment_count": compliance.attachments.count(),
|
|
}
|
|
else:
|
|
data = {
|
|
"status": "not_assessed",
|
|
"last_assessed_date": None,
|
|
"assessor": None,
|
|
"notes": "",
|
|
"attachment_count": 0,
|
|
}
|
|
|
|
return JsonResponse(data)
|
|
|
|
|
|
# ==================== Source Management Views ====================
|
|
|
|
|
|
@login_required
|
|
def source_list(request):
|
|
"""List all standard sources"""
|
|
sources = StandardSource.objects.all().order_by("name")
|
|
context = {"sources": sources}
|
|
return render(request, "standards/source_list.html", context)
|
|
|
|
|
|
@login_required
|
|
def source_create(request):
|
|
"""Create a new standard source"""
|
|
if request.method == "POST":
|
|
form = StandardSourceForm(request.POST)
|
|
if form.is_valid():
|
|
form.save()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Source created successfully.")
|
|
return redirect("standards:source_list")
|
|
else:
|
|
form = StandardSourceForm()
|
|
|
|
context = {"form": form}
|
|
return render(request, "standards/source_form.html", context)
|
|
|
|
|
|
@login_required
|
|
def source_update(request, pk):
|
|
"""Update a standard source"""
|
|
source = get_object_or_404(StandardSource, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
form = StandardSourceForm(request.POST, instance=source)
|
|
if form.is_valid():
|
|
form.save()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Source updated successfully.")
|
|
return redirect("standards:source_list")
|
|
else:
|
|
form = StandardSourceForm(instance=source)
|
|
|
|
context = {"form": form, "source": source}
|
|
return render(request, "standards/source_form.html", context)
|
|
|
|
|
|
@login_required
|
|
def source_delete(request, pk):
|
|
"""Delete a standard source"""
|
|
source = get_object_or_404(StandardSource, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
source.delete()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Source deleted successfully.")
|
|
return redirect("standards:source_list")
|
|
|
|
context = {"source": source}
|
|
return render(request, "standards/source_confirm_delete.html", context)
|
|
|
|
|
|
# ==================== Category Management Views ====================
|
|
|
|
|
|
@login_required
|
|
def category_list(request):
|
|
"""List all standard categories"""
|
|
categories = StandardCategory.objects.all().order_by("order", "name")
|
|
context = {"categories": categories}
|
|
return render(request, "standards/category_list.html", context)
|
|
|
|
|
|
@login_required
|
|
def category_create(request):
|
|
"""Create a new standard category"""
|
|
if request.method == "POST":
|
|
form = StandardCategoryForm(request.POST)
|
|
if form.is_valid():
|
|
form.save()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Category created successfully.")
|
|
return redirect("standards:category_list")
|
|
else:
|
|
form = StandardCategoryForm()
|
|
|
|
context = {"form": form}
|
|
return render(request, "standards/category_form.html", context)
|
|
|
|
|
|
@login_required
|
|
def category_update(request, pk):
|
|
"""Update a standard category"""
|
|
category = get_object_or_404(StandardCategory, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
form = StandardCategoryForm(request.POST, instance=category)
|
|
if form.is_valid():
|
|
form.save()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Category updated successfully.")
|
|
return redirect("standards:category_list")
|
|
else:
|
|
form = StandardCategoryForm(instance=category)
|
|
|
|
context = {"form": form, "category": category}
|
|
return render(request, "standards/category_form.html", context)
|
|
|
|
|
|
@login_required
|
|
def category_delete(request, pk):
|
|
"""Delete a standard category"""
|
|
category = get_object_or_404(StandardCategory, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
category.delete()
|
|
from django.contrib import messages
|
|
|
|
messages.success(request, "Category deleted successfully.")
|
|
return redirect("standards:category_list")
|
|
|
|
context = {"category": category}
|
|
return render(request, "standards/category_confirm_delete.html", context)
|