Skip to content

Commit

Permalink
Merge branch 'refs/heads/development' into layout-fixes
Browse files Browse the repository at this point in the history
# Conflicts:
#	frontend/src/assets/lang/app/en.json
#	frontend/src/assets/lang/app/nl.json
#	frontend/src/components/courses/CourseGeneralList.vue
#	frontend/src/components/projects/ProjectCreateButton.vue
#	frontend/src/components/teachers_assistants/TeacherAssistantCard.vue
#	frontend/src/views/App.vue
#	frontend/src/views/courses/SearchCourseView.vue
#	frontend/src/views/courses/roles/TeacherCourseView.vue
  • Loading branch information
EwoutV committed May 1, 2024
2 parents 89c5570 + 1a99af7 commit f982588
Show file tree
Hide file tree
Showing 64 changed files with 1,534 additions and 353 deletions.
3 changes: 1 addition & 2 deletions .dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ FRONTEND_DIR="./frontend"
SSL_DIR="./data/nginx/ssl"

# Redis
REDIS_IP=192.168.90.10
REDIS_PORT=6379

# Django
Expand All @@ -20,5 +19,5 @@ DJANGO_DOMAIN_NAME=localhost
DJANGO_CAS_URL_PREFIX=""
DJANGO_CAS_PORT=8080
DJANGO_DB_ENGINE="django.db.backends.sqlite3"
DJANGO_REDIS_HOST=${REDIS_IP}
DJANGO_REDIS_HOST="redis"
DJANGO_REDIS_PORT=${REDIS_PORT}
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.tool-versions
.env
.venv
.idea
Expand Down
6 changes: 2 additions & 4 deletions .prod.env
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ FRONTEND_DIR="./frontend"
SSL_DIR=""

# Postgress DB
POSTGRES_IP=192.168.90.9
POSTGRES_PORT=5432
POSTGRES_DB=selab
POSTGRES_USER=selab_user
POSTGRES_PASSWORD=""

# Redis
REDIS_IP=192.168.90.10
REDIS_PORT=6379

# Django
Expand All @@ -30,7 +28,7 @@ DJANGO_DB_ENGINE=django.db.backends.postgresql
DJANGO_DB_NAME=${POSTGRES_DB}
DJANGO_DB_USER=${POSTGRES_USER}
DJANGO_DB_PASSWORD=${POSTGRES_PASSWORD}
DJANGO_DB_HOST=${POSTGRES_IP}
DJANGO_DB_HOST="postgres"
DJANGO_DB_PORT=${POSTGRES_PORT}
DJANGO_REDIS_HOST=${REDIS_IP}
DJANGO_REDIS_HOST="redis"
DJANGO_REDIS_PORT=${REDIS_PORT}
2 changes: 2 additions & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python 3.11.8
nodejs 18.17.1
19 changes: 14 additions & 5 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
FROM python:3.11.4-alpine3.18
FROM python:3.11.4-alpine3.18 as requirements

RUN apk update && apk add --no-cache gettext libintl && pip install -U poetry
RUN poetry config virtualenvs.create false
RUN pip install poetry-plugin-export

WORKDIR /code

COPY pyproject.toml poetry.lock ./
RUN poetry install --only main

COPY . ./
RUN poetry export --without-hashes --format=requirements.txt > requirements.txt


FROM python:3.11.4-alpine3.18

RUN apk add --no-cache gettext libintl

WORKDIR /code

COPY --from=requirements /code/requirements.txt .

RUN pip install -r requirements.txt --no-cache-dir
4 changes: 4 additions & 0 deletions backend/api/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ msgstr "The teacher is not present in the course."
msgid "courses.error.teachers.last_teacher"
msgstr "The course must have at least one teacher."

#: serializers/course_serializer.py:116
msgid "courses.error.invitation_link"
msgstr "The invitation link is not unique, please try again."

#: serializers/docker_serializer.py:19
msgid "docker.errors.custom"
msgstr "User is not allowed to create public images"
Expand Down
4 changes: 4 additions & 0 deletions backend/api/locale/nl/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ msgstr "De lesgever bevindt zich niet in de opleiding."
msgid "courses.error.teachers.last_teacher"
msgstr "De opleiding moet minstens één lesgever hebben."

#: serializers/course_serializer.py:116
msgid "courses.error.invitation_link"
msgstr "De uitnodigingslink is niet uniek, probeer het opnieuw."

#: serializers/docker_serializer.py:19
msgid "docker.errors.custom"
msgstr "Gebruiker is niet toegelaten om publieke afbeeldingen te maken"
Expand Down
28 changes: 28 additions & 0 deletions backend/api/migrations/0018_course_invitation_link_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.0.4 on 2024-04-29 20:35

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('api', '0017_merge_20240416_1054'),
]

operations = [
migrations.AddField(
model_name='course',
name='invitation_link',
field=models.CharField(blank=True, max_length=100, null=True),
),
migrations.AddField(
model_name='course',
name='invitation_link_expires',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='course',
name='private_course',
field=models.BooleanField(default=False),
),
]
9 changes: 9 additions & 0 deletions backend/api/models/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ class Course(models.Model):
null=True,
)

# Field that defines if the course can be joined by anyone, or only using the invite link
private_course = models.BooleanField(default=False)

# Field that contains the invite link for the course
invitation_link = models.CharField(max_length=100, blank=True, null=True)

# Date when the invite link expires
invitation_link_expires = models.DateField(blank=True, null=True)

def __str__(self) -> str:
"""The string representation of the course."""
return str(self.name)
Expand Down
44 changes: 43 additions & 1 deletion backend/api/serializers/course_serializer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from nh3 import clean
from datetime import timedelta
from django.utils import timezone
from django.utils.translation import gettext
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
Expand Down Expand Up @@ -45,7 +47,20 @@ def validate(self, attrs: dict) -> dict:

class Meta:
model = Course
fields = "__all__"
fields = [
"id",
"name",
"academic_startyear",
"excerpt",
"description",
"faculty",
"parent_course",
"private_course",
"teachers",
"assistants",
"students",
"projects",
]


class CreateCourseSerializer(CourseSerializer):
Expand Down Expand Up @@ -93,6 +108,33 @@ class CourseCloneSerializer(serializers.Serializer):
clone_assistants = serializers.BooleanField()


class SaveInvitationLinkSerializer(serializers.Serializer):
invitation_link = serializers.CharField(required=True)
link_duration = serializers.IntegerField(required=True)

def validate(self, data):
# Check if there is no course with the same invitation link.
if Course.objects.filter(invitation_link=data["invitation_link"]).exists():
raise ValidationError(gettext("courses.error.invitation_link"))

return data

def create(self, validated_data):
# Save the invitation link and the expiration date.
if "course" not in self.context:
raise ValidationError(gettext("courses.error.context"))

course: Course = self.context["course"]

course.invitation_link = validated_data["invitation_link"]

# Save the expiration date as the current date + the invite link expires parameter in days.
course.invitation_link_expires = timezone.now() + timedelta(days=validated_data["link_duration"])
course.save()

return course


class StudentJoinSerializer(StudentIDSerializer):
def validate(self, data):
# The validator needs the course context.
Expand Down
26 changes: 18 additions & 8 deletions backend/api/serializers/project_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class Meta:


class CreateProjectSerializer(ProjectSerializer):
number_groups = serializers.IntegerField(min_value=1, required=False)
number_groups = serializers.IntegerField(min_value=0, required=False)
zip_structure = serializers.FileField(required=False, read_only=True)

def create(self, validated_data):
Expand All @@ -89,20 +89,30 @@ def create(self, validated_data):
# Create the project object without passing 'number_groups' field
project = super().create(validated_data)

# Create groups for the project, if specified
if number_groups is not None:

for _ in range(number_groups):
Group.objects.create(project=project)

elif project.group_size == 1:
if project.group_size == 1:
# If the group_size is set to one, create a group for each student
students = project.course.students.all()

for student in students:
group = Group.objects.create(project=project)
group.students.add(student)

elif number_groups is not None:
# Create groups for the project, if specified

if number_groups > 0:
# Create the number of groups specified
for _ in range(number_groups):
Group.objects.create(project=project)

else:
# If the number of groups is set to zero, create #students / group_size groups
number_students = project.course.students.count()
group_size = project.group_size

for _ in range(0, number_students, group_size):
group = Group.objects.create(project=project)

# If a zip_structure is provided, parse it to create the structure checks
if zip_structure is not None:
# Define the temporary storage location
Expand Down
1 change: 1 addition & 0 deletions backend/api/tests/test_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,7 @@ def test_create_project_with_number_groups(self):
"days": 50,
"deadline": timezone.now() + timezone.timedelta(days=50),
"start_date": timezone.now(),
"group_size": 3,
"number_groups": 5
},
follow=True,
Expand Down
73 changes: 69 additions & 4 deletions backend/api/views/course_view.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from api.models.course import Course
from api.models.assistant import Assistant
from api.models.teacher import Teacher
from api.models.group import Group
from api.permissions.course_permissions import (CourseAssistantPermission,
CoursePermission,
CourseStudentPermission,
Expand All @@ -9,6 +10,7 @@
from api.serializers.assistant_serializer import (AssistantIDSerializer,
AssistantSerializer)
from api.serializers.course_serializer import (CourseCloneSerializer,
SaveInvitationLinkSerializer,
CourseSerializer,
StudentJoinSerializer,
StudentLeaveSerializer,
Expand All @@ -21,6 +23,7 @@
from api.serializers.teacher_serializer import TeacherSerializer
from authentication.serializers import UserIDSerializer
from django.utils.translation import gettext
from django.utils import timezone
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status, viewsets
from rest_framework.decorators import action
Expand Down Expand Up @@ -65,9 +68,26 @@ def update(self, request: Request, *_, **__):
def search(self, request: Request) -> Response:
# Extract filter params
search = request.query_params.get("search", "")
invitation_link = request.query_params.get("invitationLink", "")
years = request.query_params.getlist("years[]")
faculties = request.query_params.getlist("faculties[]")

# If the invitation link was passed, then only the private course with the invitation link should be returned
if invitation_link != "none":

# Filter based on invitation link, and that the invitation link is not expired
queryset = self.get_queryset().filter(
invitation_link=invitation_link,
invitation_link_expires__gte=timezone.now()
)

# Serialize the resulting queryset
serializer = self.serializer_class(self.paginate_queryset(queryset), many=True, context={
"request": request
})

return self.get_paginated_response(serializer.data)

# Filter the queryset based on the search term
queryset = self.get_queryset().filter(
name__icontains=search
Expand All @@ -81,6 +101,9 @@ def search(self, request: Request) -> Response:
if faculties:
queryset = queryset.filter(faculty__in=faculties)

# No invitation link was passed, so filter out private courses
queryset = queryset.filter(private_course=False)

# Serialize the resulting queryset
serializer = self.serializer_class(self.paginate_queryset(queryset), many=True, context={
"request": request
Expand Down Expand Up @@ -190,6 +213,33 @@ def _add_student(self, request: Request, **_):
serializer.validated_data["student"]
)

# If there are individual projects, add the student to a new group
individual_projects = course.projects.filter(group_size=1)

for project in individual_projects:
# Check if the start date of the project is in the future
if project.start_date > timezone.now():
group = Group.objects.create(
project=project
)

group.students.add(
serializer.validated_data["student"]
)

# If there are now more students for a project then places in groups, create a new group
all_projects = course.projects.exclude(group_size=1)

for project in all_projects:
# Check if the start date of the project is in the future
if project.start_date > timezone.now():
number_groups = project.groups.count()

if project.group_size * number_groups < course.students.count():
Group.objects.create(
project=project
)

return Response({
"message": gettext("courses.success.students.add")
})
Expand Down Expand Up @@ -280,10 +330,6 @@ def _remove_teacher(self, request, **_):
teacher
)

# If this was the last course of the teacher, deactivate the teacher role
if not teacher.courses.exists():
teacher.deactivate()

return Response({
"message": gettext("courses.success.teachers.remove")
})
Expand Down Expand Up @@ -348,3 +394,22 @@ def clone(self, request: Request, **__):
course_serializer = CourseSerializer(course, context={"request": request})

return Response(course_serializer.data)

@action(detail=True, methods=["post"])
@swagger_auto_schema(request_body=SaveInvitationLinkSerializer)
def invitation_link(self, request, **_):
"""Save the invitation link to the course"""
course = self.get_object()

serializer = SaveInvitationLinkSerializer(
data=request.data,
context={"course": course}
)

if serializer.is_valid(raise_exception=True):
serializer.save()

# Return serialized cloned course
course_serializer = CourseSerializer(course, context={"request": request})

return Response(course_serializer.data)
Loading

0 comments on commit f982588

Please sign in to comment.