Calificar las entregas pendientes
Un programa para un profesor que recorre las entregas de una tarea que esperan calificación, muestra el contenido de cada una en el terminal y guarda la calificación y la retroalimentación que escribe el profesor, notificando al estudiante. Un segundo programa hace lo mismo con una tarea calificada con una rúbrica.
Usa: calificación, archivos.
El programa
import { createInterface } from "node:readline/promises";
import { createMoodle, FormError, GradingError } from "@falkenslab/moodle-driver";
// El id de actividad de la tarea: el `id` de mod/assign/view.php?id=45, o las actividades de courseStructure().
const ASSIGNMENT_ID = 45;
await using moodle = createMoodle({ site: "https://campus.example.org/", profileDir: "/home/teacher/.moodle-profile", headless: true, downloadBrowser: true });
await moodle.logIn();
const terminal = createInterface({ input: process.stdin, output: process.stdout });
// Un panel de calificación por participante con entrega: lento en un curso grande, pero trae el contenido.
const submissions = await moodle.assignmentSubmissions(ASSIGNMENT_ID);
// `needsGrading` es el "Requiere calificación" de Moodle: enviada, y sin calificar desde entonces.
const pending = submissions.filter((s) => s.needsGrading);
console.log(`${pending.length} de ${submissions.length} entregas por calificar.`);
let graded = 0;
for (const submission of pending) {
console.log(`\n== ${submission.fullName} (usuario ${submission.userId}), ${submission.status}`);
// Una entrada por plugin de entrega: las palabras del texto en línea, los enlaces de los archivos subidos.
for (const { plugin, text, files } of submission.content ?? []) {
if (text) console.log(`[${plugin}]\n${text}`);
for (const file of files) console.log(`[${plugin}] ${file.name}: ${file.url}`);
}
const grade = (await terminal.question("Calificación (vacío para saltar): ")).trim();
if (!grade) continue;
const comment = (await terminal.question("Retroalimentación: ")).trim();
try {
const result = await moodle.gradeSubmission(ASSIGNMENT_ID, submission.userId, {
// Tal como se escribe: "7.5", o "7,5" en un sitio que escribe los decimales con coma.
grade,
...(comment ? { feedback: `<p>${comment}</p>` } : {}),
// Moodle notifica al estudiante que hay retroalimentación en la tarea.
notify: true,
});
console.log(`${result.changed} Calificación actual: ${result.grade ?? "-"}`);
graded++;
} catch (error) {
// Una calificación fuera de rango, o un valor que el formulario de Moodle rechaza: se informa y se sigue con la siguiente.
if (error instanceof GradingError || error instanceof FormError) console.error(`No se ha guardado: ${error.message}`);
else throw error;
}
}
terminal.close();
console.log(`\nCalificadas ${graded} de ${pending.length}.`);
Paso a paso
ASSIGNMENT_IDes el id de actividad de la tarea (id del módulo del curso): el número de su URL,mod/assign/view.php?id=45, o elidde la actividad encourseStructure(courseId).logIn()vuelve al instante cuando el perfil tiene una sesión. La primera vez se abre una ventana del navegador en la página de acceso del sitio y el profesor inicia sesión allí; consulta Iniciar sesión.assignmentSubmissionsenumera todos los participantes, con el estado, las fechas y, para las entregas enviadas y en borrador, el contenido por plugin de entrega. Lee un participante cada vez, así que un curso de 100 estudiantes tarda un rato.- El programa se queda con las entregas que según Moodle requieren calificación e imprime cada una: el texto en línea como texto plano, y los archivos subidos como enlaces (descárgalos con
moodle.downloadFile(url, folder)para abrirlos). - El profesor escribe una calificación, o nada para saltar a ese estudiante, y un comentario.
gradeSubmissionrellena el panel del calificador para ese estudiante, lo envía como lo hace el calificador de Moodle y vuelve a leer el panel:result.gradees la calificación guardada. - Con
notify: true, Moodle notifica al estudiante que se ha publicado retroalimentación (por su mensajería, en la siguiente ejecución del cron del sitio). La calificación pasa al libro de calificaciones salvo que la tarea use un flujo de trabajo de calificación que aún no la haya publicado. await usingcierra el navegador (y guarda la sesión) cuando termina el programa, también tras un error.
Lo que imprime
Una ejecución de ejemplo:
3 de 24 entregas por calificar.
== Ada Lovelace (usuario 7), submitted
[onlinetext]
Una función recursiva necesita un caso base. En mi solución el caso base es n = 0...
Calificación (vacío para saltar): 8.5
Retroalimentación: Caso base claro; falta el análisis de complejidad.
Graded user 7 in assignment 45, with feedback. Calificación actual: 8.50
== Alan Turing (usuario 9), submitted
[file] recursividad.pdf: https://campus.example.org/pluginfile.php/312/assignsubmission_file/submission_files/88/recursividad.pdf?forcedownload=1
Calificación (vacío para saltar):
== Grace Hopper (usuario 12), submitted
[onlinetext]
He usado un bucle en vez de recursividad porque...
Calificación (vacío para saltar): 11
Retroalimentación: Bien.
No se ha guardado: Moodle didn't save the grade: ... Check the grade is within the assignment's range or scale, and the fields' names.
Calificadas 1 de 3.
Con una rúbrica
Una tarea calificada con una rúbrica recibe un nivel por criterio en lugar de una calificación. rubric() da los criterios y sus niveles con los ids que recibe gradeSubmission; Moodle calcula la calificación a partir de los niveles elegidos.
import { createInterface } from "node:readline/promises";
import { createMoodle, FormError, GradingError } from "@falkenslab/moodle-driver";
// Una tarea cuyo método de calificación es una rúbrica (su página "Calificación avanzada").
const ASSIGNMENT_ID = 52;
await using moodle = createMoodle({ site: "https://campus.example.org/", profileDir: "/home/teacher/.moodle-profile", headless: true, downloadBrowser: true });
await moodle.logIn();
const terminal = createInterface({ input: process.stdin, output: process.stdout });
// Se lee una vez: los criterios son los mismos para todos los estudiantes.
const definition = await moodle.rubric(ASSIGNMENT_ID);
if (definition.method !== "rubric") throw new Error(`La tarea ${ASSIGNMENT_ID} se califica con "${definition.method}", no con una rúbrica.`);
for (const submission of (await moodle.assignmentSubmissions(ASSIGNMENT_ID)).filter((s) => s.needsGrading)) {
console.log(`\n== ${submission.fullName}`);
for (const { text } of submission.content ?? []) if (text) console.log(text);
// Id del criterio -> id del nivel, elegido por el número del nivel tal como se enumera.
const levels: Record<number, number> = {};
for (const criterion of definition.criteria) {
console.log(`\n${criterion.description}`);
criterion.levels?.forEach((level, i) => console.log(` ${i + 1}. (${level.score}) ${level.definition}`));
const choice = Number(await terminal.question("Nivel: "));
const level = criterion.levels?.[choice - 1];
if (level) levels[criterion.id] = level.id;
}
// Moodle rechaza una rúbrica con un criterio sin nivel: mejor saltar al estudiante.
if (Object.keys(levels).length < definition.criteria.length) {
console.log("No todos los criterios tienen nivel: se salta.");
continue;
}
try {
const result = await moodle.gradeSubmission(ASSIGNMENT_ID, submission.userId, { rubric: levels, feedback: "<p>Consulta la rúbrica para cada criterio.</p>", notify: true });
// La calificación que Moodle calculó a partir de los niveles, tal como la tiene el libro de calificaciones.
console.log(`${result.changed} Libro de calificaciones: ${result.gradebookGrade ?? "-"}`);
} catch (error) {
if (error instanceof GradingError || error instanceof FormError) console.error(`No se ha guardado: ${error.message}`);
else throw error;
}
}
terminal.close();
rubric() lee el formulario de calificación del primer participante de la tarea, así que la tarea necesita al menos uno. El score de un nivel son los puntos que le da la rúbrica; la calificación que Moodle escribe en el libro de calificaciones son esos puntos escalados a la calificación máxima de la tarea, y por eso lo que hay que mostrar es el gradebookGrade del resultado. Un criterio también puede llevar una observación: remarks: { [criterion.id]: "Falta el caso base." } junto a rubric.
Variaciones
- Calificar a partir de una lista que preparó el profesor en vez del terminal: un
Mapdel id de usuario a la calificación y el comentario, ygradeSubmissionpara cada entrada, sin preguntas. - Solo mirar, sin calificar:
assignmentSubmissions(ASSIGNMENT_ID, { includeContent: false })es mucho más rápido cuando solo hacen falta los estados y las fechas. - Calificar sin avisar:
notify: falseguarda la calificación sin notificar al estudiante, para una primera pasada que revisar antes de publicar. - Una guía de evaluación en vez de una rúbrica:
definition.method === "guide", yguide: { [criterion.id]: score }con cada puntuación hasta elmaxScoredel criterio.
Cuando algo sale mal
NotAnAssignmentError: el id no es una tarea, o la persona no puede calificarla (un estudiante, o un profesor de otro curso). Comprueba el id de la actividad y el rol del profesor en el curso.GradingError: Moodle no guardó la calificación, y su servicio no dice por qué. Normalmente es una calificación fuera del rango de la tarea (11 cuando el máximo es 10) o que no está en su escala.rubric()también lo lanza con una tarea sin participantes.FormError: un campo no está en el formulario de calificación, comogradeen una tarea calificada por rúbrica, o un id de criterio o de nivel que no existe;error.fieldsdice cuál.MoodleErrorcon códigonot-available: se diofeedbacky la tarea tiene desactivados los comentarios de retroalimentación. No se guardó nada: califica sinfeedback, o activa los comentarios de retroalimentación en la configuración de la tarea.NotLoggedInError: la sesión caducó a mitad; vuelve a ejecutarlo, ylogIn()abre la ventana. Consulta Errores.
Datos que toca
assignmentSubmissions lee el trabajo de los estudiantes (others-personal), rubric es course-content, y gradeSubmission es others-personal y others-write: el programa decide algo sobre otras personas y Moodle se lo comunica. Consulta Categorías de datos.