diff --git a/backend/api/serializers/project_serializer.py b/backend/api/serializers/project_serializer.py
index 582a097b..afdfdce4 100644
--- a/backend/api/serializers/project_serializer.py
+++ b/backend/api/serializers/project_serializer.py
@@ -100,8 +100,7 @@ class ProjectSerializer(serializers.ModelSerializer):
)
extra_checks = serializers.HyperlinkedIdentityField(
- view_name="project-extra-checks",
- read_only=True
+ view_name="project-extra-checks"
)
groups = serializers.HyperlinkedIdentityField(
diff --git a/frontend/src/assets/lang/app/en.json b/frontend/src/assets/lang/app/en.json
index 5a09df76..bf9b1062 100644
--- a/frontend/src/assets/lang/app/en.json
+++ b/frontend/src/assets/lang/app/en.json
@@ -107,6 +107,7 @@
"courses": {
"create": "Create course",
"edit": "Edit course",
+ "save": "Save course",
"clone": "Clone course",
"cloneAssistants": "Clone assistants:",
"cloneTeachers": "Clone teachers:",
diff --git a/frontend/src/assets/lang/app/nl.json b/frontend/src/assets/lang/app/nl.json
index c9bcb259..9fc87834 100644
--- a/frontend/src/assets/lang/app/nl.json
+++ b/frontend/src/assets/lang/app/nl.json
@@ -108,6 +108,7 @@
"courses": {
"create": "Creƫer vak",
"edit": "Bewerk vak",
+ "save": "Vak opslaan",
"clone": "Kloon vak",
"cloneAssistants": "Kloon assistenten:",
"cloneTeachers": "Kloon lesgevers:",
diff --git a/frontend/src/components/Loading.vue b/frontend/src/components/Loading.vue
index 151f7e33..eac23369 100644
--- a/frontend/src/components/Loading.vue
+++ b/frontend/src/components/Loading.vue
@@ -2,19 +2,17 @@
import ProgressSpinner from 'primevue/progressspinner';
import { useTimeout } from '@vueuse/core';
-withDefaults(defineProps<{height?:string}>(), {
- height: '4rem'
+withDefaults(defineProps<{ height?: string }>(), {
+ height: '4rem',
});
const show = useTimeout(250);
-
-
-
+
-
+
diff --git a/frontend/src/components/courses/CourseForm.vue b/frontend/src/components/courses/CourseForm.vue
index d9567e4d..8cf979c1 100644
--- a/frontend/src/components/courses/CourseForm.vue
+++ b/frontend/src/components/courses/CourseForm.vue
@@ -1,10 +1,7 @@
@@ -120,13 +122,7 @@ onMounted(async () => {
-
+
diff --git a/frontend/src/components/courses/ShareCourseButton.vue b/frontend/src/components/courses/ShareCourseButton.vue
index f6302fc9..74966fff 100644
--- a/frontend/src/components/courses/ShareCourseButton.vue
+++ b/frontend/src/components/courses/ShareCourseButton.vue
@@ -8,7 +8,6 @@ import { type Course } from '@/types/Course.ts';
import { PrimeIcons } from 'primevue/api';
import { ref, computed } from 'vue';
import { useCourses } from '@/composables/services/course.service';
-import Editor from '@/components/forms/Editor.vue';
/* Composable injections */
const { t } = useI18n();
@@ -87,7 +86,7 @@ const invitationLink = computed(() => {
-
+
diff --git a/frontend/src/components/projects/ProjectForm.vue b/frontend/src/components/projects/ProjectForm.vue
index 50be3a9f..0c66a599 100644
--- a/frontend/src/components/projects/ProjectForm.vue
+++ b/frontend/src/components/projects/ProjectForm.vue
@@ -5,7 +5,6 @@ import ErrorMessage from '@/components/forms/ErrorMessage.vue';
import Button from 'primevue/button';
import Editor from '@/components/forms/Editor.vue';
import Calendar from 'primevue/calendar';
-import Skeleton from 'primevue/skeleton';
import InputSwitch from 'primevue/inputswitch';
import { Project } from '@/types/Project.ts';
import { useI18n } from 'vue-i18n';
@@ -81,12 +80,22 @@ watchEffect(() => {
const project = props.project;
if (project !== undefined) {
- form.value = Project.fromJSON(project);
- form.value.structure_checks = [...(project.structure_checks ?? [])];
- form.value.extra_checks = [...(project.extra_checks ?? [])];
- } else {
- form.value.structure_checks = [];
- form.value.extra_checks = [];
+ form.value = new Project(
+ project.id,
+ project.name,
+ project.description,
+ project.visible,
+ project.archived,
+ project.locked_groups,
+ project.start_date,
+ project.deadline,
+ project.max_score,
+ project.score_visible,
+ project.group_size,
+ );
+
+ form.value.structure_checks = [...project.structure_checks];
+ form.value.extra_checks = [...project.extra_checks];
}
});
@@ -191,41 +200,31 @@ watchEffect(() => {
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
-
+
diff --git a/frontend/src/components/projects/ProjectList.vue b/frontend/src/components/projects/ProjectList.vue
index 50103ea3..ab6be293 100644
--- a/frontend/src/components/projects/ProjectList.vue
+++ b/frontend/src/components/projects/ProjectList.vue
@@ -1,6 +1,5 @@
-
-
-
-
{{ t('views.courses.create') }}
-
-
-
+
+
{{ t('views.courses.create') }}
+
+
-
+
+
+
+
diff --git a/frontend/src/views/courses/UpdateCourseView.vue b/frontend/src/views/courses/UpdateCourseView.vue
index 496b6ab2..fa1f44ea 100644
--- a/frontend/src/views/courses/UpdateCourseView.vue
+++ b/frontend/src/views/courses/UpdateCourseView.vue
@@ -1,35 +1,64 @@
-
-
-
-
{{ t('views.courses.edit') }}
-
-
-
-
+
+
{{ t('views.courses.edit') }}
+
+
-
+
+
+
+
diff --git a/frontend/src/views/courses/roles/AssistantCourseView.vue b/frontend/src/views/courses/roles/AssistantCourseView.vue
index 01c5956a..4b513b01 100644
--- a/frontend/src/views/courses/roles/AssistantCourseView.vue
+++ b/frontend/src/views/courses/roles/AssistantCourseView.vue
@@ -5,8 +5,9 @@ import TeacherAssistantList from '@/components/teachers_assistants/TeacherAssist
import ProjectCreateButton from '@/components/projects/ProjectCreateButton.vue';
import { type Course } from '@/types/Course.ts';
import { useI18n } from 'vue-i18n';
-import { computed, watchEffect } from 'vue';
+import { computed } from 'vue';
import { useProject } from '@/composables/services/project.service.ts';
+import { watchImmediate } from '@vueuse/core';
/* Props */
const props = defineProps<{
@@ -23,9 +24,12 @@ const instructors = computed(() => {
});
/* Fetch projects when the course changes */
-watchEffect(async () => {
- await getProjectsByCourse(props.course.id);
-});
+watchImmediate(
+ () => props.course.id,
+ async (courseId: string) => {
+ await getProjectsByCourse(courseId);
+ },
+);
diff --git a/frontend/src/views/courses/roles/StudentCourseView.vue b/frontend/src/views/courses/roles/StudentCourseView.vue
index be47b060..8de95c12 100644
--- a/frontend/src/views/courses/roles/StudentCourseView.vue
+++ b/frontend/src/views/courses/roles/StudentCourseView.vue
@@ -12,7 +12,8 @@ import { useAuthStore } from '@/store/authentication.store.ts';
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router';
import { useProject } from '@/composables/services/project.service.ts';
-import { computed, watch, watchEffect } from 'vue';
+import { computed } from 'vue';
+import { watchImmediate } from '@vueuse/core';
/* Props */
const props = defineProps<{
@@ -43,17 +44,19 @@ async function leaveCourse(): Promise {
confirm.require({
message: t('confirmations.leaveCourse'),
header: t('views.courses.leave'),
- accept: async (): Promise => {
- if (user.value !== null) {
- // Leave the course
- await studentLeaveCourse(props.course.id, user.value.id);
-
- // Refresh the user so the course is removed from the user's courses
- await refreshUser();
-
- // Redirect to the dashboard
- await push({ name: 'dashboard' });
- }
+ accept: () => {
+ (async () => {
+ if (user.value !== null) {
+ // Leave the course
+ await studentLeaveCourse(props.course.id, user.value.id);
+
+ // Refresh the user so the course is removed from the user's courses
+ await refreshUser();
+
+ // Redirect to the dashboard
+ await push({ name: 'dashboard' });
+ }
+ })();
},
reject: () => {},
});
@@ -62,9 +65,12 @@ async function leaveCourse(): Promise {
/**
* Watch for changes in the course ID and fetch the projects for the course.
*/
-watchEffect(async () => {
- await getProjectsByCourse(props.course.id);
-});
+watchImmediate(
+ () => props.course.id,
+ async (courseId: string) => {
+ await getProjectsByCourse(courseId);
+ },
+);
diff --git a/frontend/src/views/courses/roles/TeacherCourseView.vue b/frontend/src/views/courses/roles/TeacherCourseView.vue
index b900d156..7e36c30f 100644
--- a/frontend/src/views/courses/roles/TeacherCourseView.vue
+++ b/frontend/src/views/courses/roles/TeacherCourseView.vue
@@ -16,7 +16,8 @@ import { RouterLink } from 'vue-router';
import { PrimeIcons } from 'primevue/api';
import { useCourses } from '@/composables/services/course.service';
import { useProject } from '@/composables/services/project.service.ts';
-import { computed, ref, watch, watchEffect } from 'vue';
+import { computed, ref } from 'vue';
+import { watchImmediate } from '@vueuse/core';
/* Props */
const props = defineProps<{
@@ -56,9 +57,12 @@ async function handleClone(): Promise {
/**
* Watch for changes in the course ID and fetch the projects for the course.
*/
-watchEffect(async () => {
- await getProjectsByCourse(props.course.id);
-});
+watchImmediate(
+ () => props.course.id,
+ async (courseId: string) => {
+ await getProjectsByCourse(courseId);
+ },
+);
diff --git a/frontend/src/views/dashboard/roles/AssistantDashboardView.vue b/frontend/src/views/dashboard/roles/AssistantDashboardView.vue
index 591adbcc..e58fc3db 100644
--- a/frontend/src/views/dashboard/roles/AssistantDashboardView.vue
+++ b/frontend/src/views/dashboard/roles/AssistantDashboardView.vue
@@ -5,12 +5,14 @@ import CourseList from '@/components/courses/CourseDetailList.vue';
import ProjectList from '@/components/projects/ProjectList.vue';
import ProjectCreateButton from '@/components/projects/ProjectCreateButton.vue';
import { useI18n } from 'vue-i18n';
-import { computed, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
import { useCourses } from '@/composables/services/course.service.ts';
import { type Assistant } from '@/types/users/Assistant';
import { getAcademicYear, getAcademicYears } from '@/types/Course.ts';
import { useProject } from '@/composables/services/project.service.ts';
import Button from 'primevue/button';
+import Loading from '@/components/Loading.vue';
+import { watchImmediate } from '@vueuse/core';
/* Props */
const props = defineProps<{
@@ -23,6 +25,8 @@ const { projects, getProjectsByAssistant } = useProject();
const { courses, getCourseByAssistant } = useCourses();
/* State */
+const loading = ref(true);
+
const selectedYear = ref(getAcademicYear());
const allYears = computed(() => getAcademicYears(...(courses.value?.map((course) => course.academic_startyear) ?? [])));
@@ -31,66 +35,71 @@ const filteredCourses = computed(
);
/* Watchers */
-watch(
- props.assistant,
- () => {
- getCourseByAssistant(props.assistant.id);
- getProjectsByAssistant(props.assistant.id);
- },
- {
- immediate: true,
+watchImmediate(
+ () => props.assistant,
+ async (assistant: Assistant) => {
+ await getCourseByAssistant(assistant.id);
+ await getProjectsByAssistant(assistant.id);
+ loading.value = false;
},
);
-
-
-
-
{{ t('views.dashboard.projects') }}
+
+
+
+
+
+
{{ t('views.dashboard.projects') }}
-
-
-
-
-
-
-
- {{ t('components.list.noCourses.teacher') }}
-
-
-
-
-
-
- {{ t('components.list.noProjects.teacher') }}
-
+
+
+
+
+
+
+
+ {{ t('components.list.noCourses.teacher') }}
+
+
+
+
+
+
+ {{ t('components.list.noProjects.teacher') }}
+
-
- {{ t('components.list.noCourses.teacher') }}
-
+
+ {{ t('components.list.noCourses.teacher') }}
+
-
-
-
-
-
-
-
-
{{ t('views.dashboard.courses') }}
+
+
+
+
+
+
+
+
{{ t('views.dashboard.courses') }}
-
-
-
-
-
-
- {{ t('components.list.noCourses.teacher') }}
-
-
-
-
-
+
+
+
+
+
+
+ {{ t('components.list.noCourses.teacher') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/dashboard/roles/StudentDashboardView.vue b/frontend/src/views/dashboard/roles/StudentDashboardView.vue
index 4435e774..11a29f6b 100644
--- a/frontend/src/views/dashboard/roles/StudentDashboardView.vue
+++ b/frontend/src/views/dashboard/roles/StudentDashboardView.vue
@@ -3,12 +3,14 @@ import Title from '@/components/layout/Title.vue';
import YearSelector from '@/components/YearSelector.vue';
import CourseList from '@/components/courses/CourseDetailList.vue';
import ProjectList from '@/components/projects/ProjectList.vue';
+import Loading from '@/components/Loading.vue';
import { type Student } from '@/types/users/Student.ts';
import { useI18n } from 'vue-i18n';
-import { computed, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
import { useCourses } from '@/composables/services/course.service.ts';
import { getAcademicYear, getAcademicYears } from '@/types/Course.ts';
import { useProject } from '@/composables/services/project.service.ts';
+import { watchImmediate } from '@vueuse/core';
/* Props */
const props = defineProps<{
@@ -21,6 +23,8 @@ const { projects, getProjectsByStudent } = useProject();
const { courses, getCoursesByStudent } = useCourses();
/* State */
+const loading = ref(true);
+
const selectedYear = ref(getAcademicYear());
const allYears = computed(() => getAcademicYears(...(courses.value?.map((course) => course.academic_startyear) ?? [])));
@@ -31,39 +35,46 @@ const filteredCourses = computed(
const visibleProjects = computed(() => projects.value?.filter((project) => project.visible) ?? null);
/* Watchers */
-watch(
- props.student,
- () => {
- getCoursesByStudent(props.student.id);
- getProjectsByStudent(props.student.id);
- },
- {
- immediate: true,
+watchImmediate(
+ () => props.student,
+ async (student: Student) => {
+ loading.value = true;
+ await getCoursesByStudent(student.id);
+ await getProjectsByStudent(student.id);
+ loading.value = false;
},
);
-
-
-
-
{{ t('views.dashboard.projects') }}
-
-
-
-
-
-
-
-
{{ t('views.dashboard.courses') }}
+
+
+
+
+
+
{{ t('views.dashboard.projects') }}
+
+
+
+
+
+
+
{{ t('views.dashboard.courses') }}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/dashboard/roles/TeacherDashboardView.vue b/frontend/src/views/dashboard/roles/TeacherDashboardView.vue
index 4060ce70..6f0faf55 100644
--- a/frontend/src/views/dashboard/roles/TeacherDashboardView.vue
+++ b/frontend/src/views/dashboard/roles/TeacherDashboardView.vue
@@ -6,13 +6,15 @@ import YearSelector from '@/components/YearSelector.vue';
import CourseList from '@/components/courses/CourseDetailList.vue';
import ProjectList from '@/components/projects/ProjectList.vue';
import ProjectCreateButton from '@/components/projects/ProjectCreateButton.vue';
+import Loading from '@/components/Loading.vue';
import { type Teacher } from '@/types/users/Teacher';
import { PrimeIcons } from 'primevue/api';
import { useI18n } from 'vue-i18n';
-import { computed, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
import { useCourses } from '@/composables/services/course.service.ts';
import { getAcademicYear, getAcademicYears } from '@/types/Course.ts';
import { useProject } from '@/composables/services/project.service.ts';
+import { watchImmediate } from '@vueuse/core';
/* Props */
const props = defineProps<{
@@ -25,6 +27,8 @@ const { projects, getProjectsByTeacher } = useProject();
const { courses, getCoursesByTeacher } = useCourses();
/* State */
+const loading = ref(true);
+
const selectedYear = ref(getAcademicYear());
const allYears = computed(() => getAcademicYears(...(courses.value?.map((course) => course.academic_startyear) ?? [])));
@@ -33,82 +37,88 @@ const filteredCourses = computed(
);
/* Watchers */
-watch(
- props.teacher,
- () => {
- getCoursesByTeacher(props.teacher.id);
- getProjectsByTeacher(props.teacher.id);
- },
- {
- immediate: true,
+watchImmediate(
+ () => props.teacher,
+ async (teacher: Teacher) => {
+ loading.value = true;
+ await getCoursesByTeacher(teacher.id);
+ await getProjectsByTeacher(teacher.id);
+ loading.value = false;
},
);
-
-
-
-
{{ t('views.dashboard.projects') }}
+
+
+
+
+
+
{{ t('views.dashboard.projects') }}
-
-
-
-
-
-
-
- {{ t('components.list.noCourses.teacher') }}
-
-
-
-
-
-
- {{ t('components.list.noProjects.teacher') }}
-
+
+
+
+
+
+
+
+ {{ t('components.list.noCourses.teacher') }}
+
+
+
+
+
+
+ {{ t('components.list.noProjects.teacher') }}
+
-
- {{ t('components.list.noCourses.teacher') }}
-
+
+ {{ t('components.list.noCourses.teacher') }}
+
-
-
-
-
-
-
-
-
{{ t('views.dashboard.courses') }}
-
-
-
-
+
+
+
+
+
+
+
+
{{ t('views.dashboard.courses') }}
+
+
+
+
-
-
-
-
-
-
-
-
-
- {{ t('components.list.noCourses.teacher') }}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {{ t('components.list.noCourses.teacher') }}
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/projects/CreateProjectView.vue b/frontend/src/views/projects/CreateProjectView.vue
index 1f5c902c..314880e4 100644
--- a/frontend/src/views/projects/CreateProjectView.vue
+++ b/frontend/src/views/projects/CreateProjectView.vue
@@ -1,7 +1,7 @@
@@ -87,15 +97,21 @@ onMounted(async () => {
{{ t('views.projects.create') }}
-
-
-
- saveProject(project, numberOfGroups)"
- @create:docker-image="saveDockerImage"
- />
+
+
+
+
+ saveProject(project, numberOfGroups)"
+ @create:docker-image="saveDockerImage"
+ />
+
+
+
+
+
diff --git a/frontend/src/views/projects/UpdateProjectView.vue b/frontend/src/views/projects/UpdateProjectView.vue
index 7ce381dc..6d7dadf4 100644
--- a/frontend/src/views/projects/UpdateProjectView.vue
+++ b/frontend/src/views/projects/UpdateProjectView.vue
@@ -3,7 +3,7 @@ import BaseLayout from '@/components/layout/base/BaseLayout.vue';
import Title from '@/components/layout/Title.vue';
import ProjectForm from '@/components/projects/ProjectForm.vue';
import Loading from '@/components/Loading.vue';
-import { onMounted, ref } from 'vue';
+import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useProject } from '@/composables/services/project.service';
@@ -15,6 +15,7 @@ import { useDockerImages } from '@/composables/services/docker.service.ts';
import { useExtraCheck } from '@/composables/services/extra_checks.service.ts';
import { type DockerImage } from '@/types/DockerImage.ts';
import { type ExtraCheck } from '@/types/ExtraCheck.ts';
+import { watchImmediate } from '@vueuse/core';
/* Composable injections */
const { t } = useI18n();
@@ -29,7 +30,7 @@ const { extraChecks, setExtraChecks, deleteExtraCheck, getExtraChecksByProject }
const { dockerImages, getDockerImages, createDockerImage } = useDockerImages();
/* State */
-const isLoading = ref(true);
+const loading = ref(true);
/**
* Save the project.
@@ -92,51 +93,60 @@ async function saveDockerImage(dockerImage: DockerImage, file: File): Promise {
- try {
- await getProjectByID(params.projectId as string);
- await getDockerImages();
+watchImmediate(
+ () => params.projectId,
+ async () => {
+ loading.value = true;
- if (project.value !== null) {
- await getStructureCheckByProject(project.value.id);
+ try {
+ await getProjectByID(params.projectId.toString());
+ await getDockerImages();
- if (structureChecks.value !== null) {
- project.value.structure_checks = structureChecks.value;
- }
+ if (project.value !== null) {
+ await getStructureCheckByProject(project.value.id);
- await getExtraChecksByProject(project.value.id);
+ if (structureChecks.value !== null) {
+ project.value.structure_checks = structureChecks.value;
+ }
- if (extraChecks.value !== null) {
- project.value.extra_checks = extraChecks.value;
+ await getExtraChecksByProject(project.value.id);
+
+ if (extraChecks.value !== null) {
+ project.value.extra_checks = extraChecks.value;
+ }
}
+ } catch (error: any) {
+ processError(error);
}
- isLoading.value = false;
- } catch (error: any) {
- processError(error);
- }
-});
+ loading.value = false;
+ },
+);
-
-
- {{ t('views.projects.edit') }}
-
-
-
-
-
+
+
+
+
+ {{ t('views.projects.edit') }}
+
+
+
+
+
+
+
-
+