feat(exam-correct): more on frontend name resolving

This commit is contained in:
Sarah Vaupel 2020-01-20 17:02:40 +01:00
parent 431d004665
commit daf9eee1d3
4 changed files with 170 additions and 65 deletions

View File

@ -19,16 +19,29 @@ const EXAM_CORRECT_HEADERS = {
const EXAM_CORRECT_IDENT = 'uw-exam-correct';
const EXAM_CORRECT_PART_INPUT_ATTR = 'uw-exam-correct--part-input';
const EXAM_CORRECT_SEND_BTN_ID = 'exam-correct__send-btn';
const EXAM_CORRECT_PARTICIPANT_INPUT_ID = 'exam-correct__user';
const EXAM_CORRECT_USER_INPUT_ID = 'exam-correct__user';
const EXAM_CORRECT_USER_INPUT_STATUS_ID = 'exam-correct__user-status';
const EXAM_CORRECT_USER_INPUT_CANDIDATES_ID = 'exam-correct__user-candidates';
const EXAM_CORRECT_INPUT_BODY_ID = 'exam-correct__new';
const EXAM_CORRECT_PARTICIPANT_ATTR = 'exam-correct--user';
const EXAM_CORRECT_USER_ATTR = 'exam-correct--user-id';
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,
};
@Utility({
selector: `table[${EXAM_CORRECT_IDENT}]`,
@ -41,6 +54,7 @@ export class ExamCorrect {
_sendBtn;
_userInput;
_userInputStatus;
_userInputCandidates;
_partInputs;
@ -56,7 +70,8 @@ export class ExamCorrect {
this._element = element;
this._app = app;
this._sendBtn = document.getElementById(EXAM_CORRECT_SEND_BTN_ID);
this._userInput = document.getElementById(EXAM_CORRECT_PARTICIPANT_INPUT_ID);
this._userInput = document.getElementById(EXAM_CORRECT_USER_INPUT_ID);
this._userInputStatus = document.getElementById(EXAM_CORRECT_USER_INPUT_STATUS_ID);
this._userInputCandidates = document.getElementById(EXAM_CORRECT_USER_INPUT_CANDIDATES_ID);
this._partInputs = [...this._element.querySelectorAll(`input[${EXAM_CORRECT_PART_INPUT_ATTR}]`)];
@ -68,6 +83,10 @@ export class ExamCorrect {
this._userInput.addEventListener('focusout', this._validateUserInput.bind(this));
else throw new Error('ExamCorrect utility could not detect user input!');
if (!this._userInputStatus) {
throw new Error('ExamCorrect utility could not detect user input status element!');
}
if (!this._userInputCandidates) {
throw new Error('ExamCorrect utility could not detect user input candidate list!');
}
@ -77,17 +96,34 @@ export class ExamCorrect {
this._sendBtn.removeEventListener('click', this._sendCorrectionHandler);
this._userInput.removeEventListener('change', this._validateUserInput);
this._userInput.removeEventListener('input', this._removeInputEmptyClassHandler);
// TODO destroy handlers on user input candidate elements
}
_validateUserInput() {
const user = this._userInput.value;
if (!user) return;
// do nothing in case of empty or too short input
if (!user || user.length < USER_VALIDATION_MIN_LENGTH) {
removeAllChildren(this._userInputCandidates);
setStatus(this._userInputStatus, STATUS.NONE);
return;
}
if (this._userInput.getAttribute(EXAM_CORRECT_USER_ATTR) !== null) {
if (this._userInput.value === this._userInput.getAttribute(EXAM_CORRECT_USER_DNAME_ATTR)) {
setStatus(this._userInputStatus, STATUS.NONE);
return;
} else {
this._userInput.removeAttribute(EXAM_CORRECT_USER_ATTR);
}
}
setStatus(this._userInputStatus, STATUS.LOADING);
const body = {
name: user,
name: user,
results: {},
op: false,
op: false,
};
this._app.httpClient.post({
@ -103,9 +139,7 @@ export class ExamCorrect {
});
}
_sendCorrectionHandler(event) {
console.log('WIP _sendCorrectionHandler', event);
_sendCorrectionHandler() {
// refocus user input element for convenience
this._userInput.focus();
@ -119,15 +153,19 @@ export class ExamCorrect {
}
const results = {};
this._partInputs.forEach((input) => {
if (input.value && !isNaN(input.value)) {
// TODO instead of ignoring NaN values, abort send and add error class to corresponding input
// (latter is optional)
for (const input of this._partInputs) {
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) throw new Error('Exam part input without part attribute!');
results[partKey] = parseFloat(input.value);
}
});
}
// abort send if there are no results (after validation)
if (Object.keys(results).length <= 0) return;
@ -141,7 +179,7 @@ export class ExamCorrect {
dateTD.setAttribute('date', moment());
const userTD = document.createElement('TD');
userTD.appendChild(document.createTextNode(user));
userTD.setAttribute(EXAM_CORRECT_PARTICIPANT_ATTR, 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);
@ -163,6 +201,7 @@ export class ExamCorrect {
// clear input values on validation success
// TODO only clear input on post success
[this._userInput, ...this._partInputs].forEach(clearInput);
setStatus(this._userInputStatus, STATUS.NONE);
const body = {
name: user,
@ -189,29 +228,49 @@ export class ExamCorrect {
if (response) {
if (response.status === 'no-op') {
if (response.users) {
// TODO directly replace input value and add attr if list contains only one element
// delete candidate list entries from previous requests
removeAllChildren(this._userInputCandidates);
// TODO add event handler on links -> display name as input value and set id as attribute
// show error if there are no matches for this input
if (response.users.length === 0) {
setStatus(this._userInputStatus, STATUS.FAILURE);
return;
}
// directly accept response if there is exactly one candidate (input not ambiguous)
if (response.users.length === 1) {
const candidate = response.users[0];
this._userInput.value = candidate['display-name'];
this._userInput.setAttribute(EXAM_CORRECT_USER_ATTR, candidate.id);
this._userInput.setAttribute(EXAM_CORRECT_USER_DNAME_ATTR, candidate['display-name']);
setStatus(this._userInputStatus, STATUS.SUCCESS);
return;
}
setStatus(this._userInputStatus, STATUS.AMBIGUOUS);
// TODO how to destroy candidate handlers?
response.users.forEach((userCandidate) => {
const candidateItem = document.createElement('li');
userAsInnerHTML(candidateItem, userCandidate);
candidateItem.setAttribute(EXAM_CORRECT_PARTICIPANT_ATTR, userCandidate.id);
candidateItem.innerHTML = userToHTML(userCandidate);
candidateItem.setAttribute(EXAM_CORRECT_USER_ATTR, userCandidate.id);
const acceptCandidateHandler = () => {
console.log('candidate accepted');
this._userInput.value = userCandidate['display-name'] || userCandidate.surname || userCandidate['mat-nr'];
this._userInput.setAttribute(EXAM_CORRECT_PARTICIPANT_ATTR, userCandidate.id);
this._userInput.setAttribute(EXAM_CORRECT_USER_ATTR, userCandidate.id);
// remove all candidates
while (this._userInputCandidates.firstChild) {
this._userInputCandidates.removeChild(this._userInputCandidates.firstChild);
}
// remove all candidates on accept
removeAllChildren(this._userInputCandidates);
setStatus(this._userInputStatus, STATUS.SUCCESS);
// TODO focus first part result input element for convenience
};
candidateItem.addEventListener('click', acceptCandidateHandler, { once: true });
candidateItem.addEventListener('click', acceptCandidateHandler);
this._userInputCandidates.appendChild(candidateItem);
});
} else {
setStatus(this._userInputStatus, STATUS.FAILURE);
}
return;
@ -219,18 +278,16 @@ export class ExamCorrect {
for (let row of [...this._element.rows]) {
const userElem = row.cells.item(1);
const userIdent = userElem && userElem.getAttribute(EXAM_CORRECT_PARTICIPANT_ATTR);
const userIdent = userElem && userElem.getAttribute(EXAM_CORRECT_USER_ATTR);
if (userIdent === user) {
let faIcon, ecClass;
let status = STATUS.ERROR;
switch (response.status) {
// TODO fetch update time from response and replace
case 'success':
faIcon = 'fa-check';
ecClass = 'exam-correct--success';
status = STATUS.SUCCESS;
if (response.user) {
userElem.setAttribute(EXAM_CORRECT_PARTICIPANT_ATTR, response.user.id);
userElem.innerHTML = '';
userAsInnerHTML(userElem, response.user);
userElem.setAttribute(EXAM_CORRECT_USER_ATTR, response.user.id);
userElem.innerHTML = userToHTML(response.user);
}
// TODO replace results with results from response
// TODO set edit button visibility
@ -238,27 +295,21 @@ export class ExamCorrect {
case 'ambiguous':
// TODO show tooltip with error message
// TODO set edit button visibility
faIcon = 'fa-times';
ecClass = 'exam-correct--error';
// TODO show users
status = STATUS.AMBIGUOUS;
if (response.users) {
showUsers(userElem, response.users);
this._showUserList(userElem, response.users);
}
break;
case 'failure':
faIcon = 'fa-times';
ecClass = 'exam-correct--error';
status = STATUS.FAILURE;
break;
default:
// TODO show tooltip with 'invalid response'
// TODO set edit button visibility
console.error('Invalid response');
faIcon = 'fa-times';
ecClass = 'exam-correct--error';
}
row.querySelectorAll('.exam-correct--loading').forEach((elem) => {
elem.classList.remove('exam-correct--loading');
elem.classList.add('fa', faIcon, ecClass);
setStatus(elem, status);
});
break;
}
@ -275,30 +326,76 @@ export class ExamCorrect {
}
}
// TODO better name
_showUserList(elem, users) {
if (users) {
removeAllChildren(elem);
const list = document.createElement('UL');
for (const user of users) {
const listItem = document.createElement('LI');
listItem.innerHTML = userToHTML(user);
listItem.setAttribute(EXAM_CORRECT_USER_ATTR, user.id);
listItem.setAttribute(EXAM_CORRECT_USER_DNAME_ATTR, user['display-name']);
// TODO destroy event handler
//const acceptCandidateHandler = () => {
// elem.innerHTML = userToHTML(user);
//};
//listItem.addEventListener('click', acceptCandidateHandler);
list.appendChild(listItem);
}
elem.appendChild(list);
} else {
console.error('Unable to show users from invalid response');
}
}
}
// TODO move to general util section?
function clearInput(inputElement) {
inputElement.value = null;
}
function removeAllChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
function userAsInnerHTML(elem, user) {
function userToHTML(user) {
if (user && user['display-name'] && user['surname']) {
elem.innerHTML += user['display-name'].replace(new RegExp(user['surname']), `<strong>${user['surname']}</strong>`) + (user['mat-nr'] ? ` (${user['mat-nr']})` : '');
return user['display-name'].replace(new RegExp(user['surname']), `<strong>${user['surname']}</strong>`) + (user['mat-nr'] ? ` (${user['mat-nr']})` : '');
} else {
console.error('Unable to format invalid user response');
return '';
}
}
// TODO better name
function showUsers(elem, users) {
elem.innerHTML = '';
if (users) {
for (const user of users) {
userAsInnerHTML(elem, user);
elem.innerHTML += '<br/>';
}
} else {
console.error('Unable to show users from invalid response');
function setStatus(elem, status) {
const successClasses = ['fas', 'fa-fw', 'fa-check', 'exam-correct--success'];
const ambiguousClasses = ['fas', 'fa-fw', 'fa-question', 'exam-correct--ambiguous'];
const errorClasses = ['fas', 'fa-fw', 'fa-times', 'exam-correct--error'];
const loadingClasses = ['fas', 'fa-fw', 'fa-spinner-third', 'exam-correct--loading'];
elem.classList.remove(...successClasses, ...ambiguousClasses, ...errorClasses, ...loadingClasses);
switch (status) {
case STATUS.NONE:
break;
case STATUS.SUCCESS:
elem.classList.add(...successClasses);
break;
case STATUS.AMBIGUOUS:
elem.classList.add(...ambiguousClasses);
break;
case STATUS.FAILURE:
elem.classList.add(...errorClasses);
break;
case STATUS.LOADING:
elem.classList.add(...loadingClasses);
break;
default:
console.error('Invalid user input status!');
}
}

View File

@ -1,18 +1,25 @@
#exam-correct__participant.input--invalid
input.input--invalid
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
margin-left: $exam-correct--input-status-margin
.exam-correct--success
color: var(--color-success)
.exam-correct--ambiguous
color: var(--color-warning)
.exam-correct--error
color: var(--color-error)
// TODO move to general app style
.exam-correct--loading
border: 3px solid var(--color-grey-light)
border-top: 3px solid var(--color-primary)
border-radius: 50%
width: 20px
height: 20px
animation: spin 2s linear infinite
@keyframes spin

View File

@ -1405,8 +1405,8 @@ ExamCorrectHeadStatus: Status
ExamCorrectButtonSend: Senden
ExamCorrectErrorMultipleMatchingParticipants: Dem Identifikator konnten mehrere Pruefungsteilnehmer zugeordnet werden.
ExamCorrectErrorNoMatchingParticipants: Dem Identifikator konnte kein Pruefungsteilnehmer zugeordnet werden.
ExamCorrectErrorMultipleMatchingParticipants: Dem Identifikator konnten mehrere Prüfungsteilnehmer zugeordnet werden.
ExamCorrectErrorNoMatchingParticipants: Dem Identifikator konnte kein Prüfungsteilnehmer zugeordnet werden.
SubmissionUserInvitationAccepted shn@SheetName: Sie wurden als Mitabgebende(r) für eine Abgabe zu #{shn} eingetragen
SubmissionUserInvitationDeclined shn@SheetName: Sie haben die Einladung, Mitabgebende(r) für #{shn} zu werden, abgelehnt
@ -2289,4 +2289,4 @@ ExternalExamUserMustBeStaff: Sie selbst müssen stets assoziierte Person sein, f
ExternalExamCourseExists: Der angegebene Kurs existiert im System. Prüfungen sollten daher direkt beim Kurs (statt extern) hinterlegt werden.
ExternalExamExists coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ existiert bereits.
ExternalExamCreated coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich angelegt.
ExternalExamEdited coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich bearbeitet.
ExternalExamEdited coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich bearbeitet.

View File

@ -25,8 +25,9 @@ $newline never
<tbody #exam-correct__new>
<tr .table__row>
<td .table__td>
<td .table__td>
<td .table__td .exam-correct--input-status>
<input #exam-correct__user type="text">
<i #exam-correct__user-status .fas .fa-fw>
<ul #exam-correct__user-candidates>
$forall ExamPart{examPartNumber} <- examParts
<td .table__td>