Version 2.6.0: Unterschreiben mit dem Finger, Ablage in eine andere Freigabe
This commit is contained in:
1 parent
07ae69e3b7
commit
754cbea6bb
11 files changed
+531
-16
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.5.2"
|
||||
version = "2.6.0"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
@@ -47,6 +50,28 @@ def _audit(action: str, target: str, detail: str = "", severity: str = "info") -
|
||||
)
|
||||
|
||||
|
||||
def _unterschriften(roh: str) -> list[str]:
|
||||
"""Die Namen aus dem Unterschriftsfeld -- Text, keine Struktur.
|
||||
|
||||
Was hier ankommt, hat der Browser geschickt und damit der Mensch davor.
|
||||
Es wird protokolliert, nicht geglaubt: laenge begrenzt, Zeilenumbrueche
|
||||
raus, mehr passiert damit nicht.
|
||||
"""
|
||||
namen: list[str] = []
|
||||
try:
|
||||
eintraege = json.loads(roh) if roh else []
|
||||
except ValueError:
|
||||
return []
|
||||
if not isinstance(eintraege, list):
|
||||
return []
|
||||
for eintrag in eintraege[:20]:
|
||||
name = str(eintrag if isinstance(eintrag, str) else eintrag.get("name", ""))
|
||||
name = " ".join(name.split())[:120]
|
||||
if name:
|
||||
namen.append(name)
|
||||
return namen
|
||||
|
||||
|
||||
def _mount_or_404(token: str) -> service.Mount:
|
||||
mounts = service.active_mounts(db(), current_user.id)
|
||||
for mount in mounts:
|
||||
@@ -82,6 +107,10 @@ def browse():
|
||||
max_upload_mb=uploads.limit_mb(conn),
|
||||
# Vorschlagsliste fuer "Ausgefuellt speichern": wohin darf es gehen?
|
||||
verzeichnisse=(service.all_directories(mount) if listing else []),
|
||||
signature_witness=bool(core().settings.get(conn, "signature_witness_enabled")),
|
||||
# Die Uhr des Servers. Das Datum unter einer Unterschrift soll nicht
|
||||
# davon abhaengen, wie das Tablet gestellt ist.
|
||||
serverzeit=datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
|
||||
)
|
||||
|
||||
|
||||
@@ -186,6 +215,21 @@ def upload(token: str):
|
||||
return redirect(url_for("fileshare.browse", share=token, path=relative))
|
||||
|
||||
|
||||
@bp.get("/<token>/folders")
|
||||
@permission_required("fileshare.view")
|
||||
def folders(token: str):
|
||||
"""Die Ordner **dieser** Freigabe als Vorschlagsliste.
|
||||
|
||||
Gebraucht, seit das Speichern in eine andere Freigabe gehen darf: die
|
||||
Vorschlaege muessen der gewaehlten Freigabe folgen. Alle Listen beim
|
||||
Seitenaufbau mitzuschicken waere die Alternative gewesen -- bei vier
|
||||
eingebundenen Freigaben mit je ein paar hundert Ordnern ist das eine
|
||||
viertel Megabyte, die fast nie gebraucht wird.
|
||||
"""
|
||||
mount = _mount_or_404(token)
|
||||
return {"folders": service.all_directories(mount)}
|
||||
|
||||
|
||||
@bp.post("/<token>/form")
|
||||
@permission_required("fileshare_pdf.create")
|
||||
def save_form(token: str):
|
||||
@@ -196,6 +240,12 @@ def save_form(token: str):
|
||||
die alte Datei danach loeschen -- das ist eine bewusste Handlung.
|
||||
"""
|
||||
mount = _mount_or_404(token)
|
||||
# Das Ziel darf eine andere Freigabe sein: die Vorlage liegt oft in einer
|
||||
# Vorlagenfreigabe, die ausgefuellte Fassung gehoert in die Ablage des
|
||||
# Bereichs. Geprueft wird es wie jede andere Einbindung -- nur eine, die
|
||||
# diesem Benutzer gerade gehoert, kommt in Frage.
|
||||
ziel_token = (request.form.get("ziel") or "").strip()
|
||||
ziel_mount = _mount_or_404(ziel_token) if ziel_token else mount
|
||||
relative = request.form.get("path") or ""
|
||||
upload_file = request.files.get("datei")
|
||||
if upload_file is None or not upload_file.filename:
|
||||
@@ -228,15 +278,22 @@ def save_form(token: str):
|
||||
|
||||
try:
|
||||
original = service.resolve_path(mount, relative)
|
||||
# Ohne Namen landet die Datei neben dem Original -- das geht nur in
|
||||
# derselben Freigabe.
|
||||
if ziel_mount.token != mount.token and not wunschname:
|
||||
return {"error": "Fuer eine andere Freigabe braucht es einen Dateinamen."}, 400
|
||||
# Mit Namen entscheidet das Ordnerfeld -- leer heisst Wurzelverzeichnis,
|
||||
# so wie es im Feld steht. Ohne Namen (Aufruf ohne Oberflaeche) bleibt
|
||||
# es beim Ordner des Originals.
|
||||
if wunschname:
|
||||
zielordner = service.resolve_path(mount, ordner)
|
||||
zielordner = service.resolve_path(ziel_mount, ordner)
|
||||
else:
|
||||
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
|
||||
return {
|
||||
"error": f"Den Ordner '{ordner}' gibt es in der Freigabe "
|
||||
f"'{ziel_mount.label}' 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
|
||||
@@ -257,9 +314,25 @@ def save_form(token: str):
|
||||
except service.FileshareError as exc:
|
||||
return {"error": str(exc)}, 400
|
||||
|
||||
wohin = ziel.relative_to(mount.path.resolve()).as_posix()
|
||||
_audit("fileshare.form_saved", f"{mount.label}:{relative}", wohin, severity="notice")
|
||||
return {"message": f"Gespeichert als {wohin}.", "path": wohin}
|
||||
wohin = ziel.relative_to(ziel_mount.path.resolve()).as_posix()
|
||||
pruefsumme = hashlib.sha256(daten).hexdigest()
|
||||
unterschriften = _unterschriften(request.form.get("unterschriften") or "")
|
||||
|
||||
# Zwei Angaben, die man nicht verwechseln darf, und deshalb stehen beide
|
||||
# da: der Name **in** der Unterschrift ist eine Angabe des
|
||||
# Unterschreibenden -- wie auf Papier. Wer die Datei abgelegt hat, steht
|
||||
# ohnehin als Handelnder in der Zeile und ist belegt.
|
||||
beschreibung = f"{ziel_mount.label}:{wohin} sha256={pruefsumme}"
|
||||
if unterschriften:
|
||||
namen = ", ".join(unterschriften)
|
||||
beschreibung = f"{beschreibung} -- angegeben unterschrieben von: {namen}"
|
||||
_audit(
|
||||
"fileshare.form_signed" if unterschriften else "fileshare.form_saved",
|
||||
f"{mount.label}:{relative}",
|
||||
beschreibung,
|
||||
severity="notice",
|
||||
)
|
||||
return {"message": f"Gespeichert als {wohin}.", "path": wohin, "sha256": pruefsumme}
|
||||
|
||||
|
||||
@bp.post("/<token>/folder")
|
||||
|
||||
@@ -82,6 +82,7 @@ def system():
|
||||
"terminal_switches_enabled",
|
||||
"terminal_clients_enabled",
|
||||
"terminal_idle_timeout",
|
||||
"signature_witness_enabled",
|
||||
"trash_retention_days",
|
||||
"session_idle_minutes",
|
||||
"session_absolute_hours",
|
||||
@@ -116,6 +117,7 @@ def system_save():
|
||||
"terminal_switches_enabled",
|
||||
"terminal_clients_enabled",
|
||||
"terminal_idle_timeout",
|
||||
"signature_witness_enabled",
|
||||
"trash_retention_days",
|
||||
"session_idle_minutes",
|
||||
"session_absolute_hours",
|
||||
|
||||
@@ -98,6 +98,18 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = (
|
||||
minimum=30,
|
||||
maximum=1800,
|
||||
),
|
||||
# -- Unterschriften ------------------------------------------------------ #
|
||||
SettingSpec(
|
||||
"signature_witness_enabled",
|
||||
True,
|
||||
"bool",
|
||||
"Unterschriften mit dem ablegenden Konto bezeugen",
|
||||
hint="Unter jede Unterschrift kommt eine Zeile, wer das Dokument in TESM abgelegt "
|
||||
"hat. Der Name in der Unterschrift ist eine Angabe des Unterschreibenden -- wie "
|
||||
"auf Papier; das Konto daneben ist belegt. Eine Passwortabfrage gibt es dafuer "
|
||||
"nicht.",
|
||||
section="fileshare",
|
||||
),
|
||||
# -- Terminal ----------------------------------------------------------- #
|
||||
SettingSpec(
|
||||
"terminal_switches_enabled",
|
||||
|
||||
@@ -224,7 +224,10 @@
|
||||
wie es ist -- mit seinen eigenen Marken, ohne Eingabefelder. #}
|
||||
data-viewer-formulare="{{ '1' if current_user.has('fileshare_pdf.view') else '' }}"
|
||||
data-viewer-pdfjs="{{ asset_url('vendor/pdfjs/pdf.min.js') }}"
|
||||
data-viewer-pdflib="{{ asset_url('vendor/pdflib/pdf-lib.min.js') }}">
|
||||
data-viewer-pdflib="{{ asset_url('vendor/pdflib/pdf-lib.min.js') }}"
|
||||
data-viewer-konto="{{ current_user.username }}"
|
||||
data-viewer-zeuge="{{ '1' if signature_witness else '0' }}"
|
||||
data-viewer-zeit="{{ serverzeit }}">
|
||||
<div class="viewer">
|
||||
<div class="viewer__stage" data-viewer-bildbuehne hidden>
|
||||
<img class="viewer__img" alt="" data-viewer-bild>
|
||||
@@ -243,7 +246,49 @@
|
||||
eine Zeile Formular über jeder Bildvorschau. Und nur, wer Formulare
|
||||
ablegen darf, sieht sie überhaupt. #}
|
||||
{% if current_user.has('fileshare_pdf.create') %}
|
||||
{# Unterschreiben: Name tippen, mit dem Finger zeichnen, ins Dokument
|
||||
setzen. Der Name daneben ist eine Angabe des Unterschreibenden – wie
|
||||
auf Papier; wer abgelegt hat, steht im Änderungsprotokoll. #}
|
||||
<div class="viewer__sign" data-viewer-unterschriftpanel hidden>
|
||||
<div class="viewer__signfelder">
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Vorname</span>
|
||||
<input class="input" data-viewer-vorname autocomplete="off">
|
||||
</label>
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Nachname</span>
|
||||
<input class="input" data-viewer-nachname autocomplete="off">
|
||||
</label>
|
||||
</div>
|
||||
<canvas class="viewer__signpad" data-viewer-signpad width="700" height="200"
|
||||
aria-label="Unterschriftsfeld"></canvas>
|
||||
<div class="viewer__signaktionen">
|
||||
<span class="fs-xs faint" data-viewer-signhinweis></span>
|
||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-signleeren>Leeren</button>
|
||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-signabbrechen>Abbrechen</button>
|
||||
<button class="btn btn--sm btn--primary" type="button" data-viewer-signsetzen>
|
||||
Unterschrift setzen
|
||||
</button>
|
||||
</div>
|
||||
<p class="fs-xs faint">
|
||||
Nach „Unterschrift setzen“ auf die Stelle im Dokument tippen. Mit Unterschrift
|
||||
wird immer abgeschlossen gespeichert – die Felder sind danach gesperrt.
|
||||
</p>
|
||||
</div>
|
||||
<div class="viewer__save" data-viewer-speichernzeile hidden>
|
||||
{# Zwei eingebundene Freigaben, ein Formular: die Vorlage liegt in der
|
||||
einen, die ausgefüllte Fassung gehört in die andere. #}
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Freigabe</span>
|
||||
<select class="select" data-viewer-ziel
|
||||
data-url-vorlage="{{ url_for('fileshare.folders', token='__token__') }}">
|
||||
{% for eintrag in mounts %}
|
||||
<option value="{{ eintrag.token }}" {% if eintrag.token == token %}selected{% endif %}>
|
||||
{{ eintrag.label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="viewer__feld">
|
||||
<span class="field__label">Ordner</span>
|
||||
<input class="input" list="freigabe-ordner" data-viewer-ordner
|
||||
@@ -293,6 +338,9 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.has('fileshare_pdf.create') %}
|
||||
<button class="btn btn--sm" type="button" data-viewer-unterschreiben hidden>
|
||||
{{ icon('pencil', size=15) }} Unterschreiben
|
||||
</button>
|
||||
<button class="btn btn--sm" type="button" data-viewer-speichern hidden
|
||||
data-url="{{ url_for('fileshare.save_form', token=token) }}">
|
||||
{{ icon('save', size=15) }} Ausgefüllt speichern
|
||||
|
||||
@@ -78,6 +78,27 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="card__header">
|
||||
<div class="flex-1">
|
||||
<div class="card__title">{{ icon('pencil', size=17) }} Unterschriften</div>
|
||||
<div class="card__sub">
|
||||
Der Name in der Unterschrift ist eine Angabe des Unterschreibenden – wie auf
|
||||
Papier. Wer das Dokument in TESM abgelegt hat, ist dagegen belegt.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('settings.system_save') }}">
|
||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||
<div class="card__body stack">
|
||||
{{ checkbox('signature_witness_enabled', 'Unterschriften mit dem ablegenden Konto bezeugen',
|
||||
checked=values.signature_witness_enabled,
|
||||
hint_text=specs['signature_witness_enabled'].hint) }}
|
||||
</div>
|
||||
<div class="card__footer"><button class="btn btn--primary" type="submit">Speichern</button></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{# Eigene Karte: die Abmeldung stand zwischen Protokollrotation und
|
||||
Papierkorb -- drei Dinge ohne Zusammenhang. Wer die Sitzungsdauer sucht,
|
||||
sucht sie nicht unter "Protokolle". #}
|
||||
|
||||
Reference in new issue
Block a user