From 53aac43289c0351a9b47dd634bf47883d828f766 Mon Sep 17 00:00:00 2001 From: saani Date: Wed, 3 Dec 2025 15:44:56 +0000 Subject: [PATCH] feat: add selected_slots field to AppointmentRequest model and update serializers --- ...remove_appointmentrequest_is_admin_join.py | 17 ++ .../0011_appointmentrequest_selected_slots.py | 18 ++ meetings/models.py | 13 +- meetings/serializers.py | 239 ++++++++---------- meetings/views.py | 1 - 5 files changed, 144 insertions(+), 144 deletions(-) create mode 100644 meetings/migrations/0010_remove_appointmentrequest_is_admin_join.py create mode 100644 meetings/migrations/0011_appointmentrequest_selected_slots.py diff --git a/meetings/migrations/0010_remove_appointmentrequest_is_admin_join.py b/meetings/migrations/0010_remove_appointmentrequest_is_admin_join.py new file mode 100644 index 0000000..7eb4019 --- /dev/null +++ b/meetings/migrations/0010_remove_appointmentrequest_is_admin_join.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.8 on 2025-12-03 14:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('meetings', '0009_appointmentrequest_is_admin_join'), + ] + + operations = [ + migrations.RemoveField( + model_name='appointmentrequest', + name='is_admin_join', + ), + ] diff --git a/meetings/migrations/0011_appointmentrequest_selected_slots.py b/meetings/migrations/0011_appointmentrequest_selected_slots.py new file mode 100644 index 0000000..f8902d1 --- /dev/null +++ b/meetings/migrations/0011_appointmentrequest_selected_slots.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-12-03 15:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('meetings', '0010_remove_appointmentrequest_is_admin_join'), + ] + + operations = [ + migrations.AddField( + model_name='appointmentrequest', + name='selected_slots', + field=models.JSONField(default=list, help_text='Original selected slots with day and time slot pairs'), + ), + ] diff --git a/meetings/models.py b/meetings/models.py index b166feb..0b3df0d 100644 --- a/meetings/models.py +++ b/meetings/models.py @@ -239,7 +239,10 @@ class AppointmentRequest(models.Model): default=dict, help_text="Additional meeting data (participants, duration, etc)" ) - + selected_slots = models.JSONField( + default=list, + help_text="Original selected slots with day and time slot pairs" + ) meeting_started_at = models.DateTimeField(null=True, blank=True) meeting_ended_at = models.DateTimeField(null=True, blank=True) meeting_duration_actual = models.PositiveIntegerField( @@ -247,11 +250,6 @@ class AppointmentRequest(models.Model): help_text="Actual meeting duration in minutes" ) - is_admin_join = models.BooleanField( - default=False, - help_text="When True, participants are allowed to join the meeting. Admin must join first and set this to True." - ) - created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -634,9 +632,6 @@ class AppointmentRequest(models.Model): if user_type == 'moderator': return meeting_start - timedelta(minutes=15) <= now <= meeting_end + timedelta(minutes=15) else: - # Participants can only join when is_admin_join is True - if not self.is_admin_join: - return False return meeting_start - timedelta(minutes=5) <= now <= meeting_end def schedule_appointment(self, datetime_obj, moderator_user, participant_user=None, duration=60, create_meeting=True, commit=True): diff --git a/meetings/serializers.py b/meetings/serializers.py index 4dee7aa..2bf4d78 100644 --- a/meetings/serializers.py +++ b/meetings/serializers.py @@ -80,11 +80,14 @@ class AppointmentRequestSerializer(serializers.ModelSerializer): participant_join_url = serializers.SerializerMethodField() meeting_analytics = serializers.SerializerMethodField() + # Add selected_slots field + selected_slots = serializers.JSONField() + class Meta: model = AppointmentRequest fields = [ 'id', 'first_name', 'last_name', 'email', 'phone', 'reason', - 'preferred_dates', 'preferred_time_slots', 'status', + 'preferred_dates', 'preferred_time_slots', 'selected_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', @@ -92,14 +95,15 @@ class AppointmentRequestSerializer(serializers.ModelSerializer): 'has_jitsi_meeting', 'can_join_meeting', 'can_join_as_moderator', 'can_join_as_participant', 'meeting_status', 'meeting_duration_display', 'matching_availability', 'are_preferences_available', 'moderator_join_url', 'participant_join_url', - 'meeting_analytics', 'is_admin_join' + 'meeting_analytics' ] read_only_fields = [ 'id', 'status', 'scheduled_datetime', 'scheduled_duration', 'rejection_reason', 'jitsi_meet_url', 'jitsi_room_id', - 'created_at', 'updated_at', + 'created_at', 'updated_at', 'preferred_dates', 'preferred_time_slots', + 'selected_slots' # Make selected_slots read-only in this serializer ] - + def get_can_join_meeting(self, obj): return obj.can_join_meeting('participant') @@ -155,22 +159,16 @@ class AppointmentRequestSerializer(serializers.ModelSerializer): if not is_authorized: return None - # Staff can always see the URL (even when is_admin_join is False) - # But actual participants can only see it when is_admin_join is True if current_user.is_staff: - # Staff can see the URL regardless of is_admin_join status return obj.get_participant_join_url( participant_user=participant_user, include_password=True ) else: - # Participants can only see the URL when is_admin_join is True - if obj.is_admin_join: - return obj.get_participant_join_url( - participant_user=participant_user, - include_password=True - ) - return None + return obj.get_participant_join_url( + participant_user=participant_user, + include_password=True + ) def get_meeting_analytics(self, obj): return { @@ -226,56 +224,26 @@ class AppointmentRequestSerializer(serializers.ModelSerializer): class AppointmentRequestCreateSerializer(serializers.ModelSerializer): selected_slots = serializers.ListField( child=serializers.DictField(), - write_only=True, - required=False, + required=True, 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' + 'selected_slots' ] - 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 + # Remove preferred_dates and preferred_time_slots from fields list + # They will be calculated automatically 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.") + # Your existing validation logic availability = get_admin_availability() if not availability: raise serializers.ValidationError("No admin availability set.") @@ -293,88 +261,75 @@ class AppointmentRequestCreateSerializer(serializers.ModelSerializer): 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) + # Add dates to the slots and store enhanced version + enhanced_slots = self._add_dates_to_slots(selected_slots) + data['selected_slots'] = enhanced_slots - del data['selected_slots'] + # Calculate preferred_dates and preferred_time_slots for backward compatibility + data['preferred_dates'] = self._extract_unique_dates(enhanced_slots) + data['preferred_time_slots'] = self._extract_unique_time_slots(enhanced_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.") + def _add_dates_to_slots(self, selected_slots): + """Add actual dates to the slots based on day numbers""" + from datetime import datetime + today = datetime.now().date() + enhanced_slots = [] - 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.") + # We need to find the next occurrence of each day + # Keep track of which dates we've assigned to which day + day_date_map = {} + + for slot in selected_slots: + day = slot.get('day') + time_slot = slot.get('time_slot') + + # If we haven't assigned a date to this day yet, find the next occurrence + if day not in day_date_map: + found_date = None + for days_ahead in range(1, 15): # Look ahead 2 weeks + check_date = today + timedelta(days=days_ahead) + if check_date.weekday() == day: + found_date = check_date + break - 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): - today = timezone.now().date() - preferred_dates = [] - - for slot in selected_slots: - target_weekday = slot['day'] + if found_date: + day_date_map[day] = found_date.strftime('%Y-%m-%d') - 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) + # Add the slot with date + if day in day_date_map: + enhanced_slots.append({ + 'day': day, + 'time_slot': time_slot, + 'date': day_date_map[day] + }) - return preferred_dates + return enhanced_slots - def _extract_time_slots(self, selected_slots): + def _extract_unique_dates(self, enhanced_slots): + """Extract unique dates from enhanced slots""" + dates = [] + for slot in enhanced_slots: + date_str = slot.get('date') + if date_str and date_str not in dates: + dates.append(date_str) + return dates + + def _extract_unique_time_slots(self, enhanced_slots): + """Extract unique time slots from enhanced slots""" time_slots = [] - for slot in selected_slots: - if slot['time_slot'] not in time_slots: - time_slots.append(slot['time_slot']) + for slot in enhanced_slots: + time_slot = slot.get('time_slot') + if time_slot and time_slot not in time_slots: + time_slots.append(time_slot) return time_slots def create(self, validated_data): + # Create the appointment with all data + # The selected_slots will be saved to the database automatically 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) @@ -497,7 +452,7 @@ class AppointmentDetailSerializer(serializers.ModelSerializer): 'created_at', 'updated_at', 'meeting_info', 'meeting_analytics', 'can_join_as_moderator', 'can_join_as_participant', - 'moderator_join_url', 'participant_join_url', 'is_admin_join', + 'moderator_join_url', 'participant_join_url' ] read_only_fields = ['id', 'created_at', 'updated_at'] @@ -583,42 +538,33 @@ class AppointmentDetailSerializer(serializers.ModelSerializer): if not obj.has_jitsi_meeting or not obj.email: return None - # Try to get the participant user from the appointment's email participant_user = None try: from users.models import CustomUser participant_user = CustomUser.objects.get(email=obj.email) except CustomUser.DoesNotExist: - # Participant user doesn't exist (not registered) return None - # Check if current user is authorized to view this URL current_user = request.user is_authorized = ( - current_user.is_staff or # Staff can see participant URLs for all appointments - current_user.email == obj.email or # Participant can see their own URL + current_user.is_staff or + current_user.email == obj.email or (hasattr(obj, 'invited_participants') and current_user.email in obj.invited_participants) ) if not is_authorized: return None - # Staff can always see the URL (even when is_admin_join is False) - # But actual participants can only see it when is_admin_join is True if current_user.is_staff: - # Staff can see the URL regardless of is_admin_join status return obj.get_participant_join_url( participant_user=participant_user, include_password=True ) else: - # Participants can only see the URL when is_admin_join is True - if obj.is_admin_join: - return obj.get_participant_join_url( - participant_user=participant_user, - include_password=True - ) - return None + return obj.get_participant_join_url( + participant_user=participant_user, + include_password=True + ) class JitsiMeetingSerializer(serializers.ModelSerializer): moderator_join_url = serializers.SerializerMethodField() @@ -637,12 +583,37 @@ class JitsiMeetingSerializer(serializers.ModelSerializer): def get_moderator_join_url(self, obj): if not obj.has_jitsi_meeting: return None - return obj.get_moderator_join_url() + request = self.context.get('request') + if request and request.user.is_authenticated and request.user.is_staff: + return obj.get_moderator_join_url(moderator_user=request.user) + class GenericModerator: + id = 'moderator' + first_name = 'Moderator' + last_name = '' + email = '' + return obj.get_moderator_join_url(moderator_user=GenericModerator()) def get_participant_join_url(self, obj): if not obj.has_jitsi_meeting: return None - return obj.get_participant_join_url() + request = self.context.get('request') + participant_user = None + if obj.email: + try: + from users.models import CustomUser + participant_user = CustomUser.objects.get(email=obj.email) + except CustomUser.DoesNotExist: + pass + + if not participant_user: + class GenericParticipant: + id = 'participant' + first_name = 'Participant' + last_name = '' + email = obj.email if obj.email else '' + participant_user = GenericParticipant() + + return obj.get_participant_join_url(participant_user=participant_user) def get_meeting_config(self, obj): if not obj.jitsi_meeting_config: diff --git a/meetings/views.py b/meetings/views.py index 3e929e4..3045bd6 100644 --- a/meetings/views.py +++ b/meetings/views.py @@ -416,7 +416,6 @@ class JoinMeetingView(generics.GenericAPIView): def get(self, request, pk): appointment = self.get_object() - # Check permissions if not self._has_join_permission(request.user, appointment): return Response( {'error': 'You do not have permission to join this meeting'}, -- 2.39.5