From 43b7b93ffaeefb5e475e9fcf179ffbb4da63da34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 21 Oct 2025 11:32:04 +0200 Subject: [PATCH] zakladka ustawien --- app.py | 88 +++++++++++++++++++++-------------- static/js/admin_settings.js | 4 -- templates/admin/settings.html | 6 --- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/app.py b/app.py index 70fa4e5..537778c 100644 --- a/app.py +++ b/app.py @@ -349,6 +349,7 @@ ShoppingList.permitted_users = db.relationship( lazy="dynamic", ) + class AppSetting(db.Model): key = db.Column(db.String(64), primary_key=True) value = db.Column(db.Text, nullable=True) @@ -356,13 +357,17 @@ class AppSetting(db.Model): class CategoryColorOverride(db.Model): id = db.Column(db.Integer, primary_key=True) - category_id = db.Column(db.Integer, db.ForeignKey("category.id"), unique=True, nullable=False) + category_id = db.Column( + db.Integer, db.ForeignKey("category.id"), unique=True, nullable=False + ) color_hex = db.Column(db.String(7), nullable=False) # "#rrggbb" + def get_setting(key: str, default: str | None = None) -> str | None: s = db.session.get(AppSetting, key) return s.value if s else default + def set_setting(key: str, value: str | None): s = db.session.get(AppSetting, key) if (value or "").strip() == "": @@ -375,20 +380,32 @@ def set_setting(key: str, value: str | None): else: s.value = value + def get_ocr_keywords() -> list[str]: raw = get_setting("ocr_keywords", None) if raw: try: - vals = json.loads(raw) if raw.strip().startswith("[") else [v.strip() for v in raw.split(",")] + vals = ( + json.loads(raw) + if raw.strip().startswith("[") + else [v.strip() for v in raw.split(",")] + ) return [v for v in vals if v] except Exception: pass # domyślne – obecne w kodzie OCR return [ - "razem do zapłaty", "do zapłaty", "suma", "kwota", "wartość", "płatność", - "total", "amount" + "razem do zapłaty", + "do zapłaty", + "suma", + "kwota", + "wartość", + "płatność", + "total", + "amount", ] + # 1) nowa funkcja: tylko frazy użytkownika (bez domyślnych) def get_user_ocr_keywords_only() -> list[str]: raw = get_setting("ocr_keywords", None) @@ -403,6 +420,7 @@ def get_user_ocr_keywords_only() -> list[str]: except Exception: return [] + _BASE_KEYWORDS_BLOCK = r""" (?: razem\s*do\s*zap[łl][aąo0]ty | @@ -416,6 +434,7 @@ _BASE_KEYWORDS_BLOCK = r""" ) """ + def priority_keywords_pattern() -> re.Pattern: user_terms = get_user_ocr_keywords_only() if user_terms: @@ -437,10 +456,12 @@ def category_color_for(c: Category) -> str: ov = CategoryColorOverride.query.filter_by(category_id=c.id).first() return ov.color_hex if ov else category_to_color(c.name) + def color_for_category_label(label: str) -> str: - cat = Category.query.filter(func.lower(Category.name)==label.lower()).first() + cat = Category.query.filter(func.lower(Category.name) == label.lower()).first() return category_color_for(cat) if cat else category_to_color(label) + def hash_password(password): pepper = app.config["BCRYPT_PEPPER"] peppered = (password + pepper).encode("utf-8") @@ -448,6 +469,7 @@ def hash_password(password): hashed = bcrypt.hashpw(peppered, salt) return hashed.decode("utf-8") + def get_int_setting(key: str, default: int) -> int: try: v = get_setting(key, None) @@ -456,7 +478,6 @@ def get_int_setting(key: str, default: int) -> int: return default - def check_password(stored_hash, password_input): pepper = app.config["BCRYPT_PEPPER"] peppered = (password_input + pepper).encode("utf-8") @@ -983,8 +1004,6 @@ def get_admin_expense_summary(): } - - def category_to_color(name: str, min_hue_gap_deg: int = 18) -> str: # Stabilny hash -> int hv = int(hashlib.md5(name.encode("utf-8")).hexdigest(), 16) @@ -1000,7 +1019,7 @@ def category_to_color(name: str, min_hue_gap_deg: int = 18) -> str: hue_deg = mix % 360 # Odpychanie lokalne po hue, by podobne nazwy nie lądowały zbyt blisko - gap = (rotl(mix, 17) % (2*min_hue_gap_deg)) - min_hue_gap_deg # [-gap, +gap] + gap = (rotl(mix, 17) % (2 * min_hue_gap_deg)) - min_hue_gap_deg # [-gap, +gap] hue_deg = (hue_deg + gap) % 360 # DARK profil: niższa jasność i nieco mniejsza saturacja @@ -1008,8 +1027,8 @@ def category_to_color(name: str, min_hue_gap_deg: int = 18) -> str: l = 0.45 # Wąska wariacja, żeby uniknąć „neonów” i zachować spójność - s_var = ((rotl(mix, 29) % 5) - 2) / 100.0 # ±0.02 - l_var = ((rotl(mix, 53) % 7) - 3) / 100.0 # ±0.03 + s_var = ((rotl(mix, 29) % 5) - 2) / 100.0 # ±0.02 + l_var = ((rotl(mix, 53) % 7) - 3) / 100.0 # ±0.03 s = min(0.76, max(0.62, s + s_var)) l = min(0.50, max(0.40, l + l_var)) @@ -1019,6 +1038,7 @@ def category_to_color(name: str, min_hue_gap_deg: int = 18) -> str: return f"#{int(round(r*255)):02x}{int(round(g*255)):02x}{int(round(b*255)):02x}" + def get_total_expenses_grouped_by_category( show_all, range_type, start_date, end_date, user_id, category_id=None ): @@ -1371,7 +1391,6 @@ def preprocess_image_for_tesseract(image): return image - def extract_total_tesseract(image): text = pytesseract.image_to_string(image, lang="pol", config="--psm 4") @@ -1460,13 +1479,13 @@ def is_ip_blocked(ip): max_attempts = get_int_setting("max_login_attempts", 10) return len(attempts) >= max_attempts + def attempts_remaining(ip): attempts = failed_login_attempts[ip] max_attempts = get_int_setting("max_login_attempts", 10) return max(0, max_attempts - len(attempts)) - def register_failed_attempt(ip): now = time.time() attempts = failed_login_attempts[ip] @@ -1479,8 +1498,6 @@ def reset_failed_attempts(ip): failed_login_attempts[ip].clear() - - #################################################### @@ -1853,9 +1870,7 @@ def main_page(): l.not_purchased_count = not_purchased_count l.total_expense = latest_expenses_map.get(l.id, 0) l.category_badges = [ - {"name": c.name, "color": category_color_for(c)} - - for c in l.categories + {"name": c.name, "color": category_color_for(c)} for c in l.categories ] else: for l in all_lists: @@ -2089,7 +2104,6 @@ def edit_my_list(list_id): ) - @app.route("/edit_my_list//suggestions", methods=["GET"]) @login_required def edit_my_list_suggestions(list_id: int): @@ -2115,10 +2129,9 @@ def edit_my_list_suggestions(list_id: int): .subquery() ) - query = ( - db.session.query(User.username, subq.c.grant_count, subq.c.last_grant_id) - .join(subq, subq.c.uid == User.id) - ) + query = db.session.query( + User.username, subq.c.grant_count, subq.c.last_grant_id + ).join(subq, subq.c.uid == User.id) if q: query = query.filter(func.lower(User.username).like(f"{q}%")) @@ -2240,9 +2253,14 @@ def view_list(list_id): is_owner = current_user.id == shopping_list.owner_id if not is_owner: - flash("Nie jesteś właścicielem listy, przekierowano do widoku publicznego.", "warning") + flash( + "Nie jesteś właścicielem listy, przekierowano do widoku publicznego.", + "warning", + ) if current_user.is_admin: - flash("W celu modyfikacji listy, przejdź do panelu administracyjnego.", "info") + flash( + "W celu modyfikacji listy, przejdź do panelu administracyjnego.", "info" + ) return redirect(url_for("shared_list", token=shopping_list.share_token)) shopping_list, items, receipts, expenses, total_expense = get_list_details(list_id) @@ -2252,13 +2270,14 @@ def view_list(list_id): for item in items: if item.added_by != shopping_list.owner_id: - item.added_by_display = (item.added_by_user.username if item.added_by_user else "?") + item.added_by_display = ( + item.added_by_user.username if item.added_by_user else "?" + ) else: item.added_by_display = None shopping_list.category_badges = [ {"name": c.name, "color": category_color_for(c)} - for c in shopping_list.categories ] @@ -2650,7 +2669,6 @@ def shared_list(token=None, list_id=None): shopping_list.category_badges = [ {"name": c.name, "color": category_color_for(c)} - for c in shopping_list.categories ] @@ -3888,9 +3906,7 @@ def admin_edit_categories(): l.categories.extend(cats) db.session.commit() flash("Zaktualizowano kategorie dla wybranych list", "success") - return redirect( - url_for("admin_edit_categories", page=page, per_page=per_page) - ) + return redirect(url_for("admin_edit_categories", page=page, per_page=per_page)) query_string = urlencode({k: v for k, v in request.args.items() if k != "page"}) @@ -3905,6 +3921,7 @@ def admin_edit_categories(): query_string=query_string, ) + @app.route("/admin/edit_categories//save", methods=["POST"]) @login_required @admin_required @@ -4141,7 +4158,9 @@ def admin_settings(): existing = CategoryColorOverride.query.filter_by(category_id=c.id).first() if val and re.fullmatch(r"^#[0-9A-Fa-f]{6}$", val): if not existing: - db.session.add(CategoryColorOverride(category_id=c.id, color_hex=val)) + db.session.add( + CategoryColorOverride(category_id=c.id, color_hex=val) + ) else: existing.color_hex = val else: @@ -4158,7 +4177,9 @@ def admin_settings(): ).all() overrides = {row.category_id: row.color_hex for row in override_rows} auto_colors = {c.id: category_to_color(c.name) for c in categories} - effective_colors = {c.id: (overrides.get(c.id) or auto_colors[c.id]) for c in categories} + effective_colors = { + c.id: (overrides.get(c.id) or auto_colors[c.id]) for c in categories + } current_ocr = get_setting("ocr_keywords", "") @@ -4180,7 +4201,6 @@ def admin_settings(): ) - @app.route("/robots.txt") def robots_txt(): content = ( diff --git a/static/js/admin_settings.js b/static/js/admin_settings.js index 2704bf0..e39b93c 100644 --- a/static/js/admin_settings.js +++ b/static/js/admin_settings.js @@ -17,7 +17,6 @@ if (hidden) hidden.remove(); } - // Podgląd: bary pod pickerem (Efektywny = override || auto) function updatePreview(input) { const card = input.closest(".col-12, .col-md-6, .col-lg-4"); const hexAutoEl = card.querySelector(".hex-auto"); @@ -35,7 +34,6 @@ if (!raw) ensureHiddenClear(input); else removeHiddenClear(input); } - // Reset jednego / wszystkich form.querySelectorAll(".reset-one").forEach(btn => { btn.addEventListener("click", () => { const name = btn.getAttribute("data-target"); @@ -52,14 +50,12 @@ }); }); - // Init + live dla pickerów form.querySelectorAll('input[type="color"].category-color').forEach(input => { updatePreview(input); input.addEventListener("input", () => updatePreview(input)); input.addEventListener("change", () => updatePreview(input)); }); - // Live etykiety dla czułości OCR (function () { const slider = document.getElementById("ocr_sensitivity"); const badge = document.getElementById("ocr_sens_badge"); diff --git a/templates/admin/settings.html b/templates/admin/settings.html index de34efc..cdcf9d0 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -8,8 +8,6 @@
- -
🔎 OCR — słowa kluczowe i czułość @@ -53,7 +51,6 @@
-
🎨 Kolory kategorii @@ -83,7 +80,6 @@
-
Efektywny @@ -102,7 +98,6 @@
-
🔐 Bezpieczeństwo @@ -124,7 +119,6 @@
-