aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2026-10-17 10:23:22 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2026-10-17 10:23:22 +0200
commit198bbebd89d212faae3e45a22830bab66dec49e0 (patch)
tree1b92b64f29e0bc50f5ff845b154450c926b82a69 /src
parent38477bab6c1c736a2d1e7dd79b75c6e122502622 (diff)
downloadbycs-sol-198bbebd89d212faae3e45a22830bab66dec49e0.tar.gz
bycs-sol-198bbebd89d212faae3e45a22830bab66dec49e0.zip
admin-tools: add export tools and improve duplicate finderHEADmain
Considered as duplicates are eequal qual changes submitted in the same hour. Bump dependencies.
Diffstat (limited to 'src')
-rw-r--r--src/admin-tools.js85
-rw-r--r--src/graduation-common.ts54
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,