""" Surveys serializers """ from rest_framework import serializers from .models import ( SurveyInstance, SurveyQuestion, SurveyResponse, SurveyTemplate, SurveyTracking, ) class SurveyQuestionSerializer(serializers.ModelSerializer): """Survey question serializer""" class Meta: model = SurveyQuestion fields = [ "id", "survey_template", "text", "text_ar", "question_type", "order", "is_required", "is_base", "event_type", "choices_json", "created_at", "updated_at", ] read_only_fields = ["id", "created_at", "updated_at"] class SurveyTemplateSerializer(serializers.ModelSerializer): """Survey template serializer""" hospital_name = serializers.CharField(source="hospital.name", read_only=True) questions = SurveyQuestionSerializer(many=True, read_only=True) question_count = serializers.SerializerMethodField() class Meta: model = SurveyTemplate fields = [ "id", "name", "name_ar", "hospital", "hospital_name", "survey_type", "scoring_method", "negative_threshold", "is_active", "questions", "question_count", "created_at", "updated_at", ] read_only_fields = ["id", "created_at", "updated_at"] def get_question_count(self, obj): """Get number of questions""" return obj.get_question_count() class SurveyResponseSerializer(serializers.ModelSerializer): """Survey response serializer""" question_text = serializers.SerializerMethodField() question_type = serializers.SerializerMethodField() class Meta: model = SurveyResponse fields = [ "id", "survey_instance", "question", "question_text", "question_type", "numeric_value", "text_value", "choice_value", "response_time_seconds", "created_at", "updated_at", ] read_only_fields = ["id", "created_at", "updated_at"] def get_question_text(self, obj): if obj.question: return obj.question.text return "" def get_question_type(self, obj): if obj.question: return obj.question.question_type return "" class SurveyInstanceSerializer(serializers.ModelSerializer): """Survey instance serializer""" survey_template_name = serializers.SerializerMethodField() recipient_name = serializers.CharField(source="get_recipient_name", read_only=True) patient_name = serializers.CharField(source="patient.get_full_name", read_only=True) patient_mrn = serializers.CharField(source="patient.mrn", read_only=True) staff_name = serializers.CharField(source="staff.get_full_name", read_only=True) staff_email = serializers.EmailField(source="staff.email", read_only=True) responses = SurveyResponseSerializer(many=True, read_only=True) survey_url = serializers.SerializerMethodField() comment_analysis = serializers.JSONField(read_only=True) class Meta: model = SurveyInstance fields = [ "id", "survey_template", "survey_template_name", "patient", "patient_name", "patient_mrn", "staff", "staff_name", "staff_email", "recipient_name", "journey_instance", "encounter_id", "delivery_channel", "recipient_phone", "recipient_email", "access_token", "token_expires_at", "survey_url", "status", "sent_at", "opened_at", "completed_at", "total_score", "is_negative", "comment", "comment_analyzed", "comment_analysis", "responses", "metadata", "created_at", "updated_at", ] read_only_fields = [ "id", "access_token", "token_expires_at", "sent_at", "opened_at", "completed_at", "total_score", "is_negative", "created_at", "updated_at", ] def get_survey_template_name(self, obj): if obj.survey_template: return obj.survey_template.name return obj.metadata.get("patient_type", "Event-based Survey") def validate(self, data): """Validate that exactly one of patient or staff is set""" patient = data.get("patient") staff = data.get("staff") # If we're updating, get existing values if self.instance: patient = patient if patient is not None else self.instance.patient staff = staff if staff is not None else self.instance.staff if patient and staff: raise serializers.ValidationError( { "patient": "Cannot specify both patient and staff for a survey", "staff": "Cannot specify both patient and staff for a survey", } ) if not patient and not staff: raise serializers.ValidationError( { "patient": "Must specify either a patient or staff recipient", "staff": "Must specify either a patient or staff recipient", } ) return data def get_survey_url(self, obj): """Get survey URL""" return obj.get_survey_url() class SurveySubmissionSerializer(serializers.Serializer): """ Serializer for submitting survey responses. Used by public survey form. """ responses = serializers.ListField( child=serializers.DictField(), help_text="Array of {question_id, numeric_value, text_value, choice_value}" ) comment = serializers.CharField( required=False, allow_blank=True, allow_null=True, help_text="Optional patient comment about their experience" ) def validate_responses(self, value): """Validate responses""" if not value: raise serializers.ValidationError("At least one response is required") for response in value: if "question_id" not in response: raise serializers.ValidationError("Each response must have question_id") return value def create(self, validated_data): """ Create survey responses and calculate score. This is called when a patient submits the survey. Uses question_id to reference template questions. """ survey_instance = self.context["survey_instance"] responses_data = validated_data["responses"] from apps.surveys.models import SurveyResponse, SurveyQuestion from django.utils import timezone for response_data in responses_data: question_id = response_data.get("question_id") choice_value = response_data.get("choice_value", "") numeric_value = response_data.get("numeric_value") text_value = response_data.get("text_value", "") if question_id: try: question = SurveyQuestion.objects.get(id=question_id) if choice_value and numeric_value is None: for choice in question.choices_json or []: if str(choice.get("value", "")) == str(choice_value): if not text_value: text_value = choice.get("label", choice_value) break try: numeric_value = float(choice_value) except (ValueError, TypeError): pass SurveyResponse.objects.create( survey_instance=survey_instance, question=question, numeric_value=numeric_value, text_value=text_value, choice_value=choice_value, response_time_seconds=response_data.get("response_time_seconds"), ) except SurveyQuestion.DoesNotExist: pass # Update survey instance survey_instance.status = "completed" survey_instance.completed_at = timezone.now() survey_instance.save() # Calculate score survey_instance.calculate_score() # Save optional comment if provided if "comment" in validated_data and validated_data["comment"]: survey_instance.comment = validated_data["comment"].strip() survey_instance.save(update_fields=["comment"]) # Queue processing task from apps.surveys.tasks import process_survey_completion process_survey_completion.delay(str(survey_instance.id)) return survey_instance class SurveyTrackingSerializer(serializers.ModelSerializer): """ Survey tracking events serializer. Tracks detailed engagement metrics for surveys. """ survey_template_name = serializers.SerializerMethodField() recipient_name = serializers.CharField(source="survey_instance.get_recipient_name", read_only=True) patient_name = serializers.CharField(source="survey_instance.patient.get_full_name", read_only=True) staff_name = serializers.CharField(source="survey_instance.staff.get_full_name", read_only=True) class Meta: model = SurveyTracking fields = [ "id", "survey_instance", "survey_template_name", "recipient_name", "patient_name", "staff_name", "event_type", "time_on_page", "total_time_spent", "current_question", "user_agent", "ip_address", "device_type", "browser", "country", "city", "metadata", "created_at", ] read_only_fields = ["id", "created_at"] def get_survey_template_name(self, obj): if obj.survey_instance.survey_template: return obj.survey_instance.survey_template.name return obj.survey_instance.metadata.get("patient_type", "Event-based Survey") class SurveyInstanceAnalyticsSerializer(serializers.ModelSerializer): """ Enhanced survey instance serializer with tracking analytics. """ survey_template_name = serializers.SerializerMethodField() recipient_name = serializers.CharField(source="get_recipient_name", read_only=True) recipient_type = serializers.SerializerMethodField() patient_name = serializers.CharField(source="patient.get_full_name", read_only=True) patient_mrn = serializers.CharField(source="patient.mrn", read_only=True) staff_name = serializers.CharField(source="staff.get_full_name", read_only=True) staff_email = serializers.EmailField(source="staff.email", read_only=True) responses = SurveyResponseSerializer(many=True, read_only=True) survey_url = serializers.SerializerMethodField() tracking_events_count = serializers.SerializerMethodField() time_to_complete_minutes = serializers.SerializerMethodField() comment_analysis = serializers.JSONField(read_only=True) class Meta: model = SurveyInstance fields = [ "id", "survey_template", "survey_template_name", "recipient_name", "recipient_type", "patient", "patient_name", "patient_mrn", "staff", "staff_name", "staff_email", "journey_instance", "encounter_id", "delivery_channel", "recipient_phone", "recipient_email", "access_token", "token_expires_at", "survey_url", "status", "sent_at", "opened_at", "completed_at", "open_count", "last_opened_at", "time_spent_seconds", "total_score", "is_negative", "comment", "comment_analyzed", "comment_analysis", "responses", "metadata", "tracking_events_count", "time_to_complete_minutes", "created_at", "updated_at", ] read_only_fields = [ "id", "access_token", "token_expires_at", "sent_at", "opened_at", "completed_at", "open_count", "last_opened_at", "time_spent_seconds", "total_score", "is_negative", "tracking_events_count", "time_to_complete_minutes", "created_at", "updated_at", ] def get_survey_url(self, obj): """Get survey URL""" return obj.get_survey_url() def get_survey_template_name(self, obj): if obj.survey_template: return obj.survey_template.name return obj.metadata.get("patient_type", "Event-based Survey") def get_recipient_type(self, obj): """Get recipient type (patient or staff)""" return "staff" if obj.staff else "patient" def get_tracking_events_count(self, obj): """Get count of tracking events""" return obj.tracking_events.count() def get_time_to_complete_minutes(self, obj): """Calculate time to complete in minutes""" if obj.sent_at and obj.completed_at: time_diff = obj.completed_at - obj.sent_at return round(time_diff.total_seconds() / 60, 2) return None class PublicSurveySerializer(serializers.ModelSerializer): """ Public survey serializer for patient-facing survey form. Excludes sensitive information. Uses template questions. """ survey_name = serializers.SerializerMethodField() survey_name_ar = serializers.SerializerMethodField() questions = serializers.SerializerMethodField() class Meta: model = SurveyInstance fields = ["id", "survey_name", "survey_name_ar", "questions", "status", "completed_at"] read_only_fields = ["id", "status", "completed_at"] def get_survey_name(self, obj): if obj.survey_template: return obj.survey_template.name return "Patient Experience Survey" def get_survey_name_ar(self, obj): if obj.survey_template: return obj.survey_template.name_ar return "" def get_questions(self, obj): from django.db.models import Q patient_events = set(obj.metadata.get("event_types", [])) qs = obj.survey_template.questions.filter(Q(is_base=True) | Q(event_type__in=patient_events)).order_by("order") return SurveyQuestionSerializer(qs, many=True).data