Skip to content

Commit 24b7bbd

Browse files
authored
Merge pull request #15 from DevOps-Cloud-Team5/SCRUM-88-enroll-api
enroll invididual students and allow admins to mass enroll
2 parents 31fdfdd + 246bda7 commit 24b7bbd

6 files changed

+106
-35
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.0.2 on 2024-03-14 11:00
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api', '0003_courselecture_attendenceacknowledgement'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='attendenceacknowledgement',
16+
name='lecture',
17+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lecture_ack', to='api.courselecture'),
18+
),
19+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.0.2 on 2024-03-14 11:10
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0004_alter_attendenceacknowledgement_lecture'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='course',
15+
name='schedule',
16+
),
17+
]

api/models.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ class LectureTypes(models.TextChoices):
6161

6262
class Course(models.Model):
6363
course_name = models.CharField(max_length=50)
64-
schedule = models.JSONField(default=list)
6564

6665
objects = models.Manager()
6766

@@ -77,6 +76,9 @@ def get_teachers(self):
7776
def get_lectures(self):
7877
return [lecture for lecture in CourseLecture.objects.filter(course=self)]
7978

79+
def is_user_enrolled(self, user : User):
80+
return bool(UserCourse.objects.filter(user=user, course=self))
81+
8082
def add_user_to_course(self, user: User):
8183
UserCourse.objects.create(user=user, course=self).save()
8284

@@ -118,7 +120,7 @@ class AttendenceAcknowledgement(models.Model):
118120
attended_student = models.BooleanField(default=False)
119121
attended_teacher = models.BooleanField(default=False)
120122
student = models.ForeignKey(User, null=False, related_name='user_ack', on_delete=models.CASCADE)
121-
lecture = models.ForeignKey(CourseLecture, null=False, related_name='lecture', on_delete=models.CASCADE)
123+
lecture = models.ForeignKey(CourseLecture, null=False, related_name='lecture_ack', on_delete=models.CASCADE)
122124

123125

124126

api/serializers.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
77
from rest_framework.validators import UniqueValidator
88

9-
from .models import Course
9+
from .models import AccountRoles, Course
1010

1111
User = get_user_model()
1212

@@ -69,7 +69,23 @@ def validate(self, attrs):
6969
return attrs
7070

7171
def create(self, validated_data):
72-
course_id = validated_data.get("course_id", "") or "_".join(validated_data['course_name'].lower().split())
73-
c = Course.objects.create(course_name=validated_data['course_name'], course_id=course_id)
72+
c = Course.objects.create(course_name=validated_data['course_name'])
7473
c.save()
75-
return c
74+
return c
75+
76+
77+
78+
class MassEnrollSerializer(serializers.Serializer):
79+
usernames = serializers.ListField(required=True, allow_empty=False, child=serializers.CharField(max_length=150))
80+
81+
def validate_enroll(self, username):
82+
user_query = User.objects.all().filter(username=username)
83+
if not user_query: raise serializers.ValidationError({"error": f"user '{username}' does not exist"})
84+
if user_query[0].role == AccountRoles.ADMIN: raise serializers.ValidationError({"error": f"cannot enroll an admin account into a course"})
85+
86+
course : Course = self.context.get("course")
87+
if course.is_user_enrolled(user_query[0]): raise serializers.ValidationError({"error": f"user '{username}' is already enrolled in '{course.course_name}'"})
88+
89+
def validate(self, attrs):
90+
for username in attrs["usernames"]: self.validate_enroll(username)
91+
return attrs

api/urls.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
1919

2020
from .views import (
21+
MassEnrollCourseView,
2122
test,
2223
genAdmin,
2324

@@ -55,10 +56,11 @@
5556

5657
# All course paths
5758
path('course/create/', CreateCourseView.as_view(), name='course_create'),
58-
path('course/update/<course_id>', UpdateCourseView.as_view(), name='course_update'),
59-
path('course/delete/<course_id>', DestroyCourseView.as_view(), name='course_delete'),
60-
path('course/enroll/<course_id>', EnrollCourseView.as_view(), name='course_enroll'),
61-
path('course/get/', GetCourseByName.as_view(), name='course_get'),
59+
path('course/update/<pk>', UpdateCourseView.as_view(), name='course_update'),
60+
path('course/delete/<pk>', DestroyCourseView.as_view(), name='course_delete'),
61+
path('course/enroll/<pk>', EnrollCourseView.as_view(), name='course_enroll'),
62+
path('course/mass_enroll/<pk>', MassEnrollCourseView.as_view(), name='course_mass_enroll'),
63+
path('course/get/<pk>', GetCourseByName.as_view(), name='course_get'),
6264
path('course/getall/', GetCoursesAll.as_view(), name='course_getall'),
6365

6466
# Documentation

api/views.py

+40-25
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from .permissions import IsTeacher, IsAdmin, IsStudent
1616
from .models import Course, AccountRoles
17-
from .serializers import CustomTokenSerializer, CreateUserSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer
17+
from .serializers import CustomTokenSerializer, CreateUserSerializer, MassEnrollSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer
1818

1919
import pdb
2020

@@ -162,51 +162,66 @@ class CreateCourseView(generics.CreateAPIView):
162162
class UpdateCourseView(generics.UpdateAPIView):
163163
authentication_classes = [JWTAuthentication]
164164
permission_classes = [IsTeacher]
165-
lookup_field = 'course_id'
165+
lookup_field = 'pk'
166166

167167
queryset = Course.objects.all()
168168
serializer_class = CourseSerializer
169169

170170
class DestroyCourseView(generics.DestroyAPIView):
171171
authentication_classes = [JWTAuthentication]
172172
permission_classes = [IsTeacher]
173-
lookup_field = 'course_id'
173+
lookup_field = 'pk'
174174

175175
queryset = Course.objects.all()
176176
serializer_class = CourseSerializer
177177

178-
# TODO: fix which username is enrolled
179178
class EnrollCourseView(generics.GenericAPIView):
180179
authentication_classes = [JWTAuthentication]
181180
permission_classes = [IsStudent]
182-
lookup_field = 'course_id'
181+
lookup_field = 'pk'
183182

184183
queryset = Course.objects.all()
185184
serializer_class = CourseSerializer
186185

187186
def post(self, request, *args, **kwargs):
188-
req = request.data
189-
username = request.user.username
190-
if "username" not in req or req["username"] == "":
191-
return Response({"error", "username is required"}, status=status.HTTP_400_BAD_REQUEST)
187+
if request.user.role == AccountRoles.ADMIN:
188+
return Response({"error": f"cannot enroll an admin account into a course"}, status=status.HTTP_400_BAD_REQUEST)
192189

193-
if request.user.role != "admin" and username != req["username"]:
194-
return Response({"error", "user does not have permissions for this action"}, status=status.HTTP_401_UNAUTHORIZED)
195-
196-
obj = self.get_object()
197-
key, target_list = ("enrolled_students", obj.enrolled_students) if request.user.role == "student" else ("teachers", obj.teachers)
198-
199-
if req["username"] in target_list:
200-
return Response({"error", f"user '{req.username}' is already part of '{obj.course_id}'"}, status=status.HTTP_400_BAD_REQUEST)
190+
obj : Course = self.get_object()
191+
user = request.user
192+
username = user.username
201193

202-
data = {key: target_list + [req["username"]]}
203-
serializer = self.get_serializer(obj, data=data, partial=True)
204-
serializer.is_valid(raise_exception=True)
205-
serializer.save()
194+
if obj.is_user_enrolled(request.user):
195+
return Response({"error": f"{username} is already enrolled in {obj.course_name}"}, status=status.HTTP_400_BAD_REQUEST)
196+
197+
obj.add_user_to_course(user)
206198

207199
if getattr(obj, '_prefetched_objects_cache', None):
208200
obj._prefetched_objects_cache = {}
209-
return Response(serializer.data)
201+
202+
return Response({"ok": f"succesfully enrolled {username} in {obj.course_name}"}, status=status.HTTP_200_OK)
203+
204+
class MassEnrollCourseView(generics.GenericAPIView):
205+
authentication_classes = [JWTAuthentication]
206+
permission_classes = [IsAdmin]
207+
lookup_field = 'pk'
208+
209+
queryset = Course.objects.all()
210+
serializer_class = CourseSerializer
211+
212+
# Mass enroll students through admin accounts
213+
def post(self, request, *args, **kwargs):
214+
course : Course = self.get_object()
215+
result = MassEnrollSerializer(data=request.data, context={ "course": course })
216+
result.is_valid(raise_exception=True)
217+
218+
queryset = User.objects.all()
219+
usernames = result.data["usernames"]
220+
for username in usernames:
221+
user = queryset.filter(username=username)[0]
222+
course.add_user_to_course(user)
223+
224+
return Response({"ok": f"succesfully enrolled {len(usernames)} students"}, status=status.HTTP_200_OK)
210225

211226
class GetCourseByName(generics.RetrieveAPIView):
212227
authentication_classes = [JWTAuthentication]
@@ -215,11 +230,11 @@ class GetCourseByName(generics.RetrieveAPIView):
215230
queryset = Course.objects.all()
216231
serializer_class = CourseSerializer
217232

218-
def get(self, _, course_id):
219-
queryset = self.get_queryset().filter(course_id=course_id)
233+
def get(self, _, pk):
234+
queryset = self.get_queryset().filter(pk=pk)
220235
serializer = self.serializer_class(queryset, many=True)
221236
if len(serializer.data) == 0:
222-
return Response({"error": f"course '{course_id}' not found"}, status=status.HTTP_404_NOT_FOUND)
237+
return Response({"error": f"course id '{pk}' not found"}, status=status.HTTP_404_NOT_FOUND)
223238
else:
224239
return Response(serializer.data, status=status.HTTP_200_OK)
225240

0 commit comments

Comments
 (0)