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

Group creation #374

Merged
merged 11 commits into from
Apr 27, 2024
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
33 changes: 29 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 @@ -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
Expand Down Expand Up @@ -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")
})
Expand Down Expand Up @@ -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")
})
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/assets/lang/app/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/assets/lang/app/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"
/>
</div>
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/composables/services/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface ProjectState {
getProjectsByAssistant: (assistantId: string) => Promise<void>;
getProjectsByTeacher: (teacherId: string) => Promise<void>;
getProjectsByCourseAndDeadline: (courseId: string, deadlineDate: Date) => Promise<void>;
createProject: (projectData: Project, courseId: string) => Promise<void>;
createProject: (projectData: Project, courseId: string, numberOfGroups: number) => Promise<void>;
updateProject: (projectData: Project) => Promise<void>;
deleteProject: (id: string) => Promise<void>;
}
Expand Down Expand Up @@ -73,7 +73,7 @@ export function useProject(): ProjectState {
});
}

async function createProject(projectData: Project, courseId: string): Promise<void> {
async function createProject(projectData: Project, courseId: string, numberOfGroups: number): Promise<void> {
const endpoint = endpoints.projects.byCourse.replace('{courseId}', courseId);
await create<Project>(
endpoint,
Expand All @@ -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,
Expand Down
27 changes: 24 additions & 3 deletions frontend/src/views/calendar/CalendarView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ const formattedDate = computed(() => {
const projectsWithDeadline = computed<Project[] | null>(() => {
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
);
});
Expand Down Expand Up @@ -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
);
}
Expand All @@ -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
);
}
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/views/courses/roles/StudentCourseView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
// 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) {
Expand Down Expand Up @@ -83,7 +85,13 @@ watch(
<Title class="m-0">{{ t('views.dashboard.projects') }}</Title>
</div>
<!-- Project list body -->
<ProjectList :projects="projects" />
<ProjectList :projects="visibleProjects">
<template #empty>
<p>
{{ t('views.courses.noProjects') }}
</p>
</template>
</ProjectList>

<!-- Heading for teachers and assistants -->
<div class="flex justify-content-between align-items-center my-6">
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/views/courses/roles/TeacherCourseView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,12 @@ watch(
<span class="font-bold text-2xl">{{ message.header }}</span>
<p class="mb-4">{{ message.message }}</p>
<div class="flex items-center mb-4">
<label for="cloneTeachers" class="mr-2">{{
t('views.courses.confirmations.cloneCourse')
}}</label>
<label for="cloneTeachers" class="mr-2">{{ t('views.courses.cloneTeachers') }}</label>
<InputSwitch v-model="cloneTeachers" id="cloneTeachers" class="p-inputswitch-sm" />
</div>
<div class="flex items-center mb-4">
<label for="cloneAssistants" class="mr-2">{{
t('views.courses.confirmations.cloneAssistants')
t('views.courses.cloneAssistants')
}}</label>
<InputSwitch v-model="cloneAssistants" id="cloneAssistants" class="p-inputswitch-sm" />
</div>
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/views/dashboard/roles/StudentDashboardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -49,7 +51,7 @@ watch(
</div>

<!-- Project list body -->
<ProjectList class="fadein" :projects="projects" />
<ProjectList class="fadein" :projects="visibleProjects" />
<!-- Course heading -->
<div
class="flex gap-6 flex-column md:flex-row justify-content-between align-items-start md:align-items-center my-6"
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/views/projects/CreateProjectView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const form = reactive({
startDate: new Date(),
deadline: new Date(),
groupSize: 1,
numberOfGroups: null,
maxScore: 10,
visibility: true,
scoreVisibility: false,
Expand Down Expand Up @@ -98,6 +99,7 @@ async function submitProject(): Promise<void> {
form.submissionStructure,
),
params.courseId as string,
form.numberOfGroups ?? 0,
);

// Redirect to the dashboard overview
Expand Down Expand Up @@ -180,8 +182,17 @@ async function submitProject(): Promise<void> {
<ErrorMessage :field="v$.groupSize" />
</div>

<!-- Max score for the project -->
<div class="field col">
<label for="numberOfGroups">
{{ t('views.projects.number_of_groups') }}
</label>
<InputNumber id="numberOfGroups" class="w-full" v-model="form.numberOfGroups" :min="1" />
</div>
</div>

<div class="grid">
<!-- Max score for the project -->
<div class="field col-6">
<label for="maxScore">{{ t('views.projects.max_score') }}</label>
<InputNumber id="maxScore" class="w-full" v-model="form.maxScore" :min="1" />
<ErrorMessage :field="v$.maxScore" />
Expand Down