diff --git a/server/controllers/memoDataCtrl.js b/server/controllers/memoDataCtrl.js index 8c58edd..4efaed1 100644 --- a/server/controllers/memoDataCtrl.js +++ b/server/controllers/memoDataCtrl.js @@ -73,6 +73,38 @@ async function getDraftByEndPoint(req, res) { } } +async function putApplicationCodesInMemo(req, res) { + // STEP 2 EDITING: USE IT IN A SECOND STEP + try { + const { applicationCodes, ladokRoundIds } = req.body + const { memoEndPoint, status, courseCode, semester } = req.params + + const dbResponse = [] + + const draftExist = await dbOneDocument.fetchMemo({ memoEndPoint, status, courseCode, ladokRoundIds, semester }) + + if (draftExist) { + log.info( + 'memo draft already exists,' + memoEndPoint + ' so it will be updated (object id ' + draftExist._id + ')' + ) + dbResponse.push( + await dbOneDocument.updateMemo( + { memoEndPoint, status, courseCode, ladokRoundIds, semester }, + { applicationCodes } + ) + ) + } else { + log.debug('no memo draft was found to update with memoEndPoint: ', memoEndPoint) + } + + log.info('dbResponse length', dbResponse.length, { memoEndPoint }) + res.status(201).json(dbResponse) + } catch (error) { + log.error('Error in while trying to putMemoById', { error }) + return error + } +} + async function putDraftByEndPoint(req, res) { // STEP 2 EDITING: USE IT IN A SECOND STEP try { @@ -172,6 +204,20 @@ async function createDraftByMemoEndPoint(req, res) { } } +// No need to merge +async function getAllMemos(req, res) { + log.info('getAllMemos: Received request for memo') + try { + const dbResponse = await dbArrayOfDocument.getAllMemos() + + res.json(dbResponse || []) + log.info('getAllMemos: Responded to request for memo') + } catch (err) { + log.error('getAllMemos: Failed request for memo, error:', { err }) + return err + } +} + async function getAllMemosByCourseCodeAndType(req, res) { // TODO: ADD FETCHING USED COURSE ROUNDS (DRAFTS + PUBLISHED) const { courseCode, type } = req.params @@ -259,10 +305,12 @@ module.exports = { createDraftByMemoEndPoint, getDraftByEndPoint, getPublishedMemoByEndPoint, + getAllMemos, getAllMemosByCourseCodeAndType, getMemosStartingFromPrevSemester, getCourseSemesterUsedRounds, deleteDraftByMemoEndPoint, postNewVersionOfPublishedMemo, putDraftByEndPoint, + putApplicationCodesInMemo, } diff --git a/server/controllers/mixedWebAndPdfMemosCtrl.js b/server/controllers/mixedWebAndPdfMemosCtrl.js index 1c3f134..e6a6b3c 100644 --- a/server/controllers/mixedWebAndPdfMemosCtrl.js +++ b/server/controllers/mixedWebAndPdfMemosCtrl.js @@ -158,12 +158,22 @@ async function _formPrioritizedMemosByCourseRounds(courseCode, pdfMemos, webBase const miniMemos = {} // firstly fetch web-based await webBasedMemos.forEach( - ({ ladokRoundIds, memoEndPoint, memoCommonLangAbbr, memoName, semester, version, lastChangeDate }) => { + ({ + ladokRoundIds, + memoEndPoint, + memoCommonLangAbbr, + memoName, + semester, + version, + lastChangeDate, + applicationCodes, + }) => { if (!semester) return if (!miniMemos[semester]) miniMemos[semester] = {} ladokRoundIds.forEach(roundId => { miniMemos[semester][roundId] = { courseCode, + applicationCodes, ladokRoundIds, semester, memoEndPoint, @@ -186,6 +196,7 @@ async function _formPrioritizedMemosByCourseRounds(courseCode, pdfMemos, webBase lastChangeDate = pdfMemoUploadDate, previousFileList, semester, + applicationCode, }) => { if (!semester) return if (!koppsRoundId) return @@ -193,6 +204,7 @@ async function _formPrioritizedMemosByCourseRounds(courseCode, pdfMemos, webBase if (!miniMemos[semester][koppsRoundId]) { miniMemos[semester][koppsRoundId] = { courseCode, + applicationCodes: [applicationCode], courseMemoFileName, ladokRoundIds: [koppsRoundId], lastChangeDate, diff --git a/server/controllers/storedMemoPdfsCtrl.js b/server/controllers/storedMemoPdfsCtrl.js index fb920ef..d9e5635 100644 --- a/server/controllers/storedMemoPdfsCtrl.js +++ b/server/controllers/storedMemoPdfsCtrl.js @@ -4,6 +4,7 @@ const log = require('@kth/log') const { StoredMemoPdfsModel } = require('../models/storedMemoPdfsModel') +const dbOneDocument = require('../lib/dbDataById') async function getStoredMemoPdfListByCourseCode(req, res) { if (!req.params.courseCode) throw new Error('courseCode must be set') @@ -55,6 +56,18 @@ async function fetchAll() { return migrated } +async function fetchAllMemoFiles(req, res) { + try { + log.debug('Fetching all migrated courseMemos ') + const migrated = await StoredMemoPdfsModel.find({}) + log.info('Length of data in db', migrated.length) + res.json(migrated || []) + } catch (error) { + log.error('Error in while trying to get all migrating memos files ', { error }) + return error + } +} + // /count async function checkLength(req, res) { try { @@ -68,7 +81,33 @@ async function checkLength(req, res) { } } +async function updateStoredPdfMemoWithApplicationCodes(req, res) { + try { + const applicationCodes = req.body + const { _id } = req.params + + const dbResponse = [] + + const draftExist = await dbOneDocument.fetchMemoFileById(_id) + + if (draftExist) { + log.info('memo file already exists,' + _id + ' so it will be updated (object id ' + draftExist._id + ')') + dbResponse.push(await dbOneDocument.updateMemoFile(_id, applicationCodes)) + } else { + log.debug('no memo file was found to update with Id: ', _id) + } + + log.info('dbResponse length', dbResponse.length, { _id }) + res.status(201).json(dbResponse) + } catch (error) { + log.error('Error in while trying to update memo file with application codes', { error }) + return error + } +} + module.exports = { collectionLength: checkLength, getStoredMemoPdfListByCourseCode, + fetchAllMemoFiles, + updateStoredPdfMemoWithApplicationCodes, } diff --git a/server/lib/dbDataById.js b/server/lib/dbDataById.js index 2ac1351..2fa9dc6 100644 --- a/server/lib/dbDataById.js +++ b/server/lib/dbDataById.js @@ -2,6 +2,7 @@ /* eslint-disable no-underscore-dangle */ const log = require('@kth/log') const { CourseMemo } = require('../models/mainMemoModel') +const { StoredMemoPdfsModel } = require('../models/storedMemoPdfsModel') /* ****** */ /* ANY BY STATUS AND MemoEndPoint */ @@ -12,6 +13,21 @@ async function fetchMemoByEndPointAndStatus(memoEndPoint, status) { const memo = await CourseMemo.findOne({ memoEndPoint, status }) // courseCode return memo } +// No need to merge this method to master. This is only for updating old memos +async function fetchMemo(memo) { + if (!memo) throw new Error('Memo must be set') + log.debug('Fetching memo based on ', memo) + const doc = await CourseMemo.findOne(memo) + return doc +} + +// No need to merge this method to master. This is only for updating old memos +async function fetchMemoFileById(Id) { + if (!Id) throw new Error('Id must be set') + log.debug('Fetching memo based on ', Id) + const doc = await StoredMemoPdfsModel.findById(Id) + return doc +} async function getMemoVersion(courseCode, memoEndPoint, version) { if (!courseCode) throw new Error('courseCode must be set') @@ -34,7 +50,7 @@ async function storeNewCourseMemoData(data) { // ***** USED TO POST NEW COURSE MEMO FIRST DRAFT if (!data) throw new Error('Trying to post empty/innacurate data in storeNewCourseMemoData') else { - if (!data.courseCode || !data.semester || !data.ladokRoundIds) + if (!data.courseCode || !data.semester || !data.applicationCodes) throw new Error('Trying to post data without courseCode or semester or ladokRoundsIds in storeNewCourseMemoData') data.lastChangeDate = new Date() const doc = new CourseMemo(data) @@ -43,6 +59,58 @@ async function storeNewCourseMemoData(data) { return result } } +// No need to merge this method to master. This is only for updating old memos +async function updateMemo(memo, data) { + // UPPDATERA DRAFT GENOM memoEndPoint + if (memo) { + log.debug('Update of existing memo: ', { memo }) + + const resultAfterUpdate = await CourseMemo.findOneAndUpdate( + memo, + { $set: data }, + { maxTimeMS: 100, new: true, useFindAndModify: false } + ) + if (resultAfterUpdate && resultAfterUpdate.version) { + log.debug('Updated draft: ', { + version: resultAfterUpdate.version, + memoEndPoint: resultAfterUpdate.memoEndPoint, + memoName: resultAfterUpdate.memoName, + id: resultAfterUpdate.id, + applicationCodes: resultAfterUpdate.applicationCodes, + semester: resultAfterUpdate.semester, + courseCode: resultAfterUpdate.courseCode, + ladokRoundIds: resultAfterUpdate.ladokRoundIds, + }) + } + return resultAfterUpdate + } + log.debug('No roundCourseMemoData found for updating it with new data', { memo }) +} + +// No need to merge this method to master. This is only for updating old memos +async function updateMemoFile(_id, data) { + // UPPDATERA DRAFT GENOM memoEndPoint + if (_id) { + log.debug('Update of existing memo: ', { _id }) + + const resultAfterUpdate = await StoredMemoPdfsModel.findByIdAndUpdate( + _id, + { $set: data }, + { maxTimeMS: 100, new: true, useFindAndModify: false } + ) + if (resultAfterUpdate && resultAfterUpdate.id) { + log.debug('Updated draft: ', { + id: resultAfterUpdate.id, + koppsRoundId: resultAfterUpdate.koppsRoundId, + semester: resultAfterUpdate.semester, + courseCode: resultAfterUpdate.courseCode, + applicationCode: resultAfterUpdate.applicationCode, + }) + } + return resultAfterUpdate + } + log.debug('No roundCourseMemoFileData found for updating it with new data', { _id }) +} async function updateMemoByEndPointAndStatus(memoEndPoint, data, status) { // UPPDATERA DRAFT GENOM memoEndPoint @@ -73,6 +141,10 @@ module.exports = { getMemoVersion, fetchMemoByEndPointAndStatus, storeNewCourseMemoData, + updateMemo, updateMemoByEndPointAndStatus, removeCourseMemoDataById, + fetchMemo, + fetchMemoFileById, + updateMemoFile, } diff --git a/server/lib/dbSeveralDocument.js b/server/lib/dbSeveralDocument.js index cc1d367..b6ae26f 100644 --- a/server/lib/dbSeveralDocument.js +++ b/server/lib/dbSeveralDocument.js @@ -17,6 +17,15 @@ function logInCaseOfPossibleLimit(doc = [], matchingParameters = {}) { return } +// No need to merge this method to master. This is only for updating old memos +async function getAllMemos() { + log.debug('Fetching all courseMemos') + const allDocuments = await CourseMemo.find() + if (allDocuments) log.debug('Done fetching memos total: ', allDocuments.length) + logInCaseOfPossibleLimit(allDocuments) + return allDocuments +} + async function getAllMemosByStatus(courseCode, status) { if (!courseCode) throw new Error('courseCode must be set') const matchingParameters = { courseCode, status } @@ -62,8 +71,8 @@ async function getCourseSemesterUsedRounds(courseCode, semester) { const finalObj = { usedRoundsThisSemester: [], } - await webBasedMemos.map(({ ladokRoundIds }) => finalObj.usedRoundsThisSemester.push(...ladokRoundIds)) - await dbMigratedPdfs.map(({ koppsRoundId }) => finalObj.usedRoundsThisSemester.push(...koppsRoundId)) + await webBasedMemos.map(({ applicationCodes }) => finalObj.usedRoundsThisSemester.push(...applicationCodes)) + await dbMigratedPdfs.map(({ applicationCode }) => finalObj.usedRoundsThisSemester.push(...applicationCode)) log.debug('Successfully got used round ids for', { courseCode, @@ -170,6 +179,7 @@ async function getMemosFromPrevSemester(courseCode, fromSemester) { } module.exports = { + getAllMemos, getAllMemosByStatus, getCourseSemesterUsedRounds, getFirstMemosBySemesterAndStatus, diff --git a/server/models/mainMemoModel.js b/server/models/mainMemoModel.js index a2a7985..5dff29e 100644 --- a/server/models/mainMemoModel.js +++ b/server/models/mainMemoModel.js @@ -1,7 +1,7 @@ 'use strict' -const combinedMemoData = require('./combinedMemoData') const mongoose = require('mongoose') +const combinedMemoData = require('./combinedMemoData') const schema = mongoose.Schema({ // _id: mongoose.Schema.Types.ObjectId, @@ -28,6 +28,12 @@ const schema = mongoose.Schema({ trim: true, required: [true, 'Enter course rounds'], }, + applicationCodes: { + type: Array, + items: String, + trim: true, + required: [true, 'Enter course application codes'], + }, lastChangeDate: { type: String, default: new Date(), diff --git a/server/models/storedMemoPdfsModel.js b/server/models/storedMemoPdfsModel.js index eff5e68..fceb672 100644 --- a/server/models/storedMemoPdfsModel.js +++ b/server/models/storedMemoPdfsModel.js @@ -49,6 +49,11 @@ const schema = mongoose.Schema({ type: Array, default: [], }, + applicationCode: { + type: String, + trim: true, + default: '', + }, }) const StoredMemoPdfsModel = mongoose.model('MemoFile', schema) diff --git a/server/server.js b/server/server.js index 01e6cf5..642b7d2 100644 --- a/server/server.js +++ b/server/server.js @@ -1,7 +1,7 @@ 'use strict' -const server = require('@kth/server') const path = require('path') +const server = require('@kth/server') // Load .env file in development mode const nodeEnv = process.env.NODE_ENV && process.env.NODE_ENV.toLowerCase() @@ -143,8 +143,8 @@ addPaths( const authByApiKey = passport.authenticate('apikey', { session: false }) // Application specific API enpoints -const { CourseMemo, MixedWebAndPdfMemosList } = require('./controllers') const { ApiRouter } = require('kth-node-express-routing') +const { CourseMemo, MixedWebAndPdfMemosList } = require('./controllers') const apiRoute = ApiRouter(authByApiKey) const paths = getPaths() @@ -158,12 +158,14 @@ apiRoute.register(paths.api.getMemoVersion, CourseMemo.getMemoVersion) // step 2 // Get one draft | update it apiRoute.register(paths.api.getDraftByEndPoint, CourseMemo.getDraftByEndPoint) // step 2: editor, fetch data apiRoute.register(paths.api.updateCreatedDraft, CourseMemo.putDraftByEndPoint) // step 2: editor, fast update +apiRoute.register(paths.api.updatedMemoWithApplicationCodes, CourseMemo.putApplicationCodesInMemo) // step 1: choose action, new draft, or copied draft from published memo (same memoEndPoint) apiRoute.register(paths.api.createDraftByMemoEndPoint, CourseMemo.createDraftByMemoEndPoint) apiRoute.register(paths.api.copyFromAPublishedMemo, CourseMemo.createDraftByMemoEndPoint) // // GET ARRAY OF MEMOS BY TYPE AND COURSE CODE +apiRoute.register(paths.api.getAllMemos, CourseMemo.getAllMemos) apiRoute.register(paths.api.getAllMemosByCourseCodeAndType, CourseMemo.getAllMemosByCourseCodeAndType) apiRoute.register(paths.api.getCourseSemesterUsedRounds, CourseMemo.getCourseSemesterUsedRounds) // step 1: to show up which rounds already taken apiRoute.register(paths.api.getMemosStartingFromPrevYearSemester, CourseMemo.getMemosStartingFromPrevSemester) // step 1: to show up which rounds already taken @@ -177,7 +179,12 @@ server.use('/', apiRoute.getRouter()) apiRoute.register(paths.api.deleteDraftByMemoEndPoint, CourseMemo.deleteDraftByMemoEndPoint) // Get list of stored pdf files for kursinfo-web (migrated from kurs-pm-api) +apiRoute.register(paths.api.getStoredMemoPdfList, StoredMemoPdf.fetchAllMemoFiles) apiRoute.register(paths.api.getStoredMemoPdfListByCourseCode, StoredMemoPdf.getStoredMemoPdfListByCourseCode) +apiRoute.register( + paths.api.updateStoredPdfMemoWithApplicationCodes, + StoredMemoPdf.updateStoredPdfMemoWithApplicationCodes +) // Get list of stored pdf files together with web-based memos all published for kurs-pm-web (migrated from kurs-pm-api) apiRoute.register( paths.api.getPdfAndWebMemosListByCourseCode, diff --git a/swagger.json b/swagger.json index 1ab6a70..688657d 100644 --- a/swagger.json +++ b/swagger.json @@ -230,6 +230,111 @@ ] } }, + "/v1/memo/file/{_id}": { + "put": { + "operationId": "updateStoredPdfMemoWithApplicationCodes", + "summary": "The draft is already in database. Update some of its properties, f.e., only text for examinations, for fast and often update", + "description": "Saves a course memo document descriptive data in the database per course and term, and per chosen {key: value}", + "parameters": [ + { + "name": "_id", + "in": "path", + "description": "_id is used to keep all versions under the same endpoint, unique for memo for one kursomgång. Unchangeable enpoint to display it in url (for bookmarks) and keep track on versions change", + "required": true, + "type": "string" + }, + { + "name": "memoFileData", + "in": "body", + "description": "Data with course code, semester and rounds to create first draft", + "required": true, + "schema": { + "$ref": "#/definitions/MemoData" + } + } + ], + "tags": ["v1"], + "responses": { + "200": { + "description": "A successful save" + }, + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "security": [ + { + "api_key": ["write"] + } + ] + } + }, + "/v1/memo/{courseCode}/{semester}/{memoEndPoint}/{status}": { + "put": { + "operationId": "updatedMemoWithApplicationCodes", + "summary": "The memo is already in database. Update some of its properties, f.e., only text for examinations, for fast and often update", + "description": "Saves a course memo document descriptive data in the database per course and term, and per chosen {key: value}", + "parameters": [ + { + "name": "courseCode", + "in": "path", + "description": "courseCode of the memo draft to update course memo data", + "required": true, + "type": "string" + }, + { + "name": "status", + "in": "path", + "description": "status of the memo draft to update course memo data", + "required": true, + "type": "string" + }, + { + "name": "semester", + "in": "path", + "description": "semester of the memo draft to update course memo data", + "required": true, + "type": "string" + }, + { + "name": "memoEndPoint", + "in": "path", + "description": "memoEndPoint of the memo draft to update course memo data", + "required": true, + "type": "string" + }, + { + "name": "roundCourseMemoData", + "in": "body", + "description": "Data with applicationCodes and rounds to update first draft", + "required": true, + "schema": { + "$ref": "#/definitions/MemoData" + } + } + ], + "tags": ["v1"], + "responses": { + "200": { + "description": "A successful save" + }, + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "security": [ + { + "api_key": ["write"] + } + ] + } + }, "/v1/memo/course/{courseCode}/memoEndPoint/{memoEndPoint}/version/{version}": { "get": { "operationId": "getMemoVersion", @@ -331,6 +436,35 @@ ] } }, + "/v1/memos/all": { + "get": { + "operationId": "getAllMemos", + "summary": "Get all course memos documents fetched by this course code", + "description": "Gets some form of data that only requires read access", + "parameters": [ + ], + "tags": ["v1"], + "responses": { + "200": { + "description": "The requested data", + "schema": { + "$ref": "#/definitions/MemoDataListForCourseCode" + } + }, + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "security": [ + { + "api_key": ["read"] + } + ] + } + }, "/v1/memo/{memoEndPoint}": { "get": { "operationId": "getPublishedMemoByEndPoint", @@ -407,6 +541,47 @@ ] } }, + "/v1/pdfCourseMemoFilesList": { + "get": { + "operationId": "getStoredMemoPdfList", + "summary": "Get a list of course memos stored as pdf files (integrated from kurs-pm-api) used by kursinfo-web", + "description": "Gets course memos for rounds in a course. Memos stored as pdfs in kursinfo storage (not generated)", + "parameters": [ + ], + "tags": ["v1"], + "responses": { + "200": { + "description": "The requested data", + "schema": { + "$ref": "#/definitions/MemoPdfFilesList" + } + }, + "400": { + "description": "Bad Request, something with the request is invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "No organization or user matched the Ids", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "Unexpected error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "security": [ + { + "api_key": ["read"] + } + ] + } + }, "/v1/pdfCourseMemoFilesList/{courseCode}/{semester}": { "get": { "operationId": "getStoredMemoPdfListByCourseCode",