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 |
* This module will tie together all of the different calls the gradable module will make.
|
|
|
18 |
*
|
|
|
19 |
* @module mod_forum/local/grades/grader
|
|
|
20 |
* @copyright 2019 Mathew May <mathew.solutions>
|
|
|
21 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
22 |
*/
|
|
|
23 |
import Templates from 'core/templates';
|
|
|
24 |
import Selectors from './local/grader/selectors';
|
|
|
25 |
import getUserPicker from './local/grader/user_picker';
|
|
|
26 |
import {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';
|
|
|
27 |
import getGradingPanelFunctions from './local/grader/gradingpanel';
|
|
|
28 |
import {add as addToast} from 'core/toast';
|
|
|
29 |
import {addNotification} from 'core/notification';
|
|
|
30 |
import {getString} from 'core/str';
|
|
|
31 |
import {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';
|
|
|
32 |
import {addIconToContainerWithPromise} from 'core/loadingicon';
|
|
|
33 |
import {debounce} from 'core/utils';
|
|
|
34 |
import {fillInitialValues} from 'core_grades/grades/grader/gradingpanel/comparison';
|
|
|
35 |
import Modal from 'core/modal_cancel';
|
|
|
36 |
import {subscribe} from 'core/pubsub';
|
|
|
37 |
import DrawerEvents from 'core/drawer_events';
|
|
|
38 |
|
|
|
39 |
const templateNames = {
|
|
|
40 |
grader: {
|
|
|
41 |
app: 'mod_forum/local/grades/grader',
|
|
|
42 |
gradingPanel: {
|
|
|
43 |
error: 'mod_forum/local/grades/local/grader/gradingpanel/error',
|
|
|
44 |
},
|
|
|
45 |
searchResults: 'mod_forum/local/grades/local/grader/user_picker/user_search',
|
|
|
46 |
status: 'mod_forum/local/grades/local/grader/status',
|
|
|
47 |
},
|
|
|
48 |
};
|
|
|
49 |
|
|
|
50 |
/**
|
|
|
51 |
* Helper function that replaces the user picker placeholder with what we get back from the user picker class.
|
|
|
52 |
*
|
|
|
53 |
* @param {HTMLElement} root
|
|
|
54 |
* @param {String} html
|
|
|
55 |
*/
|
|
|
56 |
const displayUserPicker = (root, html) => {
|
|
|
57 |
const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);
|
|
|
58 |
Templates.replaceNodeContents(pickerRegion, html, '');
|
|
|
59 |
};
|
|
|
60 |
|
|
|
61 |
/**
|
|
|
62 |
* To be removed, this is now done as a part of Templates.renderForPromise()
|
|
|
63 |
*
|
|
|
64 |
* @param {String} html
|
|
|
65 |
* @param {String} js
|
|
|
66 |
* @returns {array} An array containing the HTML, and JS.
|
|
|
67 |
*/
|
|
|
68 |
const fetchContentFromRender = (html, js) => {
|
|
|
69 |
return [html, js];
|
|
|
70 |
};
|
|
|
71 |
|
|
|
72 |
/**
|
|
|
73 |
* Here we build the function that is passed to the user picker that'll handle updating the user content area
|
|
|
74 |
* of the grading interface.
|
|
|
75 |
*
|
|
|
76 |
* @param {HTMLElement} root
|
|
|
77 |
* @param {Function} getContentForUser
|
|
|
78 |
* @param {Function} getGradeForUser
|
|
|
79 |
* @param {Function} saveGradeForUser
|
|
|
80 |
* @return {Function}
|
|
|
81 |
*/
|
|
|
82 |
const getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser, saveGradeForUser) => {
|
|
|
83 |
let firstLoad = true;
|
|
|
84 |
|
|
|
85 |
return async(user) => {
|
|
|
86 |
const spinner = firstLoad ? null : addIconToContainerWithPromise(root);
|
|
|
87 |
const [
|
|
|
88 |
[html, js],
|
|
|
89 |
userGrade,
|
|
|
90 |
] = await Promise.all([
|
|
|
91 |
getContentForUser(user.id).then(fetchContentFromRender),
|
|
|
92 |
getGradeForUser(user.id),
|
|
|
93 |
]);
|
|
|
94 |
Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);
|
|
|
95 |
|
|
|
96 |
const [
|
|
|
97 |
gradingPanelHtml,
|
|
|
98 |
gradingPanelJS
|
|
|
99 |
] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);
|
|
|
100 |
const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer);
|
|
|
101 |
const panel = panelContainer.querySelector(Selectors.regions.gradingPanel);
|
|
|
102 |
Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS);
|
|
|
103 |
|
|
|
104 |
const form = panel.querySelector('form');
|
|
|
105 |
fillInitialValues(form);
|
|
|
106 |
|
|
|
107 |
form.addEventListener('submit', event => {
|
|
|
108 |
saveGradeForUser(user);
|
|
|
109 |
event.preventDefault();
|
|
|
110 |
});
|
|
|
111 |
|
|
|
112 |
panelContainer.scrollTop = 0;
|
|
|
113 |
firstLoad = false;
|
|
|
114 |
|
|
|
115 |
if (spinner) {
|
|
|
116 |
spinner.resolve();
|
|
|
117 |
}
|
|
|
118 |
return userGrade;
|
|
|
119 |
};
|
|
|
120 |
};
|
|
|
121 |
|
|
|
122 |
/**
|
|
|
123 |
* Show the search results container and hide the user picker and body content.
|
|
|
124 |
*
|
|
|
125 |
* @param {HTMLElement} bodyContainer The container element for the body content
|
|
|
126 |
* @param {HTMLElement} userPickerContainer The container element for the user picker
|
|
|
127 |
* @param {HTMLElement} searchResultsContainer The container element for the search results
|
|
|
128 |
*/
|
|
|
129 |
const showSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {
|
|
|
130 |
bodyContainer.classList.add('hidden');
|
|
|
131 |
userPickerContainer.classList.add('hidden');
|
|
|
132 |
searchResultsContainer.classList.remove('hidden');
|
|
|
133 |
};
|
|
|
134 |
|
|
|
135 |
/**
|
|
|
136 |
* Hide the search results container and show the user picker and body content.
|
|
|
137 |
*
|
|
|
138 |
* @param {HTMLElement} bodyContainer The container element for the body content
|
|
|
139 |
* @param {HTMLElement} userPickerContainer The container element for the user picker
|
|
|
140 |
* @param {HTMLElement} searchResultsContainer The container element for the search results
|
|
|
141 |
*/
|
|
|
142 |
const hideSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {
|
|
|
143 |
bodyContainer.classList.remove('hidden');
|
|
|
144 |
userPickerContainer.classList.remove('hidden');
|
|
|
145 |
searchResultsContainer.classList.add('hidden');
|
|
|
146 |
};
|
|
|
147 |
|
|
|
148 |
/**
|
|
|
149 |
* Toggles the visibility of the user search.
|
|
|
150 |
*
|
|
|
151 |
* @param {HTMLElement} toggleSearchButton The button that toggles the search
|
|
|
152 |
* @param {HTMLElement} searchContainer The container element for the user search
|
|
|
153 |
* @param {HTMLElement} searchInput The input element for searching
|
|
|
154 |
*/
|
|
|
155 |
const showUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {
|
|
|
156 |
searchContainer.classList.remove('collapsed');
|
|
|
157 |
toggleSearchButton.setAttribute('aria-expanded', 'true');
|
|
|
158 |
toggleSearchButton.classList.add('expand');
|
|
|
159 |
toggleSearchButton.classList.remove('collapse');
|
|
|
160 |
|
|
|
161 |
// Hide the grading info container from screen reader.
|
|
|
162 |
const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);
|
|
|
163 |
gradingInfoContainer.setAttribute('aria-hidden', 'true');
|
|
|
164 |
|
|
|
165 |
// Hide the collapse grading drawer button from screen reader.
|
|
|
166 |
const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);
|
|
|
167 |
collapseGradingDrawer.setAttribute('aria-hidden', 'true');
|
|
|
168 |
collapseGradingDrawer.setAttribute('tabindex', '-1');
|
|
|
169 |
|
|
|
170 |
searchInput.focus();
|
|
|
171 |
};
|
|
|
172 |
|
|
|
173 |
/**
|
|
|
174 |
* Toggles the visibility of the user search.
|
|
|
175 |
*
|
|
|
176 |
* @param {HTMLElement} toggleSearchButton The button that toggles the search
|
|
|
177 |
* @param {HTMLElement} searchContainer The container element for the user search
|
|
|
178 |
* @param {HTMLElement} searchInput The input element for searching
|
|
|
179 |
*/
|
|
|
180 |
const hideUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {
|
|
|
181 |
searchContainer.classList.add('collapsed');
|
|
|
182 |
toggleSearchButton.setAttribute('aria-expanded', 'false');
|
|
|
183 |
toggleSearchButton.classList.add('collapse');
|
|
|
184 |
toggleSearchButton.classList.remove('expand');
|
|
|
185 |
toggleSearchButton.focus();
|
|
|
186 |
|
|
|
187 |
// Show the grading info container to screen reader.
|
|
|
188 |
const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);
|
|
|
189 |
gradingInfoContainer.removeAttribute('aria-hidden');
|
|
|
190 |
|
|
|
191 |
// Show the collapse grading drawer button from screen reader.
|
|
|
192 |
const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);
|
|
|
193 |
collapseGradingDrawer.removeAttribute('aria-hidden');
|
|
|
194 |
collapseGradingDrawer.setAttribute('tabindex', '0');
|
|
|
195 |
|
|
|
196 |
searchInput.value = '';
|
|
|
197 |
};
|
|
|
198 |
|
|
|
199 |
/**
|
|
|
200 |
* Find the list of users who's names include the given search term.
|
|
|
201 |
*
|
|
|
202 |
* @param {Array} userList List of users for the grader
|
|
|
203 |
* @param {String} searchTerm The search term to match
|
|
|
204 |
* @return {Array}
|
|
|
205 |
*/
|
|
|
206 |
const searchForUsers = (userList, searchTerm) => {
|
|
|
207 |
if (searchTerm === '') {
|
|
|
208 |
return userList;
|
|
|
209 |
}
|
|
|
210 |
|
|
|
211 |
searchTerm = searchTerm.toLowerCase();
|
|
|
212 |
|
|
|
213 |
return userList.filter((user) => {
|
|
|
214 |
return user.fullname.toLowerCase().includes(searchTerm);
|
|
|
215 |
});
|
|
|
216 |
};
|
|
|
217 |
|
|
|
218 |
/**
|
|
|
219 |
* Render the list of users in the search results area.
|
|
|
220 |
*
|
|
|
221 |
* @param {HTMLElement} searchResultsContainer The container element for search results
|
|
|
222 |
* @param {Array} users The list of users to display
|
|
|
223 |
*/
|
|
|
224 |
const renderSearchResults = async(searchResultsContainer, users) => {
|
|
|
225 |
const {html, js} = await Templates.renderForPromise(templateNames.grader.searchResults, {users});
|
|
|
226 |
Templates.replaceNodeContents(searchResultsContainer, html, js);
|
|
|
227 |
};
|
|
|
228 |
|
|
|
229 |
/**
|
|
|
230 |
* Add click handlers to the buttons in the header of the grading interface.
|
|
|
231 |
*
|
|
|
232 |
* @param {HTMLElement} graderLayout
|
|
|
233 |
* @param {Object} userPicker
|
|
|
234 |
* @param {Function} saveGradeFunction
|
|
|
235 |
* @param {Array} userList List of users for the grader.
|
|
|
236 |
*/
|
|
|
237 |
const registerEventListeners = (graderLayout, userPicker, saveGradeFunction, userList) => {
|
|
|
238 |
const graderContainer = graderLayout.getContainer();
|
|
|
239 |
const toggleSearchButton = graderContainer.querySelector(Selectors.buttons.toggleSearch);
|
|
|
240 |
const searchInputContainer = graderContainer.querySelector(Selectors.regions.userSearchContainer);
|
|
|
241 |
const searchInput = searchInputContainer.querySelector(Selectors.regions.userSearchInput);
|
|
|
242 |
const bodyContainer = graderContainer.querySelector(Selectors.regions.bodyContainer);
|
|
|
243 |
const userPickerContainer = graderContainer.querySelector(Selectors.regions.pickerRegion);
|
|
|
244 |
const searchResultsContainer = graderContainer.querySelector(Selectors.regions.searchResultsContainer);
|
|
|
245 |
|
|
|
246 |
graderContainer.addEventListener('click', (e) => {
|
|
|
247 |
if (e.target.closest(Selectors.buttons.toggleFullscreen)) {
|
|
|
248 |
e.stopImmediatePropagation();
|
|
|
249 |
e.preventDefault();
|
|
|
250 |
graderLayout.toggleFullscreen();
|
|
|
251 |
|
|
|
252 |
return;
|
|
|
253 |
}
|
|
|
254 |
|
|
|
255 |
if (e.target.closest(Selectors.buttons.closeGrader)) {
|
|
|
256 |
e.stopImmediatePropagation();
|
|
|
257 |
e.preventDefault();
|
|
|
258 |
|
|
|
259 |
graderLayout.close();
|
|
|
260 |
|
|
|
261 |
return;
|
|
|
262 |
}
|
|
|
263 |
|
|
|
264 |
if (e.target.closest(Selectors.buttons.saveGrade)) {
|
|
|
265 |
saveGradeFunction(userPicker.currentUser);
|
|
|
266 |
}
|
|
|
267 |
|
|
|
268 |
if (e.target.closest(Selectors.buttons.toggleSearch)) {
|
|
|
269 |
if (toggleSearchButton.getAttribute('aria-expanded') === 'true') {
|
|
|
270 |
// Search is open so let's close it.
|
|
|
271 |
hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);
|
|
|
272 |
hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);
|
|
|
273 |
searchResultsContainer.innerHTML = '';
|
|
|
274 |
} else {
|
|
|
275 |
// Search is closed so let's open it.
|
|
|
276 |
showUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);
|
|
|
277 |
showSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);
|
|
|
278 |
renderSearchResults(searchResultsContainer, userList);
|
|
|
279 |
}
|
|
|
280 |
|
|
|
281 |
return;
|
|
|
282 |
}
|
|
|
283 |
|
|
|
284 |
const selectUserButton = e.target.closest(Selectors.buttons.selectUser);
|
|
|
285 |
if (selectUserButton) {
|
|
|
286 |
const userId = selectUserButton.getAttribute('data-userid');
|
|
|
287 |
const user = userList.find(user => user.id == userId);
|
|
|
288 |
userPicker.setUserId(userId);
|
|
|
289 |
userPicker.showUser(user);
|
|
|
290 |
hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);
|
|
|
291 |
hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);
|
|
|
292 |
searchResultsContainer.innerHTML = '';
|
|
|
293 |
}
|
|
|
294 |
});
|
|
|
295 |
|
|
|
296 |
// Debounce the search input so that it only executes 300 milliseconds after the user has finished typing.
|
|
|
297 |
searchInput.addEventListener('input', debounce(() => {
|
|
|
298 |
const users = searchForUsers(userList, searchInput.value);
|
|
|
299 |
renderSearchResults(searchResultsContainer, users);
|
|
|
300 |
}, 300));
|
|
|
301 |
|
|
|
302 |
// Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width.
|
|
|
303 |
subscribe(DrawerEvents.DRAWER_HIDDEN, (drawerRoot) => {
|
|
|
304 |
const gradingPanel = drawerRoot[0];
|
|
|
305 |
if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {
|
|
|
306 |
setContentContainerMargin(graderContainer, 0);
|
|
|
307 |
}
|
|
|
308 |
});
|
|
|
309 |
|
|
|
310 |
// Bring back the right margin of the content container when the grading panel is shown to give space for the grading panel.
|
|
|
311 |
subscribe(DrawerEvents.DRAWER_SHOWN, (drawerRoot) => {
|
|
|
312 |
const gradingPanel = drawerRoot[0];
|
|
|
313 |
if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {
|
|
|
314 |
setContentContainerMargin(graderContainer, gradingPanel.offsetWidth);
|
|
|
315 |
}
|
|
|
316 |
});
|
|
|
317 |
};
|
|
|
318 |
|
|
|
319 |
/**
|
|
|
320 |
* Adjusts the right margin of the content container.
|
|
|
321 |
*
|
|
|
322 |
* @param {HTMLElement} graderContainer The container for the grader app.
|
|
|
323 |
* @param {Number} rightMargin The right margin value.
|
|
|
324 |
*/
|
|
|
325 |
const setContentContainerMargin = (graderContainer, rightMargin) => {
|
|
|
326 |
const contentContainer = graderContainer.querySelector(Selectors.regions.moduleContainer);
|
|
|
327 |
if (contentContainer) {
|
|
|
328 |
contentContainer.style.marginRight = `${rightMargin}px`;
|
|
|
329 |
}
|
|
|
330 |
};
|
|
|
331 |
|
|
|
332 |
/**
|
|
|
333 |
* Get the function used to save a user grade.
|
|
|
334 |
*
|
|
|
335 |
* @param {HTMLElement} root The container for the grader
|
|
|
336 |
* @param {Function} setGradeForUser The function that will be called.
|
|
|
337 |
* @return {Function}
|
|
|
338 |
*/
|
|
|
339 |
const getSaveUserGradeFunction = (root, setGradeForUser) => {
|
|
|
340 |
return async(user) => {
|
|
|
341 |
try {
|
|
|
342 |
root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';
|
|
|
343 |
const result = await setGradeForUser(
|
|
|
344 |
user.id,
|
|
|
345 |
root.querySelector(Selectors.values.sendStudentNotifications).value,
|
|
|
346 |
root.querySelector(Selectors.regions.gradingPanel)
|
|
|
347 |
);
|
|
|
348 |
if (result.success) {
|
|
|
349 |
addToast(await getString('grades:gradesavedfor', 'mod_forum', user));
|
|
|
350 |
}
|
|
|
351 |
if (result.failed) {
|
|
|
352 |
displayGradingError(root, user, result.error);
|
|
|
353 |
}
|
|
|
354 |
|
|
|
355 |
return result;
|
|
|
356 |
} catch (err) {
|
|
|
357 |
displayGradingError(root, user, err);
|
|
|
358 |
|
|
|
359 |
return failedUpdate(err);
|
|
|
360 |
}
|
|
|
361 |
};
|
|
|
362 |
};
|
|
|
363 |
|
|
|
364 |
/**
|
|
|
365 |
* Display a grading error, typically from a failed save.
|
|
|
366 |
*
|
|
|
367 |
* @param {HTMLElement} root The container for the grader
|
|
|
368 |
* @param {Object} user The user who was errored
|
|
|
369 |
* @param {Object} err The details of the error
|
|
|
370 |
*/
|
|
|
371 |
const displayGradingError = async(root, user, err) => {
|
|
|
372 |
const [
|
|
|
373 |
{html, js},
|
|
|
374 |
errorString
|
|
|
375 |
] = await Promise.all([
|
|
|
376 |
Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),
|
|
|
377 |
await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),
|
|
|
378 |
]);
|
|
|
379 |
|
|
|
380 |
Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);
|
|
|
381 |
addToast(errorString, {type: 'warning'});
|
|
|
382 |
};
|
|
|
383 |
|
|
|
384 |
/**
|
|
|
385 |
* Launch the grader interface with the specified parameters.
|
|
|
386 |
*
|
|
|
387 |
* @param {Function} getListOfUsers A function to get the list of users
|
|
|
388 |
* @param {Function} getContentForUser A function to get the content for a specific user
|
|
|
389 |
* @param {Function} getGradeForUser A function get the grade details for a specific user
|
|
|
390 |
* @param {Function} setGradeForUser A function to set the grade for a specific user
|
|
|
391 |
* @param {Object} preferences Preferences for the launch function
|
|
|
392 |
* @param {Number} preferences.initialUserId
|
|
|
393 |
* @param {string} preferences.moduleName
|
|
|
394 |
* @param {string} preferences.courseName
|
|
|
395 |
* @param {string} preferences.courseUrl
|
|
|
396 |
* @param {boolean} preferences.sendStudentNotifications
|
|
|
397 |
* @param {null|HTMLElement} preferences.focusOnClose
|
|
|
398 |
*/
|
|
|
399 |
export const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {
|
|
|
400 |
initialUserId = null,
|
|
|
401 |
moduleName,
|
|
|
402 |
courseName,
|
|
|
403 |
courseUrl,
|
|
|
404 |
sendStudentNotifications,
|
|
|
405 |
focusOnClose = null,
|
|
|
406 |
} = {}) => {
|
|
|
407 |
|
|
|
408 |
// We need all of these functions to be executed in series, if one step runs before another the interface
|
|
|
409 |
// will not work.
|
|
|
410 |
|
|
|
411 |
// We need this promise to resolve separately so that we can avoid loading the whole interface if there are no users.
|
|
|
412 |
const userList = await getListOfUsers();
|
|
|
413 |
if (!userList.length) {
|
|
|
414 |
addNotification({
|
|
|
415 |
message: await getString('nouserstograde', 'core_grades'),
|
|
|
416 |
type: "error",
|
|
|
417 |
});
|
|
|
418 |
return;
|
|
|
419 |
}
|
|
|
420 |
|
|
|
421 |
// Now that we have confirmed there are at least some users let's boot up the grader interface.
|
|
|
422 |
const [
|
|
|
423 |
graderLayout,
|
|
|
424 |
{html, js},
|
|
|
425 |
] = await Promise.all([
|
|
|
426 |
createFullScreenWindow({
|
|
|
427 |
fullscreen: false,
|
|
|
428 |
showLoader: false,
|
|
|
429 |
focusOnClose,
|
|
|
430 |
}),
|
|
|
431 |
Templates.renderForPromise(templateNames.grader.app, {
|
|
|
432 |
moduleName,
|
|
|
433 |
courseName,
|
|
|
434 |
courseUrl,
|
|
|
435 |
drawer: {show: true},
|
|
|
436 |
defaultsendnotifications: sendStudentNotifications,
|
|
|
437 |
}),
|
|
|
438 |
]);
|
|
|
439 |
|
|
|
440 |
const graderContainer = graderLayout.getContainer();
|
|
|
441 |
|
|
|
442 |
const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);
|
|
|
443 |
|
|
|
444 |
Templates.replaceNodeContents(graderContainer, html, js);
|
|
|
445 |
const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser, saveGradeFunction);
|
|
|
446 |
|
|
|
447 |
const userIds = userList.map(user => user.id);
|
|
|
448 |
const statusContainer = graderContainer.querySelector(Selectors.regions.statusContainer);
|
|
|
449 |
// Fetch the userpicker for display.
|
|
|
450 |
const userPicker = await getUserPicker(
|
|
|
451 |
userList,
|
|
|
452 |
async(user) => {
|
|
|
453 |
const userGrade = await updateUserContent(user);
|
|
|
454 |
const renderContext = {
|
|
|
455 |
status: userGrade.hasgrade,
|
|
|
456 |
index: userIds.indexOf(user.id) + 1,
|
|
|
457 |
total: userList.length
|
|
|
458 |
};
|
|
|
459 |
Templates.render(templateNames.grader.status, renderContext).then(html => {
|
|
|
460 |
statusContainer.innerHTML = html;
|
|
|
461 |
return html;
|
|
|
462 |
}).catch();
|
|
|
463 |
},
|
|
|
464 |
saveGradeFunction,
|
|
|
465 |
{
|
|
|
466 |
initialUserId,
|
|
|
467 |
},
|
|
|
468 |
);
|
|
|
469 |
|
|
|
470 |
// Register all event listeners.
|
|
|
471 |
registerEventListeners(graderLayout, userPicker, saveGradeFunction, userList);
|
|
|
472 |
|
|
|
473 |
// Display the newly created user picker.
|
|
|
474 |
displayUserPicker(graderContainer, userPicker.rootNode);
|
|
|
475 |
};
|
|
|
476 |
|
|
|
477 |
/**
|
|
|
478 |
* Show the grade for a specific user.
|
|
|
479 |
*
|
|
|
480 |
* @param {Function} getGradeForUser A function get the grade details for a specific user
|
|
|
481 |
* @param {Number} userid The ID of a specific user
|
|
|
482 |
* @param {String} moduleName the name of the module
|
|
|
483 |
* @param {object} param
|
|
|
484 |
* @param {null|HTMLElement} param.focusOnClose
|
|
|
485 |
*/
|
|
|
486 |
export const view = async(getGradeForUser, userid, moduleName, {
|
|
|
487 |
focusOnClose = null,
|
|
|
488 |
} = {}) => {
|
|
|
489 |
|
|
|
490 |
const userGrade = await getGradeForUser(userid);
|
|
|
491 |
|
|
|
492 |
const [
|
|
|
493 |
modal,
|
|
|
494 |
gradeTemplateData
|
|
|
495 |
] = await Promise.all([
|
|
|
496 |
Modal.create({
|
|
|
497 |
title: moduleName,
|
|
|
498 |
large: true,
|
|
|
499 |
removeOnClose: true,
|
|
|
500 |
returnElement: focusOnClose,
|
|
|
501 |
show: true,
|
|
|
502 |
body: Templates.render('mod_forum/local/grades/view_grade', userGrade),
|
|
|
503 |
}),
|
|
|
504 |
renderGradeTemplate(userGrade)
|
|
|
505 |
]);
|
|
|
506 |
|
|
|
507 |
const bodyPromise = await modal.getBodyPromise();
|
|
|
508 |
const gradeReplace = bodyPromise[0].querySelector('[data-region="grade-template"]');
|
|
|
509 |
Templates.replaceNodeContents(gradeReplace, gradeTemplateData.html, gradeTemplateData.js);
|
|
|
510 |
};
|
|
|
511 |
|
|
|
512 |
const renderGradeTemplate = (userGrade) => Templates.renderForPromise(userGrade.templatename, userGrade.grade);
|
|
|
513 |
|
|
|
514 |
export {getGradingPanelFunctions};
|