From b863935f7433acb76b17ae04990c23b6fa34c22c Mon Sep 17 00:00:00 2001 From: Peter Staab Date: Wed, 20 Jul 2022 13:48:51 -0400 Subject: [PATCH 1/3] WIP: form a basic instructor dashboard page and fix course switching from menubar. --- src/common/views.ts | 7 +++++++ src/layouts/MenuBar.vue | 16 ++++++++++++---- src/layouts/MenuSidebar.vue | 4 +++- src/pages/instructor/Dashboard.vue | 8 ++++++++ src/router/routes.ts | 11 ++++++++++- 5 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 src/pages/instructor/Dashboard.vue diff --git a/src/common/views.ts b/src/common/views.ts index 8eb59648..7ff09f1a 100644 --- a/src/common/views.ts +++ b/src/common/views.ts @@ -49,6 +49,13 @@ export const student_views: Array = [ ]; export const instructor_views: Array = [ + { + name: 'Dashboard', + component_name: 'Dashboard', + icon: 'speed', + route: 'dashboard', + sidebars: [] + }, { name: 'Calendar', component_name: 'Calendar', diff --git a/src/layouts/MenuBar.vue b/src/layouts/MenuBar.vue index 768ea49c..83b501c1 100644 --- a/src/layouts/MenuBar.vue +++ b/src/layouts/MenuBar.vue @@ -64,13 +64,17 @@ diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 5f462e55..7bf0ae33 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -10,9 +10,9 @@ - + - + + + diff --git a/src/stores/permissions.ts b/src/stores/permissions.ts index 9c157eb9..ef31368e 100644 --- a/src/stores/permissions.ts +++ b/src/stores/permissions.ts @@ -54,7 +54,7 @@ export const usePermissionStore = defineStore('permission', { // admin-only routes const session_store = useSessionStore(); const user = session_store.user; - if (permission.admin_required) return user.is_admin; + if (permission.admin_required) return user.is_admin ?? false; // routes that 'belong' to a user const route_user_id = parseRouteUserID(route); diff --git a/src/stores/session.ts b/src/stores/session.ts index 671232df..6b6f1f58 100644 --- a/src/stores/session.ts +++ b/src/stores/session.ts @@ -2,9 +2,9 @@ import { defineStore } from 'pinia'; import { api } from 'boot/axios'; -import { User } from 'src/common/models/users'; +import { ParseableUser, User } from 'src/common/models/users'; import type { SessionInfo } from 'src/common/models/session'; -import { ParseableUserCourse, UserCourse } from 'src/common/models/courses'; +import { ParseableUserCourse } from 'src/common/models/courses'; import { logger } from 'boot/logger'; import { ResponseError } from 'src/common/api-requests/errors'; @@ -19,13 +19,15 @@ interface CourseInfo { course_id: number; } -// SessionState should contain a de facto User, already parsed +// SessionState should contain a de facto User, already parsed. +// In order for the session to be persistent, we need to store the fields username, +// user_id, etc. instead of _username, _user_id from a Model. export interface SessionState { logged_in: boolean; expiry: number; - user: User; + user: ParseableUser; course: CourseInfo; - user_courses: UserCourse[]; + user_courses: ParseableUserCourse[]; } export const useSessionStore = defineStore('session', { @@ -34,7 +36,7 @@ export const useSessionStore = defineStore('session', { state: (): SessionState => ({ logged_in: false, expiry: 0, - user: new User({ username: 'logged_out' }), + user: { username: 'logged_out' }, course: { course_id: 0, role: '', @@ -45,6 +47,8 @@ export const useSessionStore = defineStore('session', { getters: { full_name: (state): string => `${state.user?.first_name ?? ''} ${state.user?.last_name ?? ''}`, getUser: (state): User => new User(state.user), + student_user_courses: (state) => state.user_courses.filter(c => c.role === 'student'), + instructor_user_courses: (state) => state.user_courses.filter(c => c.role === 'instructor'), }, actions: { updateExpiry(expiry: number): void { @@ -61,7 +65,7 @@ export const useSessionStore = defineStore('session', { if (this.logged_in) { this.user = session_info.user; } else { - this.user = new User({ username: 'logged_out' }); + this.user = new User({ username: 'logged_out' }).toObject(); } }, setCourse(course: CourseInfo): void { @@ -75,14 +79,7 @@ export const useSessionStore = defineStore('session', { async fetchUserCourses(user_id: number): Promise { const response = await api.get(`users/${user_id}/courses`); if (response.status === 200) { - this.user_courses = (response.data as ParseableUserCourse[]) - .map(user_course => new UserCourse({ - course_id: user_course.course_id, - user_id: user_course.user_id, - visible: user_course.visible, - role: user_course.role, - course_name: user_course.course_name, - })); + this.user_courses = response.data as ParseableUserCourse[]; } else { logger.error(response.data); throw response.data as ResponseError; @@ -90,7 +87,7 @@ export const useSessionStore = defineStore('session', { }, logout() { this.logged_in = false; - this.user = new User({ username: 'logged_out' }); + this.user = new User({ username: 'logged_out' }).toObject(); this.course = { course_id: 0, role: '', course_name: '' }; useProblemSetStore().clearAll(); useSettingsStore().clearAll(); diff --git a/src/stores/users.ts b/src/stores/users.ts index fede6a6a..c84f2a3d 100644 --- a/src/stores/users.ts +++ b/src/stores/users.ts @@ -191,14 +191,14 @@ export const useUserStore = defineStore('user', { */ async setSessionUser() { const session_store = useSessionStore(); + const user_id = session_store.user.user_id ?? 0; // if there is no user in the session don't fetch the user. - if (session_store.user.user_id === 0) return; + if (user_id === 0) return; // Get the global user. - const user_response = await api.get(`users/${session_store.user.user_id}`); + const user_response = await api.get(`users/${user_id}`); this.users = [ new User(user_response.data as ParseableUser)]; // fetch the course user information for this use - const response = await api.get(`courses/${session_store.course.course_id}/users/${ - session_store.user.user_id}`); + const response = await api.get(`courses/${session_store.course.course_id}/users/${user_id}`); this.db_course_users = [ new DBCourseUser(response.data as ParseableDBCourseUser)]; }, // CourseUser actions diff --git a/tests/stores/session.spec.ts b/tests/stores/session.spec.ts index a30d72da..572ea4e4 100644 --- a/tests/stores/session.spec.ts +++ b/tests/stores/session.spec.ts @@ -19,7 +19,7 @@ import { checkPassword } from 'src/common/api-requests/session'; import { Course, UserCourse } from 'src/common/models/courses'; import { SessionInfo } from 'src/common/models/session'; -import { User } from 'src/common/models/users'; +import { ParseableUser, User } from 'src/common/models/users'; import { cleanIDs, loadCSV } from '../utils'; @@ -29,18 +29,25 @@ describe('Session Store', () => { let lisa_courses: UserCourse[]; let lisa: User; - const user: User = new User({ + // session now just stores objects not models: + const user: ParseableUser = { first_name: 'Homer', last_name: 'Simpson', user_id: 1234, email: 'homer@msn.com', username: 'homer', is_admin: false, - }); + }; - const logged_out: User = new User({ - username: 'logged_out' - }); + const logged_out: ParseableUser = { + username: 'logged_out', + email: '', + last_name: '', + first_name: '', + user_id: 0, + is_admin: false, + student_id: '' + }; const session_info: SessionInfo = { logged_in: true, @@ -97,7 +104,7 @@ describe('Session Store', () => { const session = useSessionStore(); expect(session.logged_in).toBe(false); - expect(session.user).toStrictEqual(logged_out); + expect(session.user.username).toBe('logged_out'); expect(session.course).toStrictEqual({ course_id: 0, course_name: '', @@ -124,7 +131,7 @@ describe('Session Store', () => { test('check user courses', async () => { const session_store = useSessionStore(); await session_store.fetchUserCourses(lisa.user_id); - expect(sortAndClean(session_store.user_courses as UserCourse[])) + expect(sortAndClean(session_store.user_courses.map(c => new UserCourse(c)))) .toStrictEqual(sortAndClean(lisa_courses)); }); From 79b240d217e3d292e7e86d8c8f36f32a8e7dd802 Mon Sep 17 00:00:00 2001 From: "K. Andrew Parker" Date: Fri, 26 Aug 2022 12:56:56 -0400 Subject: [PATCH 3/3] fix permissions, tests use instructor, refactor session store --- conf/permissions.dist.yml | 4 +-- src/common/models/courses.ts | 20 ++++++++++----- src/components/common/Login.vue | 8 +++--- src/components/common/UserCourses.vue | 37 ++++++++++----------------- src/components/student/Student.vue | 11 +------- src/layouts/MenuBar.vue | 31 +++++++--------------- src/layouts/MenuSidebar.vue | 6 +++-- src/pages/instructor/Instructor.vue | 15 ++++------- src/stores/session.ts | 31 +++++++++++++++++----- src/stores/set_problems.ts | 19 ++++++++++---- tests/stores/problem_sets.spec.ts | 13 +++++----- tests/stores/session.spec.ts | 23 ++++++++++------- tests/stores/set_problems.spec.ts | 18 +++++++------ tests/stores/user_sets.spec.ts | 14 +++++----- tests/unit-tests/courses.spec.ts | 34 +++++++++++------------- 15 files changed, 141 insertions(+), 143 deletions(-) diff --git a/conf/permissions.dist.yml b/conf/permissions.dist.yml index 7bf2477d..cd310b4a 100644 --- a/conf/permissions.dist.yml +++ b/conf/permissions.dist.yml @@ -35,9 +35,9 @@ db_permissions: Course: getCourses: - allowed_roles: ['*'] + authenticated: true getCourse: - allowed_roles: ['*'] + authenticated: true updateCourse: admin_required: true addCourse: diff --git a/src/common/models/courses.ts b/src/common/models/courses.ts index 28bc28db..f269fe20 100644 --- a/src/common/models/courses.ts +++ b/src/common/models/courses.ts @@ -106,12 +106,12 @@ export class Course extends Model { */ export interface ParseableUserCourse { - course_id?: number; - user_id?: number; - course_name?: string; + course_id: number; + user_id: number; + course_name: string; username?: string; visible?: boolean; - role?: string; + role: string; course_dates?: ParseableCourseDates; } export class UserCourse extends Model { @@ -126,6 +126,13 @@ export class UserCourse extends Model { static ALL_FIELDS = ['course_id', 'course_name', 'visible', 'course_dates', 'user_id', 'username', 'role']; + static DEFAULT_VALUES = { + course_id: 0, + user_id: 0, + course_name: 'DEFAULT_USER_COURSE', + role: 'unknown', + }; + get all_field_names(): string[] { return UserCourse.ALL_FIELDS; } @@ -134,7 +141,7 @@ export class UserCourse extends Model { return ['course_dates']; } - constructor(params: ParseableUserCourse = {}) { + constructor(params: ParseableUserCourse = UserCourse.DEFAULT_VALUES) { super(); this.set(params); } @@ -171,7 +178,8 @@ export class UserCourse extends Model { set role(value: string) { this._role = value; } clone(): UserCourse { - return new UserCourse(this.toObject()); + // typescript does not recognize the getters as keys when converting with .toObject() + return new UserCourse(this.toObject() as unknown as ParseableUserCourse); } isValid(): boolean { diff --git a/src/components/common/Login.vue b/src/components/common/Login.vue index 6692d163..1c707276 100644 --- a/src/components/common/Login.vue +++ b/src/components/common/Login.vue @@ -56,23 +56,21 @@ const login = async () => { }; const session_info = await checkPassword(username_info); - if (!session_info.logged_in) { + if (!session_info.logged_in || !session_info.user.user_id) { message.value = i18n.t('authentication.failure'); } else { // success session.updateSessionInfo(session_info); // permissions require access to user courses and respective roles - await session.fetchUserCourses(session_info.user.user_id); + await session.fetchUserCourses(); await permission_store.fetchRoles(); await permission_store.fetchRoutePermissions(); - if (session.user.user_id == undefined || session.user.user_id == 0) return; - let forward = localStorage.getItem('afterLogin'); forward ||= (session_info.user.is_admin) ? '/admin' : - `/users/${session.user.user_id}/courses`; + `/users/${session_info.user.user_id}/courses`; localStorage.removeItem('afterLogin'); void router.push(forward); } diff --git a/src/components/common/UserCourses.vue b/src/components/common/UserCourses.vue index db11e864..54ffbc2a 100644 --- a/src/components/common/UserCourses.vue +++ b/src/components/common/UserCourses.vue @@ -41,41 +41,30 @@ import { logger } from 'src/boot/logger'; const session_store = useSessionStore(); const router = useRouter(); -const user_courses = computed(() => session_store.user_courses); - const user = computed(() => session_store.user); // This is used to simplify the UI. const course_types = computed(() => [ - { name: 'Student', courses: user_courses.value.filter(c => c.role == 'student') }, - { name: 'Instructor', courses: session_store.instructor_user_courses } + { name: 'Student', courses: session_store.user_courses.filter(c => c.role === 'student') }, + { name: 'Instructor', courses: session_store.user_courses.filter(c => c.role === 'instructor') } ]); -const switchCourse = async (course_id?: number) => { - if (course_id == undefined || course_id === 0) { +const switchCourse = (course_id?: number) => { + if (!course_id) { logger.error('[UserCourses/switchCourse]: the course_id is 0 or undefined.'); + return; } - const student_course = session_store.student_user_courses.find(c => c.course_id === course_id); - const instructor_course = session_store.instructor_user_courses.find(c => c.course_id === course_id); - if (student_course) { - session_store.setCourse({ - course_name: student_course.course_name ?? 'unknown', - course_id: student_course.course_id ?? 0, - role: 'student' - }); - await router.push({ + session_store.setCourse(course_id); + + if (session_store.course.role === 'student') { + void router.push({ name: 'StudentDashboard', - params: { course_id: student_course.course_id } - }); - } else if (instructor_course) { - session_store.setCourse({ - course_name: instructor_course.course_name ?? 'unknown', - course_id: instructor_course.course_id ?? 0, - role: 'instructor' + params: { course_id } }); - await router.push({ + } else if (session_store.course.role === 'instructor') { + void router.push({ name: 'InstructorDashboard', - params: { course_id: instructor_course.course_id } + params: { course_id } }); } }; diff --git a/src/components/student/Student.vue b/src/components/student/Student.vue index a55336f4..a6a079f4 100644 --- a/src/components/student/Student.vue +++ b/src/components/student/Student.vue @@ -33,16 +33,7 @@ const loadStudentSets = async () => { }; const course_id = parseRouteCourseID(route); -const course = session_store.user_courses.find(c => c.course_id === course_id); -if (course) { - session_store.setCourse({ - course_id: course_id, - course_name: course.course_name ?? 'unknown' - }); -} else { - logger.warn(`Can't find ${course_id} in ${session_store.user_courses - .map((c) => c.course_id).join(', ')}`); -} +session_store.setCourse(course_id); await loadStudentSets(); watch(() => session_store.course.course_id, async () => { diff --git a/src/layouts/MenuBar.vue b/src/layouts/MenuBar.vue index e459ffd2..4aae826d 100644 --- a/src/layouts/MenuBar.vue +++ b/src/layouts/MenuBar.vue @@ -29,7 +29,7 @@