Proyectos de Subversion Moodle

Rev

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