diff --git a/frontend/src/utils/exam-correct/exam-correct.js b/frontend/src/utils/exam-correct/exam-correct.js
index 70a9bdb63..0e57d86e3 100644
--- a/frontend/src/utils/exam-correct/exam-correct.js
+++ b/frontend/src/utils/exam-correct/exam-correct.js
@@ -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']), `${user['surname']}`) + (user['mat-nr'] ? ` (${user['mat-nr']})` : '');
+ return user['display-name'].replace(new RegExp(user['surname']), `${user['surname']}`) + (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 += '
';
- }
- } 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!');
}
}
+
diff --git a/frontend/src/utils/exam-correct/exam-correct.sass b/frontend/src/utils/exam-correct/exam-correct.sass
index 63fd21145..821feecd2 100644
--- a/frontend/src/utils/exam-correct/exam-correct.sass
+++ b/frontend/src/utils/exam-correct/exam-correct.sass
@@ -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
diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg
index 88f55b4ec..e47822fed 100644
--- a/messages/uniworx/de-de-formal.msg
+++ b/messages/uniworx/de-de-formal.msg
@@ -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.
\ No newline at end of file
+ExternalExamEdited coursen@CourseName examn@ExamName: Prüfung „#{examn}“ für Kurs „#{coursen}“ erfolgreich bearbeitet.
diff --git a/templates/exam-correct.hamlet b/templates/exam-correct.hamlet
index b825dda09..9abfdc8d7 100644
--- a/templates/exam-correct.hamlet
+++ b/templates/exam-correct.hamlet
@@ -25,8 +25,9 @@ $newline never