""" 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)