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

Project creation update #271

Merged
merged 15 commits into from
Apr 9, 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
22 changes: 22 additions & 0 deletions backend/api/serializers/course_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from api.serializers.teacher_serializer import TeacherIDSerializer
from api.serializers.faculty_serializer import FacultySerializer
from api.models.course import Course
from authentication.models import Faculty


class CourseSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -41,6 +42,27 @@ class Meta:
fields = "__all__"


class CreateCourseSerializer(CourseSerializer):
faculty = serializers.PrimaryKeyRelatedField(
queryset=Faculty.objects.all(),
required=False,
allow_null=True,
)

def create(self, validated_data):
faculty = validated_data.pop('faculty', None)

# Create the course
course = super().create(validated_data)

# Link the faculty, if specified
if faculty is not None:
course.faculty = faculty
course.save()

return course


class CourseIDSerializer(serializers.Serializer):
student_id = serializers.PrimaryKeyRelatedField(
queryset=Course.objects.all()
Expand Down
19 changes: 19 additions & 0 deletions backend/api/serializers/project_serializer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.core.files.storage import FileSystemStorage
from django.conf import settings
from django.utils.translation import gettext
from rest_framework import serializers
from api.models.project import Project
Expand All @@ -7,6 +9,7 @@
from api.models.checks import FileExtension
from api.serializers.submission_serializer import SubmissionSerializer
from api.serializers.checks_serializer import StructureCheckSerializer
from api.logic.check_folder_structure import parse_zip_file


class ProjectSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -59,11 +62,16 @@ def validate(self, data):

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

def create(self, validated_data):
# Pop the 'number_groups' field from validated_data
number_groups = validated_data.pop('number_groups', None)

# Get the zip structure file from the request
request = self.context.get('request')
zip_structure = request.FILES.get('zip_structure')

# Create the project object without passing 'number_groups' field
project = super().create(validated_data)

Expand All @@ -81,6 +89,17 @@ def create(self, validated_data):
group = Group.objects.create(project=project)
group.students.add(student)

# If a zip_structure is provided, parse it to create the structure checks
if zip_structure is not None:
# Define tje temporary storage location
temp_storage = FileSystemStorage(location=settings.MEDIA_ROOT)
# Save the file to the temporary location
temp_file_path = temp_storage.save(f"tmp/{zip_structure.name}", zip_structure)
# Pass the full path to the parse_zip_file function
parse_zip_file(project, temp_file_path)
# Delete the temporary file
temp_storage.delete(temp_file_path)

return project


Expand Down
2 changes: 0 additions & 2 deletions backend/api/serializers/submission_serializer.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Any

from api.logic.check_folder_structure import check_zip_file # , parse_zip_file
from api.models.submission import (ErrorTemplate, ExtraChecksResult,
Submission, SubmissionFile)
Expand Down
45 changes: 44 additions & 1 deletion backend/api/tests/test_course.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
from api.models.course import Course
from api.models.teacher import Teacher
from api.models.student import Student
from api.tests.helpers import create_course, create_assistant, create_student, create_teacher, create_project
from api.tests.helpers import (create_course,
create_assistant,
create_student,
create_teacher,
create_project,
create_faculty)
from django.core.files.uploadedfile import SimpleUploadedFile


def get_course():
Expand Down Expand Up @@ -785,12 +791,15 @@ def test_create_course(self):
"""
Able to create a course.
"""
faculty = create_faculty(name="Engineering")

response = self.client.post(
reverse("course-list"),
data={
"name": "Introduction to Computer Science",
"academic_startyear": 2022,
"description": "An introductory course on computer science.",
"faculty": faculty.id,
},
follow=True,
)
Expand All @@ -802,6 +811,9 @@ def test_create_course(self):
course = Course.objects.get(name="Introduction to Computer Science")
self.assertTrue(course.teachers.filter(id=self.user.id).exists())

# Make sure the course is linked to the faculty
self.assertEqual(course.faculty.id, faculty.id)

def test_create_project(self):
"""
Able to create a project for a course.
Expand Down Expand Up @@ -831,6 +843,37 @@ def test_create_project(self):
project = course.projects.get(name="become champions")
self.assertEqual(project.groups.count(), 0)

def test_create_project_with_zip_file_as_structure(self):
"""
Able to create a project for a course with a zip file as structure.
"""
course = get_course()
course.teachers.add(self.user)

with open("data/testing/structures/zip_struct1.zip", "rb") as f:
response = self.client.post(
reverse("course-projects", args=[str(course.id)]),
data={
"name": "become champions",
"description": "win the jpl",
"visible": True,
"archived": False,
"days": 50,
"deadline": timezone.now() + timezone.timedelta(days=50),
"start_date": timezone.now(),
"group_size": 2,
"zip_structure": SimpleUploadedFile('zip_struct1.zip', f.read(), content_type='application/zip'),
},
follow=True,
)

self.assertEqual(response.status_code, 200)
self.assertTrue(course.projects.filter(name="become champions").exists())

# Make sure there are structure checks added to the project
project = course.projects.get(name="become champions")
self.assertTrue(project.structure_checks.exists())

def test_create_project_with_number_groups(self):
"""
Able to create a project for a course with a number of groups.
Expand Down
10 changes: 4 additions & 6 deletions backend/api/views/course_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
StudentJoinSerializer,
StudentLeaveSerializer,
TeacherJoinSerializer,
TeacherLeaveSerializer)
TeacherLeaveSerializer,
CreateCourseSerializer)
from api.serializers.project_serializer import (CreateProjectSerializer,
ProjectSerializer)
from api.serializers.student_serializer import StudentSerializer
Expand All @@ -35,7 +36,7 @@ class CourseViewSet(viewsets.ModelViewSet):
# TODO: Creating should return the info of the new object and not a message "created" (General TODO)
def create(self, request: Request, *_):
"""Override the create method to add the teacher to the course"""
serializer = CourseSerializer(data=request.data, context={"request": request})
serializer = CreateCourseSerializer(data=request.data, context={"request": request})

if serializer.is_valid(raise_exception=True):
course = serializer.save()
Expand All @@ -44,10 +45,7 @@ def create(self, request: Request, *_):
if is_teacher(request.user):
course.teachers.add(request.user.id)

return Response(
{"message": gettext("courses.success.create")},
status=status.HTTP_201_CREATED
)
return Response(serializer.data, status=status.HTTP_201_CREATED)

@action(detail=False)
def search(self, request: Request) -> Response:
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/assets/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
"group_size": "Number of students in a group (1 for an individual project)",
"max_score": "Maximum score that can be achieved",
"visibility": "Make project visible to students",
"score_visibility": "Make score, when uploaded, automatically visible to students"
"score_visibility": "Make score, when uploaded, automatically visible to students",
"docker_upload": "Upload a Dockerfile",
"submission_structure": "Structure of how a submission should be made"
},
"submissions": {
"title": "Submissions",
Expand All @@ -66,7 +68,7 @@
"create": "Create course",
"name": "Course name",
"description": "Description",
"year": "Academic year"
"faculty": "Faculty"
}
},
"composables": {
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/assets/lang/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
"group_size": "Aantal studenten per groep (1 voor individueel project)",
"max_score": "Maximale te behalen score",
"visibility": "Project zichtbaar maken voor studenten",
"score_visibility": "Maak score, wanneer ingevuld, automatisch zichtbaar voor studenten"
"score_visibility": "Maak score, wanneer ingevuld, automatisch zichtbaar voor studenten",
"docker_upload": "Upload een Dockerfile",
"submission_structure": "Structuur van hoe de indiening moet gebeuren"
},
"submissions": {
"title": "Inzendingen",
Expand All @@ -68,7 +70,7 @@
"create": "Creëer vak",
"name": "Vaknaam",
"description": "Beschrijving",
"year": "Academiejaar",
"faculty": "Faculteit",
"search": {
"search": "Zoeken",
"faculty": "Faculteit",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/composables/services/courses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function useCourses(): CoursesState {
name: courseData.name,
description: courseData.description,
academic_startyear: courseData.academic_startyear,
faculty: courseData.faculty?.id,
},
course,
Course.fromJSON,
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/composables/services/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,14 @@ export async function create<T>(
data: any,
ref: Ref<T | null>,
fromJson: (data: any) => T,
contentType: string = 'application/json',
): Promise<void> {
try {
const response = await client.post(endpoint, data);
const response = await client.post(endpoint, data, {
headers: {
'Content-Type': contentType,
},
});
ref.value = fromJson(response.data);
} catch (error: any) {
processError(error);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/composables/services/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ export function useProject(): ProjectState {
max_score: projectData.max_score,
score_visible: projectData.score_visible,
group_size: projectData.group_size,
zip_structure: projectData.structure_file,
},
project,
Project.fromJSON,
'multipart/form-data',
);
}

Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/Projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export class Project {
public score_visible: boolean,
public group_size: number,

public structure_file: File | null = null,
public course: Course | null = null,
public structureChecks: StructureCheck[] = [],
public extra_checks: ExtraCheck[] = [],
Expand Down
54 changes: 34 additions & 20 deletions frontend/src/views/courses/CreateCourseView.vue
Original file line number Diff line number Diff line change
@@ -1,42 +1,45 @@
<script setup lang="ts">
import Calendar from 'primevue/calendar';
import Dropdown from 'primevue/dropdown';
import BaseLayout from '@/components/layout/BaseLayout.vue';
import Title from '@/components/layout/Title.vue';
import { reactive, computed } from 'vue';
import { reactive, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/store/authentication.store.ts';
import { storeToRefs } from 'pinia';
import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import Button from 'primevue/button';
import { Course } from '@/types/Course';
import { useCourses } from '@/composables/services/courses.service';
import { useFaculty } from '@/composables/services/faculties.service.ts';
import { required, helpers } from '@vuelidate/validators';
import { useVuelidate } from '@vuelidate/core';
import ErrorMessage from '@/components/forms/ErrorMessage.vue';
import { User } from '@/types/users/User.ts';

/* Composable injections */
const { t } = useI18n();
const { push } = useRouter();
const { user } = storeToRefs(useAuthStore());
const { faculties, getFaculties } = useFaculty();

/* Service injection */
const { createCourse } = useCourses();

/* Fetch the faculties */
onMounted(async () => {
await getFaculties();
});

/* Form content */
const form = reactive({
name: '',
description: '',
year: user.value !== null ? new Date(User.getAcademicYear(new Date()), 0, 1) : new Date(),
faculty: null,
});

// Define validation rules for each form field
const rules = computed(() => {
return {
name: { required: helpers.withMessage(t('validations.required'), required) },
year: { required: helpers.withMessage(t('validations.required'), required) },
faculty: { required: helpers.withMessage(t('validations.required'), required) },
};
});

Expand All @@ -52,17 +55,28 @@ const submitCourse = async (): Promise<void> => {
// Pass the course data to the service
await createCourse(
new Course(
'', // ID not needed for creation, will be generated by the backend
'',
form.name,
form.description,
form.year.getFullYear(),
currentAcademicYear(),
null, // No parent course
form.faculty,
),
);

// Redirect to the dashboard overview
push({ name: 'dashboard' });
}
};

/* Get the current academic year */
const currentAcademicYear = (): number => {
if (new Date().getMonth() < 9) {
return new Date().getFullYear() - 1;
} else {
return new Date().getFullYear();
}
};
</script>

<template>
Expand All @@ -87,17 +101,17 @@ const submitCourse = async (): Promise<void> => {
<Textarea id="courseDescription" v-model="form.description" autoResize rows="5" cols="30" />
</div>

<!-- Course academic year -->
<!-- Course faculty -->
<div class="mb-4">
<label for="courseYear">{{ t('views.courses.year') }}</label>
<Calendar id="courseYear" v-model="form.year" view="year" dateFormat="yy" showIcon>
<template #footer>
<div style="text-align: center">
{{ form.year.getFullYear() }} - {{ form.year.getFullYear() + 1 }}
</div>
</template>
</Calendar>
<ErrorMessage :field="v$.year" />
<label for="courseFaculty">{{ t('views.courses.faculty') }}</label>
<Dropdown
id="courseFaculty"
v-model="form.faculty"
:options="faculties"
optionLabel="name"
v-if="faculties"
/>
<ErrorMessage :field="v$.faculty" />
</div>

<!-- Submit button -->
Expand Down
Loading
Loading