From 29e279773ac813c1f9fe7db43b201a54be009fd6 Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 15 Jan 2026 14:42:55 +0100 Subject: [PATCH 01/12] updated data model --- prisma/schema/marking.prisma | 128 +++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 52 deletions(-) diff --git a/prisma/schema/marking.prisma b/prisma/schema/marking.prisma index b89708695..45653ee58 100644 --- a/prisma/schema/marking.prisma +++ b/prisma/schema/marking.prisma @@ -3,91 +3,115 @@ 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") } -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 + // --- + markingComponentId String @map("marking_component_id") // --- - unitOfAssessment UnitOfAssessment @relation(fields: [unitOfAssessmentId], references: [id], onDelete: Cascade, map: "submission_unit") - criterionScores CriterionScore[] + markingComponent MarkingComponent @relation(fields: [markingComponentId], references: [id], onDelete: Cascade, map: "marking_submission_marking_submission") + submission UnitOfAssessmentSubmission @relation(fields: [markerId, studentId, unitOfAssessmentId], references: [markerId, studentId, unitOfAssessmentId], onDelete: Cascade, map: "marking_submission_uoa_submission") - @@id([markerId, studentId, unitOfAssessmentId], name: "studentMarkerSubmission") - @@map("assessment_marking_submission") + @@id([markerId, studentId, markingComponentId], name: "markingComponentSubmission") + @@map("criterion_score") } -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) + studentSubmissionDeadline DateTime @map("student_submission_deadline") + markerSubmissionDeadline DateTime @map("marker_submission_deadline") + weight Int + 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 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 FinalUnitOfAssessmentGrade { +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 + method MarkingMethod - @@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 + method FinalGradeMethod // --- allocationGroupId String @map("allocation_group_id") allocationSubGroupId String @map("allocation_sub_group_id") From 5493fbd2025bfdcff9894ae1b40173f8b3a25869 Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 22 Jan 2026 16:03:18 +0000 Subject: [PATCH 02/12] added mechanisms for custom weights and deadlines as well as the submitted field --- prisma/schema/marking.prisma | 37 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/prisma/schema/marking.prisma b/prisma/schema/marking.prisma index 45653ee58..b4fd2ae9d 100644 --- a/prisma/schema/marking.prisma +++ b/prisma/schema/marking.prisma @@ -54,24 +54,25 @@ model MarkingComponentSubmission { } 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[] + 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[] // --- - allocationGroupId String @map("allocation_group_id") - allocationSubGroupId String @map("allocation_sub_group_id") - allocationInstanceId String @map("allocation_instance_id") + 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: "uoa_instance") - markingComponents MarkingComponent[] - markerSubmissions UnitOfAssessmentSubmission[] - grades UnitOfAssessmentGrade[] + 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[] @@map("unit_of_assessment") } @@ -101,6 +102,10 @@ model UnitOfAssessmentGrade { comment String status MarkingStatus method MarkingMethod + // + submitted Boolean + customDueDate DateTime? @map("custom_due_date") + customWeight Int? @map("custom_weight") @@id([studentId, unitOfAssessmentId], name: "uoaGradeId") @@map("unit_of_assessment_grade") From 3b34d0cd5866cabe6578be4f1cc71e87c92e0207 Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 22 Jan 2026 16:03:41 +0000 Subject: [PATCH 03/12] added architecture documentation --- docs/architecture.md | 100 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/architecture.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..cdb1f3b7d --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,100 @@ +## Architecture + +The SPA system is comprised of two main repositories: + +The first is the matching service + +This is a flask (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 `/stc/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 +Some extra details need to be provided via environment variables. + +See \that\file for details of which ones are needed and what they should be. From f6673b6a07a0cdf6d2592ea416fbe625d7d74afc Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 11:47:41 +0000 Subject: [PATCH 04/12] Fixed some things for PR --- docs/architecture.md | 12 +++++++----- docs/dev/design.md | 6 ------ prisma/schema/user.prisma | 1 + 3 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 docs/dev/design.md diff --git a/docs/architecture.md b/docs/architecture.md index cdb1f3b7d..7239a0a67 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ The SPA system is comprised of two main repositories: The first is the matching service -This is a flask (python) app which simply serves out a series of pre-defined algorithms. +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. @@ -26,7 +26,7 @@ The main components within this are: - the backend, - We use [tRPC](https://trpc.io/) - - contained in `/stc/router...` + - contained in `/src/router...` - Most of the logic in the procedures in reality lives in data objects ([see below](#data-objects)) - the frontend, @@ -94,7 +94,9 @@ It makes moving around different parts of the application much easier, since the ## Deployment The matching service and the allocation app are each packaged into a docker image. -We use docker compose to orchestrate them, along with -Some extra details need to be provided via environment variables. +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. -See \that\file for details of which ones are needed and what they should be. +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/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") From cec551c4ed4ca1a199a7adbc0d283b122650f205 Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 11:48:01 +0000 Subject: [PATCH 05/12] added enums to snips --- .vscode/snips.code-snippets | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) 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;", + ], + }, } From f30d4416a651d726f1471e195a0a389704a6d731 Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 15:04:46 +0000 Subject: [PATCH 06/12] updated style guide with new enum stuff --- docs/dev/style-guide.md | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) 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. From 6bea1a6235d68c0640b671ea2fdc855159267c02 Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 15:06:52 +0000 Subject: [PATCH 07/12] minor tweak to schema --- prisma/schema/marking.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema/marking.prisma b/prisma/schema/marking.prisma index b4fd2ae9d..fa1cb8e69 100644 --- a/prisma/schema/marking.prisma +++ b/prisma/schema/marking.prisma @@ -34,7 +34,7 @@ model MarkingComponent { scores MarkingComponentSubmission[] @@unique([title, unitOfAssessmentId]) - @@map("assessment_criterion") + @@map("marking_component") } model MarkingComponentSubmission { From 3874851a084cb8b19c476e7354bd466f1760ec6d Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 15:16:38 +0000 Subject: [PATCH 08/12] fixed type errors --- src/data-objects/space/instance.ts | 8 ++---- src/data-objects/user/reader.ts | 2 +- src/db/transformers.ts | 28 +++++++++++-------- src/db/types.ts | 6 ++-- src/dto/project.ts | 3 ++ .../routers/institution/instance/index.ts | 2 +- src/server/routers/marking.ts | 15 ++++++---- src/server/routers/user/marker.ts | 8 +++--- 8 files changed, 40 insertions(+), 32 deletions(-) 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/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..c20cf9e79 100644 --- a/src/dto/project.ts +++ b/src/dto/project.ts @@ -28,6 +28,9 @@ export const ProjectAllocationStatus = { export type ProjectAllocationStatus = (typeof ProjectAllocationStatus)[keyof typeof ProjectAllocationStatus]; +// Consider simplifying as below: +// export type ProjectAllocationStatus2 = keyof typeof ProjectAllocationStatus; + export const projectAllocationStatusSchema = z.enum([ ProjectAllocationStatus.UNALLOCATED, ProjectAllocationStatus.RANDOM, 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 }, }); From 7bf9c3957aa7eac632b59aeb82f66ddcfe57138d Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 15:34:57 +0000 Subject: [PATCH 09/12] fixed lint errors --- src/data-objects/user/marker.ts | 42 ++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) 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 }, }); } From 31a08457c7fffbe6e5dc14888a71c6969fee7063 Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 15:50:07 +0000 Subject: [PATCH 10/12] tiny fixup --- src/dto/project.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/dto/project.ts b/src/dto/project.ts index c20cf9e79..dfe635c5e 100644 --- a/src/dto/project.ts +++ b/src/dto/project.ts @@ -25,11 +25,7 @@ export const ProjectAllocationStatus = { UNALLOCATED: "UNALLOCATED", } as const; -export type ProjectAllocationStatus = - (typeof ProjectAllocationStatus)[keyof typeof ProjectAllocationStatus]; - -// Consider simplifying as below: -// export type ProjectAllocationStatus2 = keyof typeof ProjectAllocationStatus; +export type ProjectAllocationStatus = keyof typeof ProjectAllocationStatus; export const projectAllocationStatusSchema = z.enum([ ProjectAllocationStatus.UNALLOCATED, From 2fdfc736351e1335a431bfd8bec44514f7697da9 Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 16:18:52 +0000 Subject: [PATCH 11/12] Minor renamings in marking schema --- prisma/schema/marking.prisma | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/prisma/schema/marking.prisma b/prisma/schema/marking.prisma index fa1cb8e69..cba75f065 100644 --- a/prisma/schema/marking.prisma +++ b/prisma/schema/marking.prisma @@ -46,11 +46,11 @@ model MarkingComponentSubmission { // --- markingComponentId String @map("marking_component_id") // --- - markingComponent MarkingComponent @relation(fields: [markingComponentId], references: [id], onDelete: Cascade, map: "marking_submission_marking_submission") - submission UnitOfAssessmentSubmission @relation(fields: [markerId, studentId, unitOfAssessmentId], references: [markerId, studentId, unitOfAssessmentId], onDelete: Cascade, map: "marking_submission_uoa_submission") + 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, markingComponentId], name: "markingComponentSubmission") - @@map("criterion_score") + @@map("marking_component_submission") } model UnitOfAssessment { @@ -115,8 +115,8 @@ model FinalGrade { id String @id @default(uuid()) studentId String @map("student_id") grade Int - comment String - method FinalGradeMethod + comment String @default("") + method FinalGradeMethod @default(AUTO) // --- allocationGroupId String @map("allocation_group_id") allocationSubGroupId String @map("allocation_sub_group_id") From ca4a99395a1928180a5cdcc472958be7ad9244da Mon Sep 17 00:00:00 2001 From: Jacob Trevor Date: Thu, 5 Feb 2026 16:57:46 +0000 Subject: [PATCH 12/12] created migrations for new db schema --- prisma/schema/marking.prisma | 6 +- .../20260205163110_marking/migration.sql | 58 +++++++++++++++++++ .../migration.sql | 19 ++++++ .../migration.sql | 16 +++++ 4 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 prisma/schema/migrations/20260205163110_marking/migration.sql create mode 100644 prisma/schema/migrations/20260205164917_marking_rename/migration.sql create mode 100644 prisma/schema/migrations/20260205165612_marking_residual/migration.sql diff --git a/prisma/schema/marking.prisma b/prisma/schema/marking.prisma index cba75f065..026e43ef9 100644 --- a/prisma/schema/marking.prisma +++ b/prisma/schema/marking.prisma @@ -100,10 +100,10 @@ model UnitOfAssessmentGrade { // --- grade Int comment String - status MarkingStatus - method MarkingMethod + status MarkingStatus @default(PENDING) + method MarkingMethod @default(AUTO) // - submitted Boolean + submitted Boolean @default(false) customDueDate DateTime? @map("custom_due_date") customWeight Int? @map("custom_weight") 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";