Compare commits
No commits in common. "main" and "0.2.2" have entirely different histories.
7 changed files with 17 additions and 28250 deletions
10
Makefile
10
Makefile
|
|
@ -4,13 +4,3 @@ 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
|
|
||||||
|
|
|
||||||
|
|
@ -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.xpi).
|
- Click [here](https://imsc.uni-graz.at/jankowiak/files/sw/epas-ripper/epas-ripper-0.2.1-signed.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.4.0/epas-ripper-0.4.0.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.2.1/epas-ripper-0.2.1.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,5 +23,4 @@ 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.
|
|
||||||
127
content.js
127
content.js
|
|
@ -101,7 +101,8 @@ async function runWithConcurrencyLimit(items, limit, worker) {
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getApplicants(aid) {
|
async function getApplicants() {
|
||||||
|
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}`,
|
||||||
{
|
{
|
||||||
|
|
@ -242,58 +243,6 @@ 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(",")
|
||||||
|
|
@ -518,18 +467,14 @@ 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, aid) {
|
function rip(event) {
|
||||||
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;
|
||||||
if (aid == null) {
|
getApplicants()
|
||||||
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) => {
|
||||||
|
|
@ -578,9 +523,6 @@ function rip(event, aid) {
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -618,67 +560,22 @@ function rip(event, aid) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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()), ")")
|
|
||||||
} else if (window.location.hash === "#/job-procedures/list") {
|
const a = document.createElement("button")
|
||||||
/* procedure list page, add the button to each row */
|
a.textContent = "rip"
|
||||||
let intervalId = -1;
|
a.classList.add("ripper-btn")
|
||||||
let tries = 0;
|
a.addEventListener("click", rip, {
|
||||||
const intervalLoop = () => {
|
once: true
|
||||||
const tableBody = document.querySelector("scrm-table")?.querySelector("tbody")
|
})
|
||||||
tries += 1
|
titleTag.append(" (", a, ")")
|
||||||
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
28113
lib/xlsx.js
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "KF-EPAS-Ripper",
|
"name": "KF-EPAS-Ripper",
|
||||||
"version": "0.3.0",
|
"version": "0.2.2",
|
||||||
"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,7 +25,6 @@
|
||||||
],
|
],
|
||||||
"js": [
|
"js": [
|
||||||
"lib/fflate.min.js",
|
"lib/fflate.min.js",
|
||||||
"lib/xlsx.js",
|
|
||||||
"content.js"
|
"content.js"
|
||||||
],
|
],
|
||||||
"css": [
|
"css": [
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ button.ripper-btn {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
font-weight: bold;
|
line-height: 1.4;
|
||||||
line-height: 1.1;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,6 @@
|
||||||
{
|
{
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue