From ae89f554465d9597f18bff31c7e0b82fb873084c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 20 Jul 2025 17:34:53 +0200 Subject: [PATCH] webp support --- add_products.py | 263 +++++++++++++++++++++++++++++------ add_receipt_to_list.py | 7 +- app.py | 22 ++- config.py | 21 +-- migrate_to_webp.py | 14 +- update_missing_image_data.py | 6 +- 6 files changed, 272 insertions(+), 61 deletions(-) diff --git a/add_products.py b/add_products.py index 379025d..9f97789 100644 --- a/add_products.py +++ b/add_products.py @@ -4,66 +4,249 @@ from app import db, SuggestedProduct, app CATEGORIES = { "Przyprawa": [ - "przyprawa", "pieprz", "sól", "bazylia", "oregano", "papryka", "majeranek", "czosnek", - "tymianek", "rozmaryn", "kolendra", "curry", "imbir", "goździki", "chili", "koper", - "kminek", "liść laurowy", "ziele angielskie", "kurkuma", "musztarda", "chrzan" + "przyprawa", + "pieprz", + "sól", + "bazylia", + "oregano", + "papryka", + "majeranek", + "czosnek", + "tymianek", + "rozmaryn", + "kolendra", + "curry", + "imbir", + "goździki", + "chili", + "koper", + "kminek", + "liść laurowy", + "ziele angielskie", + "kurkuma", + "musztarda", + "chrzan", ], "Mięso": [ - "kurczak", "piersi z kurczaka", "udka z kurczaka", "wołowina", "mielona wołowina", - "wieprzowina", "schab", "łopatka", "szynka", "boczek", "indyk", "filet z indyka", - "gulasz", "pasztet", "karkówka", "żeberka", "kiełbasa", "parówki", "salami", "kabanos" + "kurczak", + "piersi z kurczaka", + "udka z kurczaka", + "wołowina", + "mielona wołowina", + "wieprzowina", + "schab", + "łopatka", + "szynka", + "boczek", + "indyk", + "filet z indyka", + "gulasz", + "pasztet", + "karkówka", + "żeberka", + "kiełbasa", + "parówki", + "salami", + "kabanos", ], "Ryba i owoce morza": [ - "łosoś", "dorsz", "mintaj", "makrela", "pstrąg", "karp", "śledź", "tuńczyk", - "morszczuk", "sardynka", "szproty", "anchois", "tilapia", "sandacz", "halibut", - "sum", "flądra", "ostrobok", "paluszki rybne", "konserwa rybna" + "łosoś", + "dorsz", + "mintaj", + "makrela", + "pstrąg", + "karp", + "śledź", + "tuńczyk", + "morszczuk", + "sardynka", + "szproty", + "anchois", + "tilapia", + "sandacz", + "halibut", + "sum", + "flądra", + "ostrobok", + "paluszki rybne", + "konserwa rybna", ], "Nabiał": [ - "mleko", "jogurt", "ser żółty", "ser biały", "twaróg", "śmietana", "masło", - "kefir", "maślanka", "serek wiejski", "serek topiony", "mozzarella", "feta", - "parmezan", "gouda", "emmental", "ser pleśniowy", "ser homogenizowany", - "serek mascarpone", "ser ricotta" + "mleko", + "jogurt", + "ser żółty", + "ser biały", + "twaróg", + "śmietana", + "masło", + "kefir", + "maślanka", + "serek wiejski", + "serek topiony", + "mozzarella", + "feta", + "parmezan", + "gouda", + "emmental", + "ser pleśniowy", + "ser homogenizowany", + "serek mascarpone", + "ser ricotta", ], "Warzywo": [ - "pomidor", "ogórek", "marchew", "cebula", "sałata", "papryka", "ziemniak", - "kapusta", "brokuł", "kalafior", "cukinia", "bakłażan", "szpinak", "rukola", - "seler", "por", "burak", "dynia", "rzodkiewka", "fasola" + "pomidor", + "ogórek", + "marchew", + "cebula", + "sałata", + "papryka", + "ziemniak", + "kapusta", + "brokuł", + "kalafior", + "cukinia", + "bakłażan", + "szpinak", + "rukola", + "seler", + "por", + "burak", + "dynia", + "rzodkiewka", + "fasola", ], "Owoc": [ - "jabłko", "banan", "gruszka", "truskawka", "winogrono", "malina", "borówka", - "czereśnia", "wiśnia", "brzoskwinia", "nektaryna", "śliwka", "ananas", - "mango", "kiwi", "cytryna", "limonka", "pomarańcza", "mandarynka", "grejpfrut" + "jabłko", + "banan", + "gruszka", + "truskawka", + "winogrono", + "malina", + "borówka", + "czereśnia", + "wiśnia", + "brzoskwinia", + "nektaryna", + "śliwka", + "ananas", + "mango", + "kiwi", + "cytryna", + "limonka", + "pomarańcza", + "mandarynka", + "grejpfrut", ], "Pieczywo i zboża": [ - "chleb", "bułka", "bagietka", "kajzerka", "pumpernikiel", "chleb razowy", - "chleb żytni", "tost", "grahamka", "croissant", "tortilla", "pizza", - "pierogi", "ryż", "makaron", "kasza jaglana", "kasza gryczana", "owsianka", - "płatki kukurydziane", "musli" + "chleb", + "bułka", + "bagietka", + "kajzerka", + "pumpernikiel", + "chleb razowy", + "chleb żytni", + "tost", + "grahamka", + "croissant", + "tortilla", + "pizza", + "pierogi", + "ryż", + "makaron", + "kasza jaglana", + "kasza gryczana", + "owsianka", + "płatki kukurydziane", + "musli", ], "Słodycze i przekąski": [ - "czekolada", "baton", "ciastko", "wafel", "lody", "cukierek", "żelki", - "herbatnik", "paluszki", "chipsy", "orzeszki", "popcorn", "krakersy", - "ciasto", "muffin", "pączek", "drożdżówka", "babeczka", "piernik", "beza" + "czekolada", + "baton", + "ciastko", + "wafel", + "lody", + "cukierek", + "żelki", + "herbatnik", + "paluszki", + "chipsy", + "orzeszki", + "popcorn", + "krakersy", + "ciasto", + "muffin", + "pączek", + "drożdżówka", + "babeczka", + "piernik", + "beza", ], "Napoje": [ - "woda", "sok jabłkowy", "sok pomarańczowy", "sok multiwitamina", "cola", - "pepsi", "napój gazowany", "kawa", "herbata", "piwo", "wino czerwone", - "wino białe", "tonik", "lemoniada", "napój izotoniczny", "kompot", - "napój mleczny", "maślanka pitna", "koktajl owocowy", "nektar" + "woda", + "sok jabłkowy", + "sok pomarańczowy", + "sok multiwitamina", + "cola", + "pepsi", + "napój gazowany", + "kawa", + "herbata", + "piwo", + "wino czerwone", + "wino białe", + "tonik", + "lemoniada", + "napój izotoniczny", + "kompot", + "napój mleczny", + "maślanka pitna", + "koktajl owocowy", + "nektar", ], "Tłuszcze i oleje": [ - "oliwa", "olej rzepakowy", "olej słonecznikowy", "masło klarowane", - "margaryna", "smalec", "masło orzechowe", "tłuszcz kokosowy", - "olej lniany", "olej z pestek winogron", "olej sezamowy", - "olej ryżowy", "olej z awokado", "olej kukurydziany", "olej arachidowy", - "olej palmowy", "olej konopny", "olej sojowy", "olej dyniowy", "olej z orzechów włoskich" + "oliwa", + "olej rzepakowy", + "olej słonecznikowy", + "masło klarowane", + "margaryna", + "smalec", + "masło orzechowe", + "tłuszcz kokosowy", + "olej lniany", + "olej z pestek winogron", + "olej sezamowy", + "olej ryżowy", + "olej z awokado", + "olej kukurydziany", + "olej arachidowy", + "olej palmowy", + "olej konopny", + "olej sojowy", + "olej dyniowy", + "olej z orzechów włoskich", ], "Dania gotowe": [ - "pizza", "hamburger", "hot dog", "zupa", "gulasz", "pierogi ruskie", - "pierogi z mięsem", "lasagne", "sałatka warzywna", "kanapka", - "wrap", "tortilla", "zapiekanka", "sushi", "falafel", "kebab", - "pyzy", "kluski śląskie", "kotlet schabowy", "gołąbki" - ] + "pizza", + "hamburger", + "hot dog", + "zupa", + "gulasz", + "pierogi ruskie", + "pierogi z mięsem", + "lasagne", + "sałatka warzywna", + "kanapka", + "wrap", + "tortilla", + "zapiekanka", + "sushi", + "falafel", + "kebab", + "pyzy", + "kluski śląskie", + "kotlet schabowy", + "gołąbki", + ], } produkty = [] diff --git a/add_receipt_to_list.py b/add_receipt_to_list.py index 89756fe..0012b85 100644 --- a/add_receipt_to_list.py +++ b/add_receipt_to_list.py @@ -2,6 +2,7 @@ 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) @@ -9,6 +10,7 @@ def extract_list_id(filename): return int(parts[1]) return None + def migrate_missing_receipts(): with app.app_context(): folder = app.config["UPLOAD_FOLDER"] @@ -30,7 +32,9 @@ def migrate_missing_receipts(): skipped += 1 continue - new_receipt = Receipt(list_id=list_id, filename=file, uploaded_at=datetime.utcnow()) + 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})") @@ -38,5 +42,6 @@ def migrate_missing_receipts(): db.session.commit() print(f"\n✅ Dodano: {added}, pominięto (już były): {skipped}") + if __name__ == "__main__": migrate_missing_receipts() diff --git a/app.py b/app.py index 9d41a74..9650308 100644 --- a/app.py +++ b/app.py @@ -48,7 +48,7 @@ from functools import wraps app = Flask(__name__) app.config.from_object(Config) -register_heif_opener() # pillow_heif dla HEIC +register_heif_opener() # pillow_heif dla HEIC ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "webp", "heic"} SQLALCHEMY_ECHO = True @@ -142,6 +142,7 @@ 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) @@ -150,6 +151,7 @@ class Receipt(db.Model): shopping_list = db.relationship("ShoppingList", backref="receipts", lazy=True) filesize = db.Column(db.Integer, nullable=True) + with app.app_context(): db.create_all() from werkzeug.security import generate_password_hash @@ -297,7 +299,6 @@ def delete_receipts_for_list(list_id): print(f"Nie udało się usunąć pliku {filename}: {e}") - # zabezpieczenie logowani do systemu - błędne hasła def is_ip_blocked(ip): now = time.time() @@ -937,6 +938,7 @@ def upload_receipt(list_id): from datetime import datetime + @app.route("/upload_receipt/", methods=["POST"]) def upload_receipt(list_id): if "receipt" not in request.files: @@ -961,12 +963,15 @@ def upload_receipt(list_id): list_id=list_id, filename=webp_filename, filesize=filesize, - uploaded_at=uploaded_at + uploaded_at=uploaded_at, ) db.session.add(new_receipt) db.session.commit() - if request.is_json or request.headers.get("X-Requested-With") == "XMLHttpRequest": + 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}) @@ -977,7 +982,6 @@ def upload_receipt(list_id): return _receipt_error("Niedozwolony format pliku") - @app.route("/uploads/") def uploaded_file(filename): response = send_from_directory(app.config["UPLOAD_FOLDER"], filename) @@ -1198,6 +1202,7 @@ def delete_user(user_id): flash("Użytkownik usunięty", "success") return redirect(url_for("list_users")) + @app.route("/admin/receipts/") @login_required @admin_required @@ -1250,7 +1255,6 @@ def delete_receipt(filename): return redirect(next_url or url_for("admin_receipts", id="all")) - @app.route("/admin/delete_selected_lists", methods=["POST"]) @login_required @admin_required @@ -1286,7 +1290,11 @@ def edit_list(list_id): db.session.query(Item).filter_by(list_id=list_id).order_by(Item.id.desc()).all() ) - receipts = Receipt.query.filter_by(list_id=list_id).order_by(Receipt.uploaded_at.desc()).all() + 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/config.py b/config.py index cfc93a2..4bbb4bd 100644 --- a/config.py +++ b/config.py @@ -1,14 +1,15 @@ import os + class Config: - SECRET_KEY = os.environ.get('SECRET_KEY', 'D8pceNZ8q%YR7^7F&9wAC2') - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///shopping.db') + SECRET_KEY = os.environ.get("SECRET_KEY", "D8pceNZ8q%YR7^7F&9wAC2") + SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL", "sqlite:///shopping.db") SQLALCHEMY_TRACK_MODIFICATIONS = False - SYSTEM_PASSWORD = os.environ.get('SYSTEM_PASSWORD', 'admin') - DEFAULT_ADMIN_USERNAME = os.environ.get('DEFAULT_ADMIN_USERNAME', 'admin') - DEFAULT_ADMIN_PASSWORD = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123') - UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', 'uploads') - AUTHORIZED_COOKIE_VALUE = os.environ.get('AUTHORIZED_COOKIE_VALUE', 'cookievalue') - AUTH_COOKIE_MAX_AGE = int(os.environ.get('AUTH_COOKIE_MAX_AGE', 86400)) - HEALTHCHECK_TOKEN = os.environ.get('HEALTHCHECK_TOKEN', 'alamapsaikota1234') - SESSION_TIMEOUT_MINUTES = int(os.environ.get('SESSION_TIMEOUT_MINUTES', 10080)) \ No newline at end of file + SYSTEM_PASSWORD = os.environ.get("SYSTEM_PASSWORD", "admin") + DEFAULT_ADMIN_USERNAME = os.environ.get("DEFAULT_ADMIN_USERNAME", "admin") + DEFAULT_ADMIN_PASSWORD = os.environ.get("DEFAULT_ADMIN_PASSWORD", "admin123") + UPLOAD_FOLDER = os.environ.get("UPLOAD_FOLDER", "uploads") + AUTHORIZED_COOKIE_VALUE = os.environ.get("AUTHORIZED_COOKIE_VALUE", "cookievalue") + AUTH_COOKIE_MAX_AGE = int(os.environ.get("AUTH_COOKIE_MAX_AGE", 86400)) + HEALTHCHECK_TOKEN = os.environ.get("HEALTHCHECK_TOKEN", "alamapsaikota1234") + SESSION_TIMEOUT_MINUTES = int(os.environ.get("SESSION_TIMEOUT_MINUTES", 10080)) diff --git a/migrate_to_webp.py b/migrate_to_webp.py index bfad307..2f0d1f5 100644 --- a/migrate_to_webp.py +++ b/migrate_to_webp.py @@ -6,6 +6,7 @@ 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") @@ -15,6 +16,7 @@ def convert_to_webp(input_path, output_path): print(f"Błąd konwersji {input_path}: {e}") return False + def extract_list_id(filename): if filename.startswith("list_"): parts = filename.split("_", 2) @@ -22,6 +24,7 @@ def extract_list_id(filename): return int(parts[1]) return None + def migrate(): global UPLOAD_FOLDER with app.app_context(): @@ -53,13 +56,19 @@ def migrate(): 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() + 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()) + 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") @@ -69,5 +78,6 @@ def migrate(): print(f"Pominięte (webp istniało): {skipped}") print(f"Duplikaty w bazie: {existing}") + if __name__ == "__main__": migrate() diff --git a/update_missing_image_data.py b/update_missing_image_data.py index 6a19c8c..6ac6847 100644 --- a/update_missing_image_data.py +++ b/update_missing_image_data.py @@ -2,13 +2,16 @@ import os from datetime import datetime from app import app, db, Receipt + def update_missing_receipt_fields(): with app.app_context(): folder = app.config["UPLOAD_FOLDER"] updated = 0 receipts = Receipt.query.filter( - (Receipt.filesize == None) | (Receipt.filesize == 0) | (Receipt.uploaded_at == None) + (Receipt.filesize == None) + | (Receipt.filesize == 0) + | (Receipt.uploaded_at == None) ).all() for r in receipts: @@ -36,5 +39,6 @@ def update_missing_receipt_fields(): db.session.commit() print(f"\nZaktualizowano {updated} rekordów.") + if __name__ == "__main__": update_missing_receipt_fields()