Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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/>.
15
 
16
/**
17
 * Javascript to handle changing users via the user selector in the header.
18
 *
19
 * @module     mod_assign/grading_navigation
20
 * @copyright  2016 Damyon Wiese <damyon@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 * @since      3.1
23
 */
24
define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete',
25
        'core/ajax', 'core_user/repository', 'mod_assign/grading_form_change_checker'],
26
       function($, notification, str, autocomplete, ajax, UserRepository, checker) {
27
 
28
    /**
29
     * GradingNavigation class.
30
     *
31
     * @class mod_assign/grading_navigation
32
     * @param {String} selector The selector for the page region containing the user navigation.
33
     */
34
    var GradingNavigation = function(selector) {
35
        this._regionSelector = selector;
36
        this._region = $(selector);
37
        this._filters = [];
38
        this._users = [];
39
        this._filteredUsers = [];
40
        this._lastXofYUpdate = 0;
41
        this._firstLoadUsers = true;
42
 
43
        let url = new URL(window.location);
44
        if (parseInt(url.searchParams.get('treset')) > 0) {
45
            // Remove 'treset' url parameter to make sure that
46
            // table preferences won't be reset on page refresh.
47
            url.searchParams.delete('treset');
48
            window.history.replaceState({}, "", url);
49
        }
50
 
51
        // Get the current user list from a webservice.
52
        this._loadAllUsers();
53
 
54
        // We do not allow navigation while ajax requests are pending.
55
        // Attach listeners to the select and arrow buttons.
56
 
57
        this._region.find('[data-action="previous-user"]').on('click', this._handlePreviousUser.bind(this));
58
        this._region.find('[data-action="next-user"]').on('click', this._handleNextUser.bind(this));
59
        this._region.find('[data-action="change-user"]').on('change', this._handleChangeUser.bind(this));
60
        this._region.find('[data-region="user-filters"]').on('click', this._toggleExpandFilters.bind(this));
61
        this._region.find('[data-region="user-resettable"]').on('click', this._toggleResetTable.bind());
62
 
63
        $(document).on('user-changed', this._refreshSelector.bind(this));
64
        $(document).on('done-saving-show-next', this._handleNextUser.bind(this));
65
 
66
        // Position the configure filters panel under the link that expands it.
67
        var toggleLink = this._region.find('[data-region="user-filters"]');
68
        var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));
69
 
70
        configPanel.on('change', 'select', this._filterChanged.bind(this));
71
 
72
        var userid = $('[data-region="grading-navigation-panel"]').data('first-userid');
73
        if (userid) {
74
            this._selectUserById(userid);
75
        }
76
 
77
        str.get_string('changeuser', 'mod_assign').done(function(s) {
78
                autocomplete.enhance('[data-action=change-user]', false, 'mod_assign/participant_selector', s);
79
            }
80
        ).fail(notification.exception);
81
 
82
        $(document).bind("start-loading-user", function() {
83
            this._isLoading = true;
84
        }.bind(this));
85
        $(document).bind("finish-loading-user", function() {
86
            this._isLoading = false;
87
        }.bind(this));
88
    };
89
 
90
    /** @property {Boolean} Boolean tracking active ajax requests. */
91
    GradingNavigation.prototype._isLoading = false;
92
 
93
    /** @property {String} Selector for the page region containing the user navigation. */
94
    GradingNavigation.prototype._regionSelector = null;
95
 
96
    /** @property {Array} The list of active filter keys */
97
    GradingNavigation.prototype._filters = null;
98
 
99
    /** @property {Array} The list of users */
100
    GradingNavigation.prototype._users = null;
101
 
102
    /** @property {JQuery} JQuery node for the page region containing the user navigation. */
103
    GradingNavigation.prototype._region = null;
104
 
105
    /** @property {String} Last active filters */
106
    GradingNavigation.prototype._lastFilters = '';
107
 
108
    /**
109
     * Load the list of all users for this assignment.
110
     *
111
     * @private
112
     * @method _loadAllUsers
113
     * @return {Boolean} True if the user list was fetched.
114
     */
115
    GradingNavigation.prototype._loadAllUsers = function() {
116
        var select = this._region.find('[data-action=change-user]');
117
        var assignmentid = select.attr('data-assignmentid');
118
        var groupid = select.attr('data-groupid');
119
 
120
        var filterPanel = this._region.find('[data-region="configure-filters"]');
121
        var filter = filterPanel.find('select[name="filter"]').val();
122
        var workflowFilter = filterPanel.find('select[name="workflowfilter"]');
123
        if (workflowFilter) {
124
            filter += ',' + workflowFilter.val();
125
        }
126
        var markerFilter = filterPanel.find('select[name="markerfilter"]');
127
        if (markerFilter) {
128
            filter += ',' + markerFilter.val();
129
        }
130
 
131
        if (this._lastFilters == filter) {
132
            return false;
133
        }
134
        this._lastFilters = filter;
135
 
136
        ajax.call([{
137
            methodname: 'mod_assign_list_participants',
138
            args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true, tablesort: true},
139
            done: this._usersLoaded.bind(this),
140
            fail: notification.exception
141
        }]);
142
        return true;
143
    };
144
 
145
    /**
146
     * Call back to rebuild the user selector and x of y info when the user list is updated.
147
     *
148
     * @private
149
     * @method _usersLoaded
150
     * @param {Array} users
151
     */
152
    GradingNavigation.prototype._usersLoaded = function(users) {
153
        this._firstLoadUsers = false;
154
        this._filteredUsers = this._users = users;
155
        if (this._users.length) {
156
            // Position the configure filters panel under the link that expands it.
157
            var toggleLink = this._region.find('[data-region="user-filters"]');
158
            var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));
159
 
160
            configPanel.find('select[name="filter"]').trigger('change');
161
        } else {
162
            this._selectNoUser();
163
        }
164
        this._triggerNextUserEvent();
165
    };
166
 
167
    /**
168
     * Close the configure filters panel if a click is detected outside of it.
169
     *
170
     * @private
171
     * @method _checkClickOutsideConfigureFilters
172
     * @param {Event} event
173
     */
174
    GradingNavigation.prototype._checkClickOutsideConfigureFilters = function(event) {
175
        var configPanel = this._region.find('[data-region="configure-filters"]');
176
 
177
        if (!configPanel.is(event.target) && configPanel.has(event.target).length === 0) {
178
            var toggleLink = this._region.find('[data-region="user-filters"]');
179
 
180
            configPanel.hide();
181
            configPanel.attr('aria-hidden', 'true');
182
            toggleLink.attr('aria-expanded', 'false');
183
            $(document).unbind('click.mod_assign_grading_navigation');
184
        }
185
    };
186
 
187
    /**
188
     * Close the configure filters panel if a click is detected outside of it.
189
     *
190
     * @private
191
     * @method _updateFilterPreference
192
     * @param {Number} userId The current user id.
193
     * @param {Array} filterList The list of current filter values.
194
     * @param {Array} preferenceNames The names of the preferences to update
195
     * @return {Promise} Resolved when all the preferences are updated.
196
     */
197
    GradingNavigation.prototype._updateFilterPreferences = function(userId, filterList, preferenceNames) {
198
        var preferences = [],
199
            i = 0;
200
 
201
        if (filterList.length == 0 || this._firstLoadUsers) {
202
            // Nothing to update.
203
            var deferred = $.Deferred();
204
            deferred.resolve();
205
            return deferred;
206
        }
207
        // General filter.
208
        // Set the user preferences to the current filters.
209
        for (i = 0; i < filterList.length; i++) {
210
            var newValue = filterList[i];
211
            if (newValue == 'none') {
212
                newValue = '';
213
            }
214
 
215
            preferences.push({
216
                userid: userId,
217
                name: preferenceNames[i],
218
                value: newValue
219
            });
220
        }
221
 
222
        return UserRepository.setUserPreferences(preferences);
223
    };
224
    /**
225
     * Turn a filter on or off.
226
     *
227
     * @private
228
     * @method _filterChanged
229
     */
230
    GradingNavigation.prototype._filterChanged = function() {
231
        // There are 3 types of filter right now.
232
        var filterPanel = this._region.find('[data-region="configure-filters"]');
233
        var filters = filterPanel.find('select');
234
        var preferenceNames = [];
235
 
236
        this._filters = [];
237
        filters.each(function(idx, ele) {
238
            var element = $(ele);
239
            this._filters.push(element.val());
240
            preferenceNames.push('assign_' + element.prop('name'));
241
        }.bind(this));
242
 
243
        // Update the active filter string.
244
        var filterlist = [];
245
        filterPanel.find('option:checked').each(function(idx, ele) {
246
            filterlist[filterlist.length] = $(ele).text();
247
        });
248
        if (filterlist.length) {
249
            this._region.find('[data-region="user-filters"] span').text(filterlist.join(', '));
250
        } else {
251
            str.get_string('nofilters', 'mod_assign').done(function(s) {
252
                this._region.find('[data-region="user-filters"] span').text(s);
253
            }.bind(this)).fail(notification.exception);
254
        }
255
 
256
        var select = this._region.find('[data-action=change-user]');
257
        var currentUserID = select.data('currentuserid');
258
        this._updateFilterPreferences(currentUserID, this._filters, preferenceNames).done(function() {
259
            // Reload the list of users to apply the new filters.
260
            if (!this._loadAllUsers()) {
261
                var userid = parseInt(select.attr('data-selected'));
262
                let foundIndex = null;
263
                // Search the returned users for the current selection.
264
                $.each(this._filteredUsers, function(index, user) {
265
                    if (userid == user.id) {
266
                        foundIndex = index;
267
                    }
268
                });
269
 
270
                if (this._filteredUsers.length && foundIndex !== null) {
271
                    this._selectUserById(this._filteredUsers[foundIndex].id);
272
                } else {
273
                    this._selectNoUser();
274
                }
275
 
276
            }
277
        }.bind(this)).fail(notification.exception);
278
        this._refreshCount();
279
    };
280
 
281
    /**
282
     * Select no users, because no users match the filters.
283
     *
284
     * @private
285
     * @method _selectNoUser
286
     */
287
    GradingNavigation.prototype._selectNoUser = function() {
288
        // Detect unsaved changes, and offer to save them - otherwise change user right now.
289
        if (this._isLoading) {
290
            return;
291
        }
292
        if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
293
            // Form has changes, so we need to confirm before switching users.
294
            str.get_strings([
295
                {key: 'unsavedchanges', component: 'mod_assign'},
296
                {key: 'unsavedchangesquestion', component: 'mod_assign'},
297
                {key: 'saveandcontinue', component: 'mod_assign'},
298
                {key: 'cancel', component: 'core'},
299
            ]).done(function(strs) {
300
                notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
301
                    $(document).trigger('save-changes', -1);
302
                });
303
            });
304
        } else {
305
            $(document).trigger('user-changed', -1);
306
        }
307
    };
308
 
309
    /**
310
     * Select the specified user by id.
311
     *
312
     * @private
313
     * @method _selectUserById
314
     * @param {Number} userid
315
     */
316
    GradingNavigation.prototype._selectUserById = function(userid) {
317
        var select = this._region.find('[data-action=change-user]');
318
        var useridnumber = parseInt(userid, 10);
319
 
320
        // Detect unsaved changes, and offer to save them - otherwise change user right now.
321
        if (this._isLoading) {
322
            return;
323
        }
324
        if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
325
            // Form has changes, so we need to confirm before switching users.
326
            str.get_strings([
327
                {key: 'unsavedchanges', component: 'mod_assign'},
328
                {key: 'unsavedchangesquestion', component: 'mod_assign'},
329
                {key: 'saveandcontinue', component: 'mod_assign'},
330
                {key: 'cancel', component: 'core'},
331
            ]).done(function(strs) {
332
                notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
333
                    $(document).trigger('save-changes', useridnumber);
334
                });
335
            });
336
        } else {
337
            select.attr('data-selected', userid);
338
 
339
            // If we have some filtered users, and userid is specified, then trigger change.
340
            if (this._filteredUsers.length > 0 && !isNaN(useridnumber) && useridnumber > 0) {
341
                $(document).trigger('user-changed', useridnumber);
342
            }
343
        }
344
    };
345
 
346
    /**
347
     * Expand or collapse the filter config panel.
348
     *
349
     * @private
350
     * @method _toggleExpandFilters
351
     * @param {Event} event
352
     */
353
    GradingNavigation.prototype._toggleExpandFilters = function(event) {
354
        event.preventDefault();
355
        var toggleLink = $(event.target).closest('[data-region="user-filters"]');
356
        var expanded = toggleLink.attr('aria-expanded') == 'true';
357
        var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));
358
 
359
        if (expanded) {
360
            configPanel.hide();
361
            configPanel.attr('aria-hidden', 'true');
362
            toggleLink.attr('aria-expanded', 'false');
363
            $(document).unbind('click.mod_assign_grading_navigation');
364
        } else {
365
            configPanel.css('display', 'inline-block');
366
            configPanel.attr('aria-hidden', 'false');
367
            toggleLink.attr('aria-expanded', 'true');
368
            event.stopPropagation();
369
            $(document).on('click.mod_assign_grading_navigation', this._checkClickOutsideConfigureFilters.bind(this));
370
        }
371
    };
372
 
373
    /**
374
     * Reset table preferences.
375
     *
376
     * @private
377
     * @method _toggleResetTable
378
     */
379
    GradingNavigation.prototype._toggleResetTable = function() {
380
        let url = new URL(window.location);
381
        url.searchParams.set('treset', '1');
382
        window.location.href = url;
383
    };
384
 
385
    /**
386
     * Change to the previous user in the grading list.
387
     *
388
     * @private
389
     * @method _handlePreviousUser
390
     * @param {Event} e
391
     */
392
    GradingNavigation.prototype._handlePreviousUser = function(e) {
393
        e.preventDefault();
394
        var select = this._region.find('[data-action=change-user]');
395
        var currentUserId = select.attr('data-selected');
396
        var i = 0;
397
        var currentIndex = 0;
398
 
399
        for (i = 0; i < this._filteredUsers.length; i++) {
400
            if (this._filteredUsers[i].id == currentUserId) {
401
                currentIndex = i;
402
                break;
403
            }
404
        }
405
 
406
        var count = this._filteredUsers.length;
407
        var newIndex = (currentIndex - 1);
408
        if (newIndex < 0) {
409
            newIndex = count - 1;
410
        }
411
 
412
        if (count) {
413
            this._selectUserById(this._filteredUsers[newIndex].id);
414
        }
415
    };
416
 
417
    /**
418
     * Change to the next user in the grading list.
419
     *
420
     * @param {Event} e
421
     * @param {Boolean} saved Has the form already been saved? Skips checking for changes if true.
422
     */
423
    GradingNavigation.prototype._handleNextUser = function(e, saved) {
424
        e.preventDefault();
425
        var select = this._region.find('[data-action=change-user]');
426
        var currentUserId = select.attr('data-selected');
427
        var i = 0;
428
        var currentIndex = 0;
429
 
430
        for (i = 0; i < this._filteredUsers.length; i++) {
431
            if (this._filteredUsers[i].id == currentUserId) {
432
                currentIndex = i;
433
                break;
434
            }
435
        }
436
 
437
        var count = this._filteredUsers.length;
438
        var newIndex = (currentIndex + 1) % count;
439
 
440
        if (saved && count) {
441
            // If we've already saved the grade, skip checking if we've made any changes.
442
            var userid = this._filteredUsers[newIndex].id;
443
            var useridnumber = parseInt(userid, 10);
444
            select.attr('data-selected', userid);
445
            if (!isNaN(useridnumber) && useridnumber > 0) {
446
                $(document).trigger('user-changed', userid);
447
            }
448
        } else if (count) {
449
            this._selectUserById(this._filteredUsers[newIndex].id);
450
        }
451
    };
452
 
453
    /**
454
     * Set count string. This method only sets the value for the last time it was ever called to deal
455
     * with promises that return in a non-predictable order.
456
     *
457
     * @private
458
     * @method _setCountString
459
     * @param {Number} x
460
     * @param {Number} y
461
     */
462
    GradingNavigation.prototype._setCountString = function(x, y) {
463
        var updateNumber = 0;
464
        this._lastXofYUpdate++;
465
        updateNumber = this._lastXofYUpdate;
466
 
467
        var param = {x: x, y: y};
468
        str.get_string('xofy', 'mod_assign', param).done(function(s) {
469
            if (updateNumber == this._lastXofYUpdate) {
470
                this._region.find('[data-region="user-count-summary"]').text(s);
471
            }
472
        }.bind(this)).fail(notification.exception);
473
    };
474
 
475
    /**
476
     * Rebuild the x of y string.
477
     *
478
     * @private
479
     * @method _refreshCount
480
     */
481
    GradingNavigation.prototype._refreshCount = function() {
482
        var select = this._region.find('[data-action=change-user]');
483
        var userid = select.attr('data-selected');
484
        var i = 0;
485
        var currentIndex = 0;
486
 
487
        if (isNaN(userid) || userid <= 0) {
488
            this._region.find('[data-region="user-count"]').hide();
489
        } else {
490
            this._region.find('[data-region="user-count"]').show();
491
 
492
            for (i = 0; i < this._filteredUsers.length; i++) {
493
                if (this._filteredUsers[i].id == userid) {
494
                    currentIndex = i;
495
                    break;
496
                }
497
            }
498
            var count = this._filteredUsers.length;
499
            if (count) {
500
                currentIndex += 1;
501
            }
502
            this._setCountString(currentIndex, count);
503
            // Update window URL
504
            if (currentIndex > 0) {
505
                var url = new URL(window.location);
506
                if (parseInt(url.searchParams.get('blindid')) > 0) {
507
                    var newid = this._filteredUsers[currentIndex - 1].recordid;
508
                    url.searchParams.set('blindid', newid);
509
                } else {
510
                    url.searchParams.set('userid', userid);
511
                }
512
                // We do this so a browser refresh will return to the same user.
513
                window.history.replaceState({}, "", url);
514
            }
515
        }
516
    };
517
 
518
    /**
519
     * Respond to a user-changed event by updating the selector.
520
     *
521
     * @private
522
     * @method _refreshSelector
523
     * @param {Event} event
524
     * @param {String} userid
525
     */
526
    GradingNavigation.prototype._refreshSelector = function(event, userid) {
527
        var select = this._region.find('[data-action=change-user]');
528
        userid = parseInt(userid, 10);
529
 
530
        if (!isNaN(userid) && userid > 0) {
531
            select.attr('data-selected', userid);
532
        }
533
        this._refreshCount();
534
    };
535
 
536
    /**
537
     * Trigger the next user event depending on the number of filtered users
538
     *
539
     * @private
540
     * @method _triggerNextUserEvent
541
     */
542
    GradingNavigation.prototype._triggerNextUserEvent = function() {
543
        if (this._filteredUsers.length > 1) {
544
            $(document).trigger('next-user', {nextUserId: null, nextUser: true});
545
        } else {
546
            $(document).trigger('next-user', {nextUser: false});
547
        }
548
    };
549
 
550
    /**
551
     * Change to a different user in the grading list.
552
     *
553
     * @private
554
     * @method _handleChangeUser
555
     */
556
    GradingNavigation.prototype._handleChangeUser = function() {
557
        var select = this._region.find('[data-action=change-user]');
558
        var userid = parseInt(select.val(), 10);
559
 
560
        if (this._isLoading) {
561
            return;
562
        }
563
        if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
564
            // Form has changes, so we need to confirm before switching users.
565
            str.get_strings([
566
                {key: 'unsavedchanges', component: 'mod_assign'},
567
                {key: 'unsavedchangesquestion', component: 'mod_assign'},
568
                {key: 'saveandcontinue', component: 'mod_assign'},
569
                {key: 'cancel', component: 'core'},
570
            ]).done(function(strs) {
571
                notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
572
                    $(document).trigger('save-changes', userid);
573
                });
574
            });
575
        } else {
576
            if (!isNaN(userid) && userid > 0) {
577
                select.attr('data-selected', userid);
578
 
579
                $(document).trigger('user-changed', userid);
580
            }
581
        }
582
    };
583
 
584
    return GradingNavigation;
585
});