524 lines
18 KiB
Python
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
|
|
)
|