webp support
This commit is contained in:
@@ -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
136
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/<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
73
migrate_to_webp.py
Normal 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
42
receitp_to_list.py
Normal 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()
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user