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/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 0fd4021..4831096 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):
@@ -68,17 +68,25 @@ 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 [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))
+ 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()
@@ -99,13 +107,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):
@@ -117,8 +125,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 af426ea..7cc21bb 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()
@@ -24,11 +24,21 @@ 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
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 +83,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 +96,55 @@ def validate_enroll(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 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():
+ 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):
+ usernames = serializers.DictField(required=True, allow_empty=False, child=
+ serializers.CharField(max_length=150)
+ )
+
+ 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}"})
+
+ 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, attended in attrs["usernames"].items(): self.validate_attendence(username, attended)
+ 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 d2c03f8..f5b569e 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -13,12 +13,23 @@
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,
+ DisenrollCourseView,
+ GetCourseLecturesView,
+ GetFullCoursePage,
+ GetLectureView,
+ GetScheduleView,
MassEnrollCourseView,
- test,
+ SetStudentAttView,
+ SetTeacherAttView,
+ test,
+ MailTestView,
+ UnsetStudentAttView,
genAdmin,
GetTokenView,
@@ -34,11 +45,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
@@ -46,6 +58,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'),
@@ -59,9 +76,23 @@
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/', 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
+ # path('course/lecture//delete', GetCourseByName.as_view(), name='lecture_add'),
+
+ path('lecture//get', GetLectureView.as_view(), name='lecture_get'),
+ 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'),
# Documentation
path('schema/', SpectacularAPIView.as_view(), name='schema'),
diff --git a/api/views.py b/api/views.py
index e07076b..9c8296a 100644
--- a/api/views.py
+++ b/api/views.py
@@ -1,4 +1,6 @@
+import datetime
import os
+from typing import List
from django.http import JsonResponse
from django.contrib.auth import get_user_model
@@ -13,10 +15,13 @@
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
-import pdb
+from .serializers import AddLectureSerializer, CourseUserSerializer, CustomTokenSerializer, CreateUserSerializer, LectureSerializer, MailTestSerializer, MassEnrollSerializer, SetAttendenceTeacherSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer
+
+
+
+from django.core.mail import send_mail
User = get_user_model()
@@ -35,6 +40,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
@@ -110,11 +141,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 +205,81 @@ 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 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)
+
+ user = User.objects.all().filter(username=request.user.username)[0]
+ 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["enrolled"] = course.is_user_enrolled(user=user)
+ response_data["attended"] = -1
+ response_data["missed"] = -1
+ response_data["users"] = CourseUserSerializer((teachers + students), many=True).data
+
+ 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]
+
+ queryset = Course.objects.all()
+ serializer_class = CourseSerializer
+
+ 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):
authentication_classes = [JWTAuthentication]
permission_classes = [IsStudent]
@@ -201,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]
@@ -223,32 +351,136 @@ 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
+ 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)
+
+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)
+
+def setAttendence(self, request, attended):
+ 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]
+ lookup_field = 'pk'
+
+ queryset = CourseLecture.objects.all()
+ serializer_class = CourseLecture
+
+ def post(self, request, *args, **kwargs):
+ return setAttendence(self, request, True)
+
+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]
+ 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, attended in usernames.items():
+ user = queryset.filter(username=username)[0]
+ 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)
+
+
+class GetScheduleView(generics.GenericAPIView):
+ authentication_classes = [JWTAuthentication]
+ permission_classes = [IsStudent]
+
+ 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 : List[Course] = user.get_enrolled_courses()
+ all_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_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
+ all_lectures.sort(key= lambda x : datetime.datetime.fromisoformat(x["start_time"]))
+
+ return Response(all_lectures, status=status.HTTP_200_OK)
+
\ No newline at end of file