MDL-60430 assignment: Assignment save and show next for 1 user
[moodle.git] / mod / assign / amd / src / grading_panel.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Javascript controller for the "Grading" panel at the right of the page.
18  *
19  * @module     mod_assign/grading_panel
20  * @package    mod_assign
21  * @class      GradingPanel
22  * @copyright  2016 Damyon Wiese <damyon@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      3.1
25  */
26 define(['jquery', 'core/yui', 'core/notification', 'core/templates', 'core/fragment',
27         'core/ajax', 'core/str', 'mod_assign/grading_form_change_checker',
28         'mod_assign/grading_events'],
29        function($, Y, notification, templates, fragment, ajax, str, checker, GradingEvents) {
31     /**
32      * GradingPanel class.
33      *
34      * @class GradingPanel
35      * @param {String} selector The selector for the page region containing the user navigation.
36      */
37     var GradingPanel = function(selector) {
38         this._regionSelector = selector;
39         this._region = $(selector);
40         this._userCache = [];
42         this.registerEventListeners();
43     };
45     /** @type {String} Selector for the page region containing the user navigation. */
46     GradingPanel.prototype._regionSelector = null;
48     /** @type {Integer} Remember the last user id to prevent unnessecary reloads. */
49     GradingPanel.prototype._lastUserId = 0;
51     /** @type {Integer} Remember the last attempt number to prevent unnessecary reloads. */
52     GradingPanel.prototype._lastAttemptNumber = -1;
54     /** @type {JQuery} JQuery node for the page region containing the user navigation. */
55     GradingPanel.prototype._region = null;
57      /** @type {Integer} The id of the next user in the grading list */
58     GradingPanel.prototype.nextUserId = null;
60      /** @type {Boolean} Next user exists in the grading list */
61     GradingPanel.prototype.nextUser = false;
63     /**
64      * Fade the dom node out, update it, and fade it back.
65      *
66      * @private
67      * @method _niceReplaceNodeContents
68      * @param {JQuery} node
69      * @param {String} html
70      * @param {String} js
71      * @return {Deferred} promise resolved when the animations are complete.
72      */
73     GradingPanel.prototype._niceReplaceNodeContents = function(node, html, js) {
74         var promise = $.Deferred();
76         node.fadeOut("fast", function() {
77             templates.replaceNodeContents(node, html, js);
78             node.fadeIn("fast", function() {
79                 promise.resolve();
80             });
81         });
83         return promise.promise();
84     };
86     /**
87      * Make sure all form fields have the latest saved state.
88      * @private
89      * @method _saveFormState
90      */
91     GradingPanel.prototype._saveFormState = function() {
92         // Grrrrr! TinyMCE you know what you did.
93         if (typeof window.tinyMCE !== 'undefined') {
94             window.tinyMCE.triggerSave();
95         }
97         // Copy data from notify students checkbox which was moved out of the form.
98         var checked = $('[data-region="grading-actions-form"] [name="sendstudentnotifications"]').prop("checked");
99         $('.gradeform [name="sendstudentnotifications"]').val(checked);
100     };
102     /**
103      * Make form submit via ajax.
104      *
105      * @private
106      * @param {Object} event
107      * @param {Integer} nextUserId
108      * @param {Boolean} nextUser optional. Load next user in the grading list.
109      * @method _submitForm
110      */
111     GradingPanel.prototype._submitForm = function(event, nextUserId, nextUser) {
112         // The form was submitted - send it via ajax instead.
113         var form = $(this._region.find('form.gradeform'));
115         $('[data-region="overlay"]').show();
117         // We call this, so other modules can update the form with the latest state.
118         form.trigger('save-form-state');
120         // Now we get all the current values from the form.
121         var data = form.serialize();
122         var assignmentid = this._region.attr('data-assignmentid');
124         // Now we can continue...
125         ajax.call([{
126             methodname: 'mod_assign_submit_grading_form',
127             args: {assignmentid: assignmentid, userid: this._lastUserId, jsonformdata: JSON.stringify(data)},
128             done: this._handleFormSubmissionResponse.bind(this, data, nextUserId, nextUser),
129             fail: notification.exception
130         }]);
131     };
133     /**
134      * Handle form submission response.
135      *
136      * @private
137      * @method _handleFormSubmissionResponse
138      * @param {Array} formdata - submitted values
139      * @param {Integer} nextUserId - optional. The id of the user to load after the form is saved.
140      * @param {Array} response List of errors.
141      * @param {Boolean} nextUser - optional. If true, switch to next user in the grading list.
142      */
143     GradingPanel.prototype._handleFormSubmissionResponse = function(formdata, nextUserId, nextUser, response) {
144         if (typeof nextUserId === "undefined") {
145             nextUserId = this._lastUserId;
146         }
147         if (response.length) {
148             // There was an error saving the grade. Re-render the form using the submitted data so we can show
149             // validation errors.
150             $(document).trigger('reset', [this._lastUserId, formdata]);
151         } else {
152             str.get_strings([
153                 {key: 'changessaved', component: 'core'},
154                 {key: 'gradechangessaveddetail', component: 'mod_assign'},
155             ]).done(function(strs) {
156                 notification.alert(strs[0], strs[1]);
157             }).fail(notification.exception);
158             Y.use('moodle-core-formchangechecker', function() {
159                 M.core_formchangechecker.reset_form_dirty_state();
160             });
161             if (nextUserId == this._lastUserId) {
162                 $(document).trigger('reset', nextUserId);
163             } else if (nextUser) {
164                 $(document).trigger('done-saving-show-next', true);
165             } else {
166                 $(document).trigger('user-changed', nextUserId);
167             }
168         }
169         $('[data-region="overlay"]').hide();
170     };
172     /**
173      * Refresh form with default values.
174      *
175      * @private
176      * @method _resetForm
177      * @param {Event} e
178      * @param {Integer} userid
179      * @param {Array} formdata
180      */
181     GradingPanel.prototype._resetForm = function(e, userid, formdata) {
182         // The form was cancelled - refresh with default values.
183         var event = $.Event("custom");
184         if (typeof userid == "undefined") {
185             userid = this._lastUserId;
186         }
187         this._lastUserId = 0;
188         this._refreshGradingPanel(event, userid, formdata);
189     };
191     /**
192      * Open a picker to choose an older attempt.
193      *
194      * @private
195      * @param {Object} e
196      * @method _chooseAttempt
197      */
198     GradingPanel.prototype._chooseAttempt = function(e) {
199         // Show a dialog.
201         // The form is in the element pointed to by data-submissions.
202         var link = $(e.target);
203         var submissionsId = link.data('submissions');
204         var submissionsform = $(document.getElementById(submissionsId));
205         var formcopy = submissionsform.clone();
206         var formhtml = formcopy.wrap($('<form/>')).html();
208         str.get_strings([
209             {key: 'viewadifferentattempt', component: 'mod_assign'},
210             {key: 'view', component: 'core'},
211             {key: 'cancel', component: 'core'},
212         ]).done(function(strs) {
213             notification.confirm(strs[0], formhtml, strs[1], strs[2], function() {
214                 var attemptnumber = $("input:radio[name='select-attemptnumber']:checked").val();
216                 this._refreshGradingPanel(null, this._lastUserId, '', attemptnumber);
217             }.bind(this));
218         }.bind(this)).fail(notification.exception);
219     };
221     /**
222      * Add popout buttons
223      *
224      * @private
225      * @method _addPopoutButtons
226      * @param {JQuery} selector The region selector to add popout buttons to.
227      */
228     GradingPanel.prototype._addPopoutButtons = function(selector) {
229         var region = $(selector);
231         templates.render('mod_assign/popout_button', {}).done(function(html) {
232             var parents = region.find('[data-fieldtype="filemanager"],[data-fieldtype="editor"],[data-fieldtype="grading"]')
233                     .closest('.fitem');
234             parents.addClass('has-popout').find('label').parent().append(html);
236             region.on('click', '[data-region="popout-button"]', this._togglePopout.bind(this));
237         }.bind(this)).fail(notification.exception);
238     };
240     /**
241      * Make a div "popout" or "popback".
242      *
243      * @private
244      * @method _togglePopout
245      * @param {Event} event
246      */
247     GradingPanel.prototype._togglePopout = function(event) {
248         event.preventDefault();
249         var container = $(event.target).closest('.fitem');
250         if (container.hasClass('popout')) {
251             $('.popout').removeClass('popout');
252         } else {
253             $('.popout').removeClass('popout');
254             container.addClass('popout');
255             container.addClass('moodle-has-zindex');
256         }
257     };
259     /**
260      * Get the user context - re-render the template in the page.
261      *
262      * @private
263      * @method _refreshGradingPanel
264      * @param {Event} event
265      * @param {Number} userid
266      * @param {String} submissiondata serialised submission data.
267      * @param {Integer} attemptnumber
268      */
269     GradingPanel.prototype._refreshGradingPanel = function(event, userid, submissiondata, attemptnumber) {
270         var contextid = this._region.attr('data-contextid');
271         if (typeof submissiondata === 'undefined') {
272             submissiondata = '';
273         }
274         if (typeof attemptnumber === 'undefined') {
275             attemptnumber = -1;
276         }
277         // Skip reloading if it is the same user.
278         if (this._lastUserId == userid && this._lastAttemptNumber == attemptnumber && submissiondata === '') {
279             return;
280         }
281         this._lastUserId = userid;
282         this._lastAttemptNumber = attemptnumber;
283         $(document).trigger('start-loading-user');
284         // Tell behat to back off too.
285         window.M.util.js_pending('mod-assign-loading-user');
286         // First insert the loading template.
287         templates.render('mod_assign/loading', {}).done(function(html, js) {
288             // Update the page.
289             this._niceReplaceNodeContents(this._region, html, js).done(function() {
290                 if (userid > 0) {
291                     this._region.show();
292                     // Reload the grading form "fragment" for this user.
293                     var params = {userid: userid, attemptnumber: attemptnumber, jsonformdata: JSON.stringify(submissiondata)};
294                     fragment.loadFragment('mod_assign', 'gradingpanel', contextid, params).done(function(html, js) {
295                         this._niceReplaceNodeContents(this._region, html, js)
296                         .done(function() {
297                             checker.saveFormState('[data-region="grade-panel"] .gradeform');
298                             $(document).on('editor-content-restored', function() {
299                                 // If the editor has some content that has been restored
300                                 // then save the form state again for comparison.
301                                 checker.saveFormState('[data-region="grade-panel"] .gradeform');
302                             });
303                             $('[data-region="attempt-chooser"]').on('click', this._chooseAttempt.bind(this));
304                             this._addPopoutButtons('[data-region="grade-panel"] .gradeform');
305                             $(document).trigger('finish-loading-user');
306                             // Tell behat we are friends again.
307                             window.M.util.js_complete('mod-assign-loading-user');
308                         }.bind(this))
309                         .fail(notification.exception);
310                     }.bind(this)).fail(notification.exception);
311                     $('[data-region="review-panel"]').show();
312                 } else {
313                     this._region.hide();
314                     $('[data-region="review-panel"]').hide();
315                     $(document).trigger('finish-loading-user');
316                     // Tell behat we are friends again.
317                     window.M.util.js_complete('mod-assign-loading-user');
318                 }
319             }.bind(this));
320         }.bind(this)).fail(notification.exception);
321     };
323     /**
324      * Get next user data and store it in global variables
325      *
326      * @private
327      * @method _getNextUser
328      * @param {Event} event
329      * @param {Object} data Next user's data
330      */
331     GradingPanel.prototype._getNextUser = function(event, data) {
332         this.nextUserId = data.nextUserId;
333         this.nextUser = data.nextUser;
334     };
336     /**
337      * Handle the save-and-show-next event
338      *
339      * @private
340      * @method _handleSaveAndShowNext
341      */
342     GradingPanel.prototype._handleSaveAndShowNext = function() {
343         this._submitForm(null, this.nextUserId, this.nextUser);
344     };
346     /**
347      * Get the grade panel element.
348      *
349      * @method getPanelElement
350      * @return {jQuery}
351      */
352     GradingPanel.prototype.getPanelElement = function() {
353         return $('[data-region="grade-panel"]');
354     };
356     /**
357      * Hide the grade panel.
358      *
359      * @method collapsePanel
360      */
361     GradingPanel.prototype.collapsePanel = function() {
362         this.getPanelElement().addClass('collapsed');
363     };
365     /**
366      * Show the grade panel.
367      *
368      * @method expandPanel
369      */
370     GradingPanel.prototype.expandPanel = function() {
371         this.getPanelElement().removeClass('collapsed');
372     };
374     /**
375      * Register event listeners for the grade panel.
376      *
377      * @method registerEventListeners
378      */
379     GradingPanel.prototype.registerEventListeners = function() {
380         var docElement = $(document);
381         var region = $(this._region);
382         // Add an event listener to prevent form submission when pressing enter key.
383         region.on('submit', 'form', function(e) {
384             e.preventDefault();
385         });
387         docElement.on('next-user', this._getNextUser.bind(this));
388         docElement.on('user-changed', this._refreshGradingPanel.bind(this));
389         docElement.on('save-changes', this._submitForm.bind(this));
390         docElement.on('save-and-show-next', this._handleSaveAndShowNext.bind(this));
391         docElement.on('reset', this._resetForm.bind(this));
393         docElement.on('save-form-state', this._saveFormState.bind(this));
395         docElement.on(GradingEvents.COLLAPSE_GRADE_PANEL, function() {
396             this.collapsePanel();
397         }.bind(this));
399         // We should expand if the review panel is collapsed.
400         docElement.on(GradingEvents.COLLAPSE_REVIEW_PANEL, function() {
401             this.expandPanel();
402         }.bind(this));
404         docElement.on(GradingEvents.EXPAND_GRADE_PANEL, function() {
405             this.expandPanel();
406         }.bind(this));
407     };
409     return GradingPanel;
410 });