Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | 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
 
1441 ariadna 16
import Config from 'core/config';
1 efrain 17
import {getString} from 'core/str';
18
import {Reactive} from 'core/reactive';
19
import notification from 'core/notification';
20
import Exporter from 'core_courseformat/local/courseeditor/exporter';
21
import log from 'core/log';
22
import ajax from 'core/ajax';
23
import * as Storage from 'core/sessionstorage';
24
import {uploadFilesToCourse} from 'core_courseformat/local/courseeditor/fileuploader';
25
 
26
/**
27
 * Main course editor module.
28
 *
29
 * All formats can register new components on this object to create new reactive
30
 * UI components that watch the current course state.
31
 *
32
 * @module     core_courseformat/local/courseeditor/courseeditor
33
 * @class     core_courseformat/local/courseeditor/courseeditor
34
 * @copyright  2021 Ferran Recio <ferran@moodle.com>
35
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36
 */
37
export default class extends Reactive {
38
 
39
    /**
40
     * The current state cache key
41
     *
42
     * The state cache is considered dirty if the state changes from the last page or
43
     * if the page has editing mode on.
44
     *
45
     * @attribute stateKey
46
     * @type number|null
47
     * @default 1
48
     * @package
49
     */
50
    stateKey = 1;
51
 
52
    /**
1441 ariadna 53
     * The section number of the current page
1 efrain 54
     * @attribute sectionReturn
55
     * @type number
56
     * @default null
57
     */
58
    sectionReturn = null;
59
 
60
    /**
1441 ariadna 61
     * The section ID of the current page
62
     * @attribute pageSectionId
63
     * @type number
64
     * @default null
65
     */
66
    pageSectionId = null;
67
 
68
    /**
1 efrain 69
     * Set up the course editor when the page is ready.
70
     *
71
     * The course can only be loaded once per instance. Otherwise an error is thrown.
72
     *
73
     * The backend can inform the module of the current state key. This key changes every time some
74
     * update in the course affect the current user state. Some examples are:
75
     *  - The course content has been edited
76
     *  - The user marks some activity as completed
77
     *  - The user collapses or uncollapses a section (it is stored as a user preference)
78
     *
79
     * @param {number} courseId course id
80
     * @param {string} serverStateKey the current backend course cache reference
81
     */
82
    async loadCourse(courseId, serverStateKey) {
83
 
84
        if (this.courseId) {
85
            throw new Error(`Cannot load ${courseId}, course already loaded with id ${this.courseId}`);
86
        }
87
 
88
        if (!serverStateKey) {
89
            // The server state key is not provided, we use a invalid statekey to force reloading.
90
            serverStateKey = `invalidStateKey_${Date.now()}`;
91
        }
92
 
93
        // Default view format setup.
94
        this._editing = false;
95
        this._supportscomponents = false;
96
        this._fileHandlers = null;
97
 
98
        this.courseId = courseId;
99
 
100
        let stateData;
101
 
102
        const storeStateKey = Storage.get(`course/${courseId}/stateKey`);
103
        try {
104
            // Check if the backend state key is the same we have in our session storage.
105
            if (!this.isEditing && serverStateKey == storeStateKey) {
106
                stateData = JSON.parse(Storage.get(`course/${courseId}/staticState`));
107
            }
108
            if (!stateData) {
109
                stateData = await this.getServerCourseState();
110
            }
111
 
112
        } catch (error) {
113
            log.error("EXCEPTION RAISED WHILE INIT COURSE EDITOR");
114
            log.error(error);
115
            return;
116
        }
117
 
118
        // The bulk editing only applies to the frontend and the state data is not created in the backend.
119
        stateData.bulk = {
120
            enabled: false,
121
            selectedType: '',
122
            selection: [],
123
        };
124
 
125
        this.setInitialState(stateData);
126
 
127
        // In editing mode, the session cache is considered dirty always.
128
        if (this.isEditing) {
129
            this.stateKey = null;
130
        } else {
131
            // Check if the last state is the same as the cached one.
132
            const newState = JSON.stringify(stateData);
133
            const previousState = Storage.get(`course/${courseId}/staticState`);
134
            if (previousState !== newState || storeStateKey !== serverStateKey) {
135
                Storage.set(`course/${courseId}/staticState`, newState);
136
                Storage.set(`course/${courseId}/stateKey`, stateData?.course?.statekey ?? serverStateKey);
137
            }
138
            this.stateKey = Storage.get(`course/${courseId}/stateKey`);
139
        }
140
 
141
        this._loadFileHandlers();
11 efrain 142
 
143
        this._pageAnchorCmInfo = this._scanPageAnchorCmInfo();
1 efrain 144
    }
145
 
146
    /**
147
     * Load the file hanlders promise.
148
     */
149
    _loadFileHandlers() {
150
        // Load the course file extensions.
151
        this._fileHandlersPromise = new Promise((resolve) => {
152
            if (!this.isEditing) {
153
                resolve([]);
154
                return;
155
            }
156
            // Check the cache.
157
            const handlersCacheKey = `course/${this.courseId}/fileHandlers`;
158
 
159
            const cacheValue = Storage.get(handlersCacheKey);
160
            if (cacheValue) {
161
                try {
162
                    const cachedHandlers = JSON.parse(cacheValue);
163
                    resolve(cachedHandlers);
164
                    return;
165
                } catch (error) {
166
                    log.error("ERROR PARSING CACHED FILE HANDLERS");
167
                }
168
            }
169
            // Call file handlers webservice.
170
            ajax.call([{
171
                methodname: 'core_courseformat_file_handlers',
172
                args: {
173
                    courseid: this.courseId,
174
                }
175
            }])[0].then((handlers) => {
176
                Storage.set(handlersCacheKey, JSON.stringify(handlers));
177
                resolve(handlers);
178
                return;
179
            }).catch(error => {
180
                log.error(error);
181
                resolve([]);
182
                return;
183
            });
184
        });
185
    }
186
 
187
    /**
188
     * Setup the current view settings
189
     *
190
     * @param {Object} setup format, page and course settings
191
     * @param {boolean} setup.editing if the page is in edit mode
192
     * @param {boolean} setup.supportscomponents if the format supports components for content
193
     * @param {string} setup.cacherev the backend cached state revision
194
     * @param {Array} setup.overriddenStrings optional overridden strings
195
     */
196
    setViewFormat(setup) {
197
        this._editing = setup.editing ?? false;
198
        this._supportscomponents = setup.supportscomponents ?? false;
199
        const overriddenStrings = setup.overriddenStrings ?? [];
200
        this._overriddenStrings = overriddenStrings.reduce(
201
            (indexed, currentValue) => indexed.set(currentValue.key, currentValue),
202
            new Map()
203
        );
204
    }
205
 
206
    /**
207
     * Execute a get string for a possible format overriden editor string.
208
     *
209
     * Return the proper getString promise for an editor string using the core_courseformat
210
     * of the format_PLUGINNAME compoment depending on the current view format setup.
211
     * @param {String} key the string key
212
     * @param {string|undefined} param The param for variable expansion in the string.
213
     * @returns {Promise<String>} a getString promise
214
     */
215
    getFormatString(key, param) {
216
        if (this._overriddenStrings.has(key)) {
217
            const override = this._overriddenStrings.get(key);
218
            return getString(key, override.component ?? 'core_courseformat', param);
219
        }
220
        // All format overridable strings are from core_courseformat lang file.
221
        return getString(key, 'core_courseformat', param);
222
    }
223
 
224
    /**
225
     * Load the current course state from the server.
226
     *
227
     * @returns {Object} the current course state
228
     */
229
    async getServerCourseState() {
1441 ariadna 230
        // Only logged users can get the course state. Filtering here will prevent unnecessary
231
        // calls to the server and login page redirects. Especially for home activities with
232
        // guest access.
233
        if (Config.userId == 0) {
234
            return {
235
                course: {},
236
                section: [],
237
                cm: [],
238
            };
239
        }
1 efrain 240
        const courseState = await ajax.call([{
241
            methodname: 'core_courseformat_get_state',
242
            args: {
243
                courseid: this.courseId,
244
            }
245
        }])[0];
246
 
247
        const stateData = JSON.parse(courseState);
248
 
249
        return {
250
            course: {},
251
            section: [],
252
            cm: [],
253
            ...stateData,
254
        };
255
    }
256
 
257
    /**
258
     * Return the current edit mode.
259
     *
260
     * Components should use this method to check if edit mode is active.
261
     *
262
     * @return {boolean} if edit is enabled
263
     */
264
    get isEditing() {
265
        return this._editing ?? false;
266
    }
267
 
268
    /**
269
     * Return a data exporter to transform state part into mustache contexts.
270
     *
271
     * @return {Exporter} the exporter class
272
     */
273
    getExporter() {
274
        return new Exporter(this);
275
    }
276
 
277
    /**
278
     * Return if the current course support components to refresh the content.
279
     *
280
     * @returns {boolean} if the current content support components
281
     */
282
    get supportComponents() {
283
        return this._supportscomponents ?? false;
284
    }
285
 
286
    /**
287
     * Return the course file handlers promise.
288
     * @returns {Promise} the promise for file handlers.
289
     */
290
    async getFileHandlersPromise() {
291
        return this._fileHandlersPromise ?? [];
292
    }
293
 
294
    /**
295
     * Upload a file list to the course.
296
     *
297
     * This method is a wrapper to the course file uploader.
298
     *
299
     * @param {number} sectionId the section id
300
     * @param {number} sectionNum the section number
301
     * @param {Array} files and array of files
302
     * @return {Promise} the file queue promise
303
     */
304
    uploadFiles(sectionId, sectionNum, files) {
305
        return uploadFilesToCourse(this.courseId, sectionId, sectionNum, files);
306
    }
307
 
308
    /**
309
     * Get a value from the course editor static storage if any.
310
     *
311
     * The course editor static storage uses the sessionStorage to store values from the
312
     * components. This is used to prevent unnecesary template loadings on every page. However,
313
     * the storage does not work if no sessionStorage can be used (in debug mode for example),
314
     * if the page is in editing mode or if the initial state change from the last page.
315
     *
316
     * @param {string} key the key to get
317
     * @return {boolean|string} the storage value or false if cannot be loaded
318
     */
319
    getStorageValue(key) {
320
        if (this.isEditing || !this.stateKey) {
321
            return false;
322
        }
323
        const dataJson = Storage.get(`course/${this.courseId}/${key}`);
324
        if (!dataJson) {
325
            return false;
326
        }
327
        // Check the stateKey.
328
        try {
329
            const data = JSON.parse(dataJson);
330
            if (data?.stateKey !== this.stateKey) {
331
                return false;
332
            }
333
            return data.value;
334
        } catch (error) {
335
            return false;
336
        }
337
    }
338
 
339
    /**
340
     * Stores a value into the course editor static storage if available
341
     *
342
     * @param {String} key the key to store
343
     * @param {*} value the value to store (must be compatible with JSON,stringify)
344
     * @returns {boolean} true if the value is stored
345
     */
346
    setStorageValue(key, value) {
347
        // Values cannot be stored on edit mode.
348
        if (this.isEditing) {
349
            return false;
350
        }
351
        const data = {
352
            stateKey: this.stateKey,
353
            value,
354
        };
355
        return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));
356
    }
357
 
358
    /**
359
     * Convert a file dragging event into a proper dragging file list.
360
     * @param {DataTransfer} dataTransfer the event to convert
361
     * @return {Array} of file list info.
362
     */
363
    getFilesDraggableData(dataTransfer) {
364
        const exporter = this.getExporter();
365
        return exporter.fileDraggableData(this.state, dataTransfer);
366
    }
367
 
368
    /**
369
     * Dispatch a change in the state.
370
     *
371
     * Usually reactive modules throw an error directly to the components when something
372
     * goes wrong. However, course editor can directly display a notification.
373
     *
374
     * @method dispatch
375
     * @param {mixed} args any number of params the mutation needs.
376
     */
377
    async dispatch(...args) {
378
        try {
379
            await super.dispatch(...args);
380
        } catch (error) {
381
            // Display error modal.
382
            notification.exception(error);
383
            // Force unlock all elements.
384
            super.dispatch('unlockAll');
385
        }
386
    }
11 efrain 387
 
388
    /**
389
     * Calculate the cm info from the current page anchor.
390
     *
391
     * @returns {Object|null} the cm info or null if not found.
392
     */
393
    _scanPageAnchorCmInfo() {
394
        const anchor = new URL(window.location.href).hash;
395
        if (!anchor.startsWith('#module-')) {
396
            return null;
397
        }
398
        // The anchor is always #module-CMID.
399
        const cmid = anchor.split('-')[1];
400
        return this.stateManager.get('cm', parseInt(cmid));
401
    }
402
 
403
    /**
404
     * Return the current page anchor cm info.
405
     */
406
    getPageAnchorCmInfo() {
407
        return this._pageAnchorCmInfo;
408
    }
1 efrain 409
}