Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0a6dd057c |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.2.1"
|
version = "2.3.0"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -353,19 +353,32 @@ def _start_fileshare_sweep(app: Flask) -> None:
|
|||||||
weiter eingebunden.
|
weiter eingebunden.
|
||||||
"""
|
"""
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
from .services import fileshare
|
from .services import fileshare
|
||||||
|
|
||||||
|
def einmal() -> None:
|
||||||
|
with app.app_context():
|
||||||
|
geloest = fileshare.sweep_expired(core(app).database)
|
||||||
|
if geloest:
|
||||||
|
logger.info("%s abgelaufene Freigabe-Einbindungen geloest", geloest)
|
||||||
|
|
||||||
def schleife() -> None:
|
def schleife() -> None:
|
||||||
while True:
|
# Zwei Netze: das innere haelt den Faden am Leben, wenn ein einzelner
|
||||||
time.sleep(FILESHARE_SWEEP_SECONDS)
|
# Durchlauf scheitert; das aeussere sorgt dafuer, dass sein Ende
|
||||||
try:
|
# ueberhaupt auffaellt. Vorher fehlte der Import von "time" -- der
|
||||||
with app.app_context():
|
# NameError kam beim ersten sleep, also vor jedem try, und der Faden
|
||||||
geloest = fileshare.sweep_expired(core(app).database)
|
# war weg, ohne eine Zeile zu hinterlassen.
|
||||||
if geloest:
|
try:
|
||||||
logger.info("%s abgelaufene Freigabe-Einbindungen geloest", geloest)
|
while True:
|
||||||
except Exception: # noqa: BLE001 - der Webprozess darf nie sterben
|
time.sleep(FILESHARE_SWEEP_SECONDS)
|
||||||
logger.exception("Aufraeumen der Dateifreigaben fehlgeschlagen")
|
try:
|
||||||
|
einmal()
|
||||||
|
except Exception: # noqa: BLE001 - der Webprozess darf nie sterben
|
||||||
|
logger.exception("Aufraeumen der Dateifreigaben fehlgeschlagen")
|
||||||
|
except BaseException: # noqa: BLE001 - auch ein Abbruch gehoert protokolliert
|
||||||
|
logger.exception("Der Aufraeumfaden der Dateifreigaben ist beendet")
|
||||||
|
raise
|
||||||
|
|
||||||
threading.Thread(target=schleife, name="fileshare-sweep", daemon=True).start()
|
threading.Thread(target=schleife, name="fileshare-sweep", daemon=True).start()
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ def browse():
|
|||||||
# vorher versprach diese Seite 512 MB, waehrend bei 64 MB abgebrochen
|
# vorher versprach diese Seite 512 MB, waehrend bei 64 MB abgebrochen
|
||||||
# wurde.
|
# wurde.
|
||||||
max_upload_mb=uploads.limit_mb(conn),
|
max_upload_mb=uploads.limit_mb(conn),
|
||||||
|
# Vorschlagsliste fuer "Ausgefuellt speichern": wohin darf es gehen?
|
||||||
|
verzeichnisse=(service.all_directories(mount) if listing else []),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -205,15 +207,36 @@ def save_form(token: str):
|
|||||||
# waere ein Umweg, um beliebige Dateien in die Freigabe zu bekommen.
|
# waere ein Umweg, um beliebige Dateien in die Freigabe zu bekommen.
|
||||||
return {"error": "Das ist keine PDF-Datei."}, 415
|
return {"error": "Das ist keine PDF-Datei."}, 415
|
||||||
|
|
||||||
|
# Wohin und unter welchem Namen -- beides darf der Benutzer bestimmen.
|
||||||
|
ordner = (request.form.get("ordner") or "").strip().strip("/")
|
||||||
|
wunschname = (request.form.get("name") or "").strip()
|
||||||
|
ersetzen = request.form.get("ersetzen") == "1"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
original = service.resolve_path(mount, relative)
|
original = service.resolve_path(mount, relative)
|
||||||
ziel = service.free_name(original.parent, original.stem, "-ausgefuellt", ".pdf")
|
zielordner = service.resolve_path(mount, ordner) if ordner else original.parent
|
||||||
|
if not zielordner.is_dir():
|
||||||
|
return {"error": f"Den Ordner '{ordner}' gibt es in dieser Freigabe nicht."}, 400
|
||||||
|
if wunschname:
|
||||||
|
name = service.safe_filename(wunschname)
|
||||||
|
if not name.lower().endswith(".pdf"):
|
||||||
|
name += ".pdf"
|
||||||
|
ziel = zielordner / name
|
||||||
|
if ziel.exists() and not ersetzen:
|
||||||
|
return {
|
||||||
|
"error": f"'{name}' gibt es dort schon. Anderen Namen waehlen oder "
|
||||||
|
"'Vorhandene ersetzen' ankreuzen."
|
||||||
|
}, 409
|
||||||
|
else:
|
||||||
|
# Ohne Angabe wie bisher: neben das Original, ohne etwas zu ueberschreiben.
|
||||||
|
ziel = service.free_name(original.parent, original.stem, "-ausgefuellt", ".pdf")
|
||||||
ziel.write_bytes(daten)
|
ziel.write_bytes(daten)
|
||||||
except service.FileshareError as exc:
|
except service.FileshareError as exc:
|
||||||
return {"error": str(exc)}, 400
|
return {"error": str(exc)}, 400
|
||||||
|
|
||||||
_audit("fileshare.form_saved", f"{mount.label}:{relative}", ziel.name, severity="notice")
|
wohin = ziel.relative_to(mount.path.resolve()).as_posix()
|
||||||
return {"message": f"Als {ziel.name} gespeichert."}
|
_audit("fileshare.form_saved", f"{mount.label}:{relative}", wohin, severity="notice")
|
||||||
|
return {"message": f"Gespeichert als {wohin}.", "pfad": wohin}
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/<token>/ordner")
|
@bp.post("/<token>/ordner")
|
||||||
|
|||||||
@@ -588,6 +588,36 @@ def safe_filename(name: str) -> str:
|
|||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
def all_directories(mount: Mount, *, tiefe: int = 4, grenze: int = 400) -> list[str]:
|
||||||
|
"""Die Ordner der Freigabe, relativ und alphabetisch -- fuer eine Auswahlliste.
|
||||||
|
|
||||||
|
Begrenzt in Tiefe und Anzahl: eine Netzwerkfreigabe kann zehntausende
|
||||||
|
Verzeichnisse haben, und eine Liste, die den Browser lahmlegt, hilft
|
||||||
|
niemandem. Wer tiefer speichern will, tippt den Pfad -- das Feld nimmt
|
||||||
|
beides.
|
||||||
|
"""
|
||||||
|
wurzel = mount.path.resolve()
|
||||||
|
gefunden: list[str] = []
|
||||||
|
|
||||||
|
def hinein(verzeichnis: Path, ebene: int) -> None:
|
||||||
|
if ebene > tiefe or len(gefunden) >= grenze:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
eintraege = sorted(verzeichnis.iterdir(), key=lambda e: e.name.lower())
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
for eintrag in eintraege:
|
||||||
|
if len(gefunden) >= grenze:
|
||||||
|
return
|
||||||
|
if not eintrag.is_dir() or eintrag.name.startswith("."):
|
||||||
|
continue
|
||||||
|
gefunden.append(eintrag.relative_to(wurzel).as_posix())
|
||||||
|
hinein(eintrag, ebene + 1)
|
||||||
|
|
||||||
|
hinein(wurzel, 1)
|
||||||
|
return gefunden
|
||||||
|
|
||||||
|
|
||||||
def free_name(parent: Path, stem: str, zusatz: str, suffix: str) -> Path:
|
def free_name(parent: Path, stem: str, zusatz: str, suffix: str) -> Path:
|
||||||
"""Ein freier Name neben dem Original: ``vorlage-ausgefuellt.pdf``.
|
"""Ein freier Name neben dem Original: ``vorlage-ausgefuellt.pdf``.
|
||||||
|
|
||||||
|
|||||||
@@ -209,16 +209,40 @@
|
|||||||
iPad in Originalgrösse (also einen Ausschnitt) und ein PDF auf iPadOS
|
iPad in Originalgrösse (also einen Ausschnitt) und ein PDF auf iPadOS
|
||||||
nur als Standbild -- Formularfelder liessen sich nicht anfassen. #}
|
nur als Standbild -- Formularfelder liessen sich nicht anfassen. #}
|
||||||
<div class="modal__body modal__body--flush" data-viewer
|
<div class="modal__body modal__body--flush" data-viewer
|
||||||
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/') }}">
|
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/pdf.min.js') }}">
|
||||||
<div class="viewer">
|
<div class="viewer">
|
||||||
<div class="viewer__stage" data-viewer-bildbuehne hidden>
|
<div class="viewer__stage" data-viewer-bildbuehne hidden>
|
||||||
<img class="viewer__img" alt="" data-viewer-bild>
|
<img class="viewer__img" alt="" data-viewer-bild>
|
||||||
</div>
|
</div>
|
||||||
<div class="viewer__pdf" data-viewer-pdf hidden>
|
<div class="viewer__pdf" data-viewer-pdf hidden>
|
||||||
<div class="pdfViewer" data-viewer-pdfseiten></div>
|
{# Zwei Ebenen: aussen ein normaler Kasten, der sich die Hoehe mit
|
||||||
|
Leiste und Speicherzeile teilt, innen der absolute Scrollbereich,
|
||||||
|
den pdf.js verlangt. #}
|
||||||
|
<div class="viewer__pdfscroll" data-viewer-pdfscroll>
|
||||||
|
<div class="pdfViewer" data-viewer-pdfseiten></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<iframe class="file-preview" title="Vorschau" data-viewer-rahmen
|
<iframe class="file-preview" title="Vorschau" data-viewer-rahmen
|
||||||
src="about:blank" referrerpolicy="same-origin" hidden></iframe>
|
src="about:blank" referrerpolicy="same-origin" hidden></iframe>
|
||||||
|
{# Gefragt wird erst, wenn gespeichert werden soll -- vorher waere es
|
||||||
|
eine Zeile Formular ueber jeder Bildvorschau. #}
|
||||||
|
<div class="viewer__save" data-viewer-speichernzeile hidden>
|
||||||
|
<label class="viewer__feld">
|
||||||
|
<span class="field__label">Ordner</span>
|
||||||
|
<input class="input" list="freigabe-ordner" data-viewer-ordner
|
||||||
|
placeholder="Wurzelverzeichnis" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
<label class="viewer__feld">
|
||||||
|
<span class="field__label">Dateiname</span>
|
||||||
|
<input class="input" data-viewer-name autocomplete="off">
|
||||||
|
</label>
|
||||||
|
<label class="check">
|
||||||
|
<input type="checkbox" data-viewer-ersetzen>
|
||||||
|
<span>Vorhandene ersetzen</span>
|
||||||
|
</label>
|
||||||
|
<button class="btn btn--sm btn--primary" type="button" data-viewer-jetzt>Speichern</button>
|
||||||
|
<button class="btn btn--sm btn--ghost" type="button" data-viewer-abbrechen>Abbrechen</button>
|
||||||
|
</div>
|
||||||
<div class="viewer__bar" data-viewer-leiste hidden>
|
<div class="viewer__bar" data-viewer-leiste hidden>
|
||||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-zoom="aus"
|
<button class="btn btn--sm btn--ghost" type="button" data-viewer-zoom="aus"
|
||||||
aria-label="Verkleinern">{{ icon('minus', size=15) }}</button>
|
aria-label="Verkleinern">{{ icon('minus', size=15) }}</button>
|
||||||
@@ -242,6 +266,10 @@
|
|||||||
<button class="btn btn--primary" type="button" data-dialog-close>Schliessen</button>
|
<button class="btn btn--primary" type="button" data-dialog-close>Schliessen</button>
|
||||||
</div>
|
</div>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
{# Die Ordner der Freigabe als Vorschlag; getippt werden darf trotzdem. #}
|
||||||
|
<datalist id="freigabe-ordner">
|
||||||
|
{% for eintrag in verzeichnisse %}<option value="{{ eintrag }}"></option>{% endfor %}
|
||||||
|
</datalist>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if listing and current_user.has('fileshare.edit') %}
|
{% if listing and current_user.has('fileshare.edit') %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.2.1"
|
version = "2.3.0"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ def create_base_app(
|
|||||||
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||||
#: Anwendungen.
|
#: Anwendungen.
|
||||||
auth_tagline: str = "",
|
auth_tagline: str = "",
|
||||||
app_version: str = "2.2.1",
|
app_version: str = "2.3.0",
|
||||||
built_in_trusted_keys: tuple[str, ...] = (),
|
built_in_trusted_keys: tuple[str, ...] = (),
|
||||||
enable_license: bool = True,
|
enable_license: bool = True,
|
||||||
template_folder: str | None = None,
|
template_folder: str | None = None,
|
||||||
|
|||||||
@@ -754,11 +754,19 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
/* pdf.js verlangt einen absolut positionierten, scrollenden Behaelter. */
|
/* pdf.js verlangt einen absolut positionierten, scrollenden Behaelter --
|
||||||
|
der liegt deshalb *innen*. Aussen ein gewoehnlicher Flex-Kasten, damit
|
||||||
|
Zoomleiste und Speicherzeile ihren Platz behalten. Vorher rechnete der
|
||||||
|
Bereich mit "bottom: var(--sp-10)", also mit genau einer Leiste; die
|
||||||
|
zweite Zeile lag dann unter dem Dialogrand. */
|
||||||
.viewer__pdf {
|
.viewer__pdf {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.viewer__pdfscroll {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
bottom: var(--sp-10);
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
@@ -773,6 +781,26 @@
|
|||||||
.viewer__hint {
|
.viewer__hint {
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
}
|
}
|
||||||
|
.viewer__save {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--sp-3);
|
||||||
|
padding: var(--sp-3) var(--sp-4);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--surface-2, var(--surface-1));
|
||||||
|
}
|
||||||
|
.viewer__save .viewer__feld {
|
||||||
|
flex: 1 1 12rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
/* Eigener Name: ".field--inline" gibt es schon und steht dort fuer
|
||||||
|
"waagerecht" -- diese Regel haette sie app-weit umgestellt. */
|
||||||
|
.viewer__feld {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
/* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein
|
/* Der Dateiname ist ein Knopf (er oeffnet die Vorschau), soll aber wie ein
|
||||||
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
|
Link aussehen -- ein Knopf mit Rahmen mitten in einer Tabellenzeile waere
|
||||||
unruhig. */
|
unruhig. */
|
||||||
|
|||||||
@@ -902,7 +902,11 @@ function boot() {
|
|||||||
einem PDF noch pdf.js nach. Beides erst dann, wenn es gebraucht wird. */
|
einem PDF noch pdf.js nach. Beides erst dann, wenn es gebraucht wird. */
|
||||||
function initViewer() {
|
function initViewer() {
|
||||||
if (!document.querySelector("[data-viewer]")) return;
|
if (!document.querySelector("[data-viewer]")) return;
|
||||||
import("./viewer.js")
|
// Dieselbe Version wie diese Datei: app.js wird als ".../app.js?v=<Zeit>"
|
||||||
|
// eingebunden, und nginx liefert /core-assets/ mit "expires 7d". Ohne das
|
||||||
|
// Anhaengsel bliebe der alte Betrachter nach einem Update im Cache liegen.
|
||||||
|
const version = new URL(import.meta.url).search;
|
||||||
|
import(`./viewer.js${version}`)
|
||||||
.then((modul) => modul.initFilePreview())
|
.then((modul) => modul.initFilePreview())
|
||||||
.catch((fehler) => console.error("Dateibetrachter", fehler));
|
.catch((fehler) => console.error("Dateibetrachter", fehler));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,21 +146,41 @@ function bildBetrachter(buehne, bild) {
|
|||||||
|
|
||||||
let pdfjsGeladen = null;
|
let pdfjsGeladen = null;
|
||||||
|
|
||||||
|
/** Die Adressen der pdf.js-Dateien -- alle mit derselben Version.
|
||||||
|
*
|
||||||
|
* Uebergeben wird die Adresse von pdf.min.js samt "?v=<Zeit>"; die
|
||||||
|
* Geschwister liegen daneben. Ohne die Version bliebe nach einem Update die
|
||||||
|
* alte Fassung im Cache von nginx ("expires 7d") haengen.
|
||||||
|
*/
|
||||||
|
function pdfjsAdressen(adresse) {
|
||||||
|
const url = new URL(adresse, location.href);
|
||||||
|
const verzeichnis = url.pathname.replace(/[^/]*$/, "");
|
||||||
|
const mit = (datei) => `${url.origin}${verzeichnis}${datei}${url.search}`;
|
||||||
|
return {
|
||||||
|
lib: mit("pdf.min.js"),
|
||||||
|
komponenten: mit("pdf_viewer.js"),
|
||||||
|
worker: mit("pdf.worker.min.js"),
|
||||||
|
stil: mit("pdf_viewer.css"),
|
||||||
|
schriften: `${url.origin}${verzeichnis}standard_fonts/`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Laedt pdf.js einmalig nach; das Paket liegt neben dieser Datei. */
|
/** Laedt pdf.js einmalig nach; das Paket liegt neben dieser Datei. */
|
||||||
async function pdfjsLaden(basis) {
|
async function pdfjsLaden(basis) {
|
||||||
|
const adressen = pdfjsAdressen(basis);
|
||||||
if (!pdfjsGeladen) {
|
if (!pdfjsGeladen) {
|
||||||
pdfjsGeladen = (async () => {
|
pdfjsGeladen = (async () => {
|
||||||
const lib = await import(`${basis}pdf.min.js`);
|
const lib = await import(adressen.lib);
|
||||||
// Der Komponentenbau von pdf.js sucht die Bibliothek unter diesem Namen.
|
// Der Komponentenbau von pdf.js sucht die Bibliothek unter diesem Namen.
|
||||||
globalThis.pdfjsLib = lib;
|
globalThis.pdfjsLib = lib;
|
||||||
const komponenten = await import(`${basis}pdf_viewer.js`);
|
const komponenten = await import(adressen.komponenten);
|
||||||
// Das Stylesheet von pdf.js zeichnet die Formularfelder. 100 KB gehoeren
|
// Das Stylesheet von pdf.js zeichnet die Formularfelder. 100 KB gehoeren
|
||||||
// nicht in jeden Seitenaufruf -- also erst hier.
|
// nicht in jeden Seitenaufruf -- also erst hier.
|
||||||
const stil = document.createElement("link");
|
const stil = document.createElement("link");
|
||||||
stil.rel = "stylesheet";
|
stil.rel = "stylesheet";
|
||||||
stil.href = `${basis}pdf_viewer.css`;
|
stil.href = adressen.stil;
|
||||||
document.head.append(stil);
|
document.head.append(stil);
|
||||||
lib.GlobalWorkerOptions.workerSrc = `${basis}pdf.worker.min.js`;
|
lib.GlobalWorkerOptions.workerSrc = adressen.worker;
|
||||||
return { lib, komponenten };
|
return { lib, komponenten };
|
||||||
})().catch((fehler) => {
|
})().catch((fehler) => {
|
||||||
pdfjsGeladen = null;
|
pdfjsGeladen = null;
|
||||||
@@ -173,6 +193,7 @@ async function pdfjsLaden(basis) {
|
|||||||
/** Zeigt ein PDF mit ausfuellbaren Formularfeldern. */
|
/** Zeigt ein PDF mit ausfuellbaren Formularfeldern. */
|
||||||
async function pdfBetrachter(behaelter, seiten, url, basis) {
|
async function pdfBetrachter(behaelter, seiten, url, basis) {
|
||||||
const { lib, komponenten } = await pdfjsLaden(basis);
|
const { lib, komponenten } = await pdfjsLaden(basis);
|
||||||
|
const adressen = pdfjsAdressen(basis);
|
||||||
const eventBus = new komponenten.EventBus();
|
const eventBus = new komponenten.EventBus();
|
||||||
const linkService = new komponenten.PDFLinkService({ eventBus });
|
const linkService = new komponenten.PDFLinkService({ eventBus });
|
||||||
const viewer = new komponenten.PDFViewer({
|
const viewer = new komponenten.PDFViewer({
|
||||||
@@ -190,7 +211,7 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
|||||||
url,
|
url,
|
||||||
// Ohne die Standardschriften bleibt jede Seite mit Text leer: Helvetica
|
// Ohne die Standardschriften bleibt jede Seite mit Text leer: Helvetica
|
||||||
// und Verwandte liegen als Datei im Paket, nicht im Browser.
|
// und Verwandte liegen als Datei im Paket, nicht im Browser.
|
||||||
standardFontDataUrl: `${basis}standard_fonts/`,
|
standardFontDataUrl: adressen.schriften,
|
||||||
isEvalSupported: false,
|
isEvalSupported: false,
|
||||||
}).promise;
|
}).promise;
|
||||||
viewer.setDocument(dokument);
|
viewer.setDocument(dokument);
|
||||||
@@ -261,11 +282,18 @@ export function initFilePreview(wurzel = document) {
|
|||||||
const bildBuehne = stueck("bildbuehne");
|
const bildBuehne = stueck("bildbuehne");
|
||||||
const bild = stueck("bild");
|
const bild = stueck("bild");
|
||||||
const pdfBehaelter = stueck("pdf");
|
const pdfBehaelter = stueck("pdf");
|
||||||
|
const pdfScroll = stueck("pdfscroll");
|
||||||
const pdfSeiten = stueck("pdfseiten");
|
const pdfSeiten = stueck("pdfseiten");
|
||||||
const rahmen = stueck("rahmen");
|
const rahmen = stueck("rahmen");
|
||||||
const leiste = stueck("leiste");
|
const leiste = stueck("leiste");
|
||||||
const hinweis = stueck("hinweis");
|
const hinweis = stueck("hinweis");
|
||||||
const speichern = stueck("speichern");
|
const speichern = stueck("speichern");
|
||||||
|
const zeile = stueck("speichernzeile");
|
||||||
|
const ordnerFeld = stueck("ordner");
|
||||||
|
const nameFeld = stueck("name");
|
||||||
|
const ersetzenFeld = stueck("ersetzen");
|
||||||
|
const jetzt = stueck("jetzt");
|
||||||
|
const abbrechen = stueck("abbrechen");
|
||||||
const basis = rumpf.dataset.viewerPdfjs || "";
|
const basis = rumpf.dataset.viewerPdfjs || "";
|
||||||
|
|
||||||
// Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari),
|
// Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari),
|
||||||
@@ -309,6 +337,7 @@ export function initFilePreview(wurzel = document) {
|
|||||||
}
|
}
|
||||||
pdfSeiten.replaceChildren();
|
pdfSeiten.replaceChildren();
|
||||||
if (hinweis) hinweis.textContent = "";
|
if (hinweis) hinweis.textContent = "";
|
||||||
|
if (zeile) zeile.hidden = true;
|
||||||
aktuell = null;
|
aktuell = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,7 +357,7 @@ export function initFilePreview(wurzel = document) {
|
|||||||
}
|
}
|
||||||
if (hinweis) hinweis.textContent = "PDF wird geladen ...";
|
if (hinweis) hinweis.textContent = "PDF wird geladen ...";
|
||||||
try {
|
try {
|
||||||
pdf = await pdfBetrachter(pdfBehaelter, pdfSeiten, quelle, basis);
|
pdf = await pdfBetrachter(pdfScroll, pdfSeiten, quelle, basis);
|
||||||
if (hinweis) hinweis.textContent = "";
|
if (hinweis) hinweis.textContent = "";
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
// Kein weisses Feld: sagen, was los ist, und auf die einfache Anzeige
|
// Kein weisses Feld: sagen, was los ist, und auf die einfache Anzeige
|
||||||
@@ -337,8 +366,12 @@ export function initFilePreview(wurzel = document) {
|
|||||||
zeigen("text");
|
zeigen("text");
|
||||||
rahmen.src = quelle;
|
rahmen.src = quelle;
|
||||||
if (hinweis) {
|
if (hinweis) {
|
||||||
|
// Den Grund mitgeben: auf einem fremden Geraet ist die Meldung des
|
||||||
|
// Browsers die einzige Spur, die man hat.
|
||||||
|
const grund = String(fehler?.message || fehler || "").slice(0, 120);
|
||||||
hinweis.textContent =
|
hinweis.textContent =
|
||||||
"Die Formularansicht liess sich nicht laden -- es bleibt bei der einfachen Anzeige.";
|
"Die Formularansicht liess sich nicht laden -- es bleibt bei der einfachen Anzeige." +
|
||||||
|
(grund ? ` (${grund})` : "");
|
||||||
// Die Leiste traegt den Hinweis. "zeigen('text')" hat sie gerade
|
// Die Leiste traegt den Hinweis. "zeigen('text')" hat sie gerade
|
||||||
// verborgen -- ohne diese Zeile blieb der Fehlschlag unsichtbar, und
|
// verborgen -- ohne diese Zeile blieb der Fehlschlag unsichtbar, und
|
||||||
// es sah aus, als koennte die Vorschau einfach keine Formulare.
|
// es sah aus, als koennte die Vorschau einfach keine Formulare.
|
||||||
@@ -365,33 +398,61 @@ export function initFilePreview(wurzel = document) {
|
|||||||
|
|
||||||
dialog.addEventListener("close", aufraeumen);
|
dialog.addEventListener("close", aufraeumen);
|
||||||
|
|
||||||
/* Ausgefuelltes Formular zurueck in die Freigabe. */
|
/* Ausgefuelltes Formular zurueck in die Freigabe -- erst fragen, wohin. */
|
||||||
speichern?.addEventListener("click", async () => {
|
|
||||||
|
const vorschlagen = () => {
|
||||||
|
if (!aktuell) return;
|
||||||
|
const teile = aktuell.pfad.split("/");
|
||||||
|
const datei = teile.pop() || "formular.pdf";
|
||||||
|
ordnerFeld.value = teile.join("/");
|
||||||
|
// Der Name der Vorlage mit Zusatz -- die Vorlage selbst bleibt so heil,
|
||||||
|
// auch wenn jemand blind auf Speichern drueckt.
|
||||||
|
nameFeld.value = datei.replace(/\.pdf$/i, "") + "-ausgefuellt.pdf";
|
||||||
|
ersetzenFeld.checked = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
speichern?.addEventListener("click", () => {
|
||||||
|
if (!pdf || !aktuell) return;
|
||||||
|
vorschlagen();
|
||||||
|
zeile.hidden = false;
|
||||||
|
nameFeld.focus();
|
||||||
|
nameFeld.select();
|
||||||
|
});
|
||||||
|
|
||||||
|
abbrechen?.addEventListener("click", () => {
|
||||||
|
zeile.hidden = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
jetzt?.addEventListener("click", async () => {
|
||||||
if (!pdf || !aktuell) return;
|
if (!pdf || !aktuell) return;
|
||||||
const ziel = speichern.dataset.url;
|
const ziel = speichern.dataset.url;
|
||||||
speichern.disabled = true;
|
jetzt.disabled = true;
|
||||||
const vorher = speichern.textContent;
|
const vorher = jetzt.textContent;
|
||||||
speichern.textContent = "Speichern ...";
|
jetzt.textContent = "Speichern ...";
|
||||||
try {
|
try {
|
||||||
const bytes = await pdf.speichern();
|
const bytes = await pdf.speichern();
|
||||||
const daten = new FormData();
|
const daten = new FormData();
|
||||||
daten.append("pfad", aktuell.pfad);
|
daten.append("pfad", aktuell.pfad);
|
||||||
|
daten.append("ordner", ordnerFeld.value.trim());
|
||||||
|
daten.append("name", nameFeld.value.trim());
|
||||||
|
if (ersetzenFeld.checked) daten.append("ersetzen", "1");
|
||||||
daten.append(
|
daten.append(
|
||||||
"datei",
|
"datei",
|
||||||
new Blob([bytes], { type: "application/pdf" }),
|
new Blob([bytes], { type: "application/pdf" }),
|
||||||
(aktuell.pfad.split("/").pop() || "formular.pdf")
|
nameFeld.value.trim() || "formular.pdf"
|
||||||
);
|
);
|
||||||
const antwort = await window.tesm.api(ziel, { method: "POST", body: daten });
|
const antwort = await window.tesm.api(ziel, { method: "POST", body: daten });
|
||||||
window.tesm.showToast(antwort.message || "Gespeichert.", "success");
|
window.tesm.showToast(antwort.message || "Gespeichert.", "success");
|
||||||
dialog.close();
|
dialog.close();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
// Die Meldung fuer den Benutzer, die Einzelheiten fuer die Konsole.
|
// Die Meldung fuer den Benutzer, die Einzelheiten fuer die Konsole. Die
|
||||||
|
// Zeile bleibt offen: meist ist es ein Name, der schon vergeben ist.
|
||||||
console.error("Formular speichern", fehler);
|
console.error("Formular speichern", fehler);
|
||||||
window.tesm.showToast(fehler.message || "Speichern fehlgeschlagen.", "danger");
|
window.tesm.showToast(fehler.message || "Speichern fehlgeschlagen.", "danger");
|
||||||
} finally {
|
} finally {
|
||||||
speichern.disabled = false;
|
jetzt.disabled = false;
|
||||||
speichern.textContent = vorher;
|
jetzt.textContent = vorher;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-licensing"
|
name = "tesm-licensing"
|
||||||
version = "2.2.1"
|
version = "2.3.0"
|
||||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = ["cryptography>=42"]
|
dependencies = ["cryptography>=42"]
|
||||||
|
|||||||
Reference in new issue
Block a user