feat(exam-correct): persist results and more

This commit is contained in:
Sarah Vaupel 2020-01-23 20:45:25 +01:00
parent a7af7ad64b
commit 53ff6298e2
3 changed files with 172 additions and 67 deletions

View File

@ -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;

View File

@ -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']), `<strong>${user['surname']}</strong>`) + (user['mat-nr'] ? ` (${user['mat-nr']})` : '');
} else if (user && user.name) {
return user.name;
} else {
console.error('Unable to format invalid user response');
return '';

View File

@ -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