diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py index e8c80cf2..f2057b73 100644 --- a/backend/api/serializers/project_serializer.py +++ b/backend/api/serializers/project_serializer.py @@ -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): @@ -89,13 +89,7 @@ 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() @@ -103,6 +97,22 @@ def create(self, validated_data): 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 diff --git a/backend/api/tests/test_course.py b/backend/api/tests/test_course.py index 9d5d69fd..55b43661 100644 --- a/backend/api/tests/test_course.py +++ b/backend/api/tests/test_course.py @@ -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, diff --git a/backend/api/views/course_view.py b/backend/api/views/course_view.py index d44901be..313a31fc 100644 --- a/backend/api/views/course_view.py +++ b/backend/api/views/course_view.py @@ -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, @@ -21,6 +22,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 @@ -190,6 +192,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") }) @@ -280,10 +309,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") }) diff --git a/frontend/src/assets/lang/app/en.json b/frontend/src/assets/lang/app/en.json index cc6eaeb5..c68e83c5 100644 --- a/frontend/src/assets/lang/app/en.json +++ b/frontend/src/assets/lang/app/en.json @@ -64,6 +64,7 @@ "description": "Description", "start_date": "Start project", "group_size": "Number of students in a group (1 for an individual project)", + "number_of_groups": "Number of groups (optional, otherwise #students / group size)", "max_score": "Maximum score that can be achieved", "visibility": "Make project visible to students", "scoreVisibility": "Make score, when uploaded, automatically visible to students", @@ -95,7 +96,7 @@ "edit": "Edit course", "clone": "Clone course", "cloneAssistants": "Clone assistants:", - "cloneCourse": "Clone teachers:", + "cloneTeachers": "Clone teachers:", "name": "Course name", "description": "Description", "excerpt": "Short description", @@ -224,8 +225,8 @@ "deadline": "The deadline must be after the start date" }, "confirmations": { - "clone_course": "Are you sure you want to clone this coure? This will create the same course for the next academic year.", - "leave_course": "Are you sure you want to leave this course? You will no longer have access to this course." + "cloneCourse": "Are you sure you want to clone this coure? This will create the same course for the next academic year.", + "leaveCourse": "Are you sure you want to leave this course? You will no longer have access to this course." }, "admin": { "title": "Admin", diff --git a/frontend/src/assets/lang/app/nl.json b/frontend/src/assets/lang/app/nl.json index 213278b7..a7ff3470 100644 --- a/frontend/src/assets/lang/app/nl.json +++ b/frontend/src/assets/lang/app/nl.json @@ -64,6 +64,7 @@ "description": "Beschrijving", "start_date": "Start project", "group_size": "Aantal studenten per groep (1 voor individueel project)", + "number_of_groups": "Aantal groepen (optioneel, anders #studenten / grootte groep)", "max_score": "Maximale te behalen score", "visibility": "Project zichtbaar maken voor studenten", "scoreVisibility": "Maak score, wanneer ingevuld, automatisch zichtbaar voor studenten", diff --git a/frontend/src/components/teachers_assistants/TeacherAssistantCard.vue b/frontend/src/components/teachers_assistants/TeacherAssistantCard.vue index afa3f4d8..16f40179 100644 --- a/frontend/src/components/teachers_assistants/TeacherAssistantCard.vue +++ b/frontend/src/components/teachers_assistants/TeacherAssistantCard.vue @@ -29,7 +29,8 @@ const { user } = storeToRefs(useAuthStore()); v-if=" props.detail && user?.isTeacher() && - !(userValue.hasRoles('teacher') && course.teachers?.length == 1) + // Explicit check on role, because the roles field in not initialized in the user object + !(userValue.getRole() === 'types.roles.teacher' && course.teachers?.length == 1) " /> diff --git a/frontend/src/composables/services/project.service.ts b/frontend/src/composables/services/project.service.ts index 34cd96b0..9c3bdbd4 100644 --- a/frontend/src/composables/services/project.service.ts +++ b/frontend/src/composables/services/project.service.ts @@ -14,7 +14,7 @@ interface ProjectState { getProjectsByAssistant: (assistantId: string) => Promise; getProjectsByTeacher: (teacherId: string) => Promise; getProjectsByCourseAndDeadline: (courseId: string, deadlineDate: Date) => Promise; - createProject: (projectData: Project, courseId: string) => Promise; + createProject: (projectData: Project, courseId: string, numberOfGroups: number) => Promise; updateProject: (projectData: Project) => Promise; deleteProject: (id: string) => Promise; } @@ -73,7 +73,7 @@ export function useProject(): ProjectState { }); } - async function createProject(projectData: Project, courseId: string): Promise { + async function createProject(projectData: Project, courseId: string, numberOfGroups: number): Promise { const endpoint = endpoints.projects.byCourse.replace('{courseId}', courseId); await create( endpoint, @@ -88,6 +88,7 @@ export function useProject(): ProjectState { max_score: projectData.max_score, score_visible: projectData.score_visible, group_size: projectData.group_size, + number_groups: numberOfGroups, zip_structure: projectData.structure_file, }, project, diff --git a/frontend/src/views/calendar/CalendarView.vue b/frontend/src/views/calendar/CalendarView.vue index e5504dd9..969a74c5 100644 --- a/frontend/src/views/calendar/CalendarView.vue +++ b/frontend/src/views/calendar/CalendarView.vue @@ -38,7 +38,14 @@ const formattedDate = computed(() => { const projectsWithDeadline = computed(() => { return ( projects.value?.filter((project) => { - return moment(project.deadline).isSame(moment(selectedDate.value), 'day'); + if (user.value !== null) { + return ( + moment(project.deadline).isSame(moment(selectedDate.value), 'day') && + (!user.value.isStudent() || project.visible) + ); + } else { + return false; + } }) ?? null ); }); @@ -104,7 +111,14 @@ function hasDeadline(date: CalendarDateSlotOptions): boolean { return ( projects.value?.some((project) => { - return moment(project.deadline).isSame(moment(dateObj), 'day'); + if (user.value !== null) { + return ( + moment(project.deadline).isSame(moment(dateObj), 'day') && + (!user.value.isStudent() || project.visible) + ); + } else { + return false; + } }) ?? false ); } @@ -119,7 +133,14 @@ function countDeadlines(date: CalendarDateSlotOptions): number { return ( projects.value?.filter((project) => { - return moment(project.deadline).isSame(moment(dateObj), 'day'); + if (user.value !== null) { + return ( + moment(project.deadline).isSame(moment(dateObj), 'day') && + (!user.value.isStudent() || project.visible) + ); + } else { + return false; + } }).length ?? 0 ); } diff --git a/frontend/src/views/courses/roles/StudentCourseView.vue b/frontend/src/views/courses/roles/StudentCourseView.vue index ff4304f7..93506a26 100644 --- a/frontend/src/views/courses/roles/StudentCourseView.vue +++ b/frontend/src/views/courses/roles/StudentCourseView.vue @@ -37,13 +37,15 @@ const instructors = computed(() => { return null; }); +const visibleProjects = computed(() => projects.value?.filter((project) => project.visible) ?? null); + /** * Leave the course as a student. */ async function leaveCourse(): Promise { // Show a confirmation dialog before leaving the course, to prevent accidental clicks confirm.require({ - message: t('confirmations.leave_course'), + message: t('confirmations.leaveCourse'), header: t('views.courses.leave'), accept: (): void => { if (user.value !== null) { @@ -83,7 +85,13 @@ watch( {{ t('views.dashboard.projects') }} - + + +
diff --git a/frontend/src/views/courses/roles/TeacherCourseView.vue b/frontend/src/views/courses/roles/TeacherCourseView.vue index fe24a48c..df841450 100644 --- a/frontend/src/views/courses/roles/TeacherCourseView.vue +++ b/frontend/src/views/courses/roles/TeacherCourseView.vue @@ -92,14 +92,12 @@ watch( {{ message.header }}

{{ message.message }}

- +
diff --git a/frontend/src/views/dashboard/roles/StudentDashboardView.vue b/frontend/src/views/dashboard/roles/StudentDashboardView.vue index 6cb6c076..4435e774 100644 --- a/frontend/src/views/dashboard/roles/StudentDashboardView.vue +++ b/frontend/src/views/dashboard/roles/StudentDashboardView.vue @@ -28,6 +28,8 @@ const filteredCourses = computed( () => courses.value?.filter((course) => course.academic_startyear === selectedYear.value) ?? null, ); +const visibleProjects = computed(() => projects.value?.filter((project) => project.visible) ?? null); + /* Watchers */ watch( props.student, @@ -49,7 +51,7 @@ watch(
- +
{ form.submissionStructure, ), params.courseId as string, + form.numberOfGroups ?? 0, ); // Redirect to the dashboard overview @@ -180,8 +182,17 @@ async function submitProject(): Promise {
-
+ + +
+ + +
+ +