From 0b948e07eaee1124e8a6eddbff946dfe5b53478a Mon Sep 17 00:00:00 2001 From: tra371 Date: Thu, 30 Oct 2025 20:04:09 +0700 Subject: [PATCH 1/2] implement add teacher and student to school --- core/serializers.py | 15 ++++ core/urls.py | 2 + core/views.py | 167 +++++++++++++++++++++++++++++++++++++++++++- docker-compose.yml | 14 ++++ 4 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 docker-compose.yml diff --git a/core/serializers.py b/core/serializers.py index 5b3da5d..b17951f 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -230,6 +230,21 @@ class Meta: fields = ['id', 'user', 'school', 'user_name', 'user_role', 'school_name', 'joined_at', 'is_active'] read_only_fields = ['id', 'joined_at'] +class SchoolAddTeacherSerializer(serializers.Serializer): + """Serializer for adding a teacher to school""" + teacher_email = serializers.EmailField(required=True) + teacher_role = serializers.ChoiceField(choices=TeacherProfile.TEACHER_ROLES, default="subject_teacher") + assigned_classes = serializers.ListField( + child=serializers.ChoiceField(choices=Class.ClassName.choices), + allow_empty=True, + required=False + ) + +class SchoolAddStudentSerializer(serializers.Serializer): + """Serializer for adding a student to school""" + student_email = serializers.EmailField(required=True) + assigned_class = serializers.ChoiceField(choices=Class.ClassName.choices) + # ============================================================================= # SUBJECT & CLASS SERIALIZERS diff --git a/core/urls.py b/core/urls.py index 0c3df57..fabfb2d 100644 --- a/core/urls.py +++ b/core/urls.py @@ -93,6 +93,8 @@ path('schools//members/', views.get_school_members, name='school-members'), path('schools//projects/', views.get_school_projects, name='school-projects'), path('schools//add-user/', views.add_user_to_school, name='add-user-to-school'), + path('schools//add-teacher-school/', views.add_teacher_to_school, name='add-teacher-to-school'), + path('schools//add-student-school/', views.add_student_to_school, name='add-student-to-school'), path('classes//add-student/', views.add_student_to_class, name='add-student-to-class'), # ================================================================= diff --git a/core/views.py b/core/views.py index 8e9b855..482db1c 100644 --- a/core/views.py +++ b/core/views.py @@ -11,6 +11,7 @@ from django.views.decorators.http import require_GET from django.core.mail import send_mail import random +import logging from rest_framework import viewsets, status, permissions, filters from rest_framework.decorators import action, api_view, permission_classes @@ -31,7 +32,7 @@ ProjectParticipant ) from .serializers import ( - UserRegistrationSerializer, UserSerializer, UserUpdateSerializer, + SchoolAddStudentSerializer, SchoolAddTeacherSerializer, UserRegistrationSerializer, UserSerializer, UserUpdateSerializer, PasswordChangeSerializer, SchoolSerializer, SchoolCreateSerializer, SchoolMembershipSerializer, SubjectSerializer, ClassSerializer, TeacherProfileSerializer, StudentProfileSerializer, ProjectSerializer, @@ -53,6 +54,8 @@ from rest_framework import serializers from rest_framework.exceptions import PermissionDenied +logger = logging.getLogger("__name__") + # ============================================================================= # AUTHENTICATION & LOGIN VIEWS (Grouped at the top for clarity) @@ -1110,6 +1113,168 @@ def add_user_to_school(request, school_id): status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@api_view(['POST']) +@permission_classes([CanManageSchoolMembers]) +def add_student_to_school(request, school_id): + """ + Add a student to a school. + Only school admins and teachers can add students to their schools. + """ + try: + school = get_object_or_404(School, id=school_id) + + # Check permission + if (school.admin != request.user or request.user.role != "teacher") and not request.user.is_staff: + return Response({'error': 'Only school admins and teachers can add students to schools'}, + status=status.HTTP_403_FORBIDDEN) + + serializer_class = SchoolAddStudentSerializer(data=request.data) + if not serializer_class.is_valid(): + return Response(serializer_class.errors, status=status.HTTP_400_BAD_REQUEST) + + data = serializer_class.validated_data + student_email = data['student_email'] + assigned_class_name = data['assigned_class'] + + try: + user = User.objects.get(email=student_email) + if user.role and user.role != "student": + return Response({'error': 'User already has a role other than student.'}, + status=status.HTTP_400_BAD_REQUEST) + elif not user.role: + user.role = "student" + user.save() + except User.DoesNotExist: + return Response({'error': 'User not found'}, + status=status.HTTP_404_NOT_FOUND) + + # Create school membership + membership, created = SchoolMembership.objects.get_or_create( + user=user, + school=school, + defaults={'is_active': True} + ) + + if not created and membership.is_active: + return Response({'error': 'User is already a member of this school'}, + status=status.HTTP_400_BAD_REQUEST) + elif not created: + membership.is_active = True + membership.save() + + try: + assigned_class = Class.objects.get(name=assigned_class_name, school=school) + except Class.DoesNotExist: + return Response({'error': 'Class not found in this school'}, status=status.HTTP_400_BAD_REQUEST) + + # Create appropriate profile + StudentProfile.objects.get_or_create( + user=user, + school=school, + defaults={ + 'student_id': f"{school.name[:3].upper()}{user.id}", + 'current_class': assigned_class + } + ) + + return Response({ + 'message': f'Successfully added {user.get_full_name()} as {user.role} to {school.name}', + 'user_id': str(user.id), + 'user_name': user.get_full_name(), + 'user_role': user.role, + 'school_name': school.name + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"Error adding student to school: {str(e)}") + return Response({'error': f'Failed to add student to school: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + +@api_view(['POST']) +@permission_classes([CanManageSchoolMembers]) +def add_teacher_to_school(request, school_id): + """ + Add a teacher to a school. + Only school admins can add teachers to their schools. + """ + try: + school = get_object_or_404(School, id=school_id) + + # Check permission + if school.admin != request.user and not request.user.is_staff: + return Response({'error': 'Only school admins can add teachers to schools'}, + status=status.HTTP_403_FORBIDDEN) + + serializer_class = SchoolAddTeacherSerializer(data=request.data) + if not serializer_class.is_valid(): + return Response(serializer_class.errors, status=status.HTTP_400_BAD_REQUEST) + + data = serializer_class.validated_data + teacher_email = data['teacher_email'] + teacher_role = data['teacher_role'] + assigned_class_names = data.get('assigned_classes') + + try: + user = User.objects.get(email=teacher_email) + if user.role and user.role != "teacher": + return Response({'error': 'User already has a role other than teacher.'}, + status=status.HTTP_400_BAD_REQUEST) + elif not user.role: + user.role = "teacher" + user.save() + except User.DoesNotExist: + return Response({'error': 'User not found'}, + status=status.HTTP_404_NOT_FOUND) + + # Create school membership + membership, created = SchoolMembership.objects.get_or_create( + user=user, + school=school, + defaults={'is_active': True} + ) + + if not created and membership.is_active: + return Response({'error': 'User is already a member of this school'}, + status=status.HTTP_400_BAD_REQUEST) + elif not created: + membership.is_active = True + membership.save() + + # Create appropriate profile + teacher_profile, _ = TeacherProfile.objects.get_or_create( + user=user, + school=school, + defaults={'teacher_role': teacher_role, 'status': 'active'} + ) + # Query classes matching names and this school + valid_class_names = Class.objects.filter(name__in=assigned_class_names, school=school).values_list('name', flat=True) + + # Find invalid class names + invalid_classes = set(assigned_class_names) - set(valid_class_names) + if invalid_classes: + return Response( + {'error': f'The following classes are invalid or do not belong to the school: {", ".join(invalid_classes)}'}, + status=status.HTTP_400_BAD_REQUEST + ) + + valid_classes_qs = Class.objects.filter(name__in=assigned_class_names, school=school) + teacher_profile.assigned_classes.set(valid_classes_qs) + + return Response({ + 'message': f'Successfully added {user.get_full_name()} as {user.role} to {school.name}', + 'user_id': str(user.id), + 'user_name': user.get_full_name(), + 'user_role': teacher_role, + 'school_name': school.name + }, status=status.HTTP_200_OK) + + except Exception as e: + logger.error(f"Error adding teacher to school: {str(e)}") + return Response({'error': f'Failed to add teacher to school: {str(e)}'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @api_view(['POST']) @permission_classes([CanManageSchoolContent]) def add_student_to_class(request, class_id): diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..510e77f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + db: + image: postgres:14-alpine + environment: + POSTGRES_USER: global_classrooms_user + POSTGRES_PASSWORD: GlobalClass2025 + POSTGRES_DB: global_classrooms + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data/ + +volumes: + postgres_data: From bd230c117922eff5d8c3be255d3dfa933b029616 Mon Sep 17 00:00:00 2001 From: tra371 Date: Thu, 30 Oct 2025 20:06:31 +0700 Subject: [PATCH 2/2] remove docker compose file --- docker-compose.yml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 510e77f..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,14 +0,0 @@ -services: - db: - image: postgres:14-alpine - environment: - POSTGRES_USER: global_classrooms_user - POSTGRES_PASSWORD: GlobalClass2025 - POSTGRES_DB: global_classrooms - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data/ - -volumes: - postgres_data: