345 lines
11 KiB
Python
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'
|