From 53ff6298e2a5f12b2453dd6d9e2bedec6494ecda Mon Sep 17 00:00:00 2001 From: Sarah Vaupel Date: Thu, 23 Jan 2020 20:45:25 +0100 Subject: [PATCH] feat(exam-correct): persist results and more --- .../lib/storage-manager/storage-manager.js | 4 + .../src/utils/exam-correct/exam-correct.js | 227 +++++++++++++----- .../src/utils/exam-correct/exam-correct.sass | 8 +- 3 files changed, 172 insertions(+), 67 deletions(-) diff --git a/frontend/src/lib/storage-manager/storage-manager.js b/frontend/src/lib/storage-manager/storage-manager.js index d071d0c2b..4f491e92c 100644 --- a/frontend/src/lib/storage-manager/storage-manager.js +++ b/frontend/src/lib/storage-manager/storage-manager.js @@ -56,6 +56,10 @@ export class StorageManager { this._saveToLocalStorage({ ...this._getFromLocalStorage(), [key]: value }); break; } + case LOCATION.SESSION: { + this._saveToSessionStorage({ ...this._getFromSessionStorage(), [key]: value }); + break; + } case LOCATION.WINDOW: { this._saveToWindow({ ...this._getFromLocalStorage(), [key]: value }); break; diff --git a/frontend/src/utils/exam-correct/exam-correct.js b/frontend/src/utils/exam-correct/exam-correct.js index a7b7612a2..4306f6db1 100644 --- a/frontend/src/utils/exam-correct/exam-correct.js +++ b/frontend/src/utils/exam-correct/exam-correct.js @@ -28,18 +28,12 @@ const EXAM_CORRECT_USER_DNAME_ATTR = 'exam-correct--user-dname'; const INPUT_EMPTY_CLASS = 'input--invalid'; -// TODO get from settings -const MOMENT_FORMAT = 'DD.MM.YY HH:mm:ss'; - -// TODO 1 for debugging only, 3 would be a better choice -const USER_VALIDATION_MIN_LENGTH = 1; - const STATUS = { - NONE: 0, - SUCCESS: 1, - AMBIGUOUS: 2, - ERROR: 3, - LOADING: 4, + NONE: null, + SUCCESS: 'success', + AMBIGUOUS: 'ambiguous', + FAILURE: 'failure', + LOADING: 'loading', }; @@ -49,6 +43,7 @@ const STATUS = { export class ExamCorrect { _storageManager = new StorageManager('EXAM_CORRECT', '0.0.0', { location: LOCATION.SESSION, encrypted: true }); + _dateFormat; _element; @@ -58,6 +53,9 @@ export class ExamCorrect { _userInputCandidates; _partInputs; + _cIndices; + _lastColumnIndex; + constructor(element, app) { if (!element) { throw new Error('Exam Correct utility cannot be setup without an element!'); @@ -75,6 +73,9 @@ export class ExamCorrect { this._userInputCandidates = document.getElementById(EXAM_CORRECT_USER_INPUT_CANDIDATES_ID); this._partInputs = [...this._element.querySelectorAll(`input[${EXAM_CORRECT_PART_INPUT_ATTR}]`)]; + // TODO get date format by post request + this._dateFormat = 'YYYY-MM-DD HH:mm:ss'; + if (this._sendBtn) this._sendBtn.addEventListener('click', this._sendCorrectionHandler.bind(this)); else console.error('ExamCorrect utility could not detect send button!'); @@ -90,6 +91,20 @@ export class ExamCorrect { if (!this._userInputCandidates) { throw new Error('ExamCorrect utility could not detect user input candidate list!'); } + + this._cIndices = new Map( + [...this._element.querySelectorAll('[uw-exam-correct-header]')] + .map((header) => [header.getAttribute('uw-exam-correct-header'), header.cellIndex]) + ); + + this._lastColumnIndex = this._element.querySelector('thead > tr').querySelectorAll('th').length - 1; + + // show previously submitted results + const previousEntries = this._storageManager.load('entries'); + if (previousEntries && previousEntries.length > 0) { + // TODO sort previous results by current sorting order first + previousEntries.forEach((entry) => this._addRow(entry)); + } } destroy() { @@ -100,10 +115,10 @@ export class ExamCorrect { } _validateUserInput() { - const user = this._userInput.value; + (!this._userInput.value) ? this._userInput.classList.add('no-value') : this._userInput.classList.remove('no-value'); - // do nothing in case of empty or too short input - if (!user || user.length < USER_VALIDATION_MIN_LENGTH) { + // do nothing in case of empty or invalid input + if (!this._userInput.value || this._userInput.reportValidity && !this._userInput.reportValidity()) { removeAllChildren(this._userInputCandidates); setStatus(this._userInputStatus, STATUS.NONE); return; @@ -122,7 +137,7 @@ export class ExamCorrect { setStatus(this._userInputStatus, STATUS.LOADING); const body = { - user: user, + user: this._userInput.value, }; this._app.httpClient.post({ @@ -132,13 +147,20 @@ export class ExamCorrect { }).then( (response) => response.json() ).then( - (response) => this._processResponse(response, user) + (response) => this._processResponse(response, body.user) ).catch((error) => { console.error('Error while validating user input', error); }); } _sendCorrectionHandler() { + // TODO avoid code duplication + if (this._userInput.reportValidity && !this._userInput.reportValidity()) { + removeAllChildren(this._userInputCandidates); + setStatus(this._userInput, STATUS.NONE); + return; + } + // refocus user input element for convenience this._userInput.focus(); @@ -154,13 +176,11 @@ export class ExamCorrect { const results = {}; for (const input of this._partInputs) { + if (input.reportValidity && !input.reportValidity()) { + input.focus(); + return; + } if (input.value) { - if (isNaN(input.value)) { - input.classList.add('input--invalid'); - input.focus(); - return; - } - const partKey = input.getAttribute(EXAM_CORRECT_PART_INPUT_ATTR); if (!partKey) { console.error('Error while parsing results: Could not detect exam part key attribute'); @@ -173,34 +193,12 @@ export class ExamCorrect { // abort send if there are no results (after validation) if (Object.keys(results).length <= 0) return; - // TODO create and use template for this - const correctionRow = document.createElement('TR'); - correctionRow.classList.add('table__row'); - const dateTD = document.createElement('TD'); - const now = moment(); - dateTD.appendChild(document.createTextNode(now.format(MOMENT_FORMAT))); - dateTD.setAttribute('date', moment()); - dateTD.classList.add('exam-correct--local-time'); - const userTD = document.createElement('TD'); - userTD.appendChild(document.createTextNode(user)); - userTD.setAttribute(EXAM_CORRECT_USER_ATTR, user); - const partTDs = this._partInputs.map((input) => { - const partTD = document.createElement('TD'); - const partKey = input.getAttribute(EXAM_CORRECT_PART_INPUT_ATTR); - if (results[partKey]) - partTD.appendChild(document.createTextNode(results[partKey])); - return partTD; - }); - const statusTD = document.createElement('TD'); - const statusDiv = document.createElement('DIV'); - statusDiv.classList.add('exam-correct--loading'); - statusTD.appendChild(statusDiv); - [dateTD,userTD,...partTDs, statusTD].forEach((td) => { - td.classList.add('table__td'); - correctionRow.appendChild(td); - }); - const tableBody = this._element.querySelector(`tbody:not(#${EXAM_CORRECT_INPUT_BODY_ID})`); - tableBody.insertBefore(correctionRow, tableBody.firstChild); + const rowInfo = { + users: [{ id: userId, name: user }], + results: results, + status: STATUS.LOADING, + }; + this._addRow(rowInfo); // clear inputs on validation success this._clearUserInput(); @@ -218,7 +216,7 @@ export class ExamCorrect { }).then( (response) => response.json() ).then( - (response) => this._processResponse(response, user, results) + (response) => this._processResponse(response, body.user, results) ).catch((error) => { console.error('Error while processing response', error); }); @@ -270,17 +268,27 @@ export class ExamCorrect { this._userInputCandidates.appendChild(candidateItem); }); } else { + // TODO what to do in this case? setStatus(this._userInputStatus, STATUS.FAILURE); } return; } - + + const savedEntries = this._storageManager.load('entries') || []; + let newEntry = { + users: null, + results: null, + status: STATUS.FAILURE, + }; + + console.log('response', response); + for (let row of [...this._element.rows]) { - const userElem = row.cells.item(1); - const userIdent = userElem && userElem.getAttribute(EXAM_CORRECT_USER_ATTR); + let userElem = row.cells.item(this._cIndices.get('user')); + const userIdent = userElem && userElem.getAttribute(EXAM_CORRECT_USER_ATTR); // TODO use other attribute identifier if (userIdent === user) { - let status = STATUS.ERROR; + let status = STATUS.FAILURE; switch (response.status) { // TODO fetch update time from response and replace case 'success': @@ -289,9 +297,11 @@ export class ExamCorrect { userElem.setAttribute(EXAM_CORRECT_USER_ATTR, response.user.id); userElem.innerHTML = userToHTML(response.user); const timeElem = row.cells.item(0); - timeElem.innerHTML = moment(response.time).format(MOMENT_FORMAT); + timeElem.innerHTML = moment(response.time).format(this._dateFormat); timeElem.classList.remove('exam-correct--local-time'); // TODO special style for server time? + newEntry.users = [response.user]; + newEntry.results = response.results; } // TODO replace results with results from response // TODO set edit button visibility @@ -301,11 +311,15 @@ export class ExamCorrect { // TODO set edit button visibility status = STATUS.AMBIGUOUS; if (response.users) { - this._showUserList(row, response.users, results); + userElem = this._showUserList(row, response.users, results); + newEntry.users = response.users; + newEntry.results = results; } break; case 'failure': status = STATUS.FAILURE; + newEntry.users = [user]; + newEntry.results = results; break; default: // TODO show tooltip with 'invalid response' @@ -315,9 +329,17 @@ export class ExamCorrect { row.querySelectorAll('.exam-correct--loading').forEach((elem) => { setStatus(elem, status); }); - break; + newEntry.status = status || STATUS.FAILURE; + savedEntries.push(newEntry); + this._storageManager.save('entries', savedEntries); + return; } } + + // insert a new row if no previous entry was found + // this._addRow(newEntry); + // savedEntries.unshift(newEntry); + // this._storageManager.save('entries', savedEntries); } } @@ -331,7 +353,10 @@ export class ExamCorrect { // TODO better name _showUserList(row, users, results) { - const userElem = row.cells.item(1); + let userElem = row.cells.item(this._cIndices.get('user')); + if (!userElem) { + userElem = document.createElement('TD'); + } if (users) { removeAllChildren(userElem); const list = document.createElement('UL'); @@ -352,12 +377,14 @@ export class ExamCorrect { } else { console.error('Unable to show users from invalid response'); } + + return userElem; } _rowToRequest(row, listItem, results) { const now = moment(); const timeElem = row.cells.item(0); - timeElem.innerHTML = now.format(MOMENT_FORMAT); + timeElem.innerHTML = now.format(this._dateFormat); timeElem.classList.add('exam-correct--local-time'); const userElem = row.cells.item(1); const statusElem = row.querySelector('.exam-correct--ambiguous'); @@ -377,13 +404,29 @@ export class ExamCorrect { (response) => response.json() ).then((response) => { switch (response.status) { - case 'success': + case 'success': { userElem.innerHTML = userToHTML(response.user); // TODO replace part results with results from server - timeElem.innerHTML = moment(response.time).format(MOMENT_FORMAT); + timeElem.innerHTML = moment(response.time).format(this._dateFormat); timeElem.classList.remove('exam-correct--local-time'); setStatus(statusElem, STATUS.SUCCESS); + const savedEntries = this._storageManager.load('entries'); + for (let i = 0; i < savedEntries.length; i++) { + for (let user of savedEntries[i].users) { + if (user.id === response.user.id) { + savedEntries[i] = { + users: [response.user], + results: response.results, + status: STATUS.SUCCESS, + date: response.time, + }; + break; + } + } + } + this._storageManager.save('entries', savedEntries); break; + } default: // non-success response on request with a uuid => panic and ignore for now } @@ -392,6 +435,60 @@ export class ExamCorrect { }); } + _addRow(rowInfo) { + console.log('rowInfo', rowInfo); + // TODO create and use template for this + const newRow = document.createElement('TR'); + newRow.classList.add('table__row'); + + const cells = new Map(); + + const dateCell = document.createElement('TD'); + const date = rowInfo.date ? moment(rowInfo.date) : moment(); + dateCell.appendChild(document.createTextNode(date.format(this._dateFormat))); + dateCell.setAttribute('date', date.utc().format()); + if (!rowInfo.date) dateCell.classList.add('exam-correct--local-time'); + cells.set(this._cIndices.get('date'), dateCell); + + let userCell = document.createElement('TD'); + if (!rowInfo.users || rowInfo.users.length === 0) { + console.error('Found rowInfo without users info!'); + } else if (rowInfo.users.length === 1) { + const user = rowInfo.users[0]; + userCell.innerHTML = userToHTML(user); + userCell.setAttribute(EXAM_CORRECT_USER_ATTR, user.id || user.name); + } else { + userCell = this._showUserList(newRow, rowInfo.users, rowInfo.results); + } + cells.set(this._cIndices.get('user'), userCell); + + for (const [partKey, partResult] of Object.entries(rowInfo.results)) { + const cellIndex = this._cIndices.get(partKey); + if (cellIndex === undefined) { + console.error('Could not determine cell index from part key!'); + } else { + const partCell = document.createElement('TD'); + partCell.innerHTML = partResult; + cells.set(cellIndex, partCell); + } + } + + const statusCell = document.createElement('TD'); + const statusDiv = document.createElement('DIV'); + setStatus(statusDiv, rowInfo.status); + statusCell.appendChild(statusDiv); + cells.set(this._cIndices.get('status'), statusCell); + + for (let i = 0; i <= this._lastColumnIndex; i++) { + const cell = cells.get(i) || document.createElement('TD'); + cell.classList.add('table__td'); + newRow.appendChild(cell); + } + + const tableBody = this._element.querySelector(`tbody:not(#${EXAM_CORRECT_INPUT_BODY_ID})`); + insertAsFirstChild(newRow, tableBody); + } + _clearUserInput() { removeAllChildren(this._userInputCandidates); clearInput(this._userInput); @@ -406,16 +503,20 @@ export class ExamCorrect { function clearInput(inputElement) { inputElement.value = null; } +function insertAsFirstChild(elementToInsert, parentElement) { + parentElement.insertBefore(elementToInsert, parentElement.firstChild); +} function removeAllChildren(element) { while (element.firstChild) { element.removeChild(element.firstChild); } } - function userToHTML(user) { if (user && user['display-name'] && user['surname']) { return user['display-name'].replace(new RegExp(user['surname']), `${user['surname']}`) + (user['mat-nr'] ? ` (${user['mat-nr']})` : ''); + } else if (user && user.name) { + return user.name; } else { console.error('Unable to format invalid user response'); return ''; diff --git a/frontend/src/utils/exam-correct/exam-correct.sass b/frontend/src/utils/exam-correct/exam-correct.sass index 386ddf327..77ad19399 100644 --- a/frontend/src/utils/exam-correct/exam-correct.sass +++ b/frontend/src/utils/exam-correct/exam-correct.sass @@ -1,9 +1,9 @@ -input.input--invalid - border: 2px solid var(--color-error) +$exam-correct--input-status-margin: 10px + +[uw-exam-correct] input:invalid:not(.no-value) + border: 2px solid var(--color-error) .exam-correct--input-status - $exam-correct--input-status-margin: 10px - input width: calc(100% - 18em/14 - #{$exam-correct--input-status-margin}) i