HH/apps/core/middleware.py
ismail 7369d08012
All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
feat: unified reference numbers + feedback modules QA audit
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001):
- new ReferenceSequence model + generate_reference() helper (apps/core)
- Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save()
- prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only)
- removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources
- migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012
- unit tests (format, sanitization, monthly reset, 40-thread concurrency)

QA audit:
- isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env)
- feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion)
- reports/feedback-modules-qa-report.md

Also bundles accumulated in-progress work across complaints, observations,
organizations, templates, and other modules.
2026-06-14 14:29:23 +03:00

192 lines
6.9 KiB
Python

"""
Tenant-aware middleware for multi-tenancy
"""
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.deprecation import MiddlewareMixin
class TenantMiddleware(MiddlewareMixin):
"""
Middleware that sets the current hospital context from the authenticated user.
This middleware ensures that:
- authenticated users have their tenant_hospital set from their profile
- PX admins can switch between hospitals via session
- PX admins without a selected hospital are redirected to select-hospital
- Source Users have their source context available
- All requests have tenant context available
"""
EXEMPT_PATHS = [
"/core/select-hospital/",
"/accounts/logout/",
"/accounts/password_reset/",
"/accounts/password_reset/done/",
"/accounts/reset/",
"/api/",
"/health/",
"/admin/",
"/__debug__/",
]
def process_request(self, request):
"""Set tenant hospital context on each request."""
if request.user and request.user.is_authenticated:
request.user_roles = request.user.get_role_names()
request.source_user = None
request.source_user_profile = None
if request.user.is_source_user():
profile = request.user.get_source_user_profile_active()
if profile:
request.source_user = profile
request.source_user_profile = profile
if request.user.is_px_admin():
hospital_id = request.session.get("selected_hospital_id")
if hospital_id:
from apps.organizations.models import Hospital
try:
request.tenant_hospital = Hospital.objects.get(id=hospital_id)
except Hospital.DoesNotExist:
request.tenant_hospital = None
request.session.pop("selected_hospital_id", None)
else:
request.tenant_hospital = None
else:
request.tenant_hospital = request.user.hospital
else:
request.tenant_hospital = None
request.user_roles = []
request.source_user = None
request.source_user_profile = None
return None
def process_view(self, request, view_func, view_args, view_kwargs):
"""Redirect PX admins without hospital to select-hospital page."""
if not request.user or not request.user.is_authenticated:
return None
if not request.user.is_px_admin():
return None
if request.tenant_hospital:
return None
path = request.path
for exempt_path in self.EXEMPT_PATHS:
if path.startswith(exempt_path):
return None
if request.headers.get("x-requested-with") == "XMLHttpRequest":
return None
return HttpResponseRedirect(reverse("core:select_hospital"))
class DepartmentRespondentMiddleware(MiddlewareMixin):
"""
Restrict Department Respondent users to only their department detail page,
inquiry department response pages, and observation department response pages.
"""
ALLOWED_PATH_PREFIXES = [
"/",
"/accounts/logout/",
"/accounts/password_reset/",
"/accounts/reset/",
"/core/select-hospital/",
"/organizations/departments/",
"/inquiries/",
"/observations/",
"/api/",
"/health/",
"/admin/",
"/__debug__/",
"/i18n/",
"/static/",
]
def process_view(self, request, view_func, view_args, view_kwargs):
if not request.user or not request.user.is_authenticated:
return None
if not request.user.is_champion():
return None
if request.user.is_px_admin() or request.user.is_hospital_admin() or request.user.is_department_manager() or request.user.is_director():
return None
if request.headers.get("x-requested-with") == "XMLHttpRequest":
return None
path = request.path
if path == "/":
return None
for prefix in self.ALLOWED_PATH_PREFIXES:
if path.startswith(prefix):
if path.startswith("/organizations/departments/"):
if "set-champion" in path or "edit" in path or "delete" in path:
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
return None
if path.startswith("/inquiries/"):
if not path.endswith("/department-response/"):
from apps.complaints.models import Inquiry
pk = view_kwargs.get("pk")
if pk:
try:
inquiry = Inquiry.objects.get(pk=pk)
user_dept = request.user.department
if inquiry.department == user_dept or inquiry.outgoing_department == user_dept:
return None
except Inquiry.DoesNotExist:
pass
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
return None
if path.startswith("/observations/"):
if path.endswith("/department-response/"):
from apps.observations.models import Observation
pk = view_kwargs.get("pk")
if pk:
try:
observation = Observation.objects.get(pk=pk)
user_dept = request.user.department
if observation.assigned_department == user_dept:
return None
except Observation.DoesNotExist:
pass
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
pk = view_kwargs.get("pk")
if pk:
from apps.observations.models import Observation
try:
observation = Observation.objects.get(pk=pk)
user_dept = request.user.department
if observation.assigned_department == user_dept:
return None
except Observation.DoesNotExist:
pass
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
return None
from django.http import HttpResponseForbidden
return HttpResponseForbidden()