From 5cacd5d8e2b25af4a2e04fd93f001f5e149f9746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolas=20G=C3=B6rlitz?= Date: Fri, 8 Dec 2023 11:34:23 +0100 Subject: [PATCH] Solo handling --- misc/CheckEndorsements.js | 53 +++++++++++-------- misc/crontab.txt | 2 +- src/Router.ts | 2 + .../EndorsementGroupAdminController.ts | 29 ++++++++++ src/controllers/solo/SoloAdminController.ts | 44 +++++++++++++-- src/controllers/solo/_SoloAdmin.validator.ts | 11 ++++ src/models/MentorGroup.ts | 3 ++ .../extensions/MentorGroupExtensions.ts | 27 ++++++++++ src/utility/helper/ValidationHelper.ts | 28 +++++----- 9 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 src/models/extensions/MentorGroupExtensions.ts diff --git a/misc/CheckEndorsements.js b/misc/CheckEndorsements.js index 540f91e..e90e557 100644 --- a/misc/CheckEndorsements.js +++ b/misc/CheckEndorsements.js @@ -1,32 +1,43 @@ const Config = require("../dist/core/Config"); -const Sequelize = require("sequelize") +const Sequelize = require("sequelize"); const dayjs = require("dayjs"); /** * This script periodically checks, if a user has an endorsement bound to a solo, whilst the solo has already passed * Once every 24 Hours. */ -const newConf = {...Config.SequelizeConfig, logging: (message) => {console.log(message)}} +const newConf = { + ...Config.SequelizeConfig, + logging: message => { + console.log(message); + }, +}; const seq = new Sequelize(newConf); -seq.authenticate() - .catch(() => { - console.log("[SEQ] Failed to authenticate..."); - }); +seq.authenticate().catch(() => { + console.log("[SEQ] Failed to authenticate..."); +}); -seq.query("SELECT endorsement_groups_belong_to_users.id, endorsement_groups_belong_to_users.user_id, endorsement_groups_belong_to_users.solo_id, user_solos.current_solo_start, user_solos.current_solo_end FROM endorsement_groups_belong_to_users JOIN user_solos ON user_solos.id = endorsement_groups_belong_to_users.solo_id", { - type: Sequelize.QueryTypes.SELECT -}) -.then((res) => { - res.forEach(async (solo) => { - if (dayjs.utc(solo.current_solo_end).isBefore(dayjs.utc())) { - console.log(`Solo ID ${solo.solo_id} has expired. Removing Endorsement Group...`); - } else { - console.log(`Solo ID ${solo.solo_id} is expiring on ${dayjs.utc(solo.current_solo_end)} (${Math.abs(dayjs.utc(solo.current_solo_end).diff(dayjs.utc()))} Day(s) remaining).`) - } +seq.query( + "SELECT endorsement_groups_belong_to_users.id, endorsement_groups_belong_to_users.user_id, endorsement_groups_belong_to_users.solo_id, user_solos.current_solo_start, user_solos.current_solo_end FROM endorsement_groups_belong_to_users JOIN user_solos ON user_solos.id = endorsement_groups_belong_to_users.solo_id", + { + type: Sequelize.QueryTypes.SELECT, + } +) + .then(res => { + res.forEach(async solo => { + if (dayjs.utc(solo.current_solo_end).isBefore(dayjs.utc())) { + console.log(`Solo ID ${solo.solo_id} has expired. Removing Endorsement Group...`); + } else { + console.log( + `Solo ID ${solo.solo_id} is expiring on ${dayjs.utc(solo.current_solo_end)} (${Math.abs( + dayjs.utc(solo.current_solo_end).diff(dayjs.utc(), "day") + )} Day(s) remaining).` + ); + } - await seq.query("DELETE FROM endorsement_groups_belong_to_users WHERE ID = ?", {replacements: [solo.id], type: Sequelize.QueryTypes.DELETE}) + await seq.query("DELETE FROM endorsement_groups_belong_to_users WHERE ID = ?", { replacements: [solo.id], type: Sequelize.QueryTypes.DELETE }); + }); + }) + .finally(async () => { + await seq.close(); }); -}) -.finally(async() => { - await seq.close() -}); \ No newline at end of file diff --git a/misc/crontab.txt b/misc/crontab.txt index 458a02f..5f6548b 100644 --- a/misc/crontab.txt +++ b/misc/crontab.txt @@ -1 +1 @@ -* * * * * cd /opt/trainingcenter_backend && node /opt/trainingcenter_backend/misc/CheckEndorsements.js >> /var/log/check_endorsement.log \ No newline at end of file +0 1 * * * cd /opt/trainingcenter_backend && node /opt/trainingcenter_backend/misc/CheckEndorsements.js >> /var/log/check_endorsement.log \ No newline at end of file diff --git a/src/Router.ts b/src/Router.ts index c8063fd..a19d349 100644 --- a/src/Router.ts +++ b/src/Router.ts @@ -214,6 +214,8 @@ router.use( r.use( "/endorsement-group", routerGroup((r: Router) => { + r.get("/mentorable", EndorsementGroupAdminController.getMentorable); + r.get("/", EndorsementGroupAdminController.getAll); r.get("/with-stations", EndorsementGroupAdminController.getAllWithStations); r.post("/", EndorsementGroupAdminController.createEndorsementGroup); diff --git a/src/controllers/endorsement-group/EndorsementGroupAdminController.ts b/src/controllers/endorsement-group/EndorsementGroupAdminController.ts index 2489749..978ca88 100644 --- a/src/controllers/endorsement-group/EndorsementGroupAdminController.ts +++ b/src/controllers/endorsement-group/EndorsementGroupAdminController.ts @@ -7,6 +7,34 @@ import { TrainingStation } from "../../models/TrainingStation"; import { EndorsementGroupsBelongsToUsers } from "../../models/through/EndorsementGroupsBelongsToUsers"; import { User } from "../../models/User"; +/** + * Returns all Endorsement groups that are mentorable by the current user + * @param request + * @param response + * @param next + */ +async function getMentorable(request: Request, response: Response, next: NextFunction) { + try { + const user: User = request.body.user; + const userMentorGroups = await user.getMentorGroups(); + + let endorsementGroups: EndorsementGroup[] = []; + for (const m of userMentorGroups) { + const egs = await m.getEndorsementGroups(); + + for (const eg of egs) { + if (endorsementGroups.find(e => e.id === eg.id) == null) { + endorsementGroups.push(eg); + } + } + } + + response.send(endorsementGroups); + } catch (e) { + next(e); + } +} + /** * Gets a collection of all endorsement groups * @param request @@ -310,6 +338,7 @@ async function createEndorsementGroup(request: Request, response: Response, next } export default { + getMentorable, getAll, getAllWithStations, getByID, diff --git a/src/controllers/solo/SoloAdminController.ts b/src/controllers/solo/SoloAdminController.ts index 24bcbe3..4316913 100644 --- a/src/controllers/solo/SoloAdminController.ts +++ b/src/controllers/solo/SoloAdminController.ts @@ -14,7 +14,8 @@ type CreateSoloRequestBody = { endorsement_group_id: string; }; -type UpdateSoloRequestBody = Omit; +type Optional = Pick, K> & Omit; +type UpdateSoloRequestBody = Optional; /** * Create a new Solo @@ -44,6 +45,7 @@ async function createSolo(request: Request, response: Response, next: NextFuncti await EndorsementGroupsBelongsToUsers.create({ user_id: body.trainee_id, + created_by: user.id, endorsement_group_id: Number(body.endorsement_group_id), solo_id: solo.id, }); @@ -75,7 +77,7 @@ async function createSolo(request: Request, response: Response, next: NextFuncti */ async function updateSolo(request: Request, response: Response, next: NextFunction) { try { - const body = request.body as UpdateSoloRequestBody; + const body = request.body as UpdateSoloRequestBody & { endorsement_group_id?: string }; _SoloAdminValidator.validateUpdateRequest(body); const currentSolo = await UserSolo.findOne({ @@ -89,6 +91,23 @@ async function updateSolo(request: Request, response: Response, next: NextFuncti return; } + if (body.endorsement_group_id != null) { + // We are trying to assign a new endorsement group, so remove all old ones first + await EndorsementGroupsBelongsToUsers.destroy({ + where: { + user_id: body.trainee_id, + solo_id: currentSolo.id, + }, + }); + + await EndorsementGroupsBelongsToUsers.create({ + user_id: body.trainee_id, + endorsement_group_id: Number(body.endorsement_group_id), + solo_id: currentSolo.id, + created_by: request.body.user.id, + }); + } + const newDuration = currentSolo.solo_used + Number(body.solo_duration); // If solo_start == NULL, then the solo is still active @@ -109,7 +128,20 @@ async function updateSolo(request: Request, response: Response, next: NextFuncti }); } - response.sendStatus(HttpStatusCode.Ok); + const returnUser = await User.findOne({ + where: { + id: body.trainee_id, + }, + include: [ + { + association: User.associations.user_solo, + include: [UserSolo.associations.solo_creator], + }, + User.associations.endorsement_groups, + ], + }); + + response.send(returnUser); } catch (e) { next(e); } @@ -177,10 +209,14 @@ async function extendSolo(request: Request, response: Response, next: NextFuncti }); if (solo == null) { - response.sendStatus(HttpStatusCode.InternalServerError); + response.sendStatus(HttpStatusCode.NotFound); return; } + await solo.update({ + extension_count: solo.extension_count + 1, + }); + response.sendStatus(HttpStatusCode.Ok); } catch (e) { next(e); diff --git a/src/controllers/solo/_SoloAdmin.validator.ts b/src/controllers/solo/_SoloAdmin.validator.ts index c87c1d5..d2cacfd 100644 --- a/src/controllers/solo/_SoloAdmin.validator.ts +++ b/src/controllers/solo/_SoloAdmin.validator.ts @@ -47,6 +47,17 @@ function validateUpdateRequest(data: any) { validationObject: data.trainee_id, toValidate: [{ val: ValidationOptions.NON_NULL }, { val: ValidationOptions.NUMBER }], }, + { + name: "endorsement_group_id_present", + validationObject: data, + toValidate: arg0 => { + if (arg0.solo_start != null) { + return arg0.endorsement_group_id != null; + } + + return true; + }, + }, ]); if (validation.invalid) { diff --git a/src/models/MentorGroup.ts b/src/models/MentorGroup.ts index 0d79cef..ea0e434 100644 --- a/src/models/MentorGroup.ts +++ b/src/models/MentorGroup.ts @@ -5,6 +5,7 @@ import { sequelize } from "../core/Sequelize"; import { Course } from "./Course"; import { UserBelongToMentorGroups } from "./through/UserBelongToMentorGroups"; import { EndorsementGroup } from "./EndorsementGroup"; +import MentorGroupExtensions from "./extensions/MentorGroupExtensions"; export class MentorGroup extends Model, InferCreationAttributes> { // @@ -38,6 +39,8 @@ export class MentorGroup extends Model, InferCreati courses: Association; endorsement_groups: Association; }; + + getEndorsementGroups = MentorGroupExtensions.getEndorsementGroups.bind(this); } MentorGroup.init( diff --git a/src/models/extensions/MentorGroupExtensions.ts b/src/models/extensions/MentorGroupExtensions.ts new file mode 100644 index 0000000..35c7d6f --- /dev/null +++ b/src/models/extensions/MentorGroupExtensions.ts @@ -0,0 +1,27 @@ +import { MentorGroup } from "../MentorGroup"; +import { EndorsementGroup } from "../EndorsementGroup"; + +/** + * Gets all the endorsement groups associated to this + */ +async function getEndorsementGroups(this: MentorGroup): Promise { + const m = await MentorGroup.findOne({ + where: { + id: this.id, + }, + include: [ + { + association: MentorGroup.associations.endorsement_groups, + through: { + attributes: [], + }, + }, + ], + }); + + return m?.endorsement_groups ?? []; +} + +export default { + getEndorsementGroups, +}; diff --git a/src/utility/helper/ValidationHelper.ts b/src/utility/helper/ValidationHelper.ts index 7763a67..7f2d34f 100644 --- a/src/utility/helper/ValidationHelper.ts +++ b/src/utility/helper/ValidationHelper.ts @@ -3,22 +3,10 @@ import { Response } from "express"; import { ValidatorType } from "../../controllers/_validators/ValidatorType"; import { HttpStatusCode } from "axios"; -export function validateObject(object: any, keys: string[], checkStringNull = false) { - let malformedKeys: any[] = []; - - keys.forEach(value => { - if (object[value] == null || (checkStringNull && object[value] == "")) { - malformedKeys.push(value); - } - }); - - return malformedKeys; -} - type ValidationType = { name: string; validationObject: any; - toValidate: Array<{ val: ValidationOptions; value?: any }>; + toValidate: Array<{ val: ValidationOptions; value?: any }> | ((arg0: any) => boolean); }; export enum ValidationOptions { @@ -50,6 +38,20 @@ function validate(options: ValidationType[]): { invalid: boolean; message: any[] // Replace spaces with nothing! if (typeof opt.validationObject == "string") toCheck = toCheck.replace(/\s/g, ""); + if (typeof opt.toValidate == "function") { + if (!opt.toValidate(toCheck)) { + return { + invalid: true, + message: ["Unknown Validation Error (custom validation func)"], + }; + } + + return { + invalid: false, + message: [], + }; + } + opt.toValidate.forEach(val => { switch (val.val) { case ValidationOptions.NON_NULL: