diff --git a/backend/Questionnaire for Upload.xlsx b/backend/Questionnaire for Upload.xlsx index 6bf47a7b..36dbf296 100644 Binary files a/backend/Questionnaire for Upload.xlsx and b/backend/Questionnaire for Upload.xlsx differ diff --git a/backend/models/questionnaires.js b/backend/models/questionnaires.js index 58964d66..da46d976 100644 --- a/backend/models/questionnaires.js +++ b/backend/models/questionnaires.js @@ -7,7 +7,7 @@ const questionnairesSchema = new Schema( // _id: mongoose.Schema.Types.ObjectId, // line above results in the following error "document must have an _id before saving" title: { type: String, required: false, unique: false }, - language: { type: String, required: false, unique: true }, + language: { type: String, required: false, unique: false }, questions: { type: Array, required: true }, }, { diff --git a/backend/routes/admins.js b/backend/routes/admins.js index b0d85340..f501c7ab 100644 --- a/backend/routes/admins.js +++ b/backend/routes/admins.js @@ -4,9 +4,13 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const Admin = require('../models/admin'); +const Questionnaires = require('../models/questionnaires'); const mongoose = require('mongoose'); -const { loadQuestionnaireXlsxIntoDB, loadTranslationXlsxIntoDB } = require('./excelToDb'); +const { + loadQuestionnaireXlsxIntoDB, + loadTranslationXlsxIntoDB, +} = require('./excelToDb'); const SALT_ROUNDS = 10; const ERRMSG = { error: { message: 'Not logged in or auth failed' } }; @@ -151,57 +155,90 @@ router.route('/:id').delete((req, res) => { /** * verify that the http request comes from a admin user - * detect the user from the body containing the JWT token + * detect the user from the body containing the JWT token * respond the request with an error if the token is missing or the user identified * in token is not an admin user * call the isAdminCallBack function if the user is an admin -**/ + **/ function enforceAdminOnly(req, res, isAdminCallBack) { - //verify the request has a jwtToken beloning to an Admin User if (!req.body || !req.body.jwToken) { return res .status(401) - .json({ error: { message: 'Missing JWT Token' } }) + .json({ error: { message: 'Missing JWT Token' } }); } jwt.verify(req.body.jwToken, process.env.JWT_KEY, function (err, token) { if (err) { return res .status(401) - .json({ error: { message: 'Invalid JWT Token' } }) + .json({ error: { message: 'Invalid JWT Token' } }); } - Admin.findOne({ email: token.email }) - .exec((error, admin) => { - if (error || !admin) { - return res - .status(401) - .json({ error: { message: 'Invalid Admin User' } }) - } - isAdminCallBack(); - }); + Admin.findOne({ email: token.email }).exec((error, admin) => { + if (error || !admin) { + return res + .status(401) + .json({ error: { message: 'Invalid Admin User' } }); + } + isAdminCallBack(); + }); }); - } //route for uploading the questionnaires spreadsheet in the database router.route('/questionnairefile').post((req, res) => { enforceAdminOnly(req, res, processQuestionnaireAsAdmin); function processQuestionnaireAsAdmin() { + console.log(req.files, req.body); if (!req.files || !req.files.questionnaire) { return res .status(400) .json({ error: { message: 'Missing Questionnaire File' } }); } if (req.files.questionnaire.truncated) { - return res - .status(400) - .json({ error: { message: 'Questionnaire File is too large' } }); + return res.status(400).json({ + error: { message: 'Questionnaire File is too large' }, + }); } const excelFileContent = req.files.questionnaire.data; - return loadQuestionnaireXlsxIntoDB(excelFileContent).then(() => { - res.status(200).send("Questionnaire Documenent Recieved"); - }).catch((err) => { - res.status(500).send("Error, Storing Questionnaire in database"); - }); + const title = req.body.title; + return loadQuestionnaireXlsxIntoDB(excelFileContent, title) + .then(() => { + res.status(200).json({ + message: 'Questionnaire Documenent Recieved', + }); + }) + .catch((err) => { + console.log(err); + res.status(500).json({ + message: 'Error, Storing Questionnaire in database', + }); + }); + } +}); +//route for deleteing a questionnaire by title +router.route('/deletequestionnaire/:title').delete((req, res) => { + enforceAdminOnly(req, res, deleteQuestionnaireByTitle); + function deleteQuestionnaireByTitle() { + return Questionnaires.deleteMany({ + title: decodeURIComponent(req.params.title), + }) + .then((results) => { + if (!results.ok) { + console.log('Delete Failed'); + res.status(500).json({ message: 'Delete Failed' }); + return; + } + if (result.deletedCount === 0) { + res.status(404).json({ message: 'Title not found' }); + return; + } + + res.status(200).json({ message: 'Questionnaire Removed' }); + }) + .catch((err) => { + res.status(500).json({ + message: 'Error, Deleting Questionnaires from database', + }); + }); } }); //route for uploading the translation spreadsheet in the database @@ -219,11 +256,13 @@ router.route('/translateContent').post((req, res) => { .json({ error: { message: 'Translation File is too large' } }); } const excelFileContent = req.files.translations.data; - return loadTranslationXlsxIntoDB(excelFileContent).then(() => { - res.status(200).send("Translation Document Recieved"); - }).catch((err) => { - res.status(500).send("Error, Storing Translation in database"); - }); + return loadTranslationXlsxIntoDB(excelFileContent) + .then(() => { + res.status(200).send('Translation Document Recieved'); + }) + .catch((err) => { + res.status(500).send('Error, Storing Translation in database'); + }); } }); diff --git a/backend/routes/excelToDb.js b/backend/routes/excelToDb.js index abedb25e..18ccc3b3 100644 --- a/backend/routes/excelToDb.js +++ b/backend/routes/excelToDb.js @@ -9,11 +9,11 @@ const { LanguageOptions, WorkshopTitle } = require('../LanguageOptions'); /** * load questionnaire excel file into objects in the Questionnaires collection - * excelFileContent - Node Buffer containing the excel file, this assumes must be formmated + * excelFileContent - Node Buffer containing the excel file, this assumes must be formmated * with proper sheets for each langauge * returns; a promise that resolves when operaiton is done * */ -function loadQuestionnaireXlsxIntoDB(excelFileContent) { +function loadQuestionnaireXlsxIntoDB(excelFileContent, title = WorkshopTitle) { const questionnairePromises = LanguageOptions.map((language, idx) => { const stream = new Readable(); stream.push(excelFileContent); @@ -25,14 +25,24 @@ function loadQuestionnaireXlsxIntoDB(excelFileContent) { rows.forEach((row, id) => { if (id === 0) { let errorMessage = ''; - const validHeaders = ["#(id)", "Slug", "Category", "Text", "QuestionType", "AnswerSelections", "AnswerSelectionsValues", "Required?", "FollowUpQuestionSlug", "ParentQuestionSlug"]; + const validHeaders = [ + '#(id)', + 'Slug', + 'Category', + 'Text', + 'QuestionType', + 'AnswerSelections', + 'AnswerSelectionsValues', + 'Required?', + 'FollowUpQuestionSlug', + 'ParentQuestionSlug', + ]; if (row.length !== validHeaders.length) { - errorMessage = "invalid column name row"; - } - else { + errorMessage = 'invalid column name row'; + } else { for (let i = 0; i < validHeaders.length; i++) { if (row[i] !== validHeaders[i]) { - errorMessage = "invalid column name: " + row[i]; + errorMessage = 'invalid column name: ' + row[i]; } } } @@ -40,7 +50,6 @@ function loadQuestionnaireXlsxIntoDB(excelFileContent) { throw new Error(errorMessage); } return; - } data.push({ @@ -60,27 +69,33 @@ function loadQuestionnaireXlsxIntoDB(excelFileContent) { }); }); return Promise.all(questionnairePromises).then((questionnaires) => { - const title = WorkshopTitle; const insertPromises = LanguageOptions.map((language, idx) => { const questions = questionnaires[idx]; - const insertNewQuestionnaire = () => { - return Questionnaires.insertMany({ title, language: language.code, questions }) + return Questionnaires.insertMany({ + title, + language: language.code, + questions, + }); }; const removeExistingQuestionnaires = (_id) => { - return Questionnaires.findByIdAndDelete({ _id }) + return Questionnaires.findByIdAndDelete({ _id }); }; - return Questionnaires.find({ title, language: language.code }).then((result) => { - if (result.length !== 0) { - return removeExistingQuestionnaires(result[0]._id).then(() => { + return Questionnaires.find({ title, language: language.code }).then( + (result) => { + if (result.length !== 0) { + return removeExistingQuestionnaires(result[0]._id).then( + () => { + return insertNewQuestionnaire(); + } + ); + } else { return insertNewQuestionnaire(); - }); - } else { - return insertNewQuestionnaire(); + } } - }); + ); }); return Promise.all(insertPromises); }); @@ -88,7 +103,7 @@ function loadQuestionnaireXlsxIntoDB(excelFileContent) { /** * load translation excel file into objects in the TranslatedContent collection - * excelFileContent - Node Buffer containing the excel file, this assumes must be formmated + * excelFileContent - Node Buffer containing the excel file, this assumes must be formmated * with proper translation sheet format * returns; a promise that resolves when operaiton is done * */ @@ -96,47 +111,56 @@ function loadTranslationXlsxIntoDB(excelFileContent) { const stream = new Readable(); stream.push(excelFileContent); stream.push(null); - return xlsxFile(stream).then((rows) => { + return xlsxFile(stream) + .then((rows) => { + const data = rows.reduce((obj, row) => { + for (let i = 1; i < row.length; i++) { + const languageObject = obj[LanguageOptions[i - 1].code]; - const data = rows.reduce((obj, row) => { - for (let i = 1; i < row.length; i++) { - const languageObject = obj[LanguageOptions[i - 1].code]; - - if (languageObject) { - languageObject[row[0]] = row[i]; - } else { - obj[LanguageOptions[i - 1].code] = { - [row[0]]: row[i], - }; + if (languageObject) { + languageObject[row[0]] = row[i]; + } else { + obj[LanguageOptions[i - 1].code] = { + [row[0]]: row[i], + }; + } } - } - return obj; - }, {}); - return data; - }).then((translations) => { - const title = WorkshopTitle; - const insertPromises = LanguageOptions.map((language) => { - const content = translations[language.code]; - const insertNewTranslatedContent = () => { - return TranslatedContent.insertMany({ title, language: language.code, content }) - }; + return obj; + }, {}); + return data; + }) + .then((translations) => { + const title = WorkshopTitle; + const insertPromises = LanguageOptions.map((language) => { + const content = translations[language.code]; + const insertNewTranslatedContent = () => { + return TranslatedContent.insertMany({ + title, + language: language.code, + content, + }); + }; - const removeExistingTranslatedContent = (_id) => { - return TranslatedContent.findByIdAndDelete({ _id }) - }; + const removeExistingTranslatedContent = (_id) => { + return TranslatedContent.findByIdAndDelete({ _id }); + }; - return TranslatedContent.find({ title, language: language.code }).then((result) => { - if (result.length !== 0) { - return removeExistingTranslatedContent(result[0]._id).then(() => { + return TranslatedContent.find({ + title, + language: language.code, + }).then((result) => { + if (result.length !== 0) { + return removeExistingTranslatedContent( + result[0]._id + ).then(() => { + return insertNewTranslatedContent(); + }); + } else { return insertNewTranslatedContent(); - }) - } - else { - return insertNewTranslatedContent(); - } + } + }); }); + return Promise.all(insertPromises); }); - return Promise.all(insertPromises); - }); } -module.exports = { loadQuestionnaireXlsxIntoDB, loadTranslationXlsxIntoDB }; \ No newline at end of file +module.exports = { loadQuestionnaireXlsxIntoDB, loadTranslationXlsxIntoDB }; diff --git a/backend/routes/questionnaires/questionnaires.js b/backend/routes/questionnaires/questionnaires.js index 015678e6..a3b74274 100644 --- a/backend/routes/questionnaires/questionnaires.js +++ b/backend/routes/questionnaires/questionnaires.js @@ -13,7 +13,7 @@ router.route('/').get((req, res) => { router.route('/:title.:language').get((req, res) => { Questionnaires.findOne({ - title: req.params.title, + title: decodeURIComponent(req.params.title), language: req.params.language, }) .then((questionnaires) => { diff --git a/src/containers/App/App.js b/src/containers/App/App.js index 7fd2c224..e365488e 100644 --- a/src/containers/App/App.js +++ b/src/containers/App/App.js @@ -3,7 +3,9 @@ import MainContainer from '../MainContainer/MainContainer'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import Admin from '../../compositions/Admin/Admin.js'; import AdminDashboard from '../../containers/AdminDashboard/AdminDashboard'; +import EditQuestionnaires from '../../containers/EditQuestionnaires/EditQuestionnaires'; import './App.css'; +import { uploadQuestinnaires } from '../../sendRequest/apis'; function App() { return ( @@ -16,6 +18,9 @@ function App() { + + + diff --git a/src/containers/EditQuestionnaires/EditQuestionnaires.css b/src/containers/EditQuestionnaires/EditQuestionnaires.css new file mode 100644 index 00000000..227ffaa8 --- /dev/null +++ b/src/containers/EditQuestionnaires/EditQuestionnaires.css @@ -0,0 +1,19 @@ +.EditQuestionnaires .questionnaire-section-title { + font-weight: 900; + margin: 2rem 0 1rem; +} + +.EditQuestionnaires .li { + font-weight: 900; + margin: 2rem 0 1rem; + color: white; +} +.EditQuestionnaires .delete { + width: 20px; + height: 20px; + border-radius: 25px; + margin: auto; + background: #fa6900; + color: #fff; + opacity: 0.3; +} diff --git a/src/containers/EditQuestionnaires/EditQuestionnaires.js b/src/containers/EditQuestionnaires/EditQuestionnaires.js new file mode 100644 index 00000000..6f81b2f3 --- /dev/null +++ b/src/containers/EditQuestionnaires/EditQuestionnaires.js @@ -0,0 +1,234 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { + uploadQuestinnaires, + getQuestionsByLanguage, + getQuestions, + deleteQuestionnaireByTitle, +} from '../../sendRequest/apis'; +import { sendRequest } from '../../sendRequest/sendRequest'; +import { getAuthToken } from '../../utilities/auth_utils'; +import Navbar from '../../compositions/Navbar/Navbar'; +import Button from '../../components/Button/Button'; +import LanguageDropdown from '../../components/LanguageDropdown/LanguageDropdown'; +import './EditQuestionnaires.css'; + +const EditQuestionnaires = () => { + const [chooseFile, toggleChooseFile] = useState(false); + const [questionnaireStatus, setQuestionnaireStatus] = useState(false); + const [questionnaires, setQuestionnaires] = useState([]); + const [titleList, setTitleList] = useState([]); + const [questionnaireTitle, setTitle] = useState(''); + const [getQuestionCall, callGetQuestions] = useState(false); + const [fetchQuestionnaire, setFetchQuestionnaire] = useState(false); + const [languageDropdown, toggleLanguageDropDown] = useState(false); + const [language, setLanguage] = useState('en'); + const [questions, setListOfQuestions] = useState([]); + const [reFetch, setRefetch] = useState(false); + const [workshopTitle, setWorkshopTitle] = useState(''); + + const content = { buttonHome: 'Home' }; + const setToggleChooseFile = () => { + if (chooseFile) { + toggleChooseFile(false); + } else { + toggleChooseFile(true); + } + }; + const softDeleteResponse = (title) => { + const confirmBox = window.confirm( + 'Do you really want to delete this questionnaire response?' + ); + if (confirmBox === false) { + return; + } + + const requestObj = { + url: deleteQuestionnaireByTitle.replace( + ':title', + encodeURIComponent(title) + ), + method: 'DELETE', + }; + const jwt = getAuthToken(); + const headers = { + Authorization: `Bearer ${jwt}`, + }; + sendRequest(requestObj, headers) + .then((response) => { + setRefetch(true); + }) + .catch((err) => { + console.log( + `error soft-deleting questionnaire response ${title}`, + err + ); + }); + }; + const changeLanguage = (language) => { + setLanguage(language); + toggleLanguageDropDown(false); + callGetQuestions(true); + }; + const getByLanguage = () => { + toggleLanguageDropDown(true); + let encodedTitle = encodeURIComponent(questionnaireTitle); + const requestObj = { + url: getQuestionsByLanguage + .replace(':title', encodedTitle) + .replace(':language', language), + method: 'GET', + }; + sendRequest(requestObj) + .then((response) => { + console.log(response); + setListOfQuestions(response.questions); + }) + .catch((error) => console.log(error)); + }; + if (getQuestionCall) { + getByLanguage(); + callGetQuestions(false); + } + useEffect(() => { + if (fetchQuestionnaire || (questionnaires.length > 0 && !reFetch)) { + return; + } else { + const requestObj = { + url: getQuestions, + method: 'GET', + }; + setFetchQuestionnaire(true); + sendRequest(requestObj) + .then((response) => { + let objs = response.responses; + let newArray = []; + setQuestionnaires(objs); + let titles = objs.map((obj) => obj.title); + titles = [...new Set(titles)]; + setTitleList(titles); + setFetchQuestionnaire(false); + setRefetch(false); + }) + .catch((error) => { + setFetchQuestionnaire(false); + }); + } + }); + const uploadNewQuestionnaire = (qustionnaireFile) => { + const formData = new FormData(); + + formData.append( + 'questionnaire', + qustionnaireFile, + qustionnaireFile.name + ); + formData.append('title', workshopTitle); + const jwt = getAuthToken(); + const headers = { + Authorization: `Bearer ${jwt}`, + }; + + const requestObj = { + url: uploadQuestinnaires, + method: 'POST', + body: formData, + }; + setQuestionnaireStatus('Uploading ' + qustionnaireFile.name); + sendRequest(requestObj, headers, true) + .then((response) => { + setQuestionnaireStatus('Uploaded ' + qustionnaireFile.name); + setRefetch(true); + + setTimeout(() => setQuestionnaireStatus(''), 10 * 1000); + setToggleChooseFile(); + }) + .catch((error) => { + setQuestionnaireStatus( + 'Error! Upload of ' + qustionnaireFile.name + ' failed' + ); + + setTimeout(() => setQuestionnaireStatus(''), 10 * 1000); + setToggleChooseFile(); + }); + }; + const selectLanguage = (title) => { + toggleLanguageDropDown(true); + setTitle(title); + changeLanguage('en'); + }; + const switchViews = () => { + toggleLanguageDropDown(false); + setListOfQuestions([]); + }; + + return ( + + + + {chooseFile ? ( + + WorkshopTitle + setWorkshopTitle(e.target.value)} + required + type="text" + > + + + uploadNewQuestionnaire(e.target.files[0]) + } + /> + + ) : ( + + )} + + {questionnaireStatus ? questionnaireStatus : ''} + + + {languageDropdown ? ( + + Back + changeLanguage(lang)} + language={language} + > + + ) : ( + + {titleList.map((title) => ( + + {title} + selectLanguage(title)}> + View + + softDeleteResponse(title)} + > + + ))} + + )} + {questionnaireTitle} + + {questions.map((q) => ( + + {q.id}. {q.text} + + ))} + + + + ); +}; +export default EditQuestionnaires; diff --git a/src/sendRequest/apis.js b/src/sendRequest/apis.js index f9cf2065..ddc9cbe5 100644 --- a/src/sendRequest/apis.js +++ b/src/sendRequest/apis.js @@ -16,10 +16,14 @@ module.exports = { addQuestionnaireResponse: '/api/questionnaire-responses/add', deleteQuestionnaireResponse: '/api/questionnaire-responses/delete/:id', addQuestionnaires: '/api/questionnaires/add', - deleteQuestionnaire: '/api/questionnaires/delete/:id', + deleteQuestionnaire: '/api/questionnaires/:id', getQuestions: '/api/questionnaires', getTranslatedContent: '/api/translatedContent', generateResponsesExcel: '/api/generateExcel/responses', getResponsesExcel: '/api/generateExcel/getLatest', deleteResponsesExcel: '/api/generateExcel/delete', + uploadQuestinnaires: '/api/admins/questionnairefile', + getListOfQuestionnaires: '/api/admins/questionnaires', + getQuestionsByLanguage: 'api/questionnaires/:title.:language', + deleteQuestionnaireByTitle: '/api/admins/deletequestionnaire/:title', }; diff --git a/src/sendRequest/sendRequest.js b/src/sendRequest/sendRequest.js index f5595153..cd8d9985 100644 --- a/src/sendRequest/sendRequest.js +++ b/src/sendRequest/sendRequest.js @@ -3,12 +3,20 @@ const DEFAULT_HEADERS = { 'Content-Type': 'application/json', }; -const sendRequest = (requestObj, headers = DEFAULT_HEADERS) => { +const sendRequest = ( + requestObj, + headers = DEFAULT_HEADERS, + browser_gen_content_type = false +) => { const url = requestObj.url; delete requestObj.url; + headers = { ...DEFAULT_HEADERS, ...headers }; + if (browser_gen_content_type) { + delete headers['Content-Type']; + } return fetch(url, { ...requestObj, - headers: { ...headers, ...DEFAULT_HEADERS }, + headers: headers, }).then((data) => data.json()); };