62 Commits

Author SHA1 Message Date
6a22ecb591 moved tests out of src, added more test cases 2025-05-08 13:31:24 +02:00
233f81c33e switched to model instead of connector, extracts config from parent 2025-05-08 13:30:49 +02:00
e939c97474 moved connector to model, instead injecting model for easier testing 2025-05-08 13:29:50 +02:00
7028c7ce69 refactored for easier testing 2025-05-08 13:28:16 +02:00
2a25111c0a refactored for easier testing 2025-05-08 13:27:25 +02:00
8125d88695 move connecter selection to model 2025-05-08 13:24:20 +02:00
b8581b34cc first test 2025-05-08 12:25:49 +02:00
3224c1c2aa Merge pull request 'fix_model_structure' (#13) from fix_model_structure into main
Reviewed-on: #13
2025-05-04 16:33:22 +02:00
ca88c4d229 compiled 0.1.0b beta version for release 2025-05-04 16:30:50 +02:00
12d0d7034f refactored model, fixed utf-8 export json 2025-05-04 16:22:56 +02:00
2d840d6ad4 Merge pull request 'Statuszeile und filtern von aktiven Adressen' (#9) from statuszeile_und_filter into main
Reviewed-on: #9
2025-04-30 20:04:41 +02:00
0ae2c7e559 new version 0.7a, and update json filename for new format 2025-04-30 20:00:20 +02:00
96995f1e80 fixed keybindings, refactoring and table focus 2025-04-30 19:58:16 +02:00
ec130fcee9 removed all "debug" print commands 2025-04-28 00:24:00 +02:00
95ed06df45 typo in label 2025-04-28 00:23:19 +02:00
9ea44933a6 recreated json file handling as connector model, fixing #10 and introducing #8 and #7 2025-04-28 00:18:22 +02:00
dc27197129 handles data storage 2025-04-27 22:56:14 +02:00
bbdbaadb92 filtering inactive records 2025-04-27 12:22:25 +02:00
ad66988e09 sorting of columns was not nice, should be better now 2025-04-27 11:54:32 +02:00
3fd0147158 implemented status bar covering #8 2025-04-27 00:33:10 +02:00
aec7770e74 added statusbar with stringvar 2025-04-27 00:24:30 +02:00
d16aefe20f refactoring 2025-04-27 00:21:12 +02:00
307057a387 0.6a release 2025-04-26 23:41:57 +02:00
4917e5e9bd added version number to compile script 2025-04-26 23:40:20 +02:00
3f6b22abc8 added version number to compile script 2025-04-26 23:40:13 +02:00
6785a8f46d icon png version 2025-04-26 23:09:21 +02:00
dd51921784 Merge branch 'detached' with bugfix #5 2025-04-26 22:55:21 +02:00
15dc182a37 bugfix #5 final and tested 2025-04-26 22:47:03 +02:00
057e0a02de #5 new fix 2025-04-26 13:27:45 +02:00
6c8ce7b208 bugfix #5 2025-04-26 13:03:15 +02:00
bcb4bb90d5 moved compile script out of source and optimized it 2025-04-22 12:23:21 +02:00
00a1e00c86 desktop file is already in deb package, removed from source 2025-04-22 12:14:42 +02:00
3d3a833d4f moved labes file out of source 2025-04-22 12:13:47 +02:00
70b5fe5ef7 not using the icon here 2025-04-22 12:08:44 +02:00
325f9214c7 version # 2025-04-22 12:06:47 +02:00
b50e0df01b no need to add deb package to git 2025-04-22 01:37:44 +02:00
8c5cb753cf creating single file binary and deb package 2025-04-22 01:37:17 +02:00
cca474b4c8 don't upload binaries 2025-04-22 01:36:45 +02:00
bd1564540d clean up 2025-04-22 01:36:16 +02:00
ca14ecea76 moved config.ini into ~/.config/brovski-adress-etiketten 2025-04-22 01:27:53 +02:00
748519f45f files for gnome desktop install via apt deb package 2025-04-22 01:20:16 +02:00
1ee64f5d95 ignore binaries for deb package 2025-04-22 01:19:32 +02:00
32eb0ee6c1 Merge pull request 'rearange_insert-edit_dialog' (#4) from rearange_insert-edit_dialog into main
Reviewed-on: #4
2025-04-20 23:41:28 +02:00
26e1d1f2da fixed enter key not working, after inserting a new record 2025-04-20 23:39:08 +02:00
dc86d8ce01 refactored entry mask to own window, split windows and config from main app file 2025-04-20 23:34:41 +02:00
7ef1fca873 WIP: moved the widgets 2025-04-19 14:23:46 +02:00
e3d8b94bd6 update compile script for removed config.ini 2025-04-19 14:15:38 +02:00
889ec19c7a clean up insert function 2025-04-19 14:14:43 +02:00
d9ae7153a5 updated config.ini path 2025-04-19 14:06:38 +02:00
043cb0a626 removed config file, will be auto created 2025-04-19 14:06:22 +02:00
d9fee6dc57 bugfix: app would start even with empty config params 2025-04-19 14:03:52 +02:00
aae762ba28 removed unneeded csv file 2025-04-19 14:01:49 +02:00
72b4614d4d refactored child windows into Window class 2025-04-19 14:01:33 +02:00
bb31a52934 instead of hiding main window, force settings on top 2025-04-19 13:38:20 +02:00
5e071503d0 refactored error message dialog 2025-04-19 13:31:10 +02:00
2f30baeed2 bugfix can't cancel settings dialog if no path defined 2025-04-19 13:11:38 +02:00
d23408ab47 bugfix can't cancel dir find in config 2025-04-19 13:08:38 +02:00
e3c7daddd3 decreased initial window height 2025-04-19 12:59:17 +02:00
974fce8a26 removes tree selection after deleting a record 2025-04-19 12:58:34 +02:00
9c6669f349 bugfix #1 2025-04-19 12:54:32 +02:00
139b951087 refactoring 2025-04-19 12:53:03 +02:00
05599acd56 refactoring 2025-04-19 12:46:30 +02:00
17 changed files with 578 additions and 273 deletions

4
.gitignore vendored
View File

@@ -166,3 +166,7 @@ cython_debug/
/.idea/modules.xml /.idea/modules.xml
/.idea/inspectionProfiles/profiles_settings.xml /.idea/inspectionProfiles/profiles_settings.xml
/.idea/inspectionProfiles/Project_Default.xml /.idea/inspectionProfiles/Project_Default.xml
/deb-package/brovski-adressetiketten/usr/local/bin/brovski-adress-etiketten-verwaltung
/deb-package/brovski-adressetiketten/usr/local/bin/brovski-adressetiketten
/brovski-adressetiketten.deb

20
compile.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
if [ $VIRTUAL_ENV=="" ]
then
source venv/bin/activate
fi
version=$(cat version.txt)
echo "current version set to: $version"
new_version=""
read -r -p "Enter new version or empty to keep the current: " new_version
if [ "$new_version" != "" ]
then
echo "$new_version" | tee version.txt
fi
sed -i "s/VERSION = '[0-9]\.[0-9]\w'/VERSION = '$(cat version.txt)'/g" src/brovski-adress-etiketten-verwaltung.py
sed -i "s/Version: [0-9]\.[0-9]\w/Version: $(cat version.txt)/g" deb-package/brovski-adressetiketten/DEBIAN/control
pyinstaller --clean --onefile src/brovski-adress-etiketten-verwaltung.py
cp dist/brovski-adress-etiketten-verwaltung deb-package/brovski-adressetiketten/usr/local/bin/brovski-adressetiketten
dpkg-deb --build deb-package/brovski-adressetiketten
mv deb-package/brovski-adressetiketten.deb ./

View File

@@ -0,0 +1,5 @@
Package: brovski-adressetiketten
Version: 0.1.0b
Maintainer: Ovski
Architecture: all
Description: manage csv files for glables address labels

View File

@@ -1,8 +1,8 @@
[Desktop Entry] [Desktop Entry]
Name=Brovski Adress Etiketten Name=Brovski Adress Etiketten
GenericName=Brovski Adress Etiketten GenericName=Brovski Adress Etiketten
Icon=/home/sroth/app/brovski-adress-etiketten-verwaltung/icon.svg Icon=/usr/share/icons/hicolor/512x512/apps/brovski-adressetiketten.svg
Exec=/home/sroth/app/brovski-adress-etiketten-verwaltung/brovski-adress-etiketten-verwaltung Exec=/usr/local/bin/brovski-adressetiketten
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Utility Categories=Utility

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -1,3 +0,0 @@
alpha muster-firma,otti,tottistrasse,1234 daheim
c3000,erich roth,zentralstrasse 120,5430 wettingen
muster-firma,Peter Muster,Peter-Musterstrasse 1,1234 Ort
1 alpha muster-firma otti tottistrasse 1234 daheim
2 c3000 erich roth zentralstrasse 120 5430 wettingen
3 muster-firma Peter Muster Peter-Musterstrasse 1 1234 Ort

View File

@@ -2,131 +2,13 @@ import csv
import os import os
import sys import sys
import tkinter as tk import tkinter as tk
from tkinter import font, filedialog
from configparser import NoSectionError, NoOptionError from configparser import NoSectionError, NoOptionError
from configparser import ConfigParser
from tkinter import messagebox from tkinter import messagebox
from tkinter import ttk from tkinter import ttk
import json
from PyInstaller.utils.hooks import collect_data_files from config import Config
from model import Model
from windows import SettingsWindow, EditRecord, show_error
class SettingsWindow(tk.Toplevel):
def __init__(self, root: tk.Tk):
super().__init__()
self.root = root
self.protocol("WM_DELETE_WINDOW", self.close_window)
self.geometry(f"500x330+{self.root.winfo_x() + 20}+{self.root.winfo_y() + 20}")
self.config = Config()
self.json_file = tk.StringVar()
self.csv_file = tk.StringVar()
title = font.Font(family='Ubuntu Mono', size=20, weight=font.BOLD)
tk.Label(self, text="Einstellungen", font=title).pack(pady=20)
path_frame = tk.Frame(self)
path_frame.pack(pady=(10, 40))
tk.Label(path_frame, text="Datenpfad JSON Datei").grid(row=0, column=0)
tk.Entry(path_frame, textvariable=self.json_file, width=50).grid(row=1, column=0)
tk.Button(path_frame, text="Pfad", command=self.set_json_path).grid(row=1, column=1)
tk.Label(path_frame, text="Datenpfad CSV Export Datei").grid(row=2, column=0)
tk.Entry(path_frame, textvariable=self.csv_file, width=50).grid(row=3, column=0)
tk.Button(path_frame, text="Pfad", command=self.set_csv_path).grid(row=3, column=1)
bottom_frame = tk.Frame(self)
bottom_frame.pack()
tk.Button(bottom_frame, text="Ok", command=self.ok).pack(side=tk.LEFT)
tk.Button(bottom_frame, text="Abbrechen", command=self.cancel).pack(side=tk.LEFT)
self.load_config()
def set_json_path(self):
self.json_file.set(filedialog.askdirectory(initialdir="~/", title="Datenpfad JSON Datei"))
def set_csv_path(self):
self.csv_file.set(filedialog.askdirectory(initialdir="~/", title="Datenpfad CSV Datei"))
def load_config(self):
try:
self.json_file.set(self.config.get("json", "path"))
except NoSectionError:
self.config.add_section("json")
self.config.set("json", "path", "")
except NoOptionError:
self.config.set("json", "path", "")
try:
self.csv_file.set(self.config.get("csv", "path"))
except NoSectionError:
self.config.add_section("csv")
self.config.set("csv", "path", "")
except NoOptionError:
self.config.set("csv", "path", "")
def ok(self):
self.config.set("json", "path", self.json_file.get())
self.config.set("csv", "path", self.csv_file.get())
self.close_window()
def cancel(self):
self.close_window()
def close_window(self):
if self.json_file.get() == "" or self.csv_file.get() == "":
messagebox.showwarning(title="Fehlerhafte Konfiguration", message="Pfad für JSON oder CSV Datei fehlt")
return
self.destroy_window()
def destroy_window(self):
self.root.update()
self.root.deiconify()
self.destroy()
class Config:
parser: ConfigParser
def __init__(self):
"""
Config parser reading config.ini
Attributes:
self.parser: ConfigParser holding list of sections and options
self.__filename: Path and name to the config file
"""
self.parser = ConfigParser()
self.config_file_name = "files/config.ini"
self.root_path = os.path.dirname(os.path.abspath(__file__))
self.config_file = os.path.join(self.root_path, self.config_file_name)
self._load()
def _save(self):
with open(self.config_file, 'w') as outfile:
self.parser.write(outfile)
def _load(self):
self.parser.read(self.config_file)
def add_section(self, section):
self._load()
self.parser.add_section(section)
self._save()
def set(self, section: str, option: str, value: str):
self._load()
self.parser.set(section, option, value)
self._save()
def get(self, section: str, option: str):
self._load()
return self.parser.get(section, option)
class Application: class Application:
@@ -135,8 +17,9 @@ class Application:
x_offset = 700 x_offset = 700
y_offset = 200 y_offset = 200
width = 1050 width = 1050
height = 1000 height = 700
title = "Brovski Adress-Etiketten Verwaltung" VERSION = '0.1.0b'
title = f"Brovski Adress-Etiketten Verwaltung {VERSION}"
self.root = tk.Tk(className="BrovskiAdressEtiketten") self.root = tk.Tk(className="BrovskiAdressEtiketten")
self.root.title(title) self.root.title(title)
@@ -149,60 +32,52 @@ class Application:
self.address_list = [] self.address_list = []
self.current_record: int | None = None self.current_record: int | None = None
self.sort_order = False self.sort_order = False
self.last_sort_field = "#3"
self.filter_active = tk.BooleanVar(value=False)
# model connector
self.model = Model(self.config)
# init paths to json and csv file # init paths to json and csv file
self.json_file_name = "brovski-adress-etiketten-verwaltung.json" self.json_file_name = "brovski-adress-etiketten-verwaltung.json"
self.csv_file_name = "brovski-adress-etiketten.csv" self.csv_file_name = "brovski-adress-etiketten.csv"
self.json_path = ""
self.csv_path = "" self.csv_path = ""
self.load_config() self.load_config()
self.json_file = os.path.join(self.json_path, self.json_file_name)
self.csv_file = os.path.join(self.csv_path, self.csv_file_name) self.csv_file = os.path.join(self.csv_path, self.csv_file_name)
# status bar content
self.statusbar = tk.StringVar()
self.length_address_list = None
self.length_address_list_active = None
# leave application if settings are bad # leave application if settings are bad
if not self.config_good: if not self.config_good:
self.show_error("Fehler Konfiguration", show_error(message_title="Fehler Konfiguration",
"Die Konfiguration ist fehlerhaft, bitte prüfe deine config.ini") message="Die Konfiguration ist fehlerhaft, bitte prüfe deine config.ini",
parent=self.root
)
sys.exit() sys.exit()
# frames
top_frame = tk.Frame(self.root) top_frame = tk.Frame(self.root)
top_frame.pack(side=tk.TOP, fill=tk.X) top_frame.pack(side=tk.TOP, fill=tk.X)
data_frame = tk.Frame(self.root, bg="teal")
data_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
bottom_frame = tk.Frame(self.root)
bottom_frame.pack(side=tk.BOTTOM, fill=tk.X)
# top buttons
button_width = 8 button_width = 8
tk.Button(top_frame, text="Insert", command=self.insert_record, width=button_width).pack(side=tk.LEFT) tk.Button(top_frame, text="Insert", command=self.insert_record, width=button_width).pack(side=tk.LEFT)
tk.Button(top_frame, text="Delete", command=self.delete_record, width=button_width).pack(side=tk.LEFT) tk.Button(top_frame, text="Delete", command=self.delete_record, width=button_width).pack(side=tk.LEFT)
tk.Button(top_frame, text="Export CSV", command=self.export_csv, width=button_width).pack(side=tk.LEFT) tk.Button(top_frame, text="Export CSV", command=self.export_csv, width=button_width).pack(side=tk.LEFT)
tk.Button(top_frame, text="Toggle Aktiv", command=self.toggle_active, width=button_width).pack(side=tk.LEFT)
tk.Checkbutton(top_frame, text="Filter aktiv", variable=self.filter_active, command=self.populate_table).pack(
side=tk.LEFT)
tk.Button(top_frame, text="Quit", command=self.on_close, width=button_width).pack(side=tk.RIGHT) tk.Button(top_frame, text="Quit", command=self.on_close, width=button_width).pack(side=tk.RIGHT)
tk.Button(top_frame, text="Settings", command=self.show_settings, width=button_width).pack(side=tk.RIGHT) tk.Button(top_frame, text="Settings", command=self.show_settings, width=button_width).pack(side=tk.RIGHT)
self.aktiv = tk.StringVar() # table content
self.firma = tk.StringVar()
self.name = tk.StringVar()
self.strasse = tk.StringVar()
self.plz_ort = tk.StringVar()
edit_frame = tk.Frame(self.root)
edit_frame.pack(side=tk.TOP, fill=tk.X)
tk.Label(edit_frame, text="Aktiv").grid(row=0, column=0)
tk.Label(edit_frame, text="Firma").grid(row=0, column=1)
tk.Label(edit_frame, text="Name").grid(row=0, column=2)
tk.Label(edit_frame, text="Strasse").grid(row=0, column=3)
tk.Label(edit_frame, text="Plz/Ort").grid(row=0, column=4)
edit_aktiv = tk.Checkbutton(edit_frame, variable=self.aktiv, onvalue="x", offvalue="")
edit_aktiv.grid(row=1, column=0)
edit_firma = tk.Entry(edit_frame, textvariable=self.firma)
edit_firma.grid(row=1, column=1)
edit_name = tk.Entry(edit_frame, textvariable=self.name)
edit_name.grid(row=1, column=2)
edit_strasse = tk.Entry(edit_frame, textvariable=self.strasse)
edit_strasse.grid(row=1, column=3)
edit_plz_ort = tk.Entry(edit_frame, textvariable=self.plz_ort)
edit_plz_ort.grid(row=1, column=4)
tk.Button(edit_frame, text="Update", command=self.update_record, width=button_width).grid(row=1, column=5)
tk.Button(edit_frame, text="Toggle Aktiv", command=self.toggle_active, width=button_width).grid(row=1, column=6)
data_frame = tk.Frame(self.root, bg="teal")
data_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(data_frame, orient=tk.VERTICAL) scrollbar = ttk.Scrollbar(data_frame, orient=tk.VERTICAL)
self.table = ttk.Treeview(data_frame, yscrollcommand=scrollbar.set, columns=("0", "1", "2", "3", "4"), self.table = ttk.Treeview(data_frame, yscrollcommand=scrollbar.set, columns=("0", "1", "2", "3", "4"),
show="headings") show="headings")
@@ -217,15 +92,23 @@ class Application:
scrollbar.pack(side=tk.LEFT, fill=tk.Y) scrollbar.pack(side=tk.LEFT, fill=tk.Y)
self.table.bind("<ButtonRelease-1>", self.mouse_click) self.table.bind("<ButtonRelease-1>", self.mouse_click)
self.table.bind("<Return>", self.enter_button)
self.table.bind("<Double-1>", self.mouse_click_double)
self.root.bind("<Up>", self.focus_table)
self.root.bind("<Down>", self.focus_table)
self._load_json_file() # bottom status bar
tk.Label(bottom_frame, textvariable=self.statusbar).pack(side=tk.LEFT)
self.first_sort_after_start()
self.root.mainloop() self.root.mainloop()
def load_config(self): def load_config(self):
try: try:
self.json_path = self.config.get("json", "path")
self.csv_path = self.config.get("csv", "path") self.csv_path = self.config.get("csv", "path")
if self.csv_path == "":
raise ValueError("Empty JSON or CSV path")
self.config_good = True self.config_good = True
except NoSectionError: except NoSectionError:
self.show_config_error() self.show_config_error()
@@ -233,157 +116,165 @@ class Application:
except NoOptionError: except NoOptionError:
self.show_config_error() self.show_config_error()
self.show_settings() self.show_settings()
except ValueError:
self.show_config_error()
self.show_settings()
def show_config_error(self): def show_config_error(self):
if self.show_error("Fehlerhafte Konfiguration", show_error(message_title="Fehlerhafte Konfiguration",
"Konnte benötigte Parameter in config.ini nicht finden"): message="Konnte benötigte Parameter in config.ini nicht finden",
print("Fehlerhafte Konfiguration") parent=self.root)
def on_close(self): def on_close(self):
self.root.destroy() self.root.destroy()
def show_settings(self): def show_settings(self):
self.root.withdraw() settings = self.open_window_settings()
settings = SettingsWindow(self.root)
settings.wait_window() settings.wait_window()
def insert_record(self): def insert_record(self):
if self.current_record is not None: values = {
self.clear_entry_fields() "aktiv": "x",
children = self.table.get_children() "firma": "Firma",
values = [ "name": "Name",
"x", "strasse": "Strasse",
"Firma", "plzort": "Plz/Ort"
"Name", }
"Strasse", self.model.create_new(values)
"Plz/Ort", self.populate_table()
] self.deselect_tree()
self.table.insert('', 'end', values=values)
self._save_json_file()
def delete_record(self): def delete_record(self):
if self.current_record is None: if self.current_record is None:
return return
if len(self.table.selection()) > 1:
show_error(message_title="Mehrere Adressen ausgewählt",
message="Es können nur einzelne Adressen gelöscht werden",
parent=self.root)
return
if messagebox.askyesno( if messagebox.askyesno(
"Eintrag löschen?", "Eintrag löschen?",
"Willst du diesen Eintrag wirklich löschen?\nDies kann nicht rückgängig gemacht werden"): "Willst du diesen Eintrag wirklich löschen?\nDies kann nicht rückgängig gemacht werden"):
self.table.delete(self.current_record) self.model.delete_by_id(self.current_record)
self.clear_entry_fields() self.deselect_tree()
self._save_json_file() self.populate_table()
def update_record(self): def update_record(self, record: dict):
if self.current_record is None: if self.current_record is None:
return return
values = { self.model.update_record(record)
0: self.aktiv.get(), self.populate_table()
1: self.firma.get(),
2: self.name.get(),
3: self.strasse.get(),
4: self.plz_ort.get(),
}
for key, value in values.items():
self.table.set(self.current_record, key, value)
self.clear_entry_fields()
self._save_json_file()
def toggle_active(self): def toggle_active(self):
items = self.table.selection() selection = self.table.selection()
if len(items) == 0: if len(selection) == 0:
return return
for id in items: item_list = [int(x) for x in selection]
values = self.table.item(id, "values") for address in self.address_list:
active = values[0] record_id = address.get("record_id")
new_active = "x" if active == "" else "" if record_id in item_list:
self.table.set(id, "0", new_active) address["aktiv"] = "x" if address["aktiv"] == "" else ""
self.model.update_record(address)
self.table.set(record_id, "0", address["aktiv"])
self.clear_entry_fields() self.update_status_bar()
self._save_json_file()
def clear_entry_fields(self): def deselect_tree(self):
entry_var_list = [self.aktiv, self.firma, self.name, self.strasse, self.plz_ort] while len(self.table.selection()) > 0:
for entry in entry_var_list: self.table.selection_remove(self.table.selection()[0])
entry.set("")
self.current_record = None self.current_record = None
def mouse_click(self, event): def mouse_click(self, event):
id_string = self.table.focus()
if id_string is not None and id_string != "":
self.current_record = int(self.table.focus())
else:
self.current_record = None
region = self.table.identify("region", event.x, event.y) region = self.table.identify("region", event.x, event.y)
match region:
case "heading":
self.click_on_header(event)
if region == "heading": def mouse_click_double(self, event):
column = self.table.identify_column(event.x) region = self.table.identify("region", event.x, event.y)
match column: match region:
case "#1": case "cell":
self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order) self.edit_selected_record()
self.populate_table()
case "#2":
self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order)
self.populate_table()
case "#3":
self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order)
self.populate_table()
case "#4":
self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order)
self.populate_table()
case "#5":
self.address_list.sort(key=lambda x: x[int(column[-1]) - 1], reverse=self.sort_order)
self.populate_table()
case _:
print(column)
self.sort_order = not self.sort_order
if region == "cell": def enter_button(self, event):
self.current_record = self.table.focus() self.edit_selected_record()
values = self.table.item(self.current_record, "values")
entry_var_list = [self.aktiv, self.firma, self.name, self.strasse, self.plz_ort]
for i in range(len(values)):
entry_var_list[i].set(values[i])
def _load_json_file(self): def click_on_header(self, event):
try: column = self.table.identify_column(event.x)
with open(self.json_file, "r", encoding="utf-8") as f: if self.last_sort_field == column:
self.address_list = json.load(f) self.sort_order = False if self.sort_order else True
self.address_list.sort(key=lambda x: (x[0], x[1])) else:
except FileNotFoundError: self.sort_order = False
self.show_error( self.last_sort_field = column
message_title="Datei nicht gefunden",
message=f"{self.json_file_name} nicht gefunden, erstelle leere Datei unter {self.json_path}" match column:
) case "#1":
self.address_list = [["", "firma", "name", "adresse", "plz/ort"]] field = "aktiv"
with open(self.json_file, "w", encoding="utf-8") as f: case "#2":
json.dump(self.address_list, f) field = "firma"
self.populate_table() case "#3":
field = "name"
case "#4":
field = "strasse"
case "#5":
field = "plzort"
case _:
field = "name"
self.address_list = self.model.get_all_sorted_by(field, self.sort_order)
self.populate_table(reload=False)
def edit_selected_record(self):
if self.table.focus() is None or self.table.focus() == "":
return
self.current_record = int(self.table.focus())
self.open_window_edit_records(self.current_record)
def _save_json_file(self):
self.export_table_to_address_list()
try:
with open(self.json_file, "w", encoding="utf-8") as f:
json.dump(self.address_list, f, indent=4, sort_keys=True)
except FileNotFoundError:
self.show_error(
message_title="Unexpected Error: File not found?!", message=f"{self.json_file_name} not found"
)
def export_csv(self): def export_csv(self):
try: try:
with open(self.csv_file, "w", encoding="utf-8") as f: with open(self.csv_file, "w", encoding="utf-8") as f:
writer = csv.writer(f, delimiter=",") writer = csv.writer(f, delimiter=",")
for address in self.address_list: for address in self.address_list:
if address[0] != "x": if address["aktiv"] != "x":
continue continue
del address[0] line = []
if address[0] == "": if address["firma"] != "":
del address[0] line.append(address["firma"])
writer.writerow(address) line.append(address["name"])
line.append(address["strasse"])
line.append(address["plzort"])
writer.writerow(line)
except FileNotFoundError: except FileNotFoundError:
self.show_error("Unexpected error", f"Could not write file {self.csv_file}") show_error(message_title="Unexpected error",
message=f"Could not write file {self.csv_file}",
parent=self.root
)
def populate_table(self): def populate_table(self, reload=True):
if reload:
self.address_list = self.model.get_all()
if len(self.address_list) == 0:
return
self.delete_all_table_items() self.delete_all_table_items()
for index, item in enumerate(self.address_list): for address in self.address_list:
self.table.insert('', 'end', iid=index, values=item) # skip inactive records if filter is true
if self.filter_active.get() and address["aktiv"] != "x":
continue
self.table.insert('', 'end', iid=address["record_id"],
values=(address["aktiv"], address["firma"], address["name"], address["strasse"],
address["plzort"])
)
self.update_status_bar()
def export_table_to_address_list(self): def export_table_to_address_list(self):
self.address_list.clear() self.address_list.clear()
@@ -391,14 +282,50 @@ class Application:
self.address_list.append([]) self.address_list.append([])
for value in self.table.item(child)['values']: for value in self.table.item(child)['values']:
self.address_list[-1].append(value) self.address_list[-1].append(value)
self.update_status_bar()
def delete_all_table_items(self): def delete_all_table_items(self):
for item in self.table.get_children(): for item in self.table.get_children():
self.table.delete(item) self.table.delete(item)
@staticmethod def open_window_settings(self):
def show_error(message_title: str, message: str): window = SettingsWindow(self, self.root)
messagebox.showwarning(title=message_title, message=message) window.wm_transient(self.root)
window.wait_visibility()
window.grab_set()
return window
def open_window_edit_records(self, record_id: int):
window = EditRecord(self, self.root, record_id)
window.wm_transient(self.root)
window.wait_visibility()
window.grab_set()
def update_status_bar(self):
self._count_address_records()
self.statusbar.set(f"Adressen: {self.length_address_list} | Aktive Adressen: {self.length_address_list_active}")
def _count_address_records(self):
self.length_address_list = len(self.address_list)
count = 0
for address in self.address_list:
if address["aktiv"] == "x":
count += 1
self.length_address_list_active = count
def first_sort_after_start(self):
self.address_list.sort(key=lambda x: (x["firma"], x["name"]))
self.populate_table()
def focus_table(self, event):
first = self.table.get_children()[0]
last = self.table.get_children()[-1]
goto = last if event.keysym == "Up" else first
if self.table.focus() == "":
self.table.selection_set(goto)
self.table.focus(goto)
self.table.see(goto)
self.table.focus_force()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,2 +0,0 @@
#!/usr/bin/env bash
pyinstaller --clean --add-data=files/config.ini:files brovski-adress-etiketten-verwaltung.py

56
src/config.py Normal file
View File

@@ -0,0 +1,56 @@
import configparser
import os
from configparser import ConfigParser, DuplicateSectionError
class Config:
parser: ConfigParser
def __init__(self, path: str = None, filename: str = "config.ini"):
"""
Config parser reading config.ini
Attributes:
self.parser: ConfigParser holding list of sections and options
self.__filename: Path and name to the config file
"""
self.parser = ConfigParser()
self.path = path
self.filename = filename
if self.path is None:
home_path = os.environ["HOME"]
full_path = os.path.join(home_path, ".config", "brovski-adress-etiketten" )
else:
full_path = self.path
if not os.path.exists(full_path):
os.makedirs(full_path)
self.config_file = os.path.join(full_path, self.filename)
self._load()
def _save(self):
with open(self.config_file, 'w') as outfile:
self.parser.write(outfile)
def _load(self):
self.parser.read(self.config_file)
def add_section(self, section):
self._load()
try:
self.parser.add_section(section)
except DuplicateSectionError:
pass
self._save()
def set(self, section: str, option: str, value: str):
self._load()
self.add_section(section)
self.parser.set(section, option, value)
self._save()
def get(self, section: str, option: str):
self._load()
return self.parser.get(section, option)

84
src/connector.py Normal file
View File

@@ -0,0 +1,84 @@
import json
import os
from abc import ABC, abstractmethod
from config import Config
class Connector(ABC):
def __init__(self, config: Config):
self.config = config
@abstractmethod
def get_all(self) -> list:
pass
@abstractmethod
def get_by_id(self, record_id: int) -> dict:
pass
@abstractmethod
def delete_by_id(self, record_id: int):
pass
@abstractmethod
def update_record(self, updated_record: dict):
pass
@abstractmethod
def create_new(self, values: dict) -> int:
pass
@abstractmethod
def get_all_sorted_by(self, field: str, reverse: bool = False) -> list:
pass
class JSONConnector(Connector):
def __init__(self, config: Config, ):
super().__init__(config)
self.json_path = self.config.get("json", "path")
self.json_file = os.path.join(self.json_path, "brovski-adress-etiketten-verwaltung-v7.json")
def get_all(self) -> list:
with open(self.json_file, "r") as f:
return json.load(f)
def get_all_sorted_by(self, field: str, reverse=False) -> list:
with open(self.json_file, "r") as f:
data = json.load(f)
return sorted(data, key=lambda k: k[field], reverse=reverse)
def get_by_id(self, record_id: int) -> dict:
data = self.get_all()
for record in data:
if record["record_id"] == record_id:
return record
def delete_by_id(self, record_id: int):
data = self.get_all()
for idx, record in enumerate(data):
if record["record_id"] == record_id:
del data[idx]
self._write_to_file(data)
def update_record(self, new_record: dict):
data = self.get_all()
for idx, record in enumerate(data):
if record["record_id"] == new_record["record_id"]:
data[idx] = new_record
self._write_to_file(data)
def create_new(self, record: dict) -> int:
data = self.get_all()
if len(data) == 0:
next_id = 0
else:
next_id = max(data, key=lambda x: x["record_id"])["record_id"] + 1
record["record_id"] = next_id
data.append(record)
self._write_to_file(data)
return next_id
def _write_to_file(self, data):
with open(self.json_file, "w", encoding="UTF-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)

24
src/model.py Normal file
View File

@@ -0,0 +1,24 @@
from connector import Connector, JSONConnector
from config import Config
class Model:
def __init__(self, config: Config):
self.connector = JSONConnector(config)
def get_all(self):
return self.connector.get_all()
def get_all_sorted_by(self, field: str, reverse: bool = False):
return self.connector.get_all_sorted_by(field=field, reverse=reverse)
def get_by_id(self, record_id: int):
return self.connector.get_by_id(record_id)
def delete_by_id(self, record_id: int):
self.connector.delete_by_id(record_id)
def update_record(self, new_record: dict):
self.connector.update_record(new_record)
def create_new(self, record: dict):
self.connector.create_new(record)

159
src/windows.py Normal file
View File

@@ -0,0 +1,159 @@
import tkinter as tk
from configparser import NoSectionError, NoOptionError
from tkinter import font, filedialog, messagebox
from config import Config
from model import Model
def show_error(message_title: str, message: str, parent: tk.Tk | tk.Toplevel):
messagebox.showwarning(
title=message_title,
message=message,
parent=parent
)
class Window(tk.Toplevel):
def __init__(self, parent, root: tk.Tk):
super().__init__(root)
self.parent = parent
self.config = parent.config
self.root = root
self.protocol("WM_DELETE_WINDOW", self.close_window)
self.bind("<Escape>", self.close_window)
def close_window(self, event = None):
self._destroy_window()
def _destroy_window(self):
self.parent.populate_table()
self.root.update()
self.destroy()
class EditRecord(Window):
def __init__(self, parent, root: tk.Tk, record_id: int):
super().__init__(parent, root)
self.bind("<Return>", self._update)
self.model = Model(self.config)
record = self.model.get_by_id(record_id)
button_width = 8
self.title("Adresse bearbeiten")
self.record_id = tk.IntVar(value=record_id)
self.aktiv = tk.StringVar(value=record.get("aktiv"))
self.firma = tk.StringVar(value=record.get("firma"))
self.name = tk.StringVar(value=record.get("name"))
self.strasse = tk.StringVar(value=record.get("strasse"))
self.plz_ort = tk.StringVar(value=record.get("plzort"))
edit_frame = tk.Frame(self)
edit_frame.pack(side=tk.TOP, fill=tk.X, pady=20, padx=20)
tk.Label(edit_frame, text="Aktiv", anchor=tk.W).grid(row=0, column=0)
tk.Label(edit_frame, text="Firma").grid(row=0, column=1)
tk.Label(edit_frame, text="Name").grid(row=0, column=2)
tk.Label(edit_frame, text="Strasse").grid(row=0, column=3)
tk.Label(edit_frame, text="Plz/Ort").grid(row=0, column=4)
edit_aktiv = tk.Checkbutton(edit_frame, variable=self.aktiv, onvalue="x", offvalue="")
edit_aktiv.grid(row=1, column=0)
edit_firma = tk.Entry(edit_frame, textvariable=self.firma)
edit_firma.grid(row=1, column=1)
edit_name = tk.Entry(edit_frame, textvariable=self.name)
edit_name.grid(row=1, column=2)
edit_strasse = tk.Entry(edit_frame, textvariable=self.strasse)
edit_strasse.grid(row=1, column=3)
edit_plz_ort = tk.Entry(edit_frame, textvariable=self.plz_ort)
edit_plz_ort.grid(row=1, column=4)
button_frame = tk.Frame(self)
button_frame.pack(side=tk.TOP, pady=10)
tk.Button(button_frame, text="Save", command=self._update, width=button_width).pack(side=tk.LEFT)
tk.Button(button_frame, text="Abbrechen", command=self.close_window, width=button_width).pack(side=tk.LEFT)
def _update(self, event = None):
new_record = {
"record_id": self.record_id.get(),
"aktiv": self.aktiv.get(),
"firma": self.firma.get(),
"name": self.name.get(),
"strasse": self.strasse.get(),
"plzort": self.plz_ort.get(),
}
self.model.update_record(new_record)
self.close_window()
class SettingsWindow(Window):
def __init__(self, parent, root: tk.Tk):
super().__init__(parent, root)
self.geometry(f"500x330+{self.root.winfo_x() + 20}+{self.root.winfo_y() + 20}")
self.config = Config()
self.json_file = tk.StringVar()
self.csv_file = tk.StringVar()
title = font.Font(family='Ubuntu Mono', size=20, weight=font.BOLD)
tk.Label(self, text="Einstellungen", font=title).pack(pady=20)
path_frame = tk.Frame(self)
path_frame.pack(pady=(10, 40))
tk.Label(path_frame, text="Datenpfad JSON Datei").grid(row=0, column=0)
tk.Entry(path_frame, textvariable=self.json_file, width=50).grid(row=1, column=0)
tk.Button(path_frame, text="Pfad", command=lambda: self.set_json_path(self.json_file.get())).grid(row=1,
column=1)
tk.Label(path_frame, text="Datenpfad CSV Export Datei").grid(row=2, column=0)
tk.Entry(path_frame, textvariable=self.csv_file, width=50).grid(row=3, column=0)
tk.Button(path_frame, text="Pfad", command=lambda: self.set_csv_path(self.csv_file.get())).grid(row=3, column=1)
bottom_frame = tk.Frame(self)
bottom_frame.pack()
tk.Button(bottom_frame, text="Ok", command=self.ok).pack(side=tk.LEFT)
tk.Button(bottom_frame, text="Abbrechen", command=self.cancel).pack(side=tk.LEFT)
self.load_config()
def set_json_path(self, initial_dir: str = ""):
initial_dir = "~/" if initial_dir == "" else initial_dir
new_path = filedialog.askdirectory(initialdir=initial_dir, title="Datenpfad JSON Datei")
new_path = initial_dir if len(new_path) == 0 else new_path
self.json_file.set(new_path)
def set_csv_path(self, initial_dir: str = ""):
initial_dir = "~/" if initial_dir == "" else initial_dir
new_path = filedialog.askdirectory(initialdir=initial_dir, title="Datenpfad CSV Datei")
new_path = initial_dir if len(new_path) == 0 else new_path
self.csv_file.set(new_path)
def load_config(self):
try:
self.json_file.set(self.config.get("json", "path"))
except NoSectionError:
self.config.add_section("json")
self.config.set("json", "path", "")
except NoOptionError:
self.config.set("json", "path", "")
try:
self.csv_file.set(self.config.get("csv", "path"))
except NoSectionError:
self.config.add_section("csv")
self.config.set("csv", "path", "")
except NoOptionError:
self.config.set("csv", "path", "")
def ok(self):
if self.json_file.get() == "" or self.csv_file.get() == "":
show_error(message_title="Fehlerhafte Konfiguration",
message="Pfad für JSON oder CSV Datei fehlt",
parent=self)
return
self.config.set("json", "path", self.json_file.get())
self.config.set("csv", "path", self.csv_file.get())
self.close_window()
def cancel(self):
self.close_window()

30
tests/test_config.py Normal file
View File

@@ -0,0 +1,30 @@
import os
from typing import assert_type
import pytest
from src.config import Config
@pytest.fixture
def config() -> Config:
config = Config(path="testfiles", filename="configtest.ini")
return config
def teardown_config():
print("tearing down config test")
def test_construction(config):
assert_type(config, Config)
def test_file_creation(config):
config._save()
assert os.path.isfile(os.path.join(config.path, config.filename))
def test_add_section(config):
config.add_section("test_section")
assert "test_section" in config.parser.sections()
def test_set_and_get(config):
config.set(section="section", option="option", value="value")
assert config.get(section="section", option="option") == "value"

1
version.txt Normal file
View File

@@ -0,0 +1 @@
0.1.0b