feat(exam-correct): more on frontend name resolving
This commit is contained in:
parent
431d004665
commit
daf9eee1d3
@ -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!');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user