From 54b434bf3fa1b983d5b79a981b43769dddd01110 Mon Sep 17 00:00:00 2001 From: Yao Wang Date: Mon, 10 Apr 2023 22:31:06 -0400 Subject: [PATCH 01/31] Add instructions field to quizzes --- gql/graphql.ts | 2 ++ prisma/dbml/schema.dbml | 1 + prisma/schema.prisma | 1 + src/quiz/quiz.service.ts | 3 +++ src/quiz/quiz.spec.ts | 2 ++ src/quiz/schema.graphql | 8 ++++++++ utils/fakes.ts | 1 + utils/tests.ts | 1 + 8 files changed, 19 insertions(+) diff --git a/gql/graphql.ts b/gql/graphql.ts index 12e510d5..a9488514 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -309,6 +309,7 @@ export interface QuizResultFields { export interface CreateQuiz { totalPoints: number; + instructions?: Nullable; dueAt?: Nullable; timeLimit?: Nullable; numQuestions: number; @@ -733,6 +734,7 @@ export interface LessonProgress { export interface Quiz { id: string; totalPoints: number; + instructions?: Nullable; dueAt?: Nullable; timeLimit?: Nullable; numQuestions: number; diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 675e2e04..540ea7dc 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -259,6 +259,7 @@ Table LessonProgress { Table Quiz { id String [pk] totalPoints Float [not null] + instructions String [not null] dueAt DateTime timeLimit Int numQuestions Int [not null] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9d75564..87fb688f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -296,6 +296,7 @@ model LessonProgress { model Quiz { id String @id @default(auto()) @map("_id") @db.ObjectId totalPoints Float + instructions String dueAt DateTime? timeLimit Int? numQuestions Int diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 66ecf558..74083830 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -88,6 +88,7 @@ export class QuizService { const where = Prisma.validator()({ id: args.id ? args.id : undefined, totalPoints: args.totalPoints ? args.totalPoints : undefined, + instructions: args.instructions ? args.instructions : undefined, dueAt: args.dueAt ? args.dueAt : undefined, timeLimit: args.timeLimit ? args.timeLimit : undefined, numQuestions: args.numQuestions ? args.numQuestions : undefined, @@ -157,6 +158,7 @@ export class QuizService { async createQuiz(input: CreateQuiz) { const create = Prisma.validator()({ totalPoints: input.totalPoints, + instructions: input.instructions ? input.instructions : undefined, dueAt: input.dueAt ? input.dueAt : undefined, timeLimit: input.timeLimit ? input.timeLimit : undefined, numQuestions: input.numQuestions, @@ -173,6 +175,7 @@ export class QuizService { const update = Prisma.validator()({ data: { totalPoints: values.totalPoints ? values.totalPoints : undefined, + instructions: values.instructions ? values.instructions : undefined, dueAt: values.dueAt ? values.dueAt : undefined, timeLimit: values.timeLimit ? values.timeLimit : undefined, numQuestions: values.numQuestions ? values.numQuestions : undefined, diff --git a/src/quiz/quiz.spec.ts b/src/quiz/quiz.spec.ts index 534a3f80..e250ad97 100644 --- a/src/quiz/quiz.spec.ts +++ b/src/quiz/quiz.spec.ts @@ -77,6 +77,7 @@ describe("Quiz Services", () => { expect(fakeQuiz).toBeDefined(); expect(fakeQuiz.id).toBeDefined(); expect(fakeQuiz.totalPoints).toEqual(quizData.totalPoints); + expect(fakeQuiz.instructions).toEqual(quizData.instructions); expect(fakeQuiz.numQuestions).toEqual(quizData.numQuestions); expect(fakeQuiz.minScore).toEqual(quizData.minScore); expect(fakeQuiz.dueAt).toEqual(quizData.dueAt); @@ -154,6 +155,7 @@ describe("Quiz Services", () => { const params = fakeQuiz; const quizzes = await resolver.quiz({ totalPoints: fakeQuiz.totalPoints, + instructions: fakeQuiz.instructions, dueAt: fakeQuiz.dueAt, numQuestions: fakeQuiz.numQuestions, minScore: fakeQuiz.minScore, diff --git a/src/quiz/schema.graphql b/src/quiz/schema.graphql index 0451c02a..e0c43a50 100644 --- a/src/quiz/schema.graphql +++ b/src/quiz/schema.graphql @@ -10,6 +10,10 @@ type Quiz { """ The time the Quiz is due """ + instructions: String + """ + The time the Quiz is due + """ dueAt: Date """ The allotted time to take this quiz in ms @@ -300,6 +304,10 @@ input CreateQuiz { """ totalPoints: Float! """ + The instruction field for this quiz + """ + instructions: String + """ The time the Quiz is due """ dueAt: Date diff --git a/utils/fakes.ts b/utils/fakes.ts index 4a7e8d9d..58788ab8 100644 --- a/utils/fakes.ts +++ b/utils/fakes.ts @@ -204,6 +204,7 @@ export function createRandomQuiz(parentID?: string): Quiz { return { id: faker.database.mongodbObjectId(), totalPoints: questions, + instructions: faker.lorem.sentence(), dueAt: faker.date.future(), timeLimit: faker.datatype.number(), numQuestions: questions, diff --git a/utils/tests.ts b/utils/tests.ts index 7f23bdd1..14bfb8c3 100644 --- a/utils/tests.ts +++ b/utils/tests.ts @@ -91,6 +91,7 @@ export const createCollection = async ( export const createQuiz = async (resolver: QuizResolver, input: Quiz) => { const data: CreateQuiz = { totalPoints: input.totalPoints, + instructions: input.instructions, dueAt: input.dueAt, timeLimit: input.timeLimit, numQuestions: input.numQuestions, From dd02ef2534072d55596706f43e364225fcad888f Mon Sep 17 00:00:00 2001 From: Yao Wang Date: Mon, 10 Apr 2023 22:50:01 -0400 Subject: [PATCH 02/31] update --- gql/graphql.ts | 1 + src/quiz/schema.graphql | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index a9488514..2eb060b5 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -270,6 +270,7 @@ export interface ProgressWaiveArgs { export interface QuizFields { id?: Nullable; totalPoints?: Nullable; + instructions?: Nullable; dueAt?: Nullable; timeLimit?: Nullable; numQuestions?: Nullable; diff --git a/src/quiz/schema.graphql b/src/quiz/schema.graphql index e0c43a50..4f1c06ba 100644 --- a/src/quiz/schema.graphql +++ b/src/quiz/schema.graphql @@ -8,7 +8,7 @@ type Quiz { """ totalPoints: Float! """ - The time the Quiz is due + The instruction field for this quiz """ instructions: String """ @@ -160,6 +160,10 @@ input QuizFields { """ totalPoints: Float """ + The instruction field for this quiz + """ + instructions: String + """ The time the Quiz is due """ dueAt: Date From 1f3b5c116854fe6820421e19fe28655c56d42829 Mon Sep 17 00:00:00 2001 From: Yao Wang Date: Mon, 10 Apr 2023 23:04:16 -0400 Subject: [PATCH 03/31] update --- src/quiz/quiz.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 74083830..e14ab205 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -175,7 +175,6 @@ export class QuizService { const update = Prisma.validator()({ data: { totalPoints: values.totalPoints ? values.totalPoints : undefined, - instructions: values.instructions ? values.instructions : undefined, dueAt: values.dueAt ? values.dueAt : undefined, timeLimit: values.timeLimit ? values.timeLimit : undefined, numQuestions: values.numQuestions ? values.numQuestions : undefined, From 30ac2d96573c0aca021bb08615838d66c0773855 Mon Sep 17 00:00:00 2001 From: Yao Wang Date: Mon, 10 Apr 2023 23:38:04 -0400 Subject: [PATCH 04/31] now the Query.quiz() all passes --- prisma/dbml/schema.dbml | 2 +- prisma/schema.prisma | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 540ea7dc..13b9e04c 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -259,7 +259,7 @@ Table LessonProgress { Table Quiz { id String [pk] totalPoints Float [not null] - instructions String [not null] + instructions String dueAt DateTime timeLimit Int numQuestions Int [not null] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 87fb688f..88066a94 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -296,7 +296,7 @@ model LessonProgress { model Quiz { id String @id @default(auto()) @map("_id") @db.ObjectId totalPoints Float - instructions String + instructions String? dueAt DateTime? timeLimit Int? numQuestions Int From ea1fd6caa4ac02a33c0aa7703fda3db239d8e53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 10:04:34 -0400 Subject: [PATCH 05/31] fix(social): Creates social modal on register for user --- src/auth/auth.service.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index db3b800c..728292a2 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -54,7 +54,7 @@ export class AuthService { }: TokenType = JSON.parse(decoded); if (!sub) { - throw new Error("Invalid token"); + return new Error("Invalid token"); } //Check to see if a user exists already @@ -79,11 +79,31 @@ export class AuthService { const account = await this.registerUser(payload); if (account instanceof Error) { - throw "Error adding plan to user."; + return Error("Error while creating user account."); } else { - await this.pos.addPlan({ + const plan = await this.pos.addPlan({ student: account.id }); + + if (!plan) { + return Error("Error while creating plan of study."); + } else { + const social = await this.prisma.social.create({ + data: { + account: { + connect: { + id: account.id + } + } + } + }); + + if (!social) { + return Error("Error while creating social profile."); + } else { + return account; + } + } } } else { const update = Prisma.validator()({ From c151c5cf4753d1f3515cf1e3c9349c864ae23c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 10:05:23 -0400 Subject: [PATCH 06/31] fix(social): Added error return to login mutation --- src/auth/auth.resolver.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/auth/auth.resolver.ts b/src/auth/auth.resolver.ts index 3e449abc..8758c414 100644 --- a/src/auth/auth.resolver.ts +++ b/src/auth/auth.resolver.ts @@ -23,9 +23,13 @@ export class AuthResolver { //Update the user data here const data = await response.json(); - await this.authService.updateUserData(data.id_token); + const account = await this.authService.updateUserData(data.id_token); - return data.id_token; + if (account instanceof Error) { + throw new Error("Error " + response.status + ": " + response.statusText); + } else { + return data.id_token; + } } @Query("refresh") From 3f1d72e7979bc9d1514377431b7b187ed5ad9667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 10:37:00 -0400 Subject: [PATCH 07/31] feat(app): Added query to get the API Version number --- src/app.resolver.ts | 18 ++++++++++++++++++ src/schema.graphql | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 src/app.resolver.ts create mode 100644 src/schema.graphql diff --git a/src/app.resolver.ts b/src/app.resolver.ts new file mode 100644 index 00000000..550c2e2a --- /dev/null +++ b/src/app.resolver.ts @@ -0,0 +1,18 @@ +import { Resolver, Query } from "@nestjs/graphql"; +import * as dotenv from "dotenv"; + +@Resolver("App") +export class AppResolver { + constructor() { + dotenv.config(); + } + @Query("appVersion") + async appVersion() { + return process.env.npm_package_version; + } + + @Query("appInstance") + async appInstance() { + return process.env.NODE_ENV; + } +} diff --git a/src/schema.graphql b/src/schema.graphql new file mode 100644 index 00000000..80271c08 --- /dev/null +++ b/src/schema.graphql @@ -0,0 +1,4 @@ +type Query { + appVersion: String! + appInstance: String! +} \ No newline at end of file From 938b1701e99c4e3b7091ce7102c738f8d8893684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 10:37:22 -0400 Subject: [PATCH 08/31] chore: Bumped version number --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index edaee400..24f91ec0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "emseapi", - "version": "0.2.4", + "version": "0.3.5", "private": true, "scripts": { "start": "nest start", From ce914c66899e3251fdd472bc0450691e4345d71e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 10:37:51 -0400 Subject: [PATCH 09/31] feat(app): Added resolver to list of providers --- src/app.module.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 7513d6de..bd99646d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,7 +13,8 @@ import { CommunityModule } from "./community/community.module"; import { DirectMessageModule } from "@/direct-message/direct-message.module"; import { ProgressModule } from "@/progress"; import { ApolloServerPluginLandingPageLocalDefault } from "apollo-server-core"; -import {QuizModule} from "@/quiz/quiz.module"; +import { QuizModule } from "@/quiz/quiz.module"; +import { AppResolver } from "@/app.resolver"; @Scalar("Date") export class DateScalar implements CustomScalar { @@ -68,7 +69,7 @@ const playgroundConfig = "graphql-ws": true, "subscriptions-transport-ws": false }, - context: ({req, res}) => ({req, res}) + context: ({ req, res }) => ({ req, res }) }), UserModule, PoSModule, @@ -80,6 +81,6 @@ const playgroundConfig = QuizModule ], controllers: [], - providers: [DateScalar] + providers: [DateScalar, AppResolver] }) export class AppModule {} From 5e16d601dbd9ccdb64e84aa092146216d30c3011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 10:43:38 -0400 Subject: [PATCH 10/31] feat(lesson): Updated schemas with removed transcript field --- gql/graphql.ts | 3 --- prisma/dbml/schema.dbml | 1 - prisma/schema.prisma | 1 - src/program/schema.graphql | 12 ------------ 4 files changed, 17 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index 12e510d5..a3220c66 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -237,7 +237,6 @@ export interface ModuleEnrollmentInput { export interface LessonInput { name: string; content?: Nullable; - transcript?: Nullable; collection: string; position?: Nullable; } @@ -246,7 +245,6 @@ export interface LessonFields { id?: Nullable; name?: Nullable; content?: Nullable; - transcript?: Nullable; thread?: Nullable; collection?: Nullable; position?: Nullable; @@ -691,7 +689,6 @@ export interface Lesson { id: string; name: string; content?: Nullable[]>; - transcript?: Nullable; threads?: Nullable[]>; collection?: Nullable; position?: Nullable; diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 675e2e04..331d9a26 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -123,7 +123,6 @@ Table Collection { Table Lesson { id String [pk] name String [not null] - transcript String collection Collection [not null] collectionID String [not null] position Int [not null, default: 0] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9d75564..f52ba2a4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -148,7 +148,6 @@ model Collection { model Lesson { id String @id @default(auto()) @map("_id") @db.ObjectId name String - transcript String? collection Collection @relation(fields: [collectionID], references: [id], onDelete: Cascade) collectionID String @db.ObjectId position Int @default(0) diff --git a/src/program/schema.graphql b/src/program/schema.graphql index 330864c7..ec2176e8 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -355,10 +355,6 @@ type Lesson { """ content: [Content] """ - An optional transcript of the content data (primarily for videos) - """ - transcript: String - """ The list of threads related to the lesson """ threads: [Thread] @@ -801,10 +797,6 @@ input LessonInput { """ content: ID """ - An optional transcript of the content data (primarily for videos) - """ - transcript: String - """ The ID of the parent collection """ collection: ID! @@ -829,10 +821,6 @@ input LessonFields { """ content: ID """ - An optional transcript of the content data (primarily for videos) - """ - transcript: String - """ The list of ids to threads related to the lesson (Only a single value is supported) """ thread: ID From e247e3f9c5e2d5e7c4eb263ad51233e6e5f7fd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 10:44:11 -0400 Subject: [PATCH 11/31] fix(lesson): Updated queries and mutations to match new schema --- src/program/program.service.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/program/program.service.ts b/src/program/program.service.ts index 46578b76..a186f2e9 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -503,12 +503,11 @@ export class ProgramService { //Fetch Lessons async lesson(input: LessonFields) { - const { id, name, content, transcript, collection, position } = input; + const { id, name, content, collection, position } = input; const where = Prisma.validator()({ ...(id && { id }), ...(name && { name }), - ...(transcript && { transcript }), ...(position && { position }), collection: { id: collection ? collection : undefined }, content: content ? { some: { id: content } } : undefined @@ -998,7 +997,6 @@ export class ProgramService { } } }), - transcript: input.transcript, collection: { connect: { id: input.collection ? input.collection : undefined @@ -1021,7 +1019,6 @@ export class ProgramService { name, // TODO: Allow for list fields to be updated // content, - transcript, // Threads are a list so how these are being updated is going to be a little strange. // The only thing i could think of is if these were a list of IDs in which case the threads // Being refererenced would all have to be modified in this update Lesson. @@ -1031,7 +1028,6 @@ export class ProgramService { const payload = { ...(id && { id }), ...(name && { name }), - ...(transcript && { transcript }), ...(collection && { collection }) }; @@ -1041,7 +1037,6 @@ export class ProgramService { }, data: { name: payload.name, - transcript: payload.transcript, collectionID: payload.collection, position: input.position ? input.position : undefined } From a52fea9904d7b16dfe2dc0a9d4be8d308f0cf9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 11:03:37 -0400 Subject: [PATCH 12/31] fix(course): Added required and Carnegie hours fields to schema --- gql/graphql.ts | 7 +++++++ prisma/dbml/schema.dbml | 2 ++ prisma/schema.prisma | 11 ++++++----- src/program/schema.graphql | 26 +++++++++++++++++++++++--- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index 12e510d5..a7fb4477 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -125,6 +125,8 @@ export interface CourseFields { id?: Nullable; name?: Nullable; module?: Nullable; + required?: Nullable; + carnegieHours?: Nullable; } export interface AssignmentFields { @@ -206,6 +208,9 @@ export interface AssignmentInput { export interface CourseInput { name: string; + module: string; + required: boolean; + carnegieHours: number; } export interface ModuleFeedbackInput { @@ -653,6 +658,8 @@ export interface Course { id: string; name: string; moduleIDs?: Nullable[]>; + required: boolean; + carnegieHours: number; } export interface Module { diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 675e2e04..1d8b21db 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -81,6 +81,8 @@ Table Token { Table Course { id String [pk] name String [not null] + required Boolean [default: false] + carnegieHours Int [default: 135] moduleIDs String[] [not null] module Module [not null] } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9d75564..cb96b7ed 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -97,11 +97,12 @@ model Token { } model Course { - id String @id @default(auto()) @map("_id") @db.ObjectId - name String - - moduleIDs String[] @default([]) @db.ObjectId - module Module[] @relation(fields: [moduleIDs], references: [id]) + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + required Boolean? @default(false) + carnegieHours Int? @default(135) + moduleIDs String[] @default([]) @db.ObjectId + module Module[] @relation(fields: [moduleIDs], references: [id]) } model Module { diff --git a/src/program/schema.graphql b/src/program/schema.graphql index 330864c7..5d8e6be7 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -198,16 +198,22 @@ type Course { course id """ id: ID! - """ Name of the course """ name: String! - """ modules in the course """ moduleIDs: [ID] + """ + Boolean value to check if the course is required/core or an elective + """ + required: Boolean! + """ + Number of Carnegie Unit and Student Hours that the course is worth + """ + carnegieHours: Int! } type Module { @@ -558,6 +564,8 @@ input CourseFields { id: ID name: String module: ID + required: Boolean + carnegieHours: Int } input AssignmentFields { @@ -718,9 +726,21 @@ input AssignmentInput { input CourseInput { """ - New Name of the Course + The name of the given Course """ name: String! + """ + Additional module to be related to the course + """ + module: ID! + """ + The boolean attribute to decide weather the course is part of core courses or electives + """ + required: Boolean! + """ + The Carnegie Hours of the Course + """ + carnegieHours: Int! } input ModuleFeedbackInput { From 5c02ba47357c86fac37de0aa34ab013290a1e5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 12 Apr 2023 11:04:01 -0400 Subject: [PATCH 13/31] fix(course): Updated queries and mutations to match new schema --- src/program/program.service.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/program/program.service.ts b/src/program/program.service.ts index 46578b76..76b7aa7c 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -649,29 +649,33 @@ export class ProgramService { } /// Create a course and assign an initial module to that course - async addCourse(data: Prisma.CourseCreateInput): Promise { - return await this.prisma.course.create({ + async addCourse(data: CourseInput) { + return this.prisma.course.create({ data: { - name: data.name + name: data.name, + carnegieHours: data.carnegieHours, + required: data.required }, include: this.courseInclude }); } - async updateCourse(id: string, data: CourseInput): Promise { - const { name } = data; + async updateCourse(id: string, data: CourseInput) { + const { name, required, carnegieHours } = data; return this.prisma.course.update({ where: { id: id }, data: { - ...(name && { name }) + ...(name && { name }), + ...(required && { required }), + ...(carnegieHours && { carnegieHours }) }, include: this.courseInclude }); } - async deleteCourse(id: string): Promise { + async deleteCourse(id: string) { await this.prisma.course.update({ where: { id @@ -684,7 +688,7 @@ export class ProgramService { include: this.courseInclude }); - return await this.prisma.course.delete({ + return this.prisma.course.delete({ where: { id } From d81faec2b8b66aec9b8de92af79ad2d313b99d78 Mon Sep 17 00:00:00 2001 From: Divya Date: Wed, 12 Apr 2023 12:24:38 -0400 Subject: [PATCH 14/31] added type text to the content type --- gql/graphql.ts | 3 ++- prisma/dbml/schema.dbml | 1 + prisma/schema.prisma | 1 + src/program/schema.graphql | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index 12e510d5..0360b29a 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -25,7 +25,8 @@ export enum ContentType { VIDEO = "VIDEO", CAPTION = "CAPTION", TRANSCRIPT = "TRANSCRIPT", - QUIZ = "QUIZ" + QUIZ = "QUIZ", + TEXT = "TEXT" } export enum FileType { diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 675e2e04..aa480066 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -330,6 +330,7 @@ Enum ContentType { CAPTION TRANSCRIPT QUIZ + TEXT } Enum FileType { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9d75564..70287688 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -369,6 +369,7 @@ enum ContentType { CAPTION TRANSCRIPT QUIZ + TEXT } enum FileType { diff --git a/src/program/schema.graphql b/src/program/schema.graphql index 330864c7..3621130c 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -16,6 +16,7 @@ enum ContentType { CAPTION TRANSCRIPT QUIZ + TEXT } From add76ae57a86a2049a4289439a9059c6f7467da9 Mon Sep 17 00:00:00 2001 From: frontoge Date: Wed, 12 Apr 2023 14:54:30 -0400 Subject: [PATCH 15/31] Add carnegie hours to modules --- gql/graphql.ts | 3 +++ prisma/dbml/schema.dbml | 1 + prisma/schema.prisma | 1 + src/program/program.service.ts | 12 ++++++++---- src/program/schema.graphql | 12 ++++++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index 4abd6763..95ae63af 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -245,6 +245,7 @@ export interface LessonInput { content?: Nullable; collection: string; position?: Nullable; + hours: number; } export interface LessonFields { @@ -254,6 +255,7 @@ export interface LessonFields { thread?: Nullable; collection?: Nullable; position?: Nullable; + hours?: Nullable; } export interface ProgressArgs { @@ -702,6 +704,7 @@ export interface Lesson { position?: Nullable; quizzes?: Nullable; lessonProgress?: Nullable[]>; + hours: number; } export interface Content { diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 5c2f9250..cfe36381 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -128,6 +128,7 @@ Table Lesson { collection Collection [not null] collectionID String [not null] position Int [not null, default: 0] + hours Float [not null, default: 0] content Content [not null] quizzes Quiz [not null] lessonProgress LessonProgress [not null] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5a604dcc..634a534a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -152,6 +152,7 @@ model Lesson { collection Collection @relation(fields: [collectionID], references: [id], onDelete: Cascade) collectionID String @db.ObjectId position Int @default(0) + hours Float @default(0.0) content Content[] quizzes Quiz[] diff --git a/src/program/program.service.ts b/src/program/program.service.ts index cb226ca1..5cab6318 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -1006,7 +1006,8 @@ export class ProgramService { id: input.collection ? input.collection : undefined } }, - position: input.position ? input.position : undefined + position: input.position ? input.position : undefined, + hours: input.hours }, include: this.lessonInclude }); @@ -1027,12 +1028,14 @@ export class ProgramService { // The only thing i could think of is if these were a list of IDs in which case the threads // Being refererenced would all have to be modified in this update Lesson. // thread, - collection + collection, + hours } = input; const payload = { ...(id && { id }), ...(name && { name }), - ...(collection && { collection }) + ...(collection && { collection }), + ...(hours && { hours }) }; const args = Prisma.validator()({ @@ -1042,7 +1045,8 @@ export class ProgramService { data: { name: payload.name, collectionID: payload.collection, - position: input.position ? input.position : undefined + position: input.position ? input.position : undefined, + hours: payload.hours } }); diff --git a/src/program/schema.graphql b/src/program/schema.graphql index 5504ea4b..f629eb61 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -382,6 +382,10 @@ type Lesson { The progress model that is associated with this lesson """ lessonProgress: [LessonProgress] + """ + The number of carnige hours granted for completion of this module + """ + hours: Float! } type Content { @@ -826,6 +830,10 @@ input LessonInput { The index of the lesson in the collection """ position: Int + """ + The number of carnige hours granted for completing this module + """ + hours: Float! } input LessonFields { @@ -853,6 +861,10 @@ input LessonFields { The index of the lesson in the collection """ position: Int + """ + The number of carnige hours granted for completing this module + """ + hours: Float } type Mutation { From e9d509cfb971fc508ffc18520d305681b513f0dd Mon Sep 17 00:00:00 2001 From: Yao Wang Date: Thu, 13 Apr 2023 12:19:20 -0400 Subject: [PATCH 16/31] update quiz now includes instructions field --- gql/graphql.ts | 1 + src/quiz/quiz.service.ts | 1 + src/quiz/quiz.spec.ts | 2 ++ src/quiz/schema.graphql | 4 ++++ 4 files changed, 8 insertions(+) diff --git a/gql/graphql.ts b/gql/graphql.ts index 2eb060b5..169474d1 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -320,6 +320,7 @@ export interface CreateQuiz { export interface UpdateQuiz { totalPoints?: Nullable; + instructions?: Nullable; dueAt?: Nullable; timeLimit?: Nullable; numQuestions?: Nullable; diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index e14ab205..74083830 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -175,6 +175,7 @@ export class QuizService { const update = Prisma.validator()({ data: { totalPoints: values.totalPoints ? values.totalPoints : undefined, + instructions: values.instructions ? values.instructions : undefined, dueAt: values.dueAt ? values.dueAt : undefined, timeLimit: values.timeLimit ? values.timeLimit : undefined, numQuestions: values.numQuestions ? values.numQuestions : undefined, diff --git a/src/quiz/quiz.spec.ts b/src/quiz/quiz.spec.ts index e250ad97..5b0624cd 100644 --- a/src/quiz/quiz.spec.ts +++ b/src/quiz/quiz.spec.ts @@ -305,6 +305,7 @@ describe("Quiz Services", () => { const quizData = createRandomQuiz(); const updatedQuiz = await resolver.updateQuiz(fakeQuiz.id, { totalPoints: quizData.totalPoints, + instructions: quizData.instructions, numQuestions: quizData.numQuestions, minScore: quizData.minScore, parentLesson: otherLesson.id, @@ -312,6 +313,7 @@ describe("Quiz Services", () => { }); expect(updatedQuiz).toBeDefined(); expect(updatedQuiz.totalPoints).toEqual(quizData.totalPoints); + expect(updatedQuiz.instructions).toEqual(quizData.instructions); expect(updatedQuiz.numQuestions).toEqual(quizData.numQuestions); expect(updatedQuiz.minScore).toEqual(quizData.minScore); expect(updatedQuiz.parentLesson.id).toEqual(otherLesson.id); diff --git a/src/quiz/schema.graphql b/src/quiz/schema.graphql index 4f1c06ba..2a55daf0 100644 --- a/src/quiz/schema.graphql +++ b/src/quiz/schema.graphql @@ -339,6 +339,10 @@ input UpdateQuiz { """ totalPoints: Float """ + The instruction field for this quiz + """ + instructions: String + """ The time the Quiz is due """ dueAt: Date From 3be2e65745f1f13a687192e5b8d31ec705585277 Mon Sep 17 00:00:00 2001 From: Matt <54252905+frontoge@users.noreply.github.com> Date: Thu, 13 Apr 2023 15:55:55 -0400 Subject: [PATCH 17/31] Add Learning objectives to modules (lessons) (#489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add learning objectives to lesson schema * Add objectives to createModule Query * Add ability to update lesson objectives * Add objective as lesson query parameter --------- Co-authored-by: Dániel B. Papp --- gql/graphql.ts | 5 ++++- prisma/dbml/schema.dbml | 1 + prisma/schema.prisma | 1 + src/program/program.resolver.ts | 7 +++++-- src/program/program.service.ts | 29 ++++++++++++++++++++++++++--- src/program/schema.graphql | 18 +++++++++++++++++- 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index eb120cd5..b81d22a0 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -245,6 +245,7 @@ export interface LessonInput { content?: Nullable; collection: string; position?: Nullable; + objectives?: Nullable; hours: number; } @@ -255,6 +256,7 @@ export interface LessonFields { thread?: Nullable; collection?: Nullable; position?: Nullable; + objectives?: Nullable; hours?: Nullable; } @@ -493,7 +495,7 @@ export interface IMutation { createCollection(data: CreateCollectionArgs): Collection | Promise; updateCollection(id: string, data: CollectionFields): Collection | Promise; createLesson(input: LessonInput): Lesson | Promise; - updateLesson(input?: Nullable): Nullable | Promise>; + updateLesson(input?: Nullable, replaceObj?: Nullable): Nullable | Promise>; deleteLesson(id: string): Nullable | Promise>; createContent(input: CreateContentArgs): Content | Promise; updateContent(input: ContentFields): Nullable | Promise>; @@ -707,6 +709,7 @@ export interface Lesson { position?: Nullable; quizzes?: Nullable; lessonProgress?: Nullable[]>; + objectives: string[]; hours: number; } diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index d0e4592b..594a10a0 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -128,6 +128,7 @@ Table Lesson { collection Collection [not null] collectionID String [not null] position Int [not null, default: 0] + objectives String[] [not null] hours Float [not null, default: 0] content Content [not null] quizzes Quiz [not null] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8a87e49c..216245e8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -152,6 +152,7 @@ model Lesson { collection Collection @relation(fields: [collectionID], references: [id], onDelete: Cascade) collectionID String @db.ObjectId position Int @default(0) + objectives String[] @default([]) hours Float @default(0.0) content Content[] diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index e275510c..84030c60 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -296,8 +296,11 @@ export class ProgramResolver { } @Mutation("updateLesson") - async updateLesson(@Args("input") input: LessonFields) { - return await this.programService.updateLesson(input); + async updateLesson( + @Args("input") input: LessonFields, + @Args("replaceObj") replaceObj: boolean = false + ) { + return await this.programService.updateLesson(input, replaceObj); } @Mutation("deleteLesson") diff --git a/src/program/program.service.ts b/src/program/program.service.ts index 5cab6318..1204c882 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -503,14 +503,15 @@ export class ProgramService { //Fetch Lessons async lesson(input: LessonFields) { - const { id, name, content, collection, position } = input; + const { id, name, content, collection, position, objectives } = input; const where = Prisma.validator()({ ...(id && { id }), ...(name && { name }), ...(position && { position }), collection: { id: collection ? collection : undefined }, - content: content ? { some: { id: content } } : undefined + content: content ? { some: { id: content } } : undefined, + objectives: objectives ? { hasEvery: objectives } : undefined }); return this.prisma.lesson.findMany({ @@ -1007,6 +1008,7 @@ export class ProgramService { } }, position: input.position ? input.position : undefined, + objectives: input.objectives ? input.objectives : undefined, hours: input.hours }, include: this.lessonInclude @@ -1018,7 +1020,7 @@ export class ProgramService { }); } - async updateLesson(input: LessonFields) { + async updateLesson(input: LessonFields, replaceObj: boolean) { const { id, name, @@ -1029,8 +1031,28 @@ export class ProgramService { // Being refererenced would all have to be modified in this update Lesson. // thread, collection, + objectives, hours } = input; + + const newObjectives = objectives; + // Check that they passed in objectives, an ID and they are NOT replacing the list + if (newObjectives && id && !replaceObj) { + const current = await this.prisma.lesson.findUnique({ + where: { + id + } + }); + if (current) { + // Check if the value is already in the list if its not add it + current.objectives.map((value) => { + if (!newObjectives.includes(value)) { + newObjectives.push(value); + } + }); + } + } + const payload = { ...(id && { id }), ...(name && { name }), @@ -1046,6 +1068,7 @@ export class ProgramService { name: payload.name, collectionID: payload.collection, position: input.position ? input.position : undefined, + objectives: newObjectives ? newObjectives : undefined, hours: payload.hours } }); diff --git a/src/program/schema.graphql b/src/program/schema.graphql index f629eb61..28e64099 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -383,6 +383,10 @@ type Lesson { """ lessonProgress: [LessonProgress] """ + A list of learning objectives being covered in this module + """ + objectives: [String!]! + """ The number of carnige hours granted for completion of this module """ hours: Float! @@ -831,6 +835,10 @@ input LessonInput { """ position: Int """ + The list of learning objectives for this module + """ + objectives: [String!] + """ The number of carnige hours granted for completing this module """ hours: Float! @@ -862,6 +870,10 @@ input LessonFields { """ position: Int """ + Learning objectives that are taught in this module + """ + objectives: [String!] + """ The number of carnige hours granted for completing this module """ hours: Float @@ -988,8 +1000,12 @@ type Mutation { createLesson(input: LessonInput!): Lesson! """ Update a lesson given its ID + The input parameter is the data that will be updated in the Lesson as well as the ID of the lesson to be updated. + The replaceObj parameter specifies the mode in which lesson objectives should be updated. A true value will repalce + existing objectives with the new list passed in, whereas a false value (default) will add the strings in the list to + the existing data. """ - updateLesson(input: LessonFields): Lesson + updateLesson(input: LessonFields, replaceObj: Boolean): Lesson deleteLesson(id: String!): Lesson From f66763decbd68aad943a15c69dd821a00c9fbc47 Mon Sep 17 00:00:00 2001 From: "Daniel B. Papp" <25376026+chef-danny-d@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:14:59 -0400 Subject: [PATCH 18/31] feat(instructor): Modified properties from string to list of strings (#491) Now office hours and research interests are a list of strings as the UI was calling for changes to be made. --- gql/graphql.ts | 8 ++++---- prisma/dbml/schema.dbml | 5 ++--- prisma/schema.prisma | 5 ++--- src/user/schema.graphql | 8 ++++---- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index b81d22a0..79dfbd63 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -434,11 +434,11 @@ export interface UpdateUser { export interface InstructorProfileInput { title?: Nullable; officeLocation?: Nullable; - officeHours?: Nullable; + officeHours?: Nullable[]>; contactPolicy?: Nullable; phone?: Nullable; background?: Nullable; - researchInterest?: Nullable; + researchInterest?: Nullable[]>; selectedPapersAndPublications?: Nullable[]>; personalWebsite?: Nullable; philosophy?: Nullable; @@ -808,11 +808,11 @@ export interface InstructorProfile { account?: Nullable; title?: Nullable; officeLocation?: Nullable; - officeHours?: Nullable; + officeHours?: Nullable[]>; contactPolicy?: Nullable; phone?: Nullable; background?: Nullable; - researchInterest?: Nullable; + researchInterest?: Nullable[]>; selectedPapersAndPublications?: Nullable[]>; personalWebsite?: Nullable; philosophy?: Nullable; diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 594a10a0..8bdbf497 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -39,14 +39,13 @@ Table InstructorProfile { accountID String [unique] title String officeLocation String - officeHours String + officeHours String[] [not null] contactPolicy String phone String background String - researchInterest String + researchInterest String[] [not null] selectedPapersAndPublications String[] [not null] personalWebsite String - philosophy String } Table PlanOfStudy { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 216245e8..c9825c51 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -52,14 +52,13 @@ model InstructorProfile { accountID String? @unique @db.ObjectId title String? officeLocation String? - officeHours String? + officeHours String[] @default([]) contactPolicy String? phone String? background String? - researchInterest String? + researchInterest String[] @default([]) selectedPapersAndPublications String[] @default([]) personalWebsite String? - philosophy String? } model PlanOfStudy { diff --git a/src/user/schema.graphql b/src/user/schema.graphql index 65d22007..80a21397 100644 --- a/src/user/schema.graphql +++ b/src/user/schema.graphql @@ -49,7 +49,7 @@ type InstructorProfile { """ office hours of the user """ - officeHours: String + officeHours: [String] """ contract policy needed """ @@ -65,7 +65,7 @@ type InstructorProfile { """ Research interest of the User """ - researchInterest: String + researchInterest: [String] """ The papers and publications selected """ @@ -322,7 +322,7 @@ input InstructorProfileInput { """ Office hours of Instructor Profile """ - officeHours: String + officeHours: [String] """ Contact Policy of Instructor Profile """ @@ -338,7 +338,7 @@ input InstructorProfileInput { """ Research Interest of the Instructor """ - researchInterest: String + researchInterest: [String] """ Selected Papers and Publications of the Instructor """ From 0104acfb3cb0144978913bcf8c446164c2db521a Mon Sep 17 00:00:00 2001 From: "Daniel B. Papp" <25376026+chef-danny-d@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:44:10 -0400 Subject: [PATCH 19/31] feat(instructor): Deprecated unused fields in instructor profile model (#492) * feat(instructor): Deprecated unused fields in instructor profile model * fix(instructor): Updated test cases to reflect schema * fix(instructor): Updated test cases to match dev branch --- gql/graphql.ts | 6 ------ prisma/schema.prisma | 2 -- src/user/schema.graphql | 24 ------------------------ src/user/user.spec.ts | 36 ++++++++++-------------------------- 4 files changed, 10 insertions(+), 58 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index 79dfbd63..7a915d64 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -436,12 +436,9 @@ export interface InstructorProfileInput { officeLocation?: Nullable; officeHours?: Nullable[]>; contactPolicy?: Nullable; - phone?: Nullable; background?: Nullable; researchInterest?: Nullable[]>; selectedPapersAndPublications?: Nullable[]>; - personalWebsite?: Nullable; - philosophy?: Nullable; } export interface SocialInput { @@ -810,12 +807,9 @@ export interface InstructorProfile { officeLocation?: Nullable; officeHours?: Nullable[]>; contactPolicy?: Nullable; - phone?: Nullable; background?: Nullable; researchInterest?: Nullable[]>; selectedPapersAndPublications?: Nullable[]>; - personalWebsite?: Nullable; - philosophy?: Nullable; } export interface User { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c9825c51..0a54e2a9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -54,11 +54,9 @@ model InstructorProfile { officeLocation String? officeHours String[] @default([]) contactPolicy String? - phone String? background String? researchInterest String[] @default([]) selectedPapersAndPublications String[] @default([]) - personalWebsite String? } model PlanOfStudy { diff --git a/src/user/schema.graphql b/src/user/schema.graphql index 80a21397..40a0529e 100644 --- a/src/user/schema.graphql +++ b/src/user/schema.graphql @@ -55,10 +55,6 @@ type InstructorProfile { """ contactPolicy: String """ - Contact phone of th user - """ - phone: String - """ Background of the Profile """ background: String @@ -70,14 +66,6 @@ type InstructorProfile { The papers and publications selected """ selectedPapersAndPublications: [String] - """ - Personal website of the user - """ - personalWebsite: String - """ - The preferred teaching philosophy of the instructor - """ - philosophy: String } type User { @@ -328,10 +316,6 @@ input InstructorProfileInput { """ contactPolicy: String """ - Phone of Instructor Profile - """ - phone: String - """ Background of Profile """ background: String @@ -343,14 +327,6 @@ input InstructorProfileInput { Selected Papers and Publications of the Instructor """ selectedPapersAndPublications: [String] - """ - Personal Website of the Instructor - """ - personalWebsite: String - """ - Philosophy of the Instructor - """ - philosophy: String } input SocialInput { diff --git a/src/user/user.spec.ts b/src/user/user.spec.ts index b4ed7777..fe2a51e3 100644 --- a/src/user/user.spec.ts +++ b/src/user/user.spec.ts @@ -2,14 +2,14 @@ import { UserService } from "./user.service"; import { UserResolver } from "./user.resolver"; import { PrismaService } from "@/prisma.service"; import { + InstructorProfileInput, Social, + SocialInput, UpdateUser, - User, - InstructorProfileInput, - SocialInput + User } from "@/types/graphql"; import { shuffle } from "../../utils/tests"; -import { test, describe, afterAll, expect } from "vitest"; +import { afterAll, describe, expect, test } from "vitest"; describe("Account services", () => { let service: UserService; @@ -41,14 +41,11 @@ describe("Account services", () => { const updatedInstructorProfileReset: InstructorProfileInput = { title: "Department Chair", officeLocation: "Online", - officeHours: "Anytime", + officeHours: ["Anytime"], contactPolicy: "Email", - phone: "757-555-5555", background: "I am a professor", - researchInterest: "I am a professor", - selectedPapersAndPublications: [""], - personalWebsite: "https://odu.edu/emse", - philosophy: "I teach people" + researchInterest: ["I am a professor"], + selectedPapersAndPublications: [""] }; prisma = new PrismaService(); @@ -152,21 +149,17 @@ describe("Account services", () => { expect(instructorProfile.researchInterest).toBeDefined(); expect(instructorProfile.selectedPapersAndPublications).toBeDefined(); expect(instructorProfile.personalWebsite).toBeDefined(); - expect(instructorProfile.philosophy).toBeDefined(); }); test("should update instructor profile given input argument", async () => { const updatedInstructorProfile: InstructorProfileInput = { title: "Adjunct Professor", officeLocation: "ESB 2101", - officeHours: "9AM - 5PM - MWF", + officeHours: ["9AM - 5PM - MWF"], contactPolicy: "Email", - phone: "111-222-3333", background: "I'm a very experienced professor, and I only use books from 50 years ago.", - researchInterest: "Technology", - selectedPapersAndPublications: [""], - personalWebsite: "https://odu.edu/emse", - philosophy: "I teach people" + researchInterest: ["Technology"], + selectedPapersAndPublications: [""] }; const input: UpdateUser = { id: userDocumentID, @@ -191,9 +184,6 @@ describe("Account services", () => { expect(updateUser.instructorProfile.contactPolicy).toEqual( updatedInstructorProfile.contactPolicy ); - expect(updateUser.instructorProfile.phone).toEqual( - updatedInstructorProfile.phone - ); expect(updateUser.instructorProfile.background).toEqual( updatedInstructorProfile.background ); @@ -203,12 +193,6 @@ describe("Account services", () => { expect( updateUser.instructorProfile.selectedPapersAndPublications ).toEqual(updatedInstructorProfile.selectedPapersAndPublications); - expect(updateUser.instructorProfile.personalWebsite).toEqual( - updatedInstructorProfile.personalWebsite - ); - expect(updateUser.instructorProfile.philosophy).toEqual( - updatedInstructorProfile.philosophy - ); }); }); describe("Social", () => { From 16d223cb28ceaeee3e6a99b51a12d81b83b90e75 Mon Sep 17 00:00:00 2001 From: Matt <54252905+frontoge@users.noreply.github.com> Date: Mon, 24 Apr 2023 13:05:47 -0400 Subject: [PATCH 20/31] Rename Modules to Sections (#496) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update module name to section in schema * Replace modules with sections in resolvers * Rename modules to sections in service files --------- Co-authored-by: Dániel B. Papp --- gql/graphql.ts | 130 ++++++------ prisma/dbml/schema.dbml | 74 +++---- prisma/schema.prisma | 116 +++++------ src/pos/pos.service.ts | 22 +- src/pos/schema.graphql | 12 +- src/program/program.resolver.ts | 160 ++++++++------- src/program/program.service.ts | 324 +++++++++++++++--------------- src/program/schema.graphql | 290 +++++++++++++------------- src/progress/progress.resolver.ts | 44 ++-- src/progress/progress.service.ts | 2 +- src/progress/schema.graphql | 10 +- src/user/schema.graphql | 4 +- src/user/user.service.ts | 8 +- 13 files changed, 602 insertions(+), 594 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index 7a915d64..8bf97397 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -68,9 +68,9 @@ export interface PlanInput { export interface PlanFields { id?: Nullable; student?: Nullable; - module?: Nullable; + section?: Nullable; assignmentResult?: Nullable; - modulesLeft?: Nullable; + sectionsLeft?: Nullable; } export interface CreateContentArgs { @@ -90,7 +90,7 @@ export interface ContentFields { export interface CreateCollectionArgs { name: string; - moduleID: string; + sectionID: string; lessons?: Nullable; positionIndex: number; } @@ -98,15 +98,15 @@ export interface CreateCollectionArgs { export interface CollectionFields { id?: Nullable; name?: Nullable; - moduleID?: Nullable; + sectionID?: Nullable; lessons?: Nullable[]>; positionIndex?: Nullable; } -export interface ModuleFields { +export interface SectionFields { id?: Nullable; - moduleNumber?: Nullable; - moduleName?: Nullable; + sectionNumber?: Nullable; + sectionName?: Nullable; description?: Nullable; duration?: Nullable; intro?: Nullable; @@ -118,14 +118,14 @@ export interface ModuleFields { assignments?: Nullable; members?: Nullable; feedback?: Nullable; - parentModules?: Nullable; - subModules?: Nullable; + parentSections?: Nullable; + subSections?: Nullable; } export interface CourseFields { id?: Nullable; name?: Nullable; - module?: Nullable; + section?: Nullable; required?: Nullable; carnegieHours?: Nullable; } @@ -138,7 +138,7 @@ export interface AssignmentFields { contentURL?: Nullable; contentType?: Nullable; acceptedTypes?: Nullable; - module?: Nullable; + section?: Nullable; assignmentResult?: Nullable; } @@ -147,7 +147,7 @@ export interface ModFeedbackFields { feedback?: Nullable; rating?: Nullable; student?: Nullable; - module?: Nullable; + section?: Nullable; } export interface AssignmentResFields { @@ -166,13 +166,13 @@ export interface ModEnrollmentFields { id?: Nullable; enrolledAt?: Nullable; role?: Nullable; - module?: Nullable; + section?: Nullable; plan?: Nullable; } -export interface NewModule { - moduleNumber: number; - moduleName: string; +export interface NewSection { + sectionNumber: number; + sectionName: string; description: string; duration: number; intro: string; @@ -180,10 +180,10 @@ export interface NewModule { keywords: string[]; } -export interface UpdateModule { +export interface UpdateSection { id: string; - moduleName?: Nullable; - moduleNumber?: Nullable; + sectionName?: Nullable; + sectionNumber?: Nullable; intro?: Nullable; description?: Nullable; duration?: Nullable; @@ -195,7 +195,7 @@ export interface UpdateModule { export interface NewAssignment { name: string; dueAt: Date; - module: string; + section: string; contentType: string; contentURL: string; acceptedTypes: FileType; @@ -204,22 +204,22 @@ export interface NewAssignment { export interface AssignmentInput { name?: Nullable; dueAt?: Nullable; - module?: Nullable; + section?: Nullable; } export interface CourseInput { name: string; - module: string; + section: string; required: boolean; carnegieHours: number; } -export interface ModuleFeedbackInput { +export interface SectionFeedbackInput { feedback: string; rating: number; } -export interface ModuleFeedbackUpdate { +export interface SectionFeedbackUpdate { feedback?: Nullable; rating?: Nullable; } @@ -233,8 +233,8 @@ export interface NewAssignmentResult { fileType: string; } -export interface ModuleEnrollmentInput { - module: string; +export interface SectionEnrollmentInput { + section: string; plan: string; role: UserRole; status: EnrollmentStatus; @@ -271,7 +271,7 @@ export interface ProgressArgs { export interface ProgressWaiveArgs { enrollmentID?: Nullable; - moduleID?: Nullable; + sectionID?: Nullable; planID?: Nullable; } @@ -468,27 +468,27 @@ export interface IMutation { addPlan(input?: Nullable): PlanOfStudy | Promise; updatePlan(id: string, input?: Nullable): Nullable | Promise>; deletePlan(id: string): Nullable | Promise>; - deleteModule(id: string): Nullable | Promise>; - addModule(input?: Nullable): Module | Promise; - updateModule(input?: Nullable): Nullable | Promise>; + deleteSection(id: string): Nullable
| Promise>; + addSection(input?: Nullable): Section | Promise
; + updateSection(input?: Nullable): Nullable
| Promise>; deleteCourse(id: string): Nullable | Promise>; addCourse(input?: Nullable): Course | Promise; updateCourse(id: string, input?: Nullable): Nullable | Promise>; addAssignment(input?: Nullable): Assignment | Promise; - addObjectives(id: string, input?: Nullable): Nullable | Promise>; - deleteAssignment(module: string, id: string): Nullable | Promise>; + addObjectives(id: string, input?: Nullable): Nullable
| Promise>; + deleteAssignment(section: string, id: string): Nullable
| Promise>; updateAssignment(id: string, input?: Nullable): Nullable | Promise>; - addModuleFeedback(moduleId: string, userId: string, input?: Nullable): Nullable | Promise>; - updateModuleFeedback(id: string, input?: Nullable): Nullable | Promise>; - deleteModuleFeedback(id: string): Nullable | Promise>; + addSectionFeedback(sectionId: string, userId: string, input?: Nullable): Nullable
| Promise>; + updateSectionFeedback(id: string, input?: Nullable): Nullable | Promise>; + deleteSectionFeedback(id: string): Nullable | Promise>; addAssignmentResult(input?: Nullable): AssignmentResult | Promise; updateAssignmentResult(id: string, result: number): Nullable | Promise>; deleteAssignmentResult(id: string): Nullable | Promise>; - addModuleEnrollment(input?: Nullable): ModuleEnrollment | Promise; - updateModuleEnrollment(id: string, input?: Nullable): Nullable | Promise>; - deleteModuleEnrollment(id: string): Nullable | Promise>; - pairCourseModule(courseId: string, moduleId: string): Module | Promise; - unpairCourseModule(courseId: string, moduleId: string): Nullable | Promise>; + addSectionEnrollment(input?: Nullable): SectionEnrollment | Promise; + updateSectionEnrollment(id: string, input?: Nullable): Nullable | Promise>; + deleteSectionEnrollment(id: string): Nullable | Promise>; + pairCourseSection(courseId: string, sectionId: string): Section | Promise
; + unpairCourseSection(courseId: string, sectionId: string): Nullable
| Promise>; createCollection(data: CreateCollectionArgs): Collection | Promise; updateCollection(id: string, data: CollectionFields): Collection | Promise; createLesson(input: LessonInput): Lesson | Promise; @@ -498,7 +498,7 @@ export interface IMutation { updateContent(input: ContentFields): Nullable | Promise>; deleteContent(contentID: string): Nullable | Promise>; createProgress(input: ProgressArgs, enrollmentID: string): Progress | Promise; - waiveModule(args: ProgressWaiveArgs): Progress | Promise; + waiveSection(args: ProgressWaiveArgs): Progress | Promise; deleteProgress(id: string): boolean | Promise; updateProgress(status: number, id?: Nullable, enrollmentID?: Nullable): Progress | Promise; createQuiz(input?: Nullable): Quiz | Promise; @@ -536,13 +536,13 @@ export interface IQuery { plans(): Nullable | Promise>; planByID(id: string): Nullable | Promise>; planByParams(input?: Nullable): Nullable | Promise>; - module(input: ModuleFields, memberRole?: Nullable): Nullable | Promise>; + section(input: SectionFields, memberRole?: Nullable): Nullable | Promise>; course(input: CourseFields): Nullable | Promise>; assignment(input: AssignmentFields): Nullable | Promise>; - moduleFeedback(input: ModFeedbackFields): Nullable | Promise>; + sectionFeedback(input: ModFeedbackFields): Nullable | Promise>; assignmentResult(input: AssignmentResFields): Nullable | Promise>; - moduleEnrollment(input: ModEnrollmentFields): Nullable | Promise>; - lessonsByModuleEnrollment(planID: string, moduleID: string): Nullable | Promise>; + sectionEnrollment(input: ModEnrollmentFields): Nullable | Promise>; + lessonsBySectionEnrollment(planID: string, SectionID: string): Nullable | Promise>; collection(input?: Nullable): Nullable[]> | Promise[]>>; lesson(input?: Nullable): Nullable | Promise>; content(input?: Nullable): Nullable | Promise>; @@ -607,18 +607,18 @@ export interface Group { export interface PlanOfStudy { id: string; student?: Nullable; - modules?: Nullable[]>; + sections?: Nullable[]>; assignmentResults?: Nullable; - modulesLeft?: Nullable[]>; + sectionsLeft?: Nullable[]>; quizResults?: Nullable; } -export interface ModuleEnrollment { +export interface SectionEnrollment { id: string; enrolledAt: Date; role: UserRole; status: EnrollmentStatus; - module: Module; + section: Section; plan?: Nullable; inactivePlan?: Nullable; progress: Progress; @@ -645,30 +645,30 @@ export interface Assignment { contentURL?: Nullable; contentType?: Nullable; acceptedTypes?: Nullable; - module: Module; + section: Section; assignmentResults?: Nullable[]>; } -export interface ModuleFeedback { +export interface SectionFeedback { id: string; feedback: string; rating: number; student?: Nullable; - module?: Nullable; + section?: Nullable
; } export interface Course { id: string; name: string; - moduleIDs?: Nullable[]>; + sectionIDs?: Nullable[]>; required: boolean; carnegieHours: number; } -export interface Module { +export interface Section { id: string; - moduleNumber: number; - moduleName: string; + sectionNumber: number; + sectionName: string; description: string; duration: number; intro: string; @@ -678,10 +678,10 @@ export interface Module { createdAt: Date; updatedAt: Date; assignments: Assignment[]; - members: ModuleEnrollment[]; - feedback: ModuleFeedback[]; - parentModules: Module[]; - subModules: Module[]; + members: SectionEnrollment[]; + feedback: SectionFeedback[]; + parentSections: Section[]; + subSections: Section[]; collections: Collection[]; courseIDs: string[]; } @@ -692,8 +692,8 @@ export interface Collection { createdAt: Date; updatedAt: Date; lessons?: Nullable[]>; - module: Module; - moduleID: string; + section: Section; + sectionID: string; position?: Nullable; } @@ -728,7 +728,7 @@ export interface Progress { completed: boolean; createdAt: Date; updatedAt: Date; - enrollment: ModuleEnrollment; + enrollment: SectionEnrollment; } export interface LessonProgress { @@ -737,7 +737,7 @@ export interface LessonProgress { completed: boolean; createdAt: Date; updatedAt: Date; - enrollment: ModuleEnrollment; + enrollment: SectionEnrollment; lesson: Lesson; } @@ -829,7 +829,7 @@ export interface User { social?: Nullable; plan?: Nullable; tokens?: Nullable; - feedback?: Nullable; + feedback?: Nullable; assignmentGraded?: Nullable; instructorProfile?: Nullable; watchedThreads?: Nullable; diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 8bdbf497..aa21d498 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -19,7 +19,7 @@ Table User { social Social plan PlanOfStudy tokens Token [not null] - feedback ModuleFeedback [not null] + feedback SectionFeedback [not null] assignmentGraded AssignmentResult [not null] instructorProfile InstructorProfile watchedThreads Thread [not null] @@ -52,7 +52,7 @@ Table PlanOfStudy { id String [pk] student User studentID String [unique] - modules ModuleEnrollment [not null] + sections SectionEnrollment [not null] assignmentResults AssignmentResult [not null] quizResults QuizResult [not null] } @@ -82,14 +82,14 @@ Table Course { name String [not null] required Boolean [default: false] carnegieHours Int [default: 135] - moduleIDs String[] [not null] - module Module [not null] + sectionIDs String[] [not null] + sections Section [not null] } -Table Module { +Table Section { id String [pk] - moduleNumber Int [unique, not null] - moduleName String [unique, not null] + sectionNumber Int [unique, not null] + sectionName String [unique, not null] description String [not null] duration Float [not null] intro String [not null] @@ -98,13 +98,13 @@ Table Module { objectives String[] [not null] createdAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null] - members ModuleEnrollment [not null] + members SectionEnrollment [not null] assignments Assignment [not null] - feedback ModuleFeedback [not null] - parentModules Module [not null] - parentModuleIDs String[] [not null] - subModules Module [not null] - subModuleIDs String[] [not null] + feedback SectionFeedback [not null] + parentSections Section [not null] + parentSectionIDs String[] [not null] + subSections Section [not null] + subSectionIDs String[] [not null] collections Collection [not null] courseIDs String[] [not null] course Course [not null] @@ -117,8 +117,8 @@ Table Collection { updatedAt DateTime [default: `now()`, not null] position Int [not null, default: 0] lessons Lesson [not null] - module Module [not null] - moduleID String [not null] + section Section [not null] + sectionID String [not null] } Table Lesson { @@ -134,23 +134,23 @@ Table Lesson { lessonProgress LessonProgress [not null] } -Table ModuleFeedback { +Table SectionFeedback { id String [pk] feedback String [not null] rating Int [not null] studentId String [not null] - moduleId String [not null] + sectionId String [not null] student User [not null] - module Module [not null] + section Section [not null] } -Table ModuleEnrollment { +Table SectionEnrollment { id String [pk] enrolledAt DateTime [default: `now()`, not null] role UserRole [not null] status EnrollmentStatus [not null, default: 'INACTIVE'] - module Module [not null] - moduleId String [not null] + section Section [not null] + sectionId String [not null] plan PlanOfStudy planID String progress Progress @@ -165,8 +165,8 @@ Table Assignment { contentURL String [not null] contentType String [not null] acceptedTypes FileType [not null, default: 'DOC'] - moduleId String [not null] - module Module [not null] + sectionId String [not null] + section Section [not null] assignmentResults AssignmentResult [not null] } @@ -242,7 +242,7 @@ Table Progress { updatedAt DateTime [default: `now()`, not null] status Float [not null, default: 0] completed Boolean [not null, default: false] - enrollment ModuleEnrollment [not null] + enrollment SectionEnrollment [not null] enrollmentID String [unique, not null] } @@ -254,7 +254,7 @@ Table LessonProgress { completed Boolean [not null, default: false] lesson Lesson [not null] lessonID String [not null] - enrollment ModuleEnrollment [not null] + enrollment SectionEnrollment [not null] enrollmentID String [unique, not null] } @@ -358,27 +358,27 @@ Ref: Social.accountID - User.id Ref: Token.userId > User.id -Ref: Course.moduleIDs > Module.id +Ref: Course.sectionIDs > Section.id -Ref: Module.parentModuleIDs > Module.id +Ref: Section.parentSectionIDs > Section.id -Ref: Module.subModuleIDs > Module.id +Ref: Section.subSectionIDs > Section.id -Ref: Module.courseIDs > Course.id +Ref: Section.courseIDs > Course.id -Ref: Collection.moduleID > Module.id [delete: Cascade] +Ref: Collection.sectionID > Section.id [delete: Cascade] Ref: Lesson.collectionID > Collection.id [delete: Cascade] -Ref: ModuleFeedback.studentId > User.id +Ref: SectionFeedback.studentId > User.id -Ref: ModuleFeedback.moduleId > Module.id +Ref: SectionFeedback.sectionId > Section.id -Ref: ModuleEnrollment.moduleId > Module.id +Ref: SectionEnrollment.sectionId > Section.id -Ref: ModuleEnrollment.planID > PlanOfStudy.id +Ref: SectionEnrollment.planID > PlanOfStudy.id -Ref: Assignment.moduleId > Module.id +Ref: Assignment.sectionId > Section.id Ref: AssignmentResult.studentId > PlanOfStudy.id @@ -404,11 +404,11 @@ Ref: DirectMessage.groupID > Group.id Ref: Group.memberIDs > User.id -Ref: Progress.enrollmentID - ModuleEnrollment.id [delete: Cascade] +Ref: Progress.enrollmentID - SectionEnrollment.id [delete: Cascade] Ref: LessonProgress.lessonID > Lesson.id [delete: Cascade] -Ref: LessonProgress.enrollmentID > ModuleEnrollment.id [delete: Cascade] +Ref: LessonProgress.enrollmentID > SectionEnrollment.id [delete: Cascade] Ref: Quiz.parentLessonID > Lesson.id [delete: Cascade] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0a54e2a9..b5a3831d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,7 +32,7 @@ model User { social Social? @relation(name: "social") plan PlanOfStudy? @relation(name: "plan") tokens Token[] - feedback ModuleFeedback[] + feedback SectionFeedback[] assignmentGraded AssignmentResult[] @relation(name: "graded") instructorProfile InstructorProfile? watchedThreads Thread[] @relation(fields: [watchedThreadIDs], references: [id]) @@ -60,13 +60,13 @@ model InstructorProfile { } model PlanOfStudy { - id String @id @default(auto()) @map("_id") @db.ObjectId - student User? @relation(name: "plan", fields: [studentID], references: [id]) - studentID String? @unique @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId + student User? @relation(name: "plan", fields: [studentID], references: [id]) + studentID String? @unique @db.ObjectId // ill figure advisors out later, my brain needs a rest // advisor User? @relation(fields: [advisorId], references:[id]) // advisorId String? @db.ObjectId - modules ModuleEnrollment[] + sections SectionEnrollment[] assignmentResults AssignmentResult[] quizResults QuizResult[] } @@ -94,37 +94,37 @@ model Token { } model Course { - id String @id @default(auto()) @map("_id") @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId name String - required Boolean? @default(false) - carnegieHours Int? @default(135) - moduleIDs String[] @default([]) @db.ObjectId - module Module[] @relation(fields: [moduleIDs], references: [id]) + required Boolean? @default(false) + carnegieHours Int? @default(135) + sectionIDs String[] @default([]) @db.ObjectId + sections Section[] @relation(fields: [sectionIDs], references: [id]) } -model Module { - id String @id @default(auto()) @map("_id") @db.ObjectId - moduleNumber Int @unique - moduleName String @unique - description String - duration Float - intro String - numSlides Int - keywords String[] - objectives String[] - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt +model Section { + id String @id @default(auto()) @map("_id") @db.ObjectId + sectionNumber Int @unique + sectionName String @unique + description String + duration Float + intro String + numSlides Int + keywords String[] + objectives String[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt // Relation fields - members ModuleEnrollment[] + members SectionEnrollment[] assignments Assignment[] - feedback ModuleFeedback[] + feedback SectionFeedback[] - parentModules Module[] @relation("parentModules", fields: [parentModuleIDs], references: [id]) - parentModuleIDs String[] @default([]) @db.ObjectId - subModules Module[] @relation("parentModules", fields: [subModuleIDs], references: [id]) - subModuleIDs String[] @default([]) @db.ObjectId - collections Collection[] @relation(name: "collections") + parentSections Section[] @relation("parentModules", fields: [parentSectionIDs], references: [id]) + parentSectionIDs String[] @default([]) @db.ObjectId + subSections Section[] @relation("parentModules", fields: [subSectionIDs], references: [id]) + subSectionIDs String[] @default([]) @db.ObjectId + collections Collection[] @relation(name: "collections") courseIDs String[] @default([]) @db.ObjectId course Course[] @relation(fields: [courseIDs], references: [id]) @@ -138,9 +138,9 @@ model Collection { position Int @default(0) // Relation fields - lessons Lesson[] - module Module @relation(name: "collections", fields: [moduleID], references: [id], onDelete: Cascade) - moduleID String @db.ObjectId + lessons Lesson[] + section Section @relation(name: "collections", fields: [sectionID], references: [id], onDelete: Cascade) + sectionID String @db.ObjectId } model Lesson { @@ -157,26 +157,26 @@ model Lesson { lessonProgress LessonProgress[] } -model ModuleFeedback { +model SectionFeedback { id String @id @default(auto()) @map("_id") @db.ObjectId feedback String rating Int - studentId String @db.ObjectId - moduleId String @db.ObjectId - student User @relation(fields: [studentId], references: [id]) - module Module @relation(fields: [moduleId], references: [id]) + studentId String @db.ObjectId + sectionId String @db.ObjectId + student User @relation(fields: [studentId], references: [id]) + section Section @relation(fields: [sectionId], references: [id]) } -model ModuleEnrollment { +model SectionEnrollment { id String @id @default(auto()) @map("_id") @db.ObjectId enrolledAt DateTime @default(now()) role UserRole // Allow for instructors, graders and students to take part in the module. status EnrollmentStatus @default(INACTIVE) // Relation Fields - module Module @relation(fields: [moduleId], references: [id]) - moduleId String @db.ObjectId + section Section @relation(fields: [sectionId], references: [id]) + sectionId String @db.ObjectId plan PlanOfStudy? @relation(fields: [planID], references: [id]) planID String? @db.ObjectId progress Progress? @@ -193,8 +193,8 @@ model Assignment { acceptedTypes FileType @default(DOC) // Relation Fields - moduleId String @db.ObjectId - module Module @relation(fields: [moduleId], references: [id]) + sectionId String @db.ObjectId + section Section @relation(fields: [sectionId], references: [id]) assignmentResults AssignmentResult[] } @@ -269,27 +269,27 @@ model Group { } model Progress { - id String @id @default(auto()) @map("_id") @db.ObjectId - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - status Float @default(0) - completed Boolean @default(false) + id String @id @default(auto()) @map("_id") @db.ObjectId + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + status Float @default(0) + completed Boolean @default(false) // Relation Fields - enrollment ModuleEnrollment @relation(fields: [enrollmentID], references: [id], onDelete: Cascade) - enrollmentID String @unique @db.ObjectId + enrollment SectionEnrollment @relation(fields: [enrollmentID], references: [id], onDelete: Cascade) + enrollmentID String @unique @db.ObjectId } model LessonProgress { - id String @id @default(auto()) @map("_id") @db.ObjectId - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - status Float @default(0) - completed Boolean @default(false) - lesson Lesson @relation(fields: [lessonID], references: [id], onDelete: Cascade) - lessonID String @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + status Float @default(0) + completed Boolean @default(false) + lesson Lesson @relation(fields: [lessonID], references: [id], onDelete: Cascade) + lessonID String @db.ObjectId // Relation Fields - enrollment ModuleEnrollment @relation(fields: [enrollmentID], references: [id], onDelete: Cascade) - enrollmentID String @unique @db.ObjectId + enrollment SectionEnrollment @relation(fields: [enrollmentID], references: [id], onDelete: Cascade) + enrollmentID String @unique @db.ObjectId } model Quiz { diff --git a/src/pos/pos.service.ts b/src/pos/pos.service.ts index 25700b76..8409f328 100644 --- a/src/pos/pos.service.ts +++ b/src/pos/pos.service.ts @@ -8,9 +8,9 @@ export class PoSService { constructor(private prisma: PrismaService) {} private PlanOfStudyInclude = Prisma.validator()({ - modules: { + sections: { include: { - module: { + section: { include: { feedback: true, assignments: true, @@ -62,7 +62,7 @@ export class PoSService { } async planByParams(params: PlanFields) { - const { id, student, module, assignmentResult, modulesLeft } = params; + const { id, student, section, assignmentResult, sectionsLeft } = params; const payload = { ...(id && { id }) @@ -71,10 +71,10 @@ export class PoSService { payload["studentId"] = student; } - if (module) { - payload["modules"] = { + if (section) { + payload["sections"] = { some: { - id: module + id: section } }; } @@ -85,10 +85,10 @@ export class PoSService { } }; } - if (modulesLeft) { - payload["modulesLeft"] = { + if (sectionsLeft) { + payload["sectionsLeft"] = { some: { - id: modulesLeft + id: sectionsLeft } }; } @@ -105,7 +105,7 @@ export class PoSService { else return res; } - // TODO: Allow for starting modules and courses + // TODO: Allow for starting sections and courses async addPlan(input: PlanInput) { return this.prisma.planOfStudy.create({ data: { @@ -117,7 +117,7 @@ export class PoSService { }); } - // TODO: Handle connections to modules, courses and assignment results + // TODO: Handle connections to sections, courses and assignment results async updatePlan(id: string, input: PlanInput) { return this.prisma.planOfStudy.update({ where: { diff --git a/src/pos/schema.graphql b/src/pos/schema.graphql index d66f262e..d595ca47 100644 --- a/src/pos/schema.graphql +++ b/src/pos/schema.graphql @@ -6,9 +6,9 @@ type PlanOfStudy { student: User """ - Modules that are enrolled by the Student + Sections that are enrolled by the Student """ - modules: [ModuleEnrollment] + sections: [SectionEnrollment] """ Results obtained for the Assignement Taken @@ -16,9 +16,9 @@ type PlanOfStudy { assignmentResults: [AssignmentResult!] """ - Modules to be completed + Sections to be completed """ - modulesLeft:[ModuleEnrollment] + sectionsLeft:[SectionEnrollment] """ Results on quizzes taken @@ -34,9 +34,9 @@ input PlanFields { id: String student: String - module: String + section: String assignmentResult: String - modulesLeft: String + sectionsLeft: String } type Query { diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index 84030c60..fd8d925f 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -1,12 +1,12 @@ import { AssignmentInput, CourseInput, - ModuleEnrollmentInput, - ModuleFeedbackUpdate, + SectionEnrollmentInput, + SectionFeedbackUpdate, NewAssignment, NewAssignmentResult, - UpdateModule, - ModuleFields, + UpdateSection, + SectionFields, CourseFields, AssignmentFields, ModFeedbackFields, @@ -17,7 +17,7 @@ import { CreateCollectionArgs, CreateContentArgs, ModEnrollmentFields, - NewModule, + NewSection, CollectionFields } from "@/types/graphql"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; @@ -31,25 +31,25 @@ import { AuthGuard } from "@/auth.guard"; export class ProgramResolver { constructor(private readonly programService: ProgramService) {} - // Get Module(s) - @Query("module") - async module( - @Args("input") args: ModuleFields, + // Get Section(s) + @Query("section") + async section( + @Args("input") args: SectionFields, @Args("memberRole") role?: UserRole ) { - const result = await this.programService.module(args); + const result = await this.programService.section(args); if (!result) { - return new Error("Module not found"); + return new Error("Section not found"); } if (!role) { return result; } else { - return result.map((module) => { - const thisModule = module; - thisModule.members = module.members.filter( + return result.map((section) => { + const thisSection = section; + thisSection.members = section.members.filter( (value) => value.role === role ); - return thisModule; + return thisSection; }); } } @@ -65,9 +65,9 @@ export class ProgramResolver { return await this.programService.assignment(args); } - @Query("moduleFeedback") - async moduleFeedback(@Args("input") args: ModFeedbackFields) { - return await this.programService.moduleFeedback(args); + @Query("sectionFeedback") + async sectionFeedback(@Args("input") args: ModFeedbackFields) { + return await this.programService.sectionFeedback(args); } @Query("assignmentResult") @@ -75,28 +75,29 @@ export class ProgramResolver { return await this.programService.assignmentResult(args); } - @Query("moduleEnrollment") - async moduleEnrollment(@Args("input") args: ModEnrollmentFields) { - return await this.programService.moduleEnrollment(args); + @Query("sectionEnrollment") + async sectionEnrollment(@Args("input") args: ModEnrollmentFields) { + return await this.programService.sectionEnrollment(args); } - @Query("lessonsByModuleEnrollment") - async lessonsByModuleEnrollment( + @Query("lessonsBySectionEnrollment") + async lessonsBySectionEnrollment( @Args("planID") planID: string, - @Args("moduleID") moduleID: string + @Args("sectionID") sectionID: string ) { - const enrollment = await this.programService.moduleEnrollment({ + const enrollment = await this.programService.sectionEnrollment({ plan: planID }); const filteredEnrollment = enrollment.filter((enrollment) => { - return enrollment.module.id === moduleID; + return enrollment.section.id === sectionID; }); - const lessons = filteredEnrollment[0].module.collections.map((collection) => - collection.lessons.map((lesson) => { - return lesson; - }) + const lessons = filteredEnrollment[0].section.collections.map( + (collection) => + collection.lessons.map((lesson) => { + return lesson; + }) ); const filteredLessons = lessons.flat().map((lesson) => { @@ -137,22 +138,22 @@ export class ProgramResolver { // Mutations - // Add a module to the db with all required initial fields - @Mutation("addModule") - async create(@Args("input") args: NewModule) { - return await this.programService.addModule(args); + // Add a section to the db with all required initial fields + @Mutation("addSection") + async create(@Args("input") args: NewSection) { + return await this.programService.addSection(args); } - // Update a single module's data in the db - @Mutation("updateModule") - async update(@Args("input") args: UpdateModule) { - return await this.programService.updateModule(args); + // Update a single Section's data in the db + @Mutation("updateSection") + async update(@Args("input") args: UpdateSection) { + return await this.programService.updateSection(args); } - // Delete a module from db - @Mutation("deleteModule") + // Delete a Section from db + @Mutation("deleteSection") async delete(@Args("id") args: string) { - return await this.programService.deleteModule(args); + return await this.programService.deleteSection(args); } // // Add a Course to the db with a course name @@ -181,7 +182,10 @@ export class ProgramResolver { // // Delete an assignment from DB @Mutation("deleteAssignment") - async deleteAssignment(@Args("module") args: string, @Args("id") id: string) { + async deleteAssignment( + @Args("section") args: string, + @Args("id") id: string + ) { return await this.programService.deleteAssignment(args, id); } @@ -194,35 +198,39 @@ export class ProgramResolver { return await this.programService.updateAssignment(id, args); } - //Adds objective to the Module + //Adds objective to the section @Mutation("addObjectives") async addObjectives(@Args("id") id: string, @Args("input") input: string[]) { return await this.programService.addObjectives(id, input); } - /// Add module feedback - @Mutation("addModuleFeedback") - async addModuleFeedback( - @Args("moduleId") moduleId: string, + /// Add section feedback + @Mutation("addSectionFeedback") + async addSectionFeedback( + @Args("sectionId") sectionId: string, @Args("userId") userId: string, - @Args("input") data: Prisma.ModuleFeedbackCreateInput + @Args("input") data: Prisma.SectionFeedbackCreateInput ) { - return await this.programService.addModuleFeedback(moduleId, userId, data); + return await this.programService.addSectionFeedback( + sectionId, + userId, + data + ); } - /// Update a modulefeedback - @Mutation("updateModuleFeedback") - async updateModuleFeedback( + /// Update a sectionfeedback + @Mutation("updateSectionFeedback") + async updateSectionFeedback( @Args("id") id: string, - @Args("input") data: ModuleFeedbackUpdate + @Args("input") data: SectionFeedbackUpdate ) { - return await this.programService.updateModuleFeedback(id, data); + return await this.programService.updateSectionFeedback(id, data); } - /// Delete a ModuleFeedback - @Mutation("deleteModuleFeedback") - async deleteModuleFeedback(@Args("id") id: string) { - return await this.programService.deleteModuleFeedback(id); + /// Delete a SectionFeedback + @Mutation("deleteSectionFeedback") + async deleteSectionFeedback(@Args("id") id: string) { + return await this.programService.deleteSectionFeedback(id); } @Mutation("addAssignmentResult") @@ -243,38 +251,38 @@ export class ProgramResolver { return await this.programService.deleteAssignmentResult(id); } - @Mutation("addModuleEnrollment") - async addModuleEnrollment(@Args("input") input: ModuleEnrollmentInput) { - return await this.programService.addModuleEnrollment(input); + @Mutation("addSectionEnrollment") + async addSectionEnrollment(@Args("input") input: SectionEnrollmentInput) { + return await this.programService.addSectionEnrollment(input); } - @Mutation("updateModuleEnrollment") - async updateModuleEnrollment( + @Mutation("updateSectionEnrollment") + async updateSectionEnrollment( @Args("id") id: string, - @Args("input") input: ModuleEnrollmentInput + @Args("input") input: SectionEnrollmentInput ) { - return await this.programService.updateModuleEnrollment(id, input); + return await this.programService.updateSectionEnrollment(id, input); } - @Mutation("deleteModuleEnrollment") - async deleteModuleEnrollment(@Args("id") id: string) { - return await this.programService.deleteModuleEnrollment(id); + @Mutation("deleteSectionEnrollment") + async deleteSectionEnrollment(@Args("id") id: string) { + return await this.programService.deleteSectionEnrollment(id); } - @Mutation("pairCourseModule") - async pairCourseModule( + @Mutation("pairCourseSection") + async pairCourseSection( @Args("courseId") courseId: string, - @Args("moduleId") moduleId: string + @Args("sectionId") sectionId: string ) { - return await this.programService.pairCourseModule(courseId, moduleId); + return await this.programService.pairCourseSection(courseId, sectionId); } - @Mutation("unpairCourseModule") - async unpairCourseModule( + @Mutation("unpairCourseSection") + async unpairCourseSection( @Args("courseId") courseId: string, - @Args("moduleId") moduleId: string + @Args("sectionId") sectionId: string ) { - return await this.programService.unpairCourseModule(courseId, moduleId); + return await this.programService.unpairCourseSection(courseId, sectionId); } @Mutation("createCollection") diff --git a/src/program/program.service.ts b/src/program/program.service.ts index 1204c882..b2ecc450 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -1,12 +1,12 @@ import { AssignmentInput, CourseInput, - ModuleFeedbackUpdate, + SectionFeedbackUpdate, NewAssignment, - UpdateModule, + UpdateSection, NewAssignmentResult, - ModuleEnrollmentInput, - ModuleFields, + SectionEnrollmentInput, + SectionFields, CourseFields, AssignmentFields, ModFeedbackFields, @@ -14,12 +14,12 @@ import { ModEnrollmentFields, LessonFields, Course, - ModuleFeedback, + SectionFeedback, CreateCollectionArgs, LessonInput, CreateContentArgs, ContentFields, - NewModule, + NewSection, CollectionFields } from "@/types/graphql"; import { Injectable } from "@nestjs/common"; @@ -35,7 +35,7 @@ export class ProgramService { }); private assignmentInclude = Prisma.validator()({ - module: true, + section: true, assignmentResults: { include: { student: true, @@ -46,18 +46,18 @@ export class ProgramService { }); private courseInclude = Prisma.validator()({ - module: { + sections: { include: { assignments: true, feedback: { include: { student: true, - module: false + section: false } }, members: { include: { - module: false, + section: false, plan: true } } @@ -65,7 +65,7 @@ export class ProgramService { } }); - public moduleInclude = Prisma.validator()({ + public sectionInclude = Prisma.validator()({ members: { include: { plan: { @@ -100,8 +100,8 @@ export class ProgramService { student: true } }, - parentModules: true, - subModules: true, + parentSections: true, + subSections: true, collections: { include: { lessons: { @@ -114,10 +114,10 @@ export class ProgramService { course: true }); - private moduleFeedbackInclude = - Prisma.validator()({ + private sectionFeedbackInclude = + Prisma.validator()({ student: true, - module: true + section: true }); private assignmentResultInclude = @@ -130,21 +130,21 @@ export class ProgramService { gradedBy: true, assignment: { include: { - module: true + section: true } } }); - public moduleEnrollmentInclude = - Prisma.validator()({ + public sectionEnrollmentInclude = + Prisma.validator()({ plan: { include: { student: true } }, - module: { + section: { include: { - parentModules: true, + parentSections: true, members: { include: { plan: { @@ -166,7 +166,7 @@ export class ProgramService { }, collection: { include: { - module: true + section: true } } } @@ -179,7 +179,7 @@ export class ProgramService { }); private collectionInclude = Prisma.validator()({ - module: true, + section: true, lessons: { include: { content: true @@ -191,7 +191,7 @@ export class ProgramService { content: true, collection: { include: { - module: { + section: { include: { collections: { include: { @@ -204,11 +204,11 @@ export class ProgramService { } }); - async module(params: ModuleFields) { + async section(params: SectionFields) { const { id, - moduleNumber, - moduleName, + sectionNumber, + sectionName, description, intro, numSlides, @@ -218,15 +218,15 @@ export class ProgramService { assignments, members, feedback, - parentModules, + parentSections, objectives, - subModules + subSections } = params; const payload = { ...(id && { id }), - ...(moduleNumber && { moduleNumber }), - ...(moduleName && { moduleName }), + ...(sectionNumber && { sectionNumber }), + ...(sectionName && { sectionName }), ...(description && { description }), ...(intro && { intro }), ...(numSlides && { numSlides }), @@ -234,25 +234,25 @@ export class ProgramService { ...(updatedAt && { updatedAt }) }; - // use the Prisma.ModuleWhereInput type and remove the AND field. Then create a union type with the AND field added back in as an array of Prisma.ModuleWhereInput - const where: Omit & { - AND: Array; + // use the Prisma.sectionWhereInput type and remove the AND field. Then create a union type with the AND field added back in as an array of Prisma.sectionWhereInput + const where: Omit & { + AND: Array; } = { AND: [] }; - if (parentModules) { + if (parentSections) { where.AND.push({ - parentModuleIDs: { - hasEvery: parentModules + parentSectionIDs: { + hasEvery: parentSections } }); } - if (subModules) { + if (subSections) { where.AND.push({ - subModuleIDs: { - hasEvery: subModules + subSectionIDs: { + hasEvery: subSections } }); } @@ -276,13 +276,13 @@ export class ProgramService { some: { id: assignments } - } as Prisma.ModuleWhereInput["assignments"]; + } as Prisma.SectionWhereInput["assignments"]; } if (keywords) { payload["keywords"] = { hasEvery: keywords - } as Prisma.ModuleWhereInput["keywords"]; + } as Prisma.SectionWhereInput["keywords"]; } if (feedback) { @@ -290,36 +290,36 @@ export class ProgramService { some: { id: feedback } - } as Prisma.ModuleWhereInput["feedback"]; + } as Prisma.SectionWhereInput["feedback"]; } if (objectives) { payload["objectives"] = { hasSome: objectives - } as Prisma.ModuleWhereInput["objectives"]; + } as Prisma.SectionWhereInput["objectives"]; } - return await this.prisma.module.findMany({ + return await this.prisma.section.findMany({ where: { ...where, ...payload }, - include: this.moduleInclude + include: this.sectionInclude }); } async course(params: CourseFields) { - const { id, name, module } = params; + const { id, name, section } = params; const payload = { ...(id && { id }), ...(name && { name }) }; - if (module) { - payload["module"] = { + if (section) { + payload["section"] = { some: { - id: module + id: section } }; } @@ -343,7 +343,7 @@ export class ProgramService { contentURL, contentType, acceptedTypes, - module, + section, assignmentResult } = params; @@ -357,7 +357,7 @@ export class ProgramService { ...(acceptedTypes && { acceptedTypes }) }; - payload["moduleId"] = module ? module : undefined; + payload["sectionId"] = section ? section : undefined; payload["assignmentResults"] = assignmentResult ? { some: { id: assignmentResult } } : undefined; @@ -372,8 +372,8 @@ export class ProgramService { }); } - async moduleFeedback(params: ModFeedbackFields) { - const { id, feedback, rating, student, module } = params; + async sectionFeedback(params: ModFeedbackFields) { + const { id, feedback, rating, student, section } = params; const payload = { ...(id && { id }), @@ -382,15 +382,15 @@ export class ProgramService { }; payload["studentId"] = student ? student : undefined; - payload["moduleId"] = module ? module : undefined; + payload["sectionId"] = section ? section : undefined; - const where = Prisma.validator()({ + const where = Prisma.validator()({ ...payload }); - return this.prisma.moduleFeedback.findMany({ + return this.prisma.sectionFeedback.findMany({ where, - include: this.moduleFeedbackInclude + include: this.sectionFeedbackInclude }); } @@ -430,8 +430,8 @@ export class ProgramService { }); } - async moduleEnrollment(params: ModEnrollmentFields) { - const { id, enrolledAt, role, module, plan } = params; + async sectionEnrollment(params: ModEnrollmentFields) { + const { id, enrolledAt, role, section, plan } = params; const payload = { ...(id && { id }), @@ -439,20 +439,20 @@ export class ProgramService { ...(role && { role }) }; - payload["moduleId"] = module - ? module - : (undefined as Prisma.ModuleEnrollmentWhereInput["moduleId"]); + payload["sectionId"] = section + ? section + : (undefined as Prisma.SectionEnrollmentWhereInput["sectionId"]); payload["planID"] = plan ? plan - : (undefined as Prisma.ModuleEnrollmentWhereInput["planID"]); + : (undefined as Prisma.SectionEnrollmentWhereInput["planID"]); - const where = Prisma.validator()({ + const where = Prisma.validator()({ ...payload }); - return this.prisma.moduleEnrollment.findMany({ + return this.prisma.sectionEnrollment.findMany({ where, - include: this.moduleEnrollmentInclude + include: this.sectionEnrollmentInclude }); } @@ -463,7 +463,7 @@ export class ProgramService { }); } - const { id, name, lessons, moduleID, positionIndex } = params; + const { id, name, lessons, sectionID, positionIndex } = params; const where = Prisma.validator()({ ...(id && { id }), @@ -472,7 +472,7 @@ export class ProgramService { contains: name } }), - ...(moduleID && { moduleID }), + ...(sectionID && { sectionID }), ...(positionIndex && { position: { equals: positionIndex @@ -540,14 +540,14 @@ export class ProgramService { name, lessons, positionIndex, - moduleID + sectionID }: CreateCollectionArgs) { const create = Prisma.validator()({ name, position: positionIndex, - module: { + section: { connect: { - id: moduleID + id: sectionID } }, lessons: { @@ -576,34 +576,34 @@ export class ProgramService { //Mutations - /// Create a new module - async addModule(data: NewModule) { - const countArgs = Prisma.validator()({ + /// Create a new section + async addSection(data: NewSection) { + const countArgs = Prisma.validator()({ where: { - moduleNumber: data.moduleNumber + sectionNumber: data.sectionNumber } }); //find out if there is a duplicate user - const count = await this.prisma.module.count(countArgs); + const count = await this.prisma.section.count(countArgs); if (count !== 0) { - throw new Error("Module already exists."); + throw new Error("Section already exists."); } else { - const create = Prisma.validator()({ + const create = Prisma.validator()({ ...data }); - return this.prisma.module.create({ + return this.prisma.section.create({ data: create, - include: this.moduleInclude + include: this.sectionInclude }); } } - /// Modify a modules data or add an assignment here - async updateModule(data: UpdateModule) { + /// Modify a sections data or add an assignment here + async updateSection(data: UpdateSection) { const { - moduleNumber, - moduleName, + sectionNumber, + sectionName, description, duration, numSlides, @@ -611,13 +611,13 @@ export class ProgramService { objectives } = data; - const args = Prisma.validator()({ + const args = Prisma.validator()({ where: { id: data.id }, data: { - ...(moduleNumber && { moduleNumber }), - ...(moduleName && { moduleName }), + ...(sectionNumber && { sectionNumber }), + ...(sectionName && { sectionName }), ...(description && { description }), ...(duration && { duration }), ...(numSlides && { numSlides }), @@ -626,29 +626,29 @@ export class ProgramService { } }); - return this.prisma.module.update({ + return this.prisma.section.update({ where: args.where, data: args.data, - include: this.moduleInclude + include: this.sectionInclude }); } - /// Remove a module and all of its assignments - async deleteModule(id: string) { + /// Remove a section and all of its assignments + async deleteSection(id: string) { await this.prisma.assignment.deleteMany({ where: { - moduleId: id + sectionId: id } }); - return this.prisma.module.delete({ + return this.prisma.section.delete({ where: { id: id } }); } - /// Create a course and assign an initial module to that course + /// Create a course and assign an initial section to that course async addCourse(data: CourseInput) { return this.prisma.course.create({ data: { @@ -681,7 +681,7 @@ export class ProgramService { id }, data: { - module: { + sections: { deleteMany: {} } }, @@ -695,12 +695,12 @@ export class ProgramService { }); } - /// Remove an assignment from a module - async deleteAssignment(module: string, id: string) { - // Do something here to disconnect an assignment from a module - return this.prisma.module.update({ + /// Remove an assignment from a section + async deleteAssignment(section: string, id: string) { + // Do something here to disconnect an assignment from a section + return this.prisma.section.update({ where: { - id: module + id: section }, data: { assignments: { @@ -715,9 +715,9 @@ export class ProgramService { return this.prisma.assignment.create({ data: { name: input.name, - module: { + section: { connect: { - id: input.module + id: input.section } }, dueAt: input.dueAt, @@ -750,15 +750,15 @@ export class ProgramService { }); } - /// Create a module feedback and link it to the user and module - async addModuleFeedback( - moduleId: string, + /// Create a section feedback and link it to the user and section + async addSectionFeedback( + sectionId: string, userId: string, - input: Prisma.ModuleFeedbackCreateInput + input: Prisma.SectionFeedbackCreateInput ) { - return this.prisma.module.update({ + return this.prisma.section.update({ where: { - id: moduleId + id: sectionId }, data: { feedback: { @@ -773,15 +773,15 @@ export class ProgramService { } } }, - include: this.moduleInclude + include: this.sectionInclude }); } - /// Update a module feedback - async updateModuleFeedback(id: string, input: ModuleFeedbackUpdate) { + /// Update a section feedback + async updateSectionFeedback(id: string, input: SectionFeedbackUpdate) { const { feedback, rating } = input; - const update = Prisma.validator()({ + const update = Prisma.validator()({ where: { id }, @@ -791,15 +791,15 @@ export class ProgramService { } }); - return this.prisma.moduleFeedback.update({ + return this.prisma.sectionFeedback.update({ ...update, - include: this.moduleFeedbackInclude + include: this.sectionFeedbackInclude }); } - /// Delete a ModuleFeedback - async deleteModuleFeedback(id: string): Promise { - return this.prisma.moduleFeedback.delete({ + /// Delete a sectionFeedback + async deleteSectionFeedback(id: string): Promise { + return this.prisma.sectionFeedback.delete({ where: { id } @@ -843,24 +843,24 @@ export class ProgramService { }); } - /// Create a ModuleEnrollment Document - async addModuleEnrollment(input: ModuleEnrollmentInput) { - const { plan, module, role, status } = input; + /// Create a SectionEnrollment Document + async addSectionEnrollment(input: SectionEnrollmentInput) { + const { plan, section, role, status } = input; - const count = await this.prisma.moduleEnrollment.count({ + const count = await this.prisma.sectionEnrollment.count({ where: { planID: plan, - moduleId: module + sectionId: section } }); if (count !== 0) { - throw new Error("This Module Enrollment already exists"); + throw new Error("This Section Enrollment already exists"); } else { - const create = Prisma.validator()({ - module: { + const create = Prisma.validator()({ + section: { connect: { - id: module + id: section } }, plan: { @@ -872,46 +872,46 @@ export class ProgramService { status }); - return this.prisma.moduleEnrollment.create({ + return this.prisma.sectionEnrollment.create({ data: create, - include: this.moduleEnrollmentInclude + include: this.sectionEnrollmentInclude }); } } - /// Update a ModuleEnrollment - async updateModuleEnrollment(id: string, input: ModuleEnrollmentInput) { - const args = Prisma.validator()({ + /// Update a SectionEnrollment + async updateSectionEnrollment(id: string, input: SectionEnrollmentInput) { + const args = Prisma.validator()({ where: { id }, data: { - moduleId: input.module, + sectionId: input.section, planID: input.plan, role: input.role } }); - return this.prisma.moduleEnrollment.update({ + return this.prisma.sectionEnrollment.update({ where: args.where, data: args.data, - include: this.moduleEnrollmentInclude + include: this.sectionEnrollmentInclude }); } - async deleteModuleEnrollment(id: string) { - return this.prisma.moduleEnrollment.delete({ + async deleteSectionEnrollment(id: string) { + return this.prisma.sectionEnrollment.delete({ where: { id } }); } - // Link a course and a module - async pairCourseModule(courseId: string, moduleId: string) { - const count = await this.prisma.module.count({ + // Link a course and a section + async pairCourseSection(courseId: string, sectionId: string) { + const count = await this.prisma.section.count({ where: { - id: moduleId, + id: sectionId, course: { some: { id: courseId @@ -921,7 +921,7 @@ export class ProgramService { }); if (count != 0) { - throw new Error("Module and Course are already Linked."); + throw new Error("Section and Course are already Linked."); } await this.prisma.course.update({ @@ -929,46 +929,46 @@ export class ProgramService { id: courseId }, data: { - moduleIDs: { - push: moduleId + sectionIDs: { + push: sectionId } } }); - return this.prisma.module.update({ + return this.prisma.section.update({ where: { - id: moduleId + id: sectionId }, data: { courseIDs: { push: courseId } }, - include: this.moduleInclude + include: this.sectionInclude }); } - async unpairCourseModule(courseId: string, moduleId: string) { + async unpairCourseSection(courseId: string, sectionId: string) { const courseIdToRemove = await this.prisma.course.findUnique({ where: { id: courseId } }); - const newModuleSet = + const newSectionSet = courseIdToRemove !== null - ? courseIdToRemove.moduleIDs.filter((module) => module !== moduleId) + ? courseIdToRemove.sectionIDs.filter((section) => section !== sectionId) : null; - const moduleIdToRemove = await this.prisma.module.findUnique({ + const sectionIdToRemove = await this.prisma.section.findUnique({ where: { - id: moduleId + id: sectionId } }); const newCourseSet = - moduleIdToRemove !== null - ? moduleIdToRemove.courseIDs.filter((course) => course !== courseId) + sectionIdToRemove !== null + ? sectionIdToRemove.courseIDs.filter((course) => course !== courseId) : null; await this.prisma.course.update({ @@ -976,13 +976,13 @@ export class ProgramService { id: courseId }, data: { - moduleIDs: newModuleSet !== null ? newModuleSet : undefined + sectionIDs: newSectionSet !== null ? newSectionSet : undefined } }); - return this.prisma.module.update({ + return this.prisma.section.update({ where: { - id: moduleId + id: sectionId }, data: { courseIDs: newCourseSet !== null ? newCourseSet : undefined @@ -1141,19 +1141,19 @@ export class ProgramService { } async addObjectives(id: string, input: string[]) { - const module = await this.prisma.module.findUnique({ + const section = await this.prisma.section.findUnique({ where: { id } }); - if (module === null) { - throw new Error("Module not found"); + if (section === null) { + throw new Error("Section not found"); } - const objectives = [...module.objectives, input] as Array; + const objectives = [...section.objectives, input] as Array; - return this.prisma.module.update({ + return this.prisma.section.update({ where: { id: id }, diff --git a/src/program/schema.graphql b/src/program/schema.graphql index 28e64099..755fa288 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -28,14 +28,14 @@ enum FileType { TXT } -type ModuleEnrollment { +type SectionEnrollment { """ - id of the ModuleEnrollment + id of the SectionEnrollment """ id: ID! """ - TimeStamp at which the module is enrolledAt + TimeStamp at which the Section is enrolledAt """ enrolledAt: Date! @@ -50,9 +50,9 @@ type ModuleEnrollment { status: EnrollmentStatus! """ - Current Module + Current Section """ - module: Module! + section: Section! """ Plan of study Chosen @@ -157,9 +157,9 @@ type Assignment { acceptedTypes: FileType """ - Assignment belonging to the module + Assignment belonging to the Section """ - module: Module! + section: Section! """ Assignment Result @@ -167,14 +167,14 @@ type Assignment { assignmentResults: [AssignmentResult] } -type ModuleFeedback { +type SectionFeedback { """ - id of ModuleFeedback + id of SectionFeedback """ id: ID! """ - Feedback fo the Module + Feedback fo the Section """ feedback: String! @@ -184,14 +184,14 @@ type ModuleFeedback { rating: Int! """ - Module feedback given by the student + Section feedback given by the student """ student: User """ - Module in which module feedback is given + Section in which Section feedback is given """ - module: Module + section: Section } type Course { @@ -204,9 +204,9 @@ type Course { """ name: String! """ - modules in the course + Sections in the course """ - moduleIDs: [ID] + sectionIDs: [ID] """ Boolean value to check if the course is required/core or an elective """ @@ -217,88 +217,88 @@ type Course { carnegieHours: Int! } -type Module { +type Section { """ - Module Id + Section Id """ id: ID! """ - Number of the Module + Number of the Section """ - moduleNumber: Int! + sectionNumber: Int! """ - Name of hte Module + Name of hte Section """ - moduleName: String! + sectionName: String! """ - Description of the Module + Description of the Section """ description: String! """ - Duration of the Module + Duration of the Section """ duration: Float! """ - Introduction of the Module + Introduction of the Section """ intro: String! """ - No of slides in the Module + No of slides in the Section """ numSlides: Int! """ - Keywords in the Modules + Keywords in the Sections """ keywords: [String!]! """ - objectives in the Modules + objectives in the Sections """ objectives: [String!]! """ - Date and Time of the Module Created at + Date and Time of the Section Created at """ createdAt: Date! """ - Date and Time of the Module Updated at + Date and Time of the Section Updated at """ updatedAt: Date! """ - Assignement in the module + Assignement in the Section """ assignments: [Assignment!]! """ - Memebers enrolled in the Module + Memebers enrolled in the Section """ - members: [ModuleEnrollment!]! + members: [SectionEnrollment!]! """ - Feedback of the Module + Feedback of the Section """ - feedback: [ModuleFeedback!]! + feedback: [SectionFeedback!]! """ - Parent Modules of the Module + Parent Sections of the Section """ - parentModules: [Module!]! + parentSections: [Section!]! """ - Child modules in the Module + Child Sections in the Section """ - subModules: [Module!]! + subSections: [Section!]! """ - A list of collections that have this module's materials + A list of collections that have this Section's materials """ collections: [Collection!]! @@ -335,15 +335,15 @@ type Collection { lessons: [Lesson] """ - The module that this collection belongs to + The Section that this collection belongs to """ - module: Module! + section: Section! """ - The module's unique ID that this collection belongs to + The Section's unique ID that this collection belongs to """ - moduleID: ID! + sectionID: ID! """ - The position index of the collection in a module + The position index of the collection in a Section """ position: Int } @@ -383,11 +383,11 @@ type Lesson { """ lessonProgress: [LessonProgress] """ - A list of learning objectives being covered in this module + A list of learning objectives being covered in this Section """ objectives: [String!]! """ - The number of carnige hours granted for completion of this module + The number of carnige hours granted for completion of this Section """ hours: Float! } @@ -429,11 +429,11 @@ type Error { type Query { """ - Get a list of modules given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. - Parameters can be any of the fields that exist in a module - An additional parameter: memberRole, can be provided to specify that module members should only be selected if they have said role. + Get a list of Sections given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. + Parameters can be any of the fields that exist in a Section + An additional parameter: memberRole, can be provided to specify that Section members should only be selected if they have said role. """ - module(input: ModuleFields!, memberRole: UserRole): [Module!] + section(input: SectionFields!, memberRole: UserRole): [Section!] """ Get a list of courses given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. @@ -448,10 +448,10 @@ type Query { assignment(input: AssignmentFields!): [Assignment!] """ - Get a list of moduleFeedbacks given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. - Parameters can be any of the fields that exist in a moduleFeedback + Get a list of SectionFeedbacks given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. + Parameters can be any of the fields that exist in a SectionFeedback """ - moduleFeedback(input: ModFeedbackFields!): [ModuleFeedback!] + sectionFeedback(input: ModFeedbackFields!): [SectionFeedback!] """ Get a list of assignmentResults given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. @@ -460,14 +460,14 @@ type Query { assignmentResult(input: AssignmentResFields!): [AssignmentResult!] """ - Get a list of moduleEnrollments given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. - Parameters can be any of the fields that exist in a moduleEnrollment + Get a list of SectionEnrollments given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. + Parameters can be any of the fields that exist in a SectionEnrollment """ - moduleEnrollment(input: ModEnrollmentFields!): [ModuleEnrollment!] + sectionEnrollment(input: ModEnrollmentFields!): [SectionEnrollment!] """ Get a list of lessons given the plan ID and enrollment ID """ - lessonsByModuleEnrollment(planID: ID!, moduleID: ID!) : [Lesson!] + lessonsBySectionEnrollment(planID: ID!, SectionID: ID!) : [Lesson!] """ Retrieve a specific collection based on document ID """ @@ -533,7 +533,7 @@ input ContentFields { input CreateCollectionArgs { name: String! - moduleID: ID! + sectionID: ID! lessons: [ID!] positionIndex: Int! } @@ -541,15 +541,15 @@ input CreateCollectionArgs { input CollectionFields { id: ID name: String - moduleID: ID + sectionID: ID lessons: [ID] positionIndex: Int } -input ModuleFields { +input SectionFields { id: ID - moduleNumber: Int - moduleName: String + sectionNumber: Int + sectionName: String description: String duration: Float intro: String @@ -561,14 +561,14 @@ input ModuleFields { assignments: ID members: [ID!] feedback: ID - parentModules: [ID!] - subModules: [ID!] + parentSections: [ID!] + subSections: [ID!] } input CourseFields { id: ID name: String - module: ID + section: ID required: Boolean carnegieHours: Int } @@ -582,7 +582,7 @@ input AssignmentFields { contentType: String acceptedTypes: FileType - module: ID + section: ID assignmentResult: ID } @@ -592,7 +592,7 @@ input ModFeedbackFields { rating: Int student: ID - module: ID + section: ID } input AssignmentResFields { @@ -613,76 +613,76 @@ input ModEnrollmentFields { enrolledAt: Date role: UserRole - module: ID + section: ID plan: ID } -input NewModule { +input NewSection { """ - Number of the Module + Number of the Section """ - moduleNumber: Int! + sectionNumber: Int! """ - Name of the Module + Name of the Section """ - moduleName: String! + sectionName: String! """ - description of the Module + description of the section """ description: String! """ - Duration of the NewModule + Duration of the NewSection """ duration: Float! """ - Introduction of the NewModule + Introduction of the NewSection """ intro: String! """ - Number of Slides in NewModule + Number of Slides in NewSection """ numSlides: Int! """ - keywords of the NewModule + keywords of the NewSection """ keywords: [String!]! } -input UpdateModule { +input UpdateSection { """ - UpdateModule id + UpdateSection id """ id: ID! """ - Module Name of the UpdateModule + Section Name of the UpdateSection """ - moduleName: String + sectionName: String """ - Module NUmber of the UpdateModule + Section NUmber of the UpdateSection """ - moduleNumber: Int + sectionNumber: Int """ - Introduction of the UpdateModule + Introduction of the UpdateSection """ intro: String """ - Decription of the UpdateModule + Decription of the UpdateSection """ description: String """ - Duration of the UpdateModule + Duration of the UpdateSection """ duration: Float """ - Number of slides in Updatemodule + Number of slides in UpdateSection """ numSlides: Int """ - Keywords in UpdateModule + Keywords in UpdateSection """ keywords: [String!] """ - The objective to be added to the module + The objective to be added to the Section """ objectives: [String!] } @@ -697,9 +697,9 @@ input NewAssignment { """ dueAt: Date! """ - Module to which the NewAssignement Belongs to + Section to which the NewAssignement Belongs to """ - module: ID! + section: ID! """ File type that the assignment is submitted in """ @@ -724,9 +724,9 @@ input AssignmentInput { """ dueAt: Date """ - Updated module linkage + Updated section linkage """ - module: ID + section: ID } input CourseInput { @@ -735,9 +735,9 @@ input CourseInput { """ name: String! """ - Additional module to be related to the course + Additional section to be related to the course """ - module: ID! + section: ID! """ The boolean attribute to decide weather the course is part of core courses or electives """ @@ -748,24 +748,24 @@ input CourseInput { carnegieHours: Int! } -input ModuleFeedbackInput { +input SectionFeedbackInput { """ - New Feedback of the Module + New Feedback of the Section """ feedback: String! """ - New Rating of the Module + New Rating of the Section """ rating: Int! } -input ModuleFeedbackUpdate { +input SectionFeedbackUpdate { """ - Updated feedback of Module + Updated feedback of Section """ feedback: String """ - Updated feedback of Module + Updated feedback of Section """ rating: Int } @@ -797,17 +797,17 @@ input NewAssignmentResult { fileType: String! } -input ModuleEnrollmentInput { +input SectionEnrollmentInput { """ - Relating Module to ModuleEnrollment + Relating Section to SectionEnrollment """ - module: ID! + section: ID! """ - Relating Plan to Module enrollment + Relating Plan to Section enrollment """ plan: ID! """ - Relating role to ModuleEnrollment + Relating role to SectionEnrollment """ role: UserRole! """ @@ -835,11 +835,11 @@ input LessonInput { """ position: Int """ - The list of learning objectives for this module + The list of learning objectives for this Section """ objectives: [String!] """ - The number of carnige hours granted for completing this module + The number of carnige hours granted for completing this Section """ hours: Float! } @@ -870,28 +870,28 @@ input LessonFields { """ position: Int """ - Learning objectives that are taught in this module + Learning objectives that are taught in this Section """ objectives: [String!] """ - The number of carnige hours granted for completing this module + The number of carnige hours granted for completing this Section """ hours: Float } type Mutation { """ - Deletes the entire Module + Deletes the entire Section """ - deleteModule(id: ID!): Module + deleteSection(id: ID!): Section """ - Adds an entire module + Adds an entire Section """ - addModule(input: NewModule): Module! + addSection(input: NewSection): Section! """ - updates a Module + updates a Section """ - updateModule(input: UpdateModule): Module + updateSection(input: UpdateSection): Section """ deletes all Courses referring id @@ -907,44 +907,44 @@ type Mutation { updateCourse(id: ID!, input: CourseInput): Course """ - adds assignement in module + adds assignement in Section """ addAssignment(input: NewAssignment): Assignment! """ - Adds Objectives to a Module while retaining the old ones + Adds Objectives to a Section while retaining the old ones """ - addObjectives(id: ID!, input: [String!]): Module + addObjectives(id: ID!, input: [String!]): Section """ - Deletes assignments in Module + Deletes assignments in Section """ - deleteAssignment(module: ID!, id: ID!): Module + deleteAssignment(section: ID!, id: ID!): Section """ Updates Assignements """ updateAssignment(id: ID!, input: AssignmentInput): Assignment - addModuleFeedback( + addSectionFeedback( """ - Module Feedback is given relating to Moduleid + Section Feedback is given relating to Sectionid """ - moduleId: ID! + sectionId: ID! """ - Module Feedback is given relating to Userid + section Feedback is given relating to Userid """ userId: ID! """ - Module Feedback is given relating to Modulefeedbackinput + section Feedback is given relating to Sectionfeedbackinput """ - input: ModuleFeedbackInput - ): Module + input: SectionFeedbackInput + ): Section """ - Module feedback is updated + Section feedback is updated """ - updateModuleFeedback(id: ID!, input: ModuleFeedbackUpdate): ModuleFeedback + updateSectionFeedback(id: ID!, input: SectionFeedbackUpdate): SectionFeedback """ - Module feedback is deleted + Section feedback is deleted """ - deleteModuleFeedback(id: ID!): ModuleFeedback + deleteSectionFeedback(id: ID!): SectionFeedback """ Assignment Result is added """ @@ -958,31 +958,31 @@ type Mutation { """ deleteAssignmentResult(id: ID!): AssignmentResult """ - Course Module is paired Relating Course ID and module Id + Course Section is paired Relating Course ID and Section Id """ - addModuleEnrollment(input: ModuleEnrollmentInput): ModuleEnrollment! - updateModuleEnrollment( + addSectionEnrollment(input: SectionEnrollmentInput): SectionEnrollment! + updateSectionEnrollment( """ - Module enrollment is updated relating to id + Section enrollment is updated relating to id """ id: ID! """ - Module enrollment input + Section enrollment input """ - input: ModuleEnrollmentInput - ): ModuleEnrollment + input: SectionEnrollmentInput + ): SectionEnrollment """ - Delete ModuleEnrollment + Delete SectionEnrollment """ - deleteModuleEnrollment(id: ID!): ModuleEnrollment + deleteSectionEnrollment(id: ID!): SectionEnrollment - pairCourseModule(courseId: ID!, moduleId: ID!): Module! + pairCourseSection(courseId: ID!, sectionId: ID!): Section! """ - Course Module is Unpaired Relating to Course Id and mobile Id + Course Section is Unpaired Relating to Course Id and mobile Id """ - unpairCourseModule(courseId: ID!, moduleId: ID!): Module + unpairCourseSection(courseId: ID!, sectionId: ID!): Section """ Create a new collection diff --git a/src/progress/progress.resolver.ts b/src/progress/progress.resolver.ts index 929f0617..ba1cea9e 100644 --- a/src/progress/progress.resolver.ts +++ b/src/progress/progress.resolver.ts @@ -33,7 +33,7 @@ export class ProgressResolver { @Args("input") input: Prisma.ProgressUncheckedCreateInput, @Args("enrollmentID") enrollmentID: string ) { - const enrollment = await this.program.moduleEnrollment({ + const enrollment = await this.program.sectionEnrollment({ id: enrollmentID }); if (!enrollment || enrollment.length === 0) @@ -46,25 +46,25 @@ export class ProgressResolver { else return response; } - @Mutation("waiveModule") - async waiveModule(@Args("args") args: ProgressWaiveArgs) { - // if moduleID and planID are provided, we can use them to find the enrollment or create a new one - if (args.moduleID && args.planID) { - const { moduleID, planID } = args; + @Mutation("waiveSection") + async waiveSection(@Args("args") args: ProgressWaiveArgs) { + // if SectionID and planID are provided, we can use them to find the enrollment or create a new one + if (args.sectionID && args.planID) { + const { sectionID, planID } = args; // check if plan exists const plan = await this.plan.planByParams({ id: planID }); if (plan instanceof Error) return new Error(plan.message); - // check if module exists - const module = await this.program.module( - { id: moduleID }, + // check if section exists + const section = await this.program.section( + { id: sectionID }, UserRole.STUDENT ); - if (module instanceof Error) return new Error(module.message); + if (section instanceof Error) return new Error(section.message); else { - // both module and plan exist, so we can create a new enrollment - const enrollment = await this.program.addModuleEnrollment({ - module: moduleID, + // both section and plan exist, so we can create a new enrollment + const enrollment = await this.program.addSectionEnrollment({ + section: sectionID, plan: planID, role: UserRole.STUDENT, status: EnrollmentStatus.ACTIVE @@ -86,15 +86,15 @@ export class ProgressResolver { // if enrollmentID is provided, we can use it to find the enrollment and create a new progress document or find the existing one else if (args.enrollmentID) { const { enrollmentID } = args; - const enrollment = await this.program.moduleEnrollment({ + const enrollment = await this.program.sectionEnrollment({ id: enrollmentID }); if (!enrollment) return new Error("Enrollment not found"); - // since moduleEnrollment returns an array, we need to get the first element as it returns a unique enrollment if id is provided + // since sectionEnrollment returns an array, we need to get the first element as it returns a unique enrollment if id is provided const progress = enrollment[0].progress ? enrollment[0].progress : null; // in case there is a population error we return an error - if (!enrollment[0].plan || !enrollment[0].module) + if (!enrollment[0].plan || !enrollment[0].section) return new Error("Enrollment not found"); if (!progress) { // progress doesn't exist, so we can create a new one @@ -106,28 +106,28 @@ export class ProgressResolver { }, enrollmentID ); - await this.program.updateModuleEnrollment(enrollmentID, { + await this.program.updateSectionEnrollment(enrollmentID, { status: EnrollmentStatus.ACTIVE, role: UserRole.STUDENT, plan: enrollment[0].plan.id, - module: enrollment[0].module.id + section: enrollment[0].section.id }); if (res instanceof Error) return new Error(res.message); else return res; } else if (progress) { // progress exists, so we can update it - const res = await this.progressService.waiveModule(enrollmentID); - await this.program.updateModuleEnrollment(enrollmentID, { + const res = await this.progressService.waiveSection(enrollmentID); + await this.program.updateSectionEnrollment(enrollmentID, { status: EnrollmentStatus.ACTIVE, role: UserRole.STUDENT, plan: enrollment[0].plan.id, - module: enrollment[0].module.id + section: enrollment[0].section.id }); if (res instanceof Error) return new Error(res.message); else return res; } } else - return new Error("No enrollment or module to be waived was specified"); + return new Error("No enrollment or section to be waived was specified"); } @Mutation("deleteProgress") diff --git a/src/progress/progress.service.ts b/src/progress/progress.service.ts index f2e31cbd..0da60064 100644 --- a/src/progress/progress.service.ts +++ b/src/progress/progress.service.ts @@ -67,7 +67,7 @@ export class ProgressService { return true; } - async waiveModule(enrollmentID: string) { + async waiveSection(enrollmentID: string) { const res = await this.prisma.progress.update({ where: { enrollmentID }, data: { diff --git a/src/progress/schema.graphql b/src/progress/schema.graphql index 2eae6aa0..36901f78 100644 --- a/src/progress/schema.graphql +++ b/src/progress/schema.graphql @@ -22,7 +22,7 @@ type Progress { """ The enrollment that this progress is associated with (one-to-one) """ - enrollment: ModuleEnrollment! + enrollment: SectionEnrollment! } input ProgressArgs { @@ -43,9 +43,9 @@ input ProgressWaiveArgs { """ enrollmentID: ID """ - The database defined ID of the module that holds the enrollment to be accredited. + The database defined ID of the section that holds the enrollment to be accredited. """ - moduleID: ID + sectionID: ID """ The database defined ID of the plan of study that holds the enrollment to be accredited. """ @@ -76,7 +76,7 @@ type LessonProgress { """ The enrollment that this progress is associated with (one-to-one) """ - enrollment: ModuleEnrollment! + enrollment: SectionEnrollment! """ The lesson that this progress is associated with (one-to-one) """ @@ -101,7 +101,7 @@ type Mutation { enrollmentID: ID! ): Progress! - waiveModule( + waiveSection( args: ProgressWaiveArgs! ): Progress! diff --git a/src/user/schema.graphql b/src/user/schema.graphql index 40a0529e..2127d80c 100644 --- a/src/user/schema.graphql +++ b/src/user/schema.graphql @@ -134,9 +134,9 @@ type User { """ tokens: [ID!] """ - feedback of the user pointing to module feedback + feedback of the user pointing to section feedback """ - feedback: [ModuleFeedback!] + feedback: [SectionFeedback!] """ assignment graded pointing to assignment result of the user """ diff --git a/src/user/user.service.ts b/src/user/user.service.ts index cfabe127..1d4c8539 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -22,14 +22,14 @@ export class UserService { feedback: true, plan: { include: { - modules: { + sections: { include: { - module: { + section: { include: { - parentModules: true, + parentSections: true, members: true, feedback: true, - subModules: true, + subSections: true, course: true, assignments: true, collections: true From 0c953c2cb1b5ce1a2c08f736aa70abc0edf0aa88 Mon Sep 17 00:00:00 2001 From: frontoge Date: Mon, 24 Apr 2023 13:38:03 -0400 Subject: [PATCH 21/31] Refactor Lessons to Modules --- gql/graphql.ts | 40 ++++++++--------- prisma/dbml/schema.dbml | 32 +++++++------- prisma/schema.prisma | 20 ++++----- src/program/program.resolver.ts | 68 ++++++++++++++-------------- src/program/program.service.ts | 77 ++++++++++++++++---------------- src/program/program.spec.ts | 54 +++++++++++------------ src/program/schema.graphql | 78 ++++++++++++++++----------------- src/progress/schema.graphql | 6 +-- src/quiz/quiz.service.ts | 16 +++---- src/quiz/quiz.spec.ts | 28 ++++++------ src/quiz/schema.graphql | 18 ++++---- utils/fakes.ts | 46 +++++++++---------- utils/tests.ts | 34 +++++++------- 13 files changed, 257 insertions(+), 260 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index 8bf97397..418eb8db 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -91,7 +91,7 @@ export interface ContentFields { export interface CreateCollectionArgs { name: string; sectionID: string; - lessons?: Nullable; + modules?: Nullable; positionIndex: number; } @@ -99,7 +99,7 @@ export interface CollectionFields { id?: Nullable; name?: Nullable; sectionID?: Nullable; - lessons?: Nullable[]>; + modules?: Nullable[]>; positionIndex?: Nullable; } @@ -240,7 +240,7 @@ export interface SectionEnrollmentInput { status: EnrollmentStatus; } -export interface LessonInput { +export interface ModuleInput { name: string; content?: Nullable; collection: string; @@ -249,7 +249,7 @@ export interface LessonInput { hours: number; } -export interface LessonFields { +export interface ModuleFields { id?: Nullable; name?: Nullable; content?: Nullable; @@ -283,7 +283,7 @@ export interface QuizFields { timeLimit?: Nullable; numQuestions?: Nullable; minScore?: Nullable; - parentLesson?: Nullable; + parentModule?: Nullable; } export interface QuizInstanceFields { @@ -323,7 +323,7 @@ export interface CreateQuiz { timeLimit?: Nullable; numQuestions: number; minScore?: Nullable; - parentLesson: string; + parentModule: string; } export interface UpdateQuiz { @@ -333,7 +333,7 @@ export interface UpdateQuiz { timeLimit?: Nullable; numQuestions?: Nullable; minScore?: Nullable; - parentLesson?: Nullable; + parentModule?: Nullable; } export interface CreateQuestion { @@ -491,9 +491,9 @@ export interface IMutation { unpairCourseSection(courseId: string, sectionId: string): Nullable
| Promise>; createCollection(data: CreateCollectionArgs): Collection | Promise; updateCollection(id: string, data: CollectionFields): Collection | Promise; - createLesson(input: LessonInput): Lesson | Promise; - updateLesson(input?: Nullable, replaceObj?: Nullable): Nullable | Promise>; - deleteLesson(id: string): Nullable | Promise>; + createModule(input: ModuleInput): Module | Promise; + updateModule(input?: Nullable, replaceObj?: Nullable): Nullable | Promise>; + deleteModule(id: string): Nullable | Promise>; createContent(input: CreateContentArgs): Content | Promise; updateContent(input: ContentFields): Nullable | Promise>; deleteContent(contentID: string): Nullable | Promise>; @@ -542,9 +542,9 @@ export interface IQuery { sectionFeedback(input: ModFeedbackFields): Nullable | Promise>; assignmentResult(input: AssignmentResFields): Nullable | Promise>; sectionEnrollment(input: ModEnrollmentFields): Nullable | Promise>; - lessonsBySectionEnrollment(planID: string, SectionID: string): Nullable | Promise>; + modulesBySectionEnrollment(planID: string, SectionID: string): Nullable | Promise>; collection(input?: Nullable): Nullable[]> | Promise[]>>; - lesson(input?: Nullable): Nullable | Promise>; + module(input?: Nullable): Nullable | Promise>; content(input?: Nullable): Nullable | Promise>; progress(args: ProgressArgs): Nullable[] | Promise[]>; quiz(args: QuizFields): Quiz[] | Promise; @@ -622,7 +622,7 @@ export interface SectionEnrollment { plan?: Nullable; inactivePlan?: Nullable; progress: Progress; - lessonProgress?: Nullable[]>; + moduleProgress?: Nullable[]>; } export interface AssignmentResult { @@ -691,13 +691,13 @@ export interface Collection { name: string; createdAt: Date; updatedAt: Date; - lessons?: Nullable[]>; + modules?: Nullable[]>; section: Section; sectionID: string; position?: Nullable; } -export interface Lesson { +export interface Module { id: string; name: string; content?: Nullable[]>; @@ -705,7 +705,7 @@ export interface Lesson { collection?: Nullable; position?: Nullable; quizzes?: Nullable; - lessonProgress?: Nullable[]>; + moduleProgress?: Nullable[]>; objectives: string[]; hours: number; } @@ -714,7 +714,7 @@ export interface Content { id: string; type: ContentType; link: string; - parent: Lesson; + parent: Module; primary: boolean; } @@ -731,14 +731,14 @@ export interface Progress { enrollment: SectionEnrollment; } -export interface LessonProgress { +export interface ModuleProgress { id: string; status: number; completed: boolean; createdAt: Date; updatedAt: Date; enrollment: SectionEnrollment; - lesson: Lesson; + module: Module; } export interface Quiz { @@ -749,7 +749,7 @@ export interface Quiz { timeLimit?: Nullable; numQuestions: number; minScore: number; - parentLesson: Lesson; + parentModule: Module; questionPool: Question[]; instances: QuizInstance[]; } diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index aa21d498..9e2fc86e 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -41,11 +41,9 @@ Table InstructorProfile { officeLocation String officeHours String[] [not null] contactPolicy String - phone String background String researchInterest String[] [not null] selectedPapersAndPublications String[] [not null] - personalWebsite String } Table PlanOfStudy { @@ -116,12 +114,12 @@ Table Collection { createdAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null] position Int [not null, default: 0] - lessons Lesson [not null] + modules Module [not null] section Section [not null] sectionID String [not null] } -Table Lesson { +Table Module { id String [pk] name String [not null] collection Collection [not null] @@ -131,7 +129,7 @@ Table Lesson { hours Float [not null, default: 0] content Content [not null] quizzes Quiz [not null] - lessonProgress LessonProgress [not null] + moduleProgress ModuleProgress [not null] } Table SectionFeedback { @@ -154,7 +152,7 @@ Table SectionEnrollment { plan PlanOfStudy planID String progress Progress - lessonProgress LessonProgress [not null] + moduleProgress ModuleProgress [not null] } Table Assignment { @@ -207,7 +205,7 @@ Table Content { id String [pk] type ContentType [not null] link String [not null] - parent Lesson [not null] + parent Module [not null] parentID String [not null] primary Boolean [not null] } @@ -246,14 +244,14 @@ Table Progress { enrollmentID String [unique, not null] } -Table LessonProgress { +Table ModuleProgress { id String [pk] createdAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null] status Float [not null, default: 0] completed Boolean [not null, default: false] - lesson Lesson [not null] - lessonID String [not null] + module Module [not null] + moduleID String [not null] enrollment SectionEnrollment [not null] enrollmentID String [unique, not null] } @@ -266,8 +264,8 @@ Table Quiz { timeLimit Int numQuestions Int [not null] minScore Float [not null, default: 0] - parentLesson Lesson [not null] - parentLessonID String [not null] + parentModule Module [not null] + parentModuleID String [not null] questionPool Question [not null] instances QuizInstance [not null] } @@ -368,7 +366,7 @@ Ref: Section.courseIDs > Course.id Ref: Collection.sectionID > Section.id [delete: Cascade] -Ref: Lesson.collectionID > Collection.id [delete: Cascade] +Ref: Module.collectionID > Collection.id [delete: Cascade] Ref: SectionFeedback.studentId > User.id @@ -394,7 +392,7 @@ Ref: Thread.parentThreadID - Thread.id [delete: No Action] Ref: Thread.upvoteUserIDs > User.id -Ref: Content.parentID > Lesson.id [delete: Cascade] +Ref: Content.parentID > Module.id [delete: Cascade] Ref: DirectMessage.authorID > User.id @@ -406,11 +404,11 @@ Ref: Group.memberIDs > User.id Ref: Progress.enrollmentID - SectionEnrollment.id [delete: Cascade] -Ref: LessonProgress.lessonID > Lesson.id [delete: Cascade] +Ref: ModuleProgress.moduleID > Module.id [delete: Cascade] -Ref: LessonProgress.enrollmentID > SectionEnrollment.id [delete: Cascade] +Ref: ModuleProgress.enrollmentID > SectionEnrollment.id [delete: Cascade] -Ref: Quiz.parentLessonID > Lesson.id [delete: Cascade] +Ref: Quiz.parentModuleID > Module.id [delete: Cascade] Ref: QuizInstance.quizID > Quiz.id [delete: Cascade] diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b5a3831d..240a6ad4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -138,12 +138,12 @@ model Collection { position Int @default(0) // Relation fields - lessons Lesson[] + modules Module[] section Section @relation(name: "collections", fields: [sectionID], references: [id], onDelete: Cascade) sectionID String @db.ObjectId } -model Lesson { +model Module { id String @id @default(auto()) @map("_id") @db.ObjectId name String collection Collection @relation(fields: [collectionID], references: [id], onDelete: Cascade) @@ -154,7 +154,7 @@ model Lesson { content Content[] quizzes Quiz[] - lessonProgress LessonProgress[] + moduleProgress ModuleProgress[] } model SectionFeedback { @@ -180,7 +180,7 @@ model SectionEnrollment { plan PlanOfStudy? @relation(fields: [planID], references: [id]) planID String? @db.ObjectId progress Progress? - lessonProgress LessonProgress[] + moduleProgress ModuleProgress[] } model Assignment { @@ -239,7 +239,7 @@ model Content { id String @id @default(auto()) @map("_id") @db.ObjectId type ContentType link String - parent Lesson @relation(fields: [parentID], references: [id], onDelete: Cascade) + parent Module @relation(fields: [parentID], references: [id], onDelete: Cascade) parentID String @db.ObjectId primary Boolean } @@ -279,14 +279,14 @@ model Progress { enrollmentID String @unique @db.ObjectId } -model LessonProgress { +model ModuleProgress { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt status Float @default(0) completed Boolean @default(false) - lesson Lesson @relation(fields: [lessonID], references: [id], onDelete: Cascade) - lessonID String @db.ObjectId + module Module @relation(fields: [moduleID], references: [id], onDelete: Cascade) + moduleID String @db.ObjectId // Relation Fields enrollment SectionEnrollment @relation(fields: [enrollmentID], references: [id], onDelete: Cascade) enrollmentID String @unique @db.ObjectId @@ -300,8 +300,8 @@ model Quiz { timeLimit Int? numQuestions Int minScore Float @default(0.0) - parentLesson Lesson @relation(fields: [parentLessonID], references: [id], onDelete: Cascade) - parentLessonID String @db.ObjectId + parentModule Module @relation(fields: [parentModuleID], references: [id], onDelete: Cascade) + parentModuleID String @db.ObjectId questionPool Question[] instances QuizInstance[] } diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index fd8d925f..fbdf2d40 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -12,8 +12,8 @@ import { ModFeedbackFields, AssignmentResFields, ContentFields, - LessonFields, - LessonInput, + ModuleFields, + ModuleInput, CreateCollectionArgs, CreateContentArgs, ModEnrollmentFields, @@ -80,8 +80,8 @@ export class ProgramResolver { return await this.programService.sectionEnrollment(args); } - @Query("lessonsBySectionEnrollment") - async lessonsBySectionEnrollment( + @Query("modulesBySectionEnrollment") + async modulesBySectionEnrollment( @Args("planID") planID: string, @Args("sectionID") sectionID: string ) { @@ -93,29 +93,29 @@ export class ProgramResolver { return enrollment.section.id === sectionID; }); - const lessons = filteredEnrollment[0].section.collections.map( + const modules = filteredEnrollment[0].section.collections.map( (collection) => - collection.lessons.map((lesson) => { - return lesson; + collection.modules.map((module) => { + return module; }) ); - const filteredLessons = lessons.flat().map((lesson) => { - return lesson.lessonProgress.filter((progress) => { + const filteredModules = modules.flat().map((module) => { + return module.moduleProgress.filter((progress) => { return progress.enrollment.id === filteredEnrollment[0].id; }); }); return [ - ...lessons + ...modules .flat() .sort((a, b) => a.position - b.position) - .map((lesson) => { + .map((module) => { return { - ...lesson, - lessonProgress: filteredLessons + ...module, + moduleProgress: filteredModules .flat() - .filter((progress) => progress.lessonID === lesson.id) + .filter((progress) => progress.moduleID === module.id) }; }) ]; @@ -131,9 +131,9 @@ export class ProgramResolver { return await this.programService.content(input); } - @Query("lesson") - async lesson(@Args("input") input: LessonFields) { - return await this.programService.lesson(input); + @Query("module") + async module(@Args("input") input: ModuleFields) { + return await this.programService.module(input); } // Mutations @@ -298,33 +298,33 @@ export class ProgramResolver { return await this.programService.updateCollection(id, data); } - @Mutation("createLesson") - async createLesson(@Args("input") input: LessonInput) { - return await this.programService.createLesson(input); + @Mutation("createModule") + async createModule(@Args("input") input: ModuleInput) { + return await this.programService.createModule(input); } - @Mutation("updateLesson") - async updateLesson( - @Args("input") input: LessonFields, + @Mutation("updateModule") + async updateModule( + @Args("input") input: ModuleFields, @Args("replaceObj") replaceObj: boolean = false ) { - return await this.programService.updateLesson(input, replaceObj); + return await this.programService.updateModule(input, replaceObj); } - @Mutation("deleteLesson") - async deleteLesson(@Args("id") id: string) { - return await this.programService.deleteLesson(id); + @Mutation("deleteModule") + async deleteModule(@Args("id") id: string) { + return await this.programService.deleteModule(id); } @Mutation("createContent") async createContent(@Args("input") input: CreateContentArgs) { - // we get the lesson based on the parent ID of the content - const lesson = await this.programService.lesson({ + // we get the module based on the parent ID of the content + const module = await this.programService.module({ id: input.parent }); // we make a copy of the content array, so we can manipulate it - let updatedContentArray = [...lesson[0].content]; + let updatedContentArray = [...module[0].content]; //checking the length of the array to see no two elements have same content type let len = updatedContentArray.filter( @@ -345,17 +345,17 @@ export class ProgramResolver { ) { // since we need the ID to update a content, we need to make sure it's there if (!input.id) throw new Error("ID field is required"); - // we get the content based on the ID passed in, in order to get the parent lesson ID + // we get the content based on the ID passed in, in order to get the parent module ID const original = await this.programService.content({ id: input.id }); - // we get the lesson based on the parent ID of the content - const lesson = await this.programService.lesson({ + // we get the module based on the parent ID of the content + const module = await this.programService.module({ id: original[0].parentID }); // we make a copy of the content array, so we can manipulate it - let updatedContentArray = [...lesson[0].content]; + let updatedContentArray = [...module[0].content]; // if the change is to make the content primary, we need to make sure there's only one primary content if (input.primary == true) { diff --git a/src/program/program.service.ts b/src/program/program.service.ts index b2ecc450..0ffed9f1 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -12,11 +12,10 @@ import { ModFeedbackFields, AssignmentResFields, ModEnrollmentFields, - LessonFields, - Course, + ModuleFields, SectionFeedback, CreateCollectionArgs, - LessonInput, + ModuleInput, CreateContentArgs, ContentFields, NewSection, @@ -104,7 +103,7 @@ export class ProgramService { subSections: true, collections: { include: { - lessons: { + modules: { include: { content: true } @@ -156,10 +155,10 @@ export class ProgramService { }, collections: { include: { - lessons: { + modules: { include: { content: true, - lessonProgress: { + moduleProgress: { include: { enrollment: true } @@ -180,14 +179,14 @@ export class ProgramService { private collectionInclude = Prisma.validator()({ section: true, - lessons: { + modules: { include: { content: true } } }); - private lessonInclude = Prisma.validator()({ + private moduleInclude = Prisma.validator()({ content: true, collection: { include: { @@ -195,7 +194,7 @@ export class ProgramService { include: { collections: { include: { - lessons: true + modules: true } } } @@ -463,7 +462,7 @@ export class ProgramService { }); } - const { id, name, lessons, sectionID, positionIndex } = params; + const { id, name, modules, sectionID, positionIndex } = params; const where = Prisma.validator()({ ...(id && { id }), @@ -480,14 +479,14 @@ export class ProgramService { }) }); - // loop out of lessons and check with and - if (lessons) { - lessons.map((lesson) => { + // loop out of modules and check with and + if (modules) { + modules.map((module) => { where["AND"] = [ { - lessons: { + modules: { some: { - id: lesson + id: module } } } @@ -501,11 +500,11 @@ export class ProgramService { }); } - //Fetch Lessons - async lesson(input: LessonFields) { + //Fetch modules + async module(input: ModuleFields) { const { id, name, content, collection, position, objectives } = input; - const where = Prisma.validator()({ + const where = Prisma.validator()({ ...(id && { id }), ...(name && { name }), ...(position && { position }), @@ -514,9 +513,9 @@ export class ProgramService { objectives: objectives ? { hasEvery: objectives } : undefined }); - return this.prisma.lesson.findMany({ + return this.prisma.module.findMany({ where, - include: this.lessonInclude + include: this.moduleInclude }); } @@ -538,7 +537,7 @@ export class ProgramService { async createCollection({ name, - lessons, + modules, positionIndex, sectionID }: CreateCollectionArgs) { @@ -550,16 +549,16 @@ export class ProgramService { id: sectionID } }, - lessons: { - connect: lessons?.map((lesson) => { - return { id: lesson }; + modules: { + connect: modules?.map((module) => { + return { id: module }; }) } }); return this.prisma.collection.create({ data: create, include: { - lessons: true + modules: true } }); } @@ -989,9 +988,9 @@ export class ProgramService { } }); } - async createLesson(input: LessonInput) { - //TODO: Support Lessons being added in the middle of an existing collection (i.e new lesson at index 4 needs to shift right starting from original index 4) - const args = Prisma.validator()({ + async createModule(input: ModuleInput) { + //TODO: Support Modules being added in the middle of an existing collection (i.e new module at index 4 needs to shift right starting from original index 4) + const args = Prisma.validator()({ data: { name: input.name, ...(input.content !== null && @@ -1011,16 +1010,16 @@ export class ProgramService { objectives: input.objectives ? input.objectives : undefined, hours: input.hours }, - include: this.lessonInclude + include: this.moduleInclude }); - return this.prisma.lesson.create({ + return this.prisma.module.create({ data: args.data, include: args.include }); } - async updateLesson(input: LessonFields, replaceObj: boolean) { + async updateModule(input: ModuleFields, replaceObj: boolean) { const { id, name, @@ -1028,7 +1027,7 @@ export class ProgramService { // content, // Threads are a list so how these are being updated is going to be a little strange. // The only thing i could think of is if these were a list of IDs in which case the threads - // Being refererenced would all have to be modified in this update Lesson. + // Being refererenced would all have to be modified in this update Module. // thread, collection, objectives, @@ -1038,7 +1037,7 @@ export class ProgramService { const newObjectives = objectives; // Check that they passed in objectives, an ID and they are NOT replacing the list if (newObjectives && id && !replaceObj) { - const current = await this.prisma.lesson.findUnique({ + const current = await this.prisma.module.findUnique({ where: { id } @@ -1060,7 +1059,7 @@ export class ProgramService { ...(hours && { hours }) }; - const args = Prisma.validator()({ + const args = Prisma.validator()({ where: { id: payload.id }, @@ -1073,16 +1072,16 @@ export class ProgramService { } }); - return this.prisma.lesson.update({ + return this.prisma.module.update({ where: args.where, data: args.data, - include: this.lessonInclude + include: this.moduleInclude }); } - async deleteLesson(id: string) { - // TODO: Shift left remaining lessons in the parent collection after deletion. - return this.prisma.lesson.delete({ + async deleteModule(id: string) { + // TODO: Shift left remaining modules in the parent collection after deletion. + return this.prisma.module.delete({ where: { id } diff --git a/src/program/program.spec.ts b/src/program/program.spec.ts index 937a2963..e6ffd85a 100644 --- a/src/program/program.spec.ts +++ b/src/program/program.spec.ts @@ -8,7 +8,7 @@ import { ContentType, CreateCollectionArgs, CreateContentArgs, - LessonInput, + ModuleInput, Module, NewModule, PlanOfStudy, @@ -299,8 +299,8 @@ describe("Collection", () => { }); }; - const deleteLesson = async (id: string) => { - return prisma.lesson.delete({ + const deleteModule = async (id: string) => { + return prisma.module.delete({ where: { id } }); }; @@ -313,21 +313,21 @@ describe("Collection", () => { return resolver.create(input); }; - const createLesson = async (input: LessonInput) => { - return resolver.createLesson(input); + const createModule = async (input: ModuleInput) => { + return resolver.createModule(input); }; - const createLessonContent = async (input: CreateContentArgs) => { + const createModuleContent = async (input: CreateContentArgs) => { return resolver.createContent(input); }; let testingContentID: string; let testingCollectionID: string; let testingModuleID: string; - let testingLessonID: string; + let testingModuleID: string; afterAll(async () => { - await deleteLesson(testingLessonID); + await deleteModule(testingModuleID); await deleteCollection(testingCollectionID); await deleteModule(testingModuleID); }); @@ -353,14 +353,14 @@ describe("Collection", () => { }); testingCollectionID = collection.id; - const lesson = await createLesson({ - name: "Test Lesson", + const module = await createModule({ + name: "Test Module", collection: testingCollectionID }); - testingLessonID = lesson.id; + testingModuleID = module.id; - const content = await createLessonContent({ - parent: testingLessonID, + const content = await createModuleContent({ + parent: testingModuleID, link: "", primary: true, type: ContentType.VIDEO @@ -394,15 +394,15 @@ describe("Collection", () => { expect(col.name).toMatch(/Test/); }); }); - test("should return a collection that has lessons with IDs matching the inputted one", async () => { + test("should return a collection that has modules with IDs matching the inputted one", async () => { const collection = await resolver.collection({ - lessons: [testingLessonID] + modules: [testingModuleID] }); expect(collection).toBeInstanceOf(Array); collection.map(async (col) => { - expect(col.lessons).toBeDefined(); - col.lessons.map((lesson) => { - expect(lesson.id).toBe(testingLessonID); + expect(col.modules).toBeDefined(); + col.modules.map((module) => { + expect(module.id).toBe(testingModuleID); }); }); }); @@ -413,15 +413,15 @@ describe("Collection", () => { expect(collection).toBeInstanceOf(Array); expect(collection[0].position).toBe(0); }); - test("should match lesson position field to array index", async () => { + test("should match module position field to array index", async () => { const coll = await resolver.collection({ id: testingCollectionID }); expect(coll).toBeInstanceOf(Array); coll.map((c) => { - c.lessons.map((lesson) => { - expect(lesson.position === c.lessons[lesson.position].position).toBe( + c.modules.map((module) => { + expect(module.position === c.modules[module.position].position).toBe( true ); - expect(lesson.collectionID === c.id).toBe(true); + expect(module.collectionID === c.id).toBe(true); }); }); }); @@ -431,19 +431,19 @@ describe("Collection", () => { expect(contents).toBeInstanceOf(Array); expect(contents.length).toBeGreaterThan(0); }); - test("should return a content that belongs to the Lesson inputted", async () => { + test("should return a content that belongs to the Module inputted", async () => { const contents = await resolver.content({ - parent: testingLessonID + parent: testingModuleID }); expect(contents).toBeInstanceOf(Array); contents.map(async (content) => { expect(content.id).toBeDefined(); - expect(content.parentID).toBe(testingLessonID); + expect(content.parentID).toBe(testingModuleID); }); }); - test("should return a content array that has only one primary content within a lesson", async () => { + test("should return a content array that has only one primary content within a module", async () => { const contents = await resolver.content({ - parent: testingLessonID + parent: testingModuleID }); expect(contents).toBeInstanceOf(Array); expect(contents.filter((c) => c.primary).length).toBe(1); diff --git a/src/program/schema.graphql b/src/program/schema.graphql index 755fa288..c4aa1a94 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -68,9 +68,9 @@ type SectionEnrollment { """ progress: Progress! """ - The progress model that is associated with this lesson + The progress model that is associated with this module """ - lessonProgress: [LessonProgress] + moduleProgress: [ModuleProgress] } type AssignmentResult { @@ -330,9 +330,9 @@ type Collection { updatedAt: Date! """ - List of lessons in the collection + List of modules in the collection """ - lessons: [Lesson] + modules: [Module] """ The Section that this collection belongs to @@ -348,13 +348,13 @@ type Collection { position: Int } -type Lesson { +type Module { """ document ID """ id: ID! """ - Lesson Name + Module Name """ name: String! """ @@ -362,26 +362,26 @@ type Lesson { """ content: [Content] """ - The list of threads related to the lesson + The list of threads related to the module """ threads: [Thread] """ - The collection that the lesson belongs to + The collection that the module belongs to """ collection: Collection """ - The position index of the lesson in the collection it belongs to. + The position index of the module in the collection it belongs to. """ position: Int """ - The quizzes that belong to this lesson + The quizzes that belong to this module """ quizzes: [Quiz!] """ - The progress model that is associated with this lesson + The progress model that is associated with this module """ - lessonProgress: [LessonProgress] + moduleProgress: [ModuleProgress] """ A list of learning objectives being covered in this Section """ @@ -409,12 +409,12 @@ type Content { link: String! """ - The lesson that this content belongs to. + The module that this content belongs to. """ - parent: Lesson! + parent: Module! """ - Boolean property that marks each element in the content array of a lesson as either the primary content type or secondary resources + Boolean property that marks each element in the content array of a module as either the primary content type or secondary resources """ primary: Boolean! @@ -465,17 +465,17 @@ type Query { """ sectionEnrollment(input: ModEnrollmentFields!): [SectionEnrollment!] """ - Get a list of lessons given the plan ID and enrollment ID + Get a list of modules given the plan ID and enrollment ID """ - lessonsBySectionEnrollment(planID: ID!, SectionID: ID!) : [Lesson!] + modulesBySectionEnrollment(planID: ID!, SectionID: ID!) : [Module!] """ Retrieve a specific collection based on document ID """ collection(input: CollectionFields): [Collection] """ - Retrieve lessons given a set of parameters + Retrieve modules given a set of parameters """ - lesson(input: LessonFields): [Lesson!] + module(input: ModuleFields): [Module!] """ Retrieve Content Given a set of parameters @@ -495,12 +495,12 @@ input CreateContentArgs { link: String! """ - The lesson that this content belongs to. + The module that this content belongs to. """ parent: ID! """ - Boolean property that marks each element in the content array of a lesson as either the primary content type or secondary resources + Boolean property that marks each element in the content array of a module as either the primary content type or secondary resources """ primary: Boolean! } @@ -522,11 +522,11 @@ input ContentFields { link: String """ - The lesson that this content belongs to. + The module that this content belongs to. """ parent: ID """ - Boolean property that marks each element in the content array of a lesson as either the primary content type or secondary resources + Boolean property that marks each element in the content array of a module as either the primary content type or secondary resources """ primary: Boolean } @@ -534,7 +534,7 @@ input ContentFields { input CreateCollectionArgs { name: String! sectionID: ID! - lessons: [ID!] + modules: [ID!] positionIndex: Int! } @@ -542,7 +542,7 @@ input CollectionFields { id: ID name: String sectionID: ID - lessons: [ID] + modules: [ID] positionIndex: Int } @@ -816,9 +816,9 @@ input SectionEnrollmentInput { status: EnrollmentStatus! } -input LessonInput { +input ModuleInput { """ - Lesson Name + Module Name """ name: String! """ @@ -831,7 +831,7 @@ input LessonInput { collection: ID! """ - The index of the lesson in the collection + The index of the Module in the collection """ position: Int """ @@ -844,13 +844,13 @@ input LessonInput { hours: Float! } -input LessonFields { +input ModuleFields { """ document ID """ id: ID """ - Lesson Name + Module Name """ name: String """ @@ -858,7 +858,7 @@ input LessonFields { """ content: ID """ - The list of ids to threads related to the lesson (Only a single value is supported) + The list of ids to threads related to the module (Only a single value is supported) """ thread: ID """ @@ -866,7 +866,7 @@ input LessonFields { """ collection: ID """ - The index of the lesson in the collection + The index of the module in the collection """ position: Int """ @@ -995,19 +995,19 @@ type Mutation { updateCollection(id: ID!, data: CollectionFields!): Collection! """ - Create a Lesson for an individual collection + Create a Module for an individual collection """ - createLesson(input: LessonInput!): Lesson! + createModule(input: ModuleInput!): Module! """ - Update a lesson given its ID - The input parameter is the data that will be updated in the Lesson as well as the ID of the lesson to be updated. - The replaceObj parameter specifies the mode in which lesson objectives should be updated. A true value will repalce + Update a module given its ID + The input parameter is the data that will be updated in the module as well as the ID of the module to be updated. + The replaceObj parameter specifies the mode in which module objectives should be updated. A true value will repalce existing objectives with the new list passed in, whereas a false value (default) will add the strings in the list to the existing data. """ - updateLesson(input: LessonFields, replaceObj: Boolean): Lesson + updateModule(input: ModuleFields, replaceObj: Boolean): Module - deleteLesson(id: String!): Lesson + deleteModule(id: String!): Module """ Create a Content Record diff --git a/src/progress/schema.graphql b/src/progress/schema.graphql index 36901f78..4633bd33 100644 --- a/src/progress/schema.graphql +++ b/src/progress/schema.graphql @@ -52,7 +52,7 @@ input ProgressWaiveArgs { planID: ID } -type LessonProgress { +type ModuleProgress { """ The database defined ID of the progress document """ @@ -78,9 +78,9 @@ type LessonProgress { """ enrollment: SectionEnrollment! """ - The lesson that this progress is associated with (one-to-one) + The module that this progress is associated with (one-to-one) """ - lesson: Lesson! + module: Module! } type Query { diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 74083830..4ad9d666 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -21,7 +21,7 @@ export class QuizService { constructor(private prisma: PrismaService) {} private quizInclude = Prisma.validator()({ - parentLesson: true, + parentModule: true, questionPool: { include: { answers: true @@ -33,7 +33,7 @@ export class QuizService { private quizInstanceInclude = Prisma.validator()({ quiz: { include: { - parentLesson: true, + parentModule: true, questionPool: { include: { answers: true @@ -53,7 +53,7 @@ export class QuizService { answers: true, parent: { include: { - parentLesson: true + parentModule: true } } }); @@ -63,7 +63,7 @@ export class QuizService { include: { parent: { include: { - parentLesson: true + parentModule: true } } } @@ -93,7 +93,7 @@ export class QuizService { timeLimit: args.timeLimit ? args.timeLimit : undefined, numQuestions: args.numQuestions ? args.numQuestions : undefined, minScore: args.minScore ? args.minScore : undefined, - parentLessonID: args.parentLesson ? args.parentLesson : undefined + parentModuleID: args.parentModule ? args.parentModule : undefined }); return this.prisma.quiz.findMany({ where, @@ -163,7 +163,7 @@ export class QuizService { timeLimit: input.timeLimit ? input.timeLimit : undefined, numQuestions: input.numQuestions, minScore: input.minScore ? input.minScore : undefined, - parentLesson: { connect: { id: input.parentLesson } } + parentModule: { connect: { id: input.parentModule } } }); return this.prisma.quiz.create({ data: create, @@ -180,8 +180,8 @@ export class QuizService { timeLimit: values.timeLimit ? values.timeLimit : undefined, numQuestions: values.numQuestions ? values.numQuestions : undefined, minScore: values.minScore ? values.minScore : undefined, - parentLesson: values.parentLesson - ? { connect: { id: values.parentLesson } } + parentModule: values.parentModule + ? { connect: { id: values.parentModule } } : undefined }, where: { diff --git a/src/quiz/quiz.spec.ts b/src/quiz/quiz.spec.ts index 5b0624cd..e453938b 100644 --- a/src/quiz/quiz.spec.ts +++ b/src/quiz/quiz.spec.ts @@ -5,12 +5,12 @@ import { test, describe, expect, afterAll } from "vitest"; import { createAnswer, createCollection, - createLesson, + createModule, createModule, createQuestion, createQuiz, createRandomAnswer, - createRandomLesson, + createRandomModule, createRandomQuestion, createRandomQuiz } from "../../utils"; @@ -30,7 +30,7 @@ describe("Quiz Services", () => { const testingAccountPlanID = "63e51cbd14406c6ad63f73a8"; let fakeModule; let fakeCollection; - let fakeLesson; + let fakeModule; let fakeQuiz; let fakeQuestion; let fakeAnswer; @@ -62,16 +62,16 @@ describe("Quiz Services", () => { positionIndex: 1 }); - fakeLesson = await createLesson( + fakeModule = await createModule( progResolver, - createRandomLesson(fakeCollection.id) + createRandomModule(fakeCollection.id) ); }); describe("Creates", () => { describe("Mutation.createQuiz()", () => { test("should create a new quiz", async () => { - const quizData = createRandomQuiz(fakeLesson.id); + const quizData = createRandomQuiz(fakeModule.id); fakeQuiz = await createQuiz(resolver, quizData); if (fakeQuiz instanceof Error) throw new Error(fakeQuiz.message); expect(fakeQuiz).toBeDefined(); @@ -81,7 +81,7 @@ describe("Quiz Services", () => { expect(fakeQuiz.numQuestions).toEqual(quizData.numQuestions); expect(fakeQuiz.minScore).toEqual(quizData.minScore); expect(fakeQuiz.dueAt).toEqual(quizData.dueAt); - expect(fakeQuiz.parentLessonID).toEqual(fakeLesson.id); + expect(fakeQuiz.parentModuleID).toEqual(fakeModule.id); }); }); describe("Mutation.createQuestion", () => { @@ -159,7 +159,7 @@ describe("Quiz Services", () => { dueAt: fakeQuiz.dueAt, numQuestions: fakeQuiz.numQuestions, minScore: fakeQuiz.minScore, - parentLesson: fakeQuiz.parentLessonID + parentModule: fakeQuiz.parentModuleID }); expect(quizzes).toBeDefined(); @@ -168,7 +168,7 @@ describe("Quiz Services", () => { expect(quiz.dueAt).toEqual(params.dueAt); expect(quiz.numQuestions).toEqual(params.numQuestions); expect(quiz.minScore).toEqual(params.minScore); - expect(quiz.parentLesson.id).toEqual(params.parentLessonID); + expect(quiz.parentModule.id).toEqual(params.parentModuleID); }); }); test("should take less than 1.5 seconds to get all quizzes", async () => { @@ -297,18 +297,18 @@ describe("Quiz Services", () => { describe("Updates", () => { describe("Mutation.updateQuiz()", () => { test("should update all specified fields", async () => { - const otherLesson = await createLesson( + const otherModule = await createModule( progResolver, - createRandomLesson(fakeCollection.id) + createRandomModule(fakeCollection.id) ); - if (otherLesson instanceof Error) throw new Error(otherLesson.message); + if (otherModule instanceof Error) throw new Error(otherModule.message); const quizData = createRandomQuiz(); const updatedQuiz = await resolver.updateQuiz(fakeQuiz.id, { totalPoints: quizData.totalPoints, instructions: quizData.instructions, numQuestions: quizData.numQuestions, minScore: quizData.minScore, - parentLesson: otherLesson.id, + parentModule: otherModule.id, dueAt: quizData.dueAt }); expect(updatedQuiz).toBeDefined(); @@ -316,7 +316,7 @@ describe("Quiz Services", () => { expect(updatedQuiz.instructions).toEqual(quizData.instructions); expect(updatedQuiz.numQuestions).toEqual(quizData.numQuestions); expect(updatedQuiz.minScore).toEqual(quizData.minScore); - expect(updatedQuiz.parentLesson.id).toEqual(otherLesson.id); + expect(updatedQuiz.parentModule.id).toEqual(otherModule.id); expect(updatedQuiz.dueAt).toEqual(quizData.dueAt); }); }); diff --git a/src/quiz/schema.graphql b/src/quiz/schema.graphql index 2a55daf0..2a8c0161 100644 --- a/src/quiz/schema.graphql +++ b/src/quiz/schema.graphql @@ -28,9 +28,9 @@ type Quiz { """ minScore: Float! """ - The lesson that this quiz belongs to + The module that this quiz belongs to """ - parentLesson: Lesson! + parentModule: Module! """ The possible questions and variants of for this quiz """ @@ -180,9 +180,9 @@ input QuizFields { """ minScore: Float """ - The lesson that this quiz belongs to + The module that this quiz belongs to """ - parentLesson: ID + parentModule: ID } input QuizInstanceFields { @@ -328,9 +328,9 @@ input CreateQuiz { """ minScore: Float """ - The lesson that this quiz belongs to + The module that this quiz belongs to """ - parentLesson: ID! + parentModule: ID! } input UpdateQuiz { @@ -359,9 +359,9 @@ input UpdateQuiz { """ minScore: Float """ - The lesson that this quiz belongs to + The module that this quiz belongs to """ - parentLesson: ID + parentModule: ID } input CreateQuestion { @@ -474,7 +474,7 @@ input QuizSubmission { # TODO: Update documentation for update mutations type Mutation { """ - Create a new quiz record and associate it with its parent Lesson + Create a new quiz record and associate it with its parent Module """ createQuiz(input: CreateQuiz): Quiz! """ diff --git a/utils/fakes.ts b/utils/fakes.ts index 58788ab8..a1afe4d7 100644 --- a/utils/fakes.ts +++ b/utils/fakes.ts @@ -5,9 +5,9 @@ import { Content, Course, InstructorProfile, - Lesson, Module, - ModuleEnrollment, + Section, + SectionEnrollment, PlanOfStudy, Progress, Social, @@ -77,12 +77,12 @@ export function createRandomPlanOfStudy(userAccountID?: string): PlanOfStudy { export function createRandomCourse(): Course { return { id: faker.database.mongodbObjectId(), - moduleIDs: [faker.database.mongodbObjectId()], + sectionIDs: [faker.database.mongodbObjectId()], name: faker.lorem.words(3) }; } -export function createRandomModule(): Module { +export function createRandomSection(): Section { return { id: faker.database.mongodbObjectId(), courseIDs: [faker.database.mongodbObjectId()], @@ -91,17 +91,17 @@ export function createRandomModule(): Module { duration: faker.datatype.number(), intro: faker.lorem.lines(1), keywords: [faker.lorem.words(3)], - moduleName: faker.lorem.words(3), - moduleNumber: faker.datatype.number(), + sectionName: faker.lorem.words(3), + sectionNumber: faker.datatype.number(), numSlides: faker.datatype.number(), objectives: [faker.lorem.words(3)], - parentModuleIDs: [faker.database.mongodbObjectId()], - subModuleIDs: [faker.database.mongodbObjectId()], + parentSectionIDs: [faker.database.mongodbObjectId()], + subSectionIDs: [faker.database.mongodbObjectId()], updatedAt: faker.date.past() }; } -export function createRandomLesson(collectionID?: string): Lesson { +export function createRandomModule(collectionID?: string): Module { return { id: faker.database.mongodbObjectId(), collectionID: collectionID @@ -113,36 +113,36 @@ export function createRandomLesson(collectionID?: string): Lesson { }; } -export function createRandomCollection(moduleID?: string): Collection { +export function createRandomCollection(sectionID?: string): Collection { return { createdAt: faker.date.past(), id: faker.database.mongodbObjectId(), - moduleID: moduleID ? moduleID : faker.database.mongodbObjectId(), + sectionID: sectionID ? sectionID : faker.database.mongodbObjectId(), name: faker.lorem.words(3), position: faker.datatype.number(), updatedAt: faker.date.past() }; } -export function createRandomModuleEnrollment( - moduleID?: string, +export function createRandomSectionEnrollment( + sectionID?: string, planID?: string -): ModuleEnrollment { +): SectionEnrollment { return { enrolledAt: faker.date.past(), id: faker.database.mongodbObjectId(), - moduleId: moduleID ? moduleID : faker.database.mongodbObjectId(), + sectionId: sectionID ? sectionID : faker.database.mongodbObjectId(), planID: planID ? planID : faker.database.mongodbObjectId(), role: faker.helpers.arrayElement(["STUDENT", "TEACHER", "GRADER"]), status: faker.helpers.arrayElement(["ACTIVE", "INACTIVE"]) }; } -export function createRandomAssignment(moduleID?: string): Assignment { +export function createRandomAssignment(sectionID?: string): Assignment { return { dueAt: faker.date.future(), id: faker.database.mongodbObjectId(), - moduleId: moduleID ? moduleID : faker.database.mongodbObjectId(), + sectionId: sectionID ? sectionID : faker.database.mongodbObjectId(), name: faker.lorem.words(3), updatedAt: faker.date.past() }; @@ -168,15 +168,15 @@ export function createRandomAssignmentResult( export function createRandomThread( authorID?: string, - parentLessonID?: string, + parentModuleID?: string, parentThreadID?: string, watcherID?: string ): Thread { return { id: faker.database.mongodbObjectId(), authorID: authorID ? authorID : faker.database.mongodbObjectId(), - parentLessonID: parentLessonID - ? parentLessonID + parentModuleID: parentModuleID + ? parentModuleID : faker.database.mongodbObjectId(), parentThreadID: parentThreadID ? parentThreadID @@ -209,7 +209,7 @@ export function createRandomQuiz(parentID?: string): Quiz { timeLimit: faker.datatype.number(), numQuestions: questions, minScore: faker.datatype.number(), - parentLessonID: parentID ? parentID : faker.database.mongodbObjectId() + parentModuleID: parentID ? parentID : faker.database.mongodbObjectId() } } @@ -258,10 +258,10 @@ export const social = createRandomSocial(); export const plan = createRandomPlanOfStudy(); export const course = createRandomCourse(); +export const section = createRandomSection(); export const module = createRandomModule(); -export const lesson = createRandomLesson(); export const collection = createRandomCollection(); -export const moduleEnrollment = createRandomModuleEnrollment(); +export const sectionEnrollment = createRandomSectionEnrollment(); export const assignment = createRandomAssignment(); export const assignmentResult = createRandomAssignmentResult(); export const thread = createRandomThread(); diff --git a/utils/tests.ts b/utils/tests.ts index 14bfb8c3..6960943a 100644 --- a/utils/tests.ts +++ b/utils/tests.ts @@ -6,11 +6,11 @@ import { CreateQuestion, CreateQuiz, EnrollmentStatus, - LessonInput, + ModuleInput, UserRole } from "@/types/graphql"; import { QuizResolver } from "@/quiz/quiz.resolver"; -import { Answer, Lesson, Question, Quiz } from "@prisma/client"; +import { Answer, Module, Question, Quiz } from "@prisma/client"; export const shuffle = (str: string) => [...str].sort(() => Math.random() - 0.5).join(""); @@ -35,11 +35,11 @@ export const createPlan = async ( } else return self[0]; }; -export const createModule = async ( +export const createSection = async ( resolver: ProgramResolver, config: { - moduleName: string; - moduleNumber: number; + sectionName: string; + sectionNumber: number; description: string; duration: number; intro: string; @@ -47,34 +47,34 @@ export const createModule = async ( numSlides: number; } ) => { - const module = await resolver.create({ ...config }); - if (module) return module; - else return new Error("Failed to create module"); + const section = await resolver.create({ ...config }); + if (section) return section; + else return new Error("Failed to create section"); }; -export const createLesson = async ( +export const createModule = async ( resolver: ProgramResolver, - config: Lesson + config: Module ) => { - const data: LessonInput = { + const data: ModuleInput = { name: config.name, collection: config.collectionID }; - const lesson = await resolver.createLesson({ ...data }); - if (data) return lesson; - else return new Error("Failed to create Lesson"); + const module = await resolver.createModule({ ...data }); + if (data) return module; + else return new Error("Failed to create Module"); }; export const createEnrollment = async ( resolver: ProgramResolver, config: { - module: string; + section: string; plan: string; status: EnrollmentStatus; role: UserRole; } ) => { - const enrollment = await resolver.addModuleEnrollment({ ...config }); + const enrollment = await resolver.addSectionEnrollment({ ...config }); if (enrollment) return enrollment; else return new Error("Failed to create enrollment"); }; @@ -96,7 +96,7 @@ export const createQuiz = async (resolver: QuizResolver, input: Quiz) => { timeLimit: input.timeLimit, numQuestions: input.numQuestions, minScore: input.minScore, - parentLesson: input.parentLessonID + parentModule: input.parentModuleID }; const quiz = await resolver.createQuiz(data); if (quiz) return quiz; From d93abf0e596677f73ce738a61a75fe69ec7da577 Mon Sep 17 00:00:00 2001 From: "Daniel B. Papp" <25376026+chef-danny-d@users.noreply.github.com> Date: Mon, 1 May 2023 15:45:36 -0400 Subject: [PATCH 22/31] feat(LP): created base schema and model structure (#493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(LP): created base schema and model structure * fix(date): Changed date returned by scalar for more accurate sorting * fix(LP): created base query and mutation The query retrieves through the user's Plan ID. The mutation expects the planID and the path structure directly inputted * fix(LP): mutation creates enrollments into each module passed in * chore(prisma): Bumped version number * chore(ts): Added type inference rule * fix(LP): Modified schema structure Now a single LP model is assigned to a single plan of study, creating a 1 to 1 relationship between the LP model and the user. However, now the LP model stores an array of paths, making the functionality nearly the same as users can still have one to many relations with paths, its just one level deeper in the composite type. * feat(LP): Added basic safeguards to creation As the input object accepts either a single path object or a list of path objects, I added a statement that checks if either of the two is passed in before continuing with the operations * feat(LP): Started implementing union path input functionality Mainly focused on getting the single path input working. Still need to match and create the module enrollments before we proceed to validating the path data * feat(LP): added LP status enum to schema The enum can be either Draft or Live. Once users update their paths to be live, they get enrolled into the modules. While the path is in draft, they aren't enrolled in any of the modules. * feat(LP): removed enrollment creation from service This operation will be handled by the update service rather then the initial creation service * feat(LP): added hours satisfied and learning outcomes to path model Hours satisfied tracks the number of hours that the current enrollment in the path satisfies. Learning outcome of the path defines all the learning outcomes of the modules being taken * feat(LP): added name field to sections and collections Since we are using fake data for the sections and collections, we need something visual to create UI layouts with. Eventually only the ID will be required, and the service will retrieve the appropriate data and concatenate it with the ID * feat(LP): manually extended course and module types Since we want to get the appropriate data from GQL, we want to allow the selection of subfields that match the composite type * fix(LP): resolve concatenation issue with query Previously, the query returned a single section with a single collection and mapped all the modules into that one element array. Now the query keeps the existing composite structure and fetches the data to concatenate into the module object. * fix(LP): resolved fields not receiving values on creation I forgot to add the learning outcomes and hoursSatisfied fields to the create operation. Now we also store collection and section names in the composite types until we get the program structure updated * feat(LP): added optional filter for LPs by pathID * feat(LP): implemented mutation to update paths The mutation takes in the inputted course structure and replaces the existing one in the array while keeping the other records untouched * fix(LP): added required import statement * feat(LP): created mutation to delete a single path User can now delete individual paths from an LP model * feat(LP): created mutation to create a single path User can now create individual paths attached to an LP model * fix(LP): resolved schema update conflicts * refactor(user): added mutation to handle create operation * fix(sections): argument in GQL and the API were mismatching * fix(schema): turned collection to module relation to many-to-many Since collections are just grouped modules, a module could be part of many collections and collections can hold a number of modules. * fix(progress): resumption needed to change according to FE * feat(lp): updated includes * refactor(lp): updated path schema to match redevelopment * feat(module): added query to get data by learning Path * feat(module): additional fields added as required by FE Modules have an optional direct relation to an instructor profile to connect instructors with their modules. Furthermore, I added module prefixes (ENMA, CS, DS, etc), and module numbers * feat(course): additional fields added as needed I added course prefixes (ENMA, CS, DS, etc), and course numbers, so not everything is stored in the course name field. The name field should be reserved for the user friendly title of the course. * refactor(course): creation of many courses at once is supported * fix(type): addressed type errors as a result of schema changes * feat(modules): created direct resolver for module flow and return type to go with it Return type to be added to the resolver once all the edge cases are figured out * refactor(LP): modified the selectable fields for sections --------- Co-authored-by: Dániel B. Papp --- .eslintrc.yml | 1 + gql/graphql.ts | 212 +++++++- package.json | 4 +- prisma/dbml/schema.dbml | 37 +- prisma/schema.prisma | 81 ++- src/app.module.ts | 3 +- src/program/program.resolver.ts | 296 ++++++++++- src/program/program.service.ts | 513 +++++++++++++++++- src/program/schema.graphql | 885 ++++++++++++++++++++++---------- src/user/user.resolver.ts | 8 +- src/user/user.service.ts | 11 +- utils/fakes.ts | 72 ++- utils/tests.ts | 3 +- yarn.lock | 36 +- 14 files changed, 1818 insertions(+), 344 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 83911004..f557db0c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -17,4 +17,5 @@ rules: { no-var: warn, prefer-const: warn, no-mixed-spaces-and-tabs: [warn, smart-tabs], + "@typescript-eslint/no-inferrable-types": off, } diff --git a/gql/graphql.ts b/gql/graphql.ts index 418eb8db..af77ddb9 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -37,6 +37,11 @@ export enum FileType { TXT = "TXT" } +export enum PathStatus { + DRAFT = "DRAFT", + LIVE = "LIVE" +} + export interface IThreadCreateInput { title?: Nullable; body: string; @@ -73,6 +78,40 @@ export interface PlanFields { sectionsLeft?: Nullable; } +export interface CreateLearningPathInput { + path?: Nullable; + paths?: Nullable; +} + +export interface PathInput { + course: CoursePathInput; + status?: Nullable; + hoursSatisfies?: Nullable; + learningOutcomes?: Nullable; +} + +export interface CoursePathInput { + id: string; + sections: SectionPathInput[]; +} + +export interface SectionPathInput { + id: string; + name: string; + collections: CollectionPathInput[]; +} + +export interface CollectionPathInput { + id: string; + name: string; + modules: ModulePathInput[]; +} + +export interface ModulePathInput { + id: string; + enrollmentID?: Nullable; +} + export interface CreateContentArgs { type: ContentType; link: string; @@ -125,6 +164,8 @@ export interface SectionFields { export interface CourseFields { id?: Nullable; name?: Nullable; + number?: Nullable; + prefix?: Nullable; section?: Nullable; required?: Nullable; carnegieHours?: Nullable; @@ -209,9 +250,11 @@ export interface AssignmentInput { export interface CourseInput { name: string; - section: string; - required: boolean; - carnegieHours: number; + number?: Nullable; + prefix?: Nullable; + section?: Nullable; + required?: Nullable; + carnegieHours?: Nullable; } export interface SectionFeedbackInput { @@ -242,22 +285,30 @@ export interface SectionEnrollmentInput { export interface ModuleInput { name: string; + prefix?: Nullable; + number?: Nullable; content?: Nullable; collection: string; position?: Nullable; objectives?: Nullable; hours: number; + description?: Nullable; + instructor?: Nullable; } export interface ModuleFields { id?: Nullable; name?: Nullable; + number?: Nullable; + prefix?: Nullable; content?: Nullable; thread?: Nullable; collection?: Nullable; position?: Nullable; objectives?: Nullable; hours?: Nullable; + description?: Nullable; + instructor?: Nullable; } export interface ProgressArgs { @@ -472,7 +523,7 @@ export interface IMutation { addSection(input?: Nullable): Section | Promise
; updateSection(input?: Nullable): Nullable
| Promise>; deleteCourse(id: string): Nullable | Promise>; - addCourse(input?: Nullable): Course | Promise; + addCourse(input?: Nullable, many?: Nullable): Course | Promise; updateCourse(id: string, input?: Nullable): Nullable | Promise>; addAssignment(input?: Nullable): Assignment | Promise; addObjectives(id: string, input?: Nullable): Nullable
| Promise>; @@ -497,6 +548,10 @@ export interface IMutation { createContent(input: CreateContentArgs): Content | Promise; updateContent(input: ContentFields): Nullable | Promise>; deleteContent(contentID: string): Nullable | Promise>; + createLearningPath(planID: string, input: CreateLearningPathInput): LearningPath | Promise; + createPath(planID: string, input: PathInput): SimpleLearningPath | Promise; + updateLearningPath(planID: string, pathID: string, input: PathInput): LearningPath | Promise; + deleteLearningPath(planID: string, pathID: string): LearningPath | Promise; createProgress(input: ProgressArgs, enrollmentID: string): Progress | Promise; waiveSection(args: ProgressWaiveArgs): Progress | Promise; deleteProgress(id: string): boolean | Promise; @@ -542,10 +597,14 @@ export interface IQuery { sectionFeedback(input: ModFeedbackFields): Nullable | Promise>; assignmentResult(input: AssignmentResFields): Nullable | Promise>; sectionEnrollment(input: ModEnrollmentFields): Nullable | Promise>; - modulesBySectionEnrollment(planID: string, SectionID: string): Nullable | Promise>; + modulesBySectionEnrollment(planID: string, sectionID: string): Nullable | Promise>; collection(input?: Nullable): Nullable[]> | Promise[]>>; module(input?: Nullable): Nullable | Promise>; content(input?: Nullable): Nullable | Promise>; + learningPath(planID: string, pathID?: Nullable): LearningPath[] | Promise; + latestModuleProgress(planID: string, sectionID: string, moduleID: string): Nullable | Promise>; + modulesFromLearningPath(planID: string): Nullable | Promise>; + moduleFlowFromLearningPath(planID: string, moduleID: string): Nullable | Promise>; progress(args: ProgressArgs): Nullable[] | Promise[]>; quiz(args: QuizFields): Quiz[] | Promise; quizInstance(args: QuizInstanceFields): QuizInstance[] | Promise; @@ -660,6 +719,8 @@ export interface SectionFeedback { export interface Course { id: string; name: string; + number?: Nullable; + prefix?: Nullable; sectionIDs?: Nullable[]>; required: boolean; carnegieHours: number; @@ -692,6 +753,7 @@ export interface Collection { createdAt: Date; updatedAt: Date; modules?: Nullable[]>; + moduleIDs?: Nullable; section: Section; sectionID: string; position?: Nullable; @@ -699,15 +761,21 @@ export interface Collection { export interface Module { id: string; + prefix?: Nullable; + number?: Nullable; name: string; content?: Nullable[]>; threads?: Nullable[]>; - collection?: Nullable; + collections?: Nullable[]>; + collectionIDs?: Nullable; position?: Nullable; quizzes?: Nullable; moduleProgress?: Nullable[]>; objectives: string[]; hours: number; + description?: Nullable; + instructor?: Nullable; + instructorID?: Nullable; } export interface Content { @@ -722,6 +790,138 @@ export interface Error { message?: Nullable; } +export interface ModuleFlow { + previousModule?: Nullable; + previousCollection?: Nullable; + nextModule?: Nullable; + nextCollection?: Nullable; + currentModule?: Nullable; + currentCollection?: Nullable; + currentSection?: Nullable
; +} + +export interface SimpleLearningPath { + id: string; + createdAt: Date; + plan: PlanOfStudy; + planID: string; + paths: SimplePath[]; +} + +export interface SimplePath { + id: string; + createdAt: Date; + updatedAt: Date; + course: SimpleCoursePath; + status: PathStatus; + hoursSatisfies: number; + learningOutcomes: string[]; +} + +export interface SimpleCoursePath { + id: string; + sections: SimpleSection[]; +} + +export interface SimpleSection { + id: string; + name: string; + collections: SimpleCollection[]; +} + +export interface SimpleCollection { + modules: SimpleModule[]; + id: string; + name: string; + createdAt: Date; + updatedAt: Date; + section: Section; + sectionID: string; + position?: Nullable; +} + +export interface SimpleModule { + id: string; + name: string; + collections: SimpleCollection[]; +} + +export interface LearningPath { + id: string; + createdAt: Date; + plan: PlanOfStudy; + planID: string; + paths: Path[]; +} + +export interface Path { + id: string; + createdAt: Date; + updatedAt: Date; + course: CoursePath; + status: PathStatus; + hoursSatisfies: number; + learningOutcomes: string[]; +} + +export interface CoursePath { + id: string; + name: string; + prefix?: Nullable; + number?: Nullable; + required: boolean; + carnegieHours: number; + sections: SectionPath[]; +} + +export interface SectionPath { + name: string; + collections: CollectionPath[]; + id: string; + sectionNumber: number; + sectionName: string; + description: string; + duration: number; + intro: string; + numSlides: number; + keywords: string[]; + objectives: string[]; + createdAt: Date; + updatedAt: Date; + assignments: Assignment[]; + members: SectionEnrollment[]; + feedback: SectionFeedback[]; + parentSections: Section[]; + subSections: Section[]; + courseIDs: string[]; +} + +export interface CollectionPath { + id: string; + name: string; + modules: ModulePath[]; +} + +export interface ModulePath { + id: string; + name: string; + number?: Nullable; + prefix?: Nullable; + content?: Nullable[]>; + threads?: Nullable[]>; + collections?: Nullable[]>; + collectionIDs?: Nullable; + position?: Nullable; + quizzes?: Nullable; + moduleProgress?: Nullable[]>; + objectives: string[]; + hours: number; + enrollmentID?: Nullable; + description?: Nullable; + instructor?: Nullable; + instructorID?: Nullable; +} + export interface Progress { id: string; status: number; diff --git a/package.json b/package.json index 24f91ec0..06f30f7c 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@nestjs/platform-express": "^8.0.6", "@nestjs/platform-socket.io": "^9.3.7", "@nestjs/websockets": "^9.3.7", - "@prisma/client": "^4.0.0", + "@prisma/client": "^4.13.0", "@redis/client": "^1.5.5", "@sentry/node": "^6.13.2", "@sentry/tracing": "^6.13.2", @@ -78,7 +78,7 @@ "eslint": "^8.33.0", "jest": "^28.1.3", "prettier": "^1.19.1", - "prisma": "^4.0.0", + "prisma": "^4.13.0", "prisma-dbml-generator": "^0.9.1", "spectaql": "^2.0.3", "ts-jest": "^28.0.8", diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 9e2fc86e..d9ae1e14 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -44,6 +44,7 @@ Table InstructorProfile { background String researchInterest String[] [not null] selectedPapersAndPublications String[] [not null] + instructedModules Module [not null] } Table PlanOfStudy { @@ -53,6 +54,7 @@ Table PlanOfStudy { sections SectionEnrollment [not null] assignmentResults AssignmentResult [not null] quizResults QuizResult [not null] + learningPaths LearningPath [not null] } Table Social { @@ -78,6 +80,8 @@ Table Token { Table Course { id String [pk] name String [not null] + number Int [default: 0] + prefix String [default: ''] required Boolean [default: false] carnegieHours Int [default: 135] sectionIDs String[] [not null] @@ -115,6 +119,7 @@ Table Collection { updatedAt DateTime [default: `now()`, not null] position Int [not null, default: 0] modules Module [not null] + moduleIDs String[] [not null] section Section [not null] sectionID String [not null] } @@ -122,11 +127,16 @@ Table Collection { Table Module { id String [pk] name String [not null] - collection Collection [not null] - collectionID String [not null] + prefix String [default: ''] + number Int [default: 1] + collections Collection [not null] + collectionIDs String[] [not null] position Int [not null, default: 0] objectives String[] [not null] hours Float [not null, default: 0] + description String [default: ''] + instructor InstructorProfile + instructorProfileID String [default: ''] content Content [not null] quizzes Quiz [not null] moduleProgress ModuleProgress [not null] @@ -312,6 +322,14 @@ Table QuizResult { quizInstance QuizInstance } +Table LearningPath { + id String [pk] + createdAt DateTime [default: `now()`, not null] + plan PlanOfStudy [not null] + planID String [unique, not null] + paths Path[] [not null] +} + Enum UserRole { STUDENT TEACHER @@ -342,6 +360,11 @@ Enum FileType { TXT } +Enum PathStatus { + DRAFT + LIVE +} + Ref: User.watchedThreadIDs > Thread.id Ref: User.groupMembershipIDs > Group.id @@ -364,9 +387,13 @@ Ref: Section.subSectionIDs > Section.id Ref: Section.courseIDs > Course.id +Ref: Collection.moduleIDs > Module.id + Ref: Collection.sectionID > Section.id [delete: Cascade] -Ref: Module.collectionID > Collection.id [delete: Cascade] +Ref: Module.collectionIDs > Collection.id + +Ref: Module.instructorProfileID > InstructorProfile.id Ref: SectionFeedback.studentId > User.id @@ -422,4 +449,6 @@ Ref: Question.instanceIDs > QuizInstance.id Ref: Answer.parentQuestionID > Question.id [delete: Cascade] -Ref: QuizResult.studentID > PlanOfStudy.id \ No newline at end of file +Ref: QuizResult.studentID > PlanOfStudy.id + +Ref: LearningPath.planID > PlanOfStudy.id [delete: Cascade] \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 240a6ad4..704750c6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -57,6 +57,7 @@ model InstructorProfile { background String? researchInterest String[] @default([]) selectedPapersAndPublications String[] @default([]) + instructedModules Module[] @relation() } model PlanOfStudy { @@ -69,6 +70,7 @@ model PlanOfStudy { sections SectionEnrollment[] assignmentResults AssignmentResult[] quizResults QuizResult[] + learningPaths LearningPath[] } model Social { @@ -96,6 +98,8 @@ model Token { model Course { id String @id @default(auto()) @map("_id") @db.ObjectId name String + number Int? @default(0) + prefix String? @default("") required Boolean? @default(false) carnegieHours Int? @default(135) sectionIDs String[] @default([]) @db.ObjectId @@ -138,23 +142,29 @@ model Collection { position Int @default(0) // Relation fields - modules Module[] + modules Module[] @relation(fields: [moduleIDs], references: [id]) + moduleIDs String[] @default([]) @db.ObjectId section Section @relation(name: "collections", fields: [sectionID], references: [id], onDelete: Cascade) sectionID String @db.ObjectId } model Module { - id String @id @default(auto()) @map("_id") @db.ObjectId - name String - collection Collection @relation(fields: [collectionID], references: [id], onDelete: Cascade) - collectionID String @db.ObjectId - position Int @default(0) - objectives String[] @default([]) - hours Float @default(0.0) - - content Content[] - quizzes Quiz[] - moduleProgress ModuleProgress[] + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + prefix String? @default("") + number Int? @default(1) + collections Collection[] @relation(fields: [collectionIDs], references: [id]) + collectionIDs String[] @default([]) @db.ObjectId + position Int @default(0) + objectives String[] @default([]) + hours Float @default(0.0) + description String? @default("") + + instructor InstructorProfile? @relation(fields: [instructorProfileID], references: [id]) + instructorProfileID String? @default("") @db.ObjectId + content Content[] + quizzes Quiz[] + moduleProgress ModuleProgress[] } model SectionFeedback { @@ -350,6 +360,48 @@ model QuizResult { quizInstance QuizInstance? } +model LearningPath { + id String @id @default(auto()) @map("_id") @db.ObjectId + createdAt DateTime @default(now()) + plan PlanOfStudy @relation(fields: [planID], references: [id], onDelete: Cascade) + planID String @db.ObjectId + paths Path[] + + @@unique([planID]) +} + +type Path { + id String + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) + course CoursePath + status PathStatus? @default(DRAFT) + hoursSatisfies Float? @default(0.0) + learningOutcomes String[] @default([]) +} + +type CoursePath { + id String + sections SectionPath[] +} + +type SectionPath { + id String + name String + collections CollectionPath[] +} + +type CollectionPath { + id String + name String + modules ModulePath[] +} + +type ModulePath { + id String + enrollmentID String +} + enum UserRole { STUDENT TEACHER @@ -380,6 +432,11 @@ enum FileType { TXT } +enum PathStatus { + DRAFT + LIVE +} + // Dont need this anymore since we arent doing API Tokens // enum TokenType { // EMAIL // used as a short lived token sent to the user's email diff --git a/src/app.module.ts b/src/app.module.ts index bd99646d..012e52e1 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -27,7 +27,8 @@ export class DateScalar implements CustomScalar { //What we are sending to the client serialize(value: MomentInput | unknown): string { - if (value instanceof Date) return moment(value).format("MM/DD/YYYY"); + if (value instanceof Date) + return moment(value).format("MM/DD/YYYY HH:mm:ss"); return ""; } diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index fbdf2d40..22dae73f 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -18,7 +18,11 @@ import { CreateContentArgs, ModEnrollmentFields, NewSection, - CollectionFields + CollectionFields, + CreateLearningPathInput, + PathInput, + Module, + ModuleFlow } from "@/types/graphql"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { ProgramService } from "./program.service"; @@ -85,6 +89,8 @@ export class ProgramResolver { @Args("planID") planID: string, @Args("sectionID") sectionID: string ) { + // using the combination of the student's plan of study and the section they are looking at + // we can find the exact enrollment they are looking for as only one enrollment can exist for a student for a given section const enrollment = await this.programService.sectionEnrollment({ plan: planID }); @@ -121,6 +127,206 @@ export class ProgramResolver { ]; } + @Query("latestModuleProgress") + async moduleProgress( + @Args("planID") planID: string, + @Args("sectionID") sectionID: string, + @Args("moduleID") moduleID: string + ) { + // we want to find the next collection of modules the student should be working on + // we can do this by finding the enrollment for the student in the section they are looking at + // and then finding the module progress for that enrollment + // we can then filter the module progress to find the latest progress for the module they are looking at + // if the module progress is empty, we can assume that the student has not started the module yet + // and we can return the first module in the collection + + const enrollment = await this.programService.sectionEnrollment({ + plan: planID + }); + + const filteredEnrollment = enrollment.filter((enrollment) => { + return enrollment.section.id === sectionID; + }); + + const modules = filteredEnrollment[0].section.collections.map( + (collection) => + collection.modules.map((module) => { + return module; + }) + ); + + const filteredModules = modules.flat().map((module) => { + return module.moduleProgress.filter((progress) => { + return progress.enrollment.id === filteredEnrollment[0].id; + }); + }); + + const latestModuleProgress = filteredModules + .flat() + .filter((progress) => progress.moduleID === moduleID) + .sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0]; + + return latestModuleProgress; + } + + @Query("modulesFromLearningPath") + async modulesFromLearningPath(@Args("planID") planID: string) { + // find enrollments for the student from planID + // find the learning path for the plan + // filter out inactive paths + // filter out modules with a completed progress status + // return the incomplete list of modules that match the student's learning path + + const enrollment = await this.programService.sectionEnrollment({ + plan: planID + }); + + const learningPath = await this.programService.learningPath(planID); + + const filteredLearningPath = learningPath[0].paths.filter((path) => { + return path.status === "LIVE"; + }); + + if (filteredLearningPath.length === 0) { + return []; + } + + const modules: Module[] = filteredLearningPath[0].course.sections + .map((section) => { + return section.collections.map((collection) => { + return collection.modules.map((module) => { + return module; + }); + }); + }) + .flat() + .flat(); + + const nonDuplicateModules = modules.filter((module, index, self) => { + return index === self.findIndex((m) => m.id === module.id); + }); + + const filteredModules = nonDuplicateModules.map((module) => { + // if there is no progress for the module, return the module + if ( + module.moduleProgress?.length === 0 || + module.moduleProgress === null || + typeof module.moduleProgress === "undefined" + ) + return module; + else { + // if there is progress for the module, filter out the progresses made by other students + // and return the module if there is no progress for the student + const filteredProgress = module.moduleProgress.filter((progress) => { + if (!progress) return false; + else if (!progress.completed) return false; + else return progress.enrollment.id !== enrollment[0].id; + }); + if (filteredProgress.length === 0) return module; + else return null; + } + }); + + return filteredModules.filter((module) => module !== null); + } + + @Query("moduleFlowFromLearningPath") + async moduleFlowFromLearningPath( + @Args("planID") planID: string, + @Args("moduleID") moduleID: string + ) { + // get lp from plan + const learningPath = await this.programService.learningPath(planID); + if (!learningPath) + throw new Error("No learning paths found for inputted user"); + + // filter out non-live paths + const filteredLearningPath = learningPath[0].paths.filter((path) => { + return path.status === "LIVE"; + }); + if (filteredLearningPath.length === 0) return []; + + const sections = filteredLearningPath[0].course.sections; + + // find the collection that the requested module is in relative to the learning path + const collection = sections + .map((section) => { + return section.collections.find((collection) => { + return collection.modules.find((module) => { + return module.id === moduleID; + }); + }); + }) + .flat() + .flat(); + + const filteredCollection = collection.filter( + (collection) => typeof collection !== "undefined" + )[0]; + + if (typeof filteredCollection === "undefined") return []; + + const currentModuleIndex = filteredCollection.modules.findIndex( + (module) => module.id === moduleID + ); + + const currentSectionIndex = sections.findIndex((section) => { + return section.collections.find((collection) => { + return collection.id === filteredCollection.id; + }); + }); + + let nextCollection = filteredCollection; + + if (currentModuleIndex + 1 === filteredCollection.modules.length) { + // get the array of collections in the sections + // find the current collection in the array + // add 1 to the index to get the next collection + // get the first module in the next collection + + const nextSection = sections[currentSectionIndex + 1]; + + nextCollection = nextSection?.collections[0]; + + // student has reached the end of the section + if ( + typeof nextSection === "undefined" || + typeof nextCollection === "undefined" + ) + return { + currentModule: filteredCollection.modules[currentModuleIndex], + nextModule: null, + currentCollection: filteredCollection, + nextCollection: null, + previousModule: filteredCollection.modules[currentModuleIndex - 1], + previousCollection: filteredCollection, + currentSection: sections[currentSectionIndex] + }; + + return { + currentModule: filteredCollection.modules[currentModuleIndex], + nextModule: nextCollection.modules[0], + currentCollection: filteredCollection, + nextCollection: nextCollection, + previousModule: filteredCollection.modules[currentModuleIndex - 1], + previousCollection: filteredCollection, + currentSection: sections[currentSectionIndex] + }; + } + + const nextModule = filteredCollection.modules[currentModuleIndex + 1]; + + return { + currentModule: filteredCollection.modules[currentModuleIndex], + nextModule: nextModule, + currentCollection: filteredCollection, + nextCollection: filteredCollection, + previousModule: filteredCollection.modules[currentModuleIndex - 1], + previousCollection: filteredCollection, + currentSection: sections[currentSectionIndex] + }; + } + @Query("collection") async collection(@Args("input") args: CollectionFields | null = null) { return await this.programService.collection(args); @@ -136,6 +342,25 @@ export class ProgramResolver { return await this.programService.module(input); } + @Query("learningPath") + async learningPath( + @Args("planID") planID: string, + @Args("pathID") pathID: string | null = null + ) { + const lps = await this.programService.learningPath(planID); + if (!lps) throw new Error("No learning paths found for inputted user"); + if (pathID !== null && typeof pathID !== "undefined") { + const lp = lps[0].paths.find((path) => path.id === pathID); + if (!lp) throw new Error("No learning path found with inputted ID"); + const payload = { + ...lps[0], + paths: [{ ...lp }] + }; + return [payload]; + } + return lps; + } + // Mutations // Add a section to the db with all required initial fields @@ -158,8 +383,19 @@ export class ProgramResolver { // // Add a Course to the db with a course name @Mutation("addCourse") - async createCourse(@Args("input") args: CourseInput) { - return await this.programService.addCourse(args); + async createCourse( + @Args("input") args: CourseInput[], + @Args("many") many: boolean = false + ) { + if (many && args.length > 1) { + return this.programService.addManyCourses(args); + } else if (args.length === 1) { + return await this.programService.addCourse(args[0]); + } else { + throw new Error( + "Input must be an array of CourseInput objects or a single CourseInput object" + ); + } } // Update a course name @@ -411,4 +647,58 @@ export class ProgramResolver { async deleteContent(@Args("contentID") contentID: string) { return await this.programService.deleteContent(contentID); } + + @Mutation("createLearningPath") + async createLearningPath( + @Args("planID") planID: string, + @Args("input") input: CreateLearningPathInput + ) { + if (!input.path && !input.paths) + throw new Error( + "Either a single path object or a list of path objects is required" + ); + const data = await this.programService.createLearningPath(planID, input); + + if (!data) + return new Error("An error occurred while creating your learning path"); + return data; + } + + @Mutation("createPath") + async createPath( + @Args("planID") planID: string, + @Args("input") input: PathInput + ) { + const data = await this.programService.createPath(planID, input); + if (data instanceof Error) + return new Error("An error occurred while creating your path"); + return data; + } + + @Mutation("updateLearningPath") + async updateLearningPath( + @Args("planID") planID: string, + @Args("pathID") pathID: string, + @Args("input") input: PathInput + ) { + const data = await this.programService.updateLearningPath( + planID, + pathID, + input + ); + if (data instanceof Error) + return new Error("An error occurred while updating your learning path"); + return data; + } + + @Mutation("deleteLearningPath") + async deleteLearningPath( + @Args("planID") planID: string, + @Args("pathID") pathID: string + ) { + const data = await this.programService.deleteLearningPath(planID, pathID); + if (data instanceof Error) + return new Error("An error occurred while deleting your learning path"); + return data; + } } diff --git a/src/program/program.service.ts b/src/program/program.service.ts index 0ffed9f1..b3629052 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -19,11 +19,18 @@ import { CreateContentArgs, ContentFields, NewSection, - CollectionFields + CollectionFields, + CreateLearningPathInput, + PathInput, + Course, + LearningPath, + Section, + Collection } from "@/types/graphql"; import { Injectable } from "@nestjs/common"; import { PrismaService } from "@/prisma.service"; import { Prisma } from "@prisma/client"; +import { faker } from "@faker-js/faker"; @Injectable() export class ProgramService { @@ -141,6 +148,16 @@ export class ProgramService { student: true } }, + moduleProgress: { + include: { + enrollment: { + include: { + section: true + } + }, + module: true + } + }, section: { include: { parentSections: true, @@ -163,7 +180,7 @@ export class ProgramService { enrollment: true } }, - collection: { + collections: { include: { section: true } @@ -188,7 +205,7 @@ export class ProgramService { private moduleInclude = Prisma.validator()({ content: true, - collection: { + collections: { include: { section: { include: { @@ -198,8 +215,41 @@ export class ProgramService { } } } + }, + modules: { + include: { + instructor: { + include: { + account: true + } + }, + moduleProgress: { + include: { + enrollment: true + } + } + } } } + }, + moduleProgress: { + include: { + enrollment: true + } + }, + instructor: { + include: { + account: true, + instructedModules: true + } + } + }); + + private learningPathInclude = Prisma.validator()({ + plan: { + include: { + student: true + } } }); @@ -508,7 +558,7 @@ export class ProgramService { ...(id && { id }), ...(name && { name }), ...(position && { position }), - collection: { id: collection ? collection : undefined }, + collections: { some: { id: collection ? collection : undefined } }, content: content ? { some: { id: content } } : undefined, objectives: objectives ? { hasEvery: objectives } : undefined }); @@ -659,6 +709,25 @@ export class ProgramService { }); } + async addManyCourses(data: CourseInput[]) { + const manyData = Prisma.validator()({ + data: data.map((course) => { + return { + name: course.name, + ...(course.number && { number: course.number }), + ...(course.prefix && { prefix: course.prefix }), + ...(course.carnegieHours && { + carnegieHours: course.carnegieHours + }), + ...(course.required && { required: course.required }) + }; + }) + }); + return this.prisma.course.createMany({ + data: manyData.data + }); + } + async updateCourse(id: string, data: CourseInput) { const { name, required, carnegieHours } = data; return this.prisma.course.update({ @@ -1001,7 +1070,7 @@ export class ProgramService { } } }), - collection: { + collections: { connect: { id: input.collection ? input.collection : undefined } @@ -1065,7 +1134,9 @@ export class ProgramService { }, data: { name: payload.name, - collectionID: payload.collection, + collectionIDs: { + push: payload.collection + }, position: input.position ? input.position : undefined, objectives: newObjectives ? newObjectives : undefined, hours: payload.hours @@ -1163,4 +1234,434 @@ export class ProgramService { } }); } + + async learningPath(planID: string) { + const lps = await this.prisma.learningPath.findMany({ + where: { + planID + }, + include: this.learningPathInclude + }); + + const courses: Array = []; + const modules: Array = []; + const sections: Array = []; + const collections: Array = []; + + lps.map((lp) => { + lp.paths.map((path) => { + courses.push(path.course.id); + path.course.sections.map((section) => { + section.collections.map((collection) => { + collection.modules.map((module) => { + modules.push(module.id); + }); + collections.push(collection.id); + }); + sections.push(section.id); + }); + }); + }); + + const courseIDs = [...new Set(courses)]; + const moduleIDs = [...new Set(modules)]; + const sectionIDs = [...new Set(sections)]; + const collectionIDs = [...new Set(collections)]; + + const coursesData = await this.prisma.course.findMany({ + where: { + id: { + in: courseIDs + } + }, + include: this.courseInclude + }); + + const modulesData = await this.prisma.module.findMany({ + where: { + id: { + in: moduleIDs + } + }, + include: this.moduleInclude + }); + + const sectionsData = await this.prisma.section.findMany({ + where: { + id: { + in: sectionIDs + } + }, + include: this.sectionInclude + }); + + const collectionsData = await this.prisma.collection.findMany({ + where: { + id: { + in: collectionIDs + } + } + }); + + const courseMap = new Map(); + const moduleMap = new Map(); + const sectionMap = new Map(); + const collectionMap = new Map(); + + coursesData.map((course) => { + courseMap.set(course.id, course); + }); + + modulesData.map((module) => { + moduleMap.set(module.id, module); + }); + + sectionsData.map((section) => { + sectionMap.set(section.id, section); + }); + + collectionsData.map((collection) => { + collectionMap.set(collection.id, collection); + }); + + const paths = lps.map((lp) => { + const paths = lp.paths.map((path) => { + const course = courseMap.get(path.course.id) as Course; + return { + ...path, + course: { + ...course, + sections: path.course.sections.map((section) => { + const sect = sectionMap.get(section.id) as Section; + return { + ...sect, + name: sect.sectionName, + collections: section.collections.map((collection) => { + const col = collectionMap.get(collection.id) as Collection; + return { + ...col, + modules: collection.modules.map((module) => { + return { + ...moduleMap.get(module.id) + }; + }) + }; + }) + }; + }) + } + }; + }); + return { + ...lp, + paths + }; + }); + + return paths as Array; + } + + async createLearningPath(planID: string, input: CreateLearningPathInput) { + // validate course ID passed in + // validate section IDs passed in + // validate collection IDs passed in + // validate module IDs passed in + // if all validations pass + // create module enrollment for each module in the learning path + // store the enrollment ID in the module composite type + // create learning path + // else return a validation error + + const { path, paths } = input; + + // handle multiple path creation + if (paths && !path) { + const courses = await this.prisma.course.findMany({ + where: { + id: { + in: paths.map((path) => path.course.id) + } + } + }); + + if (courses.length !== paths.length) { + return new Error("Not all courses could be found"); + } + } + + // handle single path creation + if (path && !paths) { + const course = await this.prisma.course.findUnique({ + where: { + id: path.course.id + } + }); + + if (!course) { + return new Error("Course not found"); + } + + const pathData = Prisma.validator()({ + plan: { + connect: { + id: planID + } + }, + paths: { + id: faker.database.mongodbObjectId(), + learningOutcomes: path.learningOutcomes || [], + hoursSatisfies: path.hoursSatisfies || 1, + course: { + id: path.course.id, + sections: path.course.sections.map((section) => { + return { + id: section.id, + name: section.name, + collections: section.collections.map((collection) => { + return { + id: collection.id, + name: collection.name, + modules: collection.modules.map((module) => { + return { + id: module.id, + enrollmentID: "" + }; + }) + }; + }) + }; + }) + } + } + }); + + return this.prisma.learningPath.create({ + data: pathData, + include: this.learningPathInclude + }); + } + } + + async createPath(planID: string, input: PathInput) { + // get base LP data so we can persist the old paths and add the new one + const LP = await this.prisma.learningPath.findMany({ + where: { + planID + } + }); + + if (LP.length !== 1) { + return new Error("The learning path you choose could not be found"); + } + + const { status, course, learningOutcomes, hoursSatisfies } = input; + + // getting the course data so we can validate and populate in the path + const courseData = await this.prisma.course.findMany({ + where: { + id: course.id + } + }); + + if (courseData.length !== 1) { + return new Error("The course you choose could not be found"); + } + + const pathData = Prisma.validator()({ + id: faker.database.mongodbObjectId(), + learningOutcomes: learningOutcomes || [], + hoursSatisfies: hoursSatisfies || 1, + status: status || "DRAFT", + course: { + id: course.id, + sections: course.sections.map((section) => { + return { + id: section.id, + name: section.name, + collections: section.collections.map((collection) => { + return { + id: collection.id, + name: collection.name, + modules: collection.modules.map((module) => { + return { + id: module.id, + enrollmentID: "" + }; + }) + }; + }) + }; + }) + } + }); + + const paths = [...LP[0].paths, pathData]; + + return this.prisma.learningPath.update({ + where: { + planID + }, + data: { + paths: paths + }, + include: this.learningPathInclude + }); + } + + async updateLearningPath(planID: string, pathID: string, input: PathInput) { + const { status, course, learningOutcomes, hoursSatisfies } = input; + + const courseData = await this.prisma.course.findMany({ + where: { + id: course.id + } + }); + + if (courseData.length !== 1) { + return new Error("The course you choose could not be found"); + } + + // needed to include the select statement here since we only want to update those two fields + const lpData = await this.prisma.learningPath.findMany({ + where: { + planID + }, + select: { + createdAt: true, + paths: true + } + }); + + if (lpData.length !== 1) { + return new Error("The learning path you choose could not be found"); + } + + const pathData = lpData[0].paths.filter((path) => path.id === pathID)[0]; + + // by referencing the pathData here, we are able to save the old data that was present in the path + let pathPayload = { + ...pathData, + ...(hoursSatisfies && { hoursSatisfies }), + ...(learningOutcomes && { learningOutcomes }), + ...(status && { status }) + } as Prisma.PathCreateInput; + + const updatedSectionsArray = course.sections.map( + (section, sectionIndex) => { + if (section.id !== pathData.course.sections[sectionIndex].id) { + return { + id: section.id, + name: section.name, + collections: [] + }; + } else { + const updatedCollectionsArray = section.collections.map( + (collection, collectionIndex) => { + if ( + collection.id !== + pathData.course.sections[sectionIndex].collections[ + collectionIndex + ].id + ) { + return { + id: collection.id, + name: collection.name, + modules: [] + }; + } else { + const updatedModulesArray = collection.modules.map( + (module, moduleIndex) => { + if ( + module.id !== + pathData.course.sections[sectionIndex].collections[ + collectionIndex + ].modules[moduleIndex].id + ) { + return { + id: module.id, + enrollmentID: "" + }; + } else { + return { + id: module.id, + enrollmentID: + pathData.course.sections[sectionIndex].collections[ + collectionIndex + ].modules[moduleIndex].enrollmentID + }; + } + } + ); + return { + id: collection.id, + name: collection.name, + modules: updatedModulesArray + }; + } + } + ); + return { + id: section.id, + name: section.name, + collections: updatedCollectionsArray + }; + } + } + ); + + pathPayload = { + ...pathPayload, + course: { + id: courseData[0].id, + sections: updatedSectionsArray + } + }; + + // we map over the existing learning paths and replace the one we are updating with the new data but keeping the old ones + const lpPayload = { + ...lpData[0], + paths: lpData[0].paths.map((path) => { + if (path.id !== pathID) { + return path; + } else { + return pathPayload; + } + }) + } as Prisma.LearningPathUpdateInput; + + return this.prisma.learningPath.update({ + where: { + planID + }, + data: lpPayload + }); + } + + async deleteLearningPath(planID: string, pathID: string) { + const lpData = await this.prisma.learningPath.findMany({ + where: { + planID + }, + select: { + createdAt: true, + paths: true + } + }); + + if (lpData.length !== 1) { + return new Error("The learning path you choose could not be found"); + } + + const lpPayload = { + ...lpData[0], + paths: lpData[0].paths.filter((path) => path.id !== pathID) + } as Prisma.LearningPathUpdateInput; + + return this.prisma.learningPath.update({ + where: { + planID + }, + data: lpPayload + }); + } } diff --git a/src/program/schema.graphql b/src/program/schema.graphql index c4aa1a94..23e54bdf 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -1,31 +1,31 @@ enum UserRole { - STUDENT - TEACHER - GRADER + STUDENT + TEACHER + GRADER } enum EnrollmentStatus { - ACTIVE - INACTIVE + ACTIVE + INACTIVE } enum ContentType { - PDF - DOC - DOCX - VIDEO - CAPTION - TRANSCRIPT - QUIZ - TEXT + PDF + DOC + DOCX + VIDEO + CAPTION + TRANSCRIPT + QUIZ + TEXT } enum FileType { - DOCX - DOC - PDF - TTT - TXT + DOCX + DOC + PDF + TTT + TXT } type SectionEnrollment { @@ -39,25 +39,25 @@ type SectionEnrollment { """ enrolledAt: Date! - """ - Role of the user:Either Student, Teacher or Grader - """ - role: UserRole! + """ + Role of the user:Either Student, Teacher or Grader + """ + role: UserRole! - """ - Status of the enrollment: Either Active or Inactive - """ - status: EnrollmentStatus! + """ + Status of the enrollment: Either Active or Inactive + """ + status: EnrollmentStatus! """ Current Section """ section: Section! - """ - Plan of study Chosen - """ - plan: PlanOfStudy + """ + Plan of study Chosen + """ + plan: PlanOfStudy """ Inactive plan of study @@ -74,97 +74,97 @@ type SectionEnrollment { } type AssignmentResult { - """ - id of AssignmentResult - """ - id: ID! + """ + id of AssignmentResult + """ + id: ID! - """ - Time at which the Assignment Result is given - """ - submittedAt: Date! + """ + Time at which the Assignment Result is given + """ + submittedAt: Date! - """ - result of the Assignment - """ - result: Float! + """ + result of the Assignment + """ + result: Float! - """ - feedback given on the Assignment - """ - feedback: String + """ + feedback given on the Assignment + """ + feedback: String - """ - s3 id of the submitted file - """ - submissionURL: String + """ + s3 id of the submitted file + """ + submissionURL: String - """ - File type that the submission is in - """ - fileType: String + """ + File type that the submission is in + """ + fileType: String - """ - Assignment Result of the student - """ - student: PlanOfStudy + """ + Assignment Result of the student + """ + student: PlanOfStudy - """ - Assignment graded by the User - """ - gradedBy: User + """ + Assignment graded by the User + """ + gradedBy: User - """ - Assignment to which result is given - """ - assignment: Assignment + """ + Assignment to which result is given + """ + assignment: Assignment } type Assignment { - """ - Assignment id - """ - id: ID! + """ + Assignment id + """ + id: ID! - """ - date and time at which assignment is updated - """ - updatedAt: Date! + """ + date and time at which assignment is updated + """ + updatedAt: Date! - """ - Name of the assignment - """ - name: String! + """ + Name of the assignment + """ + name: String! - """ - due of the assignment - """ - dueAt: Date + """ + due of the assignment + """ + dueAt: Date - """ - s3 id of the content - """ - contentURL: String + """ + s3 id of the content + """ + contentURL: String - """ - File type that the assignment is submitted in - """ - contentType: String + """ + File type that the assignment is submitted in + """ + contentType: String - """ - Accepted file types for assignments - """ - acceptedTypes: FileType + """ + Accepted file types for assignments + """ + acceptedTypes: FileType """ Assignment belonging to the Section """ section: Section! - """ - Assignment Result - """ - assignmentResults: [AssignmentResult] + """ + Assignment Result + """ + assignmentResults: [AssignmentResult] } type SectionFeedback { @@ -178,10 +178,10 @@ type SectionFeedback { """ feedback: String! - """ - Rating given in the feedback - """ - rating: Int! + """ + Rating given in the feedback + """ + rating: Int! """ Section feedback given by the student @@ -203,6 +203,8 @@ type Course { Name of the course """ name: String! + number: Int + prefix: String """ Sections in the course """ @@ -302,38 +304,40 @@ type Section { """ collections: [Collection!]! - """ - courseIDs set - """ - courseIDs: [ID!]! + """ + courseIDs set + """ + courseIDs: [ID!]! } type Collection { - """ - id of the collection - """ - id: ID! + """ + id of the collection + """ + id: ID! - """ - Name of the collection - """ - name: String! + """ + Name of the collection + """ + name: String! - """ - Date and Time of the collection created at - """ - createdAt: Date! + """ + Date and Time of the collection created at + """ + createdAt: Date! - """ - Date and Time of the collection updated at - """ - updatedAt: Date! + """ + Date and Time of the collection updated at + """ + updatedAt: Date! """ List of modules in the collection """ modules: [Module] + moduleIDs: [ID!] + """ The Section that this collection belongs to """ @@ -353,6 +357,8 @@ type Module { document ID """ id: ID! + prefix: String + number: Int """ Module Name """ @@ -368,7 +374,9 @@ type Module { """ The collection that the module belongs to """ - collection: Collection + collections: [Collection] + + collectionIDs: [ID!] """ The position index of the module in the collection it belongs to. @@ -386,27 +394,30 @@ type Module { A list of learning objectives being covered in this Section """ objectives: [String!]! - """ + """ The number of carnige hours granted for completion of this Section """ hours: Float! + description: String + instructor: InstructorProfile + instructorID: ID } type Content { - """ - Document ID for this content - """ - id: ID! + """ + Document ID for this content + """ + id: ID! - """ - The type of content that this link/id references - """ - type: ContentType! + """ + The type of content that this link/id references + """ + type: ContentType! - """ - The URL or other access point for the content being referenced. This will typically be an AWS s3 bucket ID - """ - link: String! + """ + The URL or other access point for the content being referenced. This will typically be an AWS s3 bucket ID + """ + link: String! """ The module that this content belongs to. @@ -421,10 +432,10 @@ type Content { } type Error { - """ - Error message - """ - message: String + """ + Error message + """ + message: String } type Query { @@ -435,17 +446,17 @@ type Query { """ section(input: SectionFields!, memberRole: UserRole): [Section!] - """ - Get a list of courses given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. - Parameters can be any of the fields that exist in a course - """ - course(input: CourseFields!): [Course!] + """ + Get a list of courses given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. + Parameters can be any of the fields that exist in a course + """ + course(input: CourseFields!): [Course!] - """ - Get a list of assignments given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. - Parameters can be any of the fields that exist in a assignment - """ - assignment(input: AssignmentFields!): [Assignment!] + """ + Get a list of assignments given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. + Parameters can be any of the fields that exist in a assignment + """ + assignment(input: AssignmentFields!): [Assignment!] """ Get a list of SectionFeedbacks given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. @@ -453,11 +464,11 @@ type Query { """ sectionFeedback(input: ModFeedbackFields!): [SectionFeedback!] - """ - Get a list of assignmentResults given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. - Parameters can be any of the fields that exist in a assignmentResult - """ - assignmentResult(input: AssignmentResFields!): [AssignmentResult!] + """ + Get a list of assignmentResults given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. + Parameters can be any of the fields that exist in a assignmentResult + """ + assignmentResult(input: AssignmentResFields!): [AssignmentResult!] """ Get a list of SectionEnrollments given a set of parameters, passing an empty object in will recieve all and passing in an id will fetch a single document. @@ -467,7 +478,7 @@ type Query { """ Get a list of modules given the plan ID and enrollment ID """ - modulesBySectionEnrollment(planID: ID!, SectionID: ID!) : [Module!] + modulesBySectionEnrollment(planID: ID!, sectionID: ID!) : [Module!] """ Retrieve a specific collection based on document ID """ @@ -477,22 +488,346 @@ type Query { """ module(input: ModuleFields): [Module!] - """ - Retrieve Content Given a set of parameters - """ - content(input: ContentFields): [Content!] + """ + Retrieve Content Given a set of parameters + """ + content(input: ContentFields): [Content!] + """ + Retrieve a sepcific learning path based on the user's plan of study ID + """ + learningPath(planID: ID!, pathID: ID): [LearningPath!]! + + latestModuleProgress(planID: ID!, sectionID: ID!, moduleID: ID!): ModuleProgress + + modulesFromLearningPath(planID: ID!): [Module!] + + moduleFlowFromLearningPath(planID: ID!, moduleID: ID!): ModuleFlow +} + +type ModuleFlow { + previousModule: Module + previousCollection: Collection + nextModule: Module + nextCollection: Collection + currentModule: Module + currentCollection: Collection + currentSection: Section +} + +type SimpleLearningPath { + id: ID! + createdAt: Date! + plan: PlanOfStudy! + planID: ID! + paths: [SimplePath!]! +} + +type SimplePath { + id: ID! + createdAt: Date! + updatedAt: Date! + course: SimpleCoursePath! + status: PathStatus! + hoursSatisfies: Float! + learningOutcomes: [String!]! +} + +type SimpleCoursePath { + id: ID! + sections: [SimpleSection!]! +} + +type SimpleSection { + id: ID! + name: String! + collections: [SimpleCollection!]! +} + +type SimpleCollection { + """ + List of modules in the collection + """ + modules: [SimpleModule!]! + +""" +id of the collection +""" +id: ID! + +""" +Name of the collection +""" +name: String! + +""" +Date and Time of the collection created at +""" +createdAt: Date! + +""" +Date and Time of the collection updated at +""" +updatedAt: Date! + +""" +The Section that this collection belongs to +""" +section: Section! +""" +The Section's unique ID that this collection belongs to +""" +sectionID: ID! +""" +The position index of the collection in a Section +""" +position: Int +} + +type SimpleModule { + id: ID! + name: String! + collections: [SimpleCollection!]! +} + +type LearningPath { + id: ID! + createdAt: Date! + plan: PlanOfStudy! + planID: ID! + paths: [Path!]! +} + +type Path { + id: ID! + createdAt: Date! + updatedAt: Date! + course: CoursePath! + status: PathStatus! + hoursSatisfies: Float! + learningOutcomes: [String!]! +} + +enum PathStatus { + DRAFT + LIVE +} + +type CoursePath { + """ + course id + """ + id: ID! + """ + Name of the course + """ + name: String! + prefix: String + number: Int + + """ + Boolean value to check if the course is required/core or an elective + """ + required: Boolean! + """ + Number of Carnegie Unit and Student Hours that the course is worth + """ + carnegieHours: Int! + sections: [SectionPath!]! +} + +type SectionPath { + name: String! + """ + A list of collections that have this Section's materials + """ + collections: [CollectionPath!]! +""" +Section Id +""" +id: ID! + +""" +Number of the Section +""" +sectionNumber: Int! + +""" +Name of hte Section +""" +sectionName: String! + +""" +Description of the Section +""" +description: String! + +""" +Duration of the Section +""" +duration: Float! + +""" +Introduction of the Section +""" +intro: String! + +""" +No of slides in the Section +""" +numSlides: Int! + +""" +Keywords in the Sections +""" +keywords: [String!]! + +""" +objectives in the Sections +""" +objectives: [String!]! + +""" +Date and Time of the Section Created at +""" +createdAt: Date! + +""" +Date and Time of the Section Updated at +""" +updatedAt: Date! + +""" +Assignement in the Section +""" +assignments: [Assignment!]! + +""" +Memebers enrolled in the Section +""" +members: [SectionEnrollment!]! + +""" +Feedback of the Section +""" +feedback: [SectionFeedback!]! + +""" +Parent Sections of the Section +""" +parentSections: [Section!]! + +""" +Child Sections in the Section +""" +subSections: [Section!]! +""" +courseIDs set +""" +courseIDs: [ID!]! +} + +type CollectionPath { + id: ID! + name: String! + modules: [ModulePath!]! +} + +type ModulePath { + """ + document ID + """ + id: ID! + """ + Module Name + """ + name: String! + number: Int + prefix: String + """ + The link/id of the actual content data itself + """ + content: [Content] + """ + The list of threads related to the module + """ + threads: [Thread] + """ + The collection that the module belongs to + """ + collections: [Collection] + + collectionIDs: [ID!] + + """ + The position index of the module in the collection it belongs to. + """ + position: Int + """ + The quizzes that belong to this module + """ + quizzes: [Quiz!] + """ + The progress model that is associated with this module + """ + moduleProgress: [ModuleProgress] + """ + A list of learning objectives being covered in this Section + """ + objectives: [String!]! + """ + The number of carnige hours granted for completion of this Section + """ + hours: Float! + enrollmentID: ID + description: String + instructor: InstructorProfile + instructorID: ID +} + +input CreateLearningPathInput { + path: PathInput + paths: [PathInput!] +} + +input PathInput { + course: CoursePathInput! + status: PathStatus + hoursSatisfies: Float + learningOutcomes: [String!] +} + +input CoursePathInput { + id: ID! + sections: [SectionPathInput!]! +} + +input SectionPathInput { + id: ID! + name: String! + collections: [CollectionPathInput!]! +} + +input CollectionPathInput { + id: ID! + name: String! + modules: [ModulePathInput!]! +} + +input ModulePathInput { + id: ID! + enrollmentID: ID } input CreateContentArgs { - """ - The type of content that this link/id references - """ - type: ContentType! + """ + The type of content that this link/id references + """ + type: ContentType! - """ - The URL or other access point for the content being referenced. This will typically be an AWS s3 bucket ID - """ - link: String! + """ + The URL or other access point for the content being referenced. This will typically be an AWS s3 bucket ID + """ + link: String! """ The module that this content belongs to. @@ -506,20 +841,20 @@ input CreateContentArgs { } input ContentFields { - """ - Document ID for this content - """ - id: ID + """ + Document ID for this content + """ + id: ID - """ - The type of content that this link/id references - """ - type: ContentType + """ + The type of content that this link/id references + """ + type: ContentType - """ - The URL or other access point for the content being referenced. This will typically be an AWS s3 bucket ID - """ - link: String + """ + The URL or other access point for the content being referenced. This will typically be an AWS s3 bucket ID + """ + link: String """ The module that this content belongs to. @@ -568,49 +903,51 @@ input SectionFields { input CourseFields { id: ID name: String + number: Int + prefix: String section: ID required: Boolean carnegieHours: Int } input AssignmentFields { - id: ID - updatedAt: Date - name: String - dueAt: Date - contentURL: String - contentType: String - acceptedTypes: FileType + id: ID + updatedAt: Date + name: String + dueAt: Date + contentURL: String + contentType: String + acceptedTypes: FileType section: ID assignmentResult: ID } input ModFeedbackFields { - id: ID - feedback: String - rating: Int + id: ID + feedback: String + rating: Int student: ID section: ID } input AssignmentResFields { - id: ID - submittedAt: Date - result: Float - feedback: String - submissionURL: String - fileType: String - - student: ID - gradedBy: ID - assignment: ID + id: ID + submittedAt: Date + result: Float + feedback: String + submissionURL: String + fileType: String + + student: ID + gradedBy: ID + assignment: ID } input ModEnrollmentFields { - id: ID - enrolledAt: Date + id: ID + enrolledAt: Date role: UserRole section: ID @@ -734,18 +1071,20 @@ input CourseInput { The name of the given Course """ name: String! + number: Int + prefix: String """ Additional section to be related to the course """ - section: ID! + section: ID """ The boolean attribute to decide weather the course is part of core courses or electives """ - required: Boolean! + required: Boolean """ The Carnegie Hours of the Course """ - carnegieHours: Int! + carnegieHours: Int } input SectionFeedbackInput { @@ -771,30 +1110,30 @@ input SectionFeedbackUpdate { } input NewAssignmentResult { - """ - Relating assignment result to assignment - """ - assignment: ID! - """ - Relating Assignment Result to Student - """ - student: ID! - """ - Relating Assignment Result to Grader - """ - grader: ID! - """ - result of the NewAssignement - """ - result: Float! - """ - s3 id of the submitted file - """ - submissionURL: String! - """ - File type that the submission is in - """ - fileType: String! + """ + Relating assignment result to assignment + """ + assignment: ID! + """ + Relating Assignment Result to Student + """ + student: ID! + """ + Relating Assignment Result to Grader + """ + grader: ID! + """ + result of the NewAssignement + """ + result: Float! + """ + s3 id of the submitted file + """ + submissionURL: String! + """ + File type that the submission is in + """ + fileType: String! } input SectionEnrollmentInput { @@ -821,6 +1160,8 @@ input ModuleInput { Module Name """ name: String! + prefix: String + number: Int """ The link/id of the actual content data itself """ @@ -842,6 +1183,8 @@ input ModuleInput { The number of carnige hours granted for completing this Section """ hours: Float! + description: String + instructor: ID } input ModuleFields { @@ -853,6 +1196,8 @@ input ModuleFields { Module Name """ name: String + number: Int + prefix: String """ The link/id of the actual content data itself """ @@ -877,6 +1222,8 @@ input ModuleFields { The number of carnige hours granted for completing this Section """ hours: Float + description: String + instructor: ID } type Mutation { @@ -893,18 +1240,18 @@ type Mutation { """ updateSection(input: UpdateSection): Section - """ - deletes all Courses referring id - """ - deleteCourse(id: ID!): Course - """ - adds Courses - """ - addCourse(input: CourseInput): Course! - """ - Updates Courses - """ - updateCourse(id: ID!, input: CourseInput): Course + """ + deletes all Courses referring id + """ + deleteCourse(id: ID!): Course + """ + adds Courses to the database. If many is true, it will add all the courses in the array and if false, the input will still need to be an array but with only one element + """ + addCourse(input: [CourseInput!], many: Boolean): Course! + """ + Updates Courses + """ + updateCourse(id: ID!, input: CourseInput): Course """ adds assignement in Section @@ -984,15 +1331,15 @@ type Mutation { """ unpairCourseSection(courseId: ID!, sectionId: ID!): Section - """ - Create a new collection - """ - createCollection(data: CreateCollectionArgs!): Collection! + """ + Create a new collection + """ + createCollection(data: CreateCollectionArgs!): Collection! - """ - Update a collection given its ID and the new data to update - """ - updateCollection(id: ID!, data: CollectionFields!): Collection! + """ + Update a collection given its ID and the new data to update + """ + updateCollection(id: ID!, data: CollectionFields!): Collection! """ Create a Module for an individual collection @@ -1009,18 +1356,34 @@ type Mutation { deleteModule(id: String!): Module - """ - Create a Content Record - """ - createContent(input: CreateContentArgs!): Content! + """ + Create a Content Record + """ + createContent(input: CreateContentArgs!): Content! - """ - Update a content Record - """ - updateContent(input: ContentFields!): [Content!] + """ + Update a content Record + """ + updateContent(input: ContentFields!): [Content!] - """ - Delete an existing content record - """ - deleteContent(contentID: ID!): Content + """ + Delete an existing content record + """ + deleteContent(contentID: ID!): Content + """ + Create a new learning path model with the given program structure + """ + createLearningPath(planID: ID!, input: CreateLearningPathInput!): LearningPath! + """ + Add a new path to an existing learning path model + """ + createPath(planID: ID!, input: PathInput!): SimpleLearningPath! + """ + Update an existing learning path with the given program structure + """ + updateLearningPath(planID: ID!, pathID: ID!, input: PathInput!): LearningPath! + """ + Delete an existing learning path + """ + deleteLearningPath(planID: ID!, pathID: ID!): LearningPath! } diff --git a/src/user/user.resolver.ts b/src/user/user.resolver.ts index 43ab230f..a89483a5 100644 --- a/src/user/user.resolver.ts +++ b/src/user/user.resolver.ts @@ -4,7 +4,8 @@ import { SocialInput, UserFields, SocialFields, - User + User, + NewUser } from "@/types/graphql"; import { UserService } from "./user.service"; import { Prisma } from "@prisma/client"; @@ -44,6 +45,11 @@ export class UserResolver { else return await this.userService.instructorProfile(usr[0].id); } + @Mutation("createUser") + async create(@Args("input") args: NewUser) { + return await this.userService.createUser(args); + } + @Mutation("updateUser") async update(@Args("input") args: UpdateUser) { return await this.userService.updateUser(args); diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 1d4c8539..198749f7 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -7,7 +7,8 @@ import type { InstructorProfile, Error, UserFields, - SocialFields + SocialFields, + NewUser } from "@/types/graphql"; import moment from "moment"; import { Prisma } from "@prisma/client"; @@ -382,4 +383,12 @@ export class UserService { } }); } + + async createUser(args: NewUser) { + return this.prisma.user.create({ + data: { + ...args + } + }); + } } diff --git a/utils/fakes.ts b/utils/fakes.ts index a1afe4d7..ef754d45 100644 --- a/utils/fakes.ts +++ b/utils/fakes.ts @@ -32,7 +32,11 @@ export function createRandomUser(): User { isActive: faker.datatype.boolean(), createdAt: faker.date.past(), middleName: faker.name.middleName(), - watchedThreadIDs: [faker.database.mongodbObjectId()] + watchedThreadIDs: [faker.database.mongodbObjectId()], + biography: faker.lorem.lines(1), + phoneNumber: faker.phone.phoneNumber(), + upvotedThreadIDs: [faker.database.mongodbObjectId()], + groupMembershipIDs: [faker.database.mongodbObjectId()] }; } @@ -44,13 +48,10 @@ export function createRandomInstructorProfile( accountID: userAccountID ? userAccountID : faker.database.mongodbObjectId(), background: faker.lorem.lines(1), contactPolicy: faker.lorem.lines(1), - officeHours: faker.lorem.lines(1), + officeHours: [faker.lorem.lines(1)], officeLocation: faker.lorem.lines(1), - personalWebsite: faker.lorem.lines(1), - philosophy: faker.lorem.lines(1), - phone: faker.lorem.lines(1), - researchInterest: faker.lorem.lines(1), - selectedPapersAndPublications: faker.lorem.lines(1), + researchInterest: [faker.lorem.lines(1)], + selectedPapersAndPublications: [faker.lorem.lines(1)], title: faker.lorem.lines(1) }; } @@ -78,7 +79,11 @@ export function createRandomCourse(): Course { return { id: faker.database.mongodbObjectId(), sectionIDs: [faker.database.mongodbObjectId()], - name: faker.lorem.words(3) + name: faker.lorem.words(3), + carnegieHours: faker.datatype.number(), + number: faker.datatype.number(), + required: faker.datatype.boolean(), + prefix: faker.lorem.words(3) }; } @@ -104,12 +109,17 @@ export function createRandomSection(): Section { export function createRandomModule(collectionID?: string): Module { return { id: faker.database.mongodbObjectId(), - collectionID: collectionID - ? collectionID - : faker.database.mongodbObjectId(), + collectionIDs: collectionID + ? [collectionID] + : [faker.database.mongodbObjectId()], name: faker.lorem.words(3), position: faker.datatype.number(), - transcript: faker.lorem.lines(5) + number: faker.datatype.number(), + description: faker.lorem.lines(1), + objectives: [faker.lorem.words(3)], + prefix: faker.lorem.words(3), + hours: faker.datatype.number(), + instructorProfileID: faker.database.mongodbObjectId() }; } @@ -120,7 +130,8 @@ export function createRandomCollection(sectionID?: string): Collection { sectionID: sectionID ? sectionID : faker.database.mongodbObjectId(), name: faker.lorem.words(3), position: faker.datatype.number(), - updatedAt: faker.date.past() + updatedAt: faker.date.past(), + moduleIDs: [faker.database.mongodbObjectId()] }; } @@ -144,7 +155,10 @@ export function createRandomAssignment(sectionID?: string): Assignment { id: faker.database.mongodbObjectId(), sectionId: sectionID ? sectionID : faker.database.mongodbObjectId(), name: faker.lorem.words(3), - updatedAt: faker.date.past() + updatedAt: faker.date.past(), + contentURL: faker.internet.url(), + contentType: faker.helpers.arrayElement(["QUIZ", "TEXT", "VIDEO"]), + acceptedTypes: faker.helpers.arrayElement(["DOC", "DOCX", "PDF"]) }; } @@ -162,7 +176,9 @@ export function createRandomAssignmentResult( studentId: studentID ? studentID : faker.database.mongodbObjectId(), result: faker.datatype.number(), feedback: faker.lorem.lines(1), - submittedAt: faker.date.past() + submittedAt: faker.date.past(), + fileType: faker.helpers.arrayElement(["DOC", "DOCX", "PDF"]), + submissionURL: faker.internet.url() }; } @@ -175,18 +191,16 @@ export function createRandomThread( return { id: faker.database.mongodbObjectId(), authorID: authorID ? authorID : faker.database.mongodbObjectId(), - parentModuleID: parentModuleID - ? parentModuleID - : faker.database.mongodbObjectId(), parentThreadID: parentThreadID ? parentThreadID : faker.database.mongodbObjectId(), watcherID: watcherID ? [watcherID] : [faker.database.mongodbObjectId()], title: faker.lorem.words(3), body: faker.lorem.lines(1), - upvotes: faker.datatype.number(), createdAt: faker.date.past(), - updatedAt: faker.date.past() + updatedAt: faker.date.past(), + upvoteUserIDs: [faker.database.mongodbObjectId()], + topics: [faker.lorem.words(3)] }; } @@ -194,13 +208,14 @@ export function createRandomContent(parentID?: string): Content { return { id: faker.database.mongodbObjectId(), parentID: parentID ? parentID : faker.database.mongodbObjectId(), - type: faker.helpers.arrayElement(["TEXT", "IMAGE", "VIDEO", "AUDIO"]), - link: faker.internet.url() + type: faker.helpers.arrayElement(["TEXT", "VIDEO", "QUIZ"]), + link: faker.internet.url(), + primary: faker.datatype.boolean() }; } export function createRandomQuiz(parentID?: string): Quiz { - const questions = faker.datatype.number({min: 5, max:15}) + const questions = faker.datatype.number({ min: 5, max: 15 }); return { id: faker.database.mongodbObjectId(), totalPoints: questions, @@ -210,18 +225,19 @@ export function createRandomQuiz(parentID?: string): Quiz { numQuestions: questions, minScore: faker.datatype.number(), parentModuleID: parentID ? parentID : faker.database.mongodbObjectId() - } + }; } export function createRandomQuestion(quizID?: string): Question { return { id: faker.database.mongodbObjectId(), number: faker.datatype.number(), - variant: faker.datatype.number({min: 1, max: 5}), - parentID: quizID ?quizID : faker.database.mongodbObjectId(), + variant: faker.datatype.number({ min: 1, max: 5 }), + parentID: quizID ? quizID : faker.database.mongodbObjectId(), text: faker.lorem.words(5), points: faker.datatype.number(), - } + instanceIDs: [faker.database.mongodbObjectId()] + }; } export function createRandomAnswer(questionID?: string): Answer { @@ -232,7 +248,7 @@ export function createRandomAnswer(questionID?: string): Answer { weight: faker.datatype.number(), index: faker.datatype.string(1), parentQuestionID: questionID ? questionID : faker.database.mongodbObjectId() - } + }; } export function createRandomProgress( diff --git a/utils/tests.ts b/utils/tests.ts index 6960943a..9d0cc1e2 100644 --- a/utils/tests.ts +++ b/utils/tests.ts @@ -58,7 +58,8 @@ export const createModule = async ( ) => { const data: ModuleInput = { name: config.name, - collection: config.collectionID + collection: config.collectionIDs[0], + hours: config.hours }; const module = await resolver.createModule({ ...data }); if (data) return module; diff --git a/yarn.lock b/yarn.lock index 21fba23c..0570c76b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1196,12 +1196,12 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== -"@prisma/client@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.0.0.tgz#ed2f46930a1da0d8ae88d7965485973576b04270" - integrity sha512-g1h2OGoRo7anBVQ9Cw3gsbjwPtvf7i0pkGxKeZICtwkvE5CZXW+xZF4FZdmrViYkKaAShbISL0teNpu9ecpf4g== +"@prisma/client@^4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.13.0.tgz#271d2b9756503ea17bbdb459c7995536cf2a6191" + integrity sha512-YaiiICcRB2hatxsbnfB66uWXjcRw3jsZdlAVxmx0cFcTc/Ad/sKdHCcWSnqyDX47vAewkjRFwiLwrOUjswVvmA== dependencies: - "@prisma/engines-version" "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11" + "@prisma/engines-version" "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" "@prisma/debug@3.10.0": version "3.10.0" @@ -1240,20 +1240,20 @@ terminal-link "2.1.1" undici "3.3.6" -"@prisma/engines-version@3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11": - version "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11.tgz#4b5efe5eee2feef12910e4627a572cd96ed83236" - integrity sha512-PiZhdD624SrYEjyLboI0X7OugNbxUzDJx9v/6ldTKuqNDVUCmRH/Z00XwDi/dgM4FlqOSO+YiUsSiSKjxxG8cw== +"@prisma/engines-version@4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a": + version "4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.13.0-50.1e7af066ee9cb95cf3a403c78d9aab3e6b04f37a.tgz#ae338908d11685dee50e7683502d75442b955bf9" + integrity sha512-fsQlbkhPJf08JOzKoyoD9atdUijuGBekwoOPZC3YOygXEml1MTtgXVpnUNchQlRSY82OQ6pSGQ9PxUe4arcSLQ== "@prisma/engines@3.11.0-48.b371888aaf8f51357c7457d836b86d12da91658b": version "3.11.0-48.b371888aaf8f51357c7457d836b86d12da91658b" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.11.0-48.b371888aaf8f51357c7457d836b86d12da91658b.tgz#4c1093f7b24c433a9cef2e60f13f57f7c89b5cfa" integrity sha512-m9iZd5F5vP6A2IvKWfHpOO/qK8OOO9nbsV/pdyEkF/1WNe0E8SIWFBKb+HcMLkG9OFbDDBy8QItXmp/mIULuwQ== -"@prisma/engines@3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11": - version "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11.tgz#82f0018153cffa05d61422f9c0c7b0479b180f75" - integrity sha512-u/rG4lDHALolWBLr3yebZ+N2qImp3SDMcu7bHNJuRDaYvYEXy/MqfNRNEgd9GoPsXL3gofYf0VzJf2AmCG3YVw== +"@prisma/engines@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.13.0.tgz#582a6b90b6efeb0f465984f1fe0e72a4afaaa5ae" + integrity sha512-HrniowHRZXHuGT9XRgoXEaP2gJLXM5RMoItaY2PkjvuZ+iHc0Zjbm/302MB8YsPdWozAPHHn+jpFEcEn71OgPw== "@prisma/fetch-engine@3.11.0-48.b371888aaf8f51357c7457d836b86d12da91658b": version "3.11.0-48.b371888aaf8f51357c7457d836b86d12da91658b" @@ -7105,12 +7105,12 @@ prisma-dbml-generator@^0.9.1: "@prisma/generator-helper" "3.11.0" "@prisma/sdk" "3.11.0" -prisma@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.0.0.tgz#4ddb8fcd4f64d33aff8c198a6986dcce66dc8152" - integrity sha512-Dtsar03XpCBkcEb2ooGWO/WcgblDTLzGhPcustbehwlFXuTMliMDRzXsfygsgYwQoZnAUKRd1rhpvBNEUziOVw== +prisma@^4.13.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.13.0.tgz#0b83f40acf50cd47d7463a135c4e9b275713e602" + integrity sha512-L9mqjnSmvWIRCYJ9mQkwCtj4+JDYYTdhoyo8hlsHNDXaZLh/b4hR0IoKIBbTKxZuyHQzLopb/+0Rvb69uGV7uA== dependencies: - "@prisma/engines" "3.16.0-49.da41d2bb3406da22087b849f0e911199ba4fbf11" + "@prisma/engines" "4.13.0" process-nextick-args@~2.0.0: version "2.0.1" From f598933d118f53b0b261f57c51d6559c4dcef6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Mon, 1 May 2023 15:55:27 -0400 Subject: [PATCH 23/31] refactor(modules): added return type to resolver I was also able to fix the previous collection being null or returning the current collection by default --- gql/graphql.ts | 10 +++++++ src/program/program.resolver.ts | 53 +++++++++++++++++++++++++-------- src/program/schema.graphql | 10 +++++++ 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index af77ddb9..74eb2ab9 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -800,6 +800,16 @@ export interface ModuleFlow { currentSection?: Nullable
; } +export interface SimpleModuleFlow { + previousModule?: Nullable; + previousCollection?: Nullable; + nextModule?: Nullable; + nextCollection?: Nullable; + currentModule?: Nullable; + currentCollection?: Nullable; + currentSection?: Nullable; +} + export interface SimpleLearningPath { id: string; createdAt: Date; diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index 22dae73f..fda4ca27 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -22,7 +22,9 @@ import { CreateLearningPathInput, PathInput, Module, - ModuleFlow + SimpleModuleFlow, + CollectionPath, + SectionPath } from "@/types/graphql"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { ProgramService } from "./program.service"; @@ -234,7 +236,7 @@ export class ProgramResolver { async moduleFlowFromLearningPath( @Args("planID") planID: string, @Args("moduleID") moduleID: string - ) { + ): Promise { // get lp from plan const learningPath = await this.programService.learningPath(planID); if (!learningPath) @@ -244,7 +246,8 @@ export class ProgramResolver { const filteredLearningPath = learningPath[0].paths.filter((path) => { return path.status === "LIVE"; }); - if (filteredLearningPath.length === 0) return []; + if (filteredLearningPath.length === 0) + return new Error("No live learning paths found for inputted user"); const sections = filteredLearningPath[0].course.sections; @@ -264,7 +267,8 @@ export class ProgramResolver { (collection) => typeof collection !== "undefined" )[0]; - if (typeof filteredCollection === "undefined") return []; + if (typeof filteredCollection === "undefined") + return new Error("No collection found for inputted module"); const currentModuleIndex = filteredCollection.modules.findIndex( (module) => module.id === moduleID @@ -276,7 +280,30 @@ export class ProgramResolver { }); }); - let nextCollection = filteredCollection; + const currentCollectionIndex = sections[ + currentSectionIndex + ].collections.findIndex((collection) => { + return collection.id === filteredCollection.id; + }); + + let nextCollection: CollectionPath | null = filteredCollection; + let previousCollection: CollectionPath | null = filteredCollection; + const currentSection: SectionPath = sections[currentSectionIndex]; + + if (currentModuleIndex === 0) { + let previousSection = sections[currentSectionIndex - 1]; + if (!previousSection) previousSection = currentSection; + previousCollection = + previousSection.collections[previousSection.collections.length - 1]; + } + + if ( + currentModuleIndex === 0 && + currentCollectionIndex === 0 && + currentSectionIndex === 0 + ) { + previousCollection = null; + } if (currentModuleIndex + 1 === filteredCollection.modules.length) { // get the array of collections in the sections @@ -299,18 +326,18 @@ export class ProgramResolver { currentCollection: filteredCollection, nextCollection: null, previousModule: filteredCollection.modules[currentModuleIndex - 1], - previousCollection: filteredCollection, - currentSection: sections[currentSectionIndex] + previousCollection, + currentSection }; return { - currentModule: filteredCollection.modules[currentModuleIndex], nextModule: nextCollection.modules[0], - currentCollection: filteredCollection, nextCollection: nextCollection, + currentModule: filteredCollection.modules[currentModuleIndex], + currentCollection: filteredCollection, + currentSection, previousModule: filteredCollection.modules[currentModuleIndex - 1], - previousCollection: filteredCollection, - currentSection: sections[currentSectionIndex] + previousCollection }; } @@ -322,8 +349,8 @@ export class ProgramResolver { currentCollection: filteredCollection, nextCollection: filteredCollection, previousModule: filteredCollection.modules[currentModuleIndex - 1], - previousCollection: filteredCollection, - currentSection: sections[currentSectionIndex] + previousCollection, + currentSection }; } diff --git a/src/program/schema.graphql b/src/program/schema.graphql index 23e54bdf..dc942287 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -514,6 +514,16 @@ type ModuleFlow { currentSection: Section } +type SimpleModuleFlow { + previousModule: ModulePath + previousCollection: CollectionPath + nextModule: ModulePath + nextCollection: CollectionPath + currentModule: ModulePath + currentCollection: CollectionPath + currentSection: SectionPath +} + type SimpleLearningPath { id: ID! createdAt: Date! From ba935e4bc1ceb1ba5b03abd21408183f682d8fe8 Mon Sep 17 00:00:00 2001 From: kenny-dd <54292137+kenny-dd@users.noreply.github.com> Date: Wed, 3 May 2023 12:31:18 -0400 Subject: [PATCH 24/31] Fixed addCourse mutation property not saving to DB (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed addCourse mutation property not saving to DB * added to updateCourse service --------- Co-authored-by: Dániel B. Papp --- src/program/program.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/program/program.service.ts b/src/program/program.service.ts index b3629052..4a493e98 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -702,6 +702,7 @@ export class ProgramService { return this.prisma.course.create({ data: { name: data.name, + sectionIDs: data.section, carnegieHours: data.carnegieHours, required: data.required }, @@ -729,13 +730,14 @@ export class ProgramService { } async updateCourse(id: string, data: CourseInput) { - const { name, required, carnegieHours } = data; + const { name, section, required, carnegieHours } = data; return this.prisma.course.update({ where: { id: id }, data: { ...(name && { name }), + ...(section && { section }), ...(required && { required }), ...(carnegieHours && { carnegieHours }) }, From 6f1d4be69d1ad7683374e15e094695f918edae51 Mon Sep 17 00:00:00 2001 From: "Daniel B. Papp" <25376026+chef-danny-d@users.noreply.github.com> Date: Wed, 3 May 2023 15:31:07 -0400 Subject: [PATCH 25/31] refactor(lp): Update schema to not have name and enrollmentID (#500) * refactor(lp): Update path structure to not have name and enrollmentID * refactor(lp): Fixed service setting non-existent fields --- gql/graphql.ts | 3 --- prisma/schema.prisma | 5 +---- src/program/program.service.ts | 20 +++----------------- src/program/schema.graphql | 3 --- 4 files changed, 4 insertions(+), 27 deletions(-) diff --git a/gql/graphql.ts b/gql/graphql.ts index 74eb2ab9..2a4193ba 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -97,19 +97,16 @@ export interface CoursePathInput { export interface SectionPathInput { id: string; - name: string; collections: CollectionPathInput[]; } export interface CollectionPathInput { id: string; - name: string; modules: ModulePathInput[]; } export interface ModulePathInput { id: string; - enrollmentID?: Nullable; } export interface CreateContentArgs { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 704750c6..141119f3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -387,19 +387,16 @@ type CoursePath { type SectionPath { id String - name String collections CollectionPath[] } type CollectionPath { id String - name String modules ModulePath[] } type ModulePath { - id String - enrollmentID String + id String } enum UserRole { diff --git a/src/program/program.service.ts b/src/program/program.service.ts index 4a493e98..8ba9e58d 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -1418,15 +1418,12 @@ export class ProgramService { sections: path.course.sections.map((section) => { return { id: section.id, - name: section.name, collections: section.collections.map((collection) => { return { id: collection.id, - name: collection.name, modules: collection.modules.map((module) => { return { - id: module.id, - enrollmentID: "" + id: module.id }; }) }; @@ -1479,15 +1476,12 @@ export class ProgramService { sections: course.sections.map((section) => { return { id: section.id, - name: section.name, collections: section.collections.map((collection) => { return { id: collection.id, - name: collection.name, modules: collection.modules.map((module) => { return { - id: module.id, - enrollmentID: "" + id: module.id }; }) }; @@ -1553,7 +1547,6 @@ export class ProgramService { if (section.id !== pathData.course.sections[sectionIndex].id) { return { id: section.id, - name: section.name, collections: [] }; } else { @@ -1567,7 +1560,6 @@ export class ProgramService { ) { return { id: collection.id, - name: collection.name, modules: [] }; } else { @@ -1585,18 +1577,13 @@ export class ProgramService { }; } else { return { - id: module.id, - enrollmentID: - pathData.course.sections[sectionIndex].collections[ - collectionIndex - ].modules[moduleIndex].enrollmentID + id: module.id }; } } ); return { id: collection.id, - name: collection.name, modules: updatedModulesArray }; } @@ -1604,7 +1591,6 @@ export class ProgramService { ); return { id: section.id, - name: section.name, collections: updatedCollectionsArray }; } diff --git a/src/program/schema.graphql b/src/program/schema.graphql index dc942287..671efec2 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -813,19 +813,16 @@ input CoursePathInput { input SectionPathInput { id: ID! - name: String! collections: [CollectionPathInput!]! } input CollectionPathInput { id: ID! - name: String! modules: [ModulePathInput!]! } input ModulePathInput { id: ID! - enrollmentID: ID } input CreateContentArgs { From 638dd010b55d710437821d38f36ee60a170b8e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 3 May 2023 16:01:31 -0400 Subject: [PATCH 26/31] feat(quiz): instances can have many attempts --- prisma/dbml/schema.dbml | 8 +-- prisma/schema.prisma | 154 ++++++++++++++++++++-------------------- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index d9ae1e14..44b7156a 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -286,8 +286,7 @@ Table QuizInstance { quizID String [not null] questions Question [not null] questionIDs String[] [not null] - quizResult QuizResult - quizResultID String [unique] + quizResult QuizResult [not null] } Table Question { @@ -320,6 +319,7 @@ Table QuizResult { student PlanOfStudy [not null] studentID String [not null] quizInstance QuizInstance + quizInstanceID String } Table LearningPath { @@ -441,8 +441,6 @@ Ref: QuizInstance.quizID > Quiz.id [delete: Cascade] Ref: QuizInstance.questionIDs > Question.id -Ref: QuizInstance.quizResultID - QuizResult.id - Ref: Question.parentID > Quiz.id [delete: Cascade] Ref: Question.instanceIDs > QuizInstance.id @@ -451,4 +449,6 @@ Ref: Answer.parentQuestionID > Question.id [delete: Cascade] Ref: QuizResult.studentID > PlanOfStudy.id +Ref: QuizResult.quizInstanceID > QuizInstance.id + Ref: LearningPath.planID > PlanOfStudy.id [delete: Cascade] \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 141119f3..18ad4b0b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,25 +16,25 @@ generator dbml { model User { id String @id @default(auto()) @map("_id") @db.ObjectId openID String @unique - email String - picURL String? + email String + picURL String? createdAt DateTime @default(now()) - firstName String - lastName String - middleName String? + firstName String + lastName String + middleName String? dob DateTime? @default(now()) isAdmin Boolean @default(false) isActive Boolean @default(false) - biography String? - phoneNumber String? + biography String? + phoneNumber String? // Relation fields social Social? @relation(name: "social") plan PlanOfStudy? @relation(name: "plan") - tokens Token[] - feedback SectionFeedback[] + tokens Token[] + feedback SectionFeedback[] assignmentGraded AssignmentResult[] @relation(name: "graded") - instructorProfile InstructorProfile? + instructorProfile InstructorProfile? watchedThreads Thread[] @relation(fields: [watchedThreadIDs], references: [id]) watchedThreadIDs String[] @default([]) @db.ObjectId createdThreads Thread[] @relation(name: "createdThreads") @@ -50,11 +50,11 @@ model InstructorProfile { id String @id @default(auto()) @map("_id") @db.ObjectId account User? @relation(fields: [accountID], references: [id]) accountID String? @unique @db.ObjectId - title String? - officeLocation String? + title String? + officeLocation String? officeHours String[] @default([]) - contactPolicy String? - background String? + contactPolicy String? + background String? researchInterest String[] @default([]) selectedPapersAndPublications String[] @default([]) instructedModules Module[] @relation() @@ -67,19 +67,19 @@ model PlanOfStudy { // ill figure advisors out later, my brain needs a rest // advisor User? @relation(fields: [advisorId], references:[id]) // advisorId String? @db.ObjectId - sections SectionEnrollment[] - assignmentResults AssignmentResult[] - quizResults QuizResult[] - learningPaths LearningPath[] + sections SectionEnrollment[] + assignmentResults AssignmentResult[] + quizResults QuizResult[] + learningPaths LearningPath[] } model Social { id String @id @default(auto()) @map("_id") @db.ObjectId - twitter String? - github String? - linkedin String? - facebook String? - portfolio String? + twitter String? + github String? + linkedin String? + facebook String? + portfolio String? account User? @relation(name: "social", fields: [accountID], references: [id]) accountID String? @unique @db.ObjectId } @@ -88,7 +88,7 @@ model Token { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) valid Boolean @default(true) - expiration DateTime + expiration DateTime // Relation fields user User @relation(fields: [userId], references: [id]) @@ -97,7 +97,7 @@ model Token { model Course { id String @id @default(auto()) @map("_id") @db.ObjectId - name String + name String number Int? @default(0) prefix String? @default("") required Boolean? @default(false) @@ -110,12 +110,12 @@ model Section { id String @id @default(auto()) @map("_id") @db.ObjectId sectionNumber Int @unique sectionName String @unique - description String - duration Float - intro String - numSlides Int - keywords String[] - objectives String[] + description String + duration Float + intro String + numSlides Int + keywords String[] + objectives String[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -136,7 +136,7 @@ model Section { model Collection { id String @id @default(auto()) @map("_id") @db.ObjectId - name String + name String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt position Int @default(0) @@ -150,7 +150,7 @@ model Collection { model Module { id String @id @default(auto()) @map("_id") @db.ObjectId - name String + name String prefix String? @default("") number Int? @default(1) collections Collection[] @relation(fields: [collectionIDs], references: [id]) @@ -162,15 +162,15 @@ model Module { instructor InstructorProfile? @relation(fields: [instructorProfileID], references: [id]) instructorProfileID String? @default("") @db.ObjectId - content Content[] - quizzes Quiz[] - moduleProgress ModuleProgress[] + content Content[] + quizzes Quiz[] + moduleProgress ModuleProgress[] } model SectionFeedback { id String @id @default(auto()) @map("_id") @db.ObjectId - feedback String - rating Int + feedback String + rating Int studentId String @db.ObjectId sectionId String @db.ObjectId @@ -181,7 +181,7 @@ model SectionFeedback { model SectionEnrollment { id String @id @default(auto()) @map("_id") @db.ObjectId enrolledAt DateTime @default(now()) - role UserRole // Allow for instructors, graders and students to take part in the module. + role UserRole // Allow for instructors, graders and students to take part in the module. status EnrollmentStatus @default(INACTIVE) // Relation Fields @@ -189,32 +189,32 @@ model SectionEnrollment { sectionId String @db.ObjectId plan PlanOfStudy? @relation(fields: [planID], references: [id]) planID String? @db.ObjectId - progress Progress? - moduleProgress ModuleProgress[] + progress Progress? + moduleProgress ModuleProgress[] } model Assignment { id String @id @default(auto()) @map("_id") @db.ObjectId updatedAt DateTime @default(now()) @updatedAt - name String - dueAt DateTime - contentURL String - contentType String + name String + dueAt DateTime + contentURL String + contentType String acceptedTypes FileType @default(DOC) // Relation Fields sectionId String @db.ObjectId section Section @relation(fields: [sectionId], references: [id]) - assignmentResults AssignmentResult[] + assignmentResults AssignmentResult[] } model AssignmentResult { id String @id @default(auto()) @map("_id") @db.ObjectId submittedAt DateTime @default(now()) - result Float - feedback String? - submissionURL String - fileType String + result Float + feedback String? + submissionURL String + fileType String // Relation Fields studentId String @db.ObjectId @@ -229,8 +229,8 @@ model Thread { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - title String? - body String + title String? + body String usersWatching User[] @relation(fields: [watcherID], references: [id]) watcherID String[] @default([]) @db.ObjectId author User @relation(name: "createdThreads", fields: [authorID], references: [id]) @@ -247,18 +247,18 @@ model Thread { model Content { id String @id @default(auto()) @map("_id") @db.ObjectId - type ContentType - link String + type ContentType + link String parent Module @relation(fields: [parentID], references: [id], onDelete: Cascade) parentID String @db.ObjectId - primary Boolean + primary Boolean } model DirectMessage { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - body String + body String author User @relation(name: "sentMessages", fields: [authorID], references: [id]) authorID String @db.ObjectId recipient User? @relation(name: "receivedMessages", fields: [recipientID], references: [id]) @@ -271,7 +271,7 @@ model Group { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - name String + name String public Boolean @default(false) members User[] @relation(name: "groupMemeberships", fields: [memberIDs], references: [id]) memberIDs String[] @default([]) @db.ObjectId @@ -304,35 +304,34 @@ model ModuleProgress { model Quiz { id String @id @default(auto()) @map("_id") @db.ObjectId - totalPoints Float - instructions String? - dueAt DateTime? - timeLimit Int? - numQuestions Int + totalPoints Float + instructions String? + dueAt DateTime? + timeLimit Int? + numQuestions Int minScore Float @default(0.0) parentModule Module @relation(fields: [parentModuleID], references: [id], onDelete: Cascade) parentModuleID String @db.ObjectId - questionPool Question[] - instances QuizInstance[] + questionPool Question[] + instances QuizInstance[] } model QuizInstance { - id String @id @default(auto()) @map("_id") @db.ObjectId - quiz Quiz @relation(fields: [quizID], references: [id], onDelete: Cascade) - quizID String @db.ObjectId - questions Question[] @relation(fields: [questionIDs], references: [id]) - questionIDs String[] @default([]) @db.ObjectId - quizResult QuizResult? @relation(fields: [quizResultID], references: [id]) - quizResultID String? @unique @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId + quiz Quiz @relation(fields: [quizID], references: [id], onDelete: Cascade) + quizID String @db.ObjectId + questions Question[] @relation(fields: [questionIDs], references: [id]) + questionIDs String[] @default([]) @db.ObjectId + quizResult QuizResult[] } model Question { id String @id @default(auto()) @map("_id") @db.ObjectId - number Int + number Int variant Int @default(1) - text String + text String points Float @default(1.0) - answers Answer[] + answers Answer[] parent Quiz @relation(fields: [parentID], references: [id], onDelete: Cascade) parentID String @db.ObjectId instances QuizInstance[] @relation(fields: [instanceIDs], references: [id]) @@ -355,9 +354,10 @@ model QuizResult { // answers String[] submittedAt DateTime @default(now()) - student PlanOfStudy @relation(fields: [studentID], references: [id]) - studentID String @db.ObjectId - quizInstance QuizInstance? + student PlanOfStudy @relation(fields: [studentID], references: [id]) + studentID String @db.ObjectId + quizInstance QuizInstance? @relation(fields: [quizInstanceID], references: [id]) + quizInstanceID String? @db.ObjectId } model LearningPath { From c7248d5293de390d9627e455b93261b3bcaf7e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 3 May 2023 16:05:29 -0400 Subject: [PATCH 27/31] fix(course): Fixed service connecting sections on create --- src/program/program.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/program/program.service.ts b/src/program/program.service.ts index 8ba9e58d..c86a7996 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -702,9 +702,9 @@ export class ProgramService { return this.prisma.course.create({ data: { name: data.name, - sectionIDs: data.section, carnegieHours: data.carnegieHours, - required: data.required + required: data.required, + sections: data.section ? { connect: { id: data.section } } : undefined }, include: this.courseInclude }); From 2c3d1834242a2624252de93b7c2f165c568a8758 Mon Sep 17 00:00:00 2001 From: "Daniel B. Papp" <25376026+chef-danny-d@users.noreply.github.com> Date: Fri, 19 May 2023 10:41:11 -0400 Subject: [PATCH 28/31] fix(dm): Remove Redis dependency from module and configured DM services (#501) * fix(dm): Remove redis dependency from feature The system now uses the DB to read and write direct messages * refactor(dm): started integrating messaging with client-side As I started integrating the DM service into the client side application, it was clear that it needed to be refactor to improve the DX of the API. * refactor(dm): filtered out duplicate recipients Since the `sentMessages` query is used to fetch the latest messages sent by the author to any user, we want to make sure that there are no duplicate recipients in the list, but rather use the latest message of each recipient as an entry in the list. * refactor(docker): removed Redis and Mongo instances * feat(dm): defined create group mutation and service * refactor(dm): addressed group message issues Previously, users couldn't retrieve information about groups as recipients. Now, we have full support for group and 1-1 message querying * fix(module): updating service errored out without collection ID * test(quiz): worked on broken cases that use hardcoded data * refactor(module): matched create service arguments * test(module): fixed broken cases that didn't respect program refactor * test(module): fixed cases broken due to renaming * refactor(dm): removed WS server config * feat(module): defined delete many mutation and service * refactor(module): added keywords field to DB schema * refactor(module): integrated create service with keyword schema change --- docker-compose.yml | 56 +---- gql/graphql.ts | 11 +- prisma/dbml/schema.dbml | 1 + prisma/schema.prisma | 137 ++++++------ src/direct-message/direct-message.module.ts | 3 +- src/direct-message/direct-message.resolver.ts | 41 ++-- src/direct-message/direct-message.service.ts | 201 ++++++++++++++++-- src/direct-message/schema.graphql | 8 +- src/main.ts | 21 -- src/program/program.resolver.ts | 9 + src/program/program.service.ts | 27 ++- src/program/program.spec.ts | 88 ++++---- src/program/schema.graphql | 18 +- src/progress/progress.spec.ts | 6 +- src/quiz/quiz.service.ts | 28 ++- src/quiz/quiz.spec.ts | 115 +++++++--- utils/fakes.ts | 2 +- utils/tests.ts | 36 ++-- 18 files changed, 517 insertions(+), 291 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a52d3db8..2ca0dae1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,8 @@ version: "3.9" services: api: - depends_on: - - mongo container_name: back_end_api - restart: always + restart: on-failure image: node:latest working_dir: /usr/src/app env_file: @@ -18,8 +16,7 @@ services: - apiNetwork command: yarn dev studio: - depends_on: - - mongo + restart: always container_name: back_end_studio image: node:latest working_dir: /usr/src/app @@ -32,53 +29,6 @@ services: networks: - apiNetwork command: bash -c "yarn db:studio --browser false" - redis: - container_name: back_end_redis - image: redis - ports: - - "6379:6379" - networks: - - apiNetwork - redis-insight: - container_name: back_end_redis_insight - image: redislabs/redisinsight:latest - volumes: - - redisinsight:/db - ports: - - "8001:8001" - networks: - - apiNetwork - mongo: - container_name: back_end_db - restart: always - image: mongo:latest - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: example - expose: - - "27017" - ports: - - "27017:27017" - networks: - - apiNetwork - mongo-express: - depends_on: - - mongo - container_name: back_end_db_express - restart: always - image: mongo-express:latest - environment: - ME_CONFIG_MONGODB_SERVER: back_end_db - ME_CONFIG_MONGODB_ADMINUSERNAME: root - ME_CONFIG_MONGODB_ADMINPASSWORD: example - ports: - - "8081:8081" - networks: - - apiNetwork - command: mongo-express networks: apiNetwork: - driver: bridge - -volumes: - redisinsight: \ No newline at end of file + driver: bridge \ No newline at end of file diff --git a/gql/graphql.ts b/gql/graphql.ts index 2a4193ba..56f4ea48 100644 --- a/gql/graphql.ts +++ b/gql/graphql.ts @@ -285,9 +285,10 @@ export interface ModuleInput { prefix?: Nullable; number?: Nullable; content?: Nullable; - collection: string; + collection?: Nullable; position?: Nullable; objectives?: Nullable; + keywords?: Nullable; hours: number; description?: Nullable; instructor?: Nullable; @@ -303,6 +304,7 @@ export interface ModuleFields { collection?: Nullable; position?: Nullable; objectives?: Nullable; + keywords?: Nullable; hours?: Nullable; description?: Nullable; instructor?: Nullable; @@ -513,6 +515,7 @@ export interface IMutation { addUserAsWatcherToThread(id: string, userID: string): Nullable | Promise>; createDirectMessage(receiverID: string, message: string, senderID: string): boolean | Promise; newGroupMessage(groupID: string, message: string, senderID: string): boolean | Promise; + createGroup(name: string, members: string[], publicGroup?: Nullable): Group | Promise; addPlan(input?: Nullable): PlanOfStudy | Promise; updatePlan(id: string, input?: Nullable): Nullable | Promise>; deletePlan(id: string): Nullable | Promise>; @@ -542,6 +545,7 @@ export interface IMutation { createModule(input: ModuleInput): Module | Promise; updateModule(input?: Nullable, replaceObj?: Nullable): Nullable | Promise>; deleteModule(id: string): Nullable | Promise>; + deleteManyModule(id: string[]): Nullable | Promise>; createContent(input: CreateContentArgs): Content | Promise; updateContent(input: ContentFields): Nullable | Promise>; deleteContent(contentID: string): Nullable | Promise>; @@ -581,9 +585,10 @@ export interface IMutation { export interface IQuery { refresh(token?: Nullable): Nullable | Promise>; thread(input?: Nullable): Thread[] | Promise; - directMessages(receiverID: string): DirectMessageResponse[] | Promise; + directMessages(receiverID: string, senderID: string): DirectMessageResponse[] | Promise; groups(userID: string): Group[] | Promise; groupMessages(groupID: string): DirectMessageResponse[] | Promise; + sentMessages(senderID: string): DirectMessageResponse[] | Promise; plan(studentID: string): Nullable | Promise>; plans(): Nullable | Promise>; planByID(id: string): Nullable | Promise>; @@ -769,6 +774,7 @@ export interface Module { quizzes?: Nullable; moduleProgress?: Nullable[]>; objectives: string[]; + keywords: string[]; hours: number; description?: Nullable; instructor?: Nullable; @@ -922,6 +928,7 @@ export interface ModulePath { quizzes?: Nullable; moduleProgress?: Nullable[]>; objectives: string[]; + keywords: string[]; hours: number; enrollmentID?: Nullable; description?: Nullable; diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 44b7156a..d4943e68 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -133,6 +133,7 @@ Table Module { collectionIDs String[] [not null] position Int [not null, default: 0] objectives String[] [not null] + keywords String[] [not null] hours Float [not null, default: 0] description String [default: ''] instructor InstructorProfile diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 18ad4b0b..b1e4afe3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,25 +16,25 @@ generator dbml { model User { id String @id @default(auto()) @map("_id") @db.ObjectId openID String @unique - email String - picURL String? + email String + picURL String? createdAt DateTime @default(now()) - firstName String - lastName String - middleName String? + firstName String + lastName String + middleName String? dob DateTime? @default(now()) isAdmin Boolean @default(false) isActive Boolean @default(false) - biography String? - phoneNumber String? + biography String? + phoneNumber String? // Relation fields social Social? @relation(name: "social") plan PlanOfStudy? @relation(name: "plan") - tokens Token[] - feedback SectionFeedback[] + tokens Token[] + feedback SectionFeedback[] assignmentGraded AssignmentResult[] @relation(name: "graded") - instructorProfile InstructorProfile? + instructorProfile InstructorProfile? watchedThreads Thread[] @relation(fields: [watchedThreadIDs], references: [id]) watchedThreadIDs String[] @default([]) @db.ObjectId createdThreads Thread[] @relation(name: "createdThreads") @@ -50,11 +50,11 @@ model InstructorProfile { id String @id @default(auto()) @map("_id") @db.ObjectId account User? @relation(fields: [accountID], references: [id]) accountID String? @unique @db.ObjectId - title String? - officeLocation String? + title String? + officeLocation String? officeHours String[] @default([]) - contactPolicy String? - background String? + contactPolicy String? + background String? researchInterest String[] @default([]) selectedPapersAndPublications String[] @default([]) instructedModules Module[] @relation() @@ -67,19 +67,19 @@ model PlanOfStudy { // ill figure advisors out later, my brain needs a rest // advisor User? @relation(fields: [advisorId], references:[id]) // advisorId String? @db.ObjectId - sections SectionEnrollment[] - assignmentResults AssignmentResult[] - quizResults QuizResult[] - learningPaths LearningPath[] + sections SectionEnrollment[] + assignmentResults AssignmentResult[] + quizResults QuizResult[] + learningPaths LearningPath[] } model Social { id String @id @default(auto()) @map("_id") @db.ObjectId - twitter String? - github String? - linkedin String? - facebook String? - portfolio String? + twitter String? + github String? + linkedin String? + facebook String? + portfolio String? account User? @relation(name: "social", fields: [accountID], references: [id]) accountID String? @unique @db.ObjectId } @@ -88,7 +88,7 @@ model Token { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) valid Boolean @default(true) - expiration DateTime + expiration DateTime // Relation fields user User @relation(fields: [userId], references: [id]) @@ -97,7 +97,7 @@ model Token { model Course { id String @id @default(auto()) @map("_id") @db.ObjectId - name String + name String number Int? @default(0) prefix String? @default("") required Boolean? @default(false) @@ -110,12 +110,12 @@ model Section { id String @id @default(auto()) @map("_id") @db.ObjectId sectionNumber Int @unique sectionName String @unique - description String - duration Float - intro String - numSlides Int - keywords String[] - objectives String[] + description String + duration Float + intro String + numSlides Int + keywords String[] + objectives String[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -136,7 +136,7 @@ model Section { model Collection { id String @id @default(auto()) @map("_id") @db.ObjectId - name String + name String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt position Int @default(0) @@ -150,27 +150,28 @@ model Collection { model Module { id String @id @default(auto()) @map("_id") @db.ObjectId - name String + name String prefix String? @default("") number Int? @default(1) collections Collection[] @relation(fields: [collectionIDs], references: [id]) collectionIDs String[] @default([]) @db.ObjectId position Int @default(0) objectives String[] @default([]) + keywords String[] @default([]) hours Float @default(0.0) description String? @default("") instructor InstructorProfile? @relation(fields: [instructorProfileID], references: [id]) instructorProfileID String? @default("") @db.ObjectId - content Content[] - quizzes Quiz[] - moduleProgress ModuleProgress[] + content Content[] + quizzes Quiz[] + moduleProgress ModuleProgress[] } model SectionFeedback { id String @id @default(auto()) @map("_id") @db.ObjectId - feedback String - rating Int + feedback String + rating Int studentId String @db.ObjectId sectionId String @db.ObjectId @@ -181,7 +182,7 @@ model SectionFeedback { model SectionEnrollment { id String @id @default(auto()) @map("_id") @db.ObjectId enrolledAt DateTime @default(now()) - role UserRole // Allow for instructors, graders and students to take part in the module. + role UserRole // Allow for instructors, graders and students to take part in the module. status EnrollmentStatus @default(INACTIVE) // Relation Fields @@ -189,32 +190,32 @@ model SectionEnrollment { sectionId String @db.ObjectId plan PlanOfStudy? @relation(fields: [planID], references: [id]) planID String? @db.ObjectId - progress Progress? - moduleProgress ModuleProgress[] + progress Progress? + moduleProgress ModuleProgress[] } model Assignment { id String @id @default(auto()) @map("_id") @db.ObjectId updatedAt DateTime @default(now()) @updatedAt - name String - dueAt DateTime - contentURL String - contentType String + name String + dueAt DateTime + contentURL String + contentType String acceptedTypes FileType @default(DOC) // Relation Fields sectionId String @db.ObjectId section Section @relation(fields: [sectionId], references: [id]) - assignmentResults AssignmentResult[] + assignmentResults AssignmentResult[] } model AssignmentResult { id String @id @default(auto()) @map("_id") @db.ObjectId submittedAt DateTime @default(now()) - result Float - feedback String? - submissionURL String - fileType String + result Float + feedback String? + submissionURL String + fileType String // Relation Fields studentId String @db.ObjectId @@ -229,8 +230,8 @@ model Thread { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - title String? - body String + title String? + body String usersWatching User[] @relation(fields: [watcherID], references: [id]) watcherID String[] @default([]) @db.ObjectId author User @relation(name: "createdThreads", fields: [authorID], references: [id]) @@ -247,18 +248,18 @@ model Thread { model Content { id String @id @default(auto()) @map("_id") @db.ObjectId - type ContentType - link String + type ContentType + link String parent Module @relation(fields: [parentID], references: [id], onDelete: Cascade) parentID String @db.ObjectId - primary Boolean + primary Boolean } model DirectMessage { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - body String + body String author User @relation(name: "sentMessages", fields: [authorID], references: [id]) authorID String @db.ObjectId recipient User? @relation(name: "receivedMessages", fields: [recipientID], references: [id]) @@ -271,7 +272,7 @@ model Group { id String @id @default(auto()) @map("_id") @db.ObjectId createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - name String + name String public Boolean @default(false) members User[] @relation(name: "groupMemeberships", fields: [memberIDs], references: [id]) memberIDs String[] @default([]) @db.ObjectId @@ -304,16 +305,16 @@ model ModuleProgress { model Quiz { id String @id @default(auto()) @map("_id") @db.ObjectId - totalPoints Float - instructions String? - dueAt DateTime? - timeLimit Int? - numQuestions Int + totalPoints Float + instructions String? + dueAt DateTime? + timeLimit Int? + numQuestions Int minScore Float @default(0.0) parentModule Module @relation(fields: [parentModuleID], references: [id], onDelete: Cascade) parentModuleID String @db.ObjectId - questionPool Question[] - instances QuizInstance[] + questionPool Question[] + instances QuizInstance[] } model QuizInstance { @@ -322,16 +323,16 @@ model QuizInstance { quizID String @db.ObjectId questions Question[] @relation(fields: [questionIDs], references: [id]) questionIDs String[] @default([]) @db.ObjectId - quizResult QuizResult[] + quizResult QuizResult[] } model Question { id String @id @default(auto()) @map("_id") @db.ObjectId - number Int + number Int variant Int @default(1) - text String + text String points Float @default(1.0) - answers Answer[] + answers Answer[] parent Quiz @relation(fields: [parentID], references: [id], onDelete: Cascade) parentID String @db.ObjectId instances QuizInstance[] @relation(fields: [instanceIDs], references: [id]) diff --git a/src/direct-message/direct-message.module.ts b/src/direct-message/direct-message.module.ts index fce35725..3d3cc4dd 100644 --- a/src/direct-message/direct-message.module.ts +++ b/src/direct-message/direct-message.module.ts @@ -3,10 +3,9 @@ import { DirectMessageService } from "./direct-message.service"; import { DirectMessageResolver } from "./direct-message.resolver"; import { PrismaService } from "@/prisma.service"; import { PubSubService } from "@/pub-sub/pub-sub.service"; -import { RedisModule } from "@/pub-sub/redis.module"; @Module({ - imports: [RedisModule], + imports: [], providers: [ DirectMessageService, DirectMessageResolver, diff --git a/src/direct-message/direct-message.resolver.ts b/src/direct-message/direct-message.resolver.ts index 6a41e26f..f492e335 100644 --- a/src/direct-message/direct-message.resolver.ts +++ b/src/direct-message/direct-message.resolver.ts @@ -1,12 +1,4 @@ -import { - Args, - Context, - GraphQLExecutionContext, - Mutation, - Query, - Resolver, - Subscription -} from "@nestjs/graphql"; +import { Args, Mutation, Query, Resolver, Subscription } from "@nestjs/graphql"; import { DirectMessageService } from "@/direct-message"; import { PubSubService } from "@/pub-sub/pub-sub.service"; import { DirectMessage } from "@prisma/client"; @@ -19,8 +11,11 @@ export class DirectMessageResolver { ) {} @Query("directMessages") - async directMessages(@Args("receiverID") receiverID: string) { - return await this.dmService.getConversation(receiverID); + async directMessages( + @Args("receiverID") receiverID: string, + @Args("senderID") senderID: string + ) { + return await this.dmService.getConversation(receiverID, senderID); } @Query("groups") @@ -28,10 +23,14 @@ export class DirectMessageResolver { return await this.dmService.getGroups(userID); } + @Query("sentMessages") + async sentMessages(@Args("senderID") senderID: string) { + return await this.dmService.getSentMessages(senderID); + } + @Subscription("newDirectMessage", { resolve: (payload) => payload, filter: (payload: DirectMessage, args: { receiverID: string }) => { - console.log(payload, args); return payload.recipientID === args.receiverID; } }) @@ -41,7 +40,6 @@ export class DirectMessageResolver { @Subscription("newGroupMessage", { resolve: (payload) => payload, filter: (payload, args) => { - console.log(payload, args); return payload.groupID === args.groupID; } }) @@ -85,4 +83,21 @@ export class DirectMessageResolver { } } } + + @Mutation("createGroup") + async createGroup( + @Args("name") name: string, + @Args("members") members: string[], + @Args("publicGroup") publicGroup: boolean = false + ) { + const newGroup = await this.dmService.createGroup( + name, + members, + publicGroup + ); + if (newGroup instanceof Error) return new Error(newGroup.message); + else { + return newGroup; + } + } } diff --git a/src/direct-message/direct-message.service.ts b/src/direct-message/direct-message.service.ts index 8abb0d48..a3228e5e 100644 --- a/src/direct-message/direct-message.service.ts +++ b/src/direct-message/direct-message.service.ts @@ -1,31 +1,87 @@ -import { Inject, Injectable } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { PrismaService } from "@/prisma.service"; -import { CreateMessageInput } from "@/types/graphql"; -import Redis from "ioredis"; import { DirectMessage, Group, Prisma, User } from "@prisma/client"; +import { DirectMessageResponse } from "@/types/graphql"; @Injectable() export class DirectMessageService { - constructor( - private readonly prisma: PrismaService, - @Inject("REDIS_CLIENT") private readonly redisClient: Redis - ) {} + constructor(private readonly prisma: PrismaService) {} - async getConversation(receiverID) { - const response = await this.prisma.directMessage.findMany({ + async getConversation(receiverID: string, senderID: string) { + const group = await this.prisma.group.findFirst({ where: { - recipientID: receiverID + id: receiverID }, include: { - author: true, - recipient: true + members: true } }); - if (!response) return new Error("Conversation could not be found"); - return response; + // if the recipient is a group + if (group) { + // check if the sender is a member of the group + const isMember = group.members.find((member) => member.id === senderID); + if (!isMember) return new Error("You are not a member of this group"); + // if the sender is a member of the group, return the group's messages + else { + const response = await this.prisma.directMessage.findMany({ + where: { + groupID: receiverID + }, + include: { + author: true, + recipient: true, + group: true + } + }); + + if (response.length === 0) + return new Error("Conversation could not be found"); + + return response.map((message) => { + message.recipientID = message.groupID; + //@ts-ignore + message.recipient = { + ...message.group, + __typename: "Group" + } as Group; + return message as DirectMessageResponse; + }); + } + } else { + const response = await this.prisma.directMessage.findMany({ + where: { + OR: [ + { + authorID: senderID, + recipientID: receiverID + }, + { + recipientID: senderID, + authorID: receiverID + } + ] + }, + include: { + author: true, + recipient: true + } + }); + if (response.length === 0) + return new Error("Conversation could not be found"); + + const typedResponse = response.map((message) => { + message.recipient = { + ...message.recipient, + __typename: "User" + } as User; + return message as DirectMessageResponse; + }); + + return typedResponse; + } } - async getGroups(userID) { + async getGroups(userID: string) { const response = await this.prisma.group.findMany({ where: { members: { @@ -48,7 +104,7 @@ export class DirectMessageService { return response; } - async send(senderID, recipientID, message) { + async send(senderID: string, recipientID: string, message: string) { let response: DirectMessage & { author: User; recipient: (User | null) | (Group | null); @@ -146,4 +202,117 @@ export class DirectMessageService { } } } + + async getSentMessages( + senderID: string + ): Promise | Error> { + const response = await this.prisma.directMessage.findMany({ + where: { + OR: [ + { + authorID: senderID + }, + { + recipientID: senderID + }, + { + group: { + members: { + some: { + id: senderID + } + } + } + } + ] + }, + include: { + author: true, + recipient: true, + group: { + include: { + messages: true, + members: true + } + } + } + }); + if (!response) return new Error("Messages could not be found"); + + const typedResponse = response.map((message) => { + if (!message.recipient && message.group) { + //@ts-ignore + message.recipient = { + ...message.group, + __typename: "Group" + } as Group; + message.recipientID = message.group.id; + } else { + message.recipient = { + ...message.recipient, + __typename: "User" + } as User; + } + return message; + }); + + // sort by most recent message + const sortedResponses = typedResponse.sort((a, b) => { + return b.createdAt.getTime() - a.createdAt.getTime(); + }); + + const recipientIDs = new Set(); + + // filter out duplicate recipients and keep the most recent one + const payload = sortedResponses.filter((response) => { + if (response.group) { + if (recipientIDs.has(response.group.id)) { + return false; + } else { + recipientIDs.add(response.group.id); + return true; + } + } + if (response.recipientID === senderID) return false; + if (recipientIDs.has(response.recipientID)) { + return false; + } else { + recipientIDs.add(response.recipientID); + return true; + } + }); + + if (!payload) return new Error("Messages could not be found"); + //@ts-ignore + return payload; + } + + async createGroup(name: string, members: string[], publicGroup: boolean) { + const response = await this.prisma.group.create({ + data: { + name, + members: { + connect: members.map((member) => { + return { + id: member + }; + }) + }, + public: publicGroup + }, + include: { + members: true, + messages: { + include: { + author: true, + group: true, + recipient: true + } + } + } + }); + + if (!response) return new Error("Group could not be created"); + return response; + } } diff --git a/src/direct-message/schema.graphql b/src/direct-message/schema.graphql index 6bc6107a..bb2f04fc 100644 --- a/src/direct-message/schema.graphql +++ b/src/direct-message/schema.graphql @@ -9,7 +9,7 @@ type Query { """ Get all messages in a group. """ - directMessages(receiverID: ID!): [DirectMessageResponse!]! + directMessages(receiverID: ID!, senderID: ID!): [DirectMessageResponse!]! """ Get all groups for a user. """ @@ -18,6 +18,10 @@ type Query { Get all messages in a group. If the receiverID is not matched by the receiverID of the message, the message is not returned. """ groupMessages(groupID: ID!): [DirectMessageResponse!]! + """ + Get all messages sent by a user. + """ + sentMessages(senderID: ID!): [DirectMessageResponse!]! } type Mutation { """ @@ -29,6 +33,8 @@ type Mutation { """ newGroupMessage(groupID: ID!, message: String!, senderID: ID!): Boolean! # createChannel(groupID: ID!, name: String!, publicAccess: Boolean=false): Boolean! + + createGroup(name: String!, members: [ID!]!, publicGroup: Boolean=false): Group! } type CreateMessageInput { diff --git a/src/main.ts b/src/main.ts index 4a3aea46..21f6aecf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,28 +3,8 @@ import { AppModule } from "./app.module"; import cookieParser from "cookie-parser"; import * as Sentry from "@sentry/node"; import sourceMapSupport from "source-map-support"; -import { createServer } from "http"; import { PrismaService } from "@/prisma.service"; -async function startWebSocketServer(app, port = 5000) { - const httpServer = createServer(app); - - await httpServer.listen(port, async () => { - console.log(`🚀 Subscriptions ready at ws://localhost:${port}/graphql`); - }); - - httpServer.on("connection", (ws) => { - console.log("🚀 Connected to websocket"); - ws.on("message", (message) => { - console.log("received: %s", message); - }); - ws.on("close", () => { - console.log("🚀 Disconnected from websocket"); - }); - ws.on("error", console.error); - }); -} - async function bootstrap() { const app = await NestFactory; const client = await app.create(AppModule, { @@ -69,6 +49,5 @@ async function bootstrap() { `🚀 Server ready at http://localhost:${process.env.PORT}/graphql` ); }); - await startWebSocketServer(client); } bootstrap().catch((err) => console.error(err)); diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index fda4ca27..4903443c 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -579,6 +579,15 @@ export class ProgramResolver { return await this.programService.deleteModule(id); } + @Mutation("deleteManyModule") + async deleteManyModule(@Args("id") id: string[]) { + const payload = id.map(async (element) => { + return await this.programService.deleteModule(element); + }); + + return payload.length === id.length; + } + @Mutation("createContent") async createContent(@Args("input") input: CreateContentArgs) { // we get the module based on the parent ID of the content diff --git a/src/program/program.service.ts b/src/program/program.service.ts index c86a7996..c3ab6f3c 100644 --- a/src/program/program.service.ts +++ b/src/program/program.service.ts @@ -558,7 +558,7 @@ export class ProgramService { ...(id && { id }), ...(name && { name }), ...(position && { position }), - collections: { some: { id: collection ? collection : undefined } }, + collections: collection ? { some: { id: collection } } : undefined, content: content ? { some: { id: content } } : undefined, objectives: objectives ? { hasEvery: objectives } : undefined }); @@ -1072,14 +1072,19 @@ export class ProgramService { } } }), - collections: { - connect: { - id: input.collection ? input.collection : undefined - } - }, + collections: input.collection + ? { + connect: { + id: input.collection + } + } + : undefined, position: input.position ? input.position : undefined, objectives: input.objectives ? input.objectives : undefined, - hours: input.hours + hours: input.hours, + prefix: input.prefix ? input.prefix : undefined, + number: input.number ? input.number : undefined, + keywords: input.keywords ? input.keywords : undefined }, include: this.moduleInclude }); @@ -1136,9 +1141,11 @@ export class ProgramService { }, data: { name: payload.name, - collectionIDs: { - push: payload.collection - }, + collectionIDs: payload.collection + ? { + push: payload.collection + } + : undefined, position: input.position ? input.position : undefined, objectives: newObjectives ? newObjectives : undefined, hours: payload.hours diff --git a/src/program/program.spec.ts b/src/program/program.spec.ts index e6ffd85a..92d3585e 100644 --- a/src/program/program.spec.ts +++ b/src/program/program.spec.ts @@ -10,9 +10,9 @@ import { CreateContentArgs, ModuleInput, Module, - NewModule, PlanOfStudy, - User + User, + NewSection } from "@/types/graphql"; import { afterAll, beforeAll, describe, expect, test } from "vitest"; import { faker } from "@faker-js/faker"; @@ -89,20 +89,10 @@ describe("Plan services", () => { expect(moduleFirst).toBeDefined(); if (moduleFirst) { expect(moduleFirst.id).toBe(testingModuleID); - expect(moduleFirst.moduleName).toBeDefined(); - expect(moduleFirst.moduleNumber).toBeDefined(); + expect(moduleFirst.name).toBeDefined(); + expect(moduleFirst.number).toBeDefined(); expect(moduleFirst.description).toBeDefined(); - expect(moduleFirst.createdAt).toBeDefined(); - expect(moment(moduleFirst.createdAt).isBefore(new Date())).toBe(true); - expect(moduleFirst.updatedAt).toBeDefined(); - expect(moment(moduleFirst.updatedAt).isBefore(new Date())).toBe(true); - expect(moduleFirst.duration).toBeDefined(); - expect(moduleFirst.numSlides).toBeDefined(); - - expect(moduleFirst.keywords).toBeInstanceOf(Array); expect(moduleFirst.objectives).toBeInstanceOf(Array); - expect(moduleFirst.feedback).toBeInstanceOf(Array); - expect(moduleFirst.members).toBeInstanceOf(Array); } }); }); @@ -110,7 +100,7 @@ describe("Plan services", () => { describe("Enrollment", () => { describe("Query.moduleEnrollments()", () => { test("should return an array of moduleEnrollments", async () => { - const moduleEnrollments = await resolver.moduleEnrollment({}); + const moduleEnrollments = await resolver.sectionEnrollment({}); expect(moduleEnrollments).toBeDefined(); expect(moduleEnrollments.length).toBeGreaterThan(1); moduleEnrollments.map((enrollments) => { @@ -119,7 +109,7 @@ describe("Plan services", () => { }); test("should not take longer than 1.5 seconds to return all moduleEnrollments", async () => { const start = new Date(); - const moduleEnrollments = await resolver.moduleEnrollment({}); + const moduleEnrollments = await resolver.sectionEnrollment({}); expect(moduleEnrollments.length).toBeGreaterThan(1); const end = new Date(); expect(end.getTime() - start.getTime()).toBeLessThan(1500); @@ -140,9 +130,9 @@ describe("Plan services", () => { test("should return modules related to the course", async () => { const courses = await resolver.course({}); courses.map((course) => { - expect(course.module).toBeDefined(); - expect(course.module).toBeInstanceOf(Array); - expect(course.module.length).toBeGreaterThanOrEqual(1); + expect(course.sections).toBeDefined(); + expect(course.sections).toBeInstanceOf(Array); + expect(course.sections.length).toBeGreaterThanOrEqual(1); testingCourseID = course.id; }); }); @@ -159,7 +149,7 @@ describe("Plan services", () => { expect(course[0]).toBeDefined(); expect(course[0].id).toBe(testingCourseID); expect(course[0].name).toBeDefined(); - expect(Array.isArray(course[0].module)).toBe(true); + expect(Array.isArray(course[0].sections)).toBe(true); }); }); }); @@ -205,23 +195,23 @@ describe("Plan services", () => { if (assignmentFirst) { expect(assignmentFirst.id).toBe(testingAssignmentID); expect(assignmentFirst.name).toBeDefined(); - expect(assignmentFirst.moduleId).toBeDefined(); + expect(assignmentFirst.section).toBeDefined(); expect(assignmentFirst.dueAt).toBeDefined(); // testing populated module field - const module = assignmentFirst.module; - expect(module.id).toBe(assignmentFirst.moduleId); - expect(module.moduleName).toBeDefined(); - expect(module.moduleNumber).toBeDefined(); - expect(module.duration).toBeDefined(); - expect(module.intro).toBeDefined(); - expect(module.numSlides).toBeDefined(); - expect(module.createdAt).toBeDefined(); - expect(moment(module.createdAt).isBefore(new Date())).toBe(true); - expect(module.updatedAt).toBeDefined(); - expect(moment(module.updatedAt).isBefore(new Date())).toBe(true); - expect(module.description).toBeDefined(); - expect(module.keywords).toBeDefined(); + const section = assignmentFirst.section; + expect(section.id).toBe(assignmentFirst.sectionId); + expect(section.sectionName).toBeDefined(); + expect(section.sectionNumber).toBeDefined(); + expect(section.duration).toBeDefined(); + expect(section.intro).toBeDefined(); + expect(section.numSlides).toBeDefined(); + expect(section.createdAt).toBeDefined(); + expect(moment(section.createdAt).isBefore(new Date())).toBe(true); + expect(section.updatedAt).toBeDefined(); + expect(moment(section.updatedAt).isBefore(new Date())).toBe(true); + expect(section.description).toBeDefined(); + expect(section.keywords).toBeDefined(); // testing populated assignment results field if (assignmentFirst.assignmentResults !== undefined) { @@ -287,8 +277,8 @@ describe("Collection", () => { const service = new ProgramService(prisma); const resolver = new ProgramResolver(service); - const deleteModule = async (id: string) => { - return prisma.module.delete({ + const deleteSection = async (id: string) => { + return prisma.section.delete({ where: { id } }); }; @@ -309,7 +299,7 @@ describe("Collection", () => { return resolver.createCollection(input); }; - const createModule = async (input: NewModule) => { + const createSection = async (input: NewSection) => { return resolver.create(input); }; @@ -323,18 +313,19 @@ describe("Collection", () => { let testingContentID: string; let testingCollectionID: string; - let testingModuleID: string; + let testingSectionID: string; let testingModuleID: string; afterAll(async () => { await deleteModule(testingModuleID); await deleteCollection(testingCollectionID); await deleteModule(testingModuleID); + await deleteSection(testingSectionID); }); test("should create dummy data structure", async () => { - const module = await createModule({ - moduleName: "Test Module", - moduleNumber: faker.datatype.number({ + const section = await createSection({ + sectionName: "Test Module", + sectionNumber: faker.datatype.number({ min: 123, max: 9999, precision: 1 @@ -345,17 +336,18 @@ describe("Collection", () => { description: "Test Description", keywords: ["Test Keywords"] }); - testingModuleID = module.id; + testingSectionID = section.id; const collection = await createCollection({ name: "Test Collection", - moduleID: testingModuleID, - positionIndex: 0 + positionIndex: 0, + sectionID: testingSectionID }); testingCollectionID = collection.id; const module = await createModule({ name: "Test Module", - collection: testingCollectionID + collection: testingCollectionID, + hours: 1 }); testingModuleID = module.id; @@ -375,13 +367,13 @@ describe("Collection", () => { }); test("should return a collection that belongs to the Module inputted", async () => { const collection = await resolver.collection({ - moduleID: testingModuleID + modules: [testingModuleID] }); expect(collection).toBeInstanceOf(Array); collection.map(async (col) => { expect(col.id).toBe(testingCollectionID); expect(col.name).toBeDefined(); - expect(col.moduleID).toBe(testingModuleID); + expect(col.moduleIDs).toContain(testingModuleID); }); }); test("should return a collection that partially matches the name passed in", async () => { @@ -421,7 +413,7 @@ describe("Collection", () => { expect(module.position === c.modules[module.position].position).toBe( true ); - expect(module.collectionID === c.id).toBe(true); + expect(module.collectionIDs.includes(c.id)).toBe(true); }); }); }); diff --git a/src/program/schema.graphql b/src/program/schema.graphql index 671efec2..274ef8ff 100644 --- a/src/program/schema.graphql +++ b/src/program/schema.graphql @@ -394,6 +394,10 @@ type Module { A list of learning objectives being covered in this Section """ objectives: [String!]! + """ + A list of keywords that most accurately describe the content of this Module + """ + keywords: [String!]! """ The number of carnige hours granted for completion of this Section """ @@ -785,6 +789,10 @@ type ModulePath { """ objectives: [String!]! """ + A list of keywords that most accurately describe the content of this Module + """ + keywords: [String!]! + """ The number of carnige hours granted for completion of this Section """ hours: Float! @@ -1176,7 +1184,7 @@ input ModuleInput { """ The ID of the parent collection """ - collection: ID! + collection: ID """ The index of the Module in the collection @@ -1186,7 +1194,8 @@ input ModuleInput { The list of learning objectives for this Section """ objectives: [String!] - """ + keywords: [String!] + """ The number of carnige hours granted for completing this Section """ hours: Float! @@ -1225,7 +1234,8 @@ input ModuleFields { Learning objectives that are taught in this Section """ objectives: [String!] - """ + keywords: [String!] + """ The number of carnige hours granted for completing this Section """ hours: Float @@ -1363,6 +1373,8 @@ type Mutation { deleteModule(id: String!): Module + deleteManyModule(id: [String!]!): Boolean + """ Create a Content Record """ diff --git a/src/progress/progress.spec.ts b/src/progress/progress.spec.ts index 29e3652f..79ad171a 100644 --- a/src/progress/progress.spec.ts +++ b/src/progress/progress.spec.ts @@ -6,9 +6,9 @@ import { Progress } from "@prisma/client"; import { test, describe, afterAll, expect } from "vitest"; import { createRandomModule, - createRandomModuleEnrollment, createRandomPlanOfStudy, createRandomProgress, + createRandomSectionEnrollment, createRandomUser, shuffle } from "../../utils"; @@ -153,7 +153,7 @@ describe("Progress", function () { describe("Waive module progress", function () { test("should waive a user's module and set it to completed", async function () { - const result = await resolver.waiveModule({ + const result = await resolver.waiveSection({ enrollmentID: enrollment.id }); if (result instanceof Error) throw new Error("Progress not waived."); @@ -169,7 +169,7 @@ const initializeTest = () => { const usr = createRandomUser(); const plan = createRandomPlanOfStudy(usr.id); const module = createRandomModule(); - const enrollment = createRandomModuleEnrollment(module.id, plan.id); + const enrollment = createRandomSectionEnrollment(module.id, plan.id); return [usr, plan, module, enrollment]; }; diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 4ad9d666..c597462f 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -373,18 +373,26 @@ export class QuizService { } }); - return Promise.all(results).then(() => - this.prisma.quizResult.create({ - data: { - score: score, - // answers: input.answers, - student: { connect: { id: plan } }, - quizInstance: { connect: { id: input.quizInstance } } - } - }) - ); + const res = Promise.all(results) + .then(() => + this.prisma.quizResult.create({ + data: { + score: score, + // answers: input.answers, + student: { connect: { id: plan } }, + quizInstance: { connect: { id: input.quizInstance } } + } + }) + ) + .catch((err) => { + return new Error(err.message); + }); //TODO: Add quiz grading logic // const questions = + + if (res instanceof Error) return new Error(res.message); + + return res; } async updateQuizScore(id: string, newScore: number) { diff --git a/src/quiz/quiz.spec.ts b/src/quiz/quiz.spec.ts index e453938b..ee3f71a5 100644 --- a/src/quiz/quiz.spec.ts +++ b/src/quiz/quiz.spec.ts @@ -6,16 +6,23 @@ import { createAnswer, createCollection, createModule, - createModule, + createPlan, createQuestion, createQuiz, + createQuizInstance, createRandomAnswer, createRandomModule, createRandomQuestion, - createRandomQuiz + createRandomQuiz, + createSection, + createUser } from "../../utils"; import { ProgramResolver, ProgramService } from "@/program"; import { faker } from "@faker-js/faker"; +import { UserService } from "@/user/user.service"; +import { UserResolver } from "@/user/user.resolver"; +import { PlanOfStudyResolver, PoSService } from "@/pos"; +import { QuizResult } from "@/types/graphql"; describe("Quiz Services", () => { // Init resolvers @@ -24,20 +31,32 @@ describe("Quiz Services", () => { const resolver: QuizResolver = new QuizResolver(service); const progServ: ProgramService = new ProgramService(prisma); const progResolver: ProgramResolver = new ProgramResolver(progServ); + const userService = new UserService(prisma); + const userResolver = new UserResolver(userService); + const planService = new PoSService(prisma); + const planResolver = new PlanOfStudyResolver(planService); // Make mock models for testing against - const testingAccountStudentID = "63e51cbd14406c6ad63f73a7"; - const testingAccountPlanID = "63e51cbd14406c6ad63f73a8"; - let fakeModule; let fakeCollection; + let fakeSection; let fakeModule; let fakeQuiz; let fakeQuestion; let fakeAnswer; - let fakeSubmission; + let fakeSubmission: QuizResult; + let fakeUser; + let fakePlan; + let fakeQuizInstance; afterAll(async () => { - await progResolver.delete(fakeModule.id); + await progResolver.deleteModule(fakeModule.id); + await progResolver.delete(fakeSection.id); + await resolver.deleteQuiz(fakeQuiz.id); + await resolver.deleteQuestion(fakeQuestion.id); + await resolver.deleteAnswer(fakeAnswer.id); + await resolver.deleteQuizResult(fakeSubmission.id); + await userResolver.delete(fakeUser.id); + await planResolver.deletePlan(fakePlan.id); }); test("should be defined", () => { @@ -46,26 +65,60 @@ describe("Quiz Services", () => { }); test("should create mock dependencies", async () => { + fakeUser = await createUser(userResolver, { + biography: faker.lorem.sentence(), + email: faker.internet.email(), + firstName: faker.name.firstName(), + openID: faker.datatype.uuid(), + lastName: faker.name.lastName(), + picURL: faker.image.imageUrl(), + middleName: faker.name.middleName(), + phoneNumber: faker.phone.number() + }); + + if (fakeUser instanceof Error) throw new Error(fakeUser.message); + + fakePlan = await createPlan(planResolver, { + userID: fakeUser.id + }); + + if (fakePlan instanceof Error) throw new Error(fakePlan.message); + fakeModule = await createModule(progResolver, { - moduleName: "testing3", - moduleNumber: 1003, + name: "testing3", + number: 1003, description: "Stuff", - duration: 10.0, - intro: "Intro", - keywords: ["Word", "other"], - numSlides: 10 + hours: faker.datatype.float({ + min: 0.25, + max: 3, + precision: 2 + }), + objectives: [faker.lorem.sentence()] + }); + + if (fakeModule instanceof Error) throw new Error(fakeModule.message); + + fakeSection = await createSection(progResolver, { + description: faker.lorem.sentence(), + intro: faker.lorem.sentence(), + numSlides: faker.datatype.number({ min: 1, max: 10 }), + duration: faker.datatype.number({ min: 1, max: 10 }), + keywords: [faker.lorem.word()], + sectionName: faker.lorem.word(), + sectionNumber: faker.datatype.number({ min: 1000, max: 9000 }) }); + if (fakeSection instanceof Error) throw new Error(fakeSection.message); + fakeCollection = await createCollection(progResolver, { name: "test", - moduleID: fakeModule.id, + modules: [fakeModule.id], + sectionID: fakeSection.id, positionIndex: 1 }); - fakeModule = await createModule( - progResolver, - createRandomModule(fakeCollection.id) - ); + if (fakeCollection instanceof Error) + throw new Error(fakeCollection.message); }); describe("Creates", () => { @@ -116,21 +169,27 @@ describe("Quiz Services", () => { expect(end).toEqual(start + 1); }); }); + describe("Mutation.createQuizInstance", () => { + test("should create a QuizInstance record", async () => { + fakeQuizInstance = await createQuizInstance(resolver, fakeQuiz.id); + if (fakeQuizInstance instanceof Error) + throw new Error(fakeQuizInstance.message); + expect(fakeQuizInstance).toBeDefined(); + expect(fakeQuizInstance.id).toBeDefined(); + expect(fakeQuizInstance.quiz.id).toEqual(fakeQuiz.id); + }); + }); describe("Mutation.submitQuiz", () => { test("should create a QuizResult record", async () => { - const answers: string[] = []; - for (let i = 0; i < fakeQuiz.numQuestions; i++) { - answers.push(faker.datatype.string(1)); - } + const answers: string[] = [fakeAnswer.id]; const result = await resolver.submitQuiz({ - student: testingAccountStudentID, - quiz: fakeQuiz.id, + student: fakeUser.id, + quizInstance: fakeQuizInstance.id, answers }); if (result instanceof Error) return new Error(result.message); fakeSubmission = result; expect(result).toBeDefined(); - expect(result.answers.length).toEqual(fakeQuiz.numQuestions); expect(result.score).toBeGreaterThanOrEqual(0.0); expect(result.score).toBeLessThanOrEqual(100.0); }); @@ -206,7 +265,7 @@ describe("Quiz Services", () => { variant: fakeQuestion.variant, text: fakeQuestion.text, points: fakeQuestion.points, - parentPool: fakeQuestion.parentID + parent: fakeQuestion.parentID }); expect(questions).toBeDefined(); expect(questions.length).toBeGreaterThanOrEqual(1); @@ -282,13 +341,13 @@ describe("Quiz Services", () => { const results = await resolver.quizResult({ score: fakeSubmission.score, student: fakeSubmission.studentID, - quiz: fakeSubmission.quizID + quizInstance: fakeSubmission.quizID }); expect(results).toBeDefined(); results.map((result) => { expect(result.score).toEqual(fakeSubmission.score); expect(result.studentID).toEqual(fakeSubmission.studentID); - expect(result.quizID).toEqual(fakeSubmission.quizID); + expect(result.quizInstanceID).toEqual(fakeSubmission.quizID); }); }); }); diff --git a/utils/fakes.ts b/utils/fakes.ts index ef754d45..d7ad7814 100644 --- a/utils/fakes.ts +++ b/utils/fakes.ts @@ -34,7 +34,7 @@ export function createRandomUser(): User { middleName: faker.name.middleName(), watchedThreadIDs: [faker.database.mongodbObjectId()], biography: faker.lorem.lines(1), - phoneNumber: faker.phone.phoneNumber(), + phoneNumber: faker.phone.number(), upvotedThreadIDs: [faker.database.mongodbObjectId()], groupMembershipIDs: [faker.database.mongodbObjectId()] }; diff --git a/utils/tests.ts b/utils/tests.ts index 9d0cc1e2..ca76acce 100644 --- a/utils/tests.ts +++ b/utils/tests.ts @@ -7,10 +7,12 @@ import { CreateQuiz, EnrollmentStatus, ModuleInput, + NewUser, UserRole } from "@/types/graphql"; import { QuizResolver } from "@/quiz/quiz.resolver"; -import { Answer, Module, Question, Quiz } from "@prisma/client"; +import { Answer, Question, Quiz } from "@prisma/client"; +import { UserResolver } from "@/user/user.resolver"; export const shuffle = (str: string) => [...str].sort(() => Math.random() - 0.5).join(""); @@ -35,6 +37,12 @@ export const createPlan = async ( } else return self[0]; }; +export const createUser = async (resolver: UserResolver, config: NewUser) => { + const user = await resolver.create({ ...config }); + if (user) return user; + else return new Error("Failed to create user"); +}; + export const createSection = async ( resolver: ProgramResolver, config: { @@ -48,22 +56,17 @@ export const createSection = async ( } ) => { const section = await resolver.create({ ...config }); - if (section) return section; - else return new Error("Failed to create section"); + if (!section) return new Error("Failed to create section"); + return section; }; export const createModule = async ( resolver: ProgramResolver, - config: Module + config: ModuleInput ) => { - const data: ModuleInput = { - name: config.name, - collection: config.collectionIDs[0], - hours: config.hours - }; - const module = await resolver.createModule({ ...data }); - if (data) return module; - else return new Error("Failed to create Module"); + const module = await resolver.createModule({ ...config }); + if (!module) return new Error("Failed to create Module"); + return module; }; export const createEnrollment = async ( @@ -104,6 +107,15 @@ export const createQuiz = async (resolver: QuizResolver, input: Quiz) => { else return new Error("Failed to create Quiz"); }; +export const createQuizInstance = async ( + resolver: QuizResolver, + quizID: string +) => { + const quizInstance = await resolver.createQuizInstance(quizID); + if (quizInstance) return quizInstance; + else return new Error("Failed to create quiz instance"); +}; + export const createQuestion = async ( resolver: QuizResolver, input: Question From 062813a723a70dcbe1cb21d52a5d1b18e73bc3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 24 May 2023 11:48:26 -0400 Subject: [PATCH 29/31] fix(lp): adjusted filtering method to remove empty objects Previously the resolver was only expecting null values to be present in the payload array, but empty objects can also be appended under edge cases. After updating the filter statement, the resolver functions as expected. --- src/program/program.resolver.ts | 74 ++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index 4903443c..9252a229 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -24,7 +24,8 @@ import { Module, SimpleModuleFlow, CollectionPath, - SectionPath + SectionPath, + PathStatus } from "@/types/graphql"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { ProgramService } from "./program.service"; @@ -171,20 +172,25 @@ export class ProgramResolver { return latestModuleProgress; } + /** + * 1. find enrollments for the student from planID + * 2. find the learning path for the plan + * 3. filter out inactive paths + * 4. filter out modules with a completed progress status + * 5. return the incomplete list of modules that match the student's learning path + * @param planID - the document ID of the plan of study that belongs to the student + */ @Query("modulesFromLearningPath") async modulesFromLearningPath(@Args("planID") planID: string) { - // find enrollments for the student from planID - // find the learning path for the plan - // filter out inactive paths - // filter out modules with a completed progress status - // return the incomplete list of modules that match the student's learning path - + // fetch all enrollments by the given student const enrollment = await this.programService.sectionEnrollment({ plan: planID }); + // fetch the LP model from the DB const learningPath = await this.programService.learningPath(planID); + // retrieve all the live paths for the user's LP model const filteredLearningPath = learningPath[0].paths.filter((path) => { return path.status === "LIVE"; }); @@ -204,32 +210,40 @@ export class ProgramResolver { .flat() .flat(); - const nonDuplicateModules = modules.filter((module, index, self) => { - return index === self.findIndex((m) => m.id === module.id); - }); + const cleanedModulesList = modules.filter( + (module) => Object.keys(module).length !== 0 + ); - const filteredModules = nonDuplicateModules.map((module) => { - // if there is no progress for the module, return the module - if ( - module.moduleProgress?.length === 0 || - module.moduleProgress === null || - typeof module.moduleProgress === "undefined" - ) - return module; - else { - // if there is progress for the module, filter out the progresses made by other students - // and return the module if there is no progress for the student - const filteredProgress = module.moduleProgress.filter((progress) => { - if (!progress) return false; - else if (!progress.completed) return false; - else return progress.enrollment.id !== enrollment[0].id; - }); - if (filteredProgress.length === 0) return module; - else return null; + const nonDuplicateModules = cleanedModulesList.filter( + (module, index, self) => { + return index === self.findIndex((m) => m.id === module.id); } - }); + ); + + const filteredModules = nonDuplicateModules + .map((module) => { + // if there is no progress for the module, return the module + if ( + module.moduleProgress?.length === 0 || + module.moduleProgress === null || + typeof module.moduleProgress === "undefined" + ) + return module; + else { + // if there is progress for the module, filter out the progresses made by other students + // and return the module if there is no progress for the student + const filteredProgress = module.moduleProgress.filter((progress) => { + if (!progress) return false; + else if (!progress.completed) return false; + else return progress.enrollment.id !== enrollment[0].id; + }); + if (filteredProgress.length === 0) return module; + else return null; + } + }) + .filter((module) => module !== null); - return filteredModules.filter((module) => module !== null); + return filteredModules; } @Query("moduleFlowFromLearningPath") From fb936a51176548a3e2ec35480e3ba499d94b9214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 24 May 2023 14:05:26 -0400 Subject: [PATCH 30/31] fix(flow): reworked previous module logic There was edge cases that were not being handled correctly when looking up the previous module and collection --- src/program/program.resolver.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index 9252a229..c921c223 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -25,7 +25,8 @@ import { SimpleModuleFlow, CollectionPath, SectionPath, - PathStatus + PathStatus, + ModulePath } from "@/types/graphql"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { ProgramService } from "./program.service"; @@ -303,6 +304,7 @@ export class ProgramResolver { let nextCollection: CollectionPath | null = filteredCollection; let previousCollection: CollectionPath | null = filteredCollection; const currentSection: SectionPath = sections[currentSectionIndex]; + let previousModule: ModulePath | null = null; if (currentModuleIndex === 0) { let previousSection = sections[currentSectionIndex - 1]; @@ -357,6 +359,16 @@ export class ProgramResolver { const nextModule = filteredCollection.modules[currentModuleIndex + 1]; + if ( + filteredCollection.modules[currentModuleIndex - 1] === undefined && + previousCollection !== null + ) { + previousModule = + previousCollection.modules[previousCollection.modules.length - 1]; + } else { + previousModule = filteredCollection.modules[currentModuleIndex - 1]; + } + return { currentModule: filteredCollection.modules[currentModuleIndex], nextModule: nextModule, From f1259375a8914ae5350576997397ea55d308c608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20B=2E=20Papp?= Date: Wed, 24 May 2023 14:06:30 -0400 Subject: [PATCH 31/31] fix(flow): value being returned was incorrect --- src/program/program.resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/program/program.resolver.ts b/src/program/program.resolver.ts index c921c223..0be91f81 100644 --- a/src/program/program.resolver.ts +++ b/src/program/program.resolver.ts @@ -374,7 +374,7 @@ export class ProgramResolver { nextModule: nextModule, currentCollection: filteredCollection, nextCollection: filteredCollection, - previousModule: filteredCollection.modules[currentModuleIndex - 1], + previousModule, previousCollection, currentSection };