drobne poprawki

This commit is contained in:
Mateusz Gruszczyński
2025-08-02 18:57:29 +02:00
parent 9142dc1413
commit 1cd4f62004
3 changed files with 34 additions and 115 deletions

135
app.py
View File

@@ -109,7 +109,6 @@ SESSION_COOKIE_SECURE = app.config.get("SESSION_COOKIE_SECURE")
app.config["COMPRESS_ALGORITHM"] = ["zstd", "br", "gzip", "deflate"]
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=SESSION_TIMEOUT_MINUTES)
# app.config["SESSION_COOKIE_SECURE"] = True if app.config.get("SESSION_COOKIE_SECURE") is True else False
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
DEBUG_MODE = app.config.get("DEBUG_MODE", False)
@@ -135,7 +134,7 @@ login_manager.login_view = "login"
# flask-session
app.config["SESSION_TYPE"] = "sqlalchemy"
app.config["SESSION_SQLALCHEMY"] = db # instancja SQLAlchemy
app.config["SESSION_SQLALCHEMY"] = db
Session(app)
@@ -162,7 +161,6 @@ class User(UserMixin, db.Model):
password_hash = db.Column(db.String(512), nullable=False)
is_admin = db.Column(db.Boolean, default=False)
# Tabela pośrednia
shopping_list_category = db.Table(
"shopping_list_category",
@@ -280,19 +278,14 @@ def check_password(stored_hash, password_input):
def set_authorized_cookie(response):
secure_flag = app.config["SESSION_COOKIE_SECURE"] # wartość z config.py
secure_flag = app.config["SESSION_COOKIE_SECURE"]
max_age = app.config.get("AUTH_COOKIE_MAX_AGE", 86400)
print("ENV SESSION_COOKIE_SECURE =", os.environ.get("SESSION_COOKIE_SECURE"))
print("CONFIG SESSION_COOKIE_SECURE =", app.config["SESSION_COOKIE_SECURE"])
response.set_cookie(
"authorized",
AUTHORIZED_COOKIE_VALUE,
max_age=max_age,
secure=secure_flag,
httponly=True,
samesite="Lax",
path="/",
httponly=True
)
return response
@@ -327,14 +320,11 @@ with app.app_context():
)
db.session.commit()
# --- Predefiniowane kategorie ---
default_categories = app.config["DEFAULT_CATEGORIES"]
existing_names = {
c.name for c in Category.query.filter(Category.name.isnot(None)).all()
}
# ignorujemy wielkość liter przy porównaniu
existing_names_lower = {name.lower() for name in existing_names}
missing = [
@@ -352,12 +342,8 @@ with app.app_context():
@static_bp.route("/static/js/<path:filename>")
def serve_js(filename):
response = send_from_directory("static/js", filename)
# response.cache_control.no_cache = True
# response.cache_control.no_store = True
# response.cache_control.must_revalidate = True
response.headers["Cache-Control"] = app.config["JS_CACHE_CONTROL"]
response.headers.pop("Content-Disposition", None)
# response.headers.pop("Etag", None)
return response
@@ -366,7 +352,6 @@ def serve_css(filename):
response = send_from_directory("static/css", filename)
response.headers["Cache-Control"] = app.config["CSS_CACHE_CONTROL"]
response.headers.pop("Content-Disposition", None)
# response.headers.pop("Etag", None)
return response
@@ -375,7 +360,6 @@ def serve_js_lib(filename):
response = send_from_directory("static/lib/js", filename)
response.headers["Cache-Control"] = app.config["LIB_JS_CACHE_CONTROL"]
response.headers.pop("Content-Disposition", None)
# response.headers.pop("Etag", None)
return response
@@ -384,10 +368,8 @@ def serve_css_lib(filename):
response = send_from_directory("static/lib/css", filename)
response.headers["Cache-Control"] = app.config["LIB_CSS_CACHE_CONTROL"]
response.headers.pop("Content-Disposition", None)
# response.headers.pop("Etag", None)
return response
app.register_blueprint(static_bp)
@@ -481,10 +463,9 @@ def save_resized_image(file, path):
raise ValueError("Nieprawidłowy plik graficzny")
try:
# Automatyczna rotacja według EXIF (np. zdjęcia z telefonu)
image = ImageOps.exif_transpose(image)
except Exception:
pass # ignorujemy, jeśli EXIF jest uszkodzony lub brak
pass
try:
image.thumbnail((2000, 2000))
@@ -542,7 +523,7 @@ def delete_receipts_for_list(list_id):
print(f"Nie udało się usunąć pliku {filename}: {e}")
def _receipt_error(message):
def receipt_error(message):
if request.is_json or request.headers.get("X-Requested-With") == "XMLHttpRequest":
return jsonify({"success": False, "error": message}), 400
flash(message, "danger")
@@ -637,7 +618,7 @@ def get_total_expenses_grouped_by_list_created_at(
lists_query = lists_query.filter(ShoppingList.owner_id == user_id)
if category_id:
if str(category_id) == "none": # Bez kategorii
if str(category_id) == "none":
lists_query = lists_query.filter(~ShoppingList.categories.any())
else:
try:
@@ -649,7 +630,6 @@ def get_total_expenses_grouped_by_list_created_at(
shopping_list_category.c.shopping_list_id == ShoppingList.id,
).filter(shopping_list_category.c.category_id == cat_id_int)
# Obsługa nowych zakresów
today = datetime.now(timezone.utc).date()
if range_type == "last30days":
@@ -770,15 +750,12 @@ def get_admin_expense_summary():
)
return {"total": total, "year": year_total, "month": month_total}
# baza wspólna
base = db.session.query(func.sum(Expense.amount)).join(
ShoppingList, ShoppingList.id == Expense.list_id
)
# wszystkie listy
all_lists = calc_sum(base)
# aktywne listy
active_lists = calc_sum(
base.filter(
ShoppingList.is_archived == False,
@@ -790,10 +767,8 @@ def get_admin_expense_summary():
)
)
# archiwalne
archived_lists = calc_sum(base.filter(ShoppingList.is_archived == True))
# wygasłe
expired_lists = calc_sum(
base.filter(
ShoppingList.is_archived == False,
@@ -811,19 +786,6 @@ def get_admin_expense_summary():
}
""" def category_to_color(name):
hash_val = int(hashlib.md5(name.encode("utf-8")).hexdigest(), 16)
r = (hash_val & 0xFF0000) >> 16
g = (hash_val & 0x00FF00) >> 8
b = hash_val & 0x0000FF
# Rozjaśnienie (pastel)
r = (r + 255) // 2
g = (g + 255) // 2
b = (b + 255) // 2
return f"#{r:02x}{g:02x}{b:02x}"
"""
def category_to_color(name):
hash_val = int(hashlib.md5(name.encode("utf-8")).hexdigest(), 16)
hue = (hash_val % 360) / 360.0
@@ -847,7 +809,7 @@ def get_total_expenses_grouped_by_category(
lists_query = lists_query.filter(ShoppingList.owner_id == user_id)
if category_id:
if str(category_id) == "none": # Bez kategorii
if str(category_id) == "none":
lists_query = lists_query.filter(~ShoppingList.categories.any())
else:
try:
@@ -899,13 +861,11 @@ def get_total_expenses_grouped_by_category(
all_labels.add(key)
# Specjalna obsługa dla filtra "Bez kategorii"
if str(category_id) == "none":
if not l.categories:
data_map[key]["Bez kategorii"] += total_expense
continue # 🔹 Pomijamy dalsze dodawanie innych kategorii
continue
# Standardowa logika
if not l.categories:
data_map[key]["Bez kategorii"] += total_expense
else:
@@ -943,10 +903,10 @@ def get_total_expenses_grouped_by_category(
def preprocess_image_for_tesseract(image):
image = ImageOps.autocontrast(image)
image = image.point(lambda x: 0 if x < 150 else 255) # mocniejsza binarizacja
image = image.point(lambda x: 0 if x < 150 else 255)
image = image.resize(
(image.width * 2, image.height * 2), Image.BICUBIC
) # większe powiększenie
)
return image
@@ -992,7 +952,6 @@ def extract_total_tesseract(image):
except:
continue
# Tylko w liniach priorytetowych: sprawdzamy spaced fallback
if is_priority:
spaced = re.findall(r"\d{1,4}\s\d{2}", line)
for match in spaced:
@@ -1003,7 +962,6 @@ def extract_total_tesseract(image):
except:
continue
# Preferujemy linie priorytetowe
preferred = [(val, line) for val, line, is_pref in candidates if is_pref]
if preferred:
@@ -1016,7 +974,6 @@ def extract_total_tesseract(image):
if best_val < 99999:
return round(best_val, 2), lines
# Fallback: największy font + bold
data = pytesseract.image_to_data(
image, lang="pol", config="--psm 4", output_type=Output.DICT
)
@@ -1037,7 +994,6 @@ def extract_total_tesseract(image):
continue
if font_candidates:
# Preferuj najwyższy font z sensownym confidence
best = max(font_candidates, key=lambda x: (x[1], x[2]))
return round(best[0], 2), lines
@@ -1077,10 +1033,8 @@ def attempts_remaining(ip):
def get_client_ip():
# Obsługuje: X-Forwarded-For, X-Real-IP, fallback na remote_addr
for header in ["X-Forwarded-For", "X-Real-IP"]:
if header in request.headers:
# Pierwszy IP w X-Forwarded-For jest najczęściej klientem
ip = request.headers[header].split(",")[0].strip()
if ip:
return ip
@@ -1089,7 +1043,6 @@ def get_client_ip():
@login_manager.user_loader
def load_user(user_id):
# return User.query.get(int(user_id))
return db.session.get(User, int(user_id))
@@ -1112,8 +1065,6 @@ def inject_is_blocked():
@app.before_request
def require_system_password():
endpoint = request.endpoint
# Wyjątki: lib js/css zawsze przepuszczamy
if endpoint in ("static_bp.serve_js_lib", "static_bp.serve_css_lib"):
return
@@ -1133,7 +1084,6 @@ def require_system_password():
and endpoint != "favicon"
):
# Dla serve_js przepuszczamy tylko toasts.js
if endpoint == "static_bp.serve_js":
requested_file = request.view_args.get("filename", "")
if requested_file == "toasts.js":
@@ -1142,7 +1092,6 @@ def require_system_password():
return redirect(url_for("system_auth", next=request.url))
return
# Blokujemy pozostałe static_bp
if endpoint.startswith("static_bp."):
return
@@ -1274,7 +1223,6 @@ def main_page():
)
return query
# Pobranie list
if current_user.is_authenticated:
user_lists = (
date_filter(
@@ -1327,7 +1275,6 @@ def main_page():
.all()
)
# Dodajemy statystyki
all_lists = user_lists + public_lists + archived_lists
all_ids = [l.id for l in all_lists]
@@ -1425,8 +1372,6 @@ def system_auth():
@app.route("/toggle_archive_list/<int:list_id>")
@login_required
def toggle_archive_list(list_id):
# l = ShoppingList.query.get_or_404(list_id)
l = db.session.get(ShoppingList, list_id)
if l is None:
abort(404)
@@ -1466,8 +1411,9 @@ def edit_my_list(list_id):
categories = Category.query.order_by(Category.name.asc()).all()
selected_categories_ids = {c.id for c in l.categories}
next_page = request.args.get("next") or request.referrer
if request.method == "POST":
# Obsługa zmiany miesiąca utworzenia listy
move_to_month = request.form.get("move_to_month")
if move_to_month:
try:
@@ -1479,12 +1425,11 @@ def edit_my_list(list_id):
f"Zmieniono datę utworzenia listy na {new_created_at.strftime('%Y-%m-%d')}",
"success",
)
return redirect(url_for("edit_my_list", list_id=list_id))
return redirect(next_page or url_for("main_page"))
except ValueError:
flash("Nieprawidłowy format miesiąca", "danger")
return redirect(url_for("edit_my_list", list_id=list_id))
return redirect(next_page or url_for("main_page"))
# Pozostała aktualizacja pól
new_title = request.form.get("title", "").strip()
is_public = "is_public" in request.form
is_temporary = "is_temporary" in request.form
@@ -1494,7 +1439,7 @@ def edit_my_list(list_id):
if not new_title:
flash("Podaj poprawny tytuł", "danger")
return redirect(url_for("edit_my_list", list_id=list_id))
return redirect(next_page or url_for("main_page"))
l.title = new_title
l.is_public = is_public
@@ -1508,16 +1453,14 @@ def edit_my_list(list_id):
l.expires_at = expires_dt.replace(tzinfo=timezone.utc)
except ValueError:
flash("Błędna data lub godzina wygasania", "danger")
return redirect(url_for("edit_my_list", list_id=list_id))
return redirect(next_page or url_for("main_page"))
else:
l.expires_at = None
# Obsługa wyboru kategorii
update_list_categories_from_form(l, request.form)
db.session.commit()
flash("Zaktualizowano dane listy", "success")
return redirect(url_for("main_page"))
return redirect(next_page or url_for("main_page"))
return render_template(
"edit_my_list.html",
@@ -1551,8 +1494,6 @@ def delete_user_list(list_id):
@app.route("/toggle_visibility/<int:list_id>", methods=["GET", "POST"])
@login_required
def toggle_visibility(list_id):
# l = ShoppingList.query.get_or_404(list_id)
l = db.session.get(ShoppingList, list_id)
if l is None:
abort(404)
@@ -1609,7 +1550,6 @@ def create_list():
is_temporary = request.form.get("temporary") == "1"
token = generate_share_token(8)
# expires_at = datetime.utcnow() + timedelta(days=7) if is_temporary else None
expires_at = (
datetime.now(timezone.utc) + timedelta(days=7) if is_temporary else None
)
@@ -1751,7 +1691,6 @@ def expenses():
)
totals_map = {t.list_id: t.total_expense or 0 for t in totals}
# Tabela wydatków
expense_table = [
{
"title": e.shopping_list.title if e.shopping_list else "Nieznana",
@@ -1761,7 +1700,6 @@ def expenses():
for e in expenses
]
# Lista z danymi i kategoriami (dla JS)
lists_data = [
{
"id": l.id,
@@ -1943,15 +1881,12 @@ def upload_receipt(list_id):
l = db.session.get(ShoppingList, list_id)
# if l is None or l.owner_id != current_user.id:
# return _receipt_error("Nie masz uprawnień do tej listy.")
if "receipt" not in request.files:
return _receipt_error("Brak pliku")
return receipt_error("Brak pliku")
file = request.files["receipt"]
if file.filename == "":
return _receipt_error("Nie wybrano pliku")
return receipt_error("Nie wybrano pliku")
if file and allowed_file(file.filename):
file_bytes = file.read()
@@ -1960,7 +1895,7 @@ def upload_receipt(list_id):
existing = Receipt.query.filter_by(file_hash=file_hash).first()
if existing:
return _receipt_error("Taki plik już istnieje")
return receipt_error("Taki plik już istnieje")
now = datetime.now(timezone.utc)
timestamp = now.strftime("%Y%m%d_%H%M")
@@ -1971,7 +1906,7 @@ def upload_receipt(list_id):
try:
save_resized_image(file, file_path)
except ValueError as e:
return _receipt_error(str(e))
return receipt_error(str(e))
filesize = os.path.getsize(file_path)
uploaded_at = datetime.now(timezone.utc)
@@ -1997,7 +1932,7 @@ def upload_receipt(list_id):
flash("Wgrano paragon", "success")
return redirect(request.referrer or url_for("main_page"))
return _receipt_error("Niedozwolony format pliku")
return receipt_error("Niedozwolony format pliku")
@app.route("/uploads/<filename>")
@@ -2217,7 +2152,6 @@ def admin_panel():
}
)
# Top produkty
top_products = (
db.session.query(Item.name, func.count(Item.id).label("count"))
.filter(Item.purchased.is_(True))
@@ -2228,13 +2162,9 @@ def admin_panel():
)
purchased_items_count = Item.query.filter_by(purchased=True).count()
# Nowe podsumowanie wydatków
expense_summary = get_admin_expense_summary()
# Statystyki systemowe
process = psutil.Process(os.getpid())
app_mem = process.memory_info().rss // (1024 * 1024) # MB
app_mem = process.memory_info().rss // (1024 * 1024)
db_engine = db.engine
db_info = {
@@ -2369,8 +2299,6 @@ def admin_receipts(id):
try:
if id == "all":
receipts = Receipt.query.order_by(Receipt.uploaded_at.desc()).all()
# Szukaj sierot tylko dla "all"
upload_folder = app.config["UPLOAD_FOLDER"]
all_db_filenames = set(r.filename for r in receipts)
files_on_disk = set(os.listdir(upload_folder))
@@ -2388,7 +2316,7 @@ def admin_receipts(id):
.order_by(Receipt.uploaded_at.desc())
.all()
)
stale_files = [] # brak sierot
stale_files = []
except ValueError:
flash("Nieprawidłowe ID listy.", "danger")
return redirect(url_for("admin_panel"))
@@ -2438,7 +2366,6 @@ def delete_receipt(receipt_id=None, filename=None):
flash("Plik już nie istnieje.", "warning")
return redirect(url_for("admin_receipts", id="all"))
# tryb z rekordem w bazie
try:
delete_receipt_by_id(receipt_id)
flash("Paragon usunięty", "success")
@@ -2508,7 +2435,6 @@ def delete_selected_lists():
ids = request.form.getlist("list_ids")
for list_id in ids:
# lst = ShoppingList.query.get(int(list_id))
lst = db.session.get(ShoppingList, int(list_id))
if lst:
@@ -2525,7 +2451,6 @@ def delete_selected_lists():
@login_required
@admin_required
def edit_list(list_id):
# Pobieramy listę z powiązanymi danymi jednym zapytaniem
l = db.session.get(
ShoppingList,
list_id,
@@ -2541,7 +2466,6 @@ def edit_list(list_id):
if l is None:
abort(404)
# Suma wydatków z listy
total_expense = get_total_expense_for_list(l.id)
categories = Category.query.order_by(Category.name.asc()).all()
@@ -2615,7 +2539,6 @@ def edit_list(list_id):
)
return redirect(url_for("edit_list", list_id=list_id))
# aktualizacja kategorii
update_list_categories_from_form(l, request.form)
db.session.add(l)
@@ -2718,7 +2641,6 @@ def edit_list(list_id):
flash("Nie znaleziono produktu", "danger")
return redirect(url_for("edit_list", list_id=list_id))
# Dane do widoku
users = User.query.all()
items = l.items
receipts = l.receipts
@@ -2740,11 +2662,9 @@ def edit_list(list_id):
@admin_required
def list_products():
items = Item.query.order_by(Item.id.desc()).all()
# users = User.query.all()
users = db.session.query(User).all()
users_dict = {user.id: user.username for user in users}
# Stabilne sortowanie sugestii
suggestions = SuggestedProduct.query.order_by(SuggestedProduct.name.asc()).all()
suggestions_dict = {s.name.lower(): s for s in suggestions}
@@ -2930,7 +2850,6 @@ def handle_delete_item(data):
@socketio.on("edit_item")
def handle_edit_item(data):
# item = Item.query.get(data["item_id"])
item = db.session.get(Item, data["item_id"])
new_name = data["new_name"]
@@ -2968,7 +2887,6 @@ def handle_join(data):
active_users[room] = set()
active_users[room].add(username)
# shopping_list = ShoppingList.query.get(int(data["room"]))
shopping_list = db.session.get(ShoppingList, int(data["room"]))
list_title = shopping_list.title if shopping_list else "Twoja lista"
@@ -3083,12 +3001,10 @@ def handle_add_item(data):
@socketio.on("check_item")
def handle_check_item(data):
# item = Item.query.get(data["item_id"])
item = db.session.get(Item, data["item_id"])
if item:
item.purchased = True
# item.purchased_at = datetime.utcnow()
item.purchased_at = datetime.now(UTC)
db.session.commit()
@@ -3109,7 +3025,6 @@ def handle_check_item(data):
@socketio.on("uncheck_item")
def handle_uncheck_item(data):
# item = Item.query.get(data["item_id"])
item = db.session.get(Item, data["item_id"])
if item:
@@ -3208,7 +3123,6 @@ def handle_add_expense(data):
@socketio.on("mark_not_purchased")
def handle_mark_not_purchased(data):
# item = Item.query.get(data["item_id"])
item = db.session.get(Item, data["item_id"])
reason = data.get("reason", "")
@@ -3225,7 +3139,6 @@ def handle_mark_not_purchased(data):
@socketio.on("unmark_not_purchased")
def handle_unmark_not_purchased(data):
# item = Item.query.get(data["item_id"])
item = db.session.get(Item, data["item_id"])
if item:

View File

@@ -1,7 +1,10 @@
{% extends 'base.html' %}
{% block content %}
<h2>Edytuj listę: <strong>{{ list.title }}</strong></h2>
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2>Edytuj listę: <strong>{{ list.title }}</strong></h2>
<a href="{{ url_for('main_page') }}" class="btn btn-outline-secondary">← Powrót</a>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
@@ -42,7 +45,10 @@
<div class="col-md-6">
<label class="form-label">Aktualna data utworzenia:</label>
<p class="form-control-plaintext text-white">
{{ list.created_at.strftime('%Y-%m-%d') }}
<span class="badge bg-success rounded-pill text-dark ms-1">
{{ list.created_at.strftime('%Y-%m-%d') }}
</span>
</p>
</div>
<div class="col-md-6">

View File

@@ -18,8 +18,8 @@
</span>
{% endfor %}
{% else %}
<a href="{{ url_for('edit_my_list', list_id=list.id) }}" class="ms-2 text-light small fw-light"
style="opacity: 0.9;">
<a href="{{ url_for('edit_my_list', list_id=list.id, next=url_for('view_list', list_id=list.id)) }}"
class="ms-2 text-light small fw-light" style="opacity: 0.9;">
Dodaj kategorię
</a>
{% endif %}