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.
380 lines
16 KiB
Python
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}) |