333 lines
11 KiB
Python
333 lines
11 KiB
Python
"""
|
|
Surveys admin
|
|
"""
|
|
|
|
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
from django.db.models import Count
|
|
|
|
from .models import (
|
|
SurveyInstance,
|
|
SurveyQuestion,
|
|
SurveyResponse,
|
|
SurveyTemplate,
|
|
SurveyTracking,
|
|
)
|
|
|
|
|
|
class SurveyQuestionInline(admin.TabularInline):
|
|
"""Inline admin for survey questions"""
|
|
|
|
model = SurveyQuestion
|
|
extra = 1
|
|
fields = ["order", "text", "question_type", "is_required", "is_base", "event_type"]
|
|
ordering = ["order"]
|
|
|
|
|
|
@admin.register(SurveyTemplate)
|
|
class SurveyTemplateAdmin(admin.ModelAdmin):
|
|
"""Survey template admin"""
|
|
|
|
list_display = [
|
|
"name",
|
|
"survey_type",
|
|
"hospital",
|
|
"scoring_method",
|
|
"negative_threshold",
|
|
"get_question_count",
|
|
"is_active",
|
|
]
|
|
list_filter = ["survey_type", "scoring_method", "is_active", "hospital"]
|
|
search_fields = ["name", "name_ar"]
|
|
ordering = ["hospital", "name"]
|
|
inlines = [SurveyQuestionInline]
|
|
|
|
fieldsets = (
|
|
(None, {"fields": ("name", "name_ar")}),
|
|
("Configuration", {"fields": ("hospital", "survey_type")}),
|
|
("Scoring", {"fields": ("scoring_method", "negative_threshold")}),
|
|
("Status", {"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").prefetch_related("questions")
|
|
|
|
|
|
@admin.register(SurveyQuestion)
|
|
class SurveyQuestionAdmin(admin.ModelAdmin):
|
|
"""Survey question admin"""
|
|
|
|
list_display = ["survey_template", "order", "text_preview", "question_type", "is_base", "is_required"]
|
|
list_filter = ["survey_template", "question_type", "is_required", "is_base"]
|
|
search_fields = ["text", "text_ar"]
|
|
ordering = ["survey_template", "order"]
|
|
|
|
fieldsets = (
|
|
(None, {"fields": ("survey_template", "order")}),
|
|
("Question Text", {"fields": ("text", "text_ar")}),
|
|
("Configuration", {"fields": ("question_type", "is_required", "is_base", "event_type")}),
|
|
("Choices (for multiple choice)", {"fields": ("choices_json",), "classes": ("collapse",)}),
|
|
("Metadata", {"fields": ("created_at", "updated_at")}),
|
|
)
|
|
|
|
readonly_fields = ["created_at", "updated_at"]
|
|
|
|
def text_preview(self, obj):
|
|
"""Show preview of question text"""
|
|
return obj.text[:100] + "..." if len(obj.text) > 100 else obj.text
|
|
|
|
text_preview.short_description = "Question"
|
|
|
|
def get_queryset(self, request):
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related("survey_template")
|
|
|
|
|
|
class SurveyResponseInline(admin.TabularInline):
|
|
"""Inline admin for survey responses"""
|
|
|
|
model = SurveyResponse
|
|
extra = 0
|
|
fields = ["question", "numeric_value", "text_value", "choice_value"]
|
|
readonly_fields = ["question"]
|
|
ordering = ["question__order"]
|
|
|
|
def has_add_permission(self, request, obj=None):
|
|
return False
|
|
|
|
|
|
class SurveyTrackingInline(admin.TabularInline):
|
|
"""Inline admin for survey tracking events"""
|
|
|
|
model = SurveyTracking
|
|
extra = 0
|
|
fields = ["event_type", "device_type", "browser", "total_time_spent", "created_at"]
|
|
readonly_fields = ["event_type", "device_type", "browser", "total_time_spent", "created_at"]
|
|
ordering = ["-created_at"]
|
|
can_delete = False
|
|
|
|
def has_add_permission(self, request, obj=None):
|
|
return False
|
|
|
|
|
|
@admin.register(SurveyInstance)
|
|
class SurveyInstanceAdmin(admin.ModelAdmin):
|
|
"""Survey instance admin"""
|
|
|
|
list_display = [
|
|
"survey_label",
|
|
"patient",
|
|
"encounter_id",
|
|
"status_badge",
|
|
"delivery_channel",
|
|
"open_count",
|
|
"time_spent_display",
|
|
"total_score",
|
|
"is_negative",
|
|
"sent_at",
|
|
"completed_at",
|
|
]
|
|
list_filter = [
|
|
"status",
|
|
"delivery_channel",
|
|
"is_negative",
|
|
"survey_template__survey_type",
|
|
"sent_at",
|
|
"completed_at",
|
|
]
|
|
search_fields = ["patient__mrn", "patient__first_name", "patient__last_name", "encounter_id", "access_token"]
|
|
ordering = ["-created_at"]
|
|
inlines = [SurveyResponseInline, SurveyTrackingInline]
|
|
|
|
fieldsets = (
|
|
(None, {"fields": ("survey_template", "patient", "encounter_id")}),
|
|
("Journey Linkage", {"fields": ("journey_instance",), "classes": ("collapse",)}),
|
|
(
|
|
"Delivery",
|
|
{"fields": ("delivery_channel", "recipient_phone", "recipient_email", "access_token", "token_expires_at")},
|
|
),
|
|
("Status & Timestamps", {"fields": ("status", "sent_at", "opened_at", "completed_at")}),
|
|
("Tracking", {"fields": ("open_count", "last_opened_at", "time_spent_seconds")}),
|
|
("Scoring", {"fields": ("total_score", "is_negative")}),
|
|
("Patient Comment", {"fields": ("comment", "comment_analyzed")}),
|
|
("Comment Analysis", {"fields": ("comment_analysis",), "classes": ("collapse",)}),
|
|
("Metadata", {"fields": ("metadata", "created_at", "updated_at"), "classes": ("collapse",)}),
|
|
)
|
|
|
|
readonly_fields = [
|
|
"access_token",
|
|
"token_expires_at",
|
|
"sent_at",
|
|
"opened_at",
|
|
"completed_at",
|
|
"open_count",
|
|
"last_opened_at",
|
|
"time_spent_seconds",
|
|
"total_score",
|
|
"is_negative",
|
|
"comment_analyzed",
|
|
"comment_analysis",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
|
|
def get_queryset(self, request):
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related("survey_template", "patient", "journey_instance").prefetch_related(
|
|
"responses", "tracking_events"
|
|
)
|
|
|
|
def status_badge(self, obj):
|
|
"""Display status with color badge"""
|
|
colors = {
|
|
"sent": "secondary",
|
|
"viewed": "info",
|
|
"in_progress": "warning",
|
|
"completed": "success",
|
|
"abandoned": "danger",
|
|
"expired": "secondary",
|
|
"cancelled": "dark",
|
|
}
|
|
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 survey_label(self, obj):
|
|
if obj.survey_template:
|
|
return obj.survey_template.name
|
|
return obj.metadata.get("patient_type", "Event-based Survey")
|
|
|
|
survey_label.short_description = "Survey"
|
|
|
|
def time_spent_display(self, obj):
|
|
"""Display time spent in human-readable format"""
|
|
if obj.time_spent_seconds:
|
|
minutes = obj.time_spent_seconds // 60
|
|
seconds = obj.time_spent_seconds % 60
|
|
return f"{minutes}m {seconds}s"
|
|
return "-"
|
|
|
|
time_spent_display.short_description = "Time Spent"
|
|
|
|
|
|
@admin.register(SurveyTracking)
|
|
class SurveyTrackingAdmin(admin.ModelAdmin):
|
|
"""Survey tracking admin"""
|
|
|
|
list_display = [
|
|
"survey_instance_link",
|
|
"event_type_badge",
|
|
"device_type",
|
|
"browser",
|
|
"ip_address",
|
|
"total_time_spent_display",
|
|
"created_at",
|
|
]
|
|
list_filter = ["event_type", "device_type", "browser", "survey_instance__survey_template", "created_at"]
|
|
search_fields = [
|
|
"survey_instance__patient__mrn",
|
|
"survey_instance__patient__first_name",
|
|
"survey_instance__patient__last_name",
|
|
"ip_address",
|
|
"user_agent",
|
|
]
|
|
ordering = ["-created_at"]
|
|
|
|
fieldsets = (
|
|
(None, {"fields": ("survey_instance", "event_type")}),
|
|
("Timing", {"fields": ("time_on_page", "total_time_spent")}),
|
|
("Context", {"fields": ("current_question",)}),
|
|
("Device Info", {"fields": ("user_agent", "ip_address", "device_type", "browser")}),
|
|
("Location", {"fields": ("country", "city"), "classes": ("collapse",)}),
|
|
("Metadata", {"fields": ("metadata", "created_at"), "classes": ("collapse",)}),
|
|
)
|
|
|
|
readonly_fields = ["created_at"]
|
|
|
|
def get_queryset(self, request):
|
|
qs = super().get_queryset(request)
|
|
return qs.select_related("survey_instance", "survey_instance__patient")
|
|
|
|
def survey_instance_link(self, obj):
|
|
"""Link to survey instance"""
|
|
url = f"/admin/surveys/surveyinstance/{obj.survey_instance.id}/change/"
|
|
label = (
|
|
obj.survey_instance.survey_template.name
|
|
if obj.survey_instance.survey_template
|
|
else obj.survey_instance.metadata.get("patient_type", "Survey")
|
|
)
|
|
return format_html(
|
|
'<a href="{}">{} - {}</a>',
|
|
url,
|
|
label,
|
|
obj.survey_instance.patient.get_full_name(),
|
|
)
|
|
|
|
survey_instance_link.short_description = "Survey"
|
|
|
|
def event_type_badge(self, obj):
|
|
"""Display event type with color badge"""
|
|
colors = {
|
|
"page_view": "info",
|
|
"survey_started": "primary",
|
|
"question_answered": "secondary",
|
|
"survey_completed": "success",
|
|
"survey_abandoned": "danger",
|
|
"reminder_sent": "warning",
|
|
}
|
|
color = colors.get(obj.event_type, "secondary")
|
|
return format_html('<span class="badge bg-{}">{}</span>', color, obj.get_event_type_display())
|
|
|
|
event_type_badge.short_description = "Event Type"
|
|
|
|
def total_time_spent_display(self, obj):
|
|
"""Display time spent in human-readable format"""
|
|
if obj.total_time_spent:
|
|
minutes = obj.total_time_spent // 60
|
|
seconds = obj.total_time_spent % 60
|
|
return f"{minutes}m {seconds}s"
|
|
return "-"
|
|
|
|
total_time_spent_display.short_description = "Time Spent"
|
|
|
|
|
|
@admin.register(SurveyResponse)
|
|
class SurveyResponseAdmin(admin.ModelAdmin):
|
|
"""Survey response admin"""
|
|
|
|
list_display = ["survey_instance", "question_preview", "numeric_value", "text_value_preview", "created_at"]
|
|
list_filter = ["survey_instance__survey_template", "question__question_type", "created_at"]
|
|
search_fields = ["survey_instance__patient__mrn", "question__text", "text_value"]
|
|
ordering = ["survey_instance", "question__order"]
|
|
|
|
fieldsets = (
|
|
(None, {"fields": ("survey_instance", "question")}),
|
|
("Response", {"fields": ("numeric_value", "text_value", "choice_value")}),
|
|
("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("survey_instance", "question")
|
|
|
|
def question_preview(self, obj):
|
|
"""Show preview of question"""
|
|
return obj.question.text[:50] + "..." if len(obj.question.text) > 50 else obj.question.text
|
|
|
|
question_preview.short_description = "Question"
|
|
|
|
def text_value_preview(self, obj):
|
|
"""Show preview of text response"""
|
|
if obj.text_value:
|
|
return obj.text_value[:50] + "..." if len(obj.text_value) > 50 else obj.text_value
|
|
return "-"
|
|
|
|
text_value_preview.short_description = "Text Response"
|