Commit 806c35b3 by cahrens

Warn if unsaved changes exist in assessment.

TNL-3870
parent 1aee957a
...@@ -8,17 +8,7 @@ ...@@ -8,17 +8,7 @@
</span> </span>
</h3> </h3>
{% block title %} {% include "openassessmentblock/staff_area/oa_staff_grade_learners_count.html" with staff_assessment_ungraded=staff_assessment_ungraded staff_assessment_in_progress=staff_assessment_in_progress %}
<span class="staff__grade__status">
<span class="staff__grade__value">
<span class="copy">
{% blocktrans with ungraded=staff_assessment_ungraded|stringformat:"s" in_progress=staff_assessment_in_progress|stringformat:"s" %}
{{ ungraded }} Available and {{ in_progress }} Checked Out
{% endblocktrans %}
</span>
</span>
</span>
{% endblock %}
</header> </header>
<div class="ui-staff__content__section staff__grade__content ui-toggle-visibility__content"> <div class="ui-staff__content__section staff__grade__content ui-toggle-visibility__content">
......
{% load i18n %} {% load i18n %}
{% spaceless %} {% spaceless %}
{% block body %}
<div class="staff__grade__form ui-toggle-visibility__content" data-submission-uuid="{{ submission.uuid }}"> <div class="staff__grade__form ui-toggle-visibility__content" data-submission-uuid="{{ submission.uuid }}">
<div class="wrapper--staff-assessment"> <div class="wrapper--staff-assessment">
<div> <div>
...@@ -53,6 +52,5 @@ ...@@ -53,6 +52,5 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% endspaceless %} {% endspaceless %}
{% load i18n %}
<span class="staff__grade__status">
<span class="staff__grade__value">
<span class="copy">
{% blocktrans with ungraded=staff_assessment_ungraded|stringformat:"s" in_progress=staff_assessment_in_progress|stringformat:"s" %}
{{ ungraded }} Available and {{ in_progress }} Checked Out
{% endblocktrans %}
</span>
</span>
</span>
{% load i18n %} {% load i18n %}
{% spaceless %} {% spaceless %}
{% block body %}
<div class="wrapper--staff-assessment"> <div class="wrapper--staff-assessment">
<div class="step__instruction"> <div class="step__instruction">
<p>{% trans "Override this learner's current grade using the problem's rubric." %}</p> <p>{% trans "Override this learner's current grade using the problem's rubric." %}</p>
...@@ -46,5 +45,4 @@ ...@@ -46,5 +45,4 @@
</ul> </ul>
</div> </div>
</div> </div>
{% endblock %}
{% endspaceless %} {% endspaceless %}
...@@ -169,14 +169,24 @@ class StaffAreaMixin(object): ...@@ -169,14 +169,24 @@ class StaffAreaMixin(object):
staff_assessment_required = "staff-assessment" in self.assessment_steps staff_assessment_required = "staff-assessment" in self.assessment_steps
context['staff_assessment_required'] = staff_assessment_required context['staff_assessment_required'] = staff_assessment_required
if staff_assessment_required: if staff_assessment_required:
grading_stats = staff_api.get_staff_grading_statistics( context.update(
student_item["course_id"], student_item["item_id"] self.get_staff_assessment_statistics_context(student_item["course_id"], student_item["item_id"])
) )
context['staff_assessment_ungraded'] = grading_stats['ungraded']
context['staff_assessment_in_progress'] = grading_stats['in-progress']
return path, context return path, context
@staticmethod
def get_staff_assessment_statistics_context(course_id, item_id):
"""
Returns a context with staff assessment "ungraded" and "in-progress" counts.
"""
grading_stats = staff_api.get_staff_grading_statistics(course_id, item_id)
return {
'staff_assessment_ungraded': grading_stats['ungraded'],
'staff_assessment_in_progress': grading_stats['in-progress']
}
@XBlock.json_handler @XBlock.json_handler
@require_global_admin("SCHEDULE_TRAINING") @require_global_admin("SCHEDULE_TRAINING")
def schedule_training(self, data, suffix=''): # pylint: disable=W0613 def schedule_training(self, data, suffix=''): # pylint: disable=W0613
...@@ -272,6 +282,27 @@ class StaffAreaMixin(object): ...@@ -272,6 +282,27 @@ class StaffAreaMixin(object):
except PeerAssessmentInternalError: except PeerAssessmentInternalError:
return self.render_error(self._(u"Error getting staff grade information.")) return self.render_error(self._(u"Error getting staff grade information."))
@XBlock.handler
@require_course_staff("STUDENT_GRADE")
def render_staff_grade_counts(self, data, suffix=''): # pylint: disable=W0613
"""
Renders a form to show the number of ungraded and checked out assessments.
Must be course staff to render this view.
"""
try:
student_item_dict = self.get_student_item_dict()
context = self.get_staff_assessment_statistics_context(
student_item_dict.get('course_id'), student_item_dict.get('item_id')
)
path = 'openassessmentblock/staff_area/oa_staff_grade_learners_count.html'
return self.render_assessment(path, context)
except PeerAssessmentInternalError:
return self.render_error(self._(u"Error getting staff grade ungraded and checked out counts."))
def get_student_submission_context(self, student_username, submission): def get_student_submission_context(self, student_username, submission):
""" """
Get a context dict for rendering a student submission and associated rubric (for staff grading). Get a context dict for rendering a student submission and associated rubric (for staff grading).
......
...@@ -723,8 +723,8 @@ ...@@ -723,8 +723,8 @@
"template": "openassessmentblock/staff_area/oa_staff_area.html", "template": "openassessmentblock/staff_area/oa_staff_area.html",
"context": { "context": {
"staff_assessment_required": true, "staff_assessment_required": true,
"staff_assessment_ungraded": 9, "staff_assessment_ungraded": 5,
"staff_assessment_in_progress": 0, "staff_assessment_in_progress": 2,
"status_counts": { "status_counts": {
"self": 1, "self": 1,
"peer": 2, "peer": 2,
...@@ -754,6 +754,22 @@ ...@@ -754,6 +754,22 @@
"output": "oa_staff_area_full_grading_2.html" "output": "oa_staff_area_full_grading_2.html"
}, },
{ {
"template": "openassessmentblock/staff_area/oa_staff_grade_learners_count.html",
"context": {
"staff_assessment_ungraded": 9,
"staff_assessment_in_progress": 3
},
"output": "oa_staff_grade_learners_count_1.html"
},
{
"template": "openassessmentblock/staff_area/oa_staff_grade_learners_count.html",
"context": {
"staff_assessment_ungraded": 9,
"staff_assessment_in_progress": 2
},
"output": "oa_staff_grade_learners_count_2.html"
},
{
"template": "openassessmentblock/staff_area/oa_student_info.html", "template": "openassessmentblock/staff_area/oa_student_info.html",
"context": { "context": {
"rubric_criteria": [ "rubric_criteria": [
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -75,6 +75,10 @@ describe("OpenAssessment.PeerView", function() { ...@@ -75,6 +75,10 @@ describe("OpenAssessment.PeerView", function() {
server.renderLatex = jasmine.createSpy('renderLatex'); server.renderLatex = jasmine.createSpy('renderLatex');
}); });
afterEach(function() {
OpenAssessment.clearUnsavedChanges();
});
it("sends a peer assessment to the server", function() { it("sends a peer assessment to the server", function() {
var view = createPeerAssessmentView('oa_peer_assessment.html'); var view = createPeerAssessmentView('oa_peer_assessment.html');
submitPeerAssessment(view); submitPeerAssessment(view);
...@@ -115,4 +119,34 @@ describe("OpenAssessment.PeerView", function() { ...@@ -115,4 +119,34 @@ describe("OpenAssessment.PeerView", function() {
var view = createPeerAssessmentView('oa_turbo_mode.html'); var view = createPeerAssessmentView('oa_turbo_mode.html');
submitPeerAssessment(view); submitPeerAssessment(view);
}); });
it("warns of unsubmitted assessments", function() {
var view = createPeerAssessmentView('oa_peer_assessment.html');
expect(view.baseView.unsavedWarningEnabled()).toBe(false);
// Click on radio buttons, to create unsubmitted changes.
$('.question__answers', view.el).each(function() {
$('input[type="radio"]', this).first().click();
});
expect(view.baseView.unsavedWarningEnabled()).toBe(true);
// When submitPeerAssessment is executed, the views will all re-render. However,
// as the test does not mock out the surrounding elements, the re-render
// of the peer assessment module will keep the original HTML intact (with selected
// options), causing the unsavedWarnings callback to be triggered again (after it is properly
// cleared during the submit operation). To avoid this, have the view re-render fail.
server.render = function() {
return $.Deferred(
function(defer) {
defer.fail();
}
).promise();
};
submitPeerAssessment(view);
expect(view.baseView.unsavedWarningEnabled()).toBe(false);
});
}); });
...@@ -142,7 +142,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -142,7 +142,7 @@ describe("OpenAssessment.ResponseView", function() {
view.setAutoSaveEnabled(false); view.setAutoSaveEnabled(false);
// Disable the unsaved page warning (if set) // Disable the unsaved page warning (if set)
view.unsavedWarningEnabled(false); OpenAssessment.clearUnsavedChanges();
}); });
it("updates and retrieves response text correctly", function() { it("updates and retrieves response text correctly", function() {
...@@ -299,36 +299,36 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -299,36 +299,36 @@ describe("OpenAssessment.ResponseView", function() {
it("enables the unsaved work warning when the user changes the response text", function() { it("enables the unsaved work warning when the user changes the response text", function() {
// Initially, the unsaved work warning should be disabled // Initially, the unsaved work warning should be disabled
expect(view.unsavedWarningEnabled()).toBe(false); expect(view.baseView.unsavedWarningEnabled()).toBe(false);
// Change the text, then expect the unsaved warning to be enabled. // Change the text, then expect the unsaved warning to be enabled.
view.response(['Lorem ipsum 1', 'Lorem ipsum 2']); view.response(['Lorem ipsum 1', 'Lorem ipsum 2']);
view.handleResponseChanged(); view.handleResponseChanged();
// Expect the unsaved work warning to be enabled // Expect the unsaved work warning to be enabled
expect(view.unsavedWarningEnabled()).toBe(true); expect(view.baseView.unsavedWarningEnabled()).toBe(true);
}); });
it("disables the unsaved work warning when the user saves a response", function() { it("disables the unsaved work warning when the user saves a response", function() {
// Change the text, then expect the unsaved warning to be enabled. // Change the text, then expect the unsaved warning to be enabled.
view.response(['Lorem ipsum 1', 'Lorem ipsum 2']); view.response(['Lorem ipsum 1', 'Lorem ipsum 2']);
view.handleResponseChanged(); view.handleResponseChanged();
expect(view.unsavedWarningEnabled()).toBe(true); expect(view.baseView.unsavedWarningEnabled()).toBe(true);
// Save the response and expect the unsaved warning to be disabled // Save the response and expect the unsaved warning to be disabled
view.save(); view.save();
expect(view.unsavedWarningEnabled()).toBe(false); expect(view.baseView.unsavedWarningEnabled()).toBe(false);
}); });
it("disables the unsaved work warning when the user submits a response", function() { it("disables the unsaved work warning when the user submits a response", function() {
// Change the text, then expect the unsaved warning to be enabled. // Change the text, then expect the unsaved warning to be enabled.
view.response(['Lorem ipsum 1', 'Lorem ipsum 2']); view.response(['Lorem ipsum 1', 'Lorem ipsum 2']);
view.handleResponseChanged(); view.handleResponseChanged();
expect(view.unsavedWarningEnabled()).toBe(true); expect(view.baseView.unsavedWarningEnabled()).toBe(true);
// Submit the response and expect the unsaved warning to be disabled // Submit the response and expect the unsaved warning to be disabled
view.submit(); view.submit();
expect(view.unsavedWarningEnabled()).toBe(false); expect(view.baseView.unsavedWarningEnabled()).toBe(false);
}); });
describe("auto save", function() { describe("auto save", function() {
...@@ -360,7 +360,7 @@ describe("OpenAssessment.ResponseView", function() { ...@@ -360,7 +360,7 @@ describe("OpenAssessment.ResponseView", function() {
expect(view.saveStatus()).toContain('saved but not submitted'); expect(view.saveStatus()).toContain('saved but not submitted');
// Expect that the unsaved warning is disabled // Expect that the unsaved warning is disabled
expect(view.unsavedWarningEnabled()).toBe(false); expect(view.baseView.unsavedWarningEnabled()).toBe(false);
}); });
it("schedules autosave polling", function() { it("schedules autosave polling", function() {
......
...@@ -43,6 +43,10 @@ describe("OpenAssessment.SelfView", function() { ...@@ -43,6 +43,10 @@ describe("OpenAssessment.SelfView", function() {
view.installHandlers(); view.installHandlers();
}); });
afterEach(function() {
OpenAssessment.clearUnsavedChanges();
});
it("Sends a self assessment to the server", function() { it("Sends a self assessment to the server", function() {
spyOn(server, 'selfAssess').and.callThrough(); spyOn(server, 'selfAssess').and.callThrough();
...@@ -82,4 +86,33 @@ describe("OpenAssessment.SelfView", function() { ...@@ -82,4 +86,33 @@ describe("OpenAssessment.SelfView", function() {
// Expect the submit button to have been re-enabled // Expect the submit button to have been re-enabled
expect(view.selfSubmitEnabled()).toBe(true); expect(view.selfSubmitEnabled()).toBe(true);
}); });
it("warns of unsubmitted assessments", function() {
expect(view.baseView.unsavedWarningEnabled()).toBe(false);
// Click on radio buttons, to create unsubmitted changes.
$('.question__answers', view.el).each(function() {
$('input[type="radio"]', this).first().click();
});
expect(view.baseView.unsavedWarningEnabled()).toBe(true);
// When selfAssess is executed, the views will all re-render. However,
// as the test does not mock out the surrounding elements, the re-render
// of the self assessment module will keep the original HTML intact (with selected
// options), causing the unsavedWarnings callback to be triggered again (after it is properly
// cleared during the submit operation). To avoid this, have the view re-render fail.
server.render = function() {
return $.Deferred(
function(defer) {
defer.fail();
}
).promise();
};
view.selfAssess();
expect(view.baseView.unsavedWarningEnabled()).toBe(false);
});
}); });
...@@ -28,6 +28,16 @@ OpenAssessment.BaseView = function(runtime, element, server, data) { ...@@ -28,6 +28,16 @@ OpenAssessment.BaseView = function(runtime, element, server, data) {
this.staffAreaView = new OpenAssessment.StaffAreaView(this.element, this.server, this); this.staffAreaView = new OpenAssessment.StaffAreaView(this.element, this.server, this);
}; };
if (typeof OpenAssessment.unsavedChanges === 'undefined' || !OpenAssessment.unsavedChanges) {
OpenAssessment.unsavedChanges = {};
}
// This is used by unit tests to reset state.
OpenAssessment.clearUnsavedChanges = function() {
OpenAssessment.unsavedChanges = {};
window.onbeforeunload = null;
};
OpenAssessment.BaseView.prototype = { OpenAssessment.BaseView.prototype = {
/** /**
...@@ -148,6 +158,55 @@ OpenAssessment.BaseView.prototype = { ...@@ -148,6 +158,55 @@ OpenAssessment.BaseView.prototype = {
$container.toggleClass('has--error', true); $container.toggleClass('has--error', true);
$container.find('.step__status__value i').removeClass().addClass('icon fa fa-exclamation-triangle'); $container.find('.step__status__value i').removeClass().addClass('icon fa fa-exclamation-triangle');
$container.find('.step__status__value .copy').html(_.escape(errorMessage)); $container.find('.step__status__value .copy').html(_.escape(errorMessage));
},
/**
* Enable/disable the "navigate away" warning to alert the user of unsaved changes.
*
* @param {boolean} enabled If specified, set whether the warning is enabled.
* @param {string} key A unique key related to the type of unsaved changes. Must be supplied
* if "enabled" is also supplied.
* @param {string} message The message to show if navigating away with unsaved changes. Only needed
* if "enabled" is true.
* @returns {boolean} Whether the warning is enabled (only if "enabled" argument is not supplied).
*/
unsavedWarningEnabled: function(enabled, key, message) {
if (typeof enabled === 'undefined') {
return (window.onbeforeunload !== null);
}
else {
// To support multiple ORA XBlocks on the same page, store state by XBlock usage-id.
var usageID = $(this.element).data("usage-id");
if (enabled) {
if (typeof OpenAssessment.unsavedChanges[usageID] === 'undefined' ||
!OpenAssessment.unsavedChanges[usageID]) {
OpenAssessment.unsavedChanges[usageID] = {};
}
OpenAssessment.unsavedChanges[usageID][key] = message;
window.onbeforeunload = function() {
for (var xblockUsageID in OpenAssessment.unsavedChanges) {
if (OpenAssessment.unsavedChanges.hasOwnProperty(xblockUsageID)) {
for (var key in OpenAssessment.unsavedChanges[xblockUsageID]) {
if (OpenAssessment.unsavedChanges[xblockUsageID].hasOwnProperty(key)) {
return OpenAssessment.unsavedChanges[xblockUsageID][key];
}
}
}
}
};
}
else {
if (typeof OpenAssessment.unsavedChanges[usageID] !== 'undefined') {
delete OpenAssessment.unsavedChanges[usageID][key];
if ($.isEmptyObject(OpenAssessment.unsavedChanges[usageID])) {
delete OpenAssessment.unsavedChanges[usageID];
}
if ($.isEmptyObject(OpenAssessment.unsavedChanges)) {
window.onbeforeunload = null;
}
}
}
}
} }
}; };
......
...@@ -18,6 +18,8 @@ OpenAssessment.PeerView = function(element, server, baseView) { ...@@ -18,6 +18,8 @@ OpenAssessment.PeerView = function(element, server, baseView) {
OpenAssessment.PeerView.prototype = { OpenAssessment.PeerView.prototype = {
UNSAVED_WARNING_KEY: "peer-assessment",
/** /**
Load the peer assessment view. Load the peer assessment view.
**/ **/
...@@ -101,10 +103,16 @@ OpenAssessment.PeerView.prototype = { ...@@ -101,10 +103,16 @@ OpenAssessment.PeerView.prototype = {
var rubricElement = rubricSelector.get(0); var rubricElement = rubricSelector.get(0);
this.rubric = new OpenAssessment.Rubric(rubricElement); this.rubric = new OpenAssessment.Rubric(rubricElement);
} }
else {
// If there was previously a rubric visible, clear the reference to it.
this.rubric = null;
}
// Install a change handler for rubric options to enable/disable the submit button // Install a change handler for rubric options to enable/disable the submit button
if (this.rubric !== null) { if (this.rubric !== null) {
this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled, view)); this.rubric.canSubmitCallback($.proxy(view.peerSubmitEnabled, view));
this.rubric.changesExistCallback($.proxy(view.assessmentRubricChanges, view));
} }
// Install a click handler for assessment // Install a click handler for assessment
...@@ -154,12 +162,29 @@ OpenAssessment.PeerView.prototype = { ...@@ -154,12 +162,29 @@ OpenAssessment.PeerView.prototype = {
}, },
/** /**
* Called when something is selected or typed in the assessment rubric.
* Used to set the unsaved changes warning dialog.
*
* @param {boolean} changesExist true if unsaved changes exist
*/
assessmentRubricChanges: function(changesExist) {
if (changesExist) {
this.baseView.unsavedWarningEnabled(
true,
this.UNSAVED_WARNING_KEY,
gettext("If you leave this page without submitting your peer assessment, you will lose any work you have done.") // jscs:ignore maximumLineLength
);
}
},
/**
Send an assessment to the server and update the view. Send an assessment to the server and update the view.
**/ **/
peerAssess: function() { peerAssess: function() {
var view = this; var view = this;
var baseView = view.baseView; var baseView = view.baseView;
this.peerAssessRequest(function() { this.peerAssessRequest(function() {
baseView.unsavedWarningEnabled(false, view.UNSAVED_WARNING_KEY);
baseView.loadAssessmentModules(); baseView.loadAssessmentModules();
baseView.scrollToTop(); baseView.scrollToTop();
}); });
...@@ -174,6 +199,7 @@ OpenAssessment.PeerView.prototype = { ...@@ -174,6 +199,7 @@ OpenAssessment.PeerView.prototype = {
var gradeView = this.baseView.gradeView; var gradeView = this.baseView.gradeView;
var baseView = view.baseView; var baseView = view.baseView;
view.peerAssessRequest(function() { view.peerAssessRequest(function() {
baseView.unsavedWarningEnabled(false, view.UNSAVED_WARNING_KEY);
view.loadContinuedAssessment(); view.loadContinuedAssessment();
gradeView.load(); gradeView.load();
baseView.scrollToTop(); baseView.scrollToTop();
......
...@@ -38,6 +38,8 @@ OpenAssessment.ResponseView.prototype = { ...@@ -38,6 +38,8 @@ OpenAssessment.ResponseView.prototype = {
// Maximum file size (5 MB) for an attached file. // Maximum file size (5 MB) for an attached file.
MAX_FILE_SIZE: 5242880, MAX_FILE_SIZE: 5242880,
UNSAVED_WARNING_KEY: "learner-response",
/** /**
Load the response (submission) view. Load the response (submission) view.
**/ **/
...@@ -235,37 +237,6 @@ OpenAssessment.ResponseView.prototype = { ...@@ -235,37 +237,6 @@ OpenAssessment.ResponseView.prototype = {
}, },
/** /**
Enable/disable the "navigate away" warning to alert the user of unsaved changes.
Args:
enabled (bool): If specified, set whether the warning is enabled.
Returns:
bool: Whether the warning is enabled.
Examples:
>> view.unsavedWarningEnabled(true); // enable the "unsaved" warning
>> view.unsavedWarningEnabled();
>> true
**/
unsavedWarningEnabled: function(enabled) {
if (typeof enabled === 'undefined') {
return (window.onbeforeunload !== null);
}
else {
if (enabled) {
window.onbeforeunload = function() {
// Keep this on one big line to avoid gettext bug: http://stackoverflow.com/a/24579117
return gettext("If you leave this page without saving or submitting your response, you'll lose any work you've done on the response."); // jscs:ignore maximumLineLength
};
}
else {
window.onbeforeunload = null;
}
}
},
/**
Set the response texts. Set the response texts.
Retrieve the response texts. Retrieve the response texts.
...@@ -339,7 +310,11 @@ OpenAssessment.ResponseView.prototype = { ...@@ -339,7 +310,11 @@ OpenAssessment.ResponseView.prototype = {
this.saveEnabled(isNotBlank); this.saveEnabled(isNotBlank);
this.previewEnabled(isNotBlank); this.previewEnabled(isNotBlank);
this.saveStatus(gettext('This response has not been saved.')); this.saveStatus(gettext('This response has not been saved.'));
this.unsavedWarningEnabled(true); this.baseView.unsavedWarningEnabled(
true,
this.UNSAVED_WARNING_KEY,
gettext("If you leave this page without saving or submitting your response, you will lose any work you have done on the response.") // jscs:ignore maximumLineLength
);
} }
// Record the current time (used for autosave) // Record the current time (used for autosave)
...@@ -360,7 +335,7 @@ OpenAssessment.ResponseView.prototype = { ...@@ -360,7 +335,7 @@ OpenAssessment.ResponseView.prototype = {
this.baseView.toggleActionError('save', null); this.baseView.toggleActionError('save', null);
// Disable the "unsaved changes" warning // Disable the "unsaved changes" warning
this.unsavedWarningEnabled(false); this.baseView.unsavedWarningEnabled(false, this.UNSAVED_WARNING_KEY);
var view = this; var view = this;
var savedResponse = this.response(); var savedResponse = this.response();
...@@ -463,7 +438,7 @@ OpenAssessment.ResponseView.prototype = { ...@@ -463,7 +438,7 @@ OpenAssessment.ResponseView.prototype = {
// Disable the "unsaved changes" warning if the user // Disable the "unsaved changes" warning if the user
// tries to navigate to another page. // tries to navigate to another page.
this.unsavedWarningEnabled(false); this.baseView.unsavedWarningEnabled(false, this.UNSAVED_WARNING_KEY);
}, },
/** /**
......
...@@ -156,6 +156,43 @@ OpenAssessment.Rubric.prototype = { ...@@ -156,6 +156,43 @@ OpenAssessment.Rubric.prototype = {
}, },
/** /**
* Install a callback handler to be notified when unsaved changes exist in a rubric form.
*
* @param {function} callback a function that accepts one argument, a boolean indicating
* whether the user has selected options or inserted text.
*/
changesExistCallback: function(callback) {
var rubric = this;
// Set the initial state
callback(rubric.changesExist());
// Install a handler to update on change
$(this.element).on('change keyup drop paste',
function() { callback(rubric.changesExist()); }
);
},
/**
* Helper method for determining of unsubmitted changes exist in the rubric.
*
* @returns {boolean} true if unsubmitted changes exist.
*/
changesExist: function() {
var numChecked = $('input[type=radio]:checked', this.element).length;
var textExists = false;
$('textarea', this.element).each(function() {
var trimmedText = $.trim($(this).val());
if (trimmedText !== "") {
textExists = true;
}
});
return (numChecked > 0 || textExists);
},
/**
Updates the rubric to display positive and negative messages on each Updates the rubric to display positive and negative messages on each
criterion. For each correction provided, the associated criterion will have criterion. For each correction provided, the associated criterion will have
an appropriate message displayed. an appropriate message displayed.
......
...@@ -18,6 +18,8 @@ OpenAssessment.SelfView = function(element, server, baseView) { ...@@ -18,6 +18,8 @@ OpenAssessment.SelfView = function(element, server, baseView) {
OpenAssessment.SelfView.prototype = { OpenAssessment.SelfView.prototype = {
UNSAVED_WARNING_KEY: "self-assessment",
/** /**
Load the self assessment view. Load the self assessment view.
**/ **/
...@@ -51,10 +53,16 @@ OpenAssessment.SelfView.prototype = { ...@@ -51,10 +53,16 @@ OpenAssessment.SelfView.prototype = {
var rubricElement = rubricSelector.get(0); var rubricElement = rubricSelector.get(0);
this.rubric = new OpenAssessment.Rubric(rubricElement); this.rubric = new OpenAssessment.Rubric(rubricElement);
} }
else {
// If there was previously a rubric visible, clear the reference to it.
this.rubric = null;
}
// Install a change handler for rubric options to enable/disable the submit button // Install a change handler for rubric options to enable/disable the submit button
if (this.rubric !== null) { if (this.rubric !== null) {
this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled, this)); this.rubric.canSubmitCallback($.proxy(this.selfSubmitEnabled, this));
this.rubric.changesExistCallback($.proxy(this.assessmentRubricChanges, this));
} }
// Install a click handler for the submit button // Install a click handler for the submit button
...@@ -95,6 +103,22 @@ OpenAssessment.SelfView.prototype = { ...@@ -95,6 +103,22 @@ OpenAssessment.SelfView.prototype = {
}, },
/** /**
* Called when something is selected or typed in the assessment rubric.
* Used to set the unsaved changes warning dialog.
*
* @param {boolean} changesExist true if unsaved changes exist
*/
assessmentRubricChanges: function(changesExist) {
if (changesExist) {
this.baseView.unsavedWarningEnabled(
true,
this.UNSAVED_WARNING_KEY,
gettext("If you leave this page without submitting your self assessment, you will lose any work you have done.") // jscs:ignore maximumLineLength
);
}
},
/**
Send a self-assessment to the server and update the view. Send a self-assessment to the server and update the view.
**/ **/
selfAssess: function() { selfAssess: function() {
...@@ -110,6 +134,7 @@ OpenAssessment.SelfView.prototype = { ...@@ -110,6 +134,7 @@ OpenAssessment.SelfView.prototype = {
this.rubric.overallFeedback() this.rubric.overallFeedback()
).done( ).done(
function() { function() {
baseView.unsavedWarningEnabled(false, view.UNSAVED_WARNING_KEY);
baseView.loadAssessmentModules(); baseView.loadAssessmentModules();
baseView.scrollToTop(); baseView.scrollToTop();
} }
......
...@@ -20,14 +20,13 @@ ...@@ -20,14 +20,13 @@
OpenAssessment.StaffAreaView.prototype = { OpenAssessment.StaffAreaView.prototype = {
FULL_GRADE_UNSAVED_WARNING_KEY: "staff-grade",
OVERRIDE_UNSAVED_WARNING_KEY: "staff-override",
/** /**
* Load the staff area. * Load the staff area.
*
* @param {function} onSuccessCallback an optional callback to be executed when the
* server successfully returns the staff area HTML. This callback will be the last thing
* executed, after rendering and installing click handlers.
*/ */
load: function(onSuccessCallback) { load: function() {
var view = this; var view = this;
// If we're course staff, the base template should contain a section // If we're course staff, the base template should contain a section
...@@ -39,9 +38,6 @@ ...@@ -39,9 +38,6 @@
$('.openassessment__staff-area', view.element).replaceWith(html); $('.openassessment__staff-area', view.element).replaceWith(html);
view.server.renderLatex($('.openassessment__staff-area', view.element)); view.server.renderLatex($('.openassessment__staff-area', view.element));
view.installHandlers(); view.installHandlers();
if (onSuccessCallback) {
onSuccessCallback();
}
}).fail(function() { }).fail(function() {
view.baseView.showLoadError('staff_area'); view.baseView.showLoadError('staff_area');
}); });
...@@ -97,6 +93,10 @@ ...@@ -97,6 +93,10 @@
// Install a change handler for rubric options to enable/disable the submit button // Install a change handler for rubric options to enable/disable the submit button
rubric.canSubmitCallback($.proxy(view.staffSubmitEnabled, view, $manageLearnersTab)); rubric.canSubmitCallback($.proxy(view.staffSubmitEnabled, view, $manageLearnersTab));
rubric.changesExistCallback(
$.proxy(view.assessmentRubricChanges, view, view.OVERRIDE_UNSAVED_WARNING_KEY)
);
// Install a click handler for the submit button // Install a click handler for the submit button
$manageLearnersTab.find('.wrapper--staff-assessment .action--submit', view.element).click( $manageLearnersTab.find('.wrapper--staff-assessment .action--submit', view.element).click(
function(eventObject) { function(eventObject) {
...@@ -125,19 +125,27 @@ ...@@ -125,19 +125,27 @@
* Upon request, loads the staff grade/assessment section of the staff area. * Upon request, loads the staff grade/assessment section of the staff area.
* This allows staff grading when staff assessment is a required step. * This allows staff grading when staff assessment is a required step.
* *
* @param {boolean} clearAndCollapse if true, clear the staff grade form and collapse it. Otherwise
* render the staff grade form if is not already loaded.
* @returns {promise} A promise representing the successful loading * @returns {promise} A promise representing the successful loading
* of the staff grade (assessment) section. * of the staff grade (assessment) section.
*/ */
loadStaffGradeForm: function() { loadStaffGradeForm: function(clearAndCollapse) {
var view = this; var view = this;
var $staffGradeTab = $('.openassessment__staff-grading', this.element); var $staffGradeTab = $('.openassessment__staff-grading', this.element);
var isCollapsed = $staffGradeTab.find('.staff__grade__control').hasClass("is--collapsed");
var deferred = $.Deferred(); var deferred = $.Deferred();
var showFormError = function(errorMessage) { var showFormError = function(errorMessage) {
$staffGradeTab.find('.staff__grade__form--error').text(errorMessage); $staffGradeTab.find('.staff__grade__form--error').text(errorMessage);
}; };
if (isCollapsed && !this.staffGradeFormLoaded) { if (clearAndCollapse) {
// Collapse the editor and update the counts.
$staffGradeTab.find('.staff__grade__control').toggleClass('is--collapsed', true);
$staffGradeTab.find('.staff__grade__form').replaceWith('<div class="staff__grade__form"></div>');
view.updateStaffGradeCounts();
deferred.resolve();
}
else if (!this.staffGradeFormLoaded) {
this.staffGradeFormLoaded = true; this.staffGradeFormLoaded = true;
this.server.staffGradeForm().done(function(html) { this.server.staffGradeForm().done(function(html) {
showFormError(''); showFormError('');
...@@ -145,6 +153,9 @@ ...@@ -145,6 +153,9 @@
// Load the HTML and install event handlers // Load the HTML and install event handlers
$staffGradeTab.find('.staff__grade__form').replaceWith(html); $staffGradeTab.find('.staff__grade__form').replaceWith(html);
// Update the number of ungraded and checked out assigments.
view.updateStaffGradeCounts();
var $rubric = $staffGradeTab.find('.staff-assessment__assessment'); var $rubric = $staffGradeTab.find('.staff-assessment__assessment');
if ($rubric.size() > 0) { if ($rubric.size() > 0) {
var rubricElement = $rubric.get(0); var rubricElement = $rubric.get(0);
...@@ -153,6 +164,10 @@ ...@@ -153,6 +164,10 @@
// Install a change handler for rubric options to enable/disable the submit button // Install a change handler for rubric options to enable/disable the submit button
rubric.canSubmitCallback($.proxy(view.staffSubmitEnabled, view, $staffGradeTab)); rubric.canSubmitCallback($.proxy(view.staffSubmitEnabled, view, $staffGradeTab));
rubric.changesExistCallback(
$.proxy(view.assessmentRubricChanges, view, view.FULL_GRADE_UNSAVED_WARNING_KEY)
);
// Install a click handler for the submit buttons // Install a click handler for the submit buttons
$staffGradeTab.find('.wrapper--staff-assessment .action--submit').click( $staffGradeTab.find('.wrapper--staff-assessment .action--submit').click(
function(eventObject) { function(eventObject) {
...@@ -171,10 +186,29 @@ ...@@ -171,10 +186,29 @@
deferred.reject(); deferred.reject();
}); });
} }
return deferred.promise(); return deferred.promise();
}, },
/** /**
* Update the counts of ungraded and checked out assessments.
*/
updateStaffGradeCounts: function() {
var view = this;
var $staffGradeTab = $('.openassessment__staff-grading', this.element);
view.server.staffGradeCounts().done(function(html) {
$staffGradeTab.find('.staff__grade__status').replaceWith(html);
}).fail(function() {
$staffGradeTab.find('.staff__grade__status').replaceWith(
'<span class="staff__grade__status"><span class="staff__grade__value"><span class="copy">' +
gettext("Error getting the number of ungraded responses") +
'</span></span></span>'
);
});
},
/**
* Install event handlers for the view. * Install event handlers for the view.
*/ */
installHandlers: function() { installHandlers: function() {
...@@ -255,7 +289,10 @@ ...@@ -255,7 +289,10 @@
// Install a click handler for showing the staff grading form. // Install a click handler for showing the staff grading form.
$staffGradeTool.find('.staff__grade__show-form').click( $staffGradeTool.find('.staff__grade__show-form').click(
function() { function() {
view.loadStaffGradeForm(); var wasCollapsed = $staffGradeTool.find('.staff__grade__control').hasClass("is--collapsed");
if (wasCollapsed) {
view.loadStaffGradeForm();
}
} }
); );
}, },
...@@ -380,6 +417,23 @@ ...@@ -380,6 +417,23 @@
}, },
/** /**
* Called when something is selected or typed in the assessment rubric.
* Used to set the unsaved changes warning dialog.
*
* @param {string} key the unsaved changes key
* @param {boolean} changesExist true if unsaved changes exist
*/
assessmentRubricChanges: function(key, changesExist) {
if (changesExist) {
this.baseView.unsavedWarningEnabled(
true,
key,
gettext("If you leave this page without submitting your staff assessment, you will lose any work you have done.") // jscs:ignore maximumLineLength
);
}
},
/**
* Submit the staff assessment override. * Submit the staff assessment override.
* *
* @param {string} submissionID The ID of the submission to be submitted. * @param {string} submissionID The ID of the submission to be submitted.
...@@ -390,6 +444,7 @@ ...@@ -390,6 +444,7 @@
submitStaffOverride: function(submissionID, rubric, scope) { submitStaffOverride: function(submissionID, rubric, scope) {
var view = this; var view = this;
var successCallback = function() { var successCallback = function() {
view.baseView.unsavedWarningEnabled(false, view.OVERRIDE_UNSAVED_WARNING_KEY);
// Note: we ignore any message returned from the server and instead // Note: we ignore any message returned from the server and instead
// re-render the student info with the "Learner's Final Grade" // re-render the student info with the "Learner's Final Grade"
// section expanded. This section will show the learner's // section expanded. This section will show the learner's
...@@ -413,15 +468,9 @@ ...@@ -413,15 +468,9 @@
submitStaffGrade: function(submissionID, rubric, scope, continueGrading) { submitStaffGrade: function(submissionID, rubric, scope, continueGrading) {
var view = this; var view = this;
var successCallback = function() { var successCallback = function() {
view.baseView.unsavedWarningEnabled(false, view.FULL_GRADE_UNSAVED_WARNING_KEY);
view.staffGradeFormLoaded = false; view.staffGradeFormLoaded = false;
var showFullGradeTab = function() { view.loadStaffGradeForm(!continueGrading);
// Need to show the staff grade component again, unfortunately requiring a global selector.
$('.button-staff-grading').click();
if (continueGrading) {
$('.staff__grade__show-form', view.element).click();
}
};
view.load(showFullGradeTab);
}; };
this.callStaffAssess(submissionID, rubric, scope, successCallback, '.staff-grade-error'); this.callStaffAssess(submissionID, rubric, scope, successCallback, '.staff-grade-error');
}, },
......
...@@ -137,6 +137,29 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) { ...@@ -137,6 +137,29 @@ if (typeof OpenAssessment.Server === "undefined" || !OpenAssessment.Server) {
}, },
/** /**
* Renders the count of ungraded and checked out assessemtns.
*
* @returns {promise} A JQuery promise, which resolves with the HTML of the rendered section
* fails with an error message.
*/
staffGradeCounts: function() {
var url = this.url('render_staff_grade_counts');
return $.Deferred(function(defer) {
$.ajax({
url: url,
type: "POST",
dataType: "html"
}).done(function(data) {
defer.resolveWith(this, [data]);
}).fail(function() {
defer.rejectWith(
this, [gettext('The display of ungraded and checked out responses could not be loaded.')]
);
});
}).promise();
},
/**
* Send a submission to the XBlock. * Send a submission to the XBlock.
* *
* @param {string} submission The text of the student's submission. * @param {string} submission The text of the student's submission.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment