diff --git a/ExpressAPI/.idea/material_theme_project_new.xml b/ExpressAPI/.idea/material_theme_project_new.xml index 08d93208ec31f4c00760f273384c3ace69519b66..16e830f2d917c0815e814dcbf57e219c3b85c2e2 100644 --- a/ExpressAPI/.idea/material_theme_project_new.xml +++ b/ExpressAPI/.idea/material_theme_project_new.xml @@ -3,7 +3,9 @@ <component name="MaterialThemeProjectNewConfig"> <option name="metadata"> <MTProjectMetadataState> - <option name="userId" value="104e8585:19002424fea:-7fcc" /> + <option name="migrated" value="true" /> + <option name="pristineConfig" value="false" /> + <option name="userId" value="104e8585:19002424fea:-7ffe" /> </MTProjectMetadataState> </option> </component> diff --git a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml index 8ebb21662faad48639bce22286e04c0d2f7c4450..23d099e0a7b64cb6a9bff5c5fe886336cbe3d1d8 100644 --- a/ExpressAPI/assets/OpenAPI/OpenAPI.yaml +++ b/ExpressAPI/assets/OpenAPI/OpenAPI.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Dojo API - version: 4.1.0 + version: 4.2.0 description: | **Backend API of the Dojo project.** diff --git a/ExpressAPI/prisma/migrations/20240212153007_add_tags/migration.sql b/ExpressAPI/prisma/migrations/20240212153007_add_tags/migration.sql new file mode 100644 index 0000000000000000000000000000000000000000..4760713466e16ff80d81a734fe2f3c31837eadcc --- /dev/null +++ b/ExpressAPI/prisma/migrations/20240212153007_add_tags/migration.sql @@ -0,0 +1,46 @@ +-- CreateTable +CREATE TABLE `Tag` ( + `name` CHAR(36) NOT NULL, + `type` ENUM('LANGUAGE', 'FRAMEWORK', 'THEME', 'USERDEFINED') NOT NULL, + + PRIMARY KEY (`name`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `_AssignmentToTag` ( + `A` VARCHAR(191) NOT NULL, + `B` CHAR(36) NOT NULL, + + UNIQUE INDEX `_AssignmentToTag_AB_unique`(`A`, `B`), + INDEX `_AssignmentToTag_B_index`(`B`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `_ExerciseToTag` ( + `A` CHAR(36) NOT NULL, + `B` CHAR(36) NOT NULL, + + UNIQUE INDEX `_ExerciseToTag_AB_unique`(`A`, `B`), + INDEX `_ExerciseToTag_B_index`(`B`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `_AssignmentToTag` ADD CONSTRAINT `_AssignmentToTag_A_fkey` FOREIGN KEY (`A`) REFERENCES `Assignment`(`name`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `_AssignmentToTag` ADD CONSTRAINT `_AssignmentToTag_B_fkey` FOREIGN KEY (`B`) REFERENCES `Tag`(`name`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `_ExerciseToTag` ADD CONSTRAINT `_ExerciseToTag_A_fkey` FOREIGN KEY (`A`) REFERENCES `Exercise`(`id`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `_ExerciseToTag` ADD CONSTRAINT `_ExerciseToTag_B_fkey` FOREIGN KEY (`B`) REFERENCES `Tag`(`name`) ON DELETE CASCADE ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE `SubmissionTag` ( + `name` CHAR(36) NOT NULL, + `type` ENUM('LANGUAGE', 'FRAMEWORK', 'THEME', 'USERDEFINED') NOT NULL, + `state` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`name`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/ExpressAPI/prisma/migrations/20240619160717_rename_tag_proposal_table/migration.sql b/ExpressAPI/prisma/migrations/20240619160717_rename_tag_proposal_table/migration.sql new file mode 100644 index 0000000000000000000000000000000000000000..5484fa4855165f86579d571d91ec421ee13ab5c9 --- /dev/null +++ b/ExpressAPI/prisma/migrations/20240619160717_rename_tag_proposal_table/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - You are about to drop the `SubmissionTag` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE `SubmissionTag`; + +-- CreateTable +CREATE TABLE `TagProposal` ( + `name` CHAR(36) NOT NULL, + `type` ENUM('LANGUAGE', 'FRAMEWORK', 'THEME', 'USERDEFINED') NOT NULL, + `state` VARCHAR(191) NOT NULL, + + PRIMARY KEY (`name`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/ExpressAPI/prisma/migrations/20240619232301_set_tag_proposal_state_default/migration.sql b/ExpressAPI/prisma/migrations/20240619232301_set_tag_proposal_state_default/migration.sql new file mode 100644 index 0000000000000000000000000000000000000000..f0feb8cccc7b2e7466528d133231457418edd075 --- /dev/null +++ b/ExpressAPI/prisma/migrations/20240619232301_set_tag_proposal_state_default/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `TagProposal` MODIFY `state` VARCHAR(191) NOT NULL DEFAULT 'PendingApproval'; diff --git a/ExpressAPI/prisma/migrations/20240619232804_add_tag_proposal_details/migration.sql b/ExpressAPI/prisma/migrations/20240619232804_add_tag_proposal_details/migration.sql new file mode 100644 index 0000000000000000000000000000000000000000..28577819d43b5e17369af23ae3e9ee4efb43edfc --- /dev/null +++ b/ExpressAPI/prisma/migrations/20240619232804_add_tag_proposal_details/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `TagProposal` ADD COLUMN `details` VARCHAR(191) NULL; diff --git a/ExpressAPI/prisma/schema.prisma b/ExpressAPI/prisma/schema.prisma index 8d7143ac874fd4324ce2d1c043f65c2f6e27ba68..69e3b2678b3e6e6548bc86c6f713b5560ccc31f0 100644 --- a/ExpressAPI/prisma/schema.prisma +++ b/ExpressAPI/prisma/schema.prisma @@ -13,6 +13,13 @@ enum UserRole { ADMIN } +enum TagType { + LANGUAGE + FRAMEWORK + THEME + USERDEFINED +} + model User { id Int @id /// The user's id is the same as their gitlab id name String? @@ -37,6 +44,7 @@ model Assignment { exercises Exercise[] staff User[] + tags Tag[] } model Exercise { @@ -57,6 +65,7 @@ model Exercise { members User[] results Result[] + tags Tag[] } model Result { @@ -72,3 +81,18 @@ model Result { @@id([exerciseId, dateTime]) } + +model Tag { + name String @id @db.Char(36) + type TagType + + assignments Assignment[] + exercises Exercise[] +} + +model TagProposal { + name String @id @db.Char(36) + type TagType + state String @default("PendingApproval") + details String? +} diff --git a/ExpressAPI/prisma/seed.ts b/ExpressAPI/prisma/seed.ts index 93680f6e393563ebb3adee65bdfe48e5918797a0..6ac1a5d060fb2009293ea630763ec747a2790beb 100644 --- a/ExpressAPI/prisma/seed.ts +++ b/ExpressAPI/prisma/seed.ts @@ -6,13 +6,15 @@ import SharedConfig from '../src/shared/config/SharedConfig.js'; import { UserRole } from '@prisma/client'; import logger from '../src/shared/logging/WinstonLogger.js'; import db from '../src/helpers/DatabaseHelper.js'; - +import TagManager from '../src/managers/TagManager'; +import { TagType } from '@prisma/client'; async function main() { await users(); await assignments(); await exercises(); await results(); + await tag(); } main().then(async () => { @@ -1580,4 +1582,383 @@ async function results() { } }); } -} \ No newline at end of file +} + +async function tag() { + await db.tag.upsert({ + where : { name: 'C' }, + update: {}, + create: { + name : 'C', + type : TagType.LANGUAGE + } + }); + await db.tag.upsert({ + where : { name: 'Java' }, + update: {}, + create: { + name : 'Java', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Scala' }, + update: {}, + create: { + name : 'Scala', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Kotlin' }, + update: {}, + create: { + name : 'Kotlin', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Rust' }, + update: {}, + create: { + name : 'Rust', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'JavaScript' }, + update: {}, + create: { + name : 'JavaScript', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'TypeScript' }, + update: {}, + create: { + name : 'TypeScript', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Python' }, + update: {}, + create: { + name : 'Python', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'HTML' }, + update: {}, + create: { + name : 'HTML', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'CSS' }, + update: {}, + create: { + name : 'CSS', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'C++' }, + update: {}, + create: { + name : 'C++', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Go' }, + update: {}, + create: { + name : 'Go', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'PHP' }, + update: {}, + create: { + name : 'PHP', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'C#' }, + update: {}, + create: { + name : 'C#', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Swift' }, + update: {}, + create: { + name : 'Swift', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Matlab' }, + update: {}, + create: { + name : 'Matlab', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'SQL' }, + update: {}, + create: { + name : 'SQL', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Assembly' }, + update: {}, + create: { + name : 'Assembly', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Ruby' }, + update: {}, + create: { + name : 'Ruby', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Fortran' }, + update: {}, + create: { + name : 'Fortran', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Pascal' }, + update: {}, + create: { + name : 'Pascal', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Visual Basic' }, + update: {}, + create: { + name : 'Visual Basic', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'R' }, + update: {}, + create: { + name : 'R', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Objective-C' }, + update: {}, + create: { + name : 'Objective-C', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Lua' }, + update: {}, + create: { + name : 'Lua', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Ada' }, + update: {}, + create: { + name : 'Ada', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Haskell' }, + update: {}, + create: { + name : 'Haskell', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Shell/PowerShell' }, + update: {}, + create: { + name : 'Shell/PowerShell', + type : TagType.LANGUAGE, + } + }); + await db.tag.upsert({ + where : { name: 'Express' }, + update: {}, + create: { + name : 'Express', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Django' }, + update: {}, + create: { + name : 'Django', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Ruby on Rails' }, + update: {}, + create: { + name : 'Ruby on Rails', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Angular' }, + update: {}, + create: { + name : 'Angular', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'React' }, + update: {}, + create: { + name : 'React', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Flutter' }, + update: {}, + create: { + name : 'Flutter', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Ionic' }, + update: {}, + create: { + name : 'Ionic', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Flask' }, + update: {}, + create: { + name : 'Flask', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'React Native' }, + update: {}, + create: { + name : 'React Native', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Xamarin' }, + update: {}, + create: { + name : 'Xamarin', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Laravel' }, + update: {}, + create: { + name : 'Laravel', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Spring' }, + update: {}, + create: { + name : 'Spring', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Play' }, + update: {}, + create: { + name : 'Play', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Symfony' }, + update: {}, + create: { + name : 'Symfony', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'ASP.NET' }, + update: {}, + create: { + name : 'ASP.NET', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Meteor' }, + update: {}, + create: { + name : 'Meteor', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Vue.js' }, + update: {}, + create: { + name : 'Vue.js', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Svelte' }, + update: {}, + create: { + name : 'Svelte', + type : TagType.FRAMEWORK + } + }); + await db.tag.upsert({ + where : { name: 'Express.js' }, + update: {}, + create: { + name : 'Express.js', + type : TagType.FRAMEWORK + } + }); +} diff --git a/ExpressAPI/src/managers/TagManager.ts b/ExpressAPI/src/managers/TagManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..936b3bcb04d3aea3c205215d6d283337ae62f0b9 --- /dev/null +++ b/ExpressAPI/src/managers/TagManager.ts @@ -0,0 +1,16 @@ +import { Prisma, Tag } from '@prisma/client'; +import db from '../helpers/DatabaseHelper'; + +class TagManager { + async get(name: string, include: Prisma.TagInclude | undefined = undefined): Promise<Tag | undefined> { + return await db.tag.findUnique({ + where : { + name: name + }, + include: include + }) as unknown as Tag ?? undefined; + } +} + +export default new TagManager(); + diff --git a/ExpressAPI/src/managers/TagProposalManager.ts b/ExpressAPI/src/managers/TagProposalManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..89607e24c566a377b3abc0b1e5ac66c5b1129874 --- /dev/null +++ b/ExpressAPI/src/managers/TagProposalManager.ts @@ -0,0 +1,17 @@ +import { TagProposal } from '@prisma/client'; +import db from '../helpers/DatabaseHelper'; + + +class TagProposalManager { + async get(name: string | undefined = undefined): Promise<TagProposal | undefined> { + return await db.tagProposal.findUnique({ + where: { + name: name + } + }) as unknown as TagProposal ?? undefined; + } +} + + +export default new TagProposalManager(); + diff --git a/ExpressAPI/src/middlewares/ParamsCallbackManager.ts b/ExpressAPI/src/middlewares/ParamsCallbackManager.ts index a1836d10d7349c935920061299226a7eb825568c..1dffaef8a782bb06c01e485705afc830e563f441 100644 --- a/ExpressAPI/src/middlewares/ParamsCallbackManager.ts +++ b/ExpressAPI/src/middlewares/ParamsCallbackManager.ts @@ -1,8 +1,10 @@ -import { Express } from 'express-serve-static-core'; -import express from 'express'; -import { StatusCodes } from 'http-status-codes'; -import ExerciseManager from '../managers/ExerciseManager.js'; -import AssignmentManager from '../managers/AssignmentManager.js'; +import { Express } from 'express-serve-static-core'; +import express from 'express'; +import { StatusCodes } from 'http-status-codes'; +import ExerciseManager from '../managers/ExerciseManager'; +import AssignmentManager from '../managers/AssignmentManager'; +import TagManager from '../managers/TagManager'; +import TagProposalManager from '../managers/TagProposalManager'; type GetFunction = (id: string | number, ...args: Array<unknown>) => Promise<unknown> @@ -27,8 +29,10 @@ class ParamsCallbackManager { initBoundParams(req: express.Request) { if ( !req.boundParams ) { req.boundParams = { - assignment: undefined, - exercise : undefined + assignment : undefined, + exercise : undefined, + tag : undefined, + tagProposal: undefined }; } } @@ -44,6 +48,12 @@ class ParamsCallbackManager { members : true, results : true } ], 'exercise'); + + this.listenParam('tagName', backend, (TagManager.get as GetFunction).bind(TagManager), [ { + assignments: true + } ], 'tag'); + + this.listenParam('tagProposalName', backend, (TagProposalManager.get as GetFunction).bind(TagProposalManager), [ {} ], 'tagProposal'); } } diff --git a/ExpressAPI/src/middlewares/SecurityMiddleware.ts b/ExpressAPI/src/middlewares/SecurityMiddleware.ts index 02c54c16b151e259a45b8ff47fda8f9f50b17bf6..6edc8bee8f308084bdf03e542f0f91949578788e 100644 --- a/ExpressAPI/src/middlewares/SecurityMiddleware.ts +++ b/ExpressAPI/src/middlewares/SecurityMiddleware.ts @@ -13,6 +13,8 @@ class SecurityMiddleware { private async checkType(checkType: SecurityCheckType, req: express.Request): Promise<boolean> { try { switch ( String(checkType) ) { + case SecurityCheckType.ADMIN.valueOf(): + return req.session.profile.isAdmin; case SecurityCheckType.TEACHING_STAFF.valueOf(): return req.session.profile.isTeachingStaff; case SecurityCheckType.ASSIGNMENT_STAFF.valueOf(): diff --git a/ExpressAPI/src/routes/ApiRoutesManager.ts b/ExpressAPI/src/routes/ApiRoutesManager.ts index 57a418842eb2003e5cab56d24344877837f90691..1508de4124baa104b7aac892da229685ecb9b998 100644 --- a/ExpressAPI/src/routes/ApiRoutesManager.ts +++ b/ExpressAPI/src/routes/ApiRoutesManager.ts @@ -5,6 +5,7 @@ import SessionRoutes from './SessionRoutes.js'; import AssignmentRoutes from './AssignmentRoutes.js'; import GitlabRoutes from './GitlabRoutes.js'; import ExerciseRoutes from './ExerciseRoutes.js'; +import TagsRoutes from './TagRoutes'; class AdminRoutesManager implements RoutesManager { @@ -14,6 +15,7 @@ class AdminRoutesManager implements RoutesManager { GitlabRoutes.registerOnBackend(backend); AssignmentRoutes.registerOnBackend(backend); ExerciseRoutes.registerOnBackend(backend); + TagsRoutes.registerOnBackend(backend); } } diff --git a/ExpressAPI/src/routes/AssignmentRoutes.ts b/ExpressAPI/src/routes/AssignmentRoutes.ts index 5f46129df7cf916d4b7a94f17bb108f32289ee4c..0f06d74728a684153763d8b7d710809ff6f6e9f4 100644 --- a/ExpressAPI/src/routes/AssignmentRoutes.ts +++ b/ExpressAPI/src/routes/AssignmentRoutes.ts @@ -112,7 +112,8 @@ class AssignmentRoutes implements RoutesManager { include: { assignment: false, members : true, - results : true + results : true, + tags : true } }); } diff --git a/ExpressAPI/src/routes/TagRoutes.ts b/ExpressAPI/src/routes/TagRoutes.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7e673403fe94dc209d53a91616c7140bc0ea1ec --- /dev/null +++ b/ExpressAPI/src/routes/TagRoutes.ts @@ -0,0 +1,132 @@ +import express, { RequestHandler } from 'express'; +import { TagType } from '@prisma/client'; +import * as ExpressValidator from 'express-validator'; +import { StatusCodes } from 'http-status-codes'; +import RoutesManager from '../express/RoutesManager'; +import { Express } from 'express-serve-static-core'; +import db from '../helpers/DatabaseHelper'; +import SecurityCheckType from '../types/SecurityCheckType'; +import SecurityMiddleware from '../middlewares/SecurityMiddleware'; +import ParamsValidatorMiddleware from '../middlewares/ParamsValidatorMiddleware'; +import DojoStatusCode from '../shared/types/Dojo/DojoStatusCode'; + + +class TagRoutes implements RoutesManager { + private readonly tagValidator: ExpressValidator.Schema = { + name: { + trim : true, + notEmpty: true + }, + type: { + trim : true, + notEmpty: true + } + }; + + private readonly tagProposalAnswerValidator: ExpressValidator.Schema = { + state : { + trim : true, + notEmpty: true + }, + details: { + trim : true, + optional: true + } + }; + + registerOnBackend(backend: Express) { + backend.post('/tags', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), ParamsValidatorMiddleware.validate(this.tagValidator), this.createTag.bind(this) as RequestHandler); + backend.delete('/tags/:tagName', SecurityMiddleware.check(true, SecurityCheckType.ADMIN), this.deleteTag.bind(this) as RequestHandler); + backend.get('/tags/proposals', SecurityMiddleware.check(true, SecurityCheckType.ADMIN), this.getTagProposals.bind(this) as RequestHandler); + backend.post('/tags/proposals', SecurityMiddleware.check(true, SecurityCheckType.TEACHING_STAFF), ParamsValidatorMiddleware.validate(this.tagValidator), this.createTagProposal.bind(this) as RequestHandler); + backend.patch('/tags/proposals/:tagProposalName', SecurityMiddleware.check(true, SecurityCheckType.ADMIN), ParamsValidatorMiddleware.validate(this.tagProposalAnswerValidator), this.validateTag.bind(this) as RequestHandler); + } + + private async createTag(req: express.Request, res: express.Response) { + const tagName = req.body.name; + const tagType = (req.body.type as string).toUpperCase() as TagType; + + if ( tagType !== TagType.USERDEFINED && !req.session.profile.isAdmin ) { + return req.session.sendResponse(res, StatusCodes.FORBIDDEN, {}, 'Only admins can create non userDefined tags', DojoStatusCode.TAG_ONLY_ADMIN_CREATION); + } + + await db.tag.create({ + data: { + name: tagName, + type: tagType + } + }); + + return req.session.sendResponse(res, StatusCodes.OK); + } + + private async deleteTag(req: express.Request, res: express.Response) { + if ( req.boundParams.tag!.assignments.length > 0 ) { + return req.session.sendResponse(res, StatusCodes.LOCKED, {}, 'Tag is used in assignments', DojoStatusCode.TAG_WITH_ACTIVE_LINK_DELETION); + } + + await db.tag.delete({ + where: { name: req.boundParams.tag!.name } + }); + + return req.session.sendResponse(res, StatusCodes.OK); + } + + private async getTagProposals(req: express.Request, res: express.Response) { + const state = req.query.stateFilter as string; + + const tagProposals = await db.tagProposal.findMany(state ? { + where: { state: state } + } : {}); + + return req.session.sendResponse(res, StatusCodes.OK, tagProposals); + } + + private async createTagProposal(req: express.Request, res: express.Response) { + const tagName = req.body.name; + const tagType = (req.body.type as string).toUpperCase() as TagType; + + await db.tagProposal.create({ + data: { + name: tagName, + type: tagType + } + }); + + return req.session.sendResponse(res, StatusCodes.OK); + } + + private async validateTag(req: express.Request, res: express.Response) { + if ( req.boundParams.tagProposal!.state === 'PendingApproval' ) { + const state: string = req.body.state; + + if ( state === 'Approved' ) { + try { + await db.tag.create({ + data: { + name: req.boundParams.tagProposal!.name, + type: req.boundParams.tagProposal!.type + } + }); + } catch ( error ) { + // empty + } + } + + await db.tagProposal.update({ + where: { name: req.boundParams.tagProposal?.name }, + data : { + state : state, + details: req.body.details ?? '' + } + }); + + return req.session.sendResponse(res, StatusCodes.OK); + } else { + return req.session.sendResponse(res, StatusCodes.BAD_REQUEST, {}, 'Tag proposal is not pending', DojoStatusCode.TAG_PROPOSAL_ANSWER_NOT_PENDING); + } + } +} + + +export default new TagRoutes(); diff --git a/ExpressAPI/src/shared b/ExpressAPI/src/shared index c2afa861bf6306ddec79ffd465a4c7b0edcd3453..708a3c0805fb2b2d853a781bde86a10d5282545a 160000 --- a/ExpressAPI/src/shared +++ b/ExpressAPI/src/shared @@ -1 +1 @@ -Subproject commit c2afa861bf6306ddec79ffd465a4c7b0edcd3453 +Subproject commit 708a3c0805fb2b2d853a781bde86a10d5282545a diff --git a/ExpressAPI/src/types/DatabaseTypes.ts b/ExpressAPI/src/types/DatabaseTypes.ts index 011d2efc5643f66cefb4da435b26e4579accfc32..2a7109c532b14733f5f32328757d922a404821b7 100644 --- a/ExpressAPI/src/types/DatabaseTypes.ts +++ b/ExpressAPI/src/types/DatabaseTypes.ts @@ -9,14 +9,16 @@ const userBase = Prisma.validator<Prisma.UserDefaultArgs>()({ const assignmentBase = Prisma.validator<Prisma.AssignmentDefaultArgs>()({ include: { exercises: true, - staff : true + staff : true, + tags : true } }); const exerciseBase = Prisma.validator<Prisma.ExerciseDefaultArgs>()({ include: { assignment: true, members : true, - results : true + results : true, + tags : true } }); const resultBase = Prisma.validator<Prisma.ResultDefaultArgs>()({ @@ -24,6 +26,12 @@ const resultBase = Prisma.validator<Prisma.ResultDefaultArgs>()({ exercise: true } }); +const tagBase = Prisma.validator<Prisma.TagDefaultArgs>()({ + include: { + exercises : true, + assignments: true + } + }); export type User = Prisma.UserGetPayload<typeof userBase> & { @@ -37,4 +45,5 @@ export type Exercise = Prisma.ExerciseGetPayload<typeof exerciseBase> & { export type Assignment = Prisma.AssignmentGetPayload<typeof assignmentBase> & { corrections: LazyVal<Array<Exercise>> } -export type Result = Prisma.ResultGetPayload<typeof resultBase> \ No newline at end of file +export type Result = Prisma.ResultGetPayload<typeof resultBase> +export type Tag = Prisma.TagGetPayload<typeof tagBase> \ No newline at end of file diff --git a/ExpressAPI/src/types/SecurityCheckType.ts b/ExpressAPI/src/types/SecurityCheckType.ts index 3a0b733103af4604e10f917ec3edc4c7f56b3b66..17d026a253327907355176e847a6ac4a7047922f 100644 --- a/ExpressAPI/src/types/SecurityCheckType.ts +++ b/ExpressAPI/src/types/SecurityCheckType.ts @@ -1,5 +1,6 @@ enum SecurityCheckType { TEACHING_STAFF = 'teachingStaff', + ADMIN = 'admin', ASSIGNMENT_STAFF = 'assignmentStaff', ASSIGNMENT_IS_PUBLISHED = 'assignmentIsPublished', EXERCISE_SECRET = 'exerciseSecret', diff --git a/ExpressAPI/src/types/express/index.d.ts b/ExpressAPI/src/types/express/index.d.ts index 744609f9823e62e1348d63df9252a48ccfa3b64b..59a5e0056b7d85185e11d49a66d82153cab01192 100644 --- a/ExpressAPI/src/types/express/index.d.ts +++ b/ExpressAPI/src/types/express/index.d.ts @@ -1,5 +1,6 @@ -import Session from '../../controllers/Session.js'; -import { Assignment, Exercise } from '../DatabaseTypes.js'; +import Session from '../../controllers/Session.js'; +import { Assignment, Exercise, Tag } from '../DatabaseTypes'; +import { TagProposal } from '@prisma/client'; // to make the file a module and avoid the TypeScript error export {}; @@ -9,7 +10,7 @@ declare global { export interface Request { session: Session, boundParams: { - assignment: Assignment | undefined, exercise: Exercise | undefined + assignment: Assignment | undefined, exercise: Exercise | undefined, tag: Tag | undefined, tagProposal: TagProposal | undefined } } }