All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
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.
332 lines
12 KiB
Python
332 lines
12 KiB
Python
"""
|
|
Integrations admin
|
|
"""
|
|
|
|
from django.contrib import admin
|
|
from django.utils.html import format_html, mark_safe
|
|
|
|
from .models import (
|
|
EventMapping,
|
|
ExternalAPIKey,
|
|
HISEventType,
|
|
HISTestPatient,
|
|
HISTestVisit,
|
|
HISPatientVisit,
|
|
HISVisitEvent,
|
|
InboundEvent,
|
|
IntegrationConfig,
|
|
SurveyTemplateMapping,
|
|
)
|
|
|
|
|
|
@admin.register(InboundEvent)
|
|
class InboundEventAdmin(admin.ModelAdmin):
|
|
"""Inbound event admin"""
|
|
|
|
list_display = ["source_system", "event_code", "encounter_id", "status_badge", "processing_attempts", "received_at"]
|
|
list_filter = ["status", "source_system", "received_at"]
|
|
search_fields = ["encounter_id", "patient_identifier", "event_code"]
|
|
ordering = ["-received_at"]
|
|
date_hierarchy = "received_at"
|
|
|
|
fieldsets = (
|
|
("Event Information", {"fields": ("source_system", "event_code", "encounter_id", "patient_identifier")}),
|
|
("Payload", {"fields": ("payload_json",), "classes": ("collapse",)}),
|
|
("Processing Status", {"fields": ("status", "processing_attempts", "error")}),
|
|
("Extracted Context", {"fields": ("physician_license", "department_code"), "classes": ("collapse",)}),
|
|
("Timestamps", {"fields": ("received_at", "processed_at", "created_at", "updated_at")}),
|
|
("Metadata", {"fields": ("metadata",), "classes": ("collapse",)}),
|
|
)
|
|
|
|
readonly_fields = ["received_at", "processed_at", "created_at", "updated_at"]
|
|
|
|
def status_badge(self, obj):
|
|
"""Display status with color badge"""
|
|
colors = {
|
|
"pending": "warning",
|
|
"processing": "info",
|
|
"processed": "success",
|
|
"failed": "danger",
|
|
"ignored": "secondary",
|
|
}
|
|
color = colors.get(obj.status, "secondary")
|
|
return format_html('<span class="badge bg-{}">{}</span>', color, obj.get_status_display())
|
|
|
|
status_badge.short_description = "Status"
|
|
|
|
def has_add_permission(self, request):
|
|
# Events should only be created via API
|
|
return False
|
|
|
|
|
|
class EventMappingInline(admin.TabularInline):
|
|
"""Inline admin for event mappings"""
|
|
|
|
model = EventMapping
|
|
extra = 1
|
|
fields = ["external_event_code", "internal_event_code", "is_active"]
|
|
|
|
|
|
@admin.register(IntegrationConfig)
|
|
class IntegrationConfigAdmin(admin.ModelAdmin):
|
|
"""Integration configuration admin"""
|
|
|
|
list_display = ["name", "source_system", "is_active", "last_sync_at", "created_at"]
|
|
list_filter = ["source_system", "is_active"]
|
|
search_fields = ["name", "description"]
|
|
ordering = ["name"]
|
|
inlines = [EventMappingInline]
|
|
|
|
fieldsets = (
|
|
(None, {"fields": ("name", "source_system", "description")}),
|
|
("Connection", {"fields": ("api_url", "api_key"), "classes": ("collapse",)}),
|
|
("Configuration", {"fields": ("is_active", "config_json"), "classes": ("collapse",)}),
|
|
("Metadata", {"fields": ("last_sync_at", "created_at", "updated_at")}),
|
|
)
|
|
|
|
readonly_fields = ["last_sync_at", "created_at", "updated_at"]
|
|
|
|
|
|
@admin.register(SurveyTemplateMapping)
|
|
class SurveyTemplateMappingAdmin(admin.ModelAdmin):
|
|
"""Survey template mapping admin"""
|
|
|
|
list_display = ["hospital", "patient_type", "survey_template", "is_active"]
|
|
list_filter = ["hospital", "patient_type", "is_active"]
|
|
search_fields = ["hospital__name", "survey_template__name"]
|
|
ordering = ["hospital", "patient_type"]
|
|
|
|
fieldsets = (
|
|
("Mapping Configuration", {"fields": ("hospital", "patient_type", "survey_template")}),
|
|
("Settings", {"fields": ("is_active",)}),
|
|
("Metadata", {"fields": ("created_at", "updated_at")}),
|
|
)
|
|
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
def get_queryset(self, request):
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related("hospital", "survey_template")
|
|
|
|
|
|
class HISVisitEventInline(admin.TabularInline):
|
|
model = HISVisitEvent
|
|
extra = 0
|
|
fields = ["event_type", "bill_date", "parsed_date", "visit_category"]
|
|
readonly_fields = ["event_type", "bill_date", "parsed_date", "visit_category"]
|
|
show_change_link = False
|
|
|
|
|
|
@admin.register(HISPatientVisit)
|
|
class HISPatientVisitAdmin(admin.ModelAdmin):
|
|
"""HIS patient visit admin"""
|
|
|
|
list_display = [
|
|
"patient",
|
|
"patient_type",
|
|
"admission_id",
|
|
"hospital",
|
|
"is_visit_complete",
|
|
"survey_linked",
|
|
"discharge_date",
|
|
"last_his_fetch_at",
|
|
]
|
|
list_filter = ["patient_type", "is_visit_complete", "hospital"]
|
|
search_fields = ["admission_id", "reg_code", "patient_id_his", "patient__first_name", "patient__last_name"]
|
|
ordering = ["-last_his_fetch_at"]
|
|
date_hierarchy = "last_his_fetch_at"
|
|
inlines = [HISVisitEventInline]
|
|
|
|
fieldsets = (
|
|
(
|
|
"Patient & Hospital",
|
|
{"fields": ("patient", "hospital", "admission_id", "reg_code", "patient_id_his", "patient_type")},
|
|
),
|
|
("Visit Dates", {"fields": ("admit_date", "discharge_date", "effective_discharge_date")}),
|
|
("Status", {"fields": ("is_visit_complete", "survey_instance", "last_his_fetch_at")}),
|
|
("HIS Data", {"fields": ("visit_data", "visit_timeline"), "classes": ("collapse",)}),
|
|
("Doctor & Consultant", {"fields": ("primary_doctor", "consultant_id", "primary_doctor_fk", "consultant_fk")}),
|
|
("Insurance & Billing", {"fields": ("company_name", "grade_name", "insurance_company_name", "bill_type")}),
|
|
("VIP & Nationality", {"fields": ("is_vip", "nationality")}),
|
|
("Timestamps", {"fields": ("created_at", "updated_at")}),
|
|
)
|
|
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
def survey_linked(self, obj):
|
|
if obj.survey_instance:
|
|
return format_html(
|
|
'<a href="/admin/surveys/surveyinstance/{}/">{}</a>', obj.survey_instance.id, obj.survey_instance.id
|
|
)
|
|
return mark_safe('<span class="badge bg-secondary">None</span>')
|
|
|
|
survey_linked.short_description = "Survey"
|
|
|
|
def has_add_permission(self, request):
|
|
return False
|
|
|
|
|
|
@admin.register(HISVisitEvent)
|
|
class HISVisitEventAdmin(admin.ModelAdmin):
|
|
"""HIS visit event admin"""
|
|
|
|
list_display = ["visit", "event_type", "bill_date", "parsed_date", "visit_category", "admission_id"]
|
|
list_filter = ["visit_category", "patient_type"]
|
|
search_fields = ["admission_id", "patient_id", "event_type"]
|
|
ordering = ["-parsed_date"]
|
|
date_hierarchy = "parsed_date"
|
|
|
|
fieldsets = (
|
|
("Visit", {"fields": ("visit",)}),
|
|
("Event Details", {"fields": ("event_type", "bill_date", "parsed_date", "visit_category")}),
|
|
(
|
|
"Patient Info",
|
|
{"fields": ("patient_type", "admission_id", "patient_id", "reg_code", "ssn", "mobile_no")},
|
|
),
|
|
("Timestamps", {"fields": ("created_at", "updated_at")}),
|
|
)
|
|
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
def has_add_permission(self, request):
|
|
return False
|
|
|
|
|
|
@admin.register(HISEventType)
|
|
class HISEventTypeAdmin(admin.ModelAdmin):
|
|
"""HIS event type admin - auto-populated from HIS data"""
|
|
|
|
list_display = ["event_type", "patient_types_display", "event_count", "last_seen_at"]
|
|
search_fields = ["event_type"]
|
|
ordering = ["event_type"]
|
|
list_filter = ["last_seen_at"]
|
|
|
|
readonly_fields = ["event_type", "patient_types", "event_count", "last_seen_at", "created_at", "updated_at"]
|
|
|
|
def has_add_permission(self, request):
|
|
return False
|
|
|
|
def patient_types_display(self, obj):
|
|
if not obj.patient_types:
|
|
return mark_safe('<span class="badge bg-secondary">None</span>')
|
|
return ", ".join(obj.patient_types)
|
|
|
|
patient_types_display.short_description = "Patient Types"
|
|
|
|
|
|
@admin.register(EventMapping)
|
|
class EventMappingAdmin(admin.ModelAdmin):
|
|
"""Event mapping admin"""
|
|
|
|
list_display = ["integration_config", "external_event_code", "internal_event_code", "is_active"]
|
|
list_filter = ["integration_config", "is_active"]
|
|
search_fields = ["external_event_code", "internal_event_code"]
|
|
ordering = ["integration_config", "external_event_code"]
|
|
|
|
fieldsets = (
|
|
(None, {"fields": ("integration_config", "external_event_code", "internal_event_code")}),
|
|
("Field Mappings", {"fields": ("field_mappings",), "classes": ("collapse",)}),
|
|
("Configuration", {"fields": ("is_active",)}),
|
|
("Metadata", {"fields": ("created_at", "updated_at")}),
|
|
)
|
|
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
def get_queryset(self, request):
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related("integration_config")
|
|
|
|
|
|
@admin.register(HISTestPatient)
|
|
class HISTestPatientAdmin(admin.ModelAdmin):
|
|
list_display = ["admission_id", "patient_name", "patient_type", "hospital_name", "admit_date", "discharge_date"]
|
|
list_filter = ["patient_type", "hospital_name"]
|
|
search_fields = ["admission_id", "patient_id", "ssn", "mobile_no", "patient_name"]
|
|
ordering = ["-admit_date"]
|
|
date_hierarchy = "admit_date"
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
def has_add_permission(self, request):
|
|
return False
|
|
|
|
|
|
@admin.register(HISTestVisit)
|
|
class HISTestVisitAdmin(admin.ModelAdmin):
|
|
list_display = ["admission_id", "visit_category", "event_type", "bill_date"]
|
|
list_filter = ["visit_category", "event_type"]
|
|
search_fields = ["admission_id", "patient_id"]
|
|
ordering = ["-bill_date"]
|
|
date_hierarchy = "bill_date"
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
def has_add_permission(self, request):
|
|
return False
|
|
|
|
|
|
@admin.register(ExternalAPIKey)
|
|
class ExternalAPIKeyAdmin(admin.ModelAdmin):
|
|
"""Admin for External API Keys - shows the plaintext key ONLY on creation."""
|
|
|
|
list_display = ["name", "key_prefix", "hospital", "is_active", "allowed_entities_display", "last_used_at", "created_at"]
|
|
list_filter = ["is_active", "hospital"]
|
|
search_fields = ["name", "key_prefix", "description"]
|
|
ordering = ["-created_at"]
|
|
readonly_fields = ["key_hash", "key_prefix", "last_used_at", "created_at", "updated_at"]
|
|
|
|
fieldsets = (
|
|
(
|
|
"Identity",
|
|
{"fields": ("name", "description", "key_prefix", "key_hash")},
|
|
),
|
|
(
|
|
"Scope",
|
|
{"fields": ("hospital", "allowed_entities", "rate_limit")},
|
|
),
|
|
(
|
|
"Status",
|
|
{"fields": ("is_active", "expires_at", "last_used_at")},
|
|
),
|
|
(
|
|
"Metadata",
|
|
{"fields": ("created_by", "created_at", "updated_at")},
|
|
),
|
|
)
|
|
|
|
def allowed_entities_display(self, obj):
|
|
if not obj.allowed_entities:
|
|
return mark_safe('<span class="badge bg-success">All Entities</span>')
|
|
return ", ".join(obj.allowed_entities)
|
|
|
|
allowed_entities_display.short_description = "Allowed Entities"
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
if not change:
|
|
# Creating a new key — generate it
|
|
obj.created_by = request.user
|
|
obj.save()
|
|
|
|
# Store the raw key to show in admin message
|
|
# We need to generate the key ourselves since save_model doesn't return it
|
|
import secrets
|
|
import hashlib
|
|
raw_key = secrets.token_hex(32)
|
|
obj.key_prefix = raw_key[:8]
|
|
obj.key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
|
|
obj.save(update_fields=["key_prefix", "key_hash"])
|
|
|
|
self._generated_key = raw_key
|
|
else:
|
|
obj.save()
|
|
|
|
def response_add(self, request, obj, post_url_continue=None):
|
|
resp = super().response_add(request, obj, post_url_continue)
|
|
if hasattr(self, "_generated_key"):
|
|
from django.contrib import messages
|
|
messages.success(
|
|
request,
|
|
f'API Key created. Copy it now — it will NOT be shown again: {self._generated_key}',
|
|
)
|
|
# Clear it
|
|
del self._generated_key
|
|
return resp
|