Commit | Line | Data |
---|---|---|
bae67469 MM |
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 | * This module will tie together all of the different calls the gradable module will make. | |
18 | * | |
19 | * @module mod_forum/local/grades/grader | |
20 | * @package mod_forum | |
21 | * @copyright 2019 Mathew May <mathew.solutions> | |
22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
23 | */ | |
24 | import Templates from 'core/templates'; | |
bae67469 | 25 | import Selectors from './local/grader/selectors'; |
45c0584c | 26 | import getUserPicker from './local/grader/user_picker'; |
bae67469 | 27 | import {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen'; |
f281c616 | 28 | import getGradingPanelFunctions from './local/grader/gradingpanel'; |
77ee8778 AN |
29 | import {add as addToast} from 'core/toast'; |
30 | import {get_string as getString} from 'core/str'; | |
ce1c4701 | 31 | import {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise'; |
bdcf8908 | 32 | import {addIconToContainerWithPromise} from 'core/loadingicon'; |
bae67469 MM |
33 | |
34 | const templateNames = { | |
35 | grader: { | |
36 | app: 'mod_forum/local/grades/grader', | |
ce1c4701 AN |
37 | gradingPanel: { |
38 | error: 'mod_forum/local/grades/local/grader/gradingpanel/error', | |
39 | }, | |
bae67469 MM |
40 | }, |
41 | }; | |
42 | ||
cc1a7689 MM |
43 | /** |
44 | * Helper function that replaces the user picker placeholder with what we get back from the user picker class. | |
45 | * | |
46 | * @param {HTMLElement} root | |
47 | * @param {String} html | |
48 | */ | |
bae67469 MM |
49 | const displayUserPicker = (root, html) => { |
50 | const pickerRegion = root.querySelector(Selectors.regions.pickerRegion); | |
51 | Templates.replaceNodeContents(pickerRegion, html, ''); | |
52 | }; | |
53 | ||
cc1a7689 MM |
54 | /** |
55 | * To be removed, this is now done as a part of Templates.renderForPromise() | |
56 | * | |
57 | * @param {String} html | |
58 | * @param {String} js | |
59 | * @return {[*, *]} | |
60 | */ | |
f281c616 AN |
61 | const fetchContentFromRender = (html, js) => { |
62 | return [html, js]; | |
63 | }; | |
64 | ||
cc1a7689 MM |
65 | /** |
66 | * Here we build the function that is passed to the user picker that'll handle updating the user content area | |
67 | * of the grading interface. | |
68 | * | |
69 | * @param {HTMLElement} root | |
70 | * @param {Function} getContentForUser | |
71 | * @param {Function} getGradeForUser | |
72 | * @return {Function} | |
73 | */ | |
f281c616 | 74 | const getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser) => { |
bae67469 | 75 | return async(user) => { |
bdcf8908 | 76 | const spinner = addIconToContainerWithPromise(root); |
bae67469 | 77 | const [ |
f281c616 AN |
78 | [html, js], |
79 | userGrade, | |
bae67469 | 80 | ] = await Promise.all([ |
f281c616 AN |
81 | getContentForUser(user.id).then(fetchContentFromRender), |
82 | getGradeForUser(user.id), | |
bae67469 MM |
83 | ]); |
84 | Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js); | |
f281c616 AN |
85 | |
86 | const [ | |
87 | gradingPanelHtml, | |
88 | gradingPanelJS | |
89 | ] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender); | |
46d51c8c RW |
90 | const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer); |
91 | const panel = panelContainer.querySelector(Selectors.regions.gradingPanel); | |
92 | Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS); | |
93 | panelContainer.scrollTop = 0; | |
bdcf8908 | 94 | spinner.resolve(); |
bae67469 MM |
95 | }; |
96 | }; | |
97 | ||
cc1a7689 MM |
98 | /** |
99 | * Add click handlers to the buttons in the header of the grading interface. | |
100 | * | |
101 | * @param {HTMLElement} graderLayout | |
102 | * @param {Object} userPicker | |
103 | * @param {Function} saveGradeFunction | |
104 | */ | |
eaee6477 | 105 | const registerEventListeners = (graderLayout, userPicker, saveGradeFunction) => { |
bae67469 MM |
106 | const graderContainer = graderLayout.getContainer(); |
107 | graderContainer.addEventListener('click', (e) => { | |
108 | if (e.target.closest(Selectors.buttons.toggleFullscreen)) { | |
109 | e.stopImmediatePropagation(); | |
110 | e.preventDefault(); | |
111 | graderLayout.toggleFullscreen(); | |
eaee6477 AN |
112 | |
113 | return; | |
114 | } | |
115 | ||
116 | if (e.target.closest(Selectors.buttons.closeGrader)) { | |
bae67469 MM |
117 | e.stopImmediatePropagation(); |
118 | e.preventDefault(); | |
119 | ||
120 | graderLayout.close(); | |
eaee6477 AN |
121 | |
122 | return; | |
123 | } | |
124 | ||
125 | if (e.target.closest(Selectors.buttons.saveGrade)) { | |
126 | saveGradeFunction(userPicker.currentUser); | |
bae67469 MM |
127 | } |
128 | }); | |
129 | }; | |
130 | ||
77ee8778 AN |
131 | /** |
132 | * Get the function used to save a user grade. | |
133 | * | |
cc1a7689 | 134 | * @param {HTMLElement} root The container for the grader |
77ee8778 AN |
135 | * @param {Function} setGradeForUser The function that will be called. |
136 | * @return {Function} | |
137 | */ | |
f281c616 | 138 | const getSaveUserGradeFunction = (root, setGradeForUser) => { |
cc1a7689 | 139 | return async(user) => { |
77ee8778 | 140 | try { |
ce1c4701 | 141 | root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = ''; |
77ee8778 | 142 | const result = await setGradeForUser(user.id, root.querySelector(Selectors.regions.gradingPanel)); |
ce1c4701 AN |
143 | if (result.success) { |
144 | addToast(await getString('grades:gradesavedfor', 'mod_forum', user)); | |
145 | } | |
146 | if (result.failed) { | |
147 | displayGradingError(root, user, result.error); | |
148 | } | |
77ee8778 AN |
149 | |
150 | return result; | |
ce1c4701 AN |
151 | } catch (err) { |
152 | displayGradingError(root, user, err); | |
153 | ||
154 | return failedUpdate(err); | |
77ee8778 | 155 | } |
f281c616 AN |
156 | }; |
157 | }; | |
158 | ||
ce1c4701 AN |
159 | /** |
160 | * Display a grading error, typically from a failed save. | |
161 | * | |
cc1a7689 | 162 | * @param {HTMLElement} root The container for the grader |
ce1c4701 AN |
163 | * @param {Object} user The user who was errored |
164 | * @param {Object} err The details of the error | |
165 | */ | |
166 | const displayGradingError = async(root, user, err) => { | |
167 | const [ | |
168 | {html, js}, | |
169 | errorString | |
170 | ] = await Promise.all([ | |
171 | Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}), | |
172 | await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}), | |
173 | ]); | |
174 | ||
175 | Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js); | |
176 | addToast(errorString); | |
177 | }; | |
178 | ||
eaee6477 AN |
179 | /** |
180 | * Launch the grader interface with the specified parameters. | |
181 | * | |
182 | * @param {Function} getListOfUsers A function to get the list of users | |
183 | * @param {Function} getContentForUser A function to get the content for a specific user | |
184 | * @param {Function} getGradeForUser A function get the grade details for a specific user | |
185 | * @param {Function} setGradeForUser A function to set the grade for a specific user | |
186 | */ | |
f281c616 | 187 | export const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, { |
4c98e56c | 188 | initialUserId = null, moduleName, courseName, courseUrl |
bae67469 MM |
189 | } = {}) => { |
190 | ||
cc1a7689 MM |
191 | // We need all of these functions to be executed in series, if one step runs before another the interface |
192 | // will not work. | |
bae67469 MM |
193 | const [ |
194 | graderLayout, | |
46d51c8c | 195 | {html, js}, |
bae67469 MM |
196 | userList, |
197 | ] = await Promise.all([ | |
198 | createFullScreenWindow({fullscreen: false, showLoader: false}), | |
46d51c8c RW |
199 | Templates.renderForPromise(templateNames.grader.app, { |
200 | moduleName, | |
4c98e56c RW |
201 | courseName, |
202 | courseUrl, | |
46d51c8c RW |
203 | drawer: {show: true} |
204 | }), | |
bae67469 MM |
205 | getListOfUsers(), |
206 | ]); | |
207 | const graderContainer = graderLayout.getContainer(); | |
208 | ||
45c0584c AN |
209 | const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser); |
210 | ||
46d51c8c | 211 | Templates.replaceNodeContents(graderContainer, html, js); |
f281c616 AN |
212 | const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser); |
213 | ||
45c0584c AN |
214 | // Fetch the userpicker for display. |
215 | const userPicker = await getUserPicker( | |
f281c616 | 216 | userList, |
f281c616 | 217 | updateUserContent, |
aa04b722 AN |
218 | saveGradeFunction, |
219 | { | |
220 | initialUserId, | |
221 | }, | |
f281c616 | 222 | ); |
bae67469 | 223 | |
eaee6477 AN |
224 | // Register all event listeners. |
225 | registerEventListeners(graderLayout, userPicker, saveGradeFunction); | |
226 | ||
45c0584c AN |
227 | // Display the newly created user picker. |
228 | displayUserPicker(graderContainer, userPicker.rootNode); | |
bae67469 | 229 | }; |
f281c616 AN |
230 | |
231 | export {getGradingPanelFunctions}; |