HH/apps/surveys/serializers.py
2026-03-28 14:03:56 +03:00

467 lines
15 KiB
Python

"""
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