diff --git a/backend/api/tests/test_course.py b/backend/api/tests/test_course.py index 036081cb..c19abb32 100644 --- a/backend/api/tests/test_course.py +++ b/backend/api/tests/test_course.py @@ -866,6 +866,10 @@ def test_create_course(self): self.assertEqual(response.status_code, 201) self.assertTrue(Course.objects.filter(name="Introduction to Computer Science").exists()) + # Make sure the teacher is added to the course + course = Course.objects.get(name="Introduction to Computer Science") + self.assertTrue(course.teachers.filter(id=self.user.id).exists()) + def test_create_project(self): """ Able to create a project for a course. diff --git a/backend/api/views/course_view.py b/backend/api/views/course_view.py index 5f91561d..bc61a913 100644 --- a/backend/api/views/course_view.py +++ b/backend/api/views/course_view.py @@ -5,6 +5,7 @@ from rest_framework.response import Response from rest_framework.request import Request from drf_yasg.utils import swagger_auto_schema +from rest_framework import status from api.models.course import Course from api.permissions.course_permissions import ( CoursePermission, @@ -12,7 +13,7 @@ CourseStudentPermission, CourseTeacherPermission ) -from api.permissions.role_permissions import IsTeacher +from api.permissions.role_permissions import IsTeacher, is_teacher from api.serializers.course_serializer import ( CourseSerializer, StudentJoinSerializer, StudentLeaveSerializer, CourseCloneSerializer, TeacherJoinSerializer, TeacherLeaveSerializer @@ -29,6 +30,22 @@ class CourseViewSet(viewsets.ModelViewSet): serializer_class = CourseSerializer permission_classes = [IsAdminUser | CoursePermission] + def create(self, request: Request, *_): + """Override the create method to add the teacher to the course""" + serializer = CourseSerializer(data=request.data, context={"request": request}) + + if serializer.is_valid(raise_exception=True): + course = serializer.save() + + # If it was a teacher who created the course, add them as a teacher + if is_teacher(request.user): + course.teachers.add(request.user.id) + + return Response( + {"message": gettext("courses.success.create")}, + status=status.HTTP_201_CREATED + ) + @action(detail=True, permission_classes=[IsAdminUser | CourseAssistantPermission]) def assistants(self, request: Request, **_): """Returns a list of assistants for the given course""" diff --git a/frontend/src/assets/lang/en.json b/frontend/src/assets/lang/en.json index 0ddf5c85..4eead9da 100644 --- a/frontend/src/assets/lang/en.json +++ b/frontend/src/assets/lang/en.json @@ -3,6 +3,7 @@ "header": { "logo": "Ghent University logo", "login": "login", + "view": "View as {0}", "navigation": { "dashboard": "Dashboard", "calendar": "Calendar", @@ -19,7 +20,9 @@ "views": { "dashboard": { "courses": "My courses", - "projects": "Current projects" + "projects": "Current projects", + "no_projects": "No projects available for this academic year.", + "no_courses": "No courses available for this academic year." }, "login": { "title": "Login", @@ -38,6 +41,12 @@ "deadline": "Deadline", "submissionStatus": "Indienstatus", "groupMembers": "Group members" + }, + "courses": { + "create": "Create course", + "name": "Course name", + "description": "Description", + "year": "Academic year" } }, "composables": { @@ -64,6 +73,13 @@ "open": "details" } }, + "types": { + "roles": { + "student": "Student", + "assistant": "Assistant", + "teacher": "Teacher" + } + }, "toasts": { "messages": { "unknown": "An unknown error has occurred." diff --git a/frontend/src/assets/lang/nl.json b/frontend/src/assets/lang/nl.json index c183c120..f470c32c 100644 --- a/frontend/src/assets/lang/nl.json +++ b/frontend/src/assets/lang/nl.json @@ -20,7 +20,9 @@ "views": { "dashboard": { "courses": "Mijn vakken", - "projects": "Lopende projecten" + "projects": "Lopende projecten", + "no_projects": "Geen projecten beschikbaar voor dit academiejaar.", + "no_courses": "Geen vakken beschikbaar voor dit academiejaar." }, "login": { "title": "Inloggen", @@ -39,7 +41,13 @@ "deadline": "Deadline", "submissionStatus": "Indienstatus", "groupMembers": "Groepsleden" - } + }, + "courses": { + "create": "Creƫer vak", + "name": "Vaknaam", + "description": "Beschrijving", + "year": "Academiejaar" + } }, "composables": { "helpers": { diff --git a/frontend/src/composables/services/courses.service.ts b/frontend/src/composables/services/courses.service.ts index 78c80705..157fea9b 100644 --- a/frontend/src/composables/services/courses.service.ts +++ b/frontend/src/composables/services/courses.service.ts @@ -22,6 +22,16 @@ export function useCourses() { await getList(endpoint, courses, Course.fromJSON); } + async function getCoursesByTeacher(teacher_id: string) { + const endpoint = endpoints.courses.byTeacher.replace('{teacher_id}', teacher_id); + await getList(endpoint, courses, Course.fromJSON); + } + + async function getCourseByAssistant(assistant_id: string) { + const endpoint = endpoints.courses.byAssistant.replace('{assistant_id}', assistant_id); + await getList(endpoint, courses, Course.fromJSON); + } + async function createCourse(course_data: Course) { const endpoint = endpoints.courses.index; await create(endpoint, @@ -50,6 +60,8 @@ export function useCourses() { getCourseByID, getCourses, getCoursesByStudent, + getCoursesByTeacher, + getCourseByAssistant, createCourse, cloneCourse, diff --git a/frontend/src/config/endpoints.ts b/frontend/src/config/endpoints.ts index f7f26cf1..0c1b77c9 100644 --- a/frontend/src/config/endpoints.ts +++ b/frontend/src/config/endpoints.ts @@ -13,6 +13,8 @@ export const endpoints = { index: '/api/courses/', retrieve: '/api/courses/{id}/', byStudent: '/api/students/{student_id}/courses/', + byTeacher: '/api/teachers/{teacher_id}/courses/', + byAssistant: '/api/assistants/{assistant_id}/courses/', clone: '/api/courses/{course_id}/clone/' }, students: { diff --git a/frontend/src/router/router.ts b/frontend/src/router/router.ts index ab90e414..4623afe3 100644 --- a/frontend/src/router/router.ts +++ b/frontend/src/router/router.ts @@ -4,6 +4,7 @@ import DashboardView from '@/views/dashboard/DashboardView.vue'; import CourseView from '@/views/courses/CourseView.vue'; +import CreateCourseView from '@/views/courses/CreateCourseView.vue'; import Dummy from '@/components/Dummy.vue'; import LoginView from '@/views/authentication/LoginView.vue'; import CalendarView from '@/views/calendar/CalendarView.vue'; @@ -27,7 +28,7 @@ const routes: RouteRecordRaw[] = [ // Courses { path: '/courses', children: [ { path: '', component: Dummy, name: 'courses' }, - { path: 'create', component: Dummy, name: 'course-create' }, + { path: 'create', component: CreateCourseView, name: 'course-create' }, // Single course { path: ':courseId', children: [ { path: '', component: CourseView, name: 'course' }, diff --git a/frontend/src/store/authentication.store.ts b/frontend/src/store/authentication.store.ts index a119e52c..4675fff5 100644 --- a/frontend/src/store/authentication.store.ts +++ b/frontend/src/store/authentication.store.ts @@ -4,21 +4,74 @@ import {Role, User} from '@/types/User.ts'; import {endpoints} from '@/config/endpoints.ts'; import {useMessagesStore} from '@/store/messages.store.ts'; import {client} from '@/composables/axios.ts'; -import {Teacher} from '@/types/Teacher.ts'; -import {Student} from '@/types/Student.ts'; -import {Assistant} from '@/types/Assistant.ts'; import {useLocalStorage} from '@vueuse/core'; -import {computed, ref} from 'vue'; +import { useCourses } from '@/composables/services/courses.service'; +import {computed, ref, watch} from 'vue'; +import { useAssistant } from '@/composables/services/assistant.service'; +import { useStudents } from '@/composables/services/students.service'; +import { useTeacher } from '@/composables/services/teachers.service'; export const useAuthStore = defineStore('auth', () => { /* Stores */ const user = ref(null); - const teacher = ref(null); - const student = ref(null); - const assistant = ref(null); const view = useLocalStorage('view', null); const intent = useLocalStorage('intent', '/'); + /* Services */ + const { courses, getCoursesByTeacher, getCoursesByStudent, getCourseByAssistant } = useCourses(); + const { assistant, getAssistantByID } = useAssistant(); + const { student, getStudentByID } = useStudents(); + const { teacher, getTeacherByID } = useTeacher(); + + /* Update the user object when the view changes. */ + watch(view, async () => { + initUser(); + }); + + const initUser = async () => { + if (user.value !== null) { + if (view.value === 'teacher') { + // Get the teacher information. + await getTeacherByID(user.value.id); + + // Get the courses for the teacher. + await getCoursesByTeacher(user.value.id); + + // Set the user object with the teacher information. + teacher.value ? teacher.value.courses = courses.value ?? [] : null; + teacher.value ? teacher.value.roles = user.value.roles : null; + + user.value = teacher.value; + + }else if (view.value === 'student') { + // Get the student information. + await getStudentByID(user.value.id); + + // Get the courses for the student. + await getCoursesByStudent(user.value.id); + + // Set the user object with the student information. + student.value ? student.value.courses = courses.value ?? [] : null; + student.value ? student.value.roles = user.value.roles : null; + + user.value = student.value; + }else { + // Get the assistant information. + await getAssistantByID(user.value.id); + + // Get the courses for the assistant. + await getCourseByAssistant(user.value.id); + + // Set the user object with the assistant information. + assistant.value ? assistant.value.courses = courses.value ?? [] : null; + assistant.value ? assistant.value.roles = user.value.roles : null; + + user.value = assistant.value; + } + } + }; + + /** * Attempt to log in the user using a CAS ticket. * @@ -60,8 +113,9 @@ export const useAuthStore = defineStore('auth', () => { if (view.value === null) { view.value = user.value.roles[0]; } - - // TODO: Get the teacher, student, and assistant information. + + // Init the user depending on the role selected. + initUser(); }).catch((error) => { add({ @@ -86,7 +140,7 @@ export const useAuthStore = defineStore('auth', () => { }); return { - user, teacher, student, assistant, view, intent, + user, view, intent, login, refresh, logout, isAuthenticated } diff --git a/frontend/src/types/Assistant.ts b/frontend/src/types/Assistant.ts index 215cc7f7..cfba16f9 100644 --- a/frontend/src/types/Assistant.ts +++ b/frontend/src/types/Assistant.ts @@ -15,9 +15,9 @@ export class Assistant extends User { public faculties: Faculty[] = [], public courses: Course[] = [], public create_time: Date, - public last_login: Date |null, + public last_login: Date |null ) { - super(id, username, email, first_name, last_name, last_enrolled, is_staff, roles, faculties, create_time, last_login); + super(id, username, email, first_name, last_name, last_enrolled, is_staff, roles, faculties, create_time, last_login, courses); } /** @@ -41,4 +41,13 @@ export class Assistant extends User { assistant.last_login ? new Date(assistant.last_login) : null ); } + + /** + * Check if the user is a assistant. + * + * @returns boolean + */ + public isAssistant(): boolean { + return true; + } } \ No newline at end of file diff --git a/frontend/src/types/Student.ts b/frontend/src/types/Student.ts index 5baecb5d..28b0cf9a 100644 --- a/frontend/src/types/Student.ts +++ b/frontend/src/types/Student.ts @@ -20,7 +20,7 @@ export class Student extends User { public groups: Group[] = [], public faculties: Faculty[] = [], ) { - super(id, username, email, first_name, last_name, last_enrolled, is_staff, roles, faculties, create_time, last_login); + super(id, username, email, first_name, last_name, last_enrolled, is_staff, roles, faculties, create_time, last_login, courses); } /** @@ -42,4 +42,13 @@ export class Student extends User { student.student_id ); } + + /** + * Check if the user is a student. + * + * @returns boolean + */ + public isStudent(): boolean { + return true; + } } \ No newline at end of file diff --git a/frontend/src/types/Teacher.ts b/frontend/src/types/Teacher.ts index 0360a928..09596ca1 100644 --- a/frontend/src/types/Teacher.ts +++ b/frontend/src/types/Teacher.ts @@ -17,7 +17,7 @@ export class Teacher extends User { public create_time: Date, public last_login: Date |null, ) { - super(id, username, email, first_name, last_name, last_enrolled, is_staff, roles, faculties, create_time, last_login); + super(id, username, email, first_name, last_name, last_enrolled, is_staff, roles, faculties, create_time, last_login, courses); } /** @@ -42,4 +42,13 @@ export class Teacher extends User { teacher.last_login ? new Date(teacher.last_login) : null ); } + + /** + * Check if the user is a teacher. + * + * @returns boolean + */ + public isTeacher(): boolean { + return true; + } } \ No newline at end of file diff --git a/frontend/src/types/User.ts b/frontend/src/types/User.ts index 1c25ff2a..11b83e58 100644 --- a/frontend/src/types/User.ts +++ b/frontend/src/types/User.ts @@ -1,3 +1,4 @@ +import { Course } from "./Course"; import { Faculty } from "./Faculty"; export type Role = 'user' | 'student' | 'assistant' | 'teacher'; @@ -15,6 +16,7 @@ export class User { public faculties: Faculty[] = [], public create_time: Date, public last_login: Date |null, + public courses: Course[] = [] ) { } @@ -33,7 +35,7 @@ export class User { * @returns boolean */ public isStudent(): boolean { - return this.roles.includes('student'); + return false; } /** @@ -42,7 +44,7 @@ export class User { * @returns boolean */ public isAssistant(): boolean { - return this.roles.includes('assistant'); + return false; } /** @@ -51,7 +53,7 @@ export class User { * @returns boolean */ public isTeacher(): boolean { - return this.roles.includes('teacher'); + return false; } /** diff --git a/frontend/src/views/calendar/CalendarView.vue b/frontend/src/views/calendar/CalendarView.vue index 7c4c7ae6..1e8775f5 100644 --- a/frontend/src/views/calendar/CalendarView.vue +++ b/frontend/src/views/calendar/CalendarView.vue @@ -5,11 +5,11 @@ import ProjectCard from '@/components/projects/ProjectCard.vue'; import Calendar from 'primevue/calendar'; import Title from '@/components/layout/Title.vue'; import {useProject} from '@/composables/services/project.service'; -import {useCourses} from '@/composables/services/courses.service'; import { computed, onMounted } from 'vue'; import {useI18n} from 'vue-i18n'; -import {ref} from 'vue'; +import {ref, watch} from 'vue'; import {useAuthStore} from '@/store/authentication.store.ts'; +import {storeToRefs} from 'pinia'; import {Project} from "@/types/Projects.ts"; /* Composable injections */ @@ -20,8 +20,7 @@ const allProjects = ref([]); const selectedDate = ref(new Date()); /* Service injection */ -const { user } = useAuthStore(); -const { courses, getCoursesByStudent } = useCourses(); +const { user } = storeToRefs(useAuthStore()); const { projects, getProjectsByCourse } = useProject(); const formattedDate = computed(() => { @@ -30,12 +29,12 @@ const formattedDate = computed(() => { }); const loadProjects = async () => { - if (user !== null) { - // Load the courses of the student - await getCoursesByStudent(user.id); + if (user.value !== null) { + // Clear the old data, so that the data from another role is not displayed + allProjects.value = []; // Load the projects of the courses - for (const course of courses.value ?? []) { + for (const course of user.value.courses) { await getProjectsByCourse(course.id); // Assign the course to the project @@ -62,6 +61,10 @@ onMounted(async () => { await loadProjects(); }); +watch(user, async () => { + await loadProjects(); +}); +