Skip to content

Commit

Permalink
abstract the password and end confirmation logic
Browse files Browse the repository at this point in the history
The password entry on the front page and the input box to confirm
ending the exam both need to show feedback about whether the entered
string is accepted. This commit abstracts the logic into
`Numbas.display.passwordHandler`.
  • Loading branch information
christianp committed Nov 9, 2023
1 parent c7095ea commit 2e558fe
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 58 deletions.
8 changes: 5 additions & 3 deletions locales/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"die.error": "Error",
"extension.not found": "Couldn't load the extension <code>{{name}}</code>.",
"modal.confirm": "Confirm",
"modal.confirm end exam": "Write '{{endConfirmation}}' in the box below to confirm.",
"modal.confirm end exam": "Write <code>{{endConfirmation}}</code> in the box to confirm:",
"modal.alert": "Alert",
"modal.ok": "OK",
"modal.cancel": "Cancel",
Expand Down Expand Up @@ -102,7 +102,9 @@
"control.style options": "Display options",
"control.move to next question": "Move to the next question",
"control.show introduction": "Introduction",
"control.end confirmation": "end",
"control.confirm end.correct": "You may now end the exam.",
"control.confirm end.incorrect": "This is not the expected text.",
"control.confirm end.password": "end",
"display.answer widget.unknown widget type": "The answer widget type <code>{{name}}</code> is not recognised.",
"display.part.jme.error making maths": "Error making maths display",
"display.error making html": "Error making HTML in {{contextDescription}}: {{-message}}",
Expand Down Expand Up @@ -477,4 +479,4 @@
"worksheet.reconfigure": "Generate different sheets",
"worksheet.show sheet": "Preview the sheet with ID:",
"worksheet.answersheet show question content": "Show question content in answer sheets?"
}
}
45 changes: 34 additions & 11 deletions themes/default/files/scripts/display-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ var display = Numbas.display = /** @lends Numbas.display */ {
staged_style: {
textSize: Knockout.observable('')
},
modal: this.modal,
}
vm.css = Knockout.computed(function() {
var exam = vm.exam();
Expand Down Expand Up @@ -231,18 +232,17 @@ var display = Numbas.display = /** @lends Numbas.display */ {
* @param {Function} fnEnd - callback to end the exam
* @param {Function} fnCancel - callback if cancelled
*/
showConfirmEndExam: function(msg,fnEnd,fnCancel) {
var fOK = fnEnd || function () {};
this.modal.ok = function () {
if (Numbas.util.caselessCompare(Numbas.exam.display.confirmEnd(), R('control.end confirmation'))) {
showConfirmEndExam: function(msg,fnEnd,fnCancel) {
var fOK = fnEnd || function () {};
this.modal.ok = function () {
$('#confirm-end-exam-modal').modal('hide');
fOK();
}
};
this.modal.cancel = fnCancel || function(){};
let confirmationInputMsg = R('modal.confirm end exam', {endConfirmation : R('control.end confirmation')});
$('#confirm-end-exam-modal-message').html(msg);
$('#confirm-end-exam-modal-input-message').html(confirmationInputMsg);
$('#confirm-end-exam-modal').modal('show');
};
this.modal.cancel = fnCancel || function() {};
let confirmationInputMsg = R('modal.confirm end exam', {endConfirmation : R('control.confirm end.password')});
$('#confirm-end-exam-modal-message').html(msg);
$('#confirm-end-exam-modal-input-message').html(confirmationInputMsg);
$('#confirm-end-exam-modal').modal('show');
},

/** Register event listeners to show the lightbox when images in this element are clicked.
Expand Down Expand Up @@ -670,4 +670,27 @@ var showScoreFeedback = display.showScoreFeedback = function(obj,settings)
})
}
};

var passwordHandler = display.passwordHandler = function(settings) {
var value = Knockout.observable('');

var valid = Knockout.computed(function() {
return settings.accept(value());
});

return {
value: value,
valid: valid,
feedback: Knockout.computed(function() {
if(valid()) {
return {iconClass: 'icon-ok', title: settings.correct_message, buttonClass: 'btn-success'};
} else if(value()=='') {
return {iconClass: '', title: '', buttonClass: 'btn-primary'}
} else {
return {iconClass: 'icon-remove', title: settings.incorrect_message, buttonClass: 'btn-danger'};
}
})
};
}

});
41 changes: 15 additions & 26 deletions themes/default/files/scripts/exam-display.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,13 +362,6 @@ Numbas.queueScript('exam-display',['display-base','math','util','timing'],functi
*/
this.needsPassword = e.settings.startPassword != '';

/** Password entered by the student.
*
* @member {observable|string} enteredPassword
* @memberof Numbas.display.ExamDisplay
*/
this.enteredPassword = Knockout.observable('');

/** Does this exam allow the student to download their attempt data?
*
* @member {boolean} allowAttemptDownload
Expand Down Expand Up @@ -404,30 +397,22 @@ Numbas.queueScript('exam-display',['display-base','math','util','timing'],functi
*/
this.student_name = Knockout.observable(this.exam.student_name || '');

/**
* Handler for the password on the front page.
*/
this.passwordHandler = display.passwordHandler({
accept: password => this.exam.acceptPassword(password),
correct_message: R('exam.password.correct'),
incorrect_message: R('exam.password.incorrect')
});

/** Can the exam begin? True if no password is required, or if the student has entered the right password, and no name is required or the student has entered a name.
*
* @see Numbas.Exam#acceptPassword
* @member {observable|boolean} canBegin
* @memberof Numbas.display.ExamDisplay
*/
this.canBegin = Knockout.computed(function() {
return this.exam.acceptPassword(this.enteredPassword()) && !(this.needsStudentName && this.student_name().trim() == '');
},this);

/** Feedback on the password the student has entered.
* Has properties `iconClass`, `title` and `buttonClass`.
*
* @member {observable|object} passwordFeedback
* @memberof Numbas.display.ExamDisplay
*/
this.passwordFeedback = Knockout.computed(function() {
if(this.canBegin()) {
return {iconClass: 'icon-ok', title: R('exam.password.correct'), buttonClass: 'btn-success'};
} else if(this.enteredPassword()=='') {
return {iconClass: '', title: '', buttonClass: 'btn-primary'}
} else {
return {iconClass: 'icon-remove', title: R('exam.password.incorrect'), buttonClass: 'btn-danger'};
}
return this.passwordHandler.valid() && !(this.needsStudentName && this.student_name().trim() == '');
},this);

/** The student's progress through a diagnostic test.
Expand All @@ -443,7 +428,11 @@ Numbas.queueScript('exam-display',['display-base','math','util','timing'],functi
* @member {observable|string} confirmEnd
* @memberof Numbas.display.ExamDisplay
*/
this.confirmEnd = Knockout.observable('')
this.confirmEndHandler = display.passwordHandler({
accept: value => util.caselessCompare(value, R('control.confirm end.password')),
correct_message: R('control.confirm end.correct'),
incorrect_message: R('control.confirm end.incorrect')
});

document.title = e.settings.name;
}
Expand Down
8 changes: 4 additions & 4 deletions themes/default/templates/frontpage.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ <h1 id="infopage-frontpage-header" data-bind="html: exam.settings.name"></h1>
<label for="begin-exam-student-name" data-localise="exam.enter your name"></label>
<input id="begin-exam-student-name" name="begin-exam-student-name" type="text" class="form-control" data-bind="textInput: student_name, autosize: student_name"/>
</div>
<div class="password" data-bind="visible: needsPassword, css: {'has-error': enteredPassword().length && !canBegin(), 'has-success': canBegin()}">
<div class="password" data-bind="visible: needsPassword, css: {'has-error': passwordHandler.value().length && !passwordHandler.valid(), 'has-success': passwordHandler.valid()}">
<label for="begin-exam-password" data-localise="exam.enter password"></label>
<input id="begin-exam-password" autocomplete="off" name="begin-exam-password" type="text" class="form-control" data-bind="textInput: enteredPassword, autosize: enteredPassword"/>
<span class="password-feedback feedback-icon" data-bind="css: passwordFeedback().iconClass, attr: {title: passwordFeedback().title}"></span>
<input id="begin-exam-password" autocomplete="off" name="begin-exam-password" type="text" class="form-control" data-bind="textInput: passwordHandler.value, autosize: passwordHandler.value"/>
<span class="password-feedback feedback-icon" data-bind="css: passwordHandler.feedback().iconClass, attr: {title: passwordHandler.feedback().title}"></span>
</div>
<button class="btn" id="startBtn" type="submit" data-bind="disable: !canBegin(), css: passwordFeedback().buttonClass" data-localise="frontpage.start"></button>
<button class="btn" id="startBtn" type="submit" data-bind="disable: !canBegin(), css: passwordHandler.feedback().buttonClass" data-localise="frontpage.start"></button>
</form>
23 changes: 9 additions & 14 deletions themes/default/templates/modals.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,17 @@ <h4 id="confirm-modal-title" data-localise="modal.confirm"></h4>
<div class="modal-header">
<h4 id="confirm-end-exam-modal-title" data-localise="modal.confirm"></h4>
</div>
<div class="modal-body" id="confirm-end-exam-modal-body">
<div class="modal-body" id="confirm-end-exam-modal-message"></div>
<form data-bind="submit: function(){}">
<div class="row">
<div class="form-group col-sm-6">
<label for="confirm-end-text-input" id="confirm-end-exam-modal-input-message"></label>
<input class="form-control" id="confirm-end-text-input" type="text"
data-bind="value: confirmEnd">
</div>
</div>
<div class="modal-body" id="confirm-end-exam-modal-body" data-bind="with: $root.exam">
<p id="confirm-end-exam-modal-message"></p>
<form class="form-inline" data-bind="submit: $root.modal.ok, test: $data">
<label for="confirm-end-text-input" id="confirm-end-exam-modal-input-message"></label>
<input class="form-control" id="confirm-end-text-input" type="text" data-bind="textInput: confirmEndHandler.value, autosize: confirmEndHandler.value">
<span class="password-feedback feedback-icon" data-bind="css: confirmEndHandler.feedback().iconClass, attr: {title: confirmEndHandler.feedback().title}"></span>
</form>
</div>
<div class="modal-footer">
<button type="button" class="cancel btn btn-default" data-dismiss="modal"
data-localise="modal.cancel"></button>
<button type="button" class="ok btn btn-primary" data-dismiss="modal" data-localise="modal.ok"></button>
<button type="button" class="cancel btn btn-default" data-dismiss="modal" data-localise="modal.cancel"></button>
<button type="button" class="ok btn btn-primary" data-dismiss="modal" data-bind="disable: !confirmEndHandler.valid(), css: confirmEndHandler.feedback().buttonClass" data-localise="modal.ok"></button>
</div>
</div>
</div>
Expand Down Expand Up @@ -118,4 +113,4 @@ <h4 id="next-actions-modal-title" data-localise="diagnostic.make a choice"></h4>
</div>

<div id="lightbox" tabindex="-1" role="dialog" aria-hidden="true">
</div>
</div>

0 comments on commit 2e558fe

Please sign in to comment.