diff --git a/prisma/migrations/20240622123027_quiz_notes/migration.sql b/prisma/migrations/20240622123027_quiz_notes/migration.sql new file mode 100644 index 0000000..cac2150 --- /dev/null +++ b/prisma/migrations/20240622123027_quiz_notes/migration.sql @@ -0,0 +1,19 @@ +-- CreateEnum +CREATE TYPE "QuizNoteType" AS ENUM ('ILLEGIBLE'); + +-- CreateTable +CREATE TABLE "quiz_note" ( + "id" TEXT NOT NULL, + "quiz_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "note_type" "QuizNoteType" NOT NULL, + "submitted_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "quiz_note_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "quiz_note" ADD CONSTRAINT "quiz_note_quiz_id_fkey" FOREIGN KEY ("quiz_id") REFERENCES "quiz"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "quiz_note" ADD CONSTRAINT "quiz_note_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 332381d..1eed829 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,6 +30,7 @@ model Quiz { completions QuizCompletion[] images QuizImage[] + notes QuizNote[] uploadedByUser User @relation(fields: [uploadedByUserId], references: [id]) @@unique(fields: [date, type]) @@ -79,6 +80,19 @@ model QuizCompletionUser { @@map("quiz_completion_user") } +model QuizNote { + id String @id + quizId String @map("quiz_id") + userId String @map("user_id") + noteType QuizNoteType @map("note_type") + submittedAt DateTime @map("submitted_at") + + quiz Quiz @relation(fields: [quizId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@map("quiz_note") +} + model User { id String @id email String @@ -86,11 +100,16 @@ model User { roles UserRole[] quizCompletions QuizCompletionUser[] + quizNotes QuizNote[] uploadedQuizzes Quiz[] @@map("user") } +enum QuizNoteType { + ILLEGIBLE +} + enum Role { USER ADMIN diff --git a/src/gql.ts b/src/gql.ts index 4578759..fe8122b 100644 --- a/src/gql.ts +++ b/src/gql.ts @@ -128,6 +128,17 @@ const typeDefs = gql` averageScorePercentage: Float! } + enum ExcludeIllegibleOptions { + """ + Exclude quizzes that have been marked as illegible by me. + """ + ME + """ + Exclude quizzes that have been marked as illegible by anyone. + """ + ANYONE + } + "Available filters for the quizzes query" input QuizFilters { """ @@ -135,6 +146,10 @@ const typeDefs = gql` If provided, only quizzes completed by none of these users will be included in the results. """ excludeCompletedBy: [String] + """ + Optional option to exclude quizzes that have been marked as illegible. + """ + excludeIllegible: ExcludeIllegibleOptions } type Query { @@ -171,6 +186,7 @@ const typeDefs = gql` type Mutation { createQuiz(type: QuizType!, date: Date!, files: [CreateQuizFile]): CreateQuizResult completeQuiz(quizId: String!, completedBy: [String]!, score: Float!): CompleteQuizResult + markQuizIllegible(quizId: String!): Boolean } `; diff --git a/src/quiz/quiz.dto.ts b/src/quiz/quiz.dto.ts index 9c6007e..a9eaa4d 100644 --- a/src/quiz/quiz.dto.ts +++ b/src/quiz/quiz.dto.ts @@ -37,6 +37,7 @@ export interface QuizDetails { export interface QuizFilters { excludeCompletedBy?: string[]; + excludeIllegible?: 'ME' | 'ANYONE'; } export interface CreateQuizResult { diff --git a/src/quiz/quiz.gql.ts b/src/quiz/quiz.gql.ts index 1f663f5..435a3ea 100644 --- a/src/quiz/quiz.gql.ts +++ b/src/quiz/quiz.gql.ts @@ -58,6 +58,16 @@ async function completeQuiz( }); } +async function markQuizIllegible( + _: unknown, + { quizId }: { quizId: string }, + context: QuizlordContext, +): Promise { + authorisationService.requireUserRole(context, 'USER'); + quizService.markQuizIllegible(quizId, context.email); + return true; +} + export const quizQueries = { quizzes, quiz, @@ -65,4 +75,5 @@ export const quizQueries = { export const quizMutations = { createQuiz, completeQuiz, + markQuizIllegible, }; diff --git a/src/quiz/quiz.persistence.ts b/src/quiz/quiz.persistence.ts index 1463a8f..d090637 100644 --- a/src/quiz/quiz.persistence.ts +++ b/src/quiz/quiz.persistence.ts @@ -1,4 +1,5 @@ -import { Quiz, QuizImage } from '@prisma/client'; +import { Quiz, QuizImage, QuizNoteType } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; import { PrismaService } from '../database/prisma.service'; import { QuizFilters } from './quiz.dto'; @@ -70,6 +71,23 @@ export class QuizPersistence { }, }, }), + ...(filters.excludeIllegible === 'ME' && { + notes: { + none: { + noteType: 'ILLEGIBLE', + user: { + email: userEmail, + }, + }, + }, + }), + ...(filters.excludeIllegible === 'ANYONE' && { + notes: { + none: { + noteType: 'ILLEGIBLE', + }, + }, + }), }, }); @@ -205,4 +223,37 @@ export class QuizPersistence { }); return slicePagedResults(result, limit, afterId !== undefined); } + + async addQuizNote({ + quizId, + noteType, + userEmail, + submittedAt, + }: { + quizId: string; + noteType: QuizNoteType; + userEmail: string; + submittedAt: Date; + }) { + const user = await this.#prisma.client().user.findFirst({ + where: { + email: userEmail, + }, + }); + + if (!user) { + throw new Error(`Unable to find user with email ${userEmail}`); + } + + const noteId = uuidv4(); + await this.#prisma.client().quizNote.create({ + data: { + id: noteId, + noteType, + quizId, + userId: user.id, + submittedAt, + }, + }); + } } diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 6b4379e..e7154c8 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -276,4 +276,12 @@ export class QuizService { }, }; } + async markQuizIllegible(quizId: string, userEmail: string): Promise { + await this.#persistence.addQuizNote({ + quizId, + noteType: 'ILLEGIBLE', + submittedAt: new Date(), + userEmail, + }); + } }