Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20c7fd8813 | ||
|
|
eaca6f7990 |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -224,6 +224,9 @@ def save_form(token: str):
|
||||
if not zielordner.is_dir():
|
||||
return {"error": f"Den Ordner '{ordner}' gibt es in dieser Freigabe nicht."}, 400
|
||||
if wunschname:
|
||||
# Eingegeben wird der Name ohne Endung; die kommt von hier. Wer
|
||||
# sie mittippt, bekommt sie nicht doppelt -- und wer eine falsche
|
||||
# tippt, bekommt trotzdem eine Datei, die sich oeffnen laesst.
|
||||
name = service.safe_filename(wunschname)
|
||||
if not name.lower().endswith(".pdf"):
|
||||
name += ".pdf"
|
||||
|
||||
@@ -548,6 +548,10 @@ def list_directory(mount: Mount, relative: str = "") -> dict[str, Any]:
|
||||
else:
|
||||
record["previewable"] = entry.suffix.lower() in PREVIEW_MIME
|
||||
record["preview_kind"] = preview_kind(entry.name)
|
||||
# Getrennt, damit die Oberflaeche nur den Namen zum Bearbeiten
|
||||
# gibt und die Endung danebenstellt.
|
||||
record["stem"] = entry.stem
|
||||
record["suffix"] = entry.suffix
|
||||
files.append(record)
|
||||
except OSError as exc:
|
||||
raise FileshareError(f"Verzeichnis nicht lesbar ({exc}).") from exc
|
||||
@@ -656,8 +660,26 @@ def make_directory(mount: Mount, relative: str, name: str) -> Path:
|
||||
|
||||
|
||||
def rename_entry(mount: Mount, relative: str, new_name: str) -> Path:
|
||||
"""Benennt um und behaelt die Endung der Datei.
|
||||
|
||||
Eingegeben wird nur der Name ohne Endung. Eine Datei, die danach auf
|
||||
``.pdf.txt`` oder auf gar nichts endet, waere auf einer Windows-Freigabe
|
||||
praktisch kaputt -- sie oeffnet sich nicht mehr mit dem richtigen
|
||||
Programm. Verzeichnisse haben keine Endung; dort bleibt der Name, wie er
|
||||
eingegeben wurde.
|
||||
|
||||
Wer die Endung trotzdem mittippt, bekommt sie nicht doppelt.
|
||||
"""
|
||||
path = resolve_path(mount, relative)
|
||||
target = path.parent / safe_filename(new_name)
|
||||
endung = path.suffix if path.is_file() else ""
|
||||
gewuenscht = (new_name or "").strip()
|
||||
if endung and gewuenscht.lower().endswith(endung.lower()):
|
||||
gewuenscht = gewuenscht[: -len(endung)]
|
||||
if not gewuenscht:
|
||||
raise FileshareError("Der Name darf nicht leer sein.")
|
||||
target = path.parent / safe_filename(gewuenscht + endung)
|
||||
if target == path:
|
||||
return path
|
||||
if target.exists():
|
||||
raise FileshareError("Ein Eintrag mit diesem Namen existiert bereits.")
|
||||
path.rename(target)
|
||||
|
||||
@@ -133,7 +133,8 @@
|
||||
<button class="btn btn--sm btn--ghost" type="button"
|
||||
data-dialog-open="file-rename"
|
||||
data-fill-Pfad="{{ entry.relative }}"
|
||||
data-fill-Name="{{ entry.name }}"
|
||||
data-fill-Name="{{ entry.stem }}"
|
||||
data-fill-Endung="{{ entry.suffix or '' }}"
|
||||
aria-label="Umbenennen">{{ icon('pencil', size=15) }}</button>
|
||||
{% endif %}
|
||||
<a class="btn btn--sm btn--ghost"
|
||||
@@ -234,9 +235,19 @@
|
||||
placeholder="Wurzelverzeichnis der Freigabe" autocomplete="off">
|
||||
</label>
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Dateiname</span>
|
||||
<span class="field__label">Dateiname (ohne Endung)</span>
|
||||
<input class="input" data-viewer-name autocomplete="off">
|
||||
</label>
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Speichern als</span>
|
||||
{# Zwei Wege, beide gewollt: weiter ausfüllbar (das Bild wird das
|
||||
Aussehen des Knopfes und lässt sich später austauschen) oder
|
||||
abgeschlossen (alle Felder werden Teil der Seite). #}
|
||||
<select class="select" data-viewer-modus>
|
||||
<option value="formular">Formular – weiter ausfüllbar</option>
|
||||
<option value="fertig">Abgeschlossen – Felder gesperrt</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="check">
|
||||
<input type="checkbox" data-viewer-ersetzen>
|
||||
<span>Vorhandene ersetzen</span>
|
||||
@@ -280,9 +291,12 @@
|
||||
<input type="hidden" name="parent" value="{{ listing.relative }}">
|
||||
<input type="hidden" name="pfad" data-fill-target="Pfad" value="">
|
||||
<div class="modal__body">
|
||||
{{ field('name', 'Neuer Name', required=True, attrs='data-fill-target="Name"') }}
|
||||
{{ field('name', 'Neuer Name (ohne Endung)', required=True,
|
||||
attrs='data-fill-target="Name"') }}
|
||||
<p class="fs-xs faint">
|
||||
Umbenannt wird auf der Freigabe selbst -- es gibt dort keinen Papierkorb.
|
||||
Die Endung <code data-fill-target="Endung"></code> bleibt, wie sie ist --
|
||||
eine Datei ohne passende Endung öffnet auf der Freigabe nichts mehr.
|
||||
Umbenannt wird dort direkt; einen Papierkorb gibt es nicht.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal__footer">
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-core"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -135,7 +135,7 @@ def create_base_app(
|
||||
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||
#: Anwendungen.
|
||||
auth_tagline: str = "",
|
||||
app_version: str = "2.3.1",
|
||||
app_version: str = "2.3.3",
|
||||
built_in_trusted_keys: tuple[str, ...] = (),
|
||||
enable_license: bool = True,
|
||||
template_folder: str | None = None,
|
||||
|
||||
@@ -720,6 +720,16 @@
|
||||
min-height: 220px;
|
||||
background: var(--surface-1);
|
||||
}
|
||||
/* Hochkant -- Tablet oder Monitor im Hochformat -- gibt es unter dem Dialog
|
||||
viel Platz, den die Obergrenze verschenkt. Beim Ausfuellen eines
|
||||
Formulars zaehlt jede sichtbare Zeile, also nehmen wir dort die Hoehe,
|
||||
die da ist. Der Rest (Kopf, Dateiname, Fusszeile) braucht rund 200 Pixel;
|
||||
die 240 lassen etwas Luft. */
|
||||
@media (orientation: portrait) {
|
||||
.viewer {
|
||||
height: calc(100dvh - 240px);
|
||||
}
|
||||
}
|
||||
.viewer > .file-preview {
|
||||
flex: 1;
|
||||
height: auto;
|
||||
@@ -763,6 +773,11 @@
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
/* Ein PDF ist weisses Papier. pdf.js malt Formularfelder mit Systemfarben
|
||||
und ueberlaesst Ankreuzfelder dem Browser -- im dunklen Design heisst
|
||||
CanvasText weiss, und die Haken waren auf dem Papier unsichtbar. Fuer
|
||||
diesen Ausschnitt gilt deshalb immer das helle Schema. */
|
||||
color-scheme: light;
|
||||
}
|
||||
/* pdf.js rechnet fuer die Seite mit dem Standard "content-box": ihr
|
||||
9-Pixel-Rahmen liegt *ausserhalb* der angegebenen Breite. Unser globales
|
||||
|
||||
@@ -215,44 +215,59 @@ async function pdflibLaden(adresse) {
|
||||
return pdflibGeladen;
|
||||
}
|
||||
|
||||
/** Stempelt die gewaehlten Fotos in die Bildfelder.
|
||||
/** Setzt die Fotos ein und schliesst das Formular auf Wunsch ab.
|
||||
*
|
||||
* Eingabe sind die Bytes, die pdf.js nach dem Ausfuellen geschrieben hat --
|
||||
* die Formularwerte bleiben also erhalten.
|
||||
* die Feldwerte bleiben also erhalten.
|
||||
*
|
||||
* Das Foto wird das **Aussehen des Bildknopfes** und nicht in die Seite
|
||||
* gestempelt: so bleibt der Knopf erhalten, das Bild ist trotzdem zu sehen,
|
||||
* und beim naechsten Oeffnen laesst es sich austauschen. Genau das tut
|
||||
* Acrobat mit "buttonImportIcon".
|
||||
*
|
||||
* ``abschliessen`` rechnet danach alle Felder flach in die Seite: heraus kommt
|
||||
* ein PDF ohne Formular -- nichts mehr zu verstellen.
|
||||
*/
|
||||
async function fotosEinsetzen(bytes, bilder, adresse) {
|
||||
async function nachbearbeiten(bytes, bilder, adresse, { abschliessen = false } = {}) {
|
||||
const PDFLib = await pdflibLaden(adresse);
|
||||
const dokument = await PDFLib.PDFDocument.load(bytes);
|
||||
const formular = dokument.getForm();
|
||||
|
||||
for (const eintrag of bilder) {
|
||||
const seite = dokument.getPage(eintrag.seite - 1);
|
||||
const jpeg = await dokument.embedJpg(eintrag.foto.bytes);
|
||||
const knopf = eintrag.feldname ? formular.getFieldMaybe(eintrag.feldname) : null;
|
||||
if (knopf && typeof knopf.setImage === "function") {
|
||||
knopf.setImage(jpeg);
|
||||
continue;
|
||||
}
|
||||
// Kein Knopf gefunden: dann eben in die Seite. Damit nichts darueber
|
||||
// liegt, muss das Feld weichen.
|
||||
const seite = dokument.getPage(eintrag.seite - 1);
|
||||
const [x1, y1, x2, y2] = eintrag.rechteck;
|
||||
const kastenBreite = Math.abs(x2 - x1);
|
||||
const kastenHoehe = Math.abs(y2 - y1);
|
||||
// Einpassen statt verzerren: das Seitenverhaeltnis des Fotos bleibt.
|
||||
const faktor = Math.min(kastenBreite / jpeg.width, kastenHoehe / jpeg.height);
|
||||
const breite = jpeg.width * faktor;
|
||||
const hoehe = jpeg.height * faktor;
|
||||
seite.drawImage(jpeg, {
|
||||
x: Math.min(x1, x2) + (kastenBreite - breite) / 2,
|
||||
y: Math.min(y1, y2) + (kastenHoehe - hoehe) / 2,
|
||||
width: breite,
|
||||
height: hoehe,
|
||||
x: Math.min(x1, x2) + (kastenBreite - jpeg.width * faktor) / 2,
|
||||
y: Math.min(y1, y2) + (kastenHoehe - jpeg.height * faktor) / 2,
|
||||
width: jpeg.width * faktor,
|
||||
height: jpeg.height * faktor,
|
||||
});
|
||||
// Den Knopf aus der fertigen Kopie nehmen: seine Flaeche liegt ueber dem
|
||||
// Seiteninhalt und wuerde das Foto verdecken. Zu tun haette er dort auch
|
||||
// nichts mehr -- seine Aktion fuehrt kein Browser aus, und das Bild steht
|
||||
// jetzt in der Seite. Die Vorlage bleibt unberuehrt.
|
||||
if (knopf) {
|
||||
try {
|
||||
const feld = formular.getFieldMaybe(eintrag.feldname);
|
||||
if (feld) formular.removeField(feld);
|
||||
formular.removeField(knopf);
|
||||
} catch (fehler) {
|
||||
// Bleibt der Knopf liegen, ist das Foto verdeckt -- aber die Datei ist
|
||||
// heil. Das ist der bessere von zwei schlechten Ausgaengen.
|
||||
console.warn("Bildknopf liess sich nicht entfernen", fehler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (abschliessen) {
|
||||
// Alle Felder werden Teil der Seite. Danach gibt es kein Formular mehr --
|
||||
// gewollt: so kann niemand mehr etwas verstellen.
|
||||
formular.flatten();
|
||||
}
|
||||
return dokument.save();
|
||||
}
|
||||
|
||||
@@ -439,10 +454,11 @@ async function pdfBetrachter(behaelter, seiten, url, basis) {
|
||||
* pdf.js schreibt die Feldwerte; gewaehlte Fotos kommen danach mit
|
||||
* pdf-lib hinein -- pdf.js kann nichts in ein PDF zeichnen.
|
||||
*/
|
||||
speichern: async (pdflibAdresse) => {
|
||||
speichern: async (pdflibAdresse, { abschliessen = false } = {}) => {
|
||||
const bytes = await dokument.saveDocument();
|
||||
if (!bilder.size) return bytes;
|
||||
return fotosEinsetzen(bytes, [...bilder.values()], pdflibAdresse);
|
||||
// Ohne Bild und ohne Abschluss braucht es pdf-lib gar nicht.
|
||||
if (!bilder.size && !abschliessen) return bytes;
|
||||
return nachbearbeiten(bytes, [...bilder.values()], pdflibAdresse, { abschliessen });
|
||||
},
|
||||
/** Wurde ueberhaupt etwas eingetragen? */
|
||||
veraendert: () => {
|
||||
@@ -482,6 +498,7 @@ export function initFilePreview(wurzel = document) {
|
||||
const ersetzenFeld = stueck("ersetzen");
|
||||
const jetzt = stueck("jetzt");
|
||||
const abbrechen = stueck("abbrechen");
|
||||
const modusFeld = stueck("modus");
|
||||
const basis = rumpf.dataset.viewerPdfjs || "";
|
||||
|
||||
// Ein Format, das dieser Browser nicht kann (HEIC ausserhalb von Safari),
|
||||
@@ -593,9 +610,10 @@ export function initFilePreview(wurzel = document) {
|
||||
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";
|
||||
// Ohne Endung: die haengt der Server an. Eine Datei, die auf ".pdf.txt"
|
||||
// oder auf gar nichts endet, oeffnet auf einer Freigabe nichts mehr.
|
||||
// Der Zusatz haelt die Vorlage heil, auch wenn jemand blind speichert.
|
||||
nameFeld.value = datei.replace(/\.pdf$/i, "") + "-ausgefuellt";
|
||||
ersetzenFeld.checked = false;
|
||||
};
|
||||
|
||||
@@ -618,7 +636,9 @@ export function initFilePreview(wurzel = document) {
|
||||
const vorher = jetzt.textContent;
|
||||
jetzt.textContent = "Speichern ...";
|
||||
try {
|
||||
const bytes = await pdf.speichern(rumpf.dataset.viewerPdflib || "");
|
||||
const bytes = await pdf.speichern(rumpf.dataset.viewerPdflib || "", {
|
||||
abschliessen: modusFeld?.value === "fertig",
|
||||
});
|
||||
const daten = new FormData();
|
||||
daten.append("pfad", aktuell.pfad);
|
||||
daten.append("ordner", ordnerFeld.value.trim());
|
||||
@@ -627,7 +647,7 @@ export function initFilePreview(wurzel = document) {
|
||||
daten.append(
|
||||
"datei",
|
||||
new Blob([bytes], { type: "application/pdf" }),
|
||||
nameFeld.value.trim() || "formular.pdf"
|
||||
`${nameFeld.value.trim() || "formular"}.pdf`
|
||||
);
|
||||
const antwort = await window.tesm.api(ziel, { method: "POST", body: daten });
|
||||
window.tesm.showToast(antwort.message || "Gespeichert.", "success");
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-licensing"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["cryptography>=42"]
|
||||
|
||||
Reference in new issue
Block a user