Files
deploy-email-plesk-kerio-ne…/gui.py
T
2026-05-12 11:04:17 +02:00

226 lines
8.8 KiB
Python

"""Tkinter-GUI mit Endlosfeldern (eine Zeile = ein Account)."""
from __future__ import annotations
import threading
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
from config import Config
from deploy import parse_csv, run_deploy
from models import Account
FIELDS = [
("vorname", "Vorname", 14),
("name", "Name", 14),
("emailadresse", "Emailadresse", 22),
("pleskhost", "Plesk-Host", 18),
("keriohost", "Kerio-Host", 18),
("nextcloudhost", "Nextcloud-Host", 18),
("pleskemailkennwort", "Plesk Mail-PW", 14),
("kerioemailkennwort", "Kerio PW", 14),
("nextcloudkennwort", "Nextcloud PW", 14),
("nextcloudgruppe", "NC Gruppe", 14),
("nextcloudspeicher", "NC GB (leer=∞)", 12),
]
class AccountRow(ttk.Frame):
def __init__(self, master, on_remove):
super().__init__(master)
self.entries: dict[str, ttk.Entry] = {}
for col, (key, _label, width) in enumerate(FIELDS):
e = ttk.Entry(self, width=width)
e.grid(row=0, column=col, padx=2, pady=2, sticky="ew")
self.entries[key] = e
ttk.Button(self, text="", width=3,
command=lambda: on_remove(self)).grid(
row=0, column=len(FIELDS), padx=4)
def to_dict(self) -> dict:
return {k: e.get().strip() for k, e in self.entries.items()}
def from_dict(self, d: dict) -> None:
for k, e in self.entries.items():
e.delete(0, "end")
v = d.get(k, "")
e.insert(0, "" if v is None else str(v))
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Email / Cloud Account Deployment")
self.geometry("1500x800")
self.output_dir = Path("./output").resolve()
self.rows: list[AccountRow] = []
self._build()
self.add_row()
# ----- Layout -----
def _build(self):
toolbar = ttk.Frame(self)
toolbar.pack(fill="x", padx=8, pady=6)
ttk.Button(toolbar, text="+ Zeile", command=self.add_row).pack(side="left", padx=2)
ttk.Button(toolbar, text="CSV laden …", command=self.load_csv).pack(side="left", padx=2)
ttk.Button(toolbar, text="Ausgabeordner …", command=self.choose_output).pack(side="left", padx=2)
self.output_lbl = ttk.Label(toolbar, text=f"Output: {self.output_dir}")
self.output_lbl.pack(side="left", padx=8)
self.run_btn = ttk.Button(toolbar, text="Ausführen ▶", command=self.run)
self.run_btn.pack(side="right", padx=2)
# Header über die Eingabezeilen
header = ttk.Frame(self)
header.pack(fill="x", padx=8)
for col, (_k, label, width) in enumerate(FIELDS):
ttk.Label(header, text=label, font=("Helvetica", 9, "bold"),
width=width, anchor="w").grid(row=0, column=col, padx=2, sticky="w")
# Scrollbares Eingabefeld
wrap = ttk.Frame(self)
wrap.pack(fill="both", expand=True, padx=8, pady=4)
self.canvas = tk.Canvas(wrap, highlightthickness=0)
sb = ttk.Scrollbar(wrap, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=sb.set)
self.canvas.pack(side="left", fill="both", expand=True)
sb.pack(side="right", fill="y")
self.rows_frame = ttk.Frame(self.canvas)
self.canvas.create_window((0, 0), window=self.rows_frame, anchor="nw")
self.rows_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")),
)
# Mousewheel-Scrollen
self.canvas.bind_all("<MouseWheel>",
lambda e: self.canvas.yview_scroll(int(-e.delta / 120), "units"))
self.canvas.bind_all("<Button-4>",
lambda e: self.canvas.yview_scroll(-1, "units"))
self.canvas.bind_all("<Button-5>",
lambda e: self.canvas.yview_scroll(1, "units"))
ttk.Label(self, text="Log:").pack(anchor="w", padx=8)
self.log = scrolledtext.ScrolledText(self, height=14, wrap="word",
font=("Monospace", 9))
self.log.pack(fill="x", padx=8, pady=(0, 8))
# ----- Reihen-Handling -----
def add_row(self, data: dict | None = None):
row = AccountRow(self.rows_frame, on_remove=self.remove_row)
row.pack(fill="x", pady=1)
if data:
row.from_dict(data)
self.rows.append(row)
def remove_row(self, row):
if len(self.rows) <= 1:
row.from_dict({}) # leere statt entfernen
return
self.rows.remove(row)
row.destroy()
# ----- Buttons -----
def load_csv(self):
path = filedialog.askopenfilename(
filetypes=[("CSV", "*.csv"), ("Alle Dateien", "*.*")])
if not path:
return
try:
accounts = parse_csv(path)
except Exception as e:
messagebox.showerror("CSV-Fehler", str(e))
return
for r in self.rows:
r.destroy()
self.rows = []
for acc in accounts:
self.add_row({
"vorname": acc.vorname,
"name": acc.name,
"emailadresse": acc.emailadresse,
"pleskhost": acc.pleskhost,
"keriohost": acc.keriohost,
"nextcloudhost": acc.nextcloudhost,
"pleskemailkennwort": acc.pleskemailkennwort,
"kerioemailkennwort": acc.kerioemailkennwort,
"nextcloudkennwort": acc.nextcloudkennwort,
"nextcloudgruppe": acc.nextcloudgruppe,
"nextcloudspeicher": "" if acc.nextcloudspeicher is None
else str(acc.nextcloudspeicher),
})
if not self.rows:
self.add_row()
self._log(f"CSV geladen: {len(accounts)} Account(s)")
def choose_output(self):
path = filedialog.askdirectory(initialdir=str(self.output_dir))
if path:
self.output_dir = Path(path)
self.output_lbl.config(text=f"Output: {self.output_dir}")
# ----- Ausführen -----
def _collect_accounts(self) -> list[Account]:
accounts = []
for idx, r in enumerate(self.rows, 1):
d = r.to_dict()
if not d.get("emailadresse"):
continue
required = ["name", "vorname", "emailadresse",
"pleskhost", "keriohost", "nextcloudhost",
"pleskemailkennwort", "kerioemailkennwort",
"nextcloudkennwort"]
missing = [k for k in required if not d.get(k)]
if missing:
raise ValueError(f"Zeile {idx}: Pflichtfelder leer: {', '.join(missing)}")
quota = int(d["nextcloudspeicher"]) if d.get("nextcloudspeicher") else None
accounts.append(Account(
name=d["name"], vorname=d["vorname"],
emailadresse=d["emailadresse"],
pleskhost=d["pleskhost"], keriohost=d["keriohost"],
nextcloudhost=d["nextcloudhost"],
kerioemailkennwort=d["kerioemailkennwort"],
pleskemailkennwort=d["pleskemailkennwort"],
nextcloudgruppe=d.get("nextcloudgruppe", ""),
nextcloudspeicher=quota,
nextcloudkennwort=d["nextcloudkennwort"],
))
return accounts
def _log(self, msg: str):
self.log.insert("end", msg + "\n")
self.log.see("end")
def run(self):
try:
accounts = self._collect_accounts()
except ValueError as e:
messagebox.showerror("Eingabe ungültig", str(e))
return
if not accounts:
messagebox.showinfo("Hinweis", "Keine Accounts eingetragen.")
return
cfg = Config()
self.log.delete("1.0", "end")
self.run_btn.config(state="disabled")
def worker():
try:
_, combined = run_deploy(accounts, cfg, self.output_dir,
log=lambda m: self.after(0, self._log, m))
self.after(0, lambda: messagebox.showinfo(
"Fertig", f"Verarbeitung abgeschlossen.\n\nGesamt-PDF:\n{combined}"))
except Exception as e:
self.after(0, lambda: self._log(f"FEHLER: {e}"))
self.after(0, lambda: messagebox.showerror("Fehler", str(e)))
finally:
self.after(0, lambda: self.run_btn.config(state="normal"))
threading.Thread(target=worker, daemon=True).start()
def launch():
App().mainloop()
if __name__ == "__main__":
launch()