HH/apps/surveys/analytics_views.py

524 lines
18 KiB
Python

"""
Survey analytics API views.
Provides endpoints for accessing survey engagement metrics:
- Engagement statistics
- Patient survey timelines
- Completion times
- Abandonment analysis
- Hourly activity
"""
import json
import os
from datetime import datetime
from django.conf import settings
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.http import FileResponse, HttpResponse
from .analytics import (
get_survey_engagement_stats,
get_patient_survey_timeline,
get_survey_completion_times,
get_survey_abandonment_analysis,
get_hourly_survey_activity,
)
from .analytics_utils import (
calculate_survey_analytics,
generate_markdown_report,
generate_html_report,
generate_enhanced_survey_reports,
generate_single_template_report,
)
from .models import SurveyInstance, SurveyTracking
from .serializers import SurveyInstanceSerializer, SurveyTrackingSerializer
class SurveyAnalyticsViewSet(viewsets.ViewSet):
"""
Survey analytics API.
Provides various metrics about survey engagement:
- Engagement rates (open, completion, abandonment)
- Time metrics (time to complete, time spent)
- Patient timelines
- Abandonment patterns
- Hourly activity
"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def engagement_stats(self, request):
"""
Get overall survey engagement statistics.
Query params:
- survey_template_id: Filter by survey template (optional)
- hospital_id: Filter by hospital (optional)
- days: Number of days to look back (default: 30)
"""
survey_template_id = request.query_params.get('survey_template_id')
hospital_id = request.query_params.get('hospital_id')
days = int(request.query_params.get('days', 30))
try:
stats = get_survey_engagement_stats(
survey_template_id=survey_template_id,
hospital_id=hospital_id,
days=days
)
return Response(stats)
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def patient_timeline(self, request):
"""
Get survey timeline for a specific patient.
Query params:
- patient_id: Required - Patient ID
"""
patient_id = request.query_params.get('patient_id')
if not patient_id:
return Response(
{'error': 'patient_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
timeline = get_patient_survey_timeline(patient_id)
return Response({'timeline': timeline})
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def completion_times(self, request):
"""
Get individual survey completion times.
Query params:
- survey_template_id: Filter by survey template (optional)
- hospital_id: Filter by hospital (optional)
- days: Number of days to look back (default: 30)
"""
survey_template_id = request.query_params.get('survey_template_id')
hospital_id = request.query_params.get('hospital_id')
days = int(request.query_params.get('days', 30))
try:
completion_times = get_survey_completion_times(
survey_template_id=survey_template_id,
hospital_id=hospital_id,
days=days
)
return Response({'completion_times': completion_times})
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def abandonment_analysis(self, request):
"""
Analyze abandoned surveys to identify patterns.
Query params:
- survey_template_id: Filter by survey template (optional)
- hospital_id: Filter by hospital (optional)
- days: Number of days to look back (default: 30)
"""
survey_template_id = request.query_params.get('survey_template_id')
hospital_id = request.query_params.get('hospital_id')
days = int(request.query_params.get('days', 30))
try:
analysis = get_survey_abandonment_analysis(
survey_template_id=survey_template_id,
hospital_id=hospital_id,
days=days
)
return Response(analysis)
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def hourly_activity(self, request):
"""
Get survey activity by hour.
Query params:
- hospital_id: Filter by hospital (optional)
- days: Number of days to look back (default: 7)
"""
hospital_id = request.query_params.get('hospital_id')
days = int(request.query_params.get('days', 7))
try:
activity = get_hourly_survey_activity(
hospital_id=hospital_id,
days=days
)
return Response({'activity': activity})
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def summary_dashboard(self, request):
"""
Get a comprehensive summary dashboard with all key metrics.
Query params:
- hospital_id: Filter by hospital (optional)
- days: Number of days to look back (default: 30)
"""
hospital_id = request.query_params.get('hospital_id')
days = int(request.query_params.get('days', 30))
try:
# Get engagement stats
engagement = get_survey_engagement_stats(
hospital_id=hospital_id,
days=days
)
# Get abandonment analysis
abandonment = get_survey_abandonment_analysis(
hospital_id=hospital_id,
days=days
)
# Get top 10 fastest completions
fastest_completions = get_survey_completion_times(
hospital_id=hospital_id,
days=days
)[:10]
# Get top 10 slowest completions
slowest_completions = get_survey_completion_times(
hospital_id=hospital_id,
days=days
)[-10:] if len(get_survey_completion_times(hospital_id=hospital_id, days=days)) > 10 else []
return Response({
'engagement': engagement,
'abandonment': abandonment,
'fastest_completions': fastest_completions,
'slowest_completions': slowest_completions,
})
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['post'])
def generate_report(self, request):
"""
Generate comprehensive survey analytics report.
Request body (JSON):
{
"template": "Survey template name (optional)",
"start_date": "YYYY-MM-DD (optional)",
"end_date": "YYYY-MM-DD (optional)",
"format": "json|html|markdown (optional, default: json)",
"save": true/false (optional, default: false)
}
Returns:
- If format=json: Returns analytics data as JSON
- If format=html: Returns HTML content
- If format=markdown: Returns Markdown content
- If save=true: Also saves to file and returns file path
"""
try:
data = request.data
template = data.get('template')
start_date = data.get('start_date')
end_date = data.get('end_date')
report_format = data.get('format', 'json')
save_to_file = data.get('save', False)
# Parse dates if provided
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 analytics data
analytics_data = calculate_survey_analytics(
template_name=template,
start_date=start_dt,
end_date=end_dt
)
# Format output
if report_format == 'json':
response_data = {
'success': True,
'data': analytics_data
}
if save_to_file:
# Save to JSON file
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
os.makedirs(output_dir, exist_ok=True)
filepath = os.path.join(output_dir, f'survey_analytics_data_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json')
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(analytics_data, f, indent=2, default=str)
response_data['saved_to'] = filepath
return Response(response_data)
elif report_format == 'html':
# Generate HTML report
html_content = generate_html_report(analytics_data)
response_data = {
'success': True,
'content': html_content
}
if save_to_file:
# Save to HTML file
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
os.makedirs(output_dir, exist_ok=True)
filepath = os.path.join(output_dir, f'survey_analytics_report_{datetime.now().strftime("%Y%m%d_%H%M%S")}.html')
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html_content)
response_data['saved_to'] = filepath
return Response(response_data)
elif report_format == 'markdown':
# Generate Markdown report
markdown_content = generate_markdown_report(analytics_data)
response_data = {
'success': True,
'content': markdown_content
}
if save_to_file:
# Save to Markdown file
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
os.makedirs(output_dir, exist_ok=True)
filepath = os.path.join(output_dir, f'survey_analytics_report_{datetime.now().strftime("%Y%m%d_%H%M%S")}.md')
with open(filepath, 'w', encoding='utf-8') as f:
f.write(markdown_content)
response_data['saved_to'] = filepath
return Response(response_data)
else:
return Response(
{'error': f'Invalid format: {report_format}. Use json, html, or markdown.'},
status=status.HTTP_400_BAD_REQUEST
)
except Exception as e:
import traceback
traceback.print_exc()
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def available_reports(self, request):
"""
List available survey analytics reports.
Returns list of JSON, HTML, and Markdown reports in the reports directory.
"""
try:
output_dir = getattr(settings, 'SURVEY_REPORTS_DIR', 'reports')
if not os.path.exists(output_dir):
return Response({
'success': True,
'reports': []
})
reports = []
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,
'created': datetime.fromtimestamp(stat.st_ctime).isoformat(),
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
})
# Sort by creation date (newest first)
reports.sort(key=lambda x: x['created'], reverse=True)
return Response({
'success': True,
'reports': reports
})
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['post'])
def generate_enhanced_reports(self, request):
"""
Generate enhanced survey analytics reports - one per survey template.
Creates separate HTML reports for each survey template with:
- Detailed question-level analysis
- Individual charts per question
- Performance insights and recommendations
- Master index page linking all reports
Request body (JSON):
{
"template": "Survey template name (optional - if not provided, generates for all)",
"start_date": "YYYY-MM-DD (optional)",
"end_date": "YYYY-MM-DD (optional)"
}
Returns:
{
"success": true,
"reports_dir": "/path/to/reports/reports_20240208_123045",
"index_url": "/surveys/reports/reports_20240208_123045/index.html",
"reports": [
{
"template_name": "Inpatient Satisfaction",
"filename": "inpatient_satisfaction.html",
"size": 15234
}
]
}
"""
try:
data = request.data
template = data.get('template')
start_date = data.get('start_date')
end_date = data.get('end_date')
# Parse dates if provided
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
result = generate_enhanced_survey_reports(
template_name=template,
start_date=start_dt,
end_date=end_dt
)
# Build URLs for accessing reports
import os
from django.conf import settings
reports_dir_name = os.path.basename(result['reports_dir'])
return Response({
'success': True,
'message': f"Generated {len(result['individual_reports'])} reports",
'reports_dir': result['reports_dir'],
'index_path': result['index_path'],
'summary_path': result['summary_path'],
'reports': result['individual_reports'],
'view_url': f"/surveys/enhanced-reports/{reports_dir_name}/"
})
except Exception as e:
import traceback
traceback.print_exc()
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
class SurveyTrackingViewSet(viewsets.ReadOnlyModelViewSet):
"""
Survey tracking events API.
View detailed tracking events for surveys:
- Page views
- Completions
- Abandonments
- Device/browser information
"""
queryset = SurveyTracking.objects.select_related(
'survey_instance',
'survey_instance__patient',
'survey_instance__survey_template'
).order_by('-created_at')
serializer_class = SurveyTrackingSerializer
permission_classes = [IsAuthenticated]
filterset_fields = ['survey_instance', 'event_type', 'device_type', 'browser']
search_fields = ['ip_address', 'user_agent']
@action(detail=False, methods=['get'])
def by_survey(self, request):
"""
Get tracking events for a specific survey instance.
Query params:
- survey_instance_id: Required - SurveyInstance ID
"""
survey_instance_id = request.query_params.get('survey_instance_id')
if not survey_instance_id:
return Response(
{'error': 'survey_instance_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
try:
tracking_events = self.queryset.filter(
survey_instance_id=survey_instance_id
)
# Serialize and return
serializer = self.get_serializer(tracking_events, many=True)
return Response(serializer.data)
except Exception as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)