first release
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user