diff --git a/classroom/assets/css/main.css b/classroom/assets/css/main.css
index ddc97aee..9fea75de 100644
--- a/classroom/assets/css/main.css
+++ b/classroom/assets/css/main.css
@@ -937,6 +937,12 @@ button {
+/* Skip rendering of off-screen rows — browser handles it natively, no JS needed */
+#body-table-teach tr {
+ content-visibility: auto;
+ contain-intrinsic-size: 0 44px;
+}
+
#body-table-teach .propic {
width: 36px;
height: 36px;
diff --git a/classroom/assets/js/main/ClassroomManager.js b/classroom/assets/js/main/ClassroomManager.js
index 9525db8a..b260241b 100644
--- a/classroom/assets/js/main/ClassroomManager.js
+++ b/classroom/assets/js/main/ClassroomManager.js
@@ -286,6 +286,14 @@ class ClassroomManager {
classroom.classroom.name = `${classroom.classroom.name} | ${classroom.classroom.groupe}`;
}
}
+ // Preserve already lazy-loaded students so they are not wiped
+ const oldClasses = container._myClasses || [];
+ for (let newEntry of response) {
+ const existing = oldClasses.find(c => c.classroom.id === newEntry.classroom.id);
+ if (existing && existing.students.length > 0) {
+ newEntry.students = existing.students;
+ }
+ }
container._myClasses = response;
Main.getClassroomManager().countClassroomAndStudents(response);
onEnd();
@@ -310,11 +318,38 @@ class ClassroomManager {
};
data.forEach((classroom) => {
teacherData.classrooms++;
- teacherData.students += classroom.students.length;
+ // Use server-provided studentCount (students array is lazy-loaded)
+ teacherData.students += classroom.studentCount ?? classroom.students.length;
});
UserManager.getUser().teacherData = teacherData;
}
+ /**
+ * Load full student data for one classroom (lazy, called on click).
+ * Stores result in _myClasses so subsequent clicks don't re-fetch.
+ */
+ getClassroomStudents(classroomId) {
+ return new Promise((resolve, reject) => {
+ $.ajax({
+ type: 'POST',
+ dataType: 'JSON',
+ url: '/routing/Routing.php?controller=classroom&action=get_classroom_students',
+ data: { classroomId: classroomId },
+ success: (response) => {
+ if (response.error) {
+ console.error('getClassroomStudents error:', response.error);
+ resolve([]);
+ return;
+ }
+ const entry = this._myClasses.find(c => c.classroom.id == classroomId);
+ if (entry) entry.students = response.students;
+ resolve(response.students);
+ },
+ error: (e) => { console.error(e); reject(e); }
+ });
+ });
+ }
+
/**
* Get teachers from the classroom
* Access with Main.getClassroomManager()._myTeachers
diff --git a/classroom/assets/js/scripts/buttons.js b/classroom/assets/js/scripts/buttons.js
index 8af9555c..59c1cee5 100644
--- a/classroom/assets/js/scripts/buttons.js
+++ b/classroom/assets/js/scripts/buttons.js
@@ -1077,7 +1077,7 @@ function classroomsDisplay() {
let classes = Main.getClassroomManager()._myClasses;
if (classes.length) {
classes.forEach(classroom => {
- $('.list-classes').append(classeItem(classroom.classroom, classroom.students.length, classroom.students));
+ $('.list-classes').append(classeItem(classroom.classroom, classroom.studentCount ?? classroom.students.length, classroom.students, classroom.pendingCorrections, classroom.activitiesCount));
});
} else {
$('.list-classes').append(noContentDiv).localize();
@@ -1088,7 +1088,7 @@ function classroomsDisplay() {
let classes = Main.getClassroomManager()._myClasses;
if (classes.length) {
classes.forEach(classroom => {
- $('.list-classes').append(classeItem(classroom.classroom, classroom.students.length, classroom.students));
+ $('.list-classes').append(classeItem(classroom.classroom, classroom.studentCount ?? classroom.students.length, classroom.students, classroom.pendingCorrections, classroom.activitiesCount));
});
} else {
$('.list-classes').append(noContentDiv).localize();
diff --git a/classroom/assets/js/scripts/classroomDisplay.js b/classroom/assets/js/scripts/classroomDisplay.js
index 3275ffcd..aecfb830 100644
--- a/classroom/assets/js/scripts/classroomDisplay.js
+++ b/classroom/assets/js/scripts/classroomDisplay.js
@@ -78,11 +78,13 @@ $('body').on('change', '#is-anonymised', function () {
});
function anonymizeStudents() {
- $('.username').each(function (index,el) {
+ $('.username').each(function (index, el) {
+ // Use absolute student index embedded during virtual scroll rendering
+ const absIdx = parseInt($(el).closest('tr').attr('data-student-idx') ?? index);
$(el).children().children('img').attr('src', _PATH + 'assets/media/alphabet/E.png')
- $(el).children().children('img').attr('alt', 'Photo de profil anonymisée - Étudiant ' + (index + 1))
+ $(el).children().children('img').attr('alt', 'Photo de profil anonymisée - Étudiant ' + (absIdx + 1))
$(el).children().children('img').attr('anonymized', 'true')
- $(el).children().children('.user-cell-username').text(i18next.t('classroom.activities.anoStudent') + " " + index)
+ $(el).children().children('.user-cell-username').text(i18next.t('classroom.activities.anoStudent') + " " + absIdx)
$(el).children().children('.user-cell-username').attr('title', '')
})
}
diff --git a/classroom/assets/js/scripts/dashboardActivities.js b/classroom/assets/js/scripts/dashboardActivities.js
index eb361165..08faa901 100644
--- a/classroom/assets/js/scripts/dashboardActivities.js
+++ b/classroom/assets/js/scripts/dashboardActivities.js
@@ -342,7 +342,7 @@ function teacherFolder(folder, displayStyle) {
}
-function classeItem(classe, nbStudents, students) {
+function classeItem(classe, nbStudents, students, overridePendingCorrections = null, overrideActivitiesCount = null) {
function maxLength(array) {
let count = 0
for (let i = 0; i < array.length; i++) {
@@ -353,8 +353,8 @@ function classeItem(classe, nbStudents, students) {
return count
}
- let maxAct = maxLength(students)
- let remainingCorrections = getRemainingCorrections(students);
+ let maxAct = overrideActivitiesCount !== null ? overrideActivitiesCount : maxLength(students);
+ let remainingCorrections = overridePendingCorrections !== null ? overridePendingCorrections : getRemainingCorrections(students);
let remainingCorrectionsSpanElt = remainingCorrections ? ` ${remainingCorrections}` : '';
let html = `
diff --git a/classroom/assets/js/scripts/displayPanel.js b/classroom/assets/js/scripts/displayPanel.js
index 3c4ae2d6..331784c3 100644
--- a/classroom/assets/js/scripts/displayPanel.js
+++ b/classroom/assets/js/scripts/displayPanel.js
@@ -27,7 +27,10 @@ DisplayPanel.prototype.classroom_dashboard_profil_panel_teacher = function () {
})
getIntelFromClasses();
- const correctionsCount = getRemainingCorrections(Main.getClassroomManager()._myClasses.flatMap(c => c.students));
+ // pendingCorrections is pre-computed server-side in the lightweight get_by_user response
+ const correctionsCount = Main.getClassroomManager()._myClasses.reduce(
+ (sum, c) => sum + (c.pendingCorrections ?? getRemainingCorrections(c.students ?? [])), 0
+ );
const correctionsElement = $('.tocorrect-activities');
correctionsElement.html(correctionsCount);
@@ -468,7 +471,16 @@ function updateUIWithStudents(link) {
updateClassroomLinkUI(ClassroomSettings.classroom);
updateClassroomLockUI(classroomData.classroom.isBlocked);
- displayStudentsInClassroom(classroomData.students, link);
+
+ if (classroomData.students && classroomData.students.length > 0) {
+ // Already loaded (cached from a previous click)
+ displayStudentsInClassroom(classroomData.students, link);
+ } else {
+ // Lazy-load: first click on this classroom
+ Main.getClassroomManager().getClassroomStudents(classroomData.classroom.id).then(students => {
+ displayStudentsInClassroom(students, link);
+ });
+ }
}
DisplayPanel.prototype.classroom_dashboard_new_activity_panel3 = function (ref) {
diff --git a/classroom/assets/js/scripts/manageClassroom.js b/classroom/assets/js/scripts/manageClassroom.js
index 50b723ed..f9c0a0b1 100644
--- a/classroom/assets/js/scripts/manageClassroom.js
+++ b/classroom/assets/js/scripts/manageClassroom.js
@@ -1080,7 +1080,8 @@ function displayStudentsInClassroom(students, link=false) {
// get the current classroom index of activities
let arrayIndexesActivities = listIndexesActivities(students);
- students.forEach(element => {
+ const studentRows = [];
+ students.forEach((element, elementIdx) => {
// reorder the current student activities to fit to the classroom index of activities
let arrayActivities = reorderActivities([...element.activities, ...element.courses], arrayIndexesActivities);
let pseudo = element.user.pseudo;
@@ -1370,10 +1371,22 @@ function displayStudentsInClassroom(students, link=false) {
}
// end of the current table row
html += '';
- $('#body-table-teach').append(html).localize();
- $('[data-bs-toggle="tooltip"]').tooltip()
+ studentRows.push(html);
});
-
+
+ // Preload unique letter images (max 26 requests); browser cache serves subsequent rows
+ new Set(students.map(s => getFirstLetterOfPseudo(s.user.pseudo))).forEach(letter => {
+ const _img = new Image();
+ _img.src = `${_PATH}assets/media/alphabet/${letter}.png`;
+ });
+
+ // Render all students directly into the table
+ const tbody = document.getElementById('body-table-teach');
+ const tmpTbody = document.createElement('tbody');
+ tmpTbody.innerHTML = studentRows.join('');
+ Array.from(tmpTbody.children).forEach(el => tbody.appendChild(el));
+ $(tbody).find('[data-bs-toggle="tooltip"]').tooltip();
+ $(tbody).localize();
appendAddStudentButton();
// get classroom settings from localstorage
let settings = getClassroomDisplaySettings(link);
@@ -1418,18 +1431,19 @@ function displayStudentsInClassroom(students, link=false) {
// add four empty divs for monochrome styling
$('#body-table-teach .bilan-cell').html(`
`);
- $('#classroom-panel-table-container table .dropdown').on('show.bs.dropdown', (event) => {
- let classroomTable = event.target.closest('table');
- classroomTable.classList.add('dropdowns-opened');
- $(classroomTable).find('tr').addClass('non-dropdown');
- event.target.closest('tr').classList.remove('non-dropdown');
- });
-
- $('#classroom-panel-table-container table .dropdown').on('hidden.bs.dropdown', (event) => {
- let classroomTable = event.target.closest('table');
- classroomTable.classList.remove('dropdowns-opened');
- $(classroomTable).find('tr').removeClass('non-dropdown');
- });
+ // Delegated event binding so lazy-loaded rows also receive these handlers
+ $('#classroom-panel-table-container table').off('show.bs.dropdown.lazyrows hidden.bs.dropdown.lazyrows')
+ .on('show.bs.dropdown.lazyrows', '.dropdown', (event) => {
+ let classroomTable = event.target.closest('table');
+ classroomTable.classList.add('dropdowns-opened');
+ $(classroomTable).find('tr').addClass('non-dropdown');
+ event.target.closest('tr').classList.remove('non-dropdown');
+ })
+ .on('hidden.bs.dropdown.lazyrows', '.dropdown', (event) => {
+ let classroomTable = event.target.closest('table');
+ classroomTable.classList.remove('dropdowns-opened');
+ $(classroomTable).find('tr').removeClass('non-dropdown');
+ });
// Plugin hook: post-render callback (no-op without plugin)
if (typeof window.onDashboardRendered === 'function') {
@@ -1994,27 +2008,20 @@ class DashboardAutoRefresh {
refresh() {
if($_GET('panel') == 'classroom-table-panel-teacher' && $_GET('option')){
this.isRefreshing = true;
- let previousClassroomState, newClassroomState;
- if (getClassroomInListByLink($_GET('option'))[0]) {
- previousClassroomState = {
- data: JSON.stringify(getClassroomInListByLink($_GET('option'))[0].students),
- link: $_GET('option')
- };
- }
- Main.getClassroomManager().getClasses(Main.getClassroomManager()).then(() => {
- if ($_GET('option') == previousClassroomState.link) {
- if (getClassroomInListByLink($_GET('option'))[0]) {
- newClassroomState = JSON.stringify(getClassroomInListByLink($_GET('option'))[0].students);
- }
- // Only refresh the classroom if it has changed
- if (previousClassroomState.data != newClassroomState){
- if (getClassroomInListByLink($_GET('option'))[0]) {
- let students = getClassroomInListByLink($_GET('option'))[0].students;
+ const link = $_GET('option');
+ const entry = getClassroomInListByLink(link)?.[0];
+ if (entry) {
+ const previousStudentsJson = JSON.stringify(entry.students);
+ Main.getClassroomManager().getClassroomStudents(entry.classroom.id).then((students) => {
+ if ($_GET('option') == link) {
+ const newStudentsJson = JSON.stringify(students);
+ // Only refresh the classroom display if data has changed
+ if (previousStudentsJson != newStudentsJson) {
displayStudentsInClassroom(students);
}
}
- }
- });
+ });
+ }
setTimeout(() => { this.refresh() }, this.refreshInterval);
} else {
this.isRefreshing = false;
diff --git a/classroom/assets/js/scripts/teacherActivities.js b/classroom/assets/js/scripts/teacherActivities.js
index 2c6e0a51..5105d48d 100644
--- a/classroom/assets/js/scripts/teacherActivities.js
+++ b/classroom/assets/js/scripts/teacherActivities.js
@@ -380,13 +380,18 @@ function listStudentsToAttribute(ref = null) {
if (classes.length == 0) {
$('#attribute-activity-to-students-close').after(NO_CLASS)
$('#attribute-activity-to-students-close').hide()
-
} else {
- classes.forEach(element => {
- $('#list-student-attribute-modal').append(classeList(element, ref))
+ const pending = classes
+ .filter(c => c.students.length === 0)
+ .map(c => Main.getClassroomManager().getClassroomStudents(c.classroom.id));
+
+ Promise.all(pending).then(() => {
+ classes.forEach(element => {
+ $('#list-student-attribute-modal').append(classeList(element, ref))
+ });
+ $('.no-classes').remove()
+ $('#attribute-activity-to-students-close').show()
});
- $('.no-classes').remove()
- $('#attribute-activity-to-students-close').show()
}
}
diff --git a/composer.json b/composer.json
index 38f83c1b..3e17f1da 100644
--- a/composer.json
+++ b/composer.json
@@ -12,9 +12,9 @@
},
"require-dev": {
"phpunit/phpunit": "8.0.0",
- "vittascience/vtuser": "1.3.5",
+ "vittascience/vtuser": "1.3.6",
"vittascience/vtinterfaces": "1.5.8",
- "vittascience/vtclassroom": "1.4.3",
+ "vittascience/vtclassroom": "dev-arz-optimisation",
"vittascience/vtlearn": "1.4.6",
"vittascience/vutils": "1.3.1",
"symfony/var-dumper": "^5.3"