diff --git a/.vscode/snips.code-snippets b/.vscode/snips.code-snippets index 995d03189..75330d45b 100644 --- a/.vscode/snips.code-snippets +++ b/.vscode/snips.code-snippets @@ -10,18 +10,18 @@ "scope": "javascript,typescript", "prefix": "depr", "body": ["/**", "* @deprecated $0", "*/"], - "description": "Log output to console", + "description": "Mark a feature as deprecated", }, "Page": { - "prefix": "pasy", - "body": [ - "export default async function Page() {", - " return (<>${0});", - "}" - ], - "description": "Creates a page server component with no params", - }, + "prefix": "pasy", + "body": [ + "export default async function Page() {", + " return (<>${0});", + "}", + ], + "description": "Creates a page server component with no params", + }, "Instance Page": { "prefix": "inpag", @@ -67,4 +67,15 @@ "export default ${0};", ], }, + + "Enum": { + "prefix": "enm", + "body": [ + "export const $1 = {", + "\t$2", + "} as const;", + "", + "export type $1 = keyof typeof $1;", + ], + }, } diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..7239a0a67 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,102 @@ +## Architecture + +The SPA system is comprised of two main repositories: + +The first is the matching service + +This is a FastAPI (python) app which simply serves out a series of pre-defined algorithms. +All of the difficult logic being handled by libraries like networkx or matchingproblems. +As the name implies, it is used to handle matching problems, and only needs to be updated if there are new matching algorithms to add. + +--- + +The second repository (this one) is a nextJS application which handles all the CRUD + +We broadly follow the T3 stack and architecture; +more details can be found on [the t3 website](https://create.t3.gg/en/folder-structure-app?packages=prisma%2Ctailwind%2Ctrpc). +This link has the correct packages already selected. + +The main components within this are: + +- the database schema, + + - We use [prisma](https://www.prisma.io/) as our ORM + - contained in `/prisma` + +- the backend, + + - We use [tRPC](https://trpc.io/) + - contained in `/src/router...` + - Most of the logic in the procedures in reality lives in data objects ([see below](#data-objects)) + +- the frontend, + - A set of [React](https://react.dev/) components, one for each page (see [NextJS](https://nextjs.org/)) + - contained in `/src/app` + - This is the display layer; in general, very little business logic lives here. + Anything here can run on the client, so critical information needs to be controlled and restricted so + +Some other critical dependencies: + +- [Tailwind](https://tailwindcss.com/) for styling +- [Zod](https://tailwindcss.com/) for runtime data validation +- [ShadCN UI](https://ui.shadcn.com/) for many common UI tasks + +Still, there are a few quirks unique to this application that we hand-rolled, which I will discuss in some more detail: + +### Data objects + +Previously, database calls were all done in the top level of procedures. +There were several issues with this - a big one being that it was difficult to re-use code. + +This has been refactored, and we now use a system of data objects. These can be found in `/src/data-objects/`. + +The idea is to organise functions by the objects they refer to; business logic related to a kind of object is encapsulated in the corresponding data object. + +It's driven lots of very nice patterns; for instance, the authentication middleware we use is only possible because of this design. + +In reality, the data objects are sometimes a bit messy; significant parts are not very well organised, and need cleaning up. +Maybe one day we will get around to it, but it hasn't been a priority. +We aim to remove all database calls from the tRPC procedures, and instead concentrate them in the data objects. + +Generally, you will not need to import these directly; the relevant objects will be injected into the tRPC by the middleware. + +### tRPC middlewares + +We use a custom set of middlewares for tRPC. +These need to change very rarely. + +The main idea is to abstract away a lot of the tedious boilerplate that is present in many procedures. + +If you wanted, for example, to write a procedure which updates an allocation instance, then you would need to +accept the parameters which specify the instance (it's ID, essentially) and then query the database based on these. + +Any procedure pertaining to an instance has to do this, and so we can save a lot of work by abstracting it away. This is what a middleware does. It adds the necessary input parameters and injects objects into the tRPC context. + +We also have authentication middlewares - which automatically check that the user has the correct authorisation and inject a corresponding user object. If authentication fails, the procedure will error. Generally, you should try to make sure that your procedures are properly scoped wrt authorisation - it's critical to avoid accidental data leaks. + +### Data transfer objects + +There are many places in the application where _almost_ the same data is required, though it may differ slightly. +Creating types for each individual use case proved very messy; there were lots of interfaces floating around that were used only once, +and it made knowing what data was on each hard to track. +Now, instead, for each type of object there is a single type that contains all the data for that object type, +and we always pass around the full objects. + +These large canonical types are called Data Transfer Objects (DTOs). + +If you are presenting data relating to e.g. a student, you can use the `StudentDTO` type, +rather than having to carefully hand-craft the type for what you need. +Whilst this is a little less efficient - you may end up passing around data you don't strictly need - the benefits to code style and legibility are substantial. +It makes moving around different parts of the application much easier, since the types are always the same. + +--- + +## Deployment + +The matching service and the allocation app are each packaged into a docker image. +We use docker compose to orchestrate them, along with a few chron jobs for tasks that need to run regularly. +At some point in the future, we might move this into it's own container. + +Some extra details need to be provided via environment variables. +Details of which ones are needed and what they should be can be found in `\src\env.ts`. Any variable with `.optional()` or `.default(_)` can be omitted. +`.env.example` contains an example env file. diff --git a/docs/dev/design.md b/docs/dev/design.md deleted file mode 100644 index 1f991f536..000000000 --- a/docs/dev/design.md +++ /dev/null @@ -1,6 +0,0 @@ -- backups - -> solution in sight, but some work needed. - - - automate with cron job - - store on uni git server - - version control it diff --git a/docs/dev/style-guide.md b/docs/dev/style-guide.md index f3df458ed..fdae1c2f8 100644 --- a/docs/dev/style-guide.md +++ b/docs/dev/style-guide.md @@ -92,3 +92,53 @@ function QueryManager({ initialData }: { initialData: TData }) { return
{data}
; } ``` + +## Enums + +When declaring an enum type, don't use magic strings or + +Instead, use the following pattern: + +```ts +export const MyEnum = { + OPTION_A: "OPTION_A", + OPTION_B: "OPTION_B", + OPTION_C: "OPTION_C", + // ... etc +} as const; + +export type MyEnum = keyof typeof MyEnum; +``` + +- The dictionary and type names should both be in PascalCase and must match +- Enum values should be in `SCREAMING_SNAKE_CASE` +- The key and the value in the defining dict should match exactly. +- Export the inferred type on an additional line + +There's a snippet in the repository for setting this up - the prefix is `enm`. + +When referring to an enum value, use the dictionary + +> Good: +> +> ```ts +> fun(MyEnum.OPTION_A); +> ``` + +> Bad: +> +> ```ts +> fun("OPTION_A"); +> ``` + +When necessary, you should generate the related zod schema like so: + +```ts +export const myEnumSchema = z.enum([ + MyEnum.OPTION_A, + MyEnum.OPTION_B, + MyEnum.OPTION_C, +]); +``` + +- The name of the schema should be the name of the enum in camelCase with `Schema` appended to the end. diff --git a/prisma/schema/marking.prisma b/prisma/schema/marking.prisma index b89708695..026e43ef9 100644 --- a/prisma/schema/marking.prisma +++ b/prisma/schema/marking.prisma @@ -3,91 +3,120 @@ enum MarkerType { READER } -model UnitOfAssessment { - id String @id @default(uuid()) - flagId String @map("flag_id") - title String - open Boolean @default(false) - studentSubmissionDeadline DateTime @map("student_submission_deadline") - markerSubmissionDeadline DateTime @map("marker_submission_deadline") - weight Int - allowedMarkerTypes MarkerType[] - // --- - allocationGroupId String @map("allocation_group_id") - allocationSubGroupId String @map("allocation_sub_group_id") - allocationInstanceId String @map("allocation_instance_id") - // --- - flag Flag @relation(fields: [flagId, allocationGroupId, allocationSubGroupId, allocationInstanceId], references: [id, allocationGroupId, allocationSubGroupId, allocationInstanceId], onDelete: Cascade, map: "unit_flag") - allocationInstance AllocationInstance @relation(fields: [allocationGroupId, allocationSubGroupId, allocationInstanceId], references: [allocationGroupId, allocationSubGroupId, id], onDelete: Cascade, map: "unit_instance") - assessmentCriteria AssessmentCriterion[] - markerSubmissions MarkingSubmission[] - finalUnitOfAssessmentGrades FinalUnitOfAssessmentGrade[] +enum MarkingStatus { + PENDING + DONE + MODERATE + NEGOTIATE +} - @@map("unit_of_assessment") +enum MarkingMethod { + AUTO + OVERRIDE + NEGOTIATED + MODERATED } -model AssessmentCriterion { - id String @id @default(uuid()) - unitOfAssessmentId String @map("unit_of_assessment_id") +enum FinalGradeMethod { + AUTO + OVERRIDE +} + +model MarkingComponent { + id String @id @default(uuid()) + unitOfAssessmentId String @map("unit_of_assessment_id") title String description String weight Int - layoutIndex Int @map("layout_index") + layoutIndex Int @map("layout_index") // --- - unitOfAssessment UnitOfAssessment @relation(fields: [unitOfAssessmentId], references: [id], onDelete: Cascade, map: "criterion_unit") - scores CriterionScore[] + unitOfAssessment UnitOfAssessment @relation(fields: [unitOfAssessmentId], references: [id], onDelete: Cascade, map: "marking_component_uoa") + scores MarkingComponentSubmission[] @@unique([title, unitOfAssessmentId]) - @@map("assessment_criterion") + @@map("marking_component") } -model MarkingSubmission { - summary String - grade Int - recommendedForPrize Boolean @default(false) @map("recommended_for_prize") - draft Boolean - markerId String @map("marker_id") - studentId String @map("student_id") - unitOfAssessmentId String @map("unit_of_assessment_id") +model MarkingComponentSubmission { + markerId String @map("marker_id") // marker type? + studentId String @map("student_id") + unitOfAssessmentId String @map("unit_of_assessment_id") + grade Int + justification String // --- - unitOfAssessment UnitOfAssessment @relation(fields: [unitOfAssessmentId], references: [id], onDelete: Cascade, map: "submission_unit") - criterionScores CriterionScore[] + markingComponentId String @map("marking_component_id") + // --- + markingComponent MarkingComponent @relation(fields: [markingComponentId], references: [id], onDelete: Cascade, map: "marking_component_submission_marking_component") + submission UnitOfAssessmentSubmission @relation(fields: [markerId, studentId, unitOfAssessmentId], references: [markerId, studentId, unitOfAssessmentId], onDelete: Cascade, map: "marking_component_submission_uoa_submission") - @@id([markerId, studentId, unitOfAssessmentId], name: "studentMarkerSubmission") - @@map("assessment_marking_submission") + @@id([markerId, studentId, markingComponentId], name: "markingComponentSubmission") + @@map("marking_component_submission") } -model CriterionScore { - markerId String @map("marker_id") - studentId String @map("student_id") - unitOfAssessmentId String @map("unit_of_assessment_id") - grade Int - justification String +model UnitOfAssessment { + id String @id @default(uuid()) + flagId String @map("flag_id") + title String + open Boolean @default(false) + defaultStudentSubmissionDeadline DateTime @map("default_student_submission_deadline") + // Days to mark + markerSubmissionDeadline DateTime @map("marker_submission_deadline") + defaultWeight Int @map("default_weight") + allowedMarkerTypes MarkerType[] // --- - assessmentCriterionId String @map("assessment_component_id") + allocationGroupId String @map("allocation_group_id") + allocationSubGroupId String @map("allocation_sub_group_id") + allocationInstanceId String @map("allocation_instance_id") // --- - criterion AssessmentCriterion @relation(fields: [assessmentCriterionId], references: [id], onDelete: Cascade, map: "score_criterion") - submission MarkingSubmission @relation(fields: [markerId, studentId, unitOfAssessmentId], references: [markerId, studentId, unitOfAssessmentId], onDelete: Cascade, map: "score_submission") + flag Flag @relation(fields: [flagId, allocationGroupId, allocationSubGroupId, allocationInstanceId], references: [id, allocationGroupId, allocationSubGroupId, allocationInstanceId], onDelete: Cascade, map: "unit_flag") + allocationInstance AllocationInstance @relation(fields: [allocationGroupId, allocationSubGroupId, allocationInstanceId], references: [allocationGroupId, allocationSubGroupId, id], onDelete: Cascade, map: "uoa_instance") + markingComponents MarkingComponent[] + markerSubmissions UnitOfAssessmentSubmission[] + grades UnitOfAssessmentGrade[] - @@id([markerId, studentId, assessmentCriterionId], name: "markingCriterionSubmission") - @@map("criterion_score") + @@map("unit_of_assessment") } -model FinalUnitOfAssessmentGrade { +model UnitOfAssessmentSubmission { + summary String + grade Int + recommendedForPrize Boolean @default(false) @map("recommended_for_prize") + draft Boolean + markerId String @map("marker_id") + studentId String @map("student_id") + unitOfAssessmentId String @map("unit_of_assessment_id") + // --- + unitOfAssessment UnitOfAssessment @relation(fields: [unitOfAssessmentId], references: [id], onDelete: Cascade, map: "uoa_submission_uoa") + criterionScores MarkingComponentSubmission[] + + @@id([markerId, studentId, unitOfAssessmentId], name: "uoaSubmissionId") + @@map("unit_of_assessment_submission") +} + +model UnitOfAssessmentGrade { unitOfAssessmentId String @map("unit_of_assessment_id") unitOfAssessment UnitOfAssessment @relation(fields: [unitOfAssessmentId], references: [id], onDelete: Cascade, map: "final_unit_grade_unit") studentId String @map("student_id") + // --- grade Int comment String + status MarkingStatus @default(PENDING) + method MarkingMethod @default(AUTO) + // + submitted Boolean @default(false) + customDueDate DateTime? @map("custom_due_date") + customWeight Int? @map("custom_weight") - @@id([studentId, unitOfAssessmentId], name: "studentAssessmentGrade") - @@map("final_unit_of_assessment_grade") + @@id([studentId, unitOfAssessmentId], name: "uoaGradeId") + @@map("unit_of_assessment_grade") } model FinalGrade { id String @id @default(uuid()) studentId String @map("student_id") grade Int + comment String @default("") + method FinalGradeMethod @default(AUTO) // --- allocationGroupId String @map("allocation_group_id") allocationSubGroupId String @map("allocation_sub_group_id") diff --git a/prisma/schema/migrations/20260205163110_marking/migration.sql b/prisma/schema/migrations/20260205163110_marking/migration.sql new file mode 100644 index 000000000..fe68e6f59 --- /dev/null +++ b/prisma/schema/migrations/20260205163110_marking/migration.sql @@ -0,0 +1,58 @@ +/* + Warnings: + + - fixed + +*/ +-- CreateEnum +CREATE TYPE "MarkingStatus" AS ENUM ('PENDING', 'DONE', 'MODERATE', 'NEGOTIATE'); + +-- CreateEnum +CREATE TYPE "MarkingMethod" AS ENUM ('AUTO', 'OVERRIDE', 'NEGOTIATED', 'MODERATED'); + +-- CreateEnum +CREATE TYPE "FinalGradeMethod" AS ENUM ('AUTO', 'OVERRIDE'); + +-- DropForeignKey +ALTER TABLE "criterion_score" DROP CONSTRAINT "score_criterion"; + +-- AlterTable +ALTER TABLE "criterion_score" + +RENAME COLUMN "assessment_component_id" to "marking_component_id"; + +-- AlterTable +ALTER TABLE "final_grade" ADD COLUMN "comment" TEXT NOT NULL DEFAULT '', +ADD COLUMN "method" "FinalGradeMethod" NOT NULL DEFAULT 'AUTO'; + +-- AlterTable +ALTER TABLE "final_unit_of_assessment_grade" ADD COLUMN "custom_due_date" TIMESTAMP(3), +ADD COLUMN "custom_weight" INTEGER, +ADD COLUMN "method" "MarkingMethod" NOT NULL DEFAULT 'AUTO', +ADD COLUMN "status" "MarkingStatus" NOT NULL DEFAULT 'DONE', +ADD COLUMN "submitted" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "student_details" ADD COLUMN "enrolled" BOOLEAN NOT NULL DEFAULT true; + +-- AlterTable +ALTER TABLE "unit_of_assessment" +RENAME COLUMN "student_submission_deadline" to "default_student_submission_deadline"; + +ALTER TABLE "unit_of_assessment" +RENAME COLUMN "weight" to "default_weight"; + +-- RenameForeignKey +ALTER TABLE "assessment_criterion" RENAME CONSTRAINT "criterion_unit" TO "marking_component_uoa"; + +-- RenameForeignKey +ALTER TABLE "assessment_marking_submission" RENAME CONSTRAINT "submission_unit" TO "uoa_submission_uoa"; + +-- RenameForeignKey +ALTER TABLE "criterion_score" RENAME CONSTRAINT "score_submission" TO "marking_component_submission_uoa_submission"; + +-- RenameForeignKey +ALTER TABLE "unit_of_assessment" RENAME CONSTRAINT "unit_instance" TO "uoa_instance"; + +-- AddForeignKey +ALTER TABLE "criterion_score" ADD CONSTRAINT "marking_component_submission_marking_component" FOREIGN KEY ("marking_component_id") REFERENCES "assessment_criterion"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema/migrations/20260205164917_marking_rename/migration.sql b/prisma/schema/migrations/20260205164917_marking_rename/migration.sql new file mode 100644 index 000000000..20f74aaa2 --- /dev/null +++ b/prisma/schema/migrations/20260205164917_marking_rename/migration.sql @@ -0,0 +1,19 @@ +/* + Warnings: + +*/ +-- RenameTable +ALTER TABLE "assessment_criterion" +RENAME TO "marking_component"; + +-- RenameTable +ALTER TABLE "criterion_score" +RENAME TO "marking_component_submission"; + +-- RenameTable +ALTER TABLE "assessment_marking_submission" +RENAME TO "unit_of_assessment_submission"; + +-- RenameTable +ALTER TABLE "final_unit_of_assessment_grade" +RENAME TO "unit_of_assessment_grade"; diff --git a/prisma/schema/migrations/20260205165612_marking_residual/migration.sql b/prisma/schema/migrations/20260205165612_marking_residual/migration.sql new file mode 100644 index 000000000..1a43c2fe3 --- /dev/null +++ b/prisma/schema/migrations/20260205165612_marking_residual/migration.sql @@ -0,0 +1,16 @@ +-- AlterTable +ALTER TABLE "marking_component" RENAME CONSTRAINT "assessment_criterion_pkey" TO "marking_component_pkey"; + +-- AlterTable +ALTER TABLE "marking_component_submission" RENAME CONSTRAINT "criterion_score_pkey" TO "marking_component_submission_pkey"; + +-- AlterTable +ALTER TABLE "unit_of_assessment_grade" RENAME CONSTRAINT "final_unit_of_assessment_grade_pkey" TO "unit_of_assessment_grade_pkey"; + +ALTER TABLE "unit_of_assessment_grade" ALTER COLUMN "status" SET DEFAULT 'PENDING'; + +-- AlterTable +ALTER TABLE "unit_of_assessment_submission" RENAME CONSTRAINT "assessment_marking_submission_pkey" TO "unit_of_assessment_submission_pkey"; + +-- RenameIndex +ALTER INDEX "assessment_criterion_title_unit_of_assessment_id_key" RENAME TO "marking_component_title_unit_of_assessment_id_key"; diff --git a/prisma/schema/user.prisma b/prisma/schema/user.prisma index fea47f2e8..a9dffcfd4 100644 --- a/prisma/schema/user.prisma +++ b/prisma/schema/user.prisma @@ -27,6 +27,7 @@ model StudentDetails { submittedPreferences StudentSubmittedPreference[] matchingPairs MatchingPair[] preAllocatedProjects Project[] + enrolled Boolean @default(true) @@id([userId, allocationGroupId, allocationSubGroupId, allocationInstanceId], name: "studentDetailsId", map: "student_details_id") @@map("student_details") diff --git a/src/data-objects/space/instance.ts b/src/data-objects/space/instance.ts index 01b8b2b3e..829f9df35 100644 --- a/src/data-objects/space/instance.ts +++ b/src/data-objects/space/instance.ts @@ -65,7 +65,7 @@ export class AllocationInstance extends DataObject { return await this.db.unitOfAssessment .findFirstOrThrow({ where: { id: unitOfAssessmentId }, - include: { flag: true, assessmentCriteria: true }, + include: { flag: true, markingComponents: true }, }) .then((x) => T.toUnitOfAssessmentDTO(x)); } @@ -73,7 +73,7 @@ export class AllocationInstance extends DataObject { public async getCriteria( unitOfAssessmentId: string, ): Promise { - const data = await this.db.assessmentCriterion.findMany({ + const data = await this.db.markingComponent.findMany({ where: { unitOfAssessmentId }, orderBy: { layoutIndex: "asc" }, }); @@ -87,9 +87,7 @@ export class AllocationInstance extends DataObject { const flagData = await this.db.flag.findMany({ where: expand(this.params), include: { - unitsOfAssessment: { - include: { flag: true, assessmentCriteria: true }, - }, + unitsOfAssessment: { include: { flag: true, markingComponents: true } }, }, }); diff --git a/src/data-objects/user/marker.ts b/src/data-objects/user/marker.ts index 4d9519d85..837efa26a 100644 --- a/src/data-objects/user/marker.ts +++ b/src/data-objects/user/marker.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck import { type MarkingSubmissionDTO, type ProjectDTO, @@ -47,7 +45,7 @@ export class Marker extends User { unitOfAssessmentId: string, studentId: string, ): Promise { - const result = await this.db.markingSubmission.findFirst({ + const result = await this.db.unitOfAssessmentSubmission.findFirst({ where: { markerId: this.id, studentId, unitOfAssessmentId }, include: { criterionScores: true }, }); @@ -99,7 +97,7 @@ export class Marker extends User { unitsOfAssessment: { where: { allowedMarkerTypes: { has: "SUPERVISOR" } }, include: { - assessmentCriteria: true, + markingComponents: true, flag: true, markerSubmissions: { where: { markerId } }, }, @@ -159,7 +157,11 @@ export class Marker extends User { studentFlag: { include: { unitsOfAssessment: { - include: { markerSubmissions: true }, + include: { + markerSubmissions: true, + flag: true, + markingComponents: true, + }, }, }, }, @@ -235,10 +237,8 @@ export class Marker extends User { }: Omit) { const markerId = this.id; await this.db.$transaction([ - this.db.markingSubmission.upsert({ - where: { - studentMarkerSubmission: { markerId, studentId, unitOfAssessmentId }, - }, + this.db.unitOfAssessmentSubmission.upsert({ + where: { uoaSubmissionId: { markerId, studentId, unitOfAssessmentId } }, create: { markerId, studentId, @@ -256,19 +256,19 @@ export class Marker extends User { }, }), - ...Object.entries(marks).map(([assessmentCriterionId, m]) => - this.db.criterionScore.upsert({ + ...Object.entries(marks).map(([markingComponentId, m]) => + this.db.markingComponentSubmission.upsert({ where: { - markingCriterionSubmission: { + markingComponentSubmission: { markerId, studentId, - assessmentCriterionId, + markingComponentId, }, }, create: { markerId, studentId, - assessmentCriterionId, + markingComponentId, unitOfAssessmentId, grade: m.mark ?? -1, justification: m.justification ?? "", @@ -290,9 +290,17 @@ export class Marker extends User { grade: number; comment: string; }) { - await this.db.finalUnitOfAssessmentGrade.upsert({ - where: { studentAssessmentGrade: { studentId, unitOfAssessmentId } }, - create: { studentId, unitOfAssessmentId, comment, grade }, + await this.db.unitOfAssessmentGrade.upsert({ + where: { uoaGradeId: { studentId, unitOfAssessmentId } }, + create: { + studentId, + unitOfAssessmentId, + comment, + grade, + status: "DONE", + method: "AUTO", + submitted: true, + }, update: { studentId, unitOfAssessmentId, comment, grade }, }); } diff --git a/src/data-objects/user/reader.ts b/src/data-objects/user/reader.ts index 9613acdab..9fba989d4 100644 --- a/src/data-objects/user/reader.ts +++ b/src/data-objects/user/reader.ts @@ -56,7 +56,7 @@ export class Reader extends Marker { studentFlag: { include: { unitsOfAssessment: { - include: { assessmentCriteria: true, flag: true }, + include: { markingComponents: true, flag: true }, }, }, }, diff --git a/src/db/transformers.ts b/src/db/transformers.ts index 8759dead5..b0a9122b5 100644 --- a/src/db/transformers.ts +++ b/src/db/transformers.ts @@ -23,7 +23,6 @@ import { type DB_AllocationGroup, type DB_AllocationInstance, type DB_AllocationSubGroup, - type DB_AssessmentCriterion, type DB_Flag, type DB_FlagOnProject, type DB_UnitOfAssessment, @@ -35,10 +34,11 @@ import { type DB_TagOnProject, type DB_User, type DB_UserInInstance, - type DB_CriterionScore, - type DB_MarkingSubmission, DB_ReaderPreferenceType, ExtendedReaderPreferenceType, + type DB_MarkingComponent, + type DB_UnitOfAssessmentSubmission, + type DB_MarkingComponentSubmission, } from "./types"; export class Transformers { @@ -56,7 +56,9 @@ export class Transformers { public static toMarkingSubmissionDTO( this: void, - data: DB_MarkingSubmission & { criterionScores?: DB_CriterionScore[] }, + data: DB_UnitOfAssessmentSubmission & { + criterionScores?: DB_MarkingComponentSubmission[]; + }, ): MarkingSubmissionDTO { return { markerId: data.markerId, @@ -66,7 +68,7 @@ export class Transformers { marks: (data.criterionScores ?? []).reduce( (acc, val) => ({ ...acc, - [val.assessmentCriterionId]: Transformers.toScoreDTO(val), + [val.markingComponentId]: Transformers.toScoreDTO(val), }), {}, ), @@ -75,7 +77,9 @@ export class Transformers { draft: data.draft, }; } - public static toScoreDTO(data: DB_CriterionScore): CriterionScoreDTO { + public static toScoreDTO( + data: DB_MarkingComponentSubmission, + ): CriterionScoreDTO { return { mark: data.grade, justification: data.justification }; } @@ -236,7 +240,7 @@ export class Transformers { public static toAssessmentCriterionDTO( this: void, - data: DB_AssessmentCriterion, + data: DB_MarkingComponent, ): AssessmentCriterionDTO { return { id: data.id, @@ -252,19 +256,19 @@ export class Transformers { this: void, data: DB_UnitOfAssessment & { flag: DB_Flag; - assessmentCriteria: DB_AssessmentCriterion[]; + markingComponents: DB_MarkingComponent[]; }, ): UnitOfAssessmentDTO { return { id: data.id, title: data.title, flag: Transformers.toFlagDTO(data.flag), - components: data.assessmentCriteria.map((x) => + components: data.markingComponents.map((x) => Transformers.toAssessmentCriterionDTO(x), ), - studentSubmissionDeadline: data.studentSubmissionDeadline, - markerSubmissionDeadline: data.markerSubmissionDeadline, - weight: data.weight, + studentSubmissionDeadline: data.defaultStudentSubmissionDeadline, + markerSubmissionDeadline: data.defaultStudentSubmissionDeadline, + weight: data.defaultWeight, isOpen: data.open, allowedMarkerTypes: data.allowedMarkerTypes, }; diff --git a/src/db/types.ts b/src/db/types.ts index 799a3b1f0..3881c5467 100644 --- a/src/db/types.ts +++ b/src/db/types.ts @@ -20,14 +20,14 @@ export type { AllocationGroup as DB_AllocationGroup, AllocationInstance as DB_AllocationInstance, AllocationSubGroup as DB_AllocationSubGroup, - AssessmentCriterion as DB_AssessmentCriterion, - CriterionScore as DB_CriterionScore, FinalGrade as DB_FinalGrade, Flag as DB_Flag, FlagOnProject as DB_FlagOnProject, UnitOfAssessment as DB_UnitOfAssessment, GroupAdmin as DB_GroupAdmin, - MarkingSubmission as DB_MarkingSubmission, + MarkingComponent as DB_MarkingComponent, + MarkingComponentSubmission as DB_MarkingComponentSubmission, + UnitOfAssessmentSubmission as DB_UnitOfAssessmentSubmission, MatchingPair as DB_MatchingPair, MatchingResult as DB_MatchingResult, Project as DB_Project, diff --git a/src/dto/project.ts b/src/dto/project.ts index e65d0dce3..dfe635c5e 100644 --- a/src/dto/project.ts +++ b/src/dto/project.ts @@ -25,8 +25,7 @@ export const ProjectAllocationStatus = { UNALLOCATED: "UNALLOCATED", } as const; -export type ProjectAllocationStatus = - (typeof ProjectAllocationStatus)[keyof typeof ProjectAllocationStatus]; +export type ProjectAllocationStatus = keyof typeof ProjectAllocationStatus; export const projectAllocationStatusSchema = z.enum([ ProjectAllocationStatus.UNALLOCATED, diff --git a/src/server/routers/institution/instance/index.ts b/src/server/routers/institution/instance/index.ts index c8ba1979d..e77176e18 100644 --- a/src/server/routers/institution/instance/index.ts +++ b/src/server/routers/institution/instance/index.ts @@ -1414,7 +1414,7 @@ export const instanceRouter = createTRPCRouter({ where: expand(instance.params), include: { unitsOfAssessment: { - include: { flag: true, assessmentCriteria: true }, + include: { flag: true, markingComponents: true }, orderBy: [{ markerSubmissionDeadline: "asc" }], }, }, diff --git a/src/server/routers/marking.ts b/src/server/routers/marking.ts index 0e8f6170d..6e0fd4b0f 100644 --- a/src/server/routers/marking.ts +++ b/src/server/routers/marking.ts @@ -82,19 +82,19 @@ export const markingRouter = createTRPCRouter({ const units = await db.unitOfAssessment .findMany({ where: expand(instance.params), - include: { flag: true, assessmentCriteria: true }, + include: { flag: true, markingComponents: true }, }) .then((data) => data.map((x) => T.toUnitOfAssessmentDTO(x))); - const submissions = await db.markingSubmission.findMany({ + const submissions = await db.unitOfAssessmentSubmission.findMany({ where: { unitOfAssessmentId: { in: units.map((e) => e.id) }, draft: false, }, include: { criterionScores: { - include: { criterion: true }, - orderBy: { criterion: { layoutIndex: "asc" } }, + include: { markingComponent: true }, + orderBy: { markingComponent: { layoutIndex: "asc" } }, }, }, }); @@ -113,7 +113,8 @@ export const markingRouter = createTRPCRouter({ const comment = comments.reduce((acc, val) => { - const rest = val.criterion.title + "\t" + val.justification; + const rest = + val.markingComponent.title + "\t" + val.justification; return acc + "\t\t" + rest; }, "") + "\t\tFinal Comment:\t" + @@ -144,7 +145,7 @@ export const markingRouter = createTRPCRouter({ >, ); - const unitFinalMarks = await db.finalUnitOfAssessmentGrade.findMany({ + const unitFinalMarks = await db.unitOfAssessmentGrade.findMany({ where: { unitOfAssessmentId: { in: units.map((e) => e.id) } }, }); @@ -261,6 +262,8 @@ export const markingRouter = createTRPCRouter({ grade: finalMark, ...expand(instance.params), studentId: student.id, + comment: "", + method: "AUTO", }, }); } diff --git a/src/server/routers/user/marker.ts b/src/server/routers/user/marker.ts index 615b77e90..c79d1404a 100644 --- a/src/server/routers/user/marker.ts +++ b/src/server/routers/user/marker.ts @@ -36,7 +36,7 @@ export const markerRouter = createTRPCRouter({ .query(async ({ ctx: { db }, input: { unitOfAssessmentId } }) => { const res = await db.unitOfAssessment.findFirstOrThrow({ where: { id: unitOfAssessmentId }, - include: { flag: true, assessmentCriteria: true }, + include: { flag: true, markingComponents: true }, }); return T.toUnitOfAssessmentDTO(res); @@ -145,7 +145,7 @@ export const markerRouter = createTRPCRouter({ const deadline = addWeeks(new Date(), 1); // otherwise, if this is a doubly-marked submission, and now both are submitted then: - const data = await db.markingSubmission.findMany({ + const data = await db.unitOfAssessmentSubmission.findMany({ where: { studentId, unitOfAssessmentId }, include: { criterionScores: true }, }); @@ -240,7 +240,7 @@ export const markerRouter = createTRPCRouter({ return; } - const numSubmissions = await db.markingSubmission.count({ + const numSubmissions = await db.unitOfAssessmentSubmission.count({ where: { studentId, unitOfAssessmentId, draft: false }, }); @@ -253,7 +253,7 @@ export const markerRouter = createTRPCRouter({ } // otherwise, if this is a doubly-marked submission, and now both are submitted then: - const data = await db.markingSubmission.findMany({ + const data = await db.unitOfAssessmentSubmission.findMany({ where: { studentId, unitOfAssessmentId, draft: false }, include: { criterionScores: true }, });