""" Class Serializers Serializers for class-related models in the education module """ from rest_framework import serializers from django.contrib.auth import get_user_model from django.utils import timezone from ..models.class import Class User = get_user_model() class ClassSerializer(serializers.ModelSerializer): """Serializer for Class model""" display_name = serializers.SerializerMethodField() available_seats = serializers.SerializerMethodField() enrollment_percentage = serializers.SerializerMethodField() is_enrollment_period = serializers.SerializerMethodField() is_active_currently = serializers.SerializerMethodField() duration_in_days = serializers.SerializerMethodField() schedule_summary = serializers.SerializerMethodField() capacity_stats = serializers.SerializerMethodField() class_teacher_name = serializers.CharField(source='class_teacher.name', read_only=True) assistant_teacher_name = serializers.CharField(source='assistant_teacher.name', read_only=True) grade_level_display = serializers.CharField(source='get_grade_level_display', read_only=True) stream_display = serializers.CharField(source='get_stream_display', read_only=True) status_display = serializers.CharField(source='get_status_display', read_only=True) shift_display = serializers.CharField(source='get_shift_display', read_only=True) medium_display = serializers.CharField(source='get_medium_display', read_only=True) created_by_name = serializers.CharField(source='created_by.name', read_only=True) updated_by_name = serializers.CharField(source='updated_by.name', read_only=True) subject_teachers_info = serializers.SerializerMethodField() class Meta: model = Class fields = [ 'id', 'tenant', 'name', 'class_code', 'grade_level', 'grade_level_display', 'stream', 'stream_display', 'shift', 'shift_display', 'medium', 'medium_display', 'max_students', 'current_students', 'min_students', 'waitlist_count', 'available_seats', 'enrollment_percentage', 'is_full', 'has_waitlist', 'class_teacher', 'class_teacher_name', 'assistant_teacher', 'assistant_teacher_name', 'subject_teachers', 'subject_teachers_info', 'classroom', 'building', 'floor', 'academic_year', 'semester', 'term', 'schedule', 'schedule_summary', 'meeting_days', 'start_time', 'end_time', 'break_times', 'curriculum', 'subjects_offered', 'elective_subjects', 'assessment_methods', 'grading_scale', 'passing_grade', 'special_programs', 'support_services', 'class_rules', 'attendance_policy', 'homework_policy', 'discipline_policy', 'classroom_equipment', 'learning_materials', 'digital_resources', 'parent_group_id', 'communication_preferences', 'status', 'status_display', 'start_date', 'end_date', 'enrollment_start_date', 'enrollment_end_date', 'is_enrollment_open', 'is_enrollment_period', 'is_active_currently', 'duration_in_days', 'description', 'notes', 'tags', 'requirements', 'created_by', 'created_by_name', 'updated_by', 'updated_by_name', 'created_at', 'updated_at', 'display_name', 'capacity_stats', ] read_only_fields = [ 'tenant', 'id', 'class_code', 'created_at', 'updated_at', 'created_by', 'updated_by', 'display_name', 'available_seats', 'enrollment_percentage', 'is_enrollment_period', 'is_active_currently', 'duration_in_days', 'schedule_summary', 'capacity_stats', ] def get_display_name(self, obj): """Get display name for the class""" return obj.display_name def get_available_seats(self, obj): """Get number of available seats""" return obj.available_seats def get_enrollment_percentage(self, obj): """Get enrollment percentage""" return round(obj.enrollment_percentage, 2) def get_is_enrollment_period(self, obj): """Check if currently in enrollment period""" return obj.is_enrollment_period def get_is_active_currently(self, obj): """Check if class is currently active""" return obj.is_active_currently def get_duration_in_days(self, obj): """Get class duration in days""" return obj.duration_in_days def get_schedule_summary(self, obj): """Get schedule summary""" return obj.get_schedule_summary() def get_capacity_stats(self, obj): """Get capacity statistics""" return obj.get_capacity_stats() def get_subject_teachers_info(self, obj): """Get subject teachers information""" return [ { 'id': str(teacher.id), 'name': teacher.get_full_name(), 'email': teacher.email, } for teacher in obj.subject_teachers.all() ] def validate(self, data): """Validate class data""" # Validate dates if data.get('start_date') and data.get('end_date'): if data['end_date'] <= data['start_date']: raise serializers.ValidationError( "End date must be after start date" ) # Validate enrollment dates if data.get('enrollment_start_date') and data.get('enrollment_end_date'): if data['enrollment_end_date'] <= data['enrollment_start_date']: raise serializers.ValidationError( "Enrollment end date must be after start date" ) # Validate capacity if data.get('min_students') and data.get('max_students'): if data['min_students'] > data['max_students']: raise serializers.ValidationError( "Minimum students cannot exceed maximum students" ) # Validate class times if data.get('start_time') and data.get('end_time'): if data['end_time'] <= data['start_time']: raise serializers.ValidationError( "End time must be after start time" ) # Validate schedule if provided if data.get('schedule'): self._validate_schedule(data['schedule']) # Validate teacher assignments if data.get('class_teacher') and data.get('assistant_teacher'): if data['class_teacher'].id == data['assistant_teacher'].id: raise serializers.ValidationError( "Assistant teacher cannot be the same as class teacher" ) return data def _validate_schedule(self, schedule): """Validate schedule format""" if not isinstance(schedule, dict): raise serializers.ValidationError("Schedule must be a dictionary") valid_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] for day, periods in schedule.items(): if day not in valid_days: raise serializers.ValidationError(f"Invalid day: {day}") if not isinstance(periods, list): raise serializers.ValidationError(f"Schedule for {day} must be a list") for period in periods: if not isinstance(period, dict): raise serializers.ValidationError(f"Each period in {day} must be a dictionary") required_fields = ['subject', 'time', 'teacher'] for field in required_fields: if field not in period: raise serializers.ValidationError(f"Missing required field '{field}' in period for {day}") def create(self, validated_data): """Create class with tenant context""" validated_data['tenant'] = self.context['tenant'] return super().create(validated_data) def update(self, instance, validated_data): """Update class with proper validation""" # Remove read-only fields from validated data validated_data.pop('tenant', None) validated_data.pop('class_code', None) return super().update(instance, validated_data) class ClassCreateSerializer(ClassSerializer): """Serializer for creating classes with less restrictions""" class Meta(ClassSerializer.Meta): read_only_fields = ['id', 'created_at', 'updated_at', 'created_by', 'updated_by'] class ClassUpdateSerializer(ClassSerializer): """Serializer for updating classes""" class Meta(ClassSerializer.Meta): read_only_fields = ClassSerializer.Meta.read_only_fields + ['class_code'] class ClassListSerializer(serializers.ModelSerializer): """Simplified serializer for class lists""" display_name = serializers.SerializerMethodField() grade_level_display = serializers.CharField(source='get_grade_level_display', read_only=True) stream_display = serializers.CharField(source='get_stream_display', read_only=True) status_display = serializers.CharField(source='get_status_display', read_only=True) class_teacher_name = serializers.CharField(source='class_teacher.name', read_only=True) available_seats = serializers.SerializerMethodField() enrollment_percentage = serializers.SerializerMethodField() is_enrollment_open = serializers.BooleanField(read_only=True) class Meta: model = Class fields = [ 'id', 'class_code', 'name', 'display_name', 'grade_level', 'grade_level_display', 'stream', 'stream_display', 'classroom', 'max_students', 'current_students', 'available_seats', 'enrollment_percentage', 'academic_year', 'status', 'status_display', 'class_teacher_name', 'is_enrollment_open', 'start_date', 'end_date', ] def get_display_name(self, obj): return obj.display_name def get_available_seats(self, obj): return obj.available_seats def get_enrollment_percentage(self, obj): return round(obj.enrollment_percentage, 2) class TeacherAssignmentSerializer(serializers.ModelSerializer): """Serializer for teacher assignments""" role = serializers.CharField(max_length=20) teacher_name = serializers.CharField(source='teacher.name', read_only=True) class Meta: model = Class fields = ['id', 'name', 'role', 'teacher_name', 'academic_year', 'grade_level'] class ClassStatisticsSerializer(serializers.Serializer): """Serializer for class statistics""" total_classes = serializers.IntegerField() active_classes = serializers.IntegerField() classes_with_open_enrollment = serializers.IntegerField() grade_level_distribution = serializers.DictField() stream_distribution = serializers.DictField() total_capacity = serializers.IntegerField() total_enrollment = serializers.IntegerField() overall_enrollment_rate = serializers.FloatField() class ClassImportSerializer(serializers.Serializer): """Serializer for bulk class import""" file = serializers.FileField() update_existing = serializers.BooleanField(default=False) format = serializers.ChoiceField(choices=['csv', 'excel'], default='csv') class ClassExportSerializer(serializers.Serializer): """Serializer for class export options""" format = serializers.ChoiceField(choices=['csv', 'excel', 'json'], default='csv') include_schedule = serializers.BooleanField(default=False) include_teachers = serializers.BooleanField(default=True) include_capacity = serializers.BooleanField(default=True) filters = serializers.DictField(required=False) class ScheduleUpdateSerializer(serializers.Serializer): """Serializer for updating class schedule""" schedule = serializers.DictField() check_conflicts = serializers.BooleanField(default=True) def validate_schedule(self, schedule): """Validate schedule format""" if not isinstance(schedule, dict): raise serializers.ValidationError("Schedule must be a dictionary") valid_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] for day, periods in schedule.items(): if day not in valid_days: raise serializers.ValidationError(f"Invalid day: {day}") if not isinstance(periods, list): raise serializers.ValidationError(f"Schedule for {day} must be a list") for period in periods: if not isinstance(period, dict): raise serializers.ValidationError(f"Each period in {day} must be a dictionary") required_fields = ['subject', 'time', 'teacher'] for field in required_fields: if field not in period: raise serializers.ValidationError(f"Missing required field '{field}' in period for {day}") return schedule class EnrollmentControlSerializer(serializers.Serializer): """Serializer for controlling enrollment""" is_enrollment_open = serializers.BooleanField() enrollment_start_date = serializers.DateField(required=False) enrollment_end_date = serializers.DateField(required=False) def validate(self, data): """Validate enrollment control data""" if data.get('is_enrollment_open'): if not data.get('enrollment_start_date') or not data.get('enrollment_end_date'): raise serializers.ValidationError( "Enrollment start and end dates are required when opening enrollment" ) if data['enrollment_end_date'] <= data['enrollment_start_date']: raise serializers.ValidationError( "Enrollment end date must be after start date" ) return data