Proyectos de Subversion Moodle

Rev

Ir a la última revisión | | 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();
133
    }
134
 
135
    /**
136
     * Load the file hanlders promise.
137
     */
138
    _loadFileHandlers() {
139
        // Load the course file extensions.
140
        this._fileHandlersPromise = new Promise((resolve) => {
141
            if (!this.isEditing) {
142
                resolve([]);
143
                return;
144
            }
145
            // Check the cache.
146
            const handlersCacheKey = `course/${this.courseId}/fileHandlers`;
147
 
148
            const cacheValue = Storage.get(handlersCacheKey);
149
            if (cacheValue) {
150
                try {
151
                    const cachedHandlers = JSON.parse(cacheValue);
152
                    resolve(cachedHandlers);
153
                    return;
154
                } catch (error) {
155
                    log.error("ERROR PARSING CACHED FILE HANDLERS");
156
                }
157
            }
158
            // Call file handlers webservice.
159
            ajax.call([{
160
                methodname: 'core_courseformat_file_handlers',
161
                args: {
162
                    courseid: this.courseId,
163
                }
164
            }])[0].then((handlers) => {
165
                Storage.set(handlersCacheKey, JSON.stringify(handlers));
166
                resolve(handlers);
167
                return;
168
            }).catch(error => {
169
                log.error(error);
170
                resolve([]);
171
                return;
172
            });
173
        });
174
    }
175
 
176
    /**
177
     * Setup the current view settings
178
     *
179
     * @param {Object} setup format, page and course settings
180
     * @param {boolean} setup.editing if the page is in edit mode
181
     * @param {boolean} setup.supportscomponents if the format supports components for content
182
     * @param {string} setup.cacherev the backend cached state revision
183
     * @param {Array} setup.overriddenStrings optional overridden strings
184
     */
185
    setViewFormat(setup) {
186
        this._editing = setup.editing ?? false;
187
        this._supportscomponents = setup.supportscomponents ?? false;
188
        const overriddenStrings = setup.overriddenStrings ?? [];
189
        this._overriddenStrings = overriddenStrings.reduce(
190
            (indexed, currentValue) => indexed.set(currentValue.key, currentValue),
191
            new Map()
192
        );
193
    }
194
 
195
    /**
196
     * Execute a get string for a possible format overriden editor string.
197
     *
198
     * Return the proper getString promise for an editor string using the core_courseformat
199
     * of the format_PLUGINNAME compoment depending on the current view format setup.
200
     * @param {String} key the string key
201
     * @param {string|undefined} param The param for variable expansion in the string.
202
     * @returns {Promise<String>} a getString promise
203
     */
204
    getFormatString(key, param) {
205
        if (this._overriddenStrings.has(key)) {
206
            const override = this._overriddenStrings.get(key);
207
            return getString(key, override.component ?? 'core_courseformat', param);
208
        }
209
        // All format overridable strings are from core_courseformat lang file.
210
        return getString(key, 'core_courseformat', param);
211
    }
212
 
213
    /**
214
     * Load the current course state from the server.
215
     *
216
     * @returns {Object} the current course state
217
     */
218
    async getServerCourseState() {
219
        const courseState = await ajax.call([{
220
            methodname: 'core_courseformat_get_state',
221
            args: {
222
                courseid: this.courseId,
223
            }
224
        }])[0];
225
 
226
        const stateData = JSON.parse(courseState);
227
 
228
        return {
229
            course: {},
230
            section: [],
231
            cm: [],
232
            ...stateData,
233
        };
234
    }
235
 
236
    /**
237
     * Return the current edit mode.
238
     *
239
     * Components should use this method to check if edit mode is active.
240
     *
241
     * @return {boolean} if edit is enabled
242
     */
243
    get isEditing() {
244
        return this._editing ?? false;
245
    }
246
 
247
    /**
248
     * Return a data exporter to transform state part into mustache contexts.
249
     *
250
     * @return {Exporter} the exporter class
251
     */
252
    getExporter() {
253
        return new Exporter(this);
254
    }
255
 
256
    /**
257
     * Return if the current course support components to refresh the content.
258
     *
259
     * @returns {boolean} if the current content support components
260
     */
261
    get supportComponents() {
262
        return this._supportscomponents ?? false;
263
    }
264
 
265
    /**
266
     * Return the course file handlers promise.
267
     * @returns {Promise} the promise for file handlers.
268
     */
269
    async getFileHandlersPromise() {
270
        return this._fileHandlersPromise ?? [];
271
    }
272
 
273
    /**
274
     * Upload a file list to the course.
275
     *
276
     * This method is a wrapper to the course file uploader.
277
     *
278
     * @param {number} sectionId the section id
279
     * @param {number} sectionNum the section number
280
     * @param {Array} files and array of files
281
     * @return {Promise} the file queue promise
282
     */
283
    uploadFiles(sectionId, sectionNum, files) {
284
        return uploadFilesToCourse(this.courseId, sectionId, sectionNum, files);
285
    }
286
 
287
    /**
288
     * Get a value from the course editor static storage if any.
289
     *
290
     * The course editor static storage uses the sessionStorage to store values from the
291
     * components. This is used to prevent unnecesary template loadings on every page. However,
292
     * the storage does not work if no sessionStorage can be used (in debug mode for example),
293
     * if the page is in editing mode or if the initial state change from the last page.
294
     *
295
     * @param {string} key the key to get
296
     * @return {boolean|string} the storage value or false if cannot be loaded
297
     */
298
    getStorageValue(key) {
299
        if (this.isEditing || !this.stateKey) {
300
            return false;
301
        }
302
        const dataJson = Storage.get(`course/${this.courseId}/${key}`);
303
        if (!dataJson) {
304
            return false;
305
        }
306
        // Check the stateKey.
307
        try {
308
            const data = JSON.parse(dataJson);
309
            if (data?.stateKey !== this.stateKey) {
310
                return false;
311
            }
312
            return data.value;
313
        } catch (error) {
314
            return false;
315
        }
316
    }
317
 
318
    /**
319
     * Stores a value into the course editor static storage if available
320
     *
321
     * @param {String} key the key to store
322
     * @param {*} value the value to store (must be compatible with JSON,stringify)
323
     * @returns {boolean} true if the value is stored
324
     */
325
    setStorageValue(key, value) {
326
        // Values cannot be stored on edit mode.
327
        if (this.isEditing) {
328
            return false;
329
        }
330
        const data = {
331
            stateKey: this.stateKey,
332
            value,
333
        };
334
        return Storage.set(`course/${this.courseId}/${key}`, JSON.stringify(data));
335
    }
336
 
337
    /**
338
     * Convert a file dragging event into a proper dragging file list.
339
     * @param {DataTransfer} dataTransfer the event to convert
340
     * @return {Array} of file list info.
341
     */
342
    getFilesDraggableData(dataTransfer) {
343
        const exporter = this.getExporter();
344
        return exporter.fileDraggableData(this.state, dataTransfer);
345
    }
346
 
347
    /**
348
     * Dispatch a change in the state.
349
     *
350
     * Usually reactive modules throw an error directly to the components when something
351
     * goes wrong. However, course editor can directly display a notification.
352
     *
353
     * @method dispatch
354
     * @param {mixed} args any number of params the mutation needs.
355
     */
356
    async dispatch(...args) {
357
        try {
358
            await super.dispatch(...args);
359
        } catch (error) {
360
            // Display error modal.
361
            notification.exception(error);
362
            // Force unlock all elements.
363
            super.dispatch('unlockAll');
364
        }
365
    }
366
}