HH/apps/core/views.py
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

671 lines
24 KiB
Python

"""
Core views - Health check and utility views
"""
from django.contrib.auth.decorators import login_required
from django.db import connection
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.views.decorators.cache import never_cache
from django.views.decorators.http import require_GET, require_POST
@never_cache
@require_GET
def health_check(request):
"""
Health check endpoint for monitoring and load balancers.
Returns JSON with status of various services.
"""
health_status = {"status": "ok", "services": {}}
# Check database connection
try:
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
health_status["services"]["database"] = "ok"
except Exception as e:
health_status["status"] = "error"
health_status["services"]["database"] = f"error: {str(e)}"
# Check Redis/Celery (optional - don't fail if not available)
try:
from django_celery_beat.models import PeriodicTask
PeriodicTask.objects.count()
health_status["services"]["celery_beat"] = "ok"
except Exception:
health_status["services"]["celery_beat"] = "not_configured"
# Return appropriate status code
status_code = 200 if health_status["status"] == "ok" else 503
return JsonResponse(health_status, status=status_code)
@login_required
def select_hospital(request):
"""
Hospital selection page for PX Admins.
Allows PX Admins to switch between hospitals.
Stores selected hospital in session.
"""
# Only PX Admins should access this page
if not request.user.is_px_admin():
return redirect("dashboard:dashboard")
from apps.organizations.models import Hospital
hospitals = Hospital.objects.all().order_by("name")
# Handle hospital selection
if request.method == "POST":
hospital_id = request.POST.get("hospital_id")
if hospital_id:
try:
hospital = Hospital.objects.get(id=hospital_id)
request.session["selected_hospital_id"] = str(hospital.id)
# Redirect to referring page or dashboard
next_url = request.POST.get("next", request.GET.get("next", "/"))
return redirect(next_url)
except Hospital.DoesNotExist:
pass
context = {
"hospitals": hospitals,
"selected_hospital_id": request.session.get("selected_hospital_id"),
"next": request.GET.get("next", "/"),
}
return render(request, "core/select_hospital.html", context)
@login_required
@require_POST
def switch_hospital(request):
"""
AJAX endpoint to switch hospitals for PX Admins.
Stores selected hospital in session and returns JSON response.
"""
# Only PX Admins can switch hospitals
if not request.user.is_px_admin():
return JsonResponse(
{"success": False, "error": "Permission denied. Only PX Admins can switch hospitals."}, status=403
)
from apps.organizations.models import Hospital
hospital_id = request.POST.get("hospital_id")
if not hospital_id:
return JsonResponse({"success": False, "error": "Hospital ID is required"}, status=400)
try:
hospital = Hospital.objects.get(id=hospital_id)
request.session["selected_hospital_id"] = str(hospital.id)
return JsonResponse(
{
"success": True,
"hospital": {
"id": str(hospital.id),
"name": hospital.name,
"display_name": hospital.get_display_name(),
"display_name_ar": hospital.get_display_name_ar(),
"code": hospital.code,
},
}
)
except Hospital.DoesNotExist:
return JsonResponse({"success": False, "error": "Hospital not found"}, status=404)
@login_required
def no_hospital_assigned(request):
"""
Error page for users without a hospital assigned.
Users without a hospital assignment cannot access the system.
"""
return render(request, "core/no_hospital_assigned.html", status=403)
# ============================================================================
# PUBLIC SUBMISSION VIEWS
# ============================================================================
def public_submit_landing(request):
"""
Landing page for public submissions.
Allows users to choose between Complaint, Observation, or Inquiry.
No authentication required.
"""
from apps.organizations.models import Hospital
if request.method == "POST":
# Return 405 Method Not Allowed with proper JSON response
from django.http import JsonResponse
return JsonResponse(
{"success": False, "error": "Method not allowed. Please use GET to access the landing page."}, status=405
)
hospitals = Hospital.objects.all().order_by("name")
context = {
"hospitals": hospitals,
}
return render(request, "core/public_submit.html", context)
@require_POST
def public_inquiry_submit(request):
"""
Handle public inquiry submissions.
Creates an inquiry from public submission.
Returns JSON response with reference number.
"""
from apps.complaints.models import Inquiry
from apps.organizations.models import Hospital, Location, MainSection, SubSection
import uuid
# Get form data
name = request.POST.get("name", "").strip()
email = request.POST.get("email", "").strip()
phone = request.POST.get("phone", "").strip()
hospital_id = request.POST.get("hospital")
category = request.POST.get("category", "").strip()
subject = request.POST.get("subject", "").strip()
message = request.POST.get("message", "").strip()
location_id = request.POST.get("location", "").strip()
main_section_id = request.POST.get("main_section", "").strip()
subsection_id = request.POST.get("subsection", "").strip()
# Validation
errors = []
if not name:
errors.append("Name is required")
if not phone:
errors.append("Phone number is required")
if not hospital_id:
errors.append("Hospital selection is required")
if not message:
errors.append("Message is required")
if errors:
return JsonResponse({"success": False, "errors": errors}, status=400)
try:
# Validate hospital
hospital = Hospital.objects.get(id=hospital_id)
location = Location.objects.filter(id=location_id).first() if location_id else None
main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None
subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None
# Create inquiry (using correct field names from model)
inquiry = Inquiry.objects.create(
hospital=hospital,
contact_name=name,
contact_email=email,
contact_phone=phone,
subject=subject,
message=message,
category=category,
status="open",
location=location,
main_section=main_section,
subsection=subsection,
)
reference_number = f"INQ-{str(inquiry.id)[:8].upper()}"
inquiry.reference_number = reference_number
inquiry.save(update_fields=["reference_number"])
try:
from apps.complaints.tasks import analyze_inquiry_with_ai
analyze_inquiry_with_ai.delay(str(inquiry.id))
except Exception:
pass
try:
from apps.core.services import AuditService
AuditService.log_event(
event_type="inquiry_created_public",
description=f"Public inquiry submitted: {subject}",
content_object=inquiry,
metadata={"reference": reference_number, "source": "public_form"},
)
except Exception:
pass
# Send notification email (optional)
try:
from django.core.mail import send_mail
from django.conf import settings
from django.template.loader import render_to_string
subject = f"New Public Inquiry - {reference_number}"
html_message = render_to_string(
"emails/public_inquiry_notification.html",
{
"name": name,
"subject": subject,
"message": message,
"reference_number": reference_number,
},
)
plain_message = f"Inquiry from {name}\n\nSubject: {subject}\n\nMessage:\n{message}"
send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[settings.DEFAULT_FROM_EMAIL],
fail_silently=True,
html_message=html_message,
)
except Exception:
pass # Don't fail if email doesn't send
return JsonResponse({"success": True, "reference_number": reference_number, "inquiry_id": str(inquiry.id)})
except Hospital.DoesNotExist:
return JsonResponse({"success": False, "errors": ["Invalid hospital selected"]}, status=400)
except Exception as e:
return JsonResponse({"success": False, "errors": [str(e)]}, status=500)
@require_GET
def api_hospitals(request):
"""
API endpoint to get hospitals list.
Used by public submission forms to populate hospital dropdown.
"""
from apps.organizations.models import Hospital
hospitals = Hospital.objects.all().order_by("name").values("id", "name")
return JsonResponse({"success": True, "hospitals": list(hospitals)})
@require_GET
def set_language(request):
"""
Set's language preference for the session and cookie.
Stores the selected language in session and sets a persistent cookie
for better reliability across page reloads and incognito sessions.
"""
from django.conf import settings
from django.utils import translation
from urllib.parse import urlparse
language = request.GET.get("language", "en")
# Validate language code
if language not in dict(settings.LANGUAGES):
language = "en"
# Activate and store the language in session
translation.activate(language)
request.session["django_language"] = language
# Explicitly save the session to ensure it persists
if hasattr(request, "session") and request.session.modified:
request.session.save()
# Get the referring URL or use a default
next_url = request.META.get("HTTP_REFERER", "/")
parsed_url = urlparse(next_url)
# Keep the path but remove query parameters if needed
redirect_url = parsed_url.path if parsed_url.path else "/"
# If there's no referer, redirect to home or public submit landing
if next_url == "/" or not next_url:
redirect_url = "/"
# Create response with redirect
response = redirect(redirect_url)
# Also set a persistent cookie as a fallback mechanism
# This ensures language persists even if session storage fails
response.set_cookie(
"django_language",
language,
max_age=365 * 24 * 60 * 60, # 1 year
httponly=False, # Allow JavaScript to read it if needed
secure=False, # Allow over HTTP for local development
samesite="Lax", # Standard SameSite policy
)
return response
@require_GET
def api_observation_categories(request):
"""
API endpoint to get observation categories list.
Used by public observation form to populate category dropdown.
"""
from apps.observations.models import ObservationCategory
categories = (
ObservationCategory.objects.filter(is_active=True)
.order_by("sort_order", "name_en")
.values("id", "name_en", "name_ar")
)
return JsonResponse({"success": True, "categories": list(categories)})
def public_track(request):
"""
Unified public tracking page.
Allows users to track complaints, inquiries, and observations
from a single page. Shows 3 selection cards, then inline results.
"""
return render(request, "core/public_track.html", {})
@require_GET
def public_track_api(request):
"""
API endpoint for unified tracking.
Accepts type (complaint/inquiry/observation) and reference parameters.
Returns standardized JSON with tracking information.
"""
from django.utils.translation import gettext as _
track_type = request.GET.get("type", "").strip().lower()
reference = request.GET.get("reference", "").strip()
if not track_type or not reference:
return JsonResponse({"found": False, "error": str(_("Type and reference are required."))}, status=400)
if track_type == "complaint":
return _track_complaint(reference)
elif track_type == "inquiry":
return _track_inquiry(reference)
elif track_type == "observation":
return _track_observation(reference)
else:
return JsonResponse({"found": False, "error": str(_("Invalid tracking type."))}, status=400)
def _track_complaint(reference):
from apps.complaints.models import Complaint
try:
complaint = (
Complaint.objects.select_related("hospital", "department", "location")
.prefetch_related("updates")
.get(reference_number__iexact=reference)
)
complaint.check_overdue()
except Complaint.DoesNotExist:
return JsonResponse({"found": False, "error": "Complaint not found"})
ps = complaint.public_status
public_updates = list(
complaint.updates.filter(update_type__in=["status_change", "resolution", "communication"])
.order_by("-created_at")[:20]
)
timeline = []
for u in public_updates:
icon = "refresh-cw" if u.update_type == "status_change" else ("check-circle-2" if u.update_type == "resolution" else "message-square")
title = "Status Updated" if u.update_type == "status_change" else ("Final Resolution" if u.update_type == "resolution" else "Update Received")
timeline.append({
"type": u.update_type,
"icon": icon,
"title": title,
"comment": u.comments or "",
"created_at": u.created_at.strftime("%Y-%m-%d %H:%M"),
})
info_cards = [
{"icon": "calendar", "label": "Submitted", "value": complaint.created_at.strftime("%b %d, %Y")},
{"icon": "building", "label": "Department", "value": complaint.department.name if complaint.department else "General"},
]
if complaint.due_at:
info_cards.append({
"icon": "clock",
"label": "SLA Deadline",
"value": complaint.due_at.strftime("%b %d, %H:%M"),
"alert": complaint.is_overdue,
})
else:
info_cards.append({"icon": "tag", "label": "Category", "value": complaint.get_category_display() if hasattr(complaint, 'get_category_display') else "General"})
return JsonResponse({
"found": True,
"type": "complaint",
"reference": complaint.reference_number,
"status": ps["slug"],
"status_display": ps["label"],
"progress": ps["progress"],
"status_color": ps["css"],
"escalated": bool(complaint.escalated_at),
"info_cards": info_cards,
"timeline": timeline,
})
def _track_inquiry(reference):
from apps.complaints.models import Inquiry, InquiryUpdate
inquiry = Inquiry.objects.filter(reference_number__iexact=reference).select_related("hospital", "department").first()
if not inquiry:
return JsonResponse({"found": False, "error": "Inquiry not found"})
status_map = {
"open": {"label": "Received", "progress": 15, "css": "amber"},
"in_progress": {"label": "In Progress", "progress": 50, "css": "blue"},
"contacted": {"label": "In Progress", "progress": 50, "css": "blue"},
"contacted_no_response": {"label": "In Progress", "progress": 50, "css": "blue"},
"resolved": {"label": "Resolved", "progress": 100, "css": "emerald"},
"closed": {"label": "Closed", "progress": 100, "css": "slate"},
}
sm = status_map.get(inquiry.status, {"label": inquiry.get_status_display(), "progress": 15, "css": "amber"})
updates = InquiryUpdate.objects.filter(inquiry=inquiry).select_related("created_by").order_by("-created_at")[:20]
timeline = []
for u in updates:
icon = "refresh-cw" if u.update_type == "status_change" else ("check-circle-2" if u.update_type == "response" else "message-square")
title = "Status Updated" if u.update_type == "status_change" else ("Response Received" if u.update_type == "response" else "Update Received")
timeline.append({
"type": u.update_type,
"icon": icon,
"title": title,
"comment": u.message or "",
"created_at": u.created_at.strftime("%Y-%m-%d %H:%M"),
})
info_cards = [
{"icon": "calendar", "label": "Submitted", "value": inquiry.created_at.strftime("%b %d, %Y")},
{"icon": "building", "label": "Department", "value": inquiry.department.name if inquiry.department else "General"},
]
if inquiry.due_at:
info_cards.append({
"icon": "clock",
"label": "SLA Deadline",
"value": inquiry.due_at.strftime("%b %d, %H:%M"),
"alert": inquiry.is_overdue,
})
else:
info_cards.append({"icon": "help-circle", "label": "Subject", "value": inquiry.subject[:40] if inquiry.subject else ""})
return JsonResponse({
"found": True,
"type": "inquiry",
"reference": inquiry.reference_number,
"status": inquiry.status,
"status_display": sm["label"],
"progress": sm["progress"],
"status_color": sm["css"],
"escalated": bool(inquiry.escalated_at),
"info_cards": info_cards,
"timeline": timeline,
})
def _track_observation(reference):
from apps.observations.models import Observation
try:
observation = Observation.objects.select_related("hospital", "category").get(tracking_code__iexact=reference)
except Observation.DoesNotExist:
return JsonResponse({"found": False, "error": "Observation not found"})
status_progress = {
"new": 15, "triaged": 30, "assigned": 40, "in_progress": 50,
"resolved": 100, "closed": 100, "rejected": 0, "duplicate": 0,
}
status_css = {
"new": "sky", "triaged": "teal", "assigned": "teal", "in_progress": "blue",
"resolved": "emerald", "closed": "slate", "rejected": "rose", "duplicate": "slate",
}
timeline = []
if hasattr(observation, 'public_timeline') and callable(observation.public_timeline):
for item in observation.public_timeline:
icon = "refresh-cw" if item.get("type") == "status_change" else ("message-square" if item.get("type") == "note" else "check-circle-2")
timeline.append({
"type": item.get("type", "note"),
"icon": icon,
"title": "Status Updated" if item.get("type") == "status_change" else ("Update Received" if item.get("type") == "note" else "Final Resolution"),
"comment": item.get("comment", ""),
"created_at": item.get("created_at", ""),
})
info_cards = [
{"icon": "calendar", "label": "Submitted", "value": observation.created_at.strftime("%b %d, %Y")},
{"icon": "tag", "label": "Category", "value": observation.category.name if observation.category else "Not specified"},
{"icon": "activity", "label": "Severity", "value": observation.get_severity_display(), "severity": observation.severity},
]
return JsonResponse({
"found": True,
"type": "observation",
"reference": observation.tracking_code,
"status": observation.status,
"status_display": observation.get_status_display(),
"progress": status_progress.get(observation.status, 15),
"status_color": status_css.get(observation.status, "slate"),
"escalated": False,
"info_cards": info_cards,
"timeline": timeline,
})
@require_POST
def public_observation_submit(request):
"""
Handle public observation submissions.
Creates an observation from public submission.
Returns JSON response with tracking code.
"""
from apps.observations.models import Observation, ObservationAttachment, ObservationCategory
from django.shortcuts import get_object_or_404
from apps.observations.services import ObservationService
from apps.organizations.models import Hospital, Location, MainSection, SubSection
import mimetypes
# Get form data
hospital_id = request.POST.get("hospital", "").strip()
category_id = request.POST.get("category")
severity = request.POST.get("severity", "medium")
title = request.POST.get("title", "").strip()
description = request.POST.get("description", "").strip()
location_text = request.POST.get("location_text", "").strip()
location_id = request.POST.get("location", "").strip()
main_section_id = request.POST.get("main_section", "").strip()
subsection_id = request.POST.get("subsection", "").strip()
incident_datetime = request.POST.get("incident_datetime", "")
reporter_staff_id = request.POST.get("reporter_staff_id", "").strip()
reporter_name = request.POST.get("reporter_name", "").strip()
reporter_phone = request.POST.get("reporter_phone", "").strip()
reporter_email = request.POST.get("reporter_email", "").strip()
# Validation
errors = []
if not description:
errors.append("Description is required")
if not hospital_id:
errors.append("Hospital selection is required")
if severity not in ["low", "medium", "high", "critical"]:
errors.append("Invalid severity selected")
if errors:
return JsonResponse({"success": False, "errors": errors}, status=400)
try:
hospital = Hospital.objects.get(id=hospital_id)
category = None
if category_id:
category = get_object_or_404(ObservationCategory, id=category_id)
location = Location.objects.filter(id=location_id).first() if location_id else None
main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None
subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None
# Get client info
def get_client_ip(req):
x_forwarded_for = req.META.get("HTTP_X_FORWARDED_FOR")
if x_forwarded_for:
ip = x_forwarded_for.split(",")[0].strip()
else:
ip = req.META.get("REMOTE_ADDR")
return ip
client_ip = get_client_ip(request)
user_agent = request.META.get("HTTP_USER_AGENT", "")
# Handle file uploads
attachments = request.FILES.getlist("attachments")
# Create observation using service
observation = ObservationService.create_observation(
description=description,
severity=severity,
category=category,
title=title,
hospital=hospital,
location_text=location_text,
location=location,
main_section=main_section,
subsection=subsection,
incident_datetime=incident_datetime if incident_datetime else None,
reporter_staff_id=reporter_staff_id,
reporter_name=reporter_name,
reporter_phone=reporter_phone,
reporter_email=reporter_email,
client_ip=client_ip,
user_agent=user_agent,
attachments=attachments,
)
return JsonResponse(
{"success": True, "tracking_code": observation.tracking_code, "observation_id": str(observation.id)}
)
except Exception as e:
return JsonResponse({"success": False, "errors": [str(e)]}, status=500)