From 155acb994372da433908c10b3a741c975d8e13c7 Mon Sep 17 00:00:00 2001 From: S-vz Date: Fri, 15 Mar 2024 00:07:02 +0100 Subject: [PATCH 01/11] add support for lecture manipulation --- api/serializers.py | 37 ++++++++++++++-- api/urls.py | 13 ++++++ api/views.py | 104 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 131 insertions(+), 23 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index af426ea..3a29904 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -6,7 +6,7 @@ from rest_framework_simplejwt.serializers import TokenObtainPairSerializer from rest_framework.validators import UniqueValidator -from .models import AccountRoles, Course +from .models import AccountRoles, Course, CourseLecture, LectureTypes User = get_user_model() @@ -29,6 +29,11 @@ class CourseSerializer(serializers.ModelSerializer): class Meta: model = Course fields = '__all__' + +class LectureSerializer(serializers.ModelSerializer): + class Meta: + model = CourseLecture + fields = '__all__' # Defines the rules for registering a new user. Required fields, validation rules etc. class CreateUserSerializer(serializers.ModelSerializer): @@ -73,8 +78,6 @@ def create(self, validated_data): c.save() return c - - class MassEnrollSerializer(serializers.Serializer): usernames = serializers.ListField(required=True, allow_empty=False, child=serializers.CharField(max_length=150)) @@ -88,4 +91,32 @@ def validate_enroll(self, username): def validate(self, attrs): for username in attrs["usernames"]: self.validate_enroll(username) + return attrs + +class AddLectureSerializer(serializers.Serializer): + MINIMUM_LECTURE_LENGTH = 10 # Minimum lecture length of 10 minutes + + start_time = serializers.DateTimeField(required=True) + end_time = serializers.DateTimeField(required=True) + lecture_type = serializers.CharField(required=True) + + def validate(self, attrs): + start_time, end_time = attrs["start_time"], attrs["end_time"] + if start_time > end_time: + raise serializers.ValidationError({"error": f"end_time has to be after start_time"}) + + lecture_length = (end_time - start_time).seconds / 60 + if lecture_length < self.MINIMUM_LECTURE_LENGTH: + raise serializers.ValidationError({"error": f"lecture has to be at least {self.MINIMUM_LECTURE_LENGTH} minutes"}) + + lecture_type = attrs["lecture_type"] + if lecture_type not in LectureTypes.values: + raise serializers.ValidationError({"error": f"invalid lecture type: '{lecture_type}'"}) + + course : Course = self.context.get("course") + for lecture in course.get_lectures(): + for timestamp in [start_time, end_time]: + if lecture.start_time < timestamp and timestamp < lecture.end_time: + raise serializers.ValidationError({"error": "there is already an active lecture during this time range"}) + return attrs \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index d77f9c6..2315469 100644 --- a/api/urls.py +++ b/api/urls.py @@ -18,6 +18,9 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from .views import ( + AddLectureView, + GetCourseLecturesView, + GetLectureView, MassEnrollCourseView, test, genAdmin, @@ -62,6 +65,16 @@ path('course/mass_enroll/', MassEnrollCourseView.as_view(), name='course_mass_enroll'), path('course/get/', GetCourseByName.as_view(), name='course_get'), path('course/getall/', GetCoursesAll.as_view(), name='course_getall'), + + path('course/lecture//get', GetCourseLecturesView.as_view(), name='course_get_lecture'), + path('course/lecture//add', AddLectureView.as_view(), name='course_add_lecture'), + # path('course/lecture//update', GetCourseByName.as_view(), name='lecture_add'), # TODO + # path('course/lecture//delete', GetCourseByName.as_view(), name='lecture_add'), + + path('lecture//get', GetLectureView.as_view(), name='lecture_get'), + path('lecture//student_att', GetLectureView.as_view(), name='lecture_get'), + path('lecture//teacher_att', GetLectureView.as_view(), name='lecture_get'), + # Documentation path('schema/', SpectacularAPIView.as_view(), name='schema'), diff --git a/api/views.py b/api/views.py index e07076b..d616705 100644 --- a/api/views.py +++ b/api/views.py @@ -13,8 +13,8 @@ from drf_spectacular.utils import extend_schema, OpenApiResponse from .permissions import IsTeacher, IsAdmin, IsStudent -from .models import Course, AccountRoles -from .serializers import CustomTokenSerializer, CreateUserSerializer, MassEnrollSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer +from .models import Course, AccountRoles, CourseLecture +from .serializers import AddLectureSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer import pdb @@ -110,11 +110,10 @@ class GetUserByUsername(generics.RetrieveAPIView): def get(self, _, username): queryset = self.get_queryset().filter(username=username) - serializer = self.serializer_class(queryset, many=True) - if len(serializer.data) == 0: + if not queryset: return Response({"error": f"user '{username}' not found"}, status=status.HTTP_404_NOT_FOUND) - else: - return Response(serializer.data, status=status.HTTP_200_OK) + serializer = self.serializer_class(queryset[0]) + return Response(serializer.data, status=status.HTTP_200_OK) class GetUsersByRole(generics.ListAPIView): authentication_classes = [JWTAuthentication] @@ -175,6 +174,35 @@ class DestroyCourseView(generics.DestroyAPIView): queryset = Course.objects.all() serializer_class = CourseSerializer +class GetCourseByName(generics.RetrieveAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsStudent] + + queryset = Course.objects.all() + serializer_class = CourseSerializer + + def get(self, _, pk): + queryset = self.get_queryset().filter(pk=pk) + if not queryset: + return Response({"error": f"course id '{pk}' not found"}, status=status.HTTP_404_NOT_FOUND) + serializer = self.serializer_class(queryset[0]) + return Response(serializer.data, status=status.HTTP_200_OK) + +class GetCoursesAll(generics.ListAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsStudent] + + queryset = Course.objects.all() + serializer_class = CourseSerializer + + def get(self, _): + queryset = self.get_queryset() + serializer = self.serializer_class(queryset, many=True) + if len(serializer.data) == 0: + return Response({"error": "no courses found"}, status=status.HTTP_404_NOT_FOUND) + else: + return Response(serializer.data, status=status.HTTP_200_OK) + class EnrollCourseView(generics.GenericAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsStudent] @@ -223,32 +251,68 @@ def post(self, request, *args, **kwargs): return Response({"ok": f"succesfully enrolled {len(usernames)} students"}, status=status.HTTP_200_OK) -class GetCourseByName(generics.RetrieveAPIView): +class GetCourseLecturesView(generics.ListAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsStudent] + lookup_field = 'pk' queryset = Course.objects.all() serializer_class = CourseSerializer - def get(self, _, pk): - queryset = self.get_queryset().filter(pk=pk) - serializer = self.serializer_class(queryset, many=True) + def get(self, _, *args, **kwargs): + course : Course = self.get_object() + lectures = course.get_lectures() + serializer = LectureSerializer(lectures, many=True) if len(serializer.data) == 0: - return Response({"error": f"course id '{pk}' not found"}, status=status.HTTP_404_NOT_FOUND) + return Response({"error": "no lectures found"}, status=status.HTTP_404_NOT_FOUND) else: return Response(serializer.data, status=status.HTTP_200_OK) -class GetCoursesAll(generics.ListAPIView): +class AddLectureView(generics.GenericAPIView): authentication_classes = [JWTAuthentication] - permission_classes = [IsStudent] + permission_classes = [IsTeacher] + lookup_field = 'pk' queryset = Course.objects.all() serializer_class = CourseSerializer + + def post(self, request, *args, **kwargs): + course : Course = self.get_object() + result = AddLectureSerializer(data=request.data, context={ "course": course }) + result.is_valid(raise_exception=True) + + data = result.data + course.add_lecture_to_course(data["start_time"], data["end_time"], data["lecture_type"]) + + return Response({"ok": f"successfully created lecture"}, status=status.HTTP_200_OK) + +class GetLectureView(generics.RetrieveAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsStudent] + + queryset = CourseLecture.objects.all() + serializer_class = LectureSerializer - def get(self, _): - queryset = self.get_queryset() - serializer = self.serializer_class(queryset, many=True) - if len(serializer.data) == 0: - return Response({"error": "no courses found"}, status=status.HTTP_404_NOT_FOUND) - else: - return Response(serializer.data, status=status.HTTP_200_OK) + def get(self, _, pk): + queryset = self.get_queryset().filter(pk=pk) + if not queryset: + return Response({"error": f"course id '{pk}' not found"}, status=status.HTTP_404_NOT_FOUND) + serializer = self.serializer_class(queryset[0]) + return Response(serializer.data, status=status.HTTP_200_OK) + +class SetStudentAttView(generics.GenericAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsStudent] + lookup_field = 'pk' + + queryset = CourseLecture.objects.all() + serializer_class = CourseLecture + + def post(self, request, *args, **kwargs): + if request.user.role != AccountRoles.STUDENT: + return Response({"error": f"only a student can set their attendence to a lecture"}, status=status.HTTP_200_OK) + + course : CourseLecture = self.get_object() + + + return Response({"ok": f"successfully set attendence"}, status=status.HTTP_200_OK) \ No newline at end of file From 9b22b0b32fe53a66700f07542a03e549c87a4b28 Mon Sep 17 00:00:00 2001 From: S-vz Date: Fri, 15 Mar 2024 01:06:32 +0100 Subject: [PATCH 02/11] set attendence --- api/serializers.py | 15 +++++++++++++++ api/urls.py | 6 ++++-- api/views.py | 29 +++++++++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 3a29904..0aa689a 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -119,4 +119,19 @@ def validate(self, attrs): if lecture.start_time < timestamp and timestamp < lecture.end_time: raise serializers.ValidationError({"error": "there is already an active lecture during this time range"}) + return attrs + +class SetAttendenceTeacherSerializer(serializers.Serializer): + usernames = serializers.ListField(required=True, allow_empty=False, child=serializers.CharField(max_length=150)) + + def validate_attendence(self, username): + user_query = User.objects.all().filter(username=username) + if not user_query: raise serializers.ValidationError({"error": f"user '{username}' does not exist"}) + if user_query[0].role != AccountRoles.STUDENT: raise serializers.ValidationError({"error": f"cannot set the attendence of a non-student: '{username}' is {user_query[0].role}"}) + + course : Course = self.context.get("course") + if not course.is_user_enrolled(user_query[0]): raise serializers.ValidationError({"error": f"user '{username}' is not enrolled in '{course.course_name}'"}) + + def validate(self, attrs): + for username in attrs["usernames"]: self.validate_enroll(username) return attrs \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index 2315469..5daad7c 100644 --- a/api/urls.py +++ b/api/urls.py @@ -22,6 +22,8 @@ GetCourseLecturesView, GetLectureView, MassEnrollCourseView, + SetStudentAttView, + SetTeacherAttView, test, genAdmin, @@ -72,8 +74,8 @@ # path('course/lecture//delete', GetCourseByName.as_view(), name='lecture_add'), path('lecture//get', GetLectureView.as_view(), name='lecture_get'), - path('lecture//student_att', GetLectureView.as_view(), name='lecture_get'), - path('lecture//teacher_att', GetLectureView.as_view(), name='lecture_get'), + path('lecture//student_att', SetStudentAttView.as_view(), name='lecture_att_student'), + path('lecture//teacher_att', SetTeacherAttView.as_view(), name='lecture_att_teacher'), # Documentation diff --git a/api/views.py b/api/views.py index d616705..ccd61ca 100644 --- a/api/views.py +++ b/api/views.py @@ -14,7 +14,7 @@ from .permissions import IsTeacher, IsAdmin, IsStudent from .models import Course, AccountRoles, CourseLecture -from .serializers import AddLectureSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer +from .serializers import AddLectureSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer import pdb @@ -310,9 +310,30 @@ class SetStudentAttView(generics.GenericAPIView): def post(self, request, *args, **kwargs): if request.user.role != AccountRoles.STUDENT: - return Response({"error": f"only a student can set their attendence to a lecture"}, status=status.HTTP_200_OK) + return Response({"error": f"cannot set the attendence of a non-student"}, status=status.HTTP_200_OK) - course : CourseLecture = self.get_object() + lecture : CourseLecture = self.get_object() + lecture.set_attendence_user(request.user, teacher=False) + return Response({"ok": f"successfully set attendence"}, status=status.HTTP_200_OK) + +class SetTeacherAttView(generics.GenericAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsTeacher] + lookup_field = 'pk' + + queryset = CourseLecture.objects.all() + serializer_class = CourseLecture + + def post(self, request, *args, **kwargs): + lecture : CourseLecture = self.get_object() + result = SetAttendenceTeacherSerializer(data=request.data, context={ "course": lecture.course }) + result.is_valid(raise_exception=True) + + queryset = User.objects.all() + usernames = result.data["usernames"] + for username in usernames: + user = queryset.filter(username=username)[0] + lecture.set_attendence_user(user, teacher=True) - return Response({"ok": f"successfully set attendence"}, status=status.HTTP_200_OK) \ No newline at end of file + return Response({"ok": f"succesfully set attendence for {len(usernames)} students"}, status=status.HTTP_200_OK) \ No newline at end of file From 7cc13fcd50aa3b12ba8922d974e9fe4c57ba6c64 Mon Sep 17 00:00:00 2001 From: S-vz Date: Fri, 15 Mar 2024 02:16:36 +0100 Subject: [PATCH 03/11] pass enrollment status in course view --- api/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/views.py b/api/views.py index ccd61ca..2758e4a 100644 --- a/api/views.py +++ b/api/views.py @@ -195,12 +195,15 @@ class GetCoursesAll(generics.ListAPIView): queryset = Course.objects.all() serializer_class = CourseSerializer - def get(self, _): + def get(self, request): queryset = self.get_queryset() serializer = self.serializer_class(queryset, many=True) if len(serializer.data) == 0: return Response({"error": "no courses found"}, status=status.HTTP_404_NOT_FOUND) else: + for course_serialized in serializer.data: + course : Course = queryset.filter(pk=course_serialized["id"])[0] + course_serialized["enrolled"] = course.is_user_enrolled(user=request.user) return Response(serializer.data, status=status.HTTP_200_OK) class EnrollCourseView(generics.GenericAPIView): From ffcf82344320528c1d82194fbd5266411d76b902 Mon Sep 17 00:00:00 2001 From: S-vz Date: Fri, 15 Mar 2024 10:16:38 +0100 Subject: [PATCH 04/11] minor fix get and create leectures --- api/serializers.py | 9 +++++---- api/views.py | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/api/serializers.py b/api/serializers.py index 0aa689a..e1383a9 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -102,6 +102,7 @@ class AddLectureSerializer(serializers.Serializer): def validate(self, attrs): start_time, end_time = attrs["start_time"], attrs["end_time"] + if start_time > end_time: raise serializers.ValidationError({"error": f"end_time has to be after start_time"}) @@ -115,10 +116,10 @@ def validate(self, attrs): course : Course = self.context.get("course") for lecture in course.get_lectures(): - for timestamp in [start_time, end_time]: - if lecture.start_time < timestamp and timestamp < lecture.end_time: - raise serializers.ValidationError({"error": "there is already an active lecture during this time range"}) - + if lecture.start_time <= start_time and start_time < lecture.end_time: + raise serializers.ValidationError({"error": "there is already an active lecture during this time range"}) + if lecture.start_time < end_time and end_time <= lecture.end_time: + raise serializers.ValidationError({"error": "there is already an active lecture during this time range"}) return attrs class SetAttendenceTeacherSerializer(serializers.Serializer): diff --git a/api/views.py b/api/views.py index 2758e4a..e320ec7 100644 --- a/api/views.py +++ b/api/views.py @@ -1,3 +1,4 @@ +import datetime import os from django.http import JsonResponse from django.contrib.auth import get_user_model @@ -264,7 +265,7 @@ class GetCourseLecturesView(generics.ListAPIView): def get(self, _, *args, **kwargs): course : Course = self.get_object() - lectures = course.get_lectures() + lectures = course.get_lectures() serializer = LectureSerializer(lectures, many=True) if len(serializer.data) == 0: return Response({"error": "no lectures found"}, status=status.HTTP_404_NOT_FOUND) @@ -285,7 +286,9 @@ def post(self, request, *args, **kwargs): result.is_valid(raise_exception=True) data = result.data - course.add_lecture_to_course(data["start_time"], data["end_time"], data["lecture_type"]) + start_time = datetime.datetime.fromisoformat(data["start_time"]) + end_time = datetime.datetime.fromisoformat(data["end_time"]) + course.add_lecture_to_course(start_time, end_time, data["lecture_type"]) return Response({"ok": f"successfully created lecture"}, status=status.HTTP_200_OK) From 5f1e6af4661f79891c5d9f00658be4890c55c21e Mon Sep 17 00:00:00 2001 From: S-vz Date: Fri, 15 Mar 2024 10:40:42 +0100 Subject: [PATCH 05/11] schedule view backend --- api/models.py | 2 +- api/urls.py | 2 ++ api/views.py | 24 +++++++++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/api/models.py b/api/models.py index 0fd4021..37dce96 100644 --- a/api/models.py +++ b/api/models.py @@ -50,7 +50,7 @@ def clean(self): super().clean() self.email = self.__class__.objects.normalize_email(self.email) - def get_classes(self): + def get_enrolled_courses(self): return [uc.course for uc in UserCourse.objects.filter(user__id=self.id)] class LectureTypes(models.TextChoices): diff --git a/api/urls.py b/api/urls.py index 5daad7c..da547b2 100644 --- a/api/urls.py +++ b/api/urls.py @@ -21,6 +21,7 @@ AddLectureView, GetCourseLecturesView, GetLectureView, + GetScheduleView, MassEnrollCourseView, SetStudentAttView, SetTeacherAttView, @@ -77,6 +78,7 @@ path('lecture//student_att', SetStudentAttView.as_view(), name='lecture_att_student'), path('lecture//teacher_att', SetTeacherAttView.as_view(), name='lecture_att_teacher'), + path('schedule/get', GetScheduleView.as_view(), name='schedule_get'), # Documentation path('schema/', SpectacularAPIView.as_view(), name='schema'), diff --git a/api/views.py b/api/views.py index e320ec7..3d6d3a4 100644 --- a/api/views.py +++ b/api/views.py @@ -342,4 +342,26 @@ def post(self, request, *args, **kwargs): user = queryset.filter(username=username)[0] lecture.set_attendence_user(user, teacher=True) - return Response({"ok": f"succesfully set attendence for {len(usernames)} students"}, status=status.HTTP_200_OK) \ No newline at end of file + return Response({"ok": f"succesfully set attendence for {len(usernames)} students"}, status=status.HTTP_200_OK) + +class GetScheduleView(generics.GenericAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsStudent] + + def get(self, request, *args, **kwargs): + user = User.objects.all().filter(username=request.user.username)[0] + courses = user.get_enrolled_courses() + all_lectures = [] + for course in courses: + lectures_obj = course.get_lectures() + lectures = LectureSerializer(lectures_obj, many=True) + for i, lecture_obj in enumerate(lectures_obj): + att = lecture_obj.get_attendence_user(user) + lectures.data[i]["attended_student"] = att.attended_student if att is not None else False + lectures.data[i]["attended_teacher"] = att.attended_teacher if att is not None else False + all_lectures += lectures.data + + # Sort chronological order + all_lectures.sort(key= lambda x : datetime.datetime.fromisoformat(x["start_time"])) + + return Response(all_lectures) \ No newline at end of file From 7bfc4e9250801bc7cb27ab82b5b040e2e3ae8446 Mon Sep 17 00:00:00 2001 From: S-vz Date: Fri, 15 Mar 2024 23:27:26 +0100 Subject: [PATCH 06/11] update get schedule to per year and week --- api/models.py | 5 ++++- api/urls.py | 2 +- api/views.py | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/api/models.py b/api/models.py index 37dce96..5d81de7 100644 --- a/api/models.py +++ b/api/models.py @@ -74,7 +74,10 @@ def get_teachers(self): return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.TEACHER)] def get_lectures(self): - return [lecture for lecture in CourseLecture.objects.filter(course=self)] + return CourseLecture.objects.filter(course=self) + + def get_lectures_week(self, year : int, week : int): + return CourseLecture.objects.filter(course=self, start_time__year=year, start_time__week=week) def is_user_enrolled(self, user : User): return bool(UserCourse.objects.filter(user=user, course=self)) diff --git a/api/urls.py b/api/urls.py index da547b2..e81c3ed 100644 --- a/api/urls.py +++ b/api/urls.py @@ -78,7 +78,7 @@ path('lecture//student_att', SetStudentAttView.as_view(), name='lecture_att_student'), path('lecture//teacher_att', SetTeacherAttView.as_view(), name='lecture_att_teacher'), - path('schedule/get', GetScheduleView.as_view(), name='schedule_get'), + path('schedule/get//', GetScheduleView.as_view(), name='schedule_get'), # Documentation path('schema/', SpectacularAPIView.as_view(), name='schema'), diff --git a/api/views.py b/api/views.py index 3d6d3a4..0f04ef5 100644 --- a/api/views.py +++ b/api/views.py @@ -1,5 +1,6 @@ import datetime import os +from typing import List from django.http import JsonResponse from django.contrib.auth import get_user_model @@ -344,19 +345,26 @@ def post(self, request, *args, **kwargs): return Response({"ok": f"succesfully set attendence for {len(usernames)} students"}, status=status.HTTP_200_OK) + class GetScheduleView(generics.GenericAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsStudent] - def get(self, request, *args, **kwargs): + def get(self, request, year, week): + if not year.isdigit() or int(year) < 1970: + return Response({"error": f"invalid year parameter"}, status=status.HTTP_400_BAD_REQUEST) + if not week.isdigit() or int(week) < 0 or int(week) > 52: + return Response({"error": f"invalid week parameter"}, status=status.HTTP_400_BAD_REQUEST) + user = User.objects.all().filter(username=request.user.username)[0] - courses = user.get_enrolled_courses() + courses : List[Course] = user.get_enrolled_courses() all_lectures = [] - for course in courses: - lectures_obj = course.get_lectures() + for course in courses: + lectures_obj = course.get_lectures_week(int(year), int(week)) lectures = LectureSerializer(lectures_obj, many=True) for i, lecture_obj in enumerate(lectures_obj): att = lecture_obj.get_attendence_user(user) + lectures.data[i]["course"] = lecture_obj.course.course_name lectures.data[i]["attended_student"] = att.attended_student if att is not None else False lectures.data[i]["attended_teacher"] = att.attended_teacher if att is not None else False all_lectures += lectures.data From 9a4708dcb212e7853595012d165d8016eda175d6 Mon Sep 17 00:00:00 2001 From: S-vz Date: Sun, 17 Mar 2024 17:28:58 +0100 Subject: [PATCH 07/11] support setting and unsetting of attendence --- ...knowledgement_attended_student_and_more.py | 23 +++++++++++ api/models.py | 10 ++--- api/serializers.py | 9 +++-- api/urls.py | 4 +- api/views.py | 39 +++++++++++++------ 5 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 api/migrations/0006_alter_attendenceacknowledgement_attended_student_and_more.py diff --git a/api/migrations/0006_alter_attendenceacknowledgement_attended_student_and_more.py b/api/migrations/0006_alter_attendenceacknowledgement_attended_student_and_more.py new file mode 100644 index 0000000..ffaad58 --- /dev/null +++ b/api/migrations/0006_alter_attendenceacknowledgement_attended_student_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.2 on 2024-03-17 14:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_remove_course_schedule'), + ] + + operations = [ + migrations.AlterField( + model_name='attendenceacknowledgement', + name='attended_student', + field=models.BooleanField(default=None, null=True), + ), + migrations.AlterField( + model_name='attendenceacknowledgement', + name='attended_teacher', + field=models.BooleanField(default=None, null=True), + ), + ] diff --git a/api/models.py b/api/models.py index 5d81de7..fc8493b 100644 --- a/api/models.py +++ b/api/models.py @@ -102,13 +102,13 @@ class CourseLecture(models.Model): default=LectureTypes.LECTURE, ) - def set_attendence_user(self, student : User, teacher=False): + def set_attendence_user(self, student : User, attended : bool, teacher=False): queryset = AttendenceAcknowledgement.objects.filter(lecture=self, student=student) if not queryset: ack = AttendenceAcknowledgement.objects.create(lecture=self, student=student) else: ack = queryset[0] - if teacher: ack.attended_teacher = True - else: ack.attended_student = True + if teacher: ack.attended_teacher = attended + else: ack.attended_student = attended ack.save() def get_attendence_user(self, student : User): @@ -120,8 +120,8 @@ def get_attendence(self): return [] if not queryset else queryset[0] class AttendenceAcknowledgement(models.Model): - attended_student = models.BooleanField(default=False) - attended_teacher = models.BooleanField(default=False) + attended_student = models.BooleanField(default=None, null=True) + attended_teacher = models.BooleanField(default=None, null=True) student = models.ForeignKey(User, null=False, related_name='user_ack', on_delete=models.CASCADE) lecture = models.ForeignKey(CourseLecture, null=False, related_name='lecture_ack', on_delete=models.CASCADE) diff --git a/api/serializers.py b/api/serializers.py index e1383a9..e036db4 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -123,10 +123,13 @@ def validate(self, attrs): return attrs class SetAttendenceTeacherSerializer(serializers.Serializer): - usernames = serializers.ListField(required=True, allow_empty=False, child=serializers.CharField(max_length=150)) + usernames = serializers.DictField(required=True, allow_empty=False, child= + serializers.CharField(max_length=150) + ) - def validate_attendence(self, username): + def validate_attendence(self, username, attended): user_query = User.objects.all().filter(username=username) + if (attended := attended.lower()) not in ["true", "false"]: raise serializers.ValidationError({"error": f"invalid attendence state: '{attended}'"}) if not user_query: raise serializers.ValidationError({"error": f"user '{username}' does not exist"}) if user_query[0].role != AccountRoles.STUDENT: raise serializers.ValidationError({"error": f"cannot set the attendence of a non-student: '{username}' is {user_query[0].role}"}) @@ -134,5 +137,5 @@ def validate_attendence(self, username): if not course.is_user_enrolled(user_query[0]): raise serializers.ValidationError({"error": f"user '{username}' is not enrolled in '{course.course_name}'"}) def validate(self, attrs): - for username in attrs["usernames"]: self.validate_enroll(username) + for username, attended in attrs["usernames"].items(): self.validate_attendence(username, attended) return attrs \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index e81c3ed..51c3904 100644 --- a/api/urls.py +++ b/api/urls.py @@ -25,6 +25,7 @@ MassEnrollCourseView, SetStudentAttView, SetTeacherAttView, + UnsetStudentAttView, test, genAdmin, @@ -75,7 +76,8 @@ # path('course/lecture//delete', GetCourseByName.as_view(), name='lecture_add'), path('lecture//get', GetLectureView.as_view(), name='lecture_get'), - path('lecture//student_att', SetStudentAttView.as_view(), name='lecture_att_student'), + path('lecture//student_set_att', SetStudentAttView.as_view(), name='lecture_set_att_student'), + path('lecture//student_unset_att', UnsetStudentAttView.as_view(), name='lecture_unset_att_student'), path('lecture//teacher_att', SetTeacherAttView.as_view(), name='lecture_att_teacher'), path('schedule/get//', GetScheduleView.as_view(), name='schedule_get'), diff --git a/api/views.py b/api/views.py index 0f04ef5..69717e0 100644 --- a/api/views.py +++ b/api/views.py @@ -307,6 +307,16 @@ def get(self, _, pk): serializer = self.serializer_class(queryset[0]) return Response(serializer.data, status=status.HTTP_200_OK) +def setAttendence(self, request, attended): + print(request.user) + if request.user.role != AccountRoles.STUDENT: + return Response({"error": f"cannot set the attendence of a non-student"}, status=status.HTTP_200_OK) + + lecture : CourseLecture = self.get_object() + lecture.set_attendence_user(request.user, attended=attended, teacher=False) + + return Response({"ok": f"successfully set attendence"}, status=status.HTTP_200_OK) + class SetStudentAttView(generics.GenericAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsStudent] @@ -316,13 +326,20 @@ class SetStudentAttView(generics.GenericAPIView): serializer_class = CourseLecture def post(self, request, *args, **kwargs): - if request.user.role != AccountRoles.STUDENT: - return Response({"error": f"cannot set the attendence of a non-student"}, status=status.HTTP_200_OK) - - lecture : CourseLecture = self.get_object() - lecture.set_attendence_user(request.user, teacher=False) + print(request.user) + return setAttendence(self, request, True) - return Response({"ok": f"successfully set attendence"}, status=status.HTTP_200_OK) +class UnsetStudentAttView(generics.GenericAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsStudent] + lookup_field = 'pk' + + queryset = CourseLecture.objects.all() + serializer_class = CourseLecture + + def post(self, request, *args, **kwargs): + print(request.user) + return setAttendence(self, request, False) class SetTeacherAttView(generics.GenericAPIView): authentication_classes = [JWTAuthentication] @@ -339,9 +356,9 @@ def post(self, request, *args, **kwargs): queryset = User.objects.all() usernames = result.data["usernames"] - for username in usernames: + for username, attended in usernames.items(): user = queryset.filter(username=username)[0] - lecture.set_attendence_user(user, teacher=True) + lecture.set_attendence_user(user, attended=(attended=="true"), teacher=True) return Response({"ok": f"succesfully set attendence for {len(usernames)} students"}, status=status.HTTP_200_OK) @@ -364,9 +381,9 @@ def get(self, request, year, week): lectures = LectureSerializer(lectures_obj, many=True) for i, lecture_obj in enumerate(lectures_obj): att = lecture_obj.get_attendence_user(user) - lectures.data[i]["course"] = lecture_obj.course.course_name - lectures.data[i]["attended_student"] = att.attended_student if att is not None else False - lectures.data[i]["attended_teacher"] = att.attended_teacher if att is not None else False + lectures.data[i]["course_name"] = lecture_obj.course.course_name + lectures.data[i]["attended_student"] = att.attended_student if att is not None else None + lectures.data[i]["attended_teacher"] = att.attended_teacher if att is not None else None all_lectures += lectures.data # Sort chronological order From f2c2c9c718f483965271a88518d506cf23dbabfe Mon Sep 17 00:00:00 2001 From: S-vz Date: Sun, 17 Mar 2024 17:29:12 +0100 Subject: [PATCH 08/11] remove prints --- api/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/views.py b/api/views.py index 69717e0..9cfa631 100644 --- a/api/views.py +++ b/api/views.py @@ -308,7 +308,6 @@ def get(self, _, pk): return Response(serializer.data, status=status.HTTP_200_OK) def setAttendence(self, request, attended): - print(request.user) if request.user.role != AccountRoles.STUDENT: return Response({"error": f"cannot set the attendence of a non-student"}, status=status.HTTP_200_OK) @@ -326,7 +325,6 @@ class SetStudentAttView(generics.GenericAPIView): serializer_class = CourseLecture def post(self, request, *args, **kwargs): - print(request.user) return setAttendence(self, request, True) class UnsetStudentAttView(generics.GenericAPIView): From dcfe016def68eba9e09a34a56482cb1037323354 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 18 Mar 2024 03:42:50 +0100 Subject: [PATCH 09/11] password reset done --- .env_example | 18 ++++++++ api/apps.py | 3 ++ api/serializers.py | 5 +- api/settings.py | 31 +++++++++++-- api/signals.py | 46 +++++++++++++++++++ api/templates/email/password_reset_email.html | 9 ++++ api/templates/email/password_reset_email.txt | 9 ++++ api/urls.py | 16 +++++-- api/views.py | 30 +++++++++++- 9 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 .env_example create mode 100644 api/signals.py create mode 100644 api/templates/email/password_reset_email.html create mode 100644 api/templates/email/password_reset_email.txt diff --git a/.env_example b/.env_example new file mode 100644 index 0000000..76775ea --- /dev/null +++ b/.env_example @@ -0,0 +1,18 @@ +SECRET_KEY= + +DB_NAME= +DB_HOST= +DB_USER= +DB_PORT= +DB_PASSWORD= + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_STORAGE_BUCKET_NAME= + +EMAIL_HOST= +EMAIL_PORT= +EMAIL_USER= +EMAIL_PASSWORD= + +FRONTEND_URL= # No / after url diff --git a/api/apps.py b/api/apps.py index 66656fd..9f06b4a 100644 --- a/api/apps.py +++ b/api/apps.py @@ -4,3 +4,6 @@ class ApiConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'api' + + def ready(self): + import api.signals diff --git a/api/serializers.py b/api/serializers.py index e1383a9..bed8216 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -135,4 +135,7 @@ def validate_attendence(self, username): def validate(self, attrs): for username in attrs["usernames"]: self.validate_enroll(username) - return attrs \ No newline at end of file + return attrs + +class MailTestSerializer(serializers.Serializer): + email = serializers.CharField(required=True) \ No newline at end of file diff --git a/api/settings.py b/api/settings.py index 4d7d6d7..f317049 100644 --- a/api/settings.py +++ b/api/settings.py @@ -27,7 +27,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'api', 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -81,8 +81,9 @@ 'django.contrib.staticfiles', 'corsheaders', 'rest_framework', - 'drf_spectacular', + 'django_rest_passwordreset', 'rest_framework_simplejwt.token_blacklist', + 'drf_spectacular', "api.apps.ApiConfig" ] @@ -92,6 +93,15 @@ AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID') +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = config('EMAIL_HOST') +EMAIL_PORT = config('EMAIL_PORT') +EMAIL_USE_TLS = True +EMAIL_HOST_USER = config('EMAIL_USER') +EMAIL_HOST_PASSWORD = config('EMAIL_PASSWORD') + +FRONTEND_URL = config('FRONTEND_URL') + CORS_ALLOW_HEADERS = [ 'x-requested-with', 'content-type', @@ -141,4 +151,19 @@ 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'AUTH_HEADER_TYPES': ('JWT',), -} \ No newline at end of file +} + +DJANGO_REST_PASSWORDRESET_TOKEN_CONFIG = { + "CLASS": "django_rest_passwordreset.tokens.RandomStringTokenGenerator", + "OPTIONS": { + "min_length": 20, + "max_length": 30 + } +} +DJANGO_REST_MULTITOKENAUTH_RESET_TOKEN_EXPIRY_TIME = 30 # minutes + +DJANGO_REST_PASSWORDRESET_EMAIL_TEMPLATES = { + 'subject': 'templates/email/password_reset_subject.txt', + 'plain_body': 'templates/email/password_reset_email.html', + 'html_body': 'templates/email/password_reset_email.html', +} diff --git a/api/signals.py b/api/signals.py new file mode 100644 index 0000000..8d97a63 --- /dev/null +++ b/api/signals.py @@ -0,0 +1,46 @@ +from django.core.mail import EmailMultiAlternatives +from django.dispatch import receiver +from django.template.loader import render_to_string +from django.urls import reverse +from django.conf import settings + + +from django_rest_passwordreset.signals import reset_password_token_created + + +@receiver(reset_password_token_created) +def password_reset_token_created(sender, instance, reset_password_token, *args, **kwargs): + """ + Handles password reset tokens + When a token is created, an e-mail needs to be sent to the user + :param sender: View Class that sent the signal + :param instance: View Instance that sent the signal + :param reset_password_token: Token Model Object + :param args: + :param kwargs: + :return: + """ + # send an e-mail to the user + context = { + 'current_user': reset_password_token.user, + 'username': reset_password_token.user.username, + 'email': reset_password_token.user.email, + 'reset_password_url': f"{settings.FRONTEND_URL}/reset_password?token={reset_password_token.key}" + } + + # render email text + email_html_message = render_to_string('email/password_reset_email.html', context) + email_plaintext_message = render_to_string('email/password_reset_email.txt', context) + + msg = EmailMultiAlternatives( + # title: + "Password Reset for Attendunce", + # message: + email_plaintext_message, + # from: + "noreply@somehost.local", # Does not matter, it will be overridden + # to: + [reset_password_token.user.email] + ) + msg.attach_alternative(email_html_message, "text/html") + msg.send() \ No newline at end of file diff --git a/api/templates/email/password_reset_email.html b/api/templates/email/password_reset_email.html new file mode 100644 index 0000000..fbdc1b4 --- /dev/null +++ b/api/templates/email/password_reset_email.html @@ -0,0 +1,9 @@ +

Hello,

+ +

We received a request to reset the password for your account. If you made this request, please click on the link below or copy and paste it into your browser to complete the process:

+ +

{{ reset_password_url }}

+ +

The link will be valid for the next 30 minutes

+ +

If you did not request to reset your password, please ignore this email and your password will remain unchanged.

diff --git a/api/templates/email/password_reset_email.txt b/api/templates/email/password_reset_email.txt new file mode 100644 index 0000000..7455b8c --- /dev/null +++ b/api/templates/email/password_reset_email.txt @@ -0,0 +1,9 @@ +Hello, + +We received a request to reset the password for your account. If you made this request, please click on the link below or copy and paste it into your browser to complete the process:

+ +{{ reset_password_url }} + +The link will be valid for the next 30 minutes + +If you did not request to reset your password, please ignore this email and your password will remain unchanged. diff --git a/api/urls.py b/api/urls.py index a8e9f82..a5398c6 100644 --- a/api/urls.py +++ b/api/urls.py @@ -13,9 +13,10 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ -from django.urls import path +from django.urls import include, path from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView, TokenBlacklistView from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView +from django_rest_passwordreset.views import ResetPasswordConfirm, ResetPasswordValidateToken from .views import ( AddLectureView, GetCourseLecturesView, @@ -24,7 +25,8 @@ MassEnrollCourseView, SetStudentAttView, SetTeacherAttView, - test, + test, + MailTestView, genAdmin, GetTokenView, @@ -40,11 +42,12 @@ DestroyCourseView, EnrollCourseView, GetCourseByName, - GetCoursesAll, - ) + GetCoursesAll + ) urlpatterns = [ path('test', test), + path('send_welcome_email', MailTestView.as_view(), name='send_welcome_email'), path('genadmin', genAdmin), # Manage tokens, by requesting one with credentials, refreshing or verifying one. Essentially the login API @@ -52,6 +55,11 @@ path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), path('token/blacklist/', TokenBlacklistView.as_view(), name='token_blacklist'), + + # Password resets + path('password_reset/', include('django_rest_passwordreset.urls', namespace='password_reset')), + path('password_reset/confirm', ResetPasswordConfirm.as_view(), name='password_reset_confirm'), + path('password_reset/validate_token', ResetPasswordValidateToken.as_view(), name='password_reset_validate_token'), # All user paths path('user/register/', CreateUserView.as_view(), name='user_register'), diff --git a/api/views.py b/api/views.py index 3d6d3a4..b16af92 100644 --- a/api/views.py +++ b/api/views.py @@ -15,9 +15,9 @@ from .permissions import IsTeacher, IsAdmin, IsStudent from .models import Course, AccountRoles, CourseLecture -from .serializers import AddLectureSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer +from .serializers import AddLectureSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer, MailTestSerializer -import pdb +from django.core.mail import send_mail User = get_user_model() @@ -36,6 +36,32 @@ def test(request): return Response({"ping": "pong"}, 200) +@extend_schema( + summary="Send Test Email", + description="Tries to send a test email.", + responses={ + status.HTTP_200_OK: OpenApiResponse(description="Success - An email was sent."), + status.HTTP_400_BAD_REQUEST: OpenApiResponse(description="Failure - No email given as a parameter."), + status.HTTP_418_IM_A_TEAPOT: OpenApiResponse(description="Failure - The email could not be sent.") + } +) +class MailTestView(generics.CreateAPIView): + serializer_class = MailTestSerializer + + def post(self, request, *args, **kwargs): + subject = 'Welcome to My Site' + message = 'Thank you for creating an account!' + from_email = 'admin@mysite.com' + recipient_list = [request.POST.get("email", "")] + if recipient_list[0] == "": + Response({"status": "fail"}, 400) + ret_code = send_mail(subject, message, from_email, recipient_list) + if ret_code == 1: + Response({"status": "success"}, 200) + else: + Response({"status": "fail", "return_code": ret_code}, 418) + + # DEBUG function # Generate an admin account if no admin accounts exists # Dangerous endpoint which should be turned off in production but can be used to setup the DB From 79317039c0c592b887d7f0960afe1d1d7e2ee7e2 Mon Sep 17 00:00:00 2001 From: S-vz Date: Mon, 18 Mar 2024 15:50:40 +0100 Subject: [PATCH 10/11] course page + attendence stats --- api/models.py | 4 ++-- api/serializers.py | 7 ++++++- api/urls.py | 4 +++- api/views.py | 47 ++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/api/models.py b/api/models.py index fc8493b..fc2bdbf 100644 --- a/api/models.py +++ b/api/models.py @@ -68,10 +68,10 @@ def __str__(self): return str(self.name) def get_enrolled_students(self): - return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.STUDENT)] + return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.STUDENT, course=self)] def get_teachers(self): - return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.TEACHER)] + return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.TEACHER, course=self)] def get_lectures(self): return CourseLecture.objects.filter(course=self) diff --git a/api/serializers.py b/api/serializers.py index e036db4..97a3e14 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -24,7 +24,12 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = '__all__' - + +class CourseUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["username", "first_name", "last_name", "role"] + class CourseSerializer(serializers.ModelSerializer): class Meta: model = Course diff --git a/api/urls.py b/api/urls.py index cfe2858..1dd8a75 100644 --- a/api/urls.py +++ b/api/urls.py @@ -19,6 +19,7 @@ from .views import ( AddLectureView, GetCourseLecturesView, + GetFullCoursePage, GetLectureView, GetScheduleView, MassEnrollCourseView, @@ -67,9 +68,10 @@ path('course/delete/', DestroyCourseView.as_view(), name='course_delete'), path('course/enroll/', EnrollCourseView.as_view(), name='course_enroll'), path('course/mass_enroll/', MassEnrollCourseView.as_view(), name='course_mass_enroll'), - path('course/get/', GetCourseByName.as_view(), name='course_get'), + path('course/get/', GetFullCoursePage.as_view(), name='course_get'), path('course/getall/', GetCoursesAll.as_view(), name='course_getall'), + path('course/lecture//get', GetCourseLecturesView.as_view(), name='course_get_lecture'), path('course/lecture//add', AddLectureView.as_view(), name='course_add_lecture'), # path('course/lecture//update', GetCourseByName.as_view(), name='lecture_add'), # TODO diff --git a/api/views.py b/api/views.py index 9cfa631..8ebe840 100644 --- a/api/views.py +++ b/api/views.py @@ -16,7 +16,7 @@ from .permissions import IsTeacher, IsAdmin, IsStudent from .models import Course, AccountRoles, CourseLecture -from .serializers import AddLectureSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer +from .serializers import AddLectureSerializer, CourseUserSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer import pdb @@ -190,6 +190,48 @@ def get(self, _, pk): serializer = self.serializer_class(queryset[0]) return Response(serializer.data, status=status.HTTP_200_OK) +class GetFullCoursePage(generics.RetrieveAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsStudent] + + queryset = Course.objects.all() + serializer_class = CourseSerializer + + def get_attendence_stats(self, course, user): + lectures : List[CourseLecture] = course.get_lectures() + attendence_stats = {"attended": 0, "missed": 0} + for lecture in lectures: + if lecture.end_time > datetime.datetime.now(lecture.end_time.tzinfo): continue + att = lecture.get_attendence_user(user) + if att is None or not (att.attended_student and att.attended_teacher): + attendence_stats["missed"] += 1 + else: attendence_stats["attended"] += 1 + return attendence_stats + + def get(self, request, pk): + queryset = self.get_queryset().filter(pk=pk) + if not queryset: + return Response({"error": f"course id '{pk}' not found"}, status=status.HTTP_404_NOT_FOUND) + + course : Course = queryset[0] + teachers = course.get_teachers() + students = course.get_enrolled_students() + + response_data = {} + response_data["id"] = pk + response_data["course_name"] = course.course_name + response_data["num_teachers"] = len(teachers) + response_data["num_students"] = len(students) + response_data["attended"] = -1 + response_data["missed"] = -1 + response_data["users"] = CourseUserSerializer((teachers + students), many=True).data + + user = User.objects.all().filter(username=request.user.username)[0] + if user.role == AccountRoles.STUDENT: + response_data |= self.get_attendence_stats(course, user) + + return Response(response_data, status=status.HTTP_200_OK) + class GetCoursesAll(generics.ListAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsStudent] @@ -387,4 +429,5 @@ def get(self, request, year, week): # Sort chronological order all_lectures.sort(key= lambda x : datetime.datetime.fromisoformat(x["start_time"])) - return Response(all_lectures) \ No newline at end of file + return Response(all_lectures, status=status.HTTP_200_OK) + \ No newline at end of file From 91592f79bf2ab977efd1589d8f1a0a40ca0d2647 Mon Sep 17 00:00:00 2001 From: S-vz Date: Tue, 19 Mar 2024 01:28:21 +0100 Subject: [PATCH 11/11] allow disenrollment from course --- api/models.py | 5 +++++ api/urls.py | 2 ++ api/views.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/api/models.py b/api/models.py index fc2bdbf..4831096 100644 --- a/api/models.py +++ b/api/models.py @@ -82,6 +82,11 @@ def get_lectures_week(self, year : int, week : int): def is_user_enrolled(self, user : User): return bool(UserCourse.objects.filter(user=user, course=self)) + def remove_user_from_course(self, user : User): + queryset = UserCourse.objects.filter(user=user, course=self) + if not queryset: return + queryset[0].delete() + def add_user_to_course(self, user: User): UserCourse.objects.create(user=user, course=self).save() diff --git a/api/urls.py b/api/urls.py index 1d99fb3..f5b569e 100644 --- a/api/urls.py +++ b/api/urls.py @@ -19,6 +19,7 @@ from django_rest_passwordreset.views import ResetPasswordConfirm, ResetPasswordValidateToken from .views import ( AddLectureView, + DisenrollCourseView, GetCourseLecturesView, GetFullCoursePage, GetLectureView, @@ -75,6 +76,7 @@ path('course/update/', UpdateCourseView.as_view(), name='course_update'), path('course/delete/', DestroyCourseView.as_view(), name='course_delete'), path('course/enroll/', EnrollCourseView.as_view(), name='course_enroll'), + path('course/disenroll/', DisenrollCourseView.as_view(), name='course_enroll'), path('course/mass_enroll/', MassEnrollCourseView.as_view(), name='course_mass_enroll'), path('course/get/', GetFullCoursePage.as_view(), name='course_get'), path('course/getall/', GetCoursesAll.as_view(), name='course_getall'), diff --git a/api/views.py b/api/views.py index 76b01b1..9c8296a 100644 --- a/api/views.py +++ b/api/views.py @@ -17,7 +17,7 @@ from .permissions import IsTeacher, IsAdmin, IsStudent from .models import Course, AccountRoles, CourseLecture -from .serializers import AddLectureSerializer, CourseUserSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer +from .serializers import AddLectureSerializer, CourseUserSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MailTestSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer @@ -242,6 +242,7 @@ def get(self, request, pk): if not queryset: return Response({"error": f"course id '{pk}' not found"}, status=status.HTTP_404_NOT_FOUND) + user = User.objects.all().filter(username=request.user.username)[0] course : Course = queryset[0] teachers = course.get_teachers() students = course.get_enrolled_students() @@ -251,11 +252,11 @@ def get(self, request, pk): response_data["course_name"] = course.course_name response_data["num_teachers"] = len(teachers) response_data["num_students"] = len(students) + response_data["enrolled"] = course.is_user_enrolled(user=user) response_data["attended"] = -1 response_data["missed"] = -1 response_data["users"] = CourseUserSerializer((teachers + students), many=True).data - user = User.objects.all().filter(username=request.user.username)[0] if user.role == AccountRoles.STUDENT: response_data |= self.get_attendence_stats(course, user) @@ -305,6 +306,29 @@ def post(self, request, *args, **kwargs): return Response({"ok": f"succesfully enrolled {username} in {obj.course_name}"}, status=status.HTTP_200_OK) +class DisenrollCourseView(generics.GenericAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsStudent] + lookup_field = 'pk' + + queryset = Course.objects.all() + serializer_class = CourseSerializer + + def post(self, request, *args, **kwargs): + obj : Course = self.get_object() + user = request.user + username = user.username + + if not obj.is_user_enrolled(request.user): + return Response({"error": f"{username} is not enrolled in {obj.course_name}"}, status=status.HTTP_400_BAD_REQUEST) + + obj.remove_user_from_course(user) + + if getattr(obj, '_prefetched_objects_cache', None): + obj._prefetched_objects_cache = {} + + return Response({"ok": f"succesfully disenrolled {username} from {obj.course_name}"}, status=status.HTTP_200_OK) + class MassEnrollCourseView(generics.GenericAPIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAdmin]