feat(exam-correct): persist results and more
This commit is contained in:
parent
a7af7ad64b
commit
53ff6298e2
@ -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;
|
||||
|
||||
@ -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 '';
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user