diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2026-10-17 10:23:22 +0200 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2026-10-17 10:23:22 +0200 |
| commit | 198bbebd89d212faae3e45a22830bab66dec49e0 (patch) | |
| tree | 1b92b64f29e0bc50f5ff845b154450c926b82a69 | |
| parent | 38477bab6c1c736a2d1e7dd79b75c6e122502622 (diff) | |
| download | bycs-sol-198bbebd89d212faae3e45a22830bab66dec49e0.tar.gz bycs-sol-198bbebd89d212faae3e45a22830bab66dec49e0.zip | |
Considered as duplicates are eequal qual changes submitted in the same hour.
Bump dependencies.
| -rw-r--r-- | admin-tools.html | 1 | ||||
| -rw-r--r-- | package-lock.json | 184 | ||||
| -rw-r--r-- | package.json | 4 | ||||
| -rw-r--r-- | src/admin-tools.js | 85 | ||||
| -rw-r--r-- | src/graduation-common.ts | 54 |
5 files changed, 236 insertions, 92 deletions
diff --git a/admin-tools.html b/admin-tools.html index 0b1fb28..7f3b3bc 100644 --- a/admin-tools.html +++ b/admin-tools.html @@ -27,5 +27,6 @@ </div> <button id="print-ids">StudentIds</button> <button id="find-duplicates">Finde Duplikate</button> + <button id="export-graduation-dbs">Exportiere Graduation Dbs</button> </body> </html> diff --git a/package-lock.json b/package-lock.json index d485859..0d4ab7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,10 @@ "name": "ByCS-SOL", "version": "0.1.0", "license": "GPL-3.0-or-later", + "dependencies": { + "file-saver": "^2.0.5", + "jszip": "^3.10.2" + }, "devDependencies": { "@babel/core": "^8.0.1", "@babel/preset-env": "^8.0.2", @@ -1609,29 +1613,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -2553,6 +2534,16 @@ "@babel/core": "^7.4.0 || ^8.0.0" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.11.11", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.11.tgz", @@ -2566,6 +2557,19 @@ "node": ">=6.0.0" } }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/browserslist": { "version": "4.28.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", @@ -2694,6 +2698,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2924,29 +2934,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3193,9 +3180,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -3222,6 +3209,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -3340,6 +3333,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3381,6 +3380,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -3443,6 +3448,12 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3545,6 +3556,18 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3579,6 +3602,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -3789,6 +3821,12 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3862,6 +3900,12 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3872,6 +3916,21 @@ "node": ">=6" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -3998,6 +4057,12 @@ "node": ">=8" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -4031,6 +4096,12 @@ "node": ">=10" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -4088,6 +4159,15 @@ "node": ">=0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -4274,6 +4354,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", diff --git a/package.json b/package.json index 08634e3..9262ccf 100644 --- a/package.json +++ b/package.json @@ -25,5 +25,9 @@ "typescript": "^7.0.2", "webpack": "^5.107.2", "webpack-cli": "^7.0.2" + }, + "dependencies": { + "file-saver": "^2.0.5", + "jszip": "^3.10.2" } } 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, |
