// ==UserScript==
// @name Participants: Students' lists
// @name:ca Participants: Llistes d'estudiants
// @name:en Participants: Students' lists
// @name:es Participantes: Listas de estudiantes
// @version 0.7.3
// @author Antonio Bueno <[email protected]>
// @description Generates student/group and student/teacher lists
// @description:ca Genera llistes estudiant/grup i estudiant/professor
// @description:en Generates student/group and student/teacher lists
// @description:es Genera listas estudiante/grupo y estudiante/profesor
// @license MIT
// @namespace https://github.com/buenoudg/Ajudant-UdGMoodle
// @supportURL https://github.com/buenoudg/Ajudant-UdGMoodle/issues
// @match https://moodle.udg.edu/user/index.php?*
// @match https://moodle2.udg.edu/user/index.php?*
// @icon https://raw.githubusercontent.com/buenoudg/Ajudant-UdGMoodle/master/[email protected]
// @require https://cdn.jsdelivr.net/npm/jquery@3/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/toastify-js@1/src/toastify.min.js
// @resource toastifyCSS https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
// @grant GM_addStyle
// @grant GM_getResourceText
// ==/UserScript==
(function () {
"use strict";
const langs = { // localization of text in buttons and downloaded file names
"ca": {
"ALL_TEXT": "Mostra'ls tots",
"SGL_TEXT": "Llista estudiant/grup",
"SPL_TEXT": "Llista estudiant/professor",
"SGL_FILE": "estudiant_grup",
"SPL_FILE": "estudiant_professor"
},
"en": {
"ALL_TEXT": "Show them all",
"SGL_TEXT": "Student/group list",
"SPL_TEXT": "Student/professor list",
"SGL_FILE": "student_group",
"SPL_FILE": "student_professor"
},
"es": {
"ALL_TEXT": "Mostrarlos todos",
"SGL_TEXT": "Lista estudiante/grupo",
"SPL_TEXT": "Lista estudiante/profesor",
"SGL_FILE": "estudiante_grupo",
"SPL_FILE": "estudiante_profesor"
},
};
const info = {}; // to be filled with static page details once the DOM is ready
const tableToCsvDataUrl = (table, lang="en") => { // used to generate the downloadable files
const delimiter = new Intl.NumberFormat(lang).format(1.1).charAt(1) == '.' ? ',' : ';';
// This assumes the data doesn't contain double quotes or line breaks
const csv = table.map(row => row.map(c => c.includes(delimiter) ? `"${c}"` : c).join(delimiter)).join("\r\n");
return `data:text/csv;charset=utf-8,\uFEFF${encodeURIComponent(csv)}`; // MS Excel needs the \uFEFF
};
const getUserData = () => { // used to extract all the relevant data from the main table
let users = [];
$(".userinitials:not([alt])").attr("alt", function() { return $(this).text() }).text("");
$("#participants tbody tr").not(".emptyrow").each(function () {
const $cells = $(this).children();
let user = { "name": $cells.eq(info.column.fullname).text().trim() };
if (info.column.email >= 0) user.mail = $cells.eq(info.column.email).text().trim();
user.groups = $cells.eq(info.column.groups).text().trim().split(", ").filter(g=>info.groups.includes(g));
if (info.column.idnumber >= 0) user.id = $cells.eq(info.column.idnumber).text().trim();
user.role = $cells.eq(info.column.roles).text().trim();
if (user.role == info.noRole) user.role = undefined;
users.push(user);
});
return users;
};
const updateLists = () => { // called everytime the table changes
// (Re-)generate the lists
let users = getUserData(), obj;
const professors = users.filter(user => user.role && (user.role != info.studentRole) && user.groups.length);
const groups = professors.reduce(
(obj, p) => (p.groups.map(g => { if (!obj[g]) obj[g] = []; obj[g].push([p.name, p.id]); }), obj), {}
);
const students = users.filter(user => user.role == info.studentRole);
const SGList = students.map(student => student.groups
.map(group => [info.courseId, student.name, student.id, student.mail, group])
).flat();
const SPList = students.map(student => student.groups
.filter(group => group in groups)
.map(group => groups[group].map(professor => [info.courseId, student.name, student.id, professor].flat()))
.flat()
).flat().filter((obj = {}, item => !(obj[item] = item in obj)));
// Also update the UdGMoodle buttons
if (!$("#udgmoodle_buttons").length) { // button container
$("h2+div").prepend('<div id="udgmoodle_buttons">');
}
$("#udgmoodle_buttons .showall, #udgmoodle_buttons .sglist, #udgmoodle_buttons .splist").remove();
if ($("nav.pagination").length) { // not the full list? link to it
$("#udgmoodle_buttons").append(`<a class="showall" href="${info.fullListUrl}">${info.lang.ALL_TEXT}</a>`);
} else { // already in the full list
if (SGList.length) {
$("#udgmoodle_buttons").append(`<a class="sglist" download="${info.courseId}_${info.lang.SGL_FILE}.csv"
href="${tableToCsvDataUrl(SGList, info.lang.code)}">${info.lang.SGL_TEXT}</a>`);
}
if (SPList.length) {
$("#udgmoodle_buttons").append(`<a class="splist" download="${info.courseId}_${info.lang.SPL_FILE}.csv"
href="${tableToCsvDataUrl(SPList, info.lang.code)}">${info.lang.SPL_TEXT}</a>`);
}
}
};
// CSS for the UdGMoodle buttons
GM_addStyle('' + `
#udgmoodle_buttons { text-align: right; width: 100% }
#udgmoodle_buttons a { background-color: #568; border-radius: 1.2em; color: white; display: inline-block;
margin: 4px 8px 0 0; padding: 0.425rem 1.25rem; text-decoration: none !important }
#udgmoodle_buttons a:hover { background-color: #78A; color: white; text-shadow: 0 1px 2px #000A }
`);
// CSS for the user initials
GM_addStyle('' + `
.userinitials:not([alt]) { color: red }
.userinitials[alt]:before { content: attr(alt) }
`);
/*
* DOM manipulation courtesy of jQuery
*/
// Modifications applied once the DOM has been build (images may be still loading)
$(document).ready(function () {
// Makes clear what script is running and its version
notification(GM.info.script[`name:${navigator.language.slice(0,2)}`] + " " + GM.info.script.version, "hello", 3);
// Watches for changes in the page's main table
const targetNode = document.getElementById("participantsform"); // node to watch
const config = { attributes: false, childList: true, subtree: true }; // observer parameters
const callback = () => { // called when changes are detected
clearTimeout(timer);
timer = setTimeout(updateLists, 1000); // limit calls to once per second
};
const observer = new MutationObserver(callback); // associate the callback to an observer
observer.observe(targetNode, config); // start monitoring the target for changes
// Static details from the initial DOM
info.column = { "fullname": $("#participants tbody tr th").index() };
["idnumber", "email", "roles", "groups"].forEach(i =>
info.column[i] = $(`#participants thead [data-column="${i}"]`).parents("th").index()
);
info.courseId = document.title.split("/")[0];
info.fullListUrl = String(document.location).replace(/(&perpage=\d+|perpage=\d+&)/img, "") + "&perpage=5000";
info.lang = $("html").attr("lang").slice(0,2) || "en";
info.lang = Object.assign({code: info.lang}, langs[info.lang]);
info.noRole = $("select[data-field-name='roles'] option[value='-1']").text();
info.studentRole = $("select[data-field-name='roles'] option[value='5']").text();
info.groups = [...$("select[data-field-name='groups'] option:not([value='-1']")].map(i=>i.text);
// Initial list generation
let timer = setTimeout(updateLists);
});
/*
* Notifications courtesy of Toastify JS (see https://apvarun.github.io/toastify-js/)
*/
GM_addStyle(GM_getResourceText("toastifyCSS") + `
div.toastify { margin: inherit; width: inherit; border-radius: 1.5em; font-family:
-apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif }
`);
function notification(message, type = "info", timeout = 5) {
// The "type" parameter accepts four values: "info", "warning", "error" and "hello"
// The "timeout" parameter is expressed in seconds although Toastify uses milliseconds
let color, icon;
switch (type) {
case "warning": color = "rgba(201, 201, 0, 0.8)"; icon = "⚠️"; break;
case "error": color = "rgba(201, 51, 51, 0.8)"; icon = "🛑"; break;
case "hello": color = "rgba(51, 153, 51, 0.8)"; icon = "👋🏼"; break;
default: color = "rgba(51, 51, 153, 0.8)"; icon = "📢";
}
Toastify({
text: icon + " " + message, duration: timeout * 1000, gravity: "bottom", style: { background: color }
}).showToast();
}
})();