webp support

This commit is contained in:
Mateusz Gruszczyński
2025-07-20 16:50:26 +02:00
parent 1f609b6dba
commit 470cd32745
6 changed files with 222 additions and 88 deletions

View File

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

136
app.py
View File

@@ -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/<int:list_id>", 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/<filename>")
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/<id>")
@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/<filename>")
@@ -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")

73
migrate_to_webp.py Normal file
View File

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

42
receitp_to_list.py Normal file
View File

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

View File

@@ -192,22 +192,18 @@
</div>
<div class="row g-3">
{% 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 %}
<div class="col-6 col-md-4 col-lg-3">
<div class="card bg-dark text-white h-100">
<a href="{{ url_for('uploaded_file', filename=img) }}" data-lightbox="receipts" data-title="{{ img }}"
class="glightbox">
<img src="{{ url_for('uploaded_file', filename=img) }}" class="card-img-top"
<a href="{{ url_for('uploaded_file', filename=r.filename) }}" data-lightbox="receipts"
data-title="{{ r.filename }}" class="glightbox">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}" class="card-img-top"
style="object-fit: cover; height: 200px;">
</a>
<div class="card-body text-center">
<p class="small text-truncate mb-1">{{ img }}</p>
<p class="small mb-1">Rozmiar: {{ file_size }}</p>
<p class="small mb-1">Wgrano: {{ upload_time.strftime('%Y-%m-%d %H:%M') }}</p>
<a href="{{ url_for('delete_receipt', filename=img) }}?next={{ url_for('edit_list', list_id=list.id) }}"
<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>
<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>

View File

@@ -10,36 +10,34 @@
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<div class="row g-3">
{% 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 %}
<div class="col-6 col-md-4 col-lg-3">
<div class="card bg-dark text-white h-100">
<a href="{{ url_for('uploaded_file', filename=img) }}" class="glightbox" data-gallery="receipts"
data-title="{{ img }}">
<img src="{{ url_for('uploaded_file', filename=img) }}" class="card-img-top"
<a href="{{ url_for('uploaded_file', filename=r.filename) }}" class="glightbox" data-gallery="receipts"
data-title="{{ r.filename }}">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}" class="card-img-top"
style="object-fit: cover; height: 200px;">
</a>
<div class="card-body text-center">
<p class="small text-truncate mb-1">{{ img }}</p>
<p class="small mb-1">Rozmiar: {{ file_size }}</p>
<p class="small mb-1">Wgrano: {{ upload_time.strftime('%Y-%m-%d %H:%M') }}</p>
{% if list_id %}
<a href="{{ url_for('edit_list', list_id=list_id|int) }}" class="btn btn-sm btn-outline-light w-100 mb-2">✏️
Edytuj listę #{{ list_id }}</a>
<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 %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB</p>
{% else %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB</p>
{% endif %}
<a href="{{ url_for('delete_receipt', filename=img) }}?next={{ request.path }}"
<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 }}"
class="btn btn-sm btn-outline-danger w-100">🗑️ Usuń</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% if not image_files %}
{% if not receipts %}
<div class="alert alert-info text-center mt-4" role="alert">
Nie wgrano żadnych paragonów.
</div>