zakladka ustawien
This commit is contained in:
219
app.py
219
app.py
@@ -349,6 +349,97 @@ 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)
|
||||
|
||||
|
||||
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)
|
||||
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() == "":
|
||||
if s:
|
||||
db.session.delete(s)
|
||||
else:
|
||||
if not s:
|
||||
s = AppSetting(key=key, value=value)
|
||||
db.session.add(s)
|
||||
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(",")]
|
||||
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"
|
||||
]
|
||||
|
||||
# 1) nowa funkcja: tylko frazy użytkownika (bez domyślnych)
|
||||
def get_user_ocr_keywords_only() -> list[str]:
|
||||
raw = get_setting("ocr_keywords", None)
|
||||
if not raw:
|
||||
return []
|
||||
try:
|
||||
if raw.strip().startswith("["):
|
||||
vals = json.loads(raw)
|
||||
else:
|
||||
vals = [v.strip() for v in raw.split(",")]
|
||||
return [v for v in vals if v]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
_BASE_KEYWORDS_BLOCK = r"""
|
||||
(?:
|
||||
razem\s*do\s*zap[łl][aąo0]ty |
|
||||
do\s*zap[łl][aąo0]ty |
|
||||
suma |
|
||||
kwota |
|
||||
warto[śćs] |
|
||||
płatno[śćs] |
|
||||
total |
|
||||
amount
|
||||
)
|
||||
"""
|
||||
|
||||
def priority_keywords_pattern() -> re.Pattern:
|
||||
user_terms = get_user_ocr_keywords_only()
|
||||
if user_terms:
|
||||
|
||||
escaped = [re.escape(t) for t in user_terms]
|
||||
user_block = " | ".join(escaped)
|
||||
combined = rf"""
|
||||
\b(
|
||||
{_BASE_KEYWORDS_BLOCK}
|
||||
| {user_block}
|
||||
)\b
|
||||
"""
|
||||
else:
|
||||
combined = rf"""\b({_BASE_KEYWORDS_BLOCK})\b"""
|
||||
return re.compile(combined, re.IGNORECASE | re.VERBOSE)
|
||||
|
||||
|
||||
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()
|
||||
return category_color_for(cat) if cat else category_to_color(label)
|
||||
|
||||
def hash_password(password):
|
||||
pepper = app.config["BCRYPT_PEPPER"]
|
||||
@@ -357,6 +448,14 @@ 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)
|
||||
return int(v) if v is not None and str(v).strip() != "" else default
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
|
||||
def check_password(stored_hash, password_input):
|
||||
pepper = app.config["BCRYPT_PEPPER"]
|
||||
@@ -1021,7 +1120,7 @@ def get_total_expenses_grouped_by_category(
|
||||
{
|
||||
"label": cat,
|
||||
"data": [round(data_map[label].get(cat, 0.0), 2) for label in labels],
|
||||
"backgroundColor": category_to_color(cat),
|
||||
"backgroundColor": color_for_category_label(cat),
|
||||
}
|
||||
for cat in cats
|
||||
]
|
||||
@@ -1259,12 +1358,20 @@ def get_page_args(
|
||||
|
||||
|
||||
def preprocess_image_for_tesseract(image):
|
||||
# czułość 1..10 (domyślnie 5)
|
||||
sens = get_int_setting("ocr_sensitivity", 5)
|
||||
# próg progowy – im wyższa czułość, tym niższy próg (więcej czerni)
|
||||
base_thresh = 150
|
||||
delta = int((sens - 5) * 8) # krok 8 na stopień
|
||||
thresh = max(90, min(210, base_thresh - delta))
|
||||
|
||||
image = ImageOps.autocontrast(image)
|
||||
image = image.point(lambda x: 0 if x < 150 else 255)
|
||||
image = image.point(lambda x: 0 if x < thresh else 255)
|
||||
image = image.resize((image.width * 2, image.height * 2), Image.BICUBIC)
|
||||
return image
|
||||
|
||||
|
||||
|
||||
def extract_total_tesseract(image):
|
||||
|
||||
text = pytesseract.image_to_string(image, lang="pol", config="--psm 4")
|
||||
@@ -1273,21 +1380,7 @@ def extract_total_tesseract(image):
|
||||
|
||||
blacklist_keywords = re.compile(r"\b(ptu|vat|podatek|stawka)\b", re.IGNORECASE)
|
||||
|
||||
priority_keywords = re.compile(
|
||||
r"""
|
||||
\b(
|
||||
razem\s*do\s*zap[łl][aąo0]ty |
|
||||
do\s*zap[łl][aąo0]ty |
|
||||
suma |
|
||||
kwota |
|
||||
warto[śćs] |
|
||||
płatno[śćs] |
|
||||
total |
|
||||
amount
|
||||
)\b
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
)
|
||||
priority_keywords = priority_keywords_pattern()
|
||||
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
@@ -1364,7 +1457,14 @@ def is_ip_blocked(ip):
|
||||
attempts = failed_login_attempts[ip]
|
||||
while attempts and now - attempts[0] > TIME_WINDOW:
|
||||
attempts.popleft()
|
||||
return len(attempts) >= MAX_ATTEMPTS
|
||||
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):
|
||||
@@ -1379,9 +1479,6 @@ def reset_failed_attempts(ip):
|
||||
failed_login_attempts[ip].clear()
|
||||
|
||||
|
||||
def attempts_remaining(ip):
|
||||
attempts = failed_login_attempts[ip]
|
||||
return max(0, MAX_ATTEMPTS - len(attempts))
|
||||
|
||||
|
||||
####################################################
|
||||
@@ -1756,7 +1853,8 @@ 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_to_color(c.name)}
|
||||
{"name": c.name, "color": category_color_for(c)}
|
||||
|
||||
for c in l.categories
|
||||
]
|
||||
else:
|
||||
@@ -2159,7 +2257,8 @@ def view_list(list_id):
|
||||
item.added_by_display = None
|
||||
|
||||
shopping_list.category_badges = [
|
||||
{"name": c.name, "color": category_to_color(c.name)}
|
||||
{"name": c.name, "color": category_color_for(c)}
|
||||
|
||||
for c in shopping_list.categories
|
||||
]
|
||||
|
||||
@@ -2550,7 +2649,8 @@ def shared_list(token=None, list_id=None):
|
||||
shopping_list, items, receipts, expenses, total_expense = get_list_details(list_id)
|
||||
|
||||
shopping_list.category_badges = [
|
||||
{"name": c.name, "color": category_to_color(c.name)}
|
||||
{"name": c.name, "color": category_color_for(c)}
|
||||
|
||||
for c in shopping_list.categories
|
||||
]
|
||||
|
||||
@@ -4010,6 +4110,77 @@ def healthcheck():
|
||||
return "OK", 200
|
||||
|
||||
|
||||
@app.route("/admin/settings", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_settings():
|
||||
categories = Category.query.order_by(Category.name.asc()).all()
|
||||
|
||||
if request.method == "POST":
|
||||
# OCR
|
||||
ocr_raw = (request.form.get("ocr_keywords") or "").strip()
|
||||
set_setting("ocr_keywords", ocr_raw)
|
||||
|
||||
# OCR sensitivity
|
||||
ocr_sens = (request.form.get("ocr_sensitivity") or "").strip()
|
||||
set_setting("ocr_sensitivity", ocr_sens)
|
||||
|
||||
# Bezpieczeństwo – limit błędnych prób (system_auth)
|
||||
max_attempts = (request.form.get("max_login_attempts") or "").strip()
|
||||
set_setting("max_login_attempts", max_attempts)
|
||||
|
||||
# (opcjonalnie) okno blokady
|
||||
login_window = (request.form.get("login_window_seconds") or "").strip()
|
||||
if login_window: # pole możesz ukryć w UI – zostawiam jako opcję
|
||||
set_setting("login_window_seconds", login_window)
|
||||
|
||||
# Kolory kategorii
|
||||
for c in categories:
|
||||
field = f"color_{c.id}"
|
||||
val = (request.form.get(field) or "").strip()
|
||||
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))
|
||||
else:
|
||||
existing.color_hex = val
|
||||
else:
|
||||
if existing:
|
||||
db.session.delete(existing)
|
||||
|
||||
db.session.commit()
|
||||
flash("Zapisano ustawienia.", "success")
|
||||
return redirect(url_for("admin_settings"))
|
||||
|
||||
# --- GET ---
|
||||
override_rows = CategoryColorOverride.query.filter(
|
||||
CategoryColorOverride.category_id.in_([c.id for c in categories])
|
||||
).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}
|
||||
|
||||
current_ocr = get_setting("ocr_keywords", "")
|
||||
|
||||
# nowe pola do szablonu
|
||||
ocr_sensitivity = get_int_setting("ocr_sensitivity", 5)
|
||||
max_login_attempts = get_int_setting("max_login_attempts", 10)
|
||||
login_window_seconds = get_int_setting("login_window_seconds", 3600)
|
||||
|
||||
return render_template(
|
||||
"admin/settings.html",
|
||||
categories=categories,
|
||||
overrides=overrides,
|
||||
auto_colors=auto_colors,
|
||||
effective_colors=effective_colors,
|
||||
current_ocr=current_ocr,
|
||||
ocr_sensitivity=ocr_sensitivity,
|
||||
max_login_attempts=max_login_attempts,
|
||||
login_window_seconds=login_window_seconds,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@app.route("/robots.txt")
|
||||
def robots_txt():
|
||||
content = (
|
||||
|
Reference in New Issue
Block a user