Greasy Fork is available in English.
Better moyenne experience
// ==UserScript==
// @name Esprit Moyenne
// @namespace http://tampermonkey.net/
// @version 2026-02-18
// @description Better moyenne experience
// @author NotSoHealthy
// @match https://esprit-tn.com/esponline/Etudiants/Resultat2021.aspx
// @icon https://www.google.com/s2/favicons?sz=64&domain=esprit-tn.com
// @grant none
// @license MIT
// ==/UserScript==
const matieres = [
{
name: "Advanced Big Data",
cc: 0.4,
tp: 0,
exam: 0.6
},
{
name: "Architecture des SI I",
cc: 0.6,
tp: 0,
exam: 0.4
},
{
name: "Architecture des SI II",
cc: 0.6,
tp: 0,
exam: 0.4
},
{
name: 'Architecture Orientée Services "SOA"',
cc: 0,
tp: 0.2,
exam: 0.8
},
{
name: "Communication, Culture et Citoyenneté F4"
},
{
name: "Complexité appliquée à la RO",
cc: 0.4,
tp: 0,
exam: 0.6
},
{
name: "DEVOPS",
cc: 0.6,
tp: 0,
exam: 0.4
},
{
name: "Droit de la propriété intellectuelle",
cc: 0,
tp: 0,
exam: 1
},
{
name: "Gestion de projet",
cc: 0.4,
tp: 0,
exam: 0.6
},
{
name: "Graphes et applications",
cc: 0,
tp: 0.2,
exam: 0.8
},
{
name: "Initiation application côté client",
cc: 0,
tp: 0.2,
exam: 0.8
},
{
name: "Programmation linéaire",
cc: 0,
tp: 0.2,
exam: 0.8
}
];
(function() {
'use strict';
const style = document.createElement('style');
style.innerHTML = `
.row{
margin: 0;
}
.table-header {
color: white;
background-color: #A80000;
font-weight: bold;
}
.moyenne-high {
background-color: green;
color: white;
}
.moyenne-low {
background-color: red;
color: white;
}
#ContentPlaceHolder1_GridView1 {
width: 100%;
border-collapse: collapse;
}
#ContentPlaceHolder1_GridView1 th,
#ContentPlaceHolder1_GridView1 td {
border: 1px solid #ddd;
padding: 8px;
}
#ContentPlaceHolder1_GridView1 tr:nth-child(even) {
background-color: #f2f2f2;
}
#ContentPlaceHolder1_GridView1 tr:hover {
background-color: #ddd;
}
#ContentPlaceHolder1_GridView1 .table-header:hover {
background-color: #A80000;;
}
#downloadButton {
margin-bottom: 10px;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
background-color: #A80000;
color: white;
border: none;
border-radius: 5px;
transition: transform 0.2s ease;
}
#downloadButton:hover {
transform: scale(1.05);
background-color: #CC0000;
}
`;
document.head.appendChild(style);
function loadScript(src) {
return new Promise(function(resolve, reject) {
if (document.querySelector(`script[src="${src}"]`)) {
resolve();
return;
}
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script ${src}`));
document.head.appendChild(script);
});
}
function getStudentDetails() {
const nameElement = document.getElementById('Label2');
const classElement = document.getElementById('Label3');
const studentName = nameElement ? nameElement.textContent.trim() : 'Unknown Student';
const studentClass = classElement ? classElement.textContent.trim() : 'Unknown Class';
return { studentName, studentClass };
}
function getLogoImage() {
const logoElement = document.querySelector('.img-responsive img');
return logoElement ? logoElement.src : null;
}
function tableToJson(table) {
const data = [];
const headers = Array.from(table.rows[0].cells).map(cell =>
cell.innerText.toLowerCase().replace(/ /g, '')
);
for (let i = 1; i < table.rows.length; i++) {
const row = table.rows[i];
const rowData = {};
headers.forEach((header, index) => {
rowData[header] = row.cells[index].innerText;
});
data.push(rowData);
}
data.forEach(item => {
item.coef = parseFloat(item.coef.replace(',', '.'));
item.note_exam = parseFloat(item.note_exam.replace(',', '.'));
item.note_cc = parseFloat(item.note_cc.replace(',', '.'));
item.note_tp = parseFloat(item.note_tp.replace(',', '.'));
});
return data;
}
function normalizeMatiereName(value) {
return String(value ?? '')
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.replace(/[“”"'’]/g, '')
.replace(/[^a-z0-9]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function isFiniteNumber(n) {
return typeof n === 'number' && Number.isFinite(n);
}
function buildMatieresIndex(list) {
const index = new Map();
(list || []).forEach(m => {
if (!m || !m.name) return;
index.set(normalizeMatiereName(m.name), m);
});
return index;
}
const matieresIndex = buildMatieresIndex(matieres);
function getWeightedMoyenne({ note_exam, note_cc, note_tp }, weights) {
let wExam = isFiniteNumber(weights.exam) ? weights.exam : 0;
let wCc = isFiniteNumber(weights.cc) ? weights.cc : 0;
let wTp = isFiniteNumber(weights.tp) ? weights.tp : 0;
// If a note is missing, drop its weight and renormalize to the remaining notes.
if (Number.isNaN(note_exam)) wExam = 0;
if (Number.isNaN(note_cc)) wCc = 0;
if (Number.isNaN(note_tp)) wTp = 0;
const sum = wExam + wCc + wTp;
if (sum <= 0) return null;
wExam /= sum;
wCc /= sum;
wTp /= sum;
const safeExam = Number.isNaN(note_exam) ? 0 : note_exam;
const safeCc = Number.isNaN(note_cc) ? 0 : note_cc;
const safeTp = Number.isNaN(note_tp) ? 0 : note_tp;
return safeExam * wExam + safeCc * wCc + safeTp * wTp;
}
function getFallbackMoyenne(item) {
const { note_exam, note_cc, note_tp } = item;
let moyenne = 0;
if (Number.isNaN(note_tp)) {
if (Number.isNaN(note_cc)) {
moyenne = note_exam;
} else {
moyenne = note_exam * 0.6 + note_cc * 0.4;
if (item.designation === 'Génie logiciel & atelier GL') {
moyenne = note_exam * 0.4 + note_cc * 0.6;
}
}
} else if (Number.isNaN(note_cc)) {
moyenne = note_exam * 0.8 + note_tp * 0.2;
} else {
moyenne = note_exam * 0.5 + note_cc * 0.3 + note_tp * 0.2;
}
return moyenne;
}
function calculMoyenne(dataSet, totalCoef) {
let total = 0;
dataSet.forEach(item => {
const { note_exam, note_cc, note_tp, coef } = item;
// Try to use explicit weights from matieres ("when possible").
const key = normalizeMatiereName(item.designation);
const matiere = matieresIndex.get(key);
const hasAnyWeights = !!matiere && (isFiniteNumber(matiere.exam) || isFiniteNumber(matiere.cc) || isFiniteNumber(matiere.tp));
let moyenne = null;
if (hasAnyWeights) {
moyenne = getWeightedMoyenne(
{ note_exam, note_cc, note_tp },
{ exam: matiere.exam, cc: matiere.cc, tp: matiere.tp }
);
}
// If we can't compute from matieres (no match / no weights / weights don't apply), use existing rules.
if (moyenne === null) {
moyenne = getFallbackMoyenne(item);
}
item.moyenne = moyenne;
total += moyenne * coef;
});
dataSet.push({
designation: 'Moyenne',
coef: totalCoef,
nom_ens: '',
note_cc: '',
note_tp: '',
note_exam: '',
moyenne: total / totalCoef
});
return dataSet;
}
function populateTable(table, dataSet) {
let title = document.querySelector('.col-xs-10 > center > h1');
title.innerHTML = title.innerHTML + ': ' + (document.querySelectorAll('tr').length - 1).toString();
document.querySelectorAll('.row ~ br').forEach((e) => e.remove());
let tableContent = `
<tr class="table-header">
<th scope="col">DESIGNATION</th>
<th scope="col">COEF</th>
<th scope="col">NOM_ENS</th>
<th scope="col">NOTE_CC</th>
<th scope="col">NOTE_TP</th>
<th scope="col">NOTE_EXAM</th>
<th scope="col">Moyenne</th>
</tr>`;
dataSet.forEach(item => {
const moyenneClass = !Number.isNaN(item.moyenne)
? item.moyenne >= 8 ? 'moyenne-high' : 'moyenne-low'
: '';
tableContent += `
<tr>
<td>${item.designation}</td>
<td>${item.coef}</td>
<td>${item.nom_ens}</td>
<td>${Number.isNaN(item.note_cc) ? '' : item.note_cc}</td>
<td>${Number.isNaN(item.note_tp) ? '' : item.note_tp}</td>
<td>${Number.isNaN(item.note_exam) ? '' : item.note_exam}</td>
<td class="${moyenneClass}">${!Number.isNaN(item.moyenne) ? item.moyenne.toFixed(2) : ''}</td>
</tr>`;
});
const tbody = table.querySelector('tbody') || table;
tbody.innerHTML = tableContent;
addDownloadButton(table);
}
function addDownloadButton(table) {
const existing = document.getElementById('downloadButton');
if (existing) return;
const downloadButton = document.createElement('button');
downloadButton.textContent = 'Download as PDF';
downloadButton.id = 'downloadButton';
downloadButton.type = 'button';
table.parentNode.insertBefore(downloadButton, table);
downloadButton.addEventListener('click', function(event) {
event.preventDefault();
generatePDF();
});
}
async function generatePDF() {
try {
const { studentName, studentClass } = getStudentDetails();
const logoSrc = getLogoImage();
const element = document.getElementById('ContentPlaceHolder1_GridView1');
const canvas = await html2canvas(element, {
useCORS: true,
allowTaint: true
});
const imgData = canvas.toDataURL('image/png');
const pdf = new window.jspdf.jsPDF('p', 'pt', 'a4');
pdf.setFontSize(16);
pdf.text(`Student Name: ${studentName}`, 20, 20);
pdf.setTextColor(204, 0, 0);
pdf.text(`Class: ${studentClass}`, 20, 40);
if (logoSrc) {
const logoImage = new Image();
logoImage.src = logoSrc;
pdf.addImage(logoImage, 'PNG', pdf.internal.pageSize.getWidth() - 80, 10, 60, 60);
}
pdf.setTextColor(0, 0, 0);
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
pdf.addImage(imgData, 'PNG', 0, 70, pdfWidth, pdfHeight);
pdf.save(`${studentName} Grades.pdf`);
} catch (error) {
console.error('Could not generate PDF:', error);
alert('Could not generate PDF: ' + error.message);
}
}
function newDropCheck(dataset) {
let tableList = Array.from(document.querySelectorAll('#ContentPlaceHolder1_GridView1 tr td:nth-child(1)'));
tableList.pop();
let oldDrop = JSON.parse(localStorage.getItem('oldDrop')) || [];
dataset.pop();
let newDrop = dataset.map((e) => e.designation);
localStorage.setItem('oldDrop', JSON.stringify(newDrop));
let newItems = newDrop.filter(n => !oldDrop.includes(n));
newItems.forEach((item) => {
tableList.forEach((element) => {
if (element.textContent === item) {
element.textContent += ' 🔴';
}
});
});
}
(async function main() {
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js');
const table = document.getElementById('ContentPlaceHolder1_GridView1');
if (!table) return;
const data = tableToJson(table);
const sumCoef = data.reduce((sum, item) => sum + item.coef, 0);
const newData = calculMoyenne(data, sumCoef);
populateTable(table, newData);
newDropCheck(newData);
})();
})();