467 lines
15 KiB
Python
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
|