HH/apps/surveys/ui_views.py

1685 lines
61 KiB
Python

"""
Survey Console UI views - Server-rendered templates for survey management
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q, Prefetch, Avg, Count, F, Case, When, IntegerField
from django.db.models.functions import TruncDate
from django.http import FileResponse, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.http import require_http_methods
from django.db.models import ExpressionWrapper, FloatField
from apps.core.services import AuditService
from apps.organizations.models import Department, Hospital
from .forms import ManualSurveySendForm, SurveyQuestionFormSet, SurveyTemplateForm
from .services import SurveyDeliveryService
from .models import SurveyInstance, SurveyTemplate, SurveyQuestion
from .tasks import send_satisfaction_feedback
from datetime import datetime
@login_required
def survey_instance_list(request):
"""
Survey instances list view with filters.
Features:
- Server-side pagination
- Filters (status, journey type, hospital, date range)
- Search by patient MRN
- Score display
"""
# Base queryset with optimizations
queryset = SurveyInstance.objects.select_related(
'survey_template',
'patient',
'journey_instance__journey_template'
).prefetch_related(
'responses__question'
)
# Apply RBAC filters
user = request.user
if user.is_px_admin():
pass # See all
elif user.is_hospital_admin() and user.hospital:
queryset = queryset.filter(survey_template__hospital=user.hospital)
elif user.hospital:
queryset = queryset.filter(survey_template__hospital=user.hospital)
else:
queryset = queryset.none()
# Apply filters
status_filter = request.GET.get('status')
if status_filter:
queryset = queryset.filter(status=status_filter)
survey_type = request.GET.get('survey_type')
if survey_type:
queryset = queryset.filter(survey_template__survey_type=survey_type)
is_negative = request.GET.get('is_negative')
if is_negative == 'true':
queryset = queryset.filter(is_negative=True)
hospital_filter = request.GET.get('hospital')
if hospital_filter:
queryset = queryset.filter(survey_template__hospital_id=hospital_filter)
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(patient__mrn__icontains=search_query) |
Q(patient__first_name__icontains=search_query) |
Q(patient__last_name__icontains=search_query) |
Q(encounter_id__icontains=search_query)
)
# Date range
date_from = request.GET.get('date_from')
if date_from:
queryset = queryset.filter(sent_at__gte=date_from)
date_to = request.GET.get('date_to')
if date_to:
queryset = queryset.filter(sent_at__lte=date_to)
# Ordering
order_by = request.GET.get('order_by', '-created_at')
queryset = queryset.order_by(order_by)
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
# Get base queryset for statistics (without pagination)
stats_queryset = SurveyInstance.objects.select_related('survey_template')
# Apply same RBAC filters
if user.is_px_admin():
pass
elif user.is_hospital_admin() and user.hospital:
stats_queryset = stats_queryset.filter(survey_template__hospital=user.hospital)
elif user.hospital:
stats_queryset = stats_queryset.filter(survey_template__hospital=user.hospital)
else:
stats_queryset = stats_queryset.none()
# Apply same filters to stats
if status_filter:
stats_queryset = stats_queryset.filter(status=status_filter)
if survey_type:
stats_queryset = stats_queryset.filter(survey_template__survey_type=survey_type)
if is_negative == 'true':
stats_queryset = stats_queryset.filter(is_negative=True)
if hospital_filter:
stats_queryset = stats_queryset.filter(survey_template__hospital_id=hospital_filter)
if search_query:
stats_queryset = stats_queryset.filter(
Q(patient__mrn__icontains=search_query) |
Q(patient__first_name__icontains=search_query) |
Q(patient__last_name__icontains=search_query) |
Q(encounter_id__icontains=search_query)
)
if date_from:
stats_queryset = stats_queryset.filter(sent_at__gte=date_from)
if date_to:
stats_queryset = stats_queryset.filter(sent_at__lte=date_to)
# Statistics
total_count = stats_queryset.count()
# Include both 'sent' and 'pending' statuses for sent count
sent_count = stats_queryset.filter(status__in=['sent', 'pending']).count()
completed_count = stats_queryset.filter(status='completed').count()
negative_count = stats_queryset.filter(is_negative=True).count()
# Tracking statistics
opened_count = stats_queryset.filter(open_count__gt=0).count()
in_progress_count = stats_queryset.filter(status='in_progress').count()
abandoned_count = stats_queryset.filter(status='abandoned').count()
viewed_count = stats_queryset.filter(status='viewed').count()
pending_count = stats_queryset.filter(status='pending').count()
# Time metrics
completed_surveys = stats_queryset.filter(
status='completed',
time_spent_seconds__isnull=False
)
avg_completion_time = completed_surveys.aggregate(
avg_time=Avg('time_spent_seconds')
)['avg_time'] or 0
# Time to first open
surveys_with_open = stats_queryset.filter(
opened_at__isnull=False,
sent_at__isnull=False
)
if surveys_with_open.exists():
# Calculate average time to open
total_time_to_open = 0
count = 0
for survey in surveys_with_open:
if survey.opened_at and survey.sent_at:
total_time_to_open += (survey.opened_at - survey.sent_at).total_seconds()
count += 1
avg_time_to_open = total_time_to_open / count if count > 0 else 0
else:
avg_time_to_open = 0
stats = {
'total': total_count,
'sent': sent_count,
'completed': completed_count,
'negative': negative_count,
'response_rate': round((completed_count / total_count * 100) if total_count > 0 else 0, 1),
# New tracking stats
'opened': opened_count,
'open_rate': round((opened_count / sent_count * 100) if sent_count > 0 else 0, 1),
'in_progress': in_progress_count,
'abandoned': abandoned_count,
'viewed': viewed_count,
'pending': pending_count,
'avg_completion_time': int(avg_completion_time),
'avg_time_to_open': int(avg_time_to_open),
}
# Score Distribution
score_distribution = []
score_ranges = [
('1-2', 1, 2),
('2-3', 2, 3),
('3-4', 3, 4),
('4-5', 4, 5),
]
for label, min_score, max_score in score_ranges:
# Use lte for the highest range to include exact match
if max_score == 5:
count = stats_queryset.filter(
total_score__gte=min_score,
total_score__lte=max_score
).count()
else:
count = stats_queryset.filter(
total_score__gte=min_score,
total_score__lt=max_score
).count()
score_distribution.append({
'range': label,
'count': count,
'percentage': round((count / total_count * 100) if total_count > 0 else 0, 1)
})
# Engagement Funnel Data - Include viewed and pending stages
engagement_funnel = [
{'stage': 'Sent/Pending', 'count': sent_count, 'percentage': 100},
{'stage': 'Viewed', 'count': viewed_count, 'percentage': round((viewed_count / sent_count * 100) if sent_count > 0 else 0, 1)},
{'stage': 'Opened', 'count': opened_count, 'percentage': round((opened_count / sent_count * 100) if sent_count > 0 else 0, 1)},
{'stage': 'In Progress', 'count': in_progress_count, 'percentage': round((in_progress_count / opened_count * 100) if opened_count > 0 else 0, 1)},
{'stage': 'Completed', 'count': completed_count, 'percentage': round((completed_count / sent_count * 100) if sent_count > 0 else 0, 1)},
]
# Completion Time Distribution
completion_time_ranges = [
('< 1 min', 0, 60),
('1-5 min', 60, 300),
('5-10 min', 300, 600),
('10-20 min', 600, 1200),
('20+ min', 1200, float('inf')),
]
completion_time_distribution = []
for label, min_seconds, max_seconds in completion_time_ranges:
if max_seconds == float('inf'):
count = completed_surveys.filter(time_spent_seconds__gte=min_seconds).count()
else:
count = completed_surveys.filter(
time_spent_seconds__gte=min_seconds,
time_spent_seconds__lt=max_seconds
).count()
completion_time_distribution.append({
'range': label,
'count': count,
'percentage': round((count / completed_count * 100) if completed_count > 0 else 0, 1)
})
# Device Type Distribution
device_distribution = []
from .models import SurveyTracking
tracking_events = SurveyTracking.objects.filter(
survey_instance__in=stats_queryset
).values('device_type').annotate(
count=Count('id')
).order_by('-count')
device_mapping = {
'mobile': 'Mobile',
'tablet': 'Tablet',
'desktop': 'Desktop',
}
for entry in tracking_events:
device_key = entry['device_type']
device_name = device_mapping.get(device_key, device_key.title())
count = entry['count']
percentage = round((count / tracking_events.count() * 100) if tracking_events.count() > 0 else 0, 1)
device_distribution.append({
'type': device_key,
'name': device_name,
'count': count,
'percentage': percentage
})
# Survey Trend (last 30 days) - Use created_at if sent_at is missing
from django.utils import timezone
import datetime
thirty_days_ago = timezone.now() - datetime.timedelta(days=30)
# Try sent_at first, fall back to created_at if sent_at is null
trend_queryset = stats_queryset.filter(
sent_at__gte=thirty_days_ago
)
# If no surveys with sent_at in last 30 days, try created_at
if not trend_queryset.exists():
trend_queryset = stats_queryset.filter(
created_at__gte=thirty_days_ago
).annotate(
date=TruncDate('created_at')
)
else:
trend_queryset = trend_queryset.annotate(
date=TruncDate('sent_at')
)
trend_data = trend_queryset.values('date').annotate(
sent=Count('id'),
completed=Count('id', filter=Q(status='completed'))
).order_by('date')
trend_labels = []
trend_sent = []
trend_completed = []
for entry in trend_data:
if entry['date']:
trend_labels.append(entry['date'].strftime('%Y-%m-%d'))
trend_sent.append(entry['sent'])
trend_completed.append(entry['completed'])
# Survey Type Distribution
survey_type_data = stats_queryset.values(
'survey_template__survey_type'
).annotate(
count=Count('id')
).order_by('-count')
survey_types = []
survey_type_labels = []
survey_type_counts = []
survey_type_mapping = {
'stage': 'Journey Stage',
'complaint_resolution': 'Complaint Resolution',
'general': 'General',
'nps': 'NPS',
}
for entry in survey_type_data:
type_key = entry['survey_template__survey_type']
type_name = survey_type_mapping.get(type_key, type_key.title())
count = entry['count']
percentage = round((count / total_count * 100) if total_count > 0 else 0, 1)
survey_types.append({
'type': type_key,
'name': type_name,
'count': count,
'percentage': percentage
})
survey_type_labels.append(type_name)
survey_type_counts.append(count)
# Serialize chart data to JSON for clean JavaScript usage
import json
context = {
'page_obj': page_obj,
'surveys': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'filters': request.GET,
# Visualization data as JSON for clean JavaScript
'engagement_funnel_json': json.dumps(engagement_funnel),
'completion_time_distribution_json': json.dumps(completion_time_distribution),
'device_distribution_json': json.dumps(device_distribution),
'score_distribution_json': json.dumps(score_distribution),
'survey_types_json': json.dumps(survey_types),
'trend_labels_json': json.dumps(trend_labels),
'trend_sent_json': json.dumps(trend_sent),
'trend_completed_json': json.dumps(trend_completed),
}
# Debug logging
import logging
logger = logging.getLogger(__name__)
logger.info(f"=== CHART DATA DEBUG ===")
logger.info(f"Score Distribution: {score_distribution}")
logger.info(f"Engagement Funnel: {engagement_funnel}")
logger.info(f"Completion Time Distribution: {completion_time_distribution}")
logger.info(f"Device Distribution: {device_distribution}")
logger.info(f"Total surveys in stats_queryset: {total_count}")
return render(request, 'surveys/instance_list.html', context)
@login_required
def survey_instance_detail(request, pk):
"""
Survey instance detail view with responses.
Features:
- Full survey details
- All responses
- Score breakdown
- Related journey/stage info
- Score comparison with template average
- Related surveys from same patient
"""
survey = get_object_or_404(
SurveyInstance.objects.select_related(
'survey_template',
'patient',
'journey_instance__journey_template'
).prefetch_related(
'responses__question'
),
pk=pk
)
# Get responses
responses = survey.responses.all().order_by('question__order')
# Calculate average score for this survey template
template_average = SurveyInstance.objects.filter(
survey_template=survey.survey_template,
status='completed'
).aggregate(
avg_score=Avg('total_score')
)['avg_score'] or 0
# Get related surveys from the same patient
related_surveys = SurveyInstance.objects.filter(
patient=survey.patient,
status='completed'
).exclude(
id=survey.id
).select_related(
'survey_template'
).order_by('-completed_at')[:5]
# Get response statistics for each question (for choice questions)
question_stats = {}
for response in responses:
if response.question.question_type in ['multiple_choice', 'single_choice']:
choice_responses = SurveyInstance.objects.filter(
survey_template=survey.survey_template,
status='completed'
).values(
f'responses__choice_value'
).annotate(
count=Count('id')
).filter(
responses__question=response.question
).order_by('-count')
question_stats[response.question.id] = {
'type': 'choice',
'options': [
{
'value': opt['responses__choice_value'],
'count': opt['count'],
'percentage': round((opt['count'] / choice_responses.count() * 100) if choice_responses.count() > 0 else 0, 1)
}
for opt in choice_responses
if opt['responses__choice_value']
]
}
elif response.question.question_type == 'rating':
rating_stats = SurveyInstance.objects.filter(
survey_template=survey.survey_template,
status='completed'
).aggregate(
avg_rating=Avg('responses__numeric_value'),
total_responses=Count('responses')
)
question_stats[response.question.id] = {
'type': 'rating',
'average': round(rating_stats['avg_rating'] or 0, 2),
'total_responses': rating_stats['total_responses'] or 0
}
context = {
'survey': survey,
'responses': responses,
'template_average': round(template_average, 2),
'related_surveys': related_surveys,
'question_stats': question_stats,
}
return render(request, 'surveys/instance_detail.html', context)
@login_required
def survey_template_list(request):
"""Survey templates list view"""
queryset = SurveyTemplate.objects.select_related('hospital').prefetch_related('questions')
# Apply RBAC filters
user = request.user
if user.is_px_admin():
pass
elif user.hospital:
queryset = queryset.filter(hospital=user.hospital)
else:
queryset = queryset.none()
# Apply filters
survey_type = request.GET.get('survey_type')
if survey_type:
queryset = queryset.filter(survey_type=survey_type)
hospital_filter = request.GET.get('hospital')
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
is_active = request.GET.get('is_active')
if is_active == 'true':
queryset = queryset.filter(is_active=True)
elif is_active == 'false':
queryset = queryset.filter(is_active=False)
# Ordering
queryset = queryset.order_by('hospital', 'survey_type', 'name')
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
context = {
'page_obj': page_obj,
'templates': page_obj.object_list,
'hospitals': hospitals,
'filters': request.GET,
}
return render(request, 'surveys/template_list.html', context)
@login_required
def survey_template_create(request):
"""Create a new survey template with questions"""
# Check permission
user = request.user
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to create survey templates.")
return redirect('surveys:template_list')
if request.method == 'POST':
form = SurveyTemplateForm(request.POST)
formset = SurveyQuestionFormSet(request.POST)
if form.is_valid() and formset.is_valid():
template = form.save(commit=False)
template.created_by = user
template.save()
questions = formset.save(commit=False)
for question in questions:
question.survey_template = template
question.save()
messages.success(request, "Survey template created successfully.")
return redirect('surveys:template_detail', pk=template.pk)
else:
form = SurveyTemplateForm()
formset = SurveyQuestionFormSet()
context = {
'form': form,
'formset': formset,
}
return render(request, 'surveys/template_form.html', context)
@login_required
def survey_template_detail(request, pk):
"""View survey template details"""
template = get_object_or_404(
SurveyTemplate.objects.select_related('hospital').prefetch_related('questions'),
pk=pk
)
# Check permission
user = request.user
if not user.is_px_admin() and not user.is_hospital_admin():
if user.hospital and template.hospital != user.hospital:
messages.error(request, "You don't have permission to view this template.")
return redirect('surveys:template_list')
# Get statistics
total_instances = template.instances.count()
completed_instances = template.instances.filter(status='completed').count()
negative_instances = template.instances.filter(is_negative=True).count()
avg_score = template.instances.filter(status='completed').aggregate(
avg_score=Avg('total_score')
)['avg_score'] or 0
context = {
'template': template,
'questions': template.questions.all().order_by('order'),
'stats': {
'total_instances': total_instances,
'completed_instances': completed_instances,
'negative_instances': negative_instances,
'completion_rate': round((completed_instances / total_instances * 100) if total_instances > 0 else 0, 1),
'avg_score': round(avg_score, 2),
}
}
return render(request, 'surveys/template_detail.html', context)
@login_required
def survey_template_edit(request, pk):
"""Edit an existing survey template with questions"""
template = get_object_or_404(SurveyTemplate, pk=pk)
# Check permission
user = request.user
if not user.is_px_admin() and not user.is_hospital_admin():
if user.hospital and template.hospital != user.hospital:
messages.error(request, "You don't have permission to edit this template.")
return redirect('surveys:template_list')
if request.method == 'POST':
form = SurveyTemplateForm(request.POST, instance=template)
formset = SurveyQuestionFormSet(request.POST, instance=template)
if form.is_valid() and formset.is_valid():
form.save()
formset.save()
messages.success(request, "Survey template updated successfully.")
return redirect('surveys:template_detail', pk=template.pk)
else:
form = SurveyTemplateForm(instance=template)
formset = SurveyQuestionFormSet(instance=template)
context = {
'form': form,
'formset': formset,
'template': template,
}
return render(request, 'surveys/template_form.html', context)
@login_required
def survey_template_delete(request, pk):
"""Delete a survey template"""
template = get_object_or_404(SurveyTemplate, pk=pk)
# Check permission
user = request.user
if not user.is_px_admin() and not user.is_hospital_admin():
if user.hospital and template.hospital != user.hospital:
messages.error(request, "You don't have permission to delete this template.")
return redirect('surveys:template_list')
if request.method == 'POST':
template_name = template.name
template.delete()
messages.success(request, f"Survey template '{template_name}' deleted successfully.")
return redirect('surveys:template_list')
context = {
'template': template,
}
return render(request, 'surveys/template_confirm_delete.html', context)
@login_required
@require_http_methods(["POST"])
def survey_log_patient_contact(request, pk):
"""
Log patient contact for negative survey.
This records that the user contacted the patient to discuss
the negative survey feedback.
"""
survey = get_object_or_404(SurveyInstance, pk=pk)
# Check permission
user = request.user
if not user.is_px_admin() and not user.is_hospital_admin():
if user.hospital and survey.survey_template.hospital != user.hospital:
messages.error(request, "You don't have permission to modify this survey.")
return redirect('surveys:instance_detail', pk=pk)
# Check if survey is negative
if not survey.is_negative:
messages.warning(request, "This survey is not marked as negative.")
return redirect('surveys:instance_detail', pk=pk)
# Get form data
contact_notes = request.POST.get('contact_notes', '')
issue_resolved = request.POST.get('issue_resolved') == 'on'
if not contact_notes:
messages.error(request, "Please provide contact notes.")
return redirect('surveys:instance_detail', pk=pk)
try:
# Update survey
survey.patient_contacted = True
survey.patient_contacted_at = timezone.now()
survey.patient_contacted_by = user
survey.contact_notes = contact_notes
survey.issue_resolved = issue_resolved
survey.save(update_fields=[
'patient_contacted', 'patient_contacted_at',
'patient_contacted_by', 'contact_notes', 'issue_resolved'
])
# Log audit
AuditService.log_event(
event_type='survey_patient_contacted',
description=f"Patient contacted for negative survey by {user.get_full_name()}",
user=user,
content_object=survey,
metadata={
'contact_notes': contact_notes,
'issue_resolved': issue_resolved,
'survey_score': float(survey.total_score) if survey.total_score else None
}
)
status = "resolved" if issue_resolved else "discussed"
messages.success(request, f"Patient contact logged successfully. Issue marked as {status}.")
except Exception as e:
messages.error(request, f"Error logging patient contact: {str(e)}")
return redirect('surveys:instance_detail', pk=pk)
@login_required
def survey_comments_list(request):
"""
Survey comments list view with AI analysis.
Features:
- Display all survey comments with AI analysis
- Filters (sentiment, survey type, hospital, date range)
- Search by patient MRN
- Server-side pagination
- PatientType display from journey
"""
# Base queryset - only completed surveys with comments
queryset = SurveyInstance.objects.select_related(
'survey_template',
'patient',
'journey_instance__journey_template'
).filter(
status='completed',
comment__isnull=False
).exclude(
comment=''
)
# Apply RBAC filters
user = request.user
if user.is_px_admin():
pass # See all
elif user.is_hospital_admin() and user.hospital:
queryset = queryset.filter(survey_template__hospital=user.hospital)
elif user.hospital:
queryset = queryset.filter(survey_template__hospital=user.hospital)
else:
queryset = queryset.none()
# Apply filters
sentiment_filter = request.GET.get('sentiment')
if sentiment_filter:
queryset = queryset.filter(
comment_analysis__sentiment=sentiment_filter
)
survey_type = request.GET.get('survey_type')
if survey_type:
queryset = queryset.filter(survey_template__survey_type=survey_type)
hospital_filter = request.GET.get('hospital')
if hospital_filter:
queryset = queryset.filter(survey_template__hospital_id=hospital_filter)
# Patient type filter
patient_type_filter = request.GET.get('patient_type')
if patient_type_filter:
# Map filter values to HIS codes
patient_type_map = {
'outpatient': ['1'],
'inpatient': ['2', 'O'],
'emergency': ['3', 'E'],
}
if patient_type_filter in patient_type_map:
codes = patient_type_map[patient_type_filter]
queryset = queryset.filter(
metadata__patient_type__in=codes
)
# Search
search_query = request.GET.get('search')
if search_query:
queryset = queryset.filter(
Q(patient__mrn__icontains=search_query) |
Q(patient__first_name__icontains=search_query) |
Q(patient__last_name__icontains=search_query) |
Q(comment__icontains=search_query)
)
# Date range
date_from = request.GET.get('date_from')
if date_from:
queryset = queryset.filter(completed_at__gte=date_from)
date_to = request.GET.get('date_to')
if date_to:
queryset = queryset.filter(completed_at__lte=date_to)
# Ordering
order_by = request.GET.get('order_by', '-completed_at')
queryset = queryset.order_by(order_by)
# Pagination
page_size = int(request.GET.get('page_size', 25))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
# Statistics
stats_queryset = SurveyInstance.objects.filter(
status='completed',
comment__isnull=False
).exclude(
comment=''
)
# Apply same RBAC filters to stats
if user.is_px_admin():
pass
elif user.is_hospital_admin() and user.hospital:
stats_queryset = stats_queryset.filter(survey_template__hospital=user.hospital)
elif user.hospital:
stats_queryset = stats_queryset.filter(survey_template__hospital=user.hospital)
else:
stats_queryset = stats_queryset.none()
total_count = stats_queryset.count()
positive_count = stats_queryset.filter(
comment_analysis__sentiment='positive'
).count()
negative_count = stats_queryset.filter(
comment_analysis__sentiment='negative'
).count()
neutral_count = stats_queryset.filter(
comment_analysis__sentiment='neutral'
).count()
analyzed_count = stats_queryset.filter(
comment_analyzed=True
).count()
# Patient Type Distribution Chart Data
patient_type_distribution = []
patient_type_labels = []
patient_type_mapping = {
'1': 'Outpatient',
'2': 'Inpatient',
'O': 'Inpatient',
'3': 'Emergency',
'E': 'Emergency',
}
# Count each patient type
outpatient_count = stats_queryset.filter(
metadata__patient_type__in=['1']
).count()
inpatient_count = stats_queryset.filter(
metadata__patient_type__in=['2', 'O']
).count()
emergency_count = stats_queryset.filter(
metadata__patient_type__in=['3', 'E']
).count()
unknown_count = total_count - (outpatient_count + inpatient_count + emergency_count)
patient_type_distribution = [
{'type': 'outpatient', 'label': 'Outpatient', 'count': outpatient_count, 'percentage': round((outpatient_count / total_count * 100) if total_count > 0 else 0, 1)},
{'type': 'inpatient', 'label': 'Inpatient', 'count': inpatient_count, 'percentage': round((inpatient_count / total_count * 100) if total_count > 0 else 0, 1)},
{'type': 'emergency', 'label': 'Emergency', 'count': emergency_count, 'percentage': round((emergency_count / total_count * 100) if total_count > 0 else 0, 1)},
{'type': 'unknown', 'label': 'N/A', 'count': unknown_count, 'percentage': round((unknown_count / total_count * 100) if total_count > 0 else 0, 1)},
]
# Sentiment by Patient Type Chart Data
sentiment_by_patient_type = {
'types': ['Outpatient', 'Inpatient', 'Emergency', 'N/A'],
'positive': [],
'negative': [],
'neutral': [],
}
# Calculate sentiment for each patient type
for pt_type, codes in [('outpatient', ['1']), ('inpatient', ['2', 'O']), ('emergency', ['3', 'E']), ('unknown', [])]:
if codes:
pt_queryset = stats_queryset.filter(metadata__patient_type__in=codes)
else:
pt_queryset = stats_queryset.exclude(
metadata__patient_type__in=['1', '2', 'O', '3', 'E']
)
pt_positive = pt_queryset.filter(comment_analysis__sentiment='positive').count()
pt_negative = pt_queryset.filter(comment_analysis__sentiment='negative').count()
pt_neutral = pt_queryset.filter(comment_analysis__sentiment='neutral').count()
sentiment_by_patient_type['positive'].append(pt_positive)
sentiment_by_patient_type['negative'].append(pt_negative)
sentiment_by_patient_type['neutral'].append(pt_neutral)
# PatientType mapping helper - maps HIS codes to display values
# HIS codes: "1"=Inpatient, "2"/"O"=Outpatient, "3"/"E"=Emergency
PATIENT_TYPE_MAPPING = {
'1': {'label': 'Outpatient', 'icon': 'bi-person-walking', 'color': 'bg-primary'},
'2': {'label': 'Inpatient', 'icon': 'bi-hospital', 'color': 'bg-warning'},
'O': {'label': 'Inpatient', 'icon': 'bi-hospital', 'color': 'bg-warning'},
'3': {'label': 'Emergency', 'icon': 'bi-ambulance', 'color': 'bg-danger'},
'E': {'label': 'Emergency', 'icon': 'bi-ambulance', 'color': 'bg-danger'},
}
def get_patient_type_display(survey):
"""Get patient type display for a survey from HIS metadata"""
# Get patient_type from survey metadata (saved by HIS adapter)
patient_type_code = survey.metadata.get('patient_type')
if patient_type_code:
type_info = PATIENT_TYPE_MAPPING.get(str(patient_type_code))
if type_info:
return {
'code': patient_type_code,
'label': type_info['label'],
'icon': type_info['icon'],
'color': type_info['color']
}
# Fallback: try to get from journey if available
if survey.journey_instance and survey.journey_instance.journey_template:
journey_type = survey.journey_instance.journey_template.journey_type
journey_mapping = {
'opd': {'label': 'Outpatient', 'icon': 'bi-person-walking', 'color': 'bg-primary'},
'inpatient': {'label': 'Inpatient', 'icon': 'bi-hospital', 'color': 'bg-warning'},
'ems': {'label': 'Emergency', 'icon': 'bi-ambulance', 'color': 'bg-danger'},
'day_case': {'label': 'Day Case', 'icon': 'bi-calendar-day', 'color': 'bg-info'},
}
return journey_mapping.get(journey_type, {'code': None, 'label': 'N/A', 'icon': 'bi-question-circle', 'color': 'bg-secondary'})
return {'code': None, 'label': 'N/A', 'icon': 'bi-question-circle', 'color': 'bg-secondary'}
# Add patient type info to surveys
for survey in page_obj.object_list:
survey.patient_type = get_patient_type_display(survey)
stats = {
'total': total_count,
'positive': positive_count,
'negative': negative_count,
'neutral': neutral_count,
'analyzed': analyzed_count,
'unanalyzed': total_count - analyzed_count,
}
# Serialize chart data to JSON
import json
patient_type_distribution_json = json.dumps(patient_type_distribution)
sentiment_by_patient_type_json = json.dumps(sentiment_by_patient_type)
context = {
'page_obj': page_obj,
'surveys': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'filters': request.GET,
# Chart data
'patient_type_distribution': patient_type_distribution,
'sentiment_by_patient_type': sentiment_by_patient_type,
'patient_type_distribution_json': patient_type_distribution_json,
'sentiment_by_patient_type_json': sentiment_by_patient_type_json,
}
return render(request, 'surveys/comment_list.html', context)
@login_required
@require_http_methods(["POST"])
def survey_send_satisfaction_feedback(request, pk):
"""
Send satisfaction feedback form to patient.
This creates and sends a feedback form to assess patient satisfaction
with how their negative survey concerns were addressed.
"""
survey = get_object_or_404(SurveyInstance, pk=pk)
# Check permission
user = request.user
if not user.is_px_admin() and not user.is_hospital_admin():
if user.hospital and survey.survey_template.hospital != user.hospital:
messages.error(request, "You don't have permission to modify this survey.")
return redirect('surveys:instance_detail', pk=pk)
# Check if survey is negative
if not survey.is_negative:
messages.warning(request, "This survey is not marked as negative.")
return redirect('surveys:instance_detail', pk=pk)
# Check if patient was contacted
if not survey.patient_contacted:
messages.error(request, "Please log patient contact before sending satisfaction feedback.")
return redirect('surveys:instance_detail', pk=pk)
# Check if already sent
if survey.satisfaction_feedback_sent:
messages.warning(request, "Satisfaction feedback has already been sent for this survey.")
return redirect('surveys:instance_detail', pk=pk)
try:
# Trigger async task to send satisfaction feedback
send_satisfaction_feedback.delay(str(survey.id), str(user.id))
messages.success(
request,
"Satisfaction feedback form is being sent to the patient. "
"They will receive a link to provide their feedback."
)
except Exception as e:
messages.error(request, f"Error sending satisfaction feedback: {str(e)}")
return redirect('surveys:instance_detail', pk=pk)
@login_required
def manual_survey_send(request):
"""
Manually send a survey to a patient or staff member.
Features:
- Select survey template
- Choose recipient type (patient/staff)
- Search and select recipient
- Choose delivery channel (email/SMS)
- Add custom message (optional)
- Send survey immediately
"""
user = request.user
# Check permission
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to send surveys manually.")
return redirect('surveys:instance_list')
if request.method == 'POST':
form = ManualSurveySendForm(user, request.POST)
if form.is_valid():
try:
# Get form data
survey_template = form.cleaned_data['survey_template']
recipient_type = form.cleaned_data['recipient_type']
recipient_id = form.cleaned_data['recipient']
delivery_channel = form.cleaned_data['delivery_channel']
custom_message = form.cleaned_data.get('custom_message', '')
# Get recipient object
from apps.organizations.models import Patient, Staff
if recipient_type == 'patient':
recipient = get_object_or_404(Patient, pk=recipient_id)
recipient_name = f"{recipient.first_name} {recipient.last_name}"
else:
recipient = get_object_or_404(Staff, pk=recipient_id)
recipient_name = f"{recipient.first_name} {recipient.last_name}"
# Check if recipient has contact info for selected channel
if delivery_channel == 'email':
contact_info = recipient.email
if not contact_info:
messages.error(request, f"{recipient_type.title()} does not have an email address.")
return render(request, 'surveys/manual_send.html', {'form': form})
elif delivery_channel == 'sms':
contact_info = recipient.phone
if not contact_info:
messages.error(request, f"{recipient_type.title()} does not have a phone number.")
return render(request, 'surveys/manual_send.html', {'form': form})
# Create survey instance
from .models import SurveyStatus
survey_instance = SurveyInstance.objects.create(
survey_template=survey_template,
patient=recipient if recipient_type == 'patient' else None,
staff=recipient if recipient_type == 'staff' else None,
hospital=survey_template.hospital,
delivery_channel=delivery_channel,
recipient_email=contact_info if delivery_channel == 'email' else None,
recipient_phone=contact_info if delivery_channel == 'sms' else None,
status=SurveyStatus.PENDING,
metadata={
'sent_manually': True,
'sent_by': str(user.id),
'custom_message': custom_message,
'recipient_type': recipient_type
}
)
# Send survey
success = SurveyDeliveryService.deliver_survey(survey_instance)
if success:
# Log audit
AuditService.log_event(
event_type='survey_sent_manually',
description=f"Survey sent manually to {recipient_name} ({recipient_type}) by {user.get_full_name()}",
user=user,
content_object=survey_instance,
metadata={
'survey_template': survey_template.name,
'recipient_type': recipient_type,
'recipient_id': recipient_id,
'delivery_channel': delivery_channel,
'custom_message': custom_message
}
)
messages.success(
request,
f"Survey sent successfully to {recipient_name} via {delivery_channel.upper()}. "
f"Survey ID: {survey_instance.id}"
)
return redirect('surveys:instance_detail', pk=survey_instance.pk)
else:
messages.error(request, "Failed to send survey. Please try again.")
survey_instance.delete()
return render(request, 'surveys/manual_send.html', {'form': form})
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error sending survey manually: {str(e)}", exc_info=True)
messages.error(request, f"Error sending survey: {str(e)}")
return render(request, 'surveys/manual_send.html', {'form': form})
else:
form = ManualSurveySendForm(user)
context = {
'form': form,
}
return render(request, 'surveys/manual_send.html', context)
@login_required
def survey_analytics_reports(request):
"""
Survey analytics reports management page.
Features:
- List all available reports
- View report details
- Generate new reports
- Download reports
- Delete reports
"""
import os
from django.conf import settings
user = request.user
# Check permission - only admins and hospital admins can view reports
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to view analytics reports.")
return redirect('surveys:instance_list')
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
# Get all reports
reports = []
if os.path.exists(output_dir):
for filename in os.listdir(output_dir):
filepath = os.path.join(output_dir, filename)
if os.path.isfile(filepath):
stat = os.stat(filepath)
report_type = 'unknown'
if filename.endswith('.json'):
report_type = 'json'
elif filename.endswith('.html'):
report_type = 'html'
elif filename.endswith('.md'):
report_type = 'markdown'
reports.append({
'filename': filename,
'type': report_type,
'size': stat.st_size,
'size_human': _human_readable_size(stat.st_size),
'created': stat.st_ctime,
'created_date': datetime.fromtimestamp(stat.st_ctime),
'modified': stat.st_mtime,
})
# Sort by creation date (newest first)
reports.sort(key=lambda x: x['created'], reverse=True)
# Statistics
total_reports = len(reports)
json_reports = len([r for r in reports if r['type'] == 'json'])
html_reports = len([r for r in reports if r['type'] == 'html'])
markdown_reports = len([r for r in reports if r['type'] == 'markdown'])
context = {
'reports': reports,
'stats': {
'total': total_reports,
'json': json_reports,
'html': html_reports,
'markdown': markdown_reports,
},
}
return render(request, 'surveys/analytics_reports.html', context)
@login_required
def survey_analytics_report_view(request, filename):
"""
View a specific survey analytics report.
Features:
- Display HTML reports in browser
- Provide download links for all formats
- Show report metadata
"""
import os
from django.conf import settings
user = request.user
# Check permission
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to view analytics reports.")
return redirect('surveys:analytics_reports')
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
filepath = os.path.join(output_dir, filename)
# Security check - ensure file is in reports directory
if not os.path.abspath(filepath).startswith(os.path.abspath(output_dir)):
messages.error(request, "Invalid report file.")
return redirect('surveys:analytics_reports')
if not os.path.exists(filepath):
messages.error(request, "Report file not found.")
return redirect('surveys:analytics_reports')
# Determine report type
report_type = 'unknown'
if filename.endswith('.json'):
report_type = 'json'
elif filename.endswith('.html'):
report_type = 'html'
elif filename.endswith('.md'):
report_type = 'markdown'
# Get file info
stat = os.stat(filepath)
# For HTML files, render them directly
if report_type == 'html':
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
context = {
'filename': filename,
'content': content,
'report_type': report_type,
'size': stat.st_size,
'size_human': _human_readable_size(stat.st_size),
'created_date': datetime.fromtimestamp(stat.st_ctime),
'modified_date': datetime.fromtimestamp(stat.st_mtime),
}
return render(request, 'surveys/analytics_report_view.html', context)
# For JSON and Markdown files, show info and provide download
context = {
'filename': filename,
'report_type': report_type,
'size': stat.st_size,
'size_human': _human_readable_size(stat.st_size),
'created_date': datetime.fromtimestamp(stat.st_ctime),
'modified_date': datetime.fromtimestamp(stat.st_mtime),
}
return render(request, 'surveys/analytics_report_info.html', context)
@login_required
def survey_analytics_report_download(request, filename):
"""
Download a survey analytics report file.
"""
import os
from django.conf import settings
from django.http import FileResponse
user = request.user
# Check permission
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to download analytics reports.")
return redirect('surveys:analytics_reports')
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
filepath = os.path.join(output_dir, filename)
# Security check
if not os.path.abspath(filepath).startswith(os.path.abspath(output_dir)):
messages.error(request, "Invalid report file.")
return redirect('surveys:analytics_reports')
if not os.path.exists(filepath):
messages.error(request, "Report file not found.")
return redirect('surveys:analytics_reports')
# Determine content type
content_type = 'application/octet-stream'
if filename.endswith('.html'):
content_type = 'text/html'
elif filename.endswith('.json'):
content_type = 'application/json'
elif filename.endswith('.md'):
content_type = 'text/markdown'
# Send file
return FileResponse(
open(filepath, 'rb'),
content_type=content_type,
as_attachment=True,
filename=filename
)
@login_required
def survey_analytics_report_view_inline(request, filename):
"""
View a survey analytics report inline in the browser.
Unlike download, this displays the file content directly in the browser
without forcing a download. Works for HTML, JSON, and Markdown files.
"""
import os
from django.conf import settings
from django.http import FileResponse, HttpResponse
user = request.user
# Check permission
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to view analytics reports.")
return redirect('surveys:analytics_reports')
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
filepath = os.path.join(output_dir, filename)
# Security check
if not os.path.abspath(filepath).startswith(os.path.abspath(output_dir)):
messages.error(request, "Invalid report file.")
return redirect('surveys:analytics_reports')
if not os.path.exists(filepath):
messages.error(request, "Report file not found.")
return redirect('surveys:analytics_reports')
# Determine content type
content_type = 'application/octet-stream'
if filename.endswith('.html'):
content_type = 'text/html'
elif filename.endswith('.json'):
content_type = 'application/json'
elif filename.endswith('.md'):
content_type = 'text/plain; charset=utf-8' # Plain text for markdown
# For HTML and JSON, serve inline with FileResponse
if filename.endswith(('.html', '.json')):
response = FileResponse(
open(filepath, 'rb'),
content_type=content_type,
as_attachment=False # Display inline
)
# Add CSP header to allow inline scripts for HTML reports
if filename.endswith('.html'):
response['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net cdnjs.cloudflare.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com;"
return response
# For Markdown, render it in a template with viewer
elif filename.endswith('.md'):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
stat = os.stat(filepath)
context = {
'filename': filename,
'content': content,
'report_type': 'markdown',
'size': stat.st_size,
'size_human': _human_readable_size(stat.st_size),
'created_date': datetime.fromtimestamp(stat.st_ctime),
'modified_date': datetime.fromtimestamp(stat.st_mtime),
}
return render(request, 'surveys/analytics_report_markdown_view.html', context)
# For other files, fallback to download
return FileResponse(
open(filepath, 'rb'),
content_type=content_type,
as_attachment=True,
filename=filename
)
@login_required
@require_http_methods(["POST"])
def survey_analytics_report_delete(request, filename):
"""
Delete a survey analytics report file.
"""
import os
from django.conf import settings
user = request.user
# Check permission - only PX admins can delete reports
if not user.is_px_admin():
messages.error(request, "You don't have permission to delete analytics reports.")
return redirect('surveys:analytics_reports')
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
filepath = os.path.join(output_dir, filename)
# Security check
if not os.path.abspath(filepath).startswith(os.path.abspath(output_dir)):
messages.error(request, "Invalid report file.")
return redirect('surveys:analytics_reports')
if not os.path.exists(filepath):
messages.error(request, "Report file not found.")
return redirect('surveys:analytics_reports')
try:
os.remove(filepath)
messages.success(request, f"Report '{filename}' deleted successfully.")
except Exception as e:
messages.error(request, f"Error deleting report: {str(e)}")
return redirect('surveys:analytics_reports')
def _human_readable_size(size_bytes):
"""Convert bytes to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} TB"
# ============================================================================
# ENHANCED SURVEY REPORTS - Separate reports per survey type
# ============================================================================
@login_required
def enhanced_survey_reports_list(request):
"""
List all enhanced survey report directories.
"""
from django.conf import settings
import os
user = request.user
# Check permission
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to view analytics reports.")
return redirect('surveys:analytics_reports')
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
# Find all report directories (folders starting with "reports_")
report_sets = []
if os.path.exists(output_dir):
for item in sorted(os.listdir(output_dir), reverse=True):
item_path = os.path.join(output_dir, item)
if os.path.isdir(item_path) and item.startswith('reports_'):
# Check if it has an index.html
index_path = os.path.join(item_path, 'index.html')
if os.path.exists(index_path):
stat = os.stat(item_path)
# Count individual reports
report_count = len([f for f in os.listdir(item_path) if f.endswith('.html') and f != 'index.html'])
report_sets.append({
'dir_name': item,
'created': datetime.fromtimestamp(stat.st_ctime),
'modified': datetime.fromtimestamp(stat.st_mtime),
'report_count': report_count,
'size': sum(os.path.getsize(os.path.join(item_path, f)) for f in os.listdir(item_path) if os.path.isfile(os.path.join(item_path, f)))
})
context = {
'report_sets': report_sets,
}
return render(request, 'surveys/enhanced_reports_list.html', context)
@login_required
def enhanced_survey_report_view(request, dir_name):
"""
View the index of an enhanced report set.
"""
from django.conf import settings
import os
user = request.user
# Check permission
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to view analytics reports.")
return redirect('surveys:analytics_reports')
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
dir_path = os.path.join(output_dir, dir_name)
# Security check
if not os.path.abspath(dir_path).startswith(os.path.abspath(output_dir)):
messages.error(request, "Invalid report directory.")
return redirect('surveys:enhanced_reports_list')
if not os.path.exists(dir_path):
messages.error(request, "Report directory not found.")
return redirect('surveys:enhanced_reports_list')
# Serve the index.html file
index_path = os.path.join(dir_path, 'index.html')
if os.path.exists(index_path):
with open(index_path, 'r', encoding='utf-8') as f:
content = f.read()
return HttpResponse(content, content_type='text/html')
messages.error(request, "Index file not found.")
return redirect('surveys:enhanced_reports_list')
@login_required
def enhanced_survey_report_file(request, dir_name, filename):
"""
View/serve an individual enhanced report file.
"""
from django.conf import settings
import os
from django.http import FileResponse
user = request.user
# Check permission
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to view analytics reports.")
return redirect('surveys:analytics_reports')
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
dir_path = os.path.join(output_dir, dir_name)
filepath = os.path.join(dir_path, filename)
# Security check
if not os.path.abspath(filepath).startswith(os.path.abspath(dir_path)):
messages.error(request, "Invalid file path.")
return redirect('surveys:enhanced_reports_list')
if not os.path.exists(filepath):
messages.error(request, "File not found.")
return redirect('surveys:enhanced_reports_list')
# Serve file based on type
if filename.endswith('.html'):
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
response = HttpResponse(content, content_type='text/html')
# Allow inline scripts/CDN resources
response['Content-Security-Policy'] = "default-src 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net cdnjs.cloudflare.com unpkg.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net unpkg.com; style-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.cloudflare.com fonts.googleapis.com; font-src 'self' fonts.gstatic.com;"
return response
elif filename.endswith('.json'):
return FileResponse(open(filepath, 'rb'), content_type='application/json')
else:
return FileResponse(open(filepath, 'rb'), as_attachment=True)
@login_required
def generate_enhanced_report_ui(request):
"""
UI view to generate enhanced reports with form.
"""
from apps.surveys.models import SurveyTemplate
from django.conf import settings
user = request.user
# Check permission
if not user.is_px_admin() and not user.is_hospital_admin():
messages.error(request, "You don't have permission to generate analytics reports.")
return redirect('surveys:analytics_reports')
if request.method == 'POST':
template_id = request.POST.get('template')
start_date = request.POST.get('start_date')
end_date = request.POST.get('end_date')
try:
# Get template name if specified
template_name = None
if template_id:
try:
template = SurveyTemplate.objects.get(id=template_id)
template_name = template.name
except SurveyTemplate.DoesNotExist:
pass
# Parse dates
start_dt = None
end_dt = None
if start_date:
start_dt = datetime.strptime(start_date, '%Y-%m-%d').date()
if end_date:
end_dt = datetime.strptime(end_date, '%Y-%m-%d').date()
# Generate enhanced reports
from .analytics_utils import generate_enhanced_survey_reports
result = generate_enhanced_survey_reports(
template_name=template_name,
start_date=start_dt,
end_date=end_dt
)
messages.success(
request,
f"Generated {len(result['individual_reports'])} reports successfully!"
)
# Redirect to the report directory
dir_name = os.path.basename(result['reports_dir'])
return redirect('surveys:enhanced_report_view', dir_name=dir_name)
except Exception as e:
messages.error(request, f"Error generating reports: {str(e)}")
return redirect('surveys:enhanced_reports_list')
# GET request - show form
templates = SurveyTemplate.objects.filter(is_active=True).order_by('name')
context = {
'templates': templates,
}
return render(request, 'surveys/generate_enhanced_report.html', context)