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
 * Manager for the accessreview block.
18
 *
19
 * @module block_accessreview/module
20
 * @author      Max Larkin <max@brickfieldlabs.ie>
21
 * @copyright   2020 Brickfield Education Labs <max@brickfieldlabs.ie>
22
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
import {call as fetchMany} from 'core/ajax';
26
import * as Templates from 'core/templates';
27
import {exception as displayError} from 'core/notification';
28
 
29
/**
30
 * The number of colours used to represent the heatmap. (Indexed on 0.)
31
 * @type {number}
32
 */
33
const numColours = 2;
34
 
35
/**
36
 * The toggle state of the heatmap.
37
 * @type {boolean}
38
 */
39
let toggleState = true;
40
 
41
/**
42
 * Renders the HTML template onto a particular HTML element.
43
 * @param {HTMLElement} element The element to attach the HTML to.
44
 * @param {number} errorCount The number of errors on this module/section.
45
 * @param {number} checkCount The number of checks triggered on this module/section.
46
 * @param {String} displayFormat
47
 * @param {Number} minViews
48
 * @param {Number} viewDelta
49
 * @returns {Promise}
50
 */
51
const renderTemplate = (element, errorCount, checkCount, displayFormat, minViews, viewDelta) => {
52
    // Calculate a weight?
53
    const weight = parseInt((errorCount - minViews) / viewDelta * numColours);
54
 
55
    const context = {
56
        resultPassed: !errorCount,
57
        classList: '',
58
        passRate: {
59
            errorCount,
60
            checkCount,
61
            failureRate: Math.round(errorCount / checkCount * 100),
62
        },
63
    };
64
 
65
    if (!element) {
66
        return Promise.resolve();
67
    }
68
 
69
    const elementClassList = ['block_accessreview'];
70
    if (context.resultPassed) {
71
        elementClassList.push('block_accessreview_success');
72
    } else if (weight) {
73
        elementClassList.push('block_accessreview_danger');
74
    } else {
75
        elementClassList.push('block_accessreview_warning');
76
    }
77
 
78
    const showIcons = (displayFormat == 'showicons') || (displayFormat == 'showboth');
79
    const showBackground = (displayFormat == 'showbackground') || (displayFormat == 'showboth');
80
 
81
    if (showBackground && !showIcons) {
82
        // Only the background is displayed.
83
        // No need to display the template.
84
        // Note: The case where both the background and icons are shown is handled later to avoid jankiness.
85
        element.classList.add(...elementClassList, 'alert');
86
 
87
        return Promise.resolve();
88
    }
89
 
90
    if (showIcons && !showBackground) {
91
        context.classList = elementClassList.join(' ');
92
    }
93
 
94
    // The icons are displayed either with, or without, the background.
95
    return Templates.renderForPromise('block_accessreview/status', context)
96
    .then(({html, js}) => {
97
        Templates.appendNodeContents(element, html, js);
98
 
99
        if (showBackground) {
100
            element.classList.add(...elementClassList, 'alert');
101
        }
102
 
103
        return;
104
    })
105
    .catch();
106
};
107
 
108
/**
109
 * Applies the template to all sections and modules on the course page.
110
 *
111
 * @param {Number} courseId
112
 * @param {String} displayFormat
113
 * @param {Boolean} updatePreference
114
 * @returns {Promise}
115
 */
116
const showAccessMap = (courseId, displayFormat, updatePreference = false) => {
117
    // Get error data.
118
    return Promise.all(fetchReviewData(courseId, updatePreference))
119
    .then(([sectionData, moduleData]) => {
120
        // Get total data.
121
        const {minViews, viewDelta} = getErrorTotals(sectionData, moduleData);
122
 
123
        sectionData.forEach(section => {
124
            const element = document.querySelector(`#section-${section.section} .summary`);
125
            if (!element) {
126
                return;
127
            }
128
 
129
            renderTemplate(element, section.numerrors, section.numchecks, displayFormat, minViews, viewDelta);
130
        });
131
 
132
        moduleData.forEach(module => {
133
            const element = document.getElementById(`module-${module.cmid}`);
134
            if (!element) {
135
                return;
136
            }
137
 
138
            renderTemplate(element, module.numerrors, module.numchecks, displayFormat, minViews, viewDelta);
139
        });
140
 
141
        // Change the icon display.
142
        document.querySelector('.icon-accessmap').classList.remove(...['fa-eye-slash']);
143
        document.querySelector('.icon-accessmap').classList.add(...['fa-eye']);
144
 
145
        return {
146
            sectionData,
147
            moduleData,
148
        };
149
    })
150
    .catch(displayError);
151
};
152
 
153
 
154
/**
155
 * Hides or removes the templates from the HTML of the current page.
156
 *
157
 * @param {Boolean} updatePreference
158
 */
159
const hideAccessMap = (updatePreference = false) => {
160
    // Removes the added elements.
161
    document.querySelectorAll('.block_accessreview_view').forEach(node => node.remove());
162
 
163
    const classList = [
164
        'block_accessreview',
165
        'block_accessreview_success',
166
        'block_accessreview_warning',
167
        'block_accessreview_danger',
168
        'block_accessreview_view',
169
        'alert',
170
    ];
171
 
172
    // Removes the added classes.
173
    document.querySelectorAll('.block_accessreview').forEach(node => node.classList.remove(...classList));
174
 
175
    if (updatePreference) {
176
        setToggleStatePreference(false);
177
    }
178
 
179
    // Change the icon display.
180
    document.querySelector('.icon-accessmap').classList.remove(...['fa-eye']);
181
    document.querySelector('.icon-accessmap').classList.add(...['fa-eye-slash']);
182
};
183
 
184
 
185
/**
186
 * Toggles the heatmap on/off.
187
 *
188
 * @param {Number} courseId
189
 * @param {String} displayFormat
190
 */
191
const toggleAccessMap = (courseId, displayFormat) => {
192
    toggleState = !toggleState;
193
    if (!toggleState) {
194
        hideAccessMap(true);
195
    } else {
196
        showAccessMap(courseId, displayFormat, true);
197
    }
198
};
199
 
200
/**
201
 * Parses information on the errors, generating the min, max and totals.
202
 *
203
 * @param {Object[]} sectionData The error data for course sections.
204
 * @param {Object[]} moduleData The error data for course modules.
205
 * @returns {Object} An object representing the extra error information.
206
 */
207
const getErrorTotals = (sectionData, moduleData) => {
208
    const totals = {
209
        totalErrors: 0,
210
        totalUsers: 0,
211
        minViews: 0,
212
        maxViews: 0,
213
        viewDelta: 0,
214
    };
215
 
216
    [].concat(sectionData, moduleData).forEach(item => {
217
        totals.totalErrors += item.numerrors;
218
        if (item.numerrors < totals.minViews) {
219
            totals.minViews = item.numerrors;
220
        }
221
 
222
        if (item.numerrors > totals.maxViews) {
223
            totals.maxViews = item.numerrors;
224
        }
225
        totals.totalUsers += item.numchecks;
226
    });
227
 
228
    totals.viewDelta = totals.maxViews - totals.minViews + 1;
229
 
230
    return totals;
231
};
232
 
233
const registerEventListeners = (courseId, displayFormat) => {
234
    document.addEventListener('click', e => {
235
        if (e.target.closest('#toggle-accessmap')) {
236
            e.preventDefault();
237
            toggleAccessMap(courseId, displayFormat);
238
        }
239
    });
240
};
241
 
242
/**
243
 * Set the user preference for the toggle value.
244
 *
245
 * @param   {Boolean} toggleState
246
 * @returns {Promise}
247
 */
248
const getTogglePreferenceParams = toggleState => {
249
    return {
250
        methodname: 'core_user_update_user_preferences',
251
        args: {
252
            preferences: [{
253
                type: 'block_accessreviewtogglestate',
254
                value: toggleState,
255
            }],
256
        }
257
    };
258
};
259
 
260
const setToggleStatePreference = toggleState => fetchMany([getTogglePreferenceParams(toggleState)]);
261
 
262
/**
263
 * Fetch the review data.
264
 *
265
 * @param   {Number} courseid
266
 * @param {Boolean} updatePreference
267
 * @returns {Promise[]}
268
 */
269
const fetchReviewData = (courseid, updatePreference = false) => {
270
    const calls = [
271
        {
272
            methodname: 'block_accessreview_get_section_data',
273
            args: {courseid}
274
        },
275
        {
276
            methodname: 'block_accessreview_get_module_data',
277
            args: {courseid}
278
        },
279
    ];
280
 
281
    if (updatePreference) {
282
        calls.push(getTogglePreferenceParams(true));
283
    }
284
 
285
    return fetchMany(calls);
286
};
287
 
288
/**
289
 * Setting up the access review module.
290
 * @param {number} toggled A number represnting the state of the review toggle.
291
 * @param {string} displayFormat A string representing the display format for icons.
292
 * @param {number} courseId The course ID.
293
 */
294
export const init = (toggled, displayFormat, courseId) => {
295
    // Settings consts.
296
    toggleState = toggled == 1;
297
 
298
    if (toggleState) {
299
        showAccessMap(courseId, displayFormat);
300
    }
301
 
302
    registerEventListeners(courseId, displayFormat);
303
};