HH/apps/surveys/admin.py
2026-01-24 15:27:30 +03:00

345 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']
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_required'
]
list_filter = ['survey_template', 'question_type', 'is_required']
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')
}),
('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_template', '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')
}),
('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',
'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 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',
'survey_instance__survey_template'
)
def survey_instance_link(self, obj):
"""Link to survey instance"""
url = f"/admin/surveys/surveyinstance/{obj.survey_instance.id}/change/"
return format_html('<a href="{}">{} - {}</a>', url, obj.survey_instance.survey_template.name, 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'