Compare commits

...

12 commits

Author SHA1 Message Date
3652ad31fe mention applications.xlsx 2026-06-12 15:18:10 +02:00
16272153fa Update README.md 2026-06-12 15:11:49 +02:00
gapato
7354709b78 make button injection less brittle in list 2026-06-12 14:51:29 +02:00
gapato
52a20a0b21 add button to procedure list page 2026-06-12 11:58:14 +02:00
gapato
2f5077f2e8 enable AutoFilter and set reasonable column widths in XLSX 2026-06-12 10:26:19 +02:00
gapato
4a4f316ce6 update versions.json 2026-06-11 15:37:39 +02:00
gapato
20ba463cc6 bump version 2026-06-11 15:31:02 +02:00
gapato
7e135392a4 add XLSX summary to the archive 2026-06-11 15:29:52 +02:00
gapato
081f62d099 indentation 2026-06-11 14:18:48 +02:00
gapato
458ae0c70c add source js for sheetjs 2026-06-11 12:49:21 +02:00
gapato
3cf1148732 update README 2026-06-09 11:17:27 +02:00
gapato
a405f9db66 update Makefile 2026-06-09 11:17:24 +02:00
7 changed files with 28250 additions and 17 deletions

View file

@ -4,3 +4,13 @@ SET_VERSION = $(eval CURRENT_VERSION=$(VERSION))
default: default:
$(SET_VERSION) $(SET_VERSION)
zip -r releases/epas-ripper-$(CURRENT_VERSION).xpi style.css content.js lib archive-viewer manifest.json icons zip -r releases/epas-ripper-$(CURRENT_VERSION).xpi style.css content.js lib archive-viewer manifest.json icons
prepare_pack:
rm -rf build
mkdir -p build
cp -r style.css content.js lib archive-viewer manifest.json icons build
pack: prepare_pack
$(SET_VERSION)
chromium-browser --pack-extension=build --pack-extension-key=key.pem
mv build.crx releases/epas-ripper-$(CURRENT_VERSION).crx

View file

@ -5,13 +5,13 @@ Chrome extension for downloading EPAS applicant files into a ZIP archive, includ
## Installation ## Installation
### Firefox ### Firefox
- Click [here](https://imsc.uni-graz.at/jankowiak/files/sw/epas-ripper/epas-ripper-0.2.1-signed.xpi). - Click [here](https://imsc.uni-graz.at/jankowiak/files/sw/epas-ripper/epas-ripper.xpi).
### Chrome ### Chrome
- open `about:extensions` from Chrome - open `about:extensions` from Chrome
- turn on Developer mode (Entwicklermodus) if not done already - turn on Developer mode (Entwicklermodus) if not done already
- there are then 2 options: - there are then 2 options:
- on Linux, [download](https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/releases/download/0.2.1/epas-ripper-0.2.1.crx) the `.crx` extension file and drag-and-drop it to the `about:extensions` tab. - on Linux, [download](https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/releases/download/0.4.0/epas-ripper-0.4.0.crx) the `.crx` extension file and drag-and-drop it to the `about:extensions` tab.
- on Windows (macOS too?), [download](https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/archive/main.zip) the `.zip` archive of this repository, extract it _somewhere it does not risk begin moved or deleted_ and load the `epas-ripper` directory (located inside) using the "Load unpacked" ("Entpackte Erweiterung laden") button of the `about:extensions` tab. - on Windows (macOS too?), [download](https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/archive/main.zip) the `.zip` archive of this repository, extract it _somewhere it does not risk begin moved or deleted_ and load the `epas-ripper` directory (located inside) using the "Load unpacked" ("Entpackte Erweiterung laden") button of the `about:extensions` tab.
- Chrome will display security warnings which you need to check and accept. - Chrome will display security warnings which you need to check and accept.
@ -23,4 +23,5 @@ The archive will contain:
- one directory per applicant with the corresponding files - one directory per applicant with the corresponding files
- `applications.csv`, which contains a table with the details of each applicant - `applications.csv`, which contains a table with the details of each applicant
- `applications.xlsx`,
- `viewer.html` (+ auxiliary files), a static HTML file which can be use to browse the applicants' files. After opening it in a browser, you need to load the ZIP file you just downloaded. - `viewer.html` (+ auxiliary files), a static HTML file which can be use to browse the applicants' files. After opening it in a browser, you need to load the ZIP file you just downloaded.

View file

@ -101,8 +101,7 @@ async function runWithConcurrencyLimit(items, limit, worker) {
return results return results
} }
async function getApplicants() { async function getApplicants(aid) {
const aid = getApplicationId()
const token = await getXSRFToken() const token = await getXSRFToken()
return fetch(`https://personal.uni-graz.at/api/erec/job-applications/procedure/${aid}`, return fetch(`https://personal.uni-graz.at/api/erec/job-applications/procedure/${aid}`,
{ {
@ -243,6 +242,58 @@ function escapeCsvValue(value) {
return `"${stringValue.replace(/"/g, "\"\"")}"` return `"${stringValue.replace(/"/g, "\"\"")}"`
} }
const API_FIELD_MAP = {
"last_name": "Nachname",
"first_name": "Vorname",
"email": "E-Mail Adresse",
"telephone_number": "Telefonnummer",
"gender": "Geschlecht",
"academic_title_before_name": "Akademischer Grad vorangestellt",
"academic_title_after_name": "Akademischer Grad nachgestellt",
"professional_title": "Berufstitel",
"date_of_birth": "Geburtsdatum",
"highest_educational_degree": "Höchster Bildungsabschluss",
"highest_academic_education": "Höchster akademischer Abschluss",
"final_year_studies": "Letztes Studienjahr (Master oder PhD)",
"first_language": "Erstsprache",
"nationality_arr": "Staatsangehörigkeit",
"relevant_links": "Relevante Links",
"orcid_id": "ORCID ID",
"google_scholar_profile": "Google Scholar Profil",
"how_did_you_become_aware_arr": "Wie sind Sie auf die Stelle aufmerksam geworden?",
}
const COL_WIDTHS = [ 17, 20.03, 33.45, 17.25, 12.95, 31.43, 38.52, 12.57, 15.73, 55.23, 31.68, 35.35, 37.63, 20.29, 191.81, 11.68, 21.17, 45.73 ]
function createApplicantsXlsx(applicantDetailsList) {
const sorted_mapped = applicantDetailsList.sort((appli1, appli2) => {
appli1.last_name.localeCompare(appli2.last_name)
}).map((appli) => {
const mapped = {}
Object.keys(API_FIELD_MAP).forEach(key => {
mapped[API_FIELD_MAP[key]] = appli[key]
})
return mapped
})
const sheet = XLSX.utils.json_to_sheet(sorted_mapped)
// enable easy filtering, sorting
sheet["!autofilter"] = { ref: `A1:R${applicantDetailsList.length}` }
// set basic column widths
/* create !cols array */
sheet["!cols"] = [];
COL_WIDTHS.forEach((width, idx) => {
sheet["!cols"][idx] = { width };
})
const book = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(book, sheet, "Applications")
return new Uint8Array(XLSX.toXLSXBlob(book).buffer)
}
function createApplicantsCsv(applicantDetailsList) { function createApplicantsCsv(applicantDetailsList) {
const rows = [ const rows = [
CSV_FIELDS.join(",") CSV_FIELDS.join(",")
@ -467,14 +518,18 @@ function createProgressDialog() {
} }
// Main export flow: fetch applicant data, download files, then assemble the ZIP. // Main export flow: fetch applicant data, download files, then assemble the ZIP.
function rip(event) { function rip(event, aid) {
Notification.requestPermission() Notification.requestPermission()
const btn = event.target; const btn = event.target;
const progressDialog = createProgressDialog() const progressDialog = createProgressDialog()
btn.textContent = "working ..." btn.textContent = "working ..."
btn.classList.add("loading") btn.classList.add("loading")
btn.disabled = true; btn.disabled = true;
getApplicants() if (aid == null) {
console.error("could not find procedure ID")
return
}
getApplicants(aid)
.then(async ({ aid, applicants, token }) => { .then(async ({ aid, applicants, token }) => {
progressDialog.setStatus("Downloading applicant details (will take several minutes)...") progressDialog.setStatus("Downloading applicant details (will take several minutes)...")
applicants.forEach((applicant) => { applicants.forEach((applicant) => {
@ -523,6 +578,9 @@ function rip(event) {
console.log("Creating applications.csv") console.log("Creating applications.csv")
archiveEntries["applications.csv"] = createApplicantsCsv(successfulApplicants) archiveEntries["applications.csv"] = createApplicantsCsv(successfulApplicants)
console.log("Creating applications.xlsx")
archiveEntries["applications.xlsx"] = createApplicantsXlsx(successfulApplicants)
console.log("Creating index.html") console.log("Creating index.html")
archiveEntries["viewer.html"] = viewerHtmlSource archiveEntries["viewer.html"] = viewerHtmlSource
@ -560,22 +618,67 @@ function rip(event) {
}) })
} }
function createRipButton(aid) {
const btn = document.createElement("button")
btn.textContent = "rip"
btn.classList.add("ripper-btn")
btn.addEventListener("click", event => rip(event, aid), {
once: true
})
return btn
}
function installTable(tableBody) {
if (tableBody == null) {
return false
}
tableBody.querySelectorAll("tr").forEach((tr) => {
if (tr.querySelector(".ripper-btn") != null) return true;
const nameTd = tr.querySelector(".column-name")
const link = nameTd?.querySelector("a")
const href = link?.href
if (href == null) {
return false
}
const match = href.match(/^https:\/\/personal\.uni-graz\.at\/#\/job-procedures\/record\/([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})/)
if (match == null) {
return false
}
const aid = match[1]
const actionMenu = tableBody.querySelector("scrm-line-action-menu")
if (actionMenu == null) {
return false
}
console.log("rip button installed")
actionMenu.append(createRipButton(aid))
return true
})
}
// Inject the entrypoint button into the SPA header when we are on a procedure page. // Inject the entrypoint button into the SPA header when we are on a procedure page.
function install() { function install() {
const titleTag = document.querySelector("scrm-module-title") const titleTag = document.querySelector("scrm-module-title")
if (titleTag != null && window.location.hash.startsWith("#/job-procedures/record")) { if (titleTag != null && window.location.hash.startsWith("#/job-procedures/record")) {
/* procedure specific page, add the button to the title */
/* prevent double install */
if (titleTag.querySelector(".ripper-btn") != null) { if (titleTag.querySelector(".ripper-btn") != null) {
return return
} }
titleTag.append(" (", createRipButton(getApplicationId()), ")")
const a = document.createElement("button") } else if (window.location.hash === "#/job-procedures/list") {
a.textContent = "rip" /* procedure list page, add the button to each row */
a.classList.add("ripper-btn") let intervalId = -1;
a.addEventListener("click", rip, { let tries = 0;
once: true const intervalLoop = () => {
}) const tableBody = document.querySelector("scrm-table")?.querySelector("tbody")
titleTag.append(" (", a, ")") tries += 1
if (installTable(tableBody) || tries > 20) {
clearInterval(intervalId)
}
}
intervalId = setInterval(intervalLoop, 100)
} else { } else {
console.log("could not install button") console.log("could not install button")
} }

28113
lib/xlsx.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "KF-EPAS-Ripper", "name": "KF-EPAS-Ripper",
"version": "0.2.2", "version": "0.3.0",
"browser_specific_settings": { "browser_specific_settings": {
"update_url": "https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/raw/branch/main/versions.json", "update_url": "https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/raw/branch/main/versions.json",
"gecko": { "gecko": {
@ -25,6 +25,7 @@
], ],
"js": [ "js": [
"lib/fflate.min.js", "lib/fflate.min.js",
"lib/xlsx.js",
"content.js" "content.js"
], ],
"css": [ "css": [

View file

@ -9,7 +9,8 @@ button.ripper-btn {
color: #fff; color: #fff;
font: inherit; font: inherit;
font-size: 0.85em; font-size: 0.85em;
line-height: 1.4; font-weight: bold;
line-height: 1.1;
cursor: pointer; cursor: pointer;
} }

View file

@ -9,6 +9,10 @@
{ {
"version": "0.2.2", "version": "0.2.2",
"update_link": "https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/releases/download/0.2.2/epas-ripper-0.2.2-signed.xpi" "update_link": "https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/releases/download/0.2.2/epas-ripper-0.2.2-signed.xpi"
},
{
"version": "0.3.0",
"update_link": "https://imsc.uni-graz.at/git/gjankowiak/epas-ripper/releases/download/0.3.0/epas-ripper-0.3.0-signed.xpi"
} }
] ]
} }