diff --git a/app.py b/app.py index d7d860a..36bb75f 100644 --- a/app.py +++ b/app.py @@ -284,6 +284,7 @@ class Receipt(db.Model): filesize = db.Column(db.Integer, nullable=True) file_hash = db.Column(db.String(64), nullable=True, unique=True) uploaded_by = db.Column(db.Integer, db.ForeignKey("user.id")) + version_token = db.Column(db.String(32), nullable=True) shopping_list = db.relationship("ShoppingList", back_populates="receipts") uploaded_by_user = db.relationship("User", backref="uploaded_receipts") @@ -408,6 +409,8 @@ app.register_blueprint(static_bp) def allowed_file(filename): return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS +def generate_version_token(): + return secrets.token_hex(8) def get_list_details(list_id): shopping_list = ShoppingList.query.options( @@ -419,9 +422,9 @@ def get_list_details(list_id): items = sorted(shopping_list.items, key=lambda i: i.position or 0) expenses = shopping_list.expenses total_expense = sum(e.amount for e in expenses) if expenses else 0 - receipt_files = [r.filename for r in shopping_list.receipts] + receipts = shopping_list.receipts - return shopping_list, items, receipt_files, expenses, total_expense + return shopping_list, items, receipts, expenses, total_expense def get_total_expense_for_list(list_id, start_date=None, end_date=None): @@ -505,7 +508,8 @@ def save_resized_image(file, path): image.info.clear() new_path = path.rsplit(".", 1)[0] + ".webp" - image.save(new_path, **WEBP_SAVE_PARAMS) + #image.save(new_path, **WEBP_SAVE_PARAMS) + image.save(new_path, format="WEBP", method=6, quality=100) except Exception as e: raise ValueError(f"Błąd podczas przetwarzania obrazu: {e}") @@ -569,23 +573,27 @@ def receipt_error(message): def rotate_receipt_by_id(receipt_id): receipt = Receipt.query.get_or_404(receipt_id) - old_path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename) + path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename) - if not os.path.exists(old_path): + if not os.path.exists(path): raise FileNotFoundError("Plik nie istnieje") - image = Image.open(old_path) - rotated = image.rotate(-90, expand=True) + try: + image = Image.open(path) + rotated = image.rotate(-90, expand=True) - new_filename = generate_new_receipt_filename(receipt.list_id) - new_path = os.path.join(app.config["UPLOAD_FOLDER"], new_filename) - rotated.save(new_path, **WEBP_SAVE_PARAMS) + rotated = rotated.convert("RGB") + rotated.info.clear() - os.remove(old_path) - receipt.filename = new_filename - db.session.commit() + rotated.save(path, format="WEBP", method=6, quality=100) + receipt.version_token = generate_version_token() + recalculate_filesizes(receipt.id) + db.session.commit() - return receipt + return receipt + except Exception as e: + app.logger.exception("Błąd podczas rotacji pliku") + raise RuntimeError(f"Błąd podczas rotacji pliku: {e}") def delete_receipt_by_id(receipt_id): @@ -610,23 +618,18 @@ def handle_crop_receipt(receipt_id, file): if not receipt_id or not file: return {"success": False, "error": "Brak danych"} - receipt = Receipt.query.get_or_404(receipt_id) - old_path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename) - try: - new_filename = generate_new_receipt_filename(receipt.list_id) - new_path = os.path.join(app.config["UPLOAD_FOLDER"], new_filename) + receipt = Receipt.query.get_or_404(receipt_id) + path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename) - save_resized_image(file, new_path) - - if os.path.exists(old_path): - os.remove(old_path) - - receipt.filename = os.path.basename(new_path) - db.session.commit() + save_resized_image(file, path) + receipt.version_token = generate_version_token() recalculate_filesizes(receipt.id) + db.session.commit() + return {"success": True} except Exception as e: + app.logger.exception("Błąd podczas przycinania paragonu") return {"success": False, "error": str(e)} @@ -1760,7 +1763,7 @@ def view_list(list_id): flash("W celu modyfikacji listy, przejdź do panelu administracyjnego.", "info") return redirect(url_for("shared_list", token=shopping_list.share_token)) - shopping_list, items, receipt_files, expenses, total_expense = get_list_details( + shopping_list, items, receipts, expenses, total_expense = get_list_details( list_id ) total_count = len(items) @@ -1785,7 +1788,7 @@ def view_list(list_id): "list.html", list=shopping_list, items=items, - receipt_files=receipt_files, + receipts=receipts, total_count=total_count, purchased_count=purchased_count, percent=percent, @@ -1960,7 +1963,7 @@ def shared_list(token=None, list_id=None): list_id = shopping_list.id total_expense = get_total_expense_for_list(list_id) - shopping_list, items, receipt_files, expenses, total_expense = get_list_details( + shopping_list, items, receipts, expenses, total_expense = get_list_details( list_id ) @@ -1981,7 +1984,7 @@ def shared_list(token=None, list_id=None): "list_share.html", list=shopping_list, items=items, - receipt_files=receipt_files, + receipts=receipts, expenses=expenses, total_expense=total_expense, is_share=True, @@ -2130,66 +2133,60 @@ def all_products(): @app.route("/upload_receipt/", methods=["POST"]) @login_required def upload_receipt(list_id): - l = db.session.get(ShoppingList, list_id) - if "receipt" not in request.files: - return receipt_error("Brak pliku") - - file = request.files["receipt"] - if file.filename == "": + file = request.files.get("receipt") + if not file or file.filename == "": return receipt_error("Nie wybrano pliku") - if file and allowed_file(file.filename): - file_bytes = file.read() - file.seek(0) - file_hash = hashlib.sha256(file_bytes).hexdigest() + if not allowed_file(file.filename): + return receipt_error("Niedozwolony format pliku") - existing = Receipt.query.filter_by(file_hash=file_hash).first() - if existing: - return receipt_error("Taki plik już istnieje") + file_bytes = file.read() + file.seek(0) + file_hash = hashlib.sha256(file_bytes).hexdigest() - now = datetime.now(timezone.utc) - timestamp = now.strftime("%Y%m%d_%H%M") - random_part = secrets.token_hex(3) - webp_filename = f"list_{list_id}_{timestamp}_{random_part}.webp" - file_path = os.path.join(app.config["UPLOAD_FOLDER"], webp_filename) + existing = Receipt.query.filter_by(file_hash=file_hash).first() + if existing: + return receipt_error("Taki plik już istnieje") - try: - if file.filename.lower().endswith(".pdf"): - file.seek(0) - save_pdf_as_webp(file, file_path) - else: - save_resized_image(file, file_path) - except ValueError as e: - return receipt_error(str(e)) + now = datetime.now(timezone.utc) + timestamp = now.strftime("%Y%m%d_%H%M") + random_part = secrets.token_hex(3) + webp_filename = f"list_{list_id}_{timestamp}_{random_part}.webp" + file_path = os.path.join(app.config["UPLOAD_FOLDER"], webp_filename) - filesize = os.path.getsize(file_path) - uploaded_at = datetime.now(timezone.utc) + try: + if file.filename.lower().endswith(".pdf"): + file.seek(0) + save_pdf_as_webp(file, file_path) + else: + save_resized_image(file, file_path) + except ValueError as e: + return receipt_error(str(e)) + try: new_receipt = Receipt( list_id=list_id, filename=webp_filename, - filesize=filesize, - uploaded_at=uploaded_at, + filesize=os.path.getsize(file_path), + uploaded_at=now, file_hash=file_hash, uploaded_by=current_user.id, + version_token=generate_version_token(), ) db.session.add(new_receipt) db.session.commit() + except Exception as e: + return receipt_error(f"Błąd zapisu do bazy: {str(e)}") - if ( - request.is_json - or request.headers.get("X-Requested-With") == "XMLHttpRequest" - ): - url = url_for("uploaded_file", filename=webp_filename) - socketio.emit("receipt_added", {"url": url}, to=str(list_id)) - return jsonify({"success": True, "url": url}) + if request.is_json or request.headers.get("X-Requested-With") == "XMLHttpRequest": + url = url_for("uploaded_file", filename=webp_filename) + f"?v={new_receipt.version_token or '0'}" + socketio.emit("receipt_added", {"url": url}, to=str(list_id)) + return jsonify({"success": True, "url": url}) - flash("Wgrano paragon", "success") - return redirect(request.referrer or url_for("main_page")) - - return receipt_error("Niedozwolony format pliku") + flash("Wgrano paragon", "success") + return redirect(request.referrer or url_for("main_page")) @app.route("/uploads/") diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 90835f5..7de09fb 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -7,7 +7,7 @@ ← Powrót do strony głównej -
+
👥 Użytkownicy diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index e01349b..0b5733b 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -7,7 +7,7 @@ ← Powrót do panelu
-
+

📄 Podstawowe informacje

@@ -123,7 +123,7 @@
-
+

📝 Produkty

@@ -226,7 +226,7 @@
-
+

🧾 Paragony

@@ -239,25 +239,27 @@
{% for r in receipts %}
-
- - +
+ + -
-

{{ r.filename }}

-

Wgrano: {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}

-

- Uploader: {{ r.uploaded_by_user.username if r.uploaded_by_user else "?" }} -

- {% if r.filesize and r.filesize >= 1024 * 1024 %} -

Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB

- {% elif r.filesize %} -

Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB

- {% else %} -

Brak danych o rozmiarze

- {% endif %} + +
+
📄 {{ r.filename }}
+
📅 {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}
+
👤 {{ r.uploaded_by_user.username if r.uploaded_by_user else "?" }}
+
+ 💾 + {% if r.filesize and r.filesize >= 1024 * 1024 %} + {{ (r.filesize / 1024 / 1024) | round(2) }} MB + {% elif r.filesize %} + {{ (r.filesize / 1024) | round(1) }} kB + {% else %} + Brak danych + {% endif %} +
diff --git a/templates/admin/list_products.html b/templates/admin/list_products.html index 6637b75..7068a62 100644 --- a/templates/admin/list_products.html +++ b/templates/admin/list_products.html @@ -7,7 +7,7 @@ ← Powrót do panelu
-
+
diff --git a/templates/admin/mass_edit_categories.html b/templates/admin/mass_edit_categories.html index 6daab00..0f34036 100644 --- a/templates/admin/mass_edit_categories.html +++ b/templates/admin/mass_edit_categories.html @@ -3,92 +3,102 @@ {% block content %}
-

🗂 Masowa edycja kategorii list

+

🗂 Masowa edycja kategorii

-