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
 
16
/**
17
 * Contain the logic for the add random question modal.
18
 *
19
 * @module     mod_quiz/modal_add_random_question
20
 * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import $ from 'jquery';
25
import Modal from './add_question_modal';
26
import * as Notification from 'core/notification';
27
import * as Fragment from 'core/fragment';
28
import * as Templates from 'core/templates';
29
import * as FormChangeChecker from 'core_form/changechecker';
30
import {call as fetchMany} from 'core/ajax';
31
import Pending from 'core/pending';
32
 
33
const SELECTORS = {
1441 ariadna 34
    ANCHOR: 'a[href]',
1 efrain 35
    EXISTING_CATEGORY_CONTAINER: '[data-region="existing-category-container"]',
36
    EXISTING_CATEGORY_TAB: '#id_existingcategoryheader',
37
    NEW_CATEGORY_CONTAINER: '[data-region="new-category-container"]',
38
    NEW_CATEGORY_TAB: '#id_newcategoryheader',
39
    TAB_CONTENT: '[data-region="tab-content"]',
40
    ADD_ON_PAGE_FORM_ELEMENT: '[name="addonpage"]',
41
    ADD_RANDOM_BUTTON: 'input[type="submit"][name="addrandom"]',
42
    ADD_NEW_CATEGORY_BUTTON: 'input[type="submit"][name="newcategory"]',
1441 ariadna 43
    SUBMIT_BUTTON_ELEMENT: 'input[type="submit"][name="addrandom"], '
44
        + 'input[type="submit"][name="newcategory"], '
45
        + 'input[type="submit"][name="update"]',
1 efrain 46
    FORM_HEADER: 'legend',
47
    SELECT_NUMBER_TO_ADD: '#menurandomcount',
48
    NEW_CATEGORY_ELEMENT: '#categoryname',
49
    PARENT_CATEGORY_ELEMENT: '#parentcategory',
50
    FILTER_CONDITION_ELEMENT: '[data-filtercondition]',
51
    FORM_ELEMENT: '#add_random_question_form',
52
    MESSAGE_INPUT: '[name="message"]',
1441 ariadna 53
    SWITCH_TO_OTHER_BANK: 'button[data-action="switch-question-bank"]',
54
    NEW_BANKMOD_ID: 'data-newmodid',
55
    BANK_SEARCH: '#searchbanks',
56
    GO_BACK_BUTTON: 'button[data-action="go-back"]',
57
    UPDATE_FILTER_BUTTON: 'input[type="submit"][name="update"]',
1 efrain 58
};
59
 
60
export default class ModalAddRandomQuestion extends Modal {
61
    static TYPE = 'mod_quiz-quiz-add-random-question';
62
    static TEMPLATE = 'mod_quiz/modal_add_random_question';
63
 
64
    /**
65
     * Create the add random question modal.
66
     *
67
     * @param  {Number} contextId Current context id.
1441 ariadna 68
     * @param  {Number} bankCmId Current question bank course module id.
1 efrain 69
     * @param  {string} category Category id and category context id comma separated.
70
     * @param  {string} returnUrl URL to return to after form submission.
1441 ariadna 71
     * @param  {Number} quizCmId Current quiz course module id.
1 efrain 72
     * @param  {boolean} showNewCategory Display the New category tab when selecting random questions.
73
     */
1441 ariadna 74
    static init(
75
        contextId,
76
        bankCmId,
77
        category,
78
        returnUrl,
79
        quizCmId,
80
        showNewCategory = true
81
    ) {
82
        const selector = '.menu [data-action="addarandomquestion"], [data-action="editrandomquestion"]';
1 efrain 83
        document.addEventListener('click', (e) => {
84
            const trigger = e.target.closest(selector);
85
            if (!trigger) {
86
                return;
87
            }
88
            e.preventDefault();
89
 
1441 ariadna 90
            if (trigger.dataset.slotid) {
91
                showNewCategory = false;
92
            }
93
 
1 efrain 94
            ModalAddRandomQuestion.create({
95
                contextId,
1441 ariadna 96
                bankCmId,
1 efrain 97
                category,
98
                returnUrl,
1441 ariadna 99
                quizCmId,
100
                showNewCategory,
1 efrain 101
                title: trigger.dataset.header,
102
                addOnPage: trigger.dataset.addonpage,
1441 ariadna 103
                slotId: trigger.dataset.slotid,
1 efrain 104
                templateContext: {
105
                    hidden: showNewCategory,
106
                },
107
            });
108
        });
109
    }
110
 
111
    /**
112
     * Constructor for the Modal.
113
     *
114
     * @param {object} root The root jQuery element for the modal
115
     */
116
    constructor(root) {
117
        super(root);
118
        this.category = null;
119
        this.returnUrl = null;
1441 ariadna 120
        this.quizCmId = null;
1 efrain 121
        this.loadedForm = false;
1441 ariadna 122
        this.slotId = 0;
123
        this.savedFilterCondition = null;
1 efrain 124
    }
125
 
126
    configure(modalConfig) {
11 efrain 127
        modalConfig.removeOnClose = true;
128
 
1 efrain 129
        this.setCategory(modalConfig.category);
130
        this.setReturnUrl(modalConfig.returnUrl);
1441 ariadna 131
        this.showNewCategory = modalConfig.showNewCategory;
132
        this.setSlotId(modalConfig.slotId ?? 0);
133
        this.setSavedFilterCondition(modalConfig.savedFilterCondition ?? null);
1 efrain 134
 
135
        super.configure(modalConfig);
136
    }
137
 
138
    /**
139
     * Set the id of the page that the question should be added to
140
     * when the user clicks the add to quiz link.
141
     *
142
     * @method setAddOnPageId
143
     * @param {int} id
144
     */
145
    setAddOnPageId(id) {
146
        super.setAddOnPageId(id);
147
        this.getBody().find(SELECTORS.ADD_ON_PAGE_FORM_ELEMENT).val(id);
148
    }
149
 
150
    /**
151
     * Set the category for this form. The category is a comma separated
152
     * category id and category context id.
153
     *
154
     * @method setCategory
155
     * @param {string} category
156
     */
157
    setCategory(category) {
158
        this.category = category;
159
    }
160
 
161
    /**
162
     * Returns the saved category.
163
     *
164
     * @method getCategory
165
     * @return {string}
166
     */
167
    getCategory() {
168
        return this.category;
169
    }
170
 
171
    /**
172
     * Set the return URL for the form.
173
     *
174
     * @method setReturnUrl
175
     * @param {string} url
176
     */
177
    setReturnUrl(url) {
178
        this.returnUrl = url;
179
    }
180
 
181
    /**
182
     * Returns the return URL for the form.
183
     *
184
     * @method getReturnUrl
185
     * @return {string}
186
     */
187
    getReturnUrl() {
188
        return this.returnUrl;
189
    }
190
 
191
    /**
1441 ariadna 192
     * Set the ID of the quiz slot, if we are editing an existing random question.
1 efrain 193
     *
1441 ariadna 194
     * @param {Number} slotId
1 efrain 195
     */
1441 ariadna 196
    setSlotId(slotId) {
197
        this.slotId = slotId;
1 efrain 198
    }
199
 
200
    /**
1441 ariadna 201
     * Get the current slot ID.
1 efrain 202
     *
203
     * @return {Number}
204
     */
1441 ariadna 205
    getSlotId() {
206
        return this.slotId;
1 efrain 207
    }
208
 
209
    /**
1441 ariadna 210
     * Store the current filterCondition JSON string.
211
     *
212
     * @param {String} filterCondition
213
     */
214
    setSavedFilterCondition(filterCondition) {
215
        this.savedFilterCondition = filterCondition;
216
    }
217
 
218
    /**
219
     * Return the saved filterCondition JSON string.
220
     *
221
     * @return {String}
222
     */
223
    getSavedFilterCondition() {
224
        return this.savedFilterCondition;
225
    }
226
 
227
    /**
1 efrain 228
     * Moves a given form element inside (a child of) a given tab element.
229
     *
230
     * Hides the 'legend' (e.g. header) element of the form element because the
231
     * tab has the name.
232
     *
233
     * Moves the submit button into a footer element at the bottom of the form
234
     * element for styling purposes.
235
     *
236
     * @method moveContentIntoTab
237
     * @param  {jquery} tabContent The form element to move into the tab.
238
     * @param  {jquey} tabElement The tab element for the form element to move into.
239
     */
240
    moveContentIntoTab(tabContent, tabElement) {
241
        // Hide the header because the tabs show us which part of the form we're
242
        // looking at.
243
        tabContent.find(SELECTORS.FORM_HEADER).addClass('hidden');
244
        // Move the element inside a tab.
245
        tabContent.wrap(tabElement);
246
    }
247
 
248
    /**
249
     * Empty the tab content container and move all tabs from the form into the
250
     * tab container element.
251
     *
252
     * @method moveTabsIntoTabContent
253
     * @param  {jquery} form The form element.
254
     */
255
    moveTabsIntoTabContent(form) {
256
        // Empty it to remove the loading icon.
257
        const tabContent = this.getBody().find(SELECTORS.TAB_CONTENT).empty();
258
        // Make sure all tabs are inside the tab content element.
259
        form.find('[role="tabpanel"]').wrapAll(tabContent);
260
    }
261
 
262
    /**
263
     * Make sure all of the tabs have a cancel button in their fotter to sit along
264
     * side the submit button.
265
     *
266
     * @method moveCancelButtonToTabs
267
     * @param  {jquey} form The form element.
268
     */
269
    moveCancelButtonToTabs(form) {
1441 ariadna 270
        const cancelButton = form.find(SELECTORS.CANCEL_BUTTON_ELEMENT).addClass('ms-1');
1 efrain 271
        const tabFooters = form.find('[data-region="footer"]');
272
        // Remove the buttons container element.
273
        cancelButton.closest(SELECTORS.BUTTON_CONTAINER).remove();
274
        cancelButton.clone().appendTo(tabFooters);
275
    }
276
 
277
    /**
278
     * Load the add random question form in a fragement and perform some transformation
279
     * on the HTML to convert it into tabs for rendering in the modal.
280
     *
281
     * @method loadForm
282
     * @return {promise} Resolved with form HTML and JS.
283
     */
284
    loadForm() {
285
        const addonpage = this.getAddOnPageId();
286
        const returnurl = this.getReturnUrl();
1441 ariadna 287
        const quizcmid = this.quizCmId;
288
        const bankcmid = this.bankCmId;
289
        const savedfiltercondition = this.getSavedFilterCondition();
290
        this.setSavedFilterCondition(null);
1 efrain 291
 
292
        return Fragment.loadFragment(
293
            'mod_quiz',
294
            'add_random_question_form',
295
            this.getContextId(),
296
            {
1441 ariadna 297
                addonpage: addonpage ?? null,
1 efrain 298
                returnurl,
1441 ariadna 299
                quizcmid,
300
                bankcmid,
301
                slotid: this.getSlotId(),
302
                savedfiltercondition,
1 efrain 303
            }
304
        )
1441 ariadna 305
            .then((html, js) => {
306
                const form = $(html);
307
                if (!this.getSlotId()) {
308
                    const existingCategoryTabContent = form.find(SELECTORS.EXISTING_CATEGORY_TAB);
309
                    const existingCategoryTab = this.getBody().find(SELECTORS.EXISTING_CATEGORY_CONTAINER);
310
                    const newCategoryTabContent = form.find(SELECTORS.NEW_CATEGORY_TAB);
311
                    const newCategoryTab = this.getBody().find(SELECTORS.NEW_CATEGORY_CONTAINER);
1 efrain 312
 
1441 ariadna 313
                    // Transform the form into tabs for better rendering in the modal.
314
                    this.moveContentIntoTab(existingCategoryTabContent, existingCategoryTab);
315
                    this.moveContentIntoTab(newCategoryTabContent, newCategoryTab);
316
                    this.moveTabsIntoTabContent(form);
317
                }
1 efrain 318
 
1441 ariadna 319
                Templates.replaceNode(this.getBody().find(SELECTORS.TAB_CONTENT), form, js);
320
                return;
321
            })
322
            .then(() => {
323
                // Make sure the form change checker is disabled otherwise it'll stop the user from navigating away from the
324
                // page once the modal is hidden.
325
                FormChangeChecker.disableAllChecks();
1 efrain 326
 
1441 ariadna 327
                // Add question to quiz.
328
                this.getBody()[0].addEventListener('click', (e) => {
329
                    const button = e.target.closest(SELECTORS.SUBMIT_BUTTON_ELEMENT);
330
                    if (!button) {
331
                        return;
332
                    }
333
                    e.preventDefault();
1 efrain 334
 
1441 ariadna 335
                    // Intercept the submission to adjust the POST params so that the quiz mod id is set and not the bank module id.
336
                    document.querySelector('#questionscontainer input[name="cmid"]').setAttribute('name', this.quizCmId);
1 efrain 337
 
1441 ariadna 338
                    // Add Random questions if the add random button was clicked.
339
                    const addRandomButton = e.target.closest(SELECTORS.ADD_RANDOM_BUTTON);
340
                    if (addRandomButton) {
341
                        const randomcount = document.querySelector(SELECTORS.SELECT_NUMBER_TO_ADD).value;
342
                        const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;
343
 
344
                        this.addQuestions(quizcmid, addonpage, randomcount, filtercondition, '', '');
345
                        return;
346
                    }
347
                    // Update the filter condition for the slot if the update button was clicked.
348
                    const updateFilterButton = e.target.closest(SELECTORS.UPDATE_FILTER_BUTTON);
349
                    if (updateFilterButton) {
350
                        const filtercondition = document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition;
351
                        this.updateFilterCondition(quizcmid, this.getSlotId(), filtercondition);
352
                        return;
353
                    }
354
                    // Add new category if the add category button was clicked.
355
                    const addCategoryButton = e.target.closest(SELECTORS.ADD_NEW_CATEGORY_BUTTON);
356
                    if (addCategoryButton) {
357
                        this.addQuestions(
358
                            quizcmid,
359
                            addonpage,
360
                            1,
361
                            '',
362
                            document.querySelector(SELECTORS.NEW_CATEGORY_ELEMENT).value,
363
                            document.querySelector(SELECTORS.PARENT_CATEGORY_ELEMENT).value
364
                        );
365
                        return;
366
                    }
367
                });
368
 
369
                this.getModal().on('click', SELECTORS.SWITCH_TO_OTHER_BANK, () => {
370
                    this.setSavedFilterCondition(
371
                        document.querySelector(SELECTORS.FILTER_CONDITION_ELEMENT).dataset?.filtercondition
1 efrain 372
                    );
1441 ariadna 373
                    this.handleSwitchBankContentReload(SELECTORS.BANK_SEARCH)
374
                        .then(function(ModalQuizQuestionBank) {
375
                            $(SELECTORS.BANK_SEARCH)?.on('change', (e) => {
376
                                const bankCmId = $(e.currentTarget).val();
377
                                // Have to recreate the modal as we have already used the body for the switch bank content.
378
                                if (bankCmId > 0) {
379
                                    ModalAddRandomQuestion.create({
380
                                        'contextId': ModalQuizQuestionBank.getContextId(),
381
                                        'bankCmId': bankCmId,
382
                                        'category': ModalQuizQuestionBank.getCategory(),
383
                                        'returnUrl': ModalQuizQuestionBank.getReturnUrl(),
384
                                        'quizCmId': ModalQuizQuestionBank.quizCmId,
385
                                        'title': ModalQuizQuestionBank.originalTitle,
386
                                        'addOnPage': ModalQuizQuestionBank.getAddOnPageId(),
387
                                        'templateContext': {hidden: ModalQuizQuestionBank.showNewCategory},
388
                                        'showNewCategory': ModalQuizQuestionBank.showNewCategory,
389
                                        'slotId': ModalQuizQuestionBank.getSlotId(),
390
                                    })
391
                                    .then(ModalQuizQuestionBank.destroy())
392
                                    .catch(Notification.exception);
393
                                }
394
                            });
395
                            return ModalQuizQuestionBank;
396
                        });
397
                });
398
 
399
                this.getModal().on('click', SELECTORS.GO_BACK_BUTTON, (e) => {
400
                    const anchorElement = $(e.currentTarget);
401
                    // Have to recreate the modal as we have already used the body for the switch bank content.
402
                    ModalAddRandomQuestion.create({
403
                        'contextId': this.getContextId(),
404
                        'bankCmId': anchorElement.attr('value'),
405
                        'category': this.getCategory(),
406
                        'returnUrl': this.getReturnUrl(),
407
                        'quizCmId': this.quizCmId,
408
                        'title': this.originalTitle,
409
                        'addOnPage': this.getAddOnPageId(),
410
                        'templateContext': {hidden: this.showNewCategory},
411
                        'showNewCategory': this.showNewCategory,
412
                        'savedFilterCondition': this.getSavedFilterCondition(),
413
                        'slotId': this.getSlotId(),
414
                    }).then(this.destroy()).catch(Notification.exception);
415
                });
416
 
417
                this.getModal().on('click', SELECTORS.ANCHOR, (e) => {
418
                    const anchorElement = $(e.currentTarget);
419
                    // Have to recreate the modal as we have already used the body for the switch bank content.
420
                    if (anchorElement.closest('a[' + SELECTORS.NEW_BANKMOD_ID + ']').length) {
421
                        ModalAddRandomQuestion.create({
422
                            'contextId': this.getContextId(),
423
                            'bankCmId': anchorElement.attr(SELECTORS.NEW_BANKMOD_ID),
424
                            'category': this.getCategory(),
425
                            'returnUrl': this.getReturnUrl(),
426
                            'quizCmId': this.quizCmId,
427
                            'title': this.originalTitle,
428
                            'addOnPage': this.getAddOnPageId(),
429
                            'templateContext': {hidden: this.showNewCategory},
430
                            'showNewCategory': this.showNewCategory,
431
                            'slotId': this.getSlotId(),
432
                        }).then(this.destroy()).catch(Notification.exception);
433
                    }
434
                });
435
            })
436
            .catch(Notification.exception);
1 efrain 437
    }
438
 
439
    /**
440
     * Call web service function to add random questions
441
     *
1441 ariadna 442
     * @param {number} quizcmid the course module id of the quiz to add questions to.
1 efrain 443
     * @param {number} addonpage the page where random questions will be added to
444
     * @param {number} randomcount Number of random questions
445
     * @param {string} filtercondition Filter condition
446
     * @param {string} newcategory add new category
447
     * @param {string} parentcategory parent category of new category
448
     */
449
    async addQuestions(
1441 ariadna 450
        quizcmid,
1 efrain 451
        addonpage,
452
        randomcount,
453
        filtercondition,
454
        newcategory,
455
        parentcategory
456
    ) {
457
        // We do not need to resolve this Pending because the form submission will result in a page redirect.
458
        new Pending('mod-quiz/modal_add_random_questions');
459
        const call = {
460
            methodname: 'mod_quiz_add_random_questions',
461
            args: {
1441 ariadna 462
                cmid: quizcmid,
1 efrain 463
                addonpage,
464
                randomcount,
465
                filtercondition,
466
                newcategory,
467
                parentcategory,
468
            }
469
        };
470
        try {
471
            const response = await fetchMany([call])[0];
472
            const form = document.querySelector(SELECTORS.FORM_ELEMENT);
473
            const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);
474
            messageInput.value = response.message;
475
            form.submit();
476
        } catch (e) {
477
            Notification.exception(e);
478
        }
479
    }
480
 
481
    /**
1441 ariadna 482
     * Call web service function to update the filter condition for an existing slot.
483
     *
484
     * @param {number} quizcmid the course module id of the quiz.
485
     * @param {number} slotid The slot the random question is in.
486
     * @param {string} filtercondition The new filter condition.
487
     */
488
    async updateFilterCondition(
489
        quizcmid,
490
        slotid,
491
        filtercondition,
492
    ) {
493
        // We do not need to resolve this Pending because the form submission will result in a page redirect.
494
        new Pending('mod-quiz/modal_add_random_questions');
495
        const call = {
496
            methodname: 'mod_quiz_update_filter_condition',
497
            args: {
498
                cmid: quizcmid,
499
                slotid,
500
                filtercondition,
501
            }
502
        };
503
        try {
504
            const response = await fetchMany([call])[0];
505
            const form = document.querySelector(SELECTORS.FORM_ELEMENT);
506
            const messageInput = form.querySelector(SELECTORS.MESSAGE_INPUT);
507
            messageInput.value = response.message;
508
            form.submit();
509
        } catch (e) {
510
            Notification.exception(e);
511
        }
512
    }
513
 
514
    /**
1 efrain 515
     * Override the modal show function to load the form when this modal is first
516
     * shown.
517
     *
518
     * @method show
519
     */
520
    show() {
521
        super.show(this);
522
 
523
        if (!this.loadedForm) {
1441 ariadna 524
            this.tabHtml = this.getBody();
1 efrain 525
            this.loadForm(window.location.search);
526
            this.loadedForm = true;
527
        }
528
    }
529
}
530
 
531
ModalAddRandomQuestion.registerModalType();