""" Public survey views - Token-based survey forms (no login required) """ from django.contrib import messages from django.db.models import Q from django.http import JsonResponse 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.views.decorators.csrf import csrf_exempt from user_agents import parse from apps.core.services import AuditService from .models import SurveyInstance, SurveyQuestion, SurveyResponse, SurveyTracking from .analytics import track_survey_open, track_survey_completion @require_http_methods(["GET", "POST"]) def survey_form(request, token): """ Public survey form - accessible via secure token link. Features: - No login required - Token-based access - Mobile-first responsive design - Bilingual support (AR/EN) - Progress indicator - Question type rendering - Form validation """ # Get survey instance by token # Allow access until survey is completed or token expires (2 days by default) try: survey = ( SurveyInstance.objects.select_related("survey_template", "patient", "journey_instance") .prefetch_related("survey_template__questions") .get( access_token=token, status__in=["pending", "sent", "viewed", "in_progress"], token_expires_at__gt=timezone.now(), ) ) except SurveyInstance.DoesNotExist: return render(request, "surveys/invalid_token.html", {"error": "invalid_or_expired"}) # Track survey open - increment count and record tracking event # Get device info from user agent user_agent_str = request.META.get("HTTP_USER_AGENT", "") ip_address = request.META.get("REMOTE_ADDR", "") # Parse user agent for device info user_agent = parse(user_agent_str) device_type = "mobile" if user_agent.is_mobile else ("tablet" if user_agent.is_tablet else "desktop") browser = f"{user_agent.browser.family} {user_agent.browser.version_string}" # Update survey instance tracking fields survey.open_count += 1 survey.last_opened_at = timezone.now() # Update status based on current state if not survey.opened_at: survey.opened_at = timezone.now() survey.status = "viewed" elif survey.status == "sent": survey.status = "viewed" survey.save(update_fields=["open_count", "last_opened_at", "opened_at", "status"]) # Track page view event SurveyTracking.track_event( survey, "page_view", user_agent=user_agent_str[:500] if user_agent_str else "", ip_address=ip_address, device_type=device_type, browser=browser, metadata={ "referrer": request.META.get("HTTP_REFERER", ""), "language": request.GET.get("lang", "en"), }, ) # Get questions — filter by patient's experienced events patient_events = set(survey.metadata.get("event_types", [])) questions = survey.survey_template.questions.filter(Q(is_base=True) | Q(event_type__in=patient_events)).order_by( "order" ) if request.method == "POST": # Process survey responses language = request.POST.get("language", "en") errors = [] responses_data = [] # Validate and collect responses for question in questions: field_name = f"question_{question.id}" # Check if required if question.is_required and not request.POST.get(field_name): errors.append(f"Question {question.order + 1} is required") continue # Get response value based on question type if question.question_type in ["rating", "likert"]: numeric_value = request.POST.get(field_name) if numeric_value: responses_data.append( { "question": question, "numeric_value": float(numeric_value), "text_value": "", "choice_value": "", } ) elif question.question_type == "nps": numeric_value = request.POST.get(field_name) if numeric_value: responses_data.append( { "question": question, "numeric_value": float(numeric_value), "text_value": "", "choice_value": "", } ) elif question.question_type == "yes_no": choice_value = request.POST.get(field_name) if choice_value: # Convert yes/no to numeric for scoring numeric_value = 5.0 if choice_value == "yes" else 1.0 responses_data.append( { "question": question, "numeric_value": numeric_value, "text_value": "", "choice_value": choice_value, } ) elif question.question_type == "multiple_choice": choice_value = request.POST.get(field_name) if choice_value: # Find the selected choice to get its label selected_choice = None for choice in question.choices_json: if str(choice.get("value", "")) == str(choice_value): selected_choice = choice break # Get the label based on language language = request.POST.get("language", "en") if language == "ar" and selected_choice and selected_choice.get("label_ar"): text_value = selected_choice["label_ar"] elif selected_choice and selected_choice.get("label"): text_value = selected_choice["label"] else: text_value = choice_value # Try to convert choice value to numeric for scoring try: numeric_value = float(choice_value) except (ValueError, TypeError): numeric_value = None responses_data.append( { "question": question, "numeric_value": numeric_value, "text_value": text_value, "choice_value": choice_value, } ) elif question.question_type in ["text", "textarea"]: text_value = request.POST.get(field_name, "") if text_value: responses_data.append( {"question": question, "numeric_value": None, "text_value": text_value, "choice_value": ""} ) # Get optional comment comment = request.POST.get("comment", "").strip() # If validation errors, show form again if errors: context = { "survey": survey, "questions": questions, "errors": errors, "language": language, } return render(request, "surveys/public_form.html", context) # Save responses for response_data in responses_data: SurveyResponse.objects.update_or_create( survey_instance=survey, question=response_data["question"], defaults={ "numeric_value": response_data["numeric_value"], "text_value": response_data["text_value"], "choice_value": response_data["choice_value"], }, ) # Update survey status survey.status = "completed" survey.completed_at = timezone.now() # Calculate time spent (from opened_at to completed_at) if survey.opened_at: time_spent = (timezone.now() - survey.opened_at).total_seconds() survey.time_spent_seconds = int(time_spent) survey.save(update_fields=["status", "completed_at", "time_spent_seconds"]) # Track completion event SurveyTracking.track_event( survey, "survey_completed", total_time_spent=survey.time_spent_seconds, user_agent=user_agent_str[:500] if user_agent_str else "", ip_address=ip_address, metadata={ "response_count": len(responses_data), "language": language, }, ) # Calculate score score = survey.calculate_score() # Log completion AuditService.log_event( event_type="survey_completed", description=f"Survey completed: {survey.survey_template.name}", user=None, content_object=survey, metadata={ "score": float(score) if score else None, "is_negative": survey.is_negative, "response_count": len(responses_data), "has_comment": bool(comment), }, ) # Save comment and trigger AI analysis if present if comment: survey.comment = comment survey.save(update_fields=["comment"]) # Trigger background task for comment analysis from apps.surveys.tasks import analyze_survey_comment try: analyze_survey_comment.delay(str(survey.id)) except Exception as e: # Log but don't fail the survey submission import logging logger = logging.getLogger(__name__) logger.error(f"Failed to trigger comment analysis: {str(e)}") # Create PX action if negative if survey.is_negative: from apps.surveys.tasks import create_action_from_negative_survey try: create_action_from_negative_survey.delay(str(survey.id)) except Exception as e: # Log but don't fail the survey submission import logging logger = logging.getLogger(__name__) logger.error(f"Failed to trigger action creation: {str(e)}") # Redirect to thank you page return redirect("surveys:thank_you", token=token) # GET request - show form # Determine language from query param or browser language = request.GET.get("lang", "en") context = { "survey": survey, "questions": questions, "language": language, "total_questions": questions.count(), } return render(request, "surveys/public_form.html", context) def thank_you(request, token): """Thank you page after survey completion""" try: survey = SurveyInstance.objects.select_related("survey_template", "patient").get( access_token=token, status="completed" ) except SurveyInstance.DoesNotExist: return render(request, "surveys/invalid_token.html", {"error": "not_found"}) language = request.GET.get("lang", "en") context = { "survey": survey, "language": language, } return render(request, "surveys/thank_you.html", context) def invalid_token(request): """Invalid or expired token page""" return render(request, "surveys/invalid_token.html") @csrf_exempt @require_http_methods(["POST"]) def track_survey_start(request, token): """ API endpoint to track when patient starts answering survey. Called via AJAX when patient first interacts with the form. Updates status from 'viewed' to 'in_progress'. """ try: # Get survey instance survey = SurveyInstance.objects.get( access_token=token, status__in=["viewed", "in_progress"], token_expires_at__gt=timezone.now() ) # Only update if not already in_progress if survey.status == "viewed": survey.status = "in_progress" survey.save(update_fields=["status"]) # Track survey started event SurveyTracking.track_event( survey, "survey_started", user_agent=request.META.get("HTTP_USER_AGENT", "")[:500] if request.META.get("HTTP_USER_AGENT") else "", ip_address=request.META.get("REMOTE_ADDR", ""), metadata={ "referrer": request.META.get("HTTP_REFERER", ""), }, ) return JsonResponse( { "status": "success", "survey_status": survey.status, } ) except SurveyInstance.DoesNotExist: return JsonResponse({"status": "error", "message": "Survey not found or invalid token"}, status=404)