Skip to content

Commit 5357c8f

Browse files
authored
Merge pull request #16 from DevOps-Cloud-Team5/main
merging main stuff to this branch
2 parents eee3979 + 24b7bbd commit 5357c8f

14 files changed

+2391
-37
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
venv/*
22
.env
33
.vscode
4-
**/__pycache__
4+
**/__pycache__
5+
6+
node_modules
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generated by Django 5.0.2 on 2024-03-13 13:57
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('api', '0002_alter_user_role_usercourse'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='CourseLecture',
17+
fields=[
18+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('start_time', models.DateTimeField()),
20+
('end_time', models.DateTimeField()),
21+
('lecture_type', models.CharField(choices=[('lecture', 'Lecture'), ('practical', 'Practical'), ('workshop', 'Workshop'), ('exam', 'Exam')], default='lecture')),
22+
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_lecture', to='api.course')),
23+
],
24+
),
25+
migrations.CreateModel(
26+
name='AttendenceAcknowledgement',
27+
fields=[
28+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29+
('attended_student', models.BooleanField(default=False)),
30+
('attended_teacher', models.BooleanField(default=False)),
31+
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_ack', to=settings.AUTH_USER_MODEL)),
32+
('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lecture', to='api.courselecture')),
33+
],
34+
),
35+
]
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

+50-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
from django.contrib.auth.models import AbstractUser, AbstractBaseUser, UserManager
23
from django.db import models
34
from django.contrib.auth.validators import UnicodeUsernameValidator
@@ -52,10 +53,14 @@ def clean(self):
5253
def get_classes(self):
5354
return [uc.course for uc in UserCourse.objects.filter(user__id=self.id)]
5455

56+
class LectureTypes(models.TextChoices):
57+
LECTURE = "lecture"
58+
PRACTICAL = "practical"
59+
WORKSHOP = "workshop"
60+
EXAM = "exam"
5561

5662
class Course(models.Model):
5763
course_name = models.CharField(max_length=50)
58-
schedule = models.JSONField(default=list)
5964

6065
objects = models.Manager()
6166

@@ -68,11 +73,54 @@ def get_enrolled_students(self):
6873
def get_teachers(self):
6974
return [uc.user for uc in UserCourse.objects.filter(user__role=AccountRoles.TEACHER)]
7075

76+
def get_lectures(self):
77+
return [lecture for lecture in CourseLecture.objects.filter(course=self)]
78+
79+
def is_user_enrolled(self, user : User):
80+
return bool(UserCourse.objects.filter(user=user, course=self))
81+
7182
def add_user_to_course(self, user: User):
72-
UserCourse.objects.create(user=user, course=self)
83+
UserCourse.objects.create(user=user, course=self).save()
84+
85+
def add_lecture_to_course(self, start_time: datetime.datetime, end_time: datetime.datetime, lecture_type : LectureTypes = LectureTypes.LECTURE):
86+
CourseLecture.objects.create(start_time=start_time, end_time=end_time, lecture_type=lecture_type, course=self).save()
7387

7488
# This is for storing users (teachers/students) in courses
7589
class UserCourse(models.Model):
7690
user = models.ForeignKey(User, null=False, related_name='user', on_delete=models.CASCADE)
7791
course = models.ForeignKey(Course, null=False, related_name='course', on_delete=models.CASCADE)
7892

93+
class CourseLecture(models.Model):
94+
course = models.ForeignKey(Course, null=False, related_name='course_lecture', on_delete=models.CASCADE)
95+
start_time = models.DateTimeField()
96+
end_time = models.DateTimeField()
97+
lecture_type = models.CharField(
98+
choices=LectureTypes.choices,
99+
default=LectureTypes.LECTURE,
100+
)
101+
102+
def set_attendence_user(self, student : User, teacher=False):
103+
queryset = AttendenceAcknowledgement.objects.filter(lecture=self, student=student)
104+
if not queryset: ack = AttendenceAcknowledgement.objects.create(lecture=self, student=student)
105+
else: ack = queryset[0]
106+
107+
if teacher: ack.attended_teacher = True
108+
else: ack.attended_student = True
109+
ack.save()
110+
111+
def get_attendence_user(self, student : User):
112+
queryset = AttendenceAcknowledgement.objects.filter(lecture=self, student=student)
113+
return None if not queryset else queryset[0]
114+
115+
def get_attendence(self):
116+
queryset = AttendenceAcknowledgement.objects.filter(lecture=self)
117+
return [] if not queryset else queryset[0]
118+
119+
class AttendenceAcknowledgement(models.Model):
120+
attended_student = models.BooleanField(default=False)
121+
attended_teacher = models.BooleanField(default=False)
122+
student = models.ForeignKey(User, null=False, related_name='user_ack', on_delete=models.CASCADE)
123+
lecture = models.ForeignKey(CourseLecture, null=False, related_name='lecture_ack', on_delete=models.CASCADE)
124+
125+
126+

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

example.env

-1
This file was deleted.

0 commit comments

Comments
 (0)