zakladka ustawien
This commit is contained in:
88
app.py
88
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/<int:list_id>/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/<int:list_id>/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 = (
|
||||
|
@@ -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");
|
||||
|
@@ -8,8 +8,6 @@
|
||||
</div>
|
||||
|
||||
<form method="post" id="settings-form">
|
||||
|
||||
<!-- OCR -->
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-header border-0">
|
||||
<strong>🔎 OCR — słowa kluczowe i czułość</strong>
|
||||
@@ -53,7 +51,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KOLORY KATEGORII -->
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-header border-0 d-flex align-items-center justify-content-between">
|
||||
<strong>🎨 Kolory kategorii</strong>
|
||||
@@ -83,7 +80,6 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Wskaźniki pod pickerem -->
|
||||
<div class="color-indicators mt-2">
|
||||
<div class="indicator">
|
||||
<span class="badge text-bg-dark me-2">Efektywny</span>
|
||||
@@ -102,7 +98,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BEZPIECZEŃSTWO -->
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-header border-0">
|
||||
<strong>🔐 Bezpieczeństwo</strong>
|
||||
@@ -124,7 +119,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AKCJE -->
|
||||
<div class="mt-4 d-flex">
|
||||
<div class="btn-group" role="group" aria-label="Akcje ustawień">
|
||||
<button type="submit" class="btn btn-outline-light">💾 Zapisz</button>
|
||||
|
Reference in New Issue
Block a user