Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merging main stuff to this branch #16

Merged
merged 8 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
venv/*
.env
.vscode
**/__pycache__
**/__pycache__

node_modules
35 changes: 35 additions & 0 deletions api/migrations/0003_courselecture_attendenceacknowledgement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 5.0.2 on 2024-03-13 13:57

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0002_alter_user_role_usercourse'),
]

operations = [
migrations.CreateModel(
name='CourseLecture',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.DateTimeField()),
('end_time', models.DateTimeField()),
('lecture_type', models.CharField(choices=[('lecture', 'Lecture'), ('practical', 'Practical'), ('workshop', 'Workshop'), ('exam', 'Exam')], default='lecture')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_lecture', to='api.course')),
],
),
migrations.CreateModel(
name='AttendenceAcknowledgement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attended_student', models.BooleanField(default=False)),
('attended_teacher', models.BooleanField(default=False)),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_ack', to=settings.AUTH_USER_MODEL)),
('lecture', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lecture', to='api.courselecture')),
],
),
]
19 changes: 19 additions & 0 deletions api/migrations/0004_alter_attendenceacknowledgement_lecture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-03-14 11:00

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0003_courselecture_attendenceacknowledgement'),
]

operations = [
migrations.AlterField(
model_name='attendenceacknowledgement',
name='lecture',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lecture_ack', to='api.courselecture'),
),
]
17 changes: 17 additions & 0 deletions api/migrations/0005_remove_course_schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.2 on 2024-03-14 11:10

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0004_alter_attendenceacknowledgement_lecture'),
]

operations = [
migrations.RemoveField(
model_name='course',
name='schedule',
),
]
52 changes: 50 additions & 2 deletions api/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from django.contrib.auth.models import AbstractUser, AbstractBaseUser, UserManager
from django.db import models
from django.contrib.auth.validators import UnicodeUsernameValidator
Expand Down Expand Up @@ -52,10 +53,14 @@ def clean(self):
def get_classes(self):
return [uc.course for uc in UserCourse.objects.filter(user__id=self.id)]

class LectureTypes(models.TextChoices):
LECTURE = "lecture"
PRACTICAL = "practical"
WORKSHOP = "workshop"
EXAM = "exam"

class Course(models.Model):
course_name = models.CharField(max_length=50)
schedule = models.JSONField(default=list)

objects = models.Manager()

Expand All @@ -68,11 +73,54 @@ def get_enrolled_students(self):
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)]

def is_user_enrolled(self, user : User):
return bool(UserCourse.objects.filter(user=user, course=self))

def add_user_to_course(self, user: User):
UserCourse.objects.create(user=user, course=self)
UserCourse.objects.create(user=user, course=self).save()

def add_lecture_to_course(self, start_time: datetime.datetime, end_time: datetime.datetime, lecture_type : LectureTypes = LectureTypes.LECTURE):
CourseLecture.objects.create(start_time=start_time, end_time=end_time, lecture_type=lecture_type, course=self).save()

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

class CourseLecture(models.Model):
course = models.ForeignKey(Course, null=False, related_name='course_lecture', on_delete=models.CASCADE)
start_time = models.DateTimeField()
end_time = models.DateTimeField()
lecture_type = models.CharField(
choices=LectureTypes.choices,
default=LectureTypes.LECTURE,
)

def set_attendence_user(self, student : User, 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
ack.save()

def get_attendence_user(self, student : User):
queryset = AttendenceAcknowledgement.objects.filter(lecture=self, student=student)
return None if not queryset else queryset[0]

def get_attendence(self):
queryset = AttendenceAcknowledgement.objects.filter(lecture=self)
return [] if not queryset else queryset[0]

class AttendenceAcknowledgement(models.Model):
attended_student = models.BooleanField(default=False)
attended_teacher = models.BooleanField(default=False)
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)



24 changes: 20 additions & 4 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework.validators import UniqueValidator

from .models import Course
from .models import AccountRoles, Course

User = get_user_model()

Expand Down Expand Up @@ -69,7 +69,23 @@ def validate(self, attrs):
return attrs

def create(self, validated_data):
course_id = validated_data.get("course_id", "") or "_".join(validated_data['course_name'].lower().split())
c = Course.objects.create(course_name=validated_data['course_name'], course_id=course_id)
c = Course.objects.create(course_name=validated_data['course_name'])
c.save()
return c
return c



class MassEnrollSerializer(serializers.Serializer):
usernames = serializers.ListField(required=True, allow_empty=False, child=serializers.CharField(max_length=150))

def validate_enroll(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.ADMIN: raise serializers.ValidationError({"error": f"cannot enroll an admin account into a course"})

course : Course = self.context.get("course")
if course.is_user_enrolled(user_query[0]): raise serializers.ValidationError({"error": f"user '{username}' is already enrolled in '{course.course_name}'"})

def validate(self, attrs):
for username in attrs["usernames"]: self.validate_enroll(username)
return attrs
10 changes: 6 additions & 4 deletions api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

from .views import (
MassEnrollCourseView,
test,
genAdmin,

Expand Down Expand Up @@ -55,10 +56,11 @@

# All course paths
path('course/create/', CreateCourseView.as_view(), name='course_create'),
path('course/update/<course_id>', UpdateCourseView.as_view(), name='course_update'),
path('course/delete/<course_id>', DestroyCourseView.as_view(), name='course_delete'),
path('course/enroll/<course_id>', EnrollCourseView.as_view(), name='course_enroll'),
path('course/get/', GetCourseByName.as_view(), name='course_get'),
path('course/update/<pk>', UpdateCourseView.as_view(), name='course_update'),
path('course/delete/<pk>', DestroyCourseView.as_view(), name='course_delete'),
path('course/enroll/<pk>', EnrollCourseView.as_view(), name='course_enroll'),
path('course/mass_enroll/<pk>', MassEnrollCourseView.as_view(), name='course_mass_enroll'),
path('course/get/<pk>', GetCourseByName.as_view(), name='course_get'),
path('course/getall/', GetCoursesAll.as_view(), name='course_getall'),

# Documentation
Expand Down
65 changes: 40 additions & 25 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from .permissions import IsTeacher, IsAdmin, IsStudent
from .models import Course, AccountRoles
from .serializers import CustomTokenSerializer, CreateUserSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer
from .serializers import CustomTokenSerializer, CreateUserSerializer, MassEnrollSerializer, UserSerializer, CourseCreateSerializer, CourseSerializer

import pdb

Expand Down Expand Up @@ -162,51 +162,66 @@ class CreateCourseView(generics.CreateAPIView):
class UpdateCourseView(generics.UpdateAPIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsTeacher]
lookup_field = 'course_id'
lookup_field = 'pk'

queryset = Course.objects.all()
serializer_class = CourseSerializer

class DestroyCourseView(generics.DestroyAPIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsTeacher]
lookup_field = 'course_id'
lookup_field = 'pk'

queryset = Course.objects.all()
serializer_class = CourseSerializer

# TODO: fix which username is enrolled
class EnrollCourseView(generics.GenericAPIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsStudent]
lookup_field = 'course_id'
lookup_field = 'pk'

queryset = Course.objects.all()
serializer_class = CourseSerializer

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

if request.user.role != "admin" and username != req["username"]:
return Response({"error", "user does not have permissions for this action"}, status=status.HTTP_401_UNAUTHORIZED)

obj = self.get_object()
key, target_list = ("enrolled_students", obj.enrolled_students) if request.user.role == "student" else ("teachers", obj.teachers)

if req["username"] in target_list:
return Response({"error", f"user '{req.username}' is already part of '{obj.course_id}'"}, status=status.HTTP_400_BAD_REQUEST)
obj : Course = self.get_object()
user = request.user
username = user.username

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

if getattr(obj, '_prefetched_objects_cache', None):
obj._prefetched_objects_cache = {}
return Response(serializer.data)

return Response({"ok": f"succesfully enrolled {username} in {obj.course_name}"}, status=status.HTTP_200_OK)

class MassEnrollCourseView(generics.GenericAPIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAdmin]
lookup_field = 'pk'

queryset = Course.objects.all()
serializer_class = CourseSerializer

# Mass enroll students through admin accounts
def post(self, request, *args, **kwargs):
course : Course = self.get_object()
result = MassEnrollSerializer(data=request.data, context={ "course": 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]
course.add_user_to_course(user)

return Response({"ok": f"succesfully enrolled {len(usernames)} students"}, status=status.HTTP_200_OK)

class GetCourseByName(generics.RetrieveAPIView):
authentication_classes = [JWTAuthentication]
Expand All @@ -215,11 +230,11 @@ class GetCourseByName(generics.RetrieveAPIView):
queryset = Course.objects.all()
serializer_class = CourseSerializer

def get(self, _, course_id):
queryset = self.get_queryset().filter(course_id=course_id)
def get(self, _, pk):
queryset = self.get_queryset().filter(pk=pk)
serializer = self.serializer_class(queryset, many=True)
if len(serializer.data) == 0:
return Response({"error": f"course '{course_id}' not found"}, status=status.HTTP_404_NOT_FOUND)
return Response({"error": f"course id '{pk}' not found"}, status=status.HTTP_404_NOT_FOUND)
else:
return Response(serializer.data, status=status.HTTP_200_OK)

Expand Down
1 change: 0 additions & 1 deletion example.env

This file was deleted.

Loading
Loading