diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/admin-tools.js | 85 | ||||
| -rw-r--r-- | src/graduation-common.ts | 54 |
2 files changed, 96 insertions, 43 deletions
diff --git a/src/admin-tools.js b/src/admin-tools.js index 116b5ee..b3135b1 100644 --- a/src/admin-tools.js +++ b/src/admin-tools.js @@ -1,9 +1,13 @@ import { GraduationChange, submitGraduationChange } from "./graduation-ENV.ts"; +import { findDuplicateChanges } from "./graduation-common.ts"; import { fetchStudent, fetchStudents } from "./student-ENV.ts"; import { fetchCourses } from "./moodle-ENV.ts"; import { DB } from "./database-ENV.ts"; import { DATETIME_FMT } from "./utils-common.ts"; +import JSZip from "jszip"; +import FileSaver from "file-saver"; + let outputNode; function output(msg) { if (outputNode.style.display === "none") { @@ -83,53 +87,19 @@ async function findDuplicates(course) { const entries = await db.fetchEntries(); const changes = entries.map((e) => GraduationChange.fromDBEntry(e)); - const searched = new Set(); - const duplicates = []; - for (const change of changes) { - if (searched.has(change)) continue; - searched.add(change); - - const curDuplicates = [change]; - for (const other of changes) { - if (other === change) continue; - - let same = true; - for (const key in change) { - switch (key) { - case "changeId": - continue; - case "date": - same = same && change.date.getTime() === other.date.getTime(); - break; - case "context": - same = same && JSON.stringify(change.context) === JSON.stringify(other.context); - break; - default: - same = same && change[key] === other[key]; - } - if (!same) break; - } - - if (same) { - curDuplicates.push(other); - searched.add(other); - } - } + const duplicates = findDuplicateChanges(changes); - if (curDuplicates.length > 1) { - duplicates.push(curDuplicates); + if (duplicates.length !== 0) { + let msg = `${course.title} Duplikate:<br>`; + for (const d of duplicates) { + const ids = d.map((e) => e.changeId); + const c = d[0]; + msg += `- ${DATETIME_FMT.format(c.date)} ${c.coach} ${c.studentId} ${c.change} ${JSON.stringify(c.context)} ids: ${ids}<br>`; } + output(msg + "\n"); } - if (duplicates.length === 0) return; - - let msg = `${course.title} Duplikate:<br>`; - for (const d of duplicates) { - const ids = d.map((e) => e.changeId); - const c = d[0]; - msg += `- ${DATETIME_FMT.format(c.date)} ${c.coach} ${c.studentId} ${c.change} ${JSON.stringify(c.context)} ids: ${ids}<br>`; - } - output(msg + "\n"); + return [course, duplicates]; } async function findAllDuplicates() { @@ -141,6 +111,34 @@ async function findAllDuplicates() { } } +async function exportGraduations(course) { + const dbActivity = await course.getActivity("Graduation"); + const db = new DB(dbActivity.id); + const exportResponse = await db.exportEntries(); + return [course, await exportResponse.text()]; +} + +async function exportAllGraduations() { + output("loading courses ..."); + const courses = await fetchCourses("SOL Graduierung"); + output(`exporting graduations of ${courses.length} courses ... `); + const promises = []; + for (const course of courses) { + promises.push(exportGraduations(course)); + } + const results = await Promise.all(promises); + + const zip = new JSZip(); + + for (const [course, entries] of results) { + zip.file(`${course.title}.csv`, entries); + } + + zip.generateAsync({ type: "blob" }).then(function (content) { + FileSaver.saveAs(content, "graduation-db-exports.zip"); + }); +} + async function init() { outputNode = document.getElementById("admin-tools-output"); output("Admin tools ready."); @@ -153,6 +151,7 @@ async function init() { output(JSON.stringify(students.map((s) => s.id))); }); document.getElementById("find-duplicates").addEventListener("click", findAllDuplicates); + document.getElementById("export-graduation-dbs").addEventListener("click", exportAllGraduations); } if (document.readyState === "loading") { diff --git a/src/graduation-common.ts b/src/graduation-common.ts index 2d1f21d..fe244bc 100644 --- a/src/graduation-common.ts +++ b/src/graduation-common.ts @@ -248,6 +248,60 @@ async function deleteGraduationChange(graduation: Graduation, change: Graduation return await db.deleteEntries([change.changeId]); } +export function findDuplicateChanges( + changes: Array<GraduationChange>, +): Array<Array<GraduationChange>> { + // Sets of potential duplicates + const duplicates: Array<Array<GraduationChange>> = []; + + // Sort the changes first by studentId and than by time + changes.sort((a, b): number => { + // Sort by studentId first + if (a.studentId !== b.studentId) return a.studentId - b.studentId; + return a.date.getTime() - b.date.getTime(); + }); + + let candidates = []; + outer_loop: for (let i = 0; i < changes.length - 1; i++) { + const change = changes[i]; + const other = changes[i + 1]; + + for (const key in change) { + let similar; + switch (key) { + case "changeId": + continue; + case "date": + // Are the dates less than an hour apart? + similar = Math.abs(change.date.getTime() - other.date.getTime()) < 24 * 60 * 60 * 1000; + break; + case "context": + similar = JSON.stringify(change.context) === JSON.stringify(other.context); + break; + default: + const tkey = key as keyof GraduationChange; + similar = change[tkey] === other[tkey]; + } + if (!similar) { + // The input is sorted duplicates must be in consecutive order. + if (candidates.length > 0) { + duplicates.push(candidates); + candidates = []; + } + continue outer_loop; + } + } + + // Found candidates + if (candidates.length === 0) { + candidates.push(change, other); // New candidate pair + } else { + candidates.push(other); // Additional candidate found + } + } + return duplicates; +} + export { LEVEL_NAMES, LEVEL_IMAGES, |
