/* JavaScript for student-facing views of Open Assessment XBlock */ /* Namespace for open assessment */ if (typeof OpenAssessment == "undefined" || !OpenAssessment) { OpenAssessment = {}; } /** Interface for student-facing views. Args: runtime (Runtime): an XBlock runtime instance. element (DOM element): The DOM element representing this XBlock. server (OpenAssessment.Server): The interface to the XBlock server. Returns: OpenAssessment.BaseView **/ OpenAssessment.BaseView = function(runtime, element, server) { this.runtime = runtime; this.element = element; this.server = server; this.responseView = new OpenAssessment.ResponseView(this.element, this.server, this); this.gradeView = new OpenAssessment.GradeView(this.element, this.server, this); }; OpenAssessment.BaseView.prototype = { /** * Checks to see if the scrollTo function is available, then scrolls to the * top of the list of steps for this display. * * Ideally, we would not need to check if the function exists, and could * import scrollTo, or other dependencies, into workbench. */ scrollToTop: function() { if ($.scrollTo instanceof Function) { $(window).scrollTo($("#openassessment__steps"), 800, {offset:-50}); } }, /** Install click handlers to expand/collapse a section. Args: parentSel (JQuery selector): CSS selector for the container element. onExpand (function): Function to execute when expanding (if provided). Accepts no args. **/ setUpCollapseExpand: function(parentSel, onExpand) { parentSel.find('.ui-toggle-visibility__control').click( function(eventData) { var sel = $(eventData.target).closest('.ui-toggle-visibility'); // If we're expanding and have an `onExpand` callback defined, execute it. if (sel.hasClass('is--collapsed') && onExpand !== undefined) { onExpand(); } sel.toggleClass('is--collapsed'); } ); }, /** * Asynchronously load each sub-view into the DOM. */ load: function() { this.responseView.load(); this.renderPeerAssessmentStep(); this.renderSelfAssessmentStep(); this.gradeView.load(); // Set up expand/collapse for course staff debug, if available courseStaffDebug = $('.wrapper--staff-info'); if (courseStaffDebug.length > 0) { this.setUpCollapseExpand(courseStaffDebug, function() {}); } }, /** Render the peer-assessment step. **/ renderPeerAssessmentStep: function() { var view = this; this.server.render('peer_assessment').done( function(html) { // Load the HTML $('#openassessment__peer-assessment', view.element).replaceWith(html); var sel = $('#openassessment__peer-assessment', view.element); // Install a click handler for collapse/expand view.setUpCollapseExpand(sel, $.proxy(view.renderContinuedPeerAssessmentStep, view)); // Install a change handler for rubric options to enable/disable the submit button sel.find("#peer-assessment--001__assessment").change( function() { var numChecked = $('input[type=radio]:checked', this).length; var numAvailable = $('.field--radio.assessment__rubric__question', this).length; $("#peer-assessment--001__assessment__submit", view.element).toggleClass( 'is--disabled', numChecked != numAvailable ); } ); // Install a click handler for assessment sel.find('#peer-assessment--001__assessment__submit').click( function(eventObject) { // Override default form submission eventObject.preventDefault(); // Handle the click view.peerAssess(); } ); } ).fail(function(errMsg) { view.showLoadError('peer-assessment'); }); }, /** * Render the peer-assessment step for continued grading. Always renders as * expanded, since this should be called for an explicit continuation of the * peer grading process. */ renderContinuedPeerAssessmentStep: function() { var view = this; this.server.renderContinuedPeer().done( function(html) { // Load the HTML $('#openassessment__peer-assessment', view.element).replaceWith(html); var sel = $('#openassessment__peer-assessment', view.element); // Install a click handler for collapse/expand view.setUpCollapseExpand(sel); // Install a click handler for assessment sel.find('#peer-assessment--001__assessment__submit').click( function(eventObject) { // Override default form submission eventObject.preventDefault(); // Handle the click view.continuedPeerAssess(); } ); // Install a change handler for rubric options to enable/disable the submit button sel.find("#peer-assessment--001__assessment").change( function() { var numChecked = $('input[type=radio]:checked', this).length; var numAvailable = $('.field--radio.assessment__rubric__question', this).length; $("#peer-assessment--001__assessment__submit", view.element).toggleClass( 'is--disabled', numChecked != numAvailable ); } ); } ).fail(function(errMsg) { view.showLoadError('peer-assessment'); }); }, /** Render the self-assessment step. **/ renderSelfAssessmentStep: function() { var view = this; this.server.render('self_assessment').done( function(html) { // Load the HTML $('#openassessment__self-assessment', view.element).replaceWith(html); var sel = $('#openassessment__self-assessment', view.element); // Install a click handler for collapse/expand view.setUpCollapseExpand(sel); // Install a change handler for rubric options to enable/disable the submit button $("#self-assessment--001__assessment", view.element).change( function() { var numChecked = $('input[type=radio]:checked', this).length; var numAvailable = $('.field--radio.assessment__rubric__question', this).length; $("#self-assessment--001__assessment__submit", view.element).toggleClass( 'is--disabled', numChecked != numAvailable ); } ); // Install a click handler for the submit button sel.find('#self-assessment--001__assessment__submit').click( function(eventObject) { // Override default form submission eventObject.preventDefault(); // Handle the click view.selfAssess(); } ); } ).fail(function(errMsg) { view.showLoadError('self-assessment'); }); }, /** Send an assessment to the server and update the view. **/ peerAssess: function() { var view = this; this.peerAssessRequest(function() { view.renderPeerAssessmentStep(); view.renderSelfAssessmentStep(); view.gradeView.load(); view.scrollToTop(); }); }, /** * Send an assessment to the server and update the view, with the assumption * that we are continuing peer assessments beyond the required amount. */ continuedPeerAssess: function() { var view = this; view.peerAssessRequest(function() { view.renderContinuedPeerAssessmentStep(); view.gradeView.load(); }); }, /** * Common peer assessment request building, used for all types of peer * assessments. * * Args: * successFunction (function): The function called if the request is * successful. This varies based on the type of request to submit * a peer assessment. */ peerAssessRequest: function(successFunction) { // Retrieve assessment info from the DOM var submissionId = $("#peer_submission_uuid", this.element)[0].innerHTML.trim(); var optionsSelected = {}; $("#peer-assessment--001__assessment input[type=radio]:checked", this.element).each( function(index, sel) { optionsSelected[sel.name] = sel.value; } ); var feedback = $('#assessment__rubric__question--feedback__value', this.element).val(); // Send the assessment to the server var view = this; this.toggleActionError('peer', null); this.server.peerAssess(submissionId, optionsSelected, feedback).done( successFunction ).fail(function(errMsg) { view.toggleActionError('peer', errMsg); }); }, /** Send a self-assessment to the server and update the view. **/ selfAssess: function() { // Retrieve self-assessment info from the DOM var submissionId = $("#self_submission_uuid", this.element)[0].innerHTML.trim(); var optionsSelected = {}; $("#self-assessment--001__assessment input[type=radio]:checked", this.element).each( function(index, sel) { optionsSelected[sel.name] = sel.value; } ); // Send the assessment to the server var view = this; this.toggleActionError('self', null); this.server.selfAssess(submissionId, optionsSelected).done( function() { view.renderPeerAssessmentStep(); view.renderSelfAssessmentStep(); view.gradeView.load(); view.scrollToTop(); } ).fail(function(errMsg) { view.toggleActionError('self', errMsg); }); }, /** Report an error to the user. Args: type (str): Which type of error. Options are "save", submit", "peer", and "self". msg (str or null): The error message to display. If null, hide the error message (with one exception: loading errors are never hidden once displayed) **/ toggleActionError: function(type, msg) { var element = this.element; var container = null; if (type == 'save') { container = '.response__submission__actions'; } else if (type == 'submit' || type == 'peer' || type == 'self') { container = '.step__actions'; } else if (type == 'feedback_assess') { container = '.submission__feedback__actions'; } // If we don't have anywhere to put the message, just log it to the console if (container === null) { if (msg !== null) { console.log(msg); } } else { // Insert the error message var msgHtml = (msg === null) ? "" : msg; $(container + " .message__content", element).html('<p>' + msgHtml + '</p>'); // Toggle the error class $(container, element).toggleClass('has--error', msg !== null); } }, /** Report an error loading a step. Args: step (str): the step that could not be loaded. **/ showLoadError: function(step) { var container = '#openassessment__' + step; $(container).toggleClass('has--error', true); $(container + ' .step__status__value i').removeClass().addClass('ico icon-warning-sign'); $(container + ' .step__status__value .copy').html('Unable to Load'); }, /** * Get the contents of the Step Actions error message box, for unit test validation. * * Step Actions are the UX-level parts of the student interaction flow - * Submission, Peer Assessment, and Self Assessment. Since steps are mutually * exclusive, only one error box should be rendered on screen at a time. * * Returns: * One HTML string */ getStepActionsErrorMessage: function() { return $('.step__actions .message__content').html(); } }; /* XBlock JavaScript entry point for OpenAssessmentXBlock. */ function OpenAssessmentBlock(runtime, element) { /** Render views within the base view on page load. **/ $(function($) { var server = new OpenAssessment.Server(runtime, element); var view = new OpenAssessment.BaseView(runtime, element, server); view.load(); }); }