diff --git a/app.py b/app.py index ce06b2c..6b6785b 100644 --- a/app.py +++ b/app.py @@ -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/") 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/") @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/", 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/") @@ -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: diff --git a/templates/edit_my_list.html b/templates/edit_my_list.html index 55c3471..26766b3 100644 --- a/templates/edit_my_list.html +++ b/templates/edit_my_list.html @@ -1,7 +1,10 @@ {% extends 'base.html' %} {% block content %} -

Edytuj listę: {{ list.title }}

+
+

Edytuj listę: {{ list.title }}

+ ← Powrót +
@@ -42,7 +45,10 @@

- {{ list.created_at.strftime('%Y-%m-%d') }} + + {{ list.created_at.strftime('%Y-%m-%d') }} + +

diff --git a/templates/list.html b/templates/list.html index 1fbc267..e07a344 100644 --- a/templates/list.html +++ b/templates/list.html @@ -18,8 +18,8 @@ {% endfor %} {% else %} - + ➕ Dodaj kategorię {% endif %}