diff --git a/src/helpers/csv-helper-util.js b/src/helpers/csv-helper-util.js index 424140b31c13ea94ac3c17561e282bdb446696eb..20ab9ec4a34ff2d5bd0793be18e816e7db7e322f 100644 --- a/src/helpers/csv-helper-util.js +++ b/src/helpers/csv-helper-util.js @@ -33,7 +33,7 @@ class CSVFileValidator { this.response.inValidMessages.push(message); } return this.response; - } + } /** * @private @@ -180,6 +180,19 @@ class CSVFileValidator { return; } } + + // speacial character validation + if (valueConfig.isSpecialChar && !_.isEmpty(columnValue)) { + const specialChars = "!`~@#$^*+=[]\\\'{}|\"<>%/"; + const isSpecialCharsPresent = specialChars.split('').some(char => + columnValue.includes(char)); + if (isSpecialCharsPresent) { + this.handleError(valueConfig, 'specialCharError', `${valueConfig.name} has special character at (${rowIndex + 1}) row / (${columnIndex + 1}) column`, [valueConfig.name, rowIndex + 1, columnIndex + 1, columnValue]); + hasError = true; + return; + } + } + // Array validation if (valueConfig.isArray) { rowData[valueConfig.inputName] = _.isEmpty(columnValue) ? [] : columnValue.split(',') diff --git a/src/service/kafkaQumlConsumerService.js b/src/service/kafkaQumlConsumerService.js index b9b8df82b198a8c8a140208226d1fa16152066b5..32a86be8203a866834c65463ca3c6ab4c5a3b01a 100644 --- a/src/service/kafkaQumlConsumerService.js +++ b/src/service/kafkaQumlConsumerService.js @@ -17,6 +17,7 @@ const templateClassMap = { "2" : 'mcq-vertical-split', "3" : 'mcq-horizontal' } +const allowedMimeType = ['image/jpeg', 'image/png']; const total_options = 4; const API_URL = { ASSET_CREATE: "asset/v4/create", @@ -25,8 +26,11 @@ const API_URL = { QUESTION_REVIEW: "/question/v4/review/", QUESTION_PUBLISH: "/question/v4/publish/", QUESTION_UPDATE: "/question/v4/update/", + QUESTION_RETIRE: "/question/v4/retire/", QUESTIONSET_ADD: "/questionset/v4/add", - +} +const questionTypeMap = { + 'mcq': "Multiple Choice Question" } const rspObj = {}; @@ -73,9 +77,10 @@ const qumlConsumer = () => { const initQuestionCreateProcess = (questionData) => { logger.info({ message: "Question creating process started" }); async.waterfall([ + async.apply(createQuestion, questionData), async.apply(startDownloadFileProcess, questionData), async.apply(prepareQuestionBody), - async.apply(createQuestion), + async.apply(updateQuestion), async.apply(reviewQuestion, questionData.status), async.apply(publishQuestion, questionData.status), async.apply(linkQuestionToQuestionSet, questionData) @@ -92,10 +97,51 @@ const initQuestionCreateProcess = (questionData) => { }); }; -const startDownloadFileProcess = (question, outerCallback) => { +const createQuestion = (questionData, callback) => { + const questionType = questionData.questionType.toLowerCase(); + let createApiData = { + "request": { + "question": { + "code" : uuidv4(), + "name": questionData.name ? questionData.name : questionTypeMap[questionType], + "mimeType": 'application/vnd.sunbird.question', + "primaryCategory": questionTypeMap[questionType], + "questionFileRefId": questionData.questionFileRefId, + "processId": questionData.processId + } + } + }; + console.log('createQuestionBody:: =====> ' , JSON.stringify(createApiData)); + fetch(`${envVariables.SUNBIRD_ASSESSMENT_SERVICE_BASE_URL}${API_URL.QUESTION_CREATE}`, { + method: "POST", // or 'PUT' + headers: { + "Content-Type": "application/json", + "Authorization" : `Bearer ${envVariables.SUNBIRD_PORTAL_API_AUTH_TOKEN}` + }, + body: JSON.stringify(createApiData), + }) + .then((response) => response.json()) + .then((createResponseData) => { + console.log('createQuestion response :: =====> ' , JSON.stringify(createApiData)); + if (createResponseData.responseCode && _.toLower(createResponseData.responseCode) === "ok") { + callback(null, createResponseData); + } else { + callback(createResponseData); + } + }) + .catch((error) => { + logger.error({ + message: `Error while creating the question :: ${JSON.stringify(error)}`, + }); + callback(error); + }); + +} + +const startDownloadFileProcess = (question, createQuestionRes, outerCallback) => { const filesToDownload = _.omitBy(_.pick(question, ['questionImage','option1Image', 'option2Image', 'option3Image', 'option4Image']), _.isEmpty); if(_.isEmpty(filesToDownload)) { - return outerCallback(null, question); + return outerCallback(null, question, createQuestionRes); } const downloadedFiles = {}; async.eachOfSeries(filesToDownload, function (data, key, callback) { @@ -107,6 +153,7 @@ const startDownloadFileProcess = (question, outerCallback) => { } else { async.waterfall([ async.apply(downloadFile, data), + async.apply(validateFileType), async.apply(createAssest, question), async.apply(uploadAsset), async.apply(deleteFileFromTemp), @@ -120,16 +167,19 @@ const startDownloadFileProcess = (question, outerCallback) => { }); } }, function (error) { - console.log("===================error", error); + console.log(" startDownloadFileProcess :: error ::", JSON.stringify(error)); if (error) { + updateResponse( + createQuestionRes.result.identifier, + `Something went wrong while downloading the files from google drive: ${JSON.stringify(error)}` + ); outerCallback(error); } else { - outerCallback(null, question); + outerCallback(null, question, createQuestionRes); } }); } - const downloadFile = (data, callback) => { const googleAuth = new GoogleOauth(); const fileId = getIdFromUrl(data); @@ -137,10 +187,23 @@ const downloadFile = (data, callback) => { console.log("RESULT :: =====> ", JSON.stringify(result)); callback(null, result); }).catch((error) => { - callback(error); + console.log("downloadFile Func error: ", JSON.stringify(error)); + if(error.errors || error.response) { + callback(error.errors || _.pick(error.response, ['status', 'statusText', 'request'])); + } else { + callback(error); + } }) } +const validateFileType = (data, callback) => { + if(allowedMimeType.includes(data.mimeType)) { + callback(null, data); + } else { + callback(`Images mimetype should be one of: [${allowedMimeType}]'`); + } +} + const createAssest = (question, data, callback) => { const extension = path.extname(data.name); const filename = path.basename(data.name, extension); @@ -201,8 +264,7 @@ const uploadAsset = (data, callback) => { } else { callback(uploadResponseData); } - }) - .catch((error) => { + }).catch((error) => { logger.error({ message: `Error while uploading the assest :: ${JSON.stringify(error)}`, }); @@ -238,10 +300,8 @@ const getIdFromUrl = (url) => { } } -const prepareQuestionBody = (question, callback) => { +const prepareQuestionBody = (question, createQuestionRes, callback) => { let metadata = { - code : uuidv4(), - mimeType: 'application/vnd.sunbird.question', editorState: {}, body: mergeQuestionTextAndImage(question.questionText, question.questionImage) }; @@ -256,7 +316,7 @@ const prepareQuestionBody = (question, callback) => { metadata.editorState.question = mergeQuestionTextAndImage(question.questionText, question.questionImage); metadata = _.omitBy(metadata, _.isEmpty); console.log("prepareQuestionBody :: => ", JSON.stringify(metadata)); - callback(null, metadata); + callback(null, metadata, createQuestionRes); } const mergeQuestionTextAndImage = (questionText, questionImage) => { @@ -331,39 +391,36 @@ const getResponseDeclaration = (question) => { return responseDeclaration; } -const createQuestion = (questionBody, callback) => { - let createApiData = { - "request": { - "question": questionBody +const updateQuestion = (questionBody, createQuestionRes, callback) => { + const updateNewData = { + request: { + question: questionBody } }; //fetch call for creating a question. - console.log('createQuestionBody:: =====> ' , JSON.stringify(createApiData)); - fetch(`${envVariables.SUNBIRD_ASSESSMENT_SERVICE_BASE_URL}${API_URL.QUESTION_CREATE}`, { - method: "POST", // or 'PUT' + console.log('updateQuestionBody:: =====> ' , JSON.stringify(updateNewData)); + fetch(`${envVariables.SUNBIRD_ASSESSMENT_SERVICE_BASE_URL}${API_URL.QUESTION_UPDATE}${createQuestionRes.result.identifier}`, { + method: "PATCH", // or 'PUT' headers: { "Content-Type": "application/json", "Authorization" : `Bearer ${envVariables.SUNBIRD_PORTAL_API_AUTH_TOKEN}` }, - body: JSON.stringify(createApiData), + body: JSON.stringify(updateNewData), }) .then((response) => response.json()) - .then((createResponseData) => { - if (createResponseData.responseCode && _.toLower(createResponseData.responseCode) === "ok") { - console.log('createResponseData OK IF:: =====> ' , JSON.stringify(createResponseData)); - callback(null, createResponseData); + .then((updateResponseData) => { + if (updateResponseData.responseCode && _.toLower(updateResponseData.responseCode) === "ok") { + callback(null, updateResponseData); } else { - console.log('createResponseData ELSE:: =====> ' , JSON.stringify(createResponseData)); - callback(createResponseData); + callback(updateResponseData); } }) .catch((error) => { logger.error({ - message: `Error while creating the question :: ${JSON.stringify(error)}`, + message: `Error while updating the question :: ${JSON.stringify(error)}`, }); callback(error); }); - } const reviewQuestion = (status, questionRes, callback) => { @@ -494,9 +551,48 @@ const linkQuestionToQuestionSet = (questionData, questionRes, callback) => { }); } +const retireQuestion = (identifier) => { + const reqBody = { + "request": { + "question": {} + } + }; + console.log("retireQuestion :: request Body:: =====> ", JSON.stringify(reqBody)); + fetch(`${envVariables.SUNBIRD_ASSESSMENT_SERVICE_BASE_URL}${API_URL.QUESTION_RETIRE}${identifier}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "Authorization" : `Bearer ${envVariables.SUNBIRD_PORTAL_API_AUTH_TOKEN}` + }, + body: JSON.stringify(reqBody), + }) + .then((response) => response.json()) + .then((response) => { + rspObj.responseCode = "OK"; + rspObj.result = { + questionStatus: `Successfully retire the question for the identifier: ${identifier}`, + }; + logger.info({ + message: "Successfully retire the question", + rspObj, + }); + }) + .catch((error) => { + rspObj.errMsg = `Something Went wrong while retiring question :: ${identifier} `; + rspObj.responseCode = responseCode.SERVER_ERROR; + logger.error( + { + message: "Something Went wrong while retiring question", + errorData: error, + rspObj, + }, + errorCodes.CODE2 + ); + }); +}; //function to update the status of all other fetch calls mentioned above using question update. -const updateResponse = (updateData, updateMessage) => { +const updateResponse = (identifier, updateMessage) => { const updateNewData = { request: { question: { @@ -505,7 +601,7 @@ const updateResponse = (updateData, updateMessage) => { } }; console.log("updateResponse :: request Body:: =====> ", JSON.stringify(updateNewData)); - fetch(`${envVariables.SUNBIRD_ASSESSMENT_SERVICE_BASE_URL}${API_URL.QUESTION_UPDATE}${updateData}`, { + fetch(`${envVariables.SUNBIRD_ASSESSMENT_SERVICE_BASE_URL}${API_URL.QUESTION_UPDATE}${identifier}`, { method: "PATCH", // or 'PUT' headers: { "Content-Type": "application/json", @@ -516,21 +612,23 @@ const updateResponse = (updateData, updateMessage) => { .then((response) => response.json()) .then((updateResult) => { console.log("updateResult :: ======> ", JSON.stringify(updateResult)); + retireQuestion(identifier); rspObj.responseCode = "OK"; rspObj.result = { - questionStatus: `Successfully updated the question data for the identifier: ${updateData}`, + questionStatus: `Successfully updated the question error data for the identifier: ${identifier}`, }; logger.info({ - message: "Successfully updated the question data", + message: "Successfully updated the question error data", rspObj, }); }) .catch((error) => { - rspObj.errMsg = "Something Went wrong while updating question data"; + retireQuestion(identifier); + rspObj.errMsg = `Something Went wrong while updating question error data :: ${identifier}`; rspObj.responseCode = responseCode.SERVER_ERROR; logger.error( { - message: "Something Went wrong while updating question data", + message: "Something Went wrong while updating question error data", errorData: error, rspObj, }, diff --git a/src/service/qumlBulkService.js b/src/service/qumlBulkService.js index a47cc75b410a341671a323070a0d4bf0f32e7f16..d95c634b130349faf05208b3f806b1e65f7dafc5 100644 --- a/src/service/qumlBulkService.js +++ b/src/service/qumlBulkService.js @@ -117,6 +117,11 @@ const setBulkUploadCsvConfig = () => { const urlError = (headerName, rowNumber, columnNumber, value) => { setError(`${headerName} has invalid url value at row: ${rowNumber}`); }; + + const specialCharError = (headerName, rowNumber, columnNumber, value) => { + setError(`${headerName} has special character value at row: ${rowNumber}`); + }; + const maxLengthError = (headerName, rowNumber, columnNumber, maxLength, length) => { setError(`Length of ${headerName} exceeds ${maxLength}. Please give a shorter ${headerName} at row: ${rowNumber}`); }; @@ -132,7 +137,7 @@ const setBulkUploadCsvConfig = () => { }; const headers = [ - { name: 'Name of the Question', inputName: 'name', maxLength: 120, required: true, requiredError, headerError, maxLengthError }, + { name: 'Name of the Question', inputName: 'name', isSpecialChar: true, maxLength: 120, required: true, requiredError, headerError, maxLengthError, specialCharError }, { name: 'QuestionText', inputName: 'questionText', headerError, maxLength: 1000, maxLengthError }, { name: 'QuestionImage', inputName: 'questionImage', headerError, isUrl: true, urlError}, { name: 'Option Layout', inputName: 'optionLayout', required: true, requiredError, headerError, in: ['1', '2', '3'], inError }, @@ -146,10 +151,10 @@ const setBulkUploadCsvConfig = () => { { name: 'Option4Image', inputName: 'option4Image', headerError}, { name: 'AnswerNo', inputName: 'answerNo', required: true, requiredError, headerError }, { name: 'Level 1 Question Set Section', inputName: 'level1', headerError }, - { name: 'Keywords', inputName: 'keywords', isArray: true, headerError }, - { name: 'Author', inputName: 'author',headerError, maxLength: 300, maxLengthError }, - { name: 'Copyright', inputName: 'copyright',headerError, maxLength: 300, maxLengthError }, - { name: 'Attributions', inputName: 'attributions', isArray: true, headerError, maxLength: 300, maxLengthError } + { name: 'Keywords', inputName: 'keywords', isSpecialChar: true, isArray: true, headerError, specialCharError }, + { name: 'Author', inputName: 'author',isSpecialChar: true, headerError, maxLength: 300, maxLengthError, specialCharError }, + { name: 'Copyright', inputName: 'copyright',isSpecialChar: true, headerError, maxLength: 300, maxLengthError, specialCharError }, + { name: 'Attributions', inputName: 'attributions',isSpecialChar: true, isArray: true, headerError, maxLength: 300, maxLengthError, specialCharError } ]; const validateRow = (row, rowIndex, flattenHierarchyObj) => {