2 Commits

Author SHA1 Message Date
Mateusz Gruszczyński
ae89f55446 webp support 2025-07-20 17:34:53 +02:00
Mateusz Gruszczyński
3ebb364322 webp support 2025-07-20 17:34:21 +02:00
9 changed files with 340 additions and 76 deletions

View File

@@ -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 = []

View File

@@ -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()

View File

@@ -46,3 +46,6 @@ CREATE TABLE receipt (
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (list_id) REFERENCES shopping_list(id)
);
ALTER TABLE receipt ADD COLUMN filesize INTEGER;

43
app.py
View File

@@ -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,13 +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)
filesize = db.Column(db.Integer, nullable=True)
with app.app_context():
@@ -935,6 +936,8 @@ def upload_receipt(list_id):
return redirect(request.referrer) """
from datetime import datetime
@app.route("/upload_receipt/<int:list_id>", methods=["POST"])
def upload_receipt(list_id):
@@ -953,11 +956,22 @@ def upload_receipt(list_id):
save_resized_image(file, file_path)
new_receipt = Receipt(list_id=list_id, filename=webp_filename)
filesize = os.path.getsize(file_path) if os.path.exists(file_path) else None
uploaded_at = datetime.utcnow()
new_receipt = Receipt(
list_id=list_id,
filename=webp_filename,
filesize=filesize,
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})
@@ -967,13 +981,6 @@ def upload_receipt(list_id):
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": message}), 400
flash(message, "danger")
return redirect(request.referrer)
@app.route("/uploads/<filename>")
def uploaded_file(filename):
@@ -1196,8 +1203,6 @@ def delete_user(user_id):
return redirect(url_for("list_users"))
import os
@app.route("/admin/receipts/<id>")
@login_required
@admin_required
@@ -1216,14 +1221,9 @@ def admin_receipts(id):
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)
@app.route("/admin/delete_receipt/<filename>")
@login_required
@admin_required
@@ -1255,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
@@ -1291,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")

View File

@@ -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))
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))

View File

@@ -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()

View File

@@ -202,13 +202,24 @@
</a>
<div class="card-body text-center">
<p class="small text-truncate mb-1">{{ r.filename }}</p>
{% if r.filesize and r.filesize >= 1024 * 1024 %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB</p>
{% elif r.filesize %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB</p>
{% else %}
<p class="small mb-1 text-muted">Rozmiar nieznany</p>
{% endif %}
<p class="small mb-1">Wgrano: {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</p>
<a href="{{ url_for('delete_receipt', filename=r.filename) }}?next={{ url_for('edit_list', list_id=list.id) }}"
class="btn btn-sm btn-outline-danger w-100">🗑️ Usuń</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% if not receipts %}

View File

@@ -21,15 +21,19 @@
<div class="card-body text-center">
<p class="small text-truncate mb-1">{{ r.filename }}</p>
<p class="small mb-1">Wgrano: {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</p>
{% if r.filesize >= 1024 * 1024 %}
{% if r.filesize and r.filesize >= 1024 * 1024 %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB</p>
{% else %}
{% elif r.filesize %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB</p>
{% else %}
<p class="small mb-1 text-muted">Brak danych o rozmiarze</p>
{% endif %}
<a href="{{ url_for('edit_list', list_id=r.list_id) }}" class="btn btn-sm btn-outline-light w-100 mb-2">✏️
Edytuj listę #{{ r.list_id }}</a>
<a href="{{ url_for('delete_receipt', filename=r.filename) }}?next={{ request.path }}"
<a href="{{ url_for('delete_receipt', filename=r.filename|urlencode) }}?next={{ request.path }}"
class="btn btn-sm btn-outline-danger w-100">🗑️ Usuń</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,44 @@
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)
).all()
for r in receipts:
path = os.path.join(folder, r.filename)
if not os.path.exists(path):
print(f"Brak pliku: {r.filename}")
continue
changed = False
if not r.filesize:
r.filesize = os.path.getsize(path)
changed = True
print(f"{r.filename} → filesize: {r.filesize} B")
if not r.uploaded_at:
timestamp = os.path.getmtime(path)
r.uploaded_at = datetime.fromtimestamp(timestamp)
changed = True
print(f"{r.filename} → uploaded_at: {r.uploaded_at}")
if changed:
updated += 1
db.session.commit()
print(f"\nZaktualizowano {updated} rekordów.")
if __name__ == "__main__":
update_missing_receipt_fields()