From 470cd327456722d16c6fe2fddea22086fd43de45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 20 Jul 2025 16:50:26 +0200 Subject: [PATCH] webp support --- alters.txt | 9 +++ app.py | 136 ++++++++++++++++++--------------- migrate_to_webp.py | 73 ++++++++++++++++++ receitp_to_list.py | 42 ++++++++++ templates/admin/edit_list.html | 18 ++--- templates/admin/receipts.html | 32 ++++---- 6 files changed, 222 insertions(+), 88 deletions(-) create mode 100644 migrate_to_webp.py create mode 100644 receitp_to_list.py diff --git a/alters.txt b/alters.txt index 1731e50..27ee04c 100644 --- a/alters.txt +++ b/alters.txt @@ -37,3 +37,12 @@ ALTER TABLE item ADD COLUMN not_purchased BOOLEAN DEFAULT 0; # funkcja sortowania ALTER TABLE item ADD COLUMN position INTEGER DEFAULT 0; + +# migracja paragonów do nowej tabeli +CREATE TABLE receipt ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + list_id INTEGER NOT NULL, + filename TEXT NOT NULL, + uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (list_id) REFERENCES shopping_list(id) +); diff --git a/app.py b/app.py index 08e1f8f..2a1de6e 100644 --- a/app.py +++ b/app.py @@ -142,6 +142,14 @@ class Expense(db.Model): receipt_filename = db.Column(db.String(255), nullable=True) list = db.relationship("ShoppingList", backref="expenses", lazy=True) +class Receipt(db.Model): + id = db.Column(db.Integer, primary_key=True) + list_id = db.Column(db.Integer, db.ForeignKey("shopping_list.id"), nullable=False) + filename = db.Column(db.String(255), nullable=False) + uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) + + shopping_list = db.relationship("ShoppingList", backref="receipts", lazy=True) + with app.app_context(): db.create_all() @@ -215,11 +223,12 @@ def allowed_file(filename): def get_list_details(list_id): shopping_list = ShoppingList.query.get_or_404(list_id) items = Item.query.filter_by(list_id=list_id).order_by(Item.position.asc()).all() - receipt_pattern = f"list_{list_id}" - all_files = os.listdir(app.config["UPLOAD_FOLDER"]) - receipt_files = [f for f in all_files if receipt_pattern in f] expenses = Expense.query.filter_by(list_id=list_id).all() total_expense = sum(e.amount for e in expenses) + + receipts = Receipt.query.filter_by(list_id=list_id).all() + receipt_files = [r.filename for r in receipts] + return shopping_list, items, receipt_files, expenses, total_expense @@ -243,16 +252,15 @@ def enrich_list_data(l): l.total_expense = sum(e.amount for e in expenses) return l + def save_resized_image(file, path): image = Image.open(file) image.thumbnail((2000, 2000)) - if image.format == "HEIF": - path = path.rsplit(".", 1)[0] + ".jpg" - image = image.convert("RGB") - image.save(path, format="JPEG") - else: - image.save(path) + new_path = path.rsplit(".", 1)[0] + ".webp" + image = image.convert("RGB") + image.save(new_path, format="WEBP", quality=85) + def redirect_with_flash( message: str, category: str = "info", endpoint: str = "main_page" @@ -927,54 +935,46 @@ def upload_receipt(list_id): return redirect(request.referrer) """ + @app.route("/upload_receipt/", methods=["POST"]) def upload_receipt(list_id): if "receipt" not in request.files: - if ( - request.is_json - or request.headers.get("X-Requested-With") == "XMLHttpRequest" - ): - return jsonify({"success": False, "message": "Brak pliku"}), 400 - flash("Brak pliku", "danger") - return redirect(request.referrer) + return _receipt_error("Brak pliku") file = request.files["receipt"] - if file.filename == "": - if ( - request.is_json - or request.headers.get("X-Requested-With") == "XMLHttpRequest" - ): - return jsonify({"success": False, "message": "Nie wybrano pliku"}), 400 - flash("Nie wybrano pliku", "danger") - return redirect(request.referrer) + return _receipt_error("Nie wybrano pliku") if file and allowed_file(file.filename): filename = secure_filename(file.filename) - full_filename = f"list_{list_id}_{filename}" - file_path = os.path.join(app.config["UPLOAD_FOLDER"], full_filename) + base_name = f"list_{list_id}_{filename.rsplit('.', 1)[0]}" + webp_filename = base_name + ".webp" + file_path = os.path.join(app.config["UPLOAD_FOLDER"], webp_filename) save_resized_image(file, file_path) - if ( - request.is_json - or request.headers.get("X-Requested-With") == "XMLHttpRequest" - ): - url = url_for("uploaded_file", filename=full_filename) + new_receipt = Receipt(list_id=list_id, filename=webp_filename) + db.session.add(new_receipt) + db.session.commit() + 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}) flash("Wgrano paragon", "success") return redirect(request.referrer) + return _receipt_error("Niedozwolony format pliku") + +def _receipt_error(message): if request.is_json or request.headers.get("X-Requested-With") == "XMLHttpRequest": - return jsonify({"success": False, "message": "Niedozwolony format pliku"}), 400 - flash("Niedozwolony format pliku", "danger") + return jsonify({"success": False, "message": message}), 400 + flash(message, "danger") return redirect(request.referrer) + @app.route("/uploads/") def uploaded_file(filename): response = send_from_directory(app.config["UPLOAD_FOLDER"], filename) @@ -1196,29 +1196,32 @@ def delete_user(user_id): return redirect(url_for("list_users")) +import os + @app.route("/admin/receipts/") @login_required @admin_required def admin_receipts(id): - all_files = os.listdir(app.config["UPLOAD_FOLDER"]) - image_files = [f for f in all_files if allowed_file(f)] - - if id == "all": - filtered_files = image_files - else: - try: + try: + if id == "all": + receipts = Receipt.query.order_by(Receipt.uploaded_at.desc()).all() + else: list_id = int(id) - receipt_prefix = f"list_{list_id}_" - filtered_files = [f for f in image_files if f.startswith(receipt_prefix)] - except ValueError: - flash("Nieprawidłowe ID listy.", "danger") - return redirect(url_for("admin_panel")) + receipts = ( + Receipt.query.filter_by(list_id=list_id) + .order_by(Receipt.uploaded_at.desc()) + .all() + ) + except ValueError: + flash("Nieprawidłowe ID listy.", "danger") + return redirect(url_for("admin_panel")) + + for r in receipts: + path = os.path.join(app.config["UPLOAD_FOLDER"], r.filename) + r.filesize = os.path.getsize(path) if os.path.exists(path) else 0 + + return render_template("admin/receipts.html", receipts=receipts) - return render_template( - "admin/receipts.html", - image_files=filtered_files, - upload_folder=app.config["UPLOAD_FOLDER"], - ) @app.route("/admin/delete_receipt/") @@ -1226,16 +1229,31 @@ def admin_receipts(id): @admin_required def delete_receipt(filename): file_path = os.path.join(app.config["UPLOAD_FOLDER"], filename) + removed_file = False + removed_db = False + + # Usuń plik z dysku if os.path.exists(file_path): os.remove(file_path) - flash("Plik usunięty", "success") - else: - flash("Plik nie istnieje", "danger") + removed_file = True + # Usuń rekord z bazy + receipt = Receipt.query.filter_by(filename=filename).first() + if receipt: + db.session.delete(receipt) + db.session.commit() + removed_db = True + + # Komunikat + if removed_file or removed_db: + flash("Paragon usunięty", "success") + else: + flash("Paragon nie istnieje", "danger") + + # Powrót next_url = request.args.get("next") - if next_url: - return redirect(next_url) - return redirect(url_for("admin_receipts")) + return redirect(next_url or url_for("admin_receipts", id="all")) + @app.route("/admin/delete_selected_lists", methods=["POST"]) @@ -1273,9 +1291,7 @@ def edit_list(list_id): db.session.query(Item).filter_by(list_id=list_id).order_by(Item.id.desc()).all() ) - receipt_pattern = f"list_{list_id}_" - all_files = os.listdir(app.config["UPLOAD_FOLDER"]) - receipts = [f for f in all_files if f.startswith(receipt_pattern)] + receipts = Receipt.query.filter_by(list_id=list_id).order_by(Receipt.uploaded_at.desc()).all() if request.method == "POST": action = request.form.get("action") diff --git a/migrate_to_webp.py b/migrate_to_webp.py new file mode 100644 index 0000000..bfad307 --- /dev/null +++ b/migrate_to_webp.py @@ -0,0 +1,73 @@ +import os +from datetime import datetime +from PIL import Image +from app import db, app, Receipt + +ALLOWED_EXTS = ("jpg", "jpeg", "png", "gif", "heic") +UPLOAD_FOLDER = None + +def convert_to_webp(input_path, output_path): + try: + image = Image.open(input_path).convert("RGB") + image.save(output_path, "WEBP", quality=85) + return True + except Exception as e: + print(f"Błąd konwersji {input_path}: {e}") + return False + +def extract_list_id(filename): + if filename.startswith("list_"): + parts = filename.split("_", 2) + if len(parts) >= 2 and parts[1].isdigit(): + return int(parts[1]) + return None + +def migrate(): + global UPLOAD_FOLDER + with app.app_context(): + UPLOAD_FOLDER = app.config["UPLOAD_FOLDER"] + files = os.listdir(UPLOAD_FOLDER) + created = 0 + skipped = 0 + existing = 0 + + for file in files: + ext = file.rsplit(".", 1)[-1].lower() + if ext not in ALLOWED_EXTS: + continue + + list_id = extract_list_id(file) + if list_id is None: + print(f"Pominięto (brak list_id): {file}") + continue + + src_path = os.path.join(UPLOAD_FOLDER, file) + base = os.path.splitext(file)[0] + webp_filename = base + ".webp" + dst_path = os.path.join(UPLOAD_FOLDER, webp_filename) + + if os.path.exists(dst_path): + print(f"Pominięto (webp istnieje): {webp_filename}") + skipped += 1 + continue + + if convert_to_webp(src_path, dst_path): + os.remove(src_path) + r = Receipt.query.filter_by(list_id=list_id, filename=webp_filename).first() + if r: + print(f"Już istnieje w Receipt: {webp_filename}") + existing += 1 + continue + + new_receipt = Receipt(list_id=list_id, filename=webp_filename, uploaded_at=datetime.utcnow()) + db.session.add(new_receipt) + created += 1 + print(f"{file} → {webp_filename} + zapis do Receipt") + + db.session.commit() + print(f"\nNowe wpisy: {created}") + print(f"Pominięte (webp istniało): {skipped}") + print(f"Duplikaty w bazie: {existing}") + +if __name__ == "__main__": + migrate() diff --git a/receitp_to_list.py b/receitp_to_list.py new file mode 100644 index 0000000..89756fe --- /dev/null +++ b/receitp_to_list.py @@ -0,0 +1,42 @@ +import os +from datetime import datetime +from app import db, app, Receipt + +def extract_list_id(filename): + if filename.startswith("list_"): + parts = filename.split("_", 2) + if len(parts) >= 2 and parts[1].isdigit(): + return int(parts[1]) + return None + +def migrate_missing_receipts(): + with app.app_context(): + folder = app.config["UPLOAD_FOLDER"] + files = os.listdir(folder) + added = 0 + skipped = 0 + + for file in files: + if not file.endswith(".webp"): + continue + + list_id = extract_list_id(file) + if list_id is None: + print(f"Pominięto (brak list_id): {file}") + continue + + exists = Receipt.query.filter_by(list_id=list_id, filename=file).first() + if exists: + skipped += 1 + continue + + new_receipt = Receipt(list_id=list_id, filename=file, uploaded_at=datetime.utcnow()) + db.session.add(new_receipt) + added += 1 + print(f"📄 {file} dodany do Receipt (list_id={list_id})") + + db.session.commit() + print(f"\n✅ Dodano: {added}, pominięto (już były): {skipped}") + +if __name__ == "__main__": + migrate_missing_receipts() diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index 1efd3a3..bda1085 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -192,22 +192,18 @@
- {% for img in receipts %} - {% set file_path = upload_folder ~ '/' ~ img %} - {% set file_size = (file_path | filesizeformat) %} - {% set upload_time = (file_path | filemtime) %} + {% for r in receipts %}
- - +
-

{{ img }}

-

Rozmiar: {{ file_size }}

-

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

- {{ r.filename }}

+

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

+
🗑️ Usuń
diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index 07338f4..d8db03f 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -10,36 +10,34 @@
- {% for img in image_files %} - {% set list_id = img.split('_')[1] if '_' in img else None %} - {% set file_path = upload_folder ~ '/' ~ img %} - {% set file_size = (file_path | filesizeformat) %} - {% set upload_time = (file_path | filemtime) %} + {% for r in receipts %}
- - +
-

{{ img }}

-

Rozmiar: {{ file_size }}

-

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

- {% if list_id %} - ✏️ - Edytuj listę #{{ list_id }} +

{{ r.filename }}

+

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

+ {% if r.filesize >= 1024 * 1024 %} +

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

+ {% else %} +

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

{% endif %} - ✏️ + Edytuj listę #{{ r.list_id }} + 🗑️ Usuń
{% endfor %} +
- - {% if not image_files %} + {% if not receipts %}