alternative-backend-service/meetings/serializers.py
saani a7d451702f docs(api): refactor appointments endpoint documentation structure
Enhanced the API root documentation for the appointments system with improved formatting and updated description to include "flexible availability" feature. Restructured the endpoint documentation for better readability and maintainability while preserving all endpoint information including Jitsi meeting integration details.
2025-11-26 19:30:26 +00:00

380 lines
16 KiB
Python

from rest_framework import serializers
from .models import AdminWeeklyAvailability, AppointmentRequest, get_admin_availability, check_date_availability
from django.utils import timezone
from datetime import datetime, timedelta
import json
class AdminWeeklyAvailabilitySerializer(serializers.ModelSerializer):
availability_schedule_display = serializers.SerializerMethodField()
all_available_slots = serializers.SerializerMethodField()
class Meta:
model = AdminWeeklyAvailability
fields = [
'id', 'availability_schedule', 'availability_schedule_display',
'all_available_slots', 'created_at', 'updated_at'
]
def get_availability_schedule_display(self, obj):
if not obj.availability_schedule:
return "No availability set"
display = {}
days_map = dict(AdminWeeklyAvailability.DAYS_OF_WEEK)
time_slots_map = dict(AdminWeeklyAvailability.TIME_SLOT_CHOICES)
for day_num, time_slots in obj.availability_schedule.items():
day_name = days_map.get(int(day_num))
slot_names = [time_slots_map.get(slot, slot) for slot in time_slots]
display[day_name] = slot_names
return display
def get_all_available_slots(self, obj):
return obj.get_all_available_slots()
class AdminWeeklyAvailabilityUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = AdminWeeklyAvailability
fields = ['availability_schedule']
def validate_availability_schedule(self, value):
if not isinstance(value, dict):
raise serializers.ValidationError("Availability schedule must be a dictionary.")
valid_days = [str(day[0]) for day in AdminWeeklyAvailability.DAYS_OF_WEEK]
valid_slots = [slot[0] for slot in AdminWeeklyAvailability.TIME_SLOT_CHOICES]
for day, time_slots in value.items():
if day not in valid_days:
raise serializers.ValidationError(f"Invalid day: {day}. Must be one of {valid_days}.")
if not isinstance(time_slots, list):
raise serializers.ValidationError(f"Time slots for day {day} must be a list.")
for slot in time_slots:
if slot not in valid_slots:
raise serializers.ValidationError(
f"Invalid time slot: {slot} for day {day}. Must be one of {valid_slots}."
)
return value
class AppointmentRequestSerializer(serializers.ModelSerializer):
full_name = serializers.ReadOnlyField()
formatted_created_at = serializers.ReadOnlyField()
formatted_scheduled_datetime = serializers.ReadOnlyField()
preferred_dates_display = serializers.ReadOnlyField()
preferred_time_slots_display = serializers.ReadOnlyField()
has_jitsi_meeting = serializers.ReadOnlyField()
jitsi_meet_url = serializers.ReadOnlyField()
jitsi_room_id = serializers.ReadOnlyField()
can_join_meeting = serializers.ReadOnlyField()
meeting_status = serializers.ReadOnlyField()
meeting_duration_display = serializers.ReadOnlyField()
matching_availability = serializers.SerializerMethodField()
are_preferences_available = serializers.SerializerMethodField()
class Meta:
model = AppointmentRequest
fields = [
'id', 'first_name', 'last_name', 'email', 'phone', 'reason',
'preferred_dates', 'preferred_time_slots', 'status',
'scheduled_datetime', 'scheduled_duration', 'rejection_reason',
'jitsi_meet_url', 'jitsi_room_id', 'created_at', 'updated_at',
'full_name', 'formatted_created_at', 'formatted_scheduled_datetime',
'preferred_dates_display', 'preferred_time_slots_display',
'has_jitsi_meeting', 'can_join_meeting', 'meeting_status',
'meeting_duration_display', 'matching_availability', 'are_preferences_available'
]
read_only_fields = [
'id', 'status', 'scheduled_datetime', 'scheduled_duration',
'rejection_reason', 'jitsi_meet_url', 'jitsi_room_id',
'created_at', 'updated_at'
]
def get_matching_availability(self, obj):
"""Get matching availability for this appointment request"""
return obj.get_matching_availability()
def get_are_preferences_available(self, obj):
"""Check if preferences match admin availability"""
return obj.are_preferences_available()
class AppointmentRequestCreateSerializer(serializers.ModelSerializer):
selected_slots = serializers.ListField(
child=serializers.DictField(),
write_only=True,
required=False,
help_text="List of selected day-time combinations: [{'day': 0, 'time_slot': 'morning'}]"
)
available_slots_info = serializers.SerializerMethodField(read_only=True)
class Meta:
model = AppointmentRequest
fields = [
'first_name', 'last_name', 'email', 'phone', 'reason',
'preferred_dates', 'preferred_time_slots', 'selected_slots',
'available_slots_info'
]
extra_kwargs = {
'preferred_dates': {'required': False},
'preferred_time_slots': {'required': False}
}
def get_available_slots_info(self, obj):
if not hasattr(obj, 'preferred_dates') or not obj.preferred_dates:
return {}
availability_info = {}
for date_str in obj.preferred_dates:
available_slots = check_date_availability(date_str)
availability_info[date_str] = {
'available_slots': available_slots,
'is_available': any(slot in available_slots for slot in obj.preferred_time_slots)
}
return availability_info
def validate(self, data):
selected_slots = data.get('selected_slots')
preferred_dates = data.get('preferred_dates')
preferred_time_slots = data.get('preferred_time_slots')
if selected_slots:
return self._validate_selected_slots(data, selected_slots)
elif preferred_dates and preferred_time_slots:
return self._validate_old_format(data, preferred_dates, preferred_time_slots)
else:
raise serializers.ValidationError(
"Either provide 'selected_slots' or both 'preferred_dates' and 'preferred_time_slots'."
)
def _validate_selected_slots(self, data, selected_slots):
if not selected_slots:
raise serializers.ValidationError("At least one time slot must be selected.")
availability = get_admin_availability()
if not availability:
raise serializers.ValidationError("No admin availability set.")
for i, slot in enumerate(selected_slots):
day = slot.get('day')
time_slot = slot.get('time_slot')
if day is None or time_slot is None:
raise serializers.ValidationError(f"Slot {i+1}: Must have 'day' and 'time_slot'.")
if not availability.is_available(day, time_slot):
day_name = dict(AdminWeeklyAvailability.DAYS_OF_WEEK).get(day, 'Unknown day')
raise serializers.ValidationError(
f"Slot {i+1}: '{time_slot}' on {day_name} is not available."
)
data['preferred_dates'] = self._convert_slots_to_dates(selected_slots)
data['preferred_time_slots'] = self._extract_time_slots(selected_slots)
del data['selected_slots']
return data
def _validate_old_format(self, data, preferred_dates, preferred_time_slots):
if not preferred_dates or len(preferred_dates) == 0:
raise serializers.ValidationError("At least one preferred date is required.")
today = timezone.now().date()
for date_str in preferred_dates:
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
if date_obj < today:
raise serializers.ValidationError("Preferred dates cannot be in the past.")
available_slots = check_date_availability(date_str)
if not available_slots:
raise serializers.ValidationError(
f"No admin availability on {date_obj.strftime('%A, %B %d, %Y')}"
)
except ValueError:
raise serializers.ValidationError(f"Invalid date format: {date_str}. Use YYYY-MM-DD.")
if not preferred_time_slots or len(preferred_time_slots) == 0:
raise serializers.ValidationError("At least one time slot is required.")
valid_slots = ['morning', 'afternoon', 'evening']
for slot in preferred_time_slots:
if slot not in valid_slots:
raise serializers.ValidationError(f"Invalid time slot: {slot}. Must be one of {valid_slots}.")
has_available_slot = False
for date_str in preferred_dates:
available_slots = check_date_availability(date_str)
if any(slot in available_slots for slot in preferred_time_slots):
has_available_slot = True
break
if not has_available_slot:
raise serializers.ValidationError(
"None of your preferred date and time combinations match the admin's availability. "
"Please check the admin's schedule and adjust your preferences."
)
return data
def _convert_slots_to_dates(self, selected_slots):
from datetime import timedelta
today = timezone.now().date()
preferred_dates = []
for slot in selected_slots:
target_weekday = slot['day']
found_date = None
for days_ahead in range(1, 15):
check_date = today + timedelta(days=days_ahead)
if check_date.weekday() == target_weekday:
found_date = check_date
break
if found_date:
date_str = found_date.strftime('%Y-%m-%d')
if date_str not in preferred_dates:
preferred_dates.append(date_str)
return preferred_dates
def _extract_time_slots(self, selected_slots):
time_slots = []
for slot in selected_slots:
if slot['time_slot'] not in time_slots:
time_slots.append(slot['time_slot'])
return time_slots
def create(self, validated_data):
return super().create(validated_data)
class AppointmentScheduleSerializer(serializers.Serializer):
scheduled_datetime = serializers.DateTimeField()
scheduled_duration = serializers.IntegerField(default=60, min_value=30, max_value=240)
date_str = serializers.CharField(required=False, write_only=True)
time_slot = serializers.CharField(required=False, write_only=True)
def validate(self, data):
scheduled_datetime = data.get('scheduled_datetime')
date_str = data.get('date_str')
time_slot = data.get('time_slot')
if date_str and time_slot:
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
day_of_week = date_obj.weekday()
availability = get_admin_availability()
if not availability.is_available(day_of_week, time_slot):
raise serializers.ValidationError(
f"The admin is not available on {date_obj.strftime('%A')} during the {time_slot} time slot."
)
datetime_obj = self._convert_to_datetime(date_obj, time_slot)
data['scheduled_datetime'] = datetime_obj
except ValueError as e:
raise serializers.ValidationError(f"Invalid date format: {e}")
if scheduled_datetime and scheduled_datetime <= timezone.now():
raise serializers.ValidationError("Scheduled datetime must be in the future.")
return data
def _convert_to_datetime(self, date_obj, time_slot):
"""Convert date and time slot to actual datetime"""
time_mapping = {
'morning': (9, 0),
'afternoon': (13, 0),
'evening': (18, 0)
}
if time_slot not in time_mapping:
raise serializers.ValidationError(f"Invalid time slot: {time_slot}")
hour, minute = time_mapping[time_slot]
return timezone.make_aware(
datetime.combine(date_obj, datetime.min.time().replace(hour=hour, minute=minute))
)
def validate_scheduled_duration(self, value):
if value < 30:
raise serializers.ValidationError("Duration must be at least 30 minutes.")
if value > 240:
raise serializers.ValidationError("Duration cannot exceed 4 hours.")
return value
class AppointmentRejectSerializer(serializers.Serializer):
rejection_reason = serializers.CharField(required=False, allow_blank=True)
class AvailabilityCheckSerializer(serializers.Serializer):
date = serializers.CharField(required=True)
def validate_date(self, value):
try:
date_obj = datetime.strptime(value, '%Y-%m-%d').date()
if date_obj < timezone.now().date():
raise serializers.ValidationError("Date cannot be in the past.")
return value
except ValueError:
raise serializers.ValidationError("Invalid date format. Use YYYY-MM-DD.")
class AvailabilityResponseSerializer(serializers.Serializer):
date = serializers.CharField()
day_name = serializers.CharField()
available_slots = serializers.ListField(child=serializers.CharField())
available_slots_display = serializers.ListField(child=serializers.CharField())
is_available = serializers.BooleanField()
class WeeklyAvailabilitySerializer(serializers.Serializer):
day_number = serializers.IntegerField()
day_name = serializers.CharField()
available_slots = serializers.ListField(child=serializers.CharField())
available_slots_display = serializers.ListField(child=serializers.CharField())
class TimeSlotSerializer(serializers.Serializer):
value = serializers.CharField()
display = serializers.CharField()
disabled = serializers.BooleanField(default=False)
class DayAvailabilitySerializer(serializers.Serializer):
day_number = serializers.IntegerField()
day_name = serializers.CharField()
time_slots = TimeSlotSerializer(many=True)
is_available = serializers.BooleanField()
class AdminAvailabilityConfigSerializer(serializers.Serializer):
days = DayAvailabilitySerializer(many=True)
@classmethod
def get_default_config(cls):
days_config = []
for day_num, day_name in AdminWeeklyAvailability.DAYS_OF_WEEK:
days_config.append({
'day_number': day_num,
'day_name': day_name,
'time_slots': [
{
'value': 'morning',
'display': 'Morning (9AM - 12PM)',
'disabled': False
},
{
'value': 'afternoon',
'display': 'Afternoon (1PM - 5PM)',
'disabled': False
},
{
'value': 'evening',
'display': 'Evening (6PM - 9PM)',
'disabled': False
}
],
'is_available': False
})
return cls({'days': days_config})