listy, i inne funkcje

This commit is contained in:
Mateusz Gruszczyński
2025-09-22 14:01:57 +02:00
parent 0b221696d4
commit 1a423a8b92
8 changed files with 877 additions and 64 deletions

268
app.py
View File

@@ -9,9 +9,9 @@ from flask_login import (
UserMixin,
)
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
from datetime import datetime, timezone
from markupsafe import Markup
from sqlalchemy import event
from sqlalchemy import event, Numeric
from sqlalchemy.engine import Engine
from decimal import Decimal, InvalidOperation
import markdown as md
@@ -28,9 +28,16 @@ db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = "zaloguj"
try:
from zoneinfo import ZoneInfo # Python 3.9+
except ImportError:
from backports.zoneinfo import ZoneInfo
LOCAL_TZ = ZoneInfo("Europe/Warsaw")
# MODELE
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
@@ -50,8 +57,8 @@ class Zbiorka(db.Model):
opis = db.Column(db.Text, nullable=False)
numer_konta = db.Column(db.String(50), nullable=False)
numer_telefonu_blik = db.Column(db.String(50), nullable=False)
cel = db.Column(db.Float, nullable=False, default=0.0)
stan = db.Column(db.Float, default=0.0)
cel = db.Column(Numeric(12, 2), nullable=False, default=0)
stan = db.Column(Numeric(12, 2), default=0)
ukryta = db.Column(db.Boolean, default=False)
ukryj_kwote = db.Column(db.Boolean, default=False)
zrealizowana = db.Column(db.Boolean, default=False)
@@ -74,6 +81,26 @@ class Zbiorka(db.Model):
passive_deletes=True,
)
przedmioty = db.relationship(
"Przedmiot",
backref="zbiorka",
lazy=True,
order_by="Przedmiot.id.asc()",
cascade="all, delete-orphan",
passive_deletes=True,
)
class Przedmiot(db.Model):
id = db.Column(db.Integer, primary_key=True)
zbiorka_id = db.Column(
db.Integer,
db.ForeignKey("zbiorka.id", ondelete="CASCADE"),
nullable=False,
)
nazwa = db.Column(db.String(120), nullable=False)
link = db.Column(db.String(255), nullable=True)
cena = db.Column(Numeric(12, 2), nullable=True)
kupione = db.Column(db.Boolean, default=False)
class Wplata(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -82,7 +109,7 @@ class Wplata(db.Model):
db.ForeignKey("zbiorka.id", ondelete="CASCADE"),
nullable=False,
)
kwota = db.Column(db.Float, nullable=False)
kwota = db.Column(Numeric(12, 2), nullable=False)
data = db.Column(db.DateTime, default=datetime.utcnow)
opis = db.Column(db.Text, nullable=True)
@@ -96,7 +123,7 @@ class Wydatek(db.Model):
db.ForeignKey("zbiorka.id", ondelete="CASCADE"),
nullable=False,
)
kwota = db.Column(db.Float, nullable=False)
kwota = db.Column(Numeric(12, 2), nullable=False)
data = db.Column(db.DateTime, default=datetime.utcnow)
opis = db.Column(db.Text, nullable=True)
@@ -109,7 +136,6 @@ class GlobalSettings(db.Model):
logo_url = db.Column(db.String(255), nullable=True)
site_title = db.Column(db.String(120), nullable=True)
show_logo_in_navbar = db.Column(db.Boolean, default=False)
show_logo_in_navbar = db.Column(db.Boolean, default=False)
navbar_brand_mode = db.Column(db.String(10), default="text")
footer_brand_mode = db.Column(db.String(10), default="text")
footer_text = db.Column(db.String(200), nullable=True)
@@ -172,7 +198,23 @@ def is_allowed_ip(remote_ip, allowed_hosts_str):
return remote_ip in allowed_ips
# Dodaj filtr Markdown pozwala na zagnieżdżanie linków i obrazków w opisie
def to_local(dt):
if dt is None:
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(LOCAL_TZ)
@app.template_filter("dt")
def dt_filter(dt, fmt="%Y-%m-%d %H:%M"):
try:
ldt = to_local(dt)
return ldt.strftime(fmt) if ldt else ""
except Exception:
return ""
@app.template_filter("markdown")
def markdown_filter(text):
return Markup(md.markdown(text))
@@ -200,7 +242,7 @@ def index():
return render_template("index.html", zbiorki=zbiorki)
@app.route("/zbiorki_zrealizowane")
@app.route("/zrealizowane")
def zbiorki_zrealizowane():
zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all()
return render_template("index.html", zbiorki=zbiorki)
@@ -297,8 +339,6 @@ def zarejestruj():
# PANEL ADMINISTRACYJNY
@app.route("/admin")
@login_required
def admin_dashboard():
@@ -336,7 +376,7 @@ def formularz_zbiorek(zbiorka_id=None):
numer_konta = request.form.get("numer_konta", "").strip()
numer_telefonu_blik = request.form.get("numer_telefonu_blik", "").strip()
# Cel — walidacja liczby
# Cel — walidacja liczby (Decimal, nie float)
try:
cel_str = request.form.get("cel", "").replace(",", ".").strip()
cel = Decimal(cel_str)
@@ -361,27 +401,85 @@ def formularz_zbiorek(zbiorka_id=None):
ukryj_kwote = "ukryj_kwote" in request.form
names = request.form.getlist("item_nazwa[]")
links = request.form.getlist("item_link[]")
prices = request.form.getlist("item_cena[]")
def _read_price(val):
"""Zwraca Decimal(>=0) albo None; akceptuje przecinek jako separator dziesiętny."""
if not val or not val.strip():
return None
try:
d = Decimal(val.replace(",", "."))
if d < 0:
return None
return d
except Exception:
return None
# --- ZAPIS ZBIÓRKI + PRODUKTÓW ---
if is_edit:
# Aktualizacja istniejącej
# Aktualizacja istniejącej zbiórki
zb.nazwa = nazwa
zb.opis = opis
zb.numer_konta = numer_konta
zb.numer_telefonu_blik = numer_telefonu_blik
zb.cel = float(cel) # jeśli masz Decimal w modelu, przypisz bez konwersji
zb.cel = cel # ❗ bez float(cel) — zostaje Decimal
zb.ukryj_kwote = ukryj_kwote
db.session.commit() # najpierw zapisz bazowe pola
# Nadpisz listę produktów (czyść i dodaj od nowa dla prostoty)
zb.przedmioty.clear()
for i, raw_name in enumerate(names):
name = (raw_name or "").strip()
if not name:
continue
link = (links[i] if i < len(links) else "").strip() or None
cena_val = _read_price(prices[i] if i < len(prices) else "")
kupione_val = request.form.get(f"item_kupione_val_{i}") == "1"
db.session.add(Przedmiot(
zbiorka_id=zb.id,
nazwa=name,
link=link,
cena=cena_val, # Decimal albo None
kupione=kupione_val
))
db.session.commit()
flash("Zbiórka została zaktualizowana", "success")
else:
# Utworzenie nowej
# Utworzenie nowej zbiórki
nowa = Zbiorka(
nazwa=nazwa,
opis=opis,
numer_konta=numer_konta,
numer_telefonu_blik=numer_telefonu_blik,
cel=float(cel),
cel=cel, # ❗ Decimal
ukryj_kwote=ukryj_kwote,
)
db.session.add(nowa)
db.session.commit() # potrzebujemy ID nowej zbiórki
# Dodaj produkty do nowej zbiórki
for i, raw_name in enumerate(names):
name = (raw_name or "").strip()
if not name:
continue
link = (links[i] if i < len(links) else "").strip() or None
cena_val = _read_price(prices[i] if i < len(prices) else "")
kupione_val = request.form.get(f"item_kupione_val_{i}") == "1"
db.session.add(Przedmiot(
zbiorka_id=nowa.id,
nazwa=name,
link=link,
cena=cena_val, # Decimal albo None
kupione=kupione_val
))
db.session.commit()
flash("Zbiórka została dodana", "success")
@@ -393,24 +491,33 @@ def formularz_zbiorek(zbiorka_id=None):
)
# TRASA DODAWANIA WPŁATY Z OPISEM
# TRASA DODAWANIA WPŁATY W PANELU ADMINA
@app.route("/admin/zbiorka/<int:zbiorka_id>/wplata/dodaj", methods=["GET", "POST"])
@login_required
def dodaj_wplate(zbiorka_id):
if not current_user.is_admin:
flash("Brak uprawnień", "danger")
return redirect(url_for("index"))
zb = Zbiorka.query.get_or_404(zbiorka_id)
if request.method == "POST":
kwota = float(request.form["kwota"])
try:
kwota = Decimal(request.form.get("kwota", "").replace(",", "."))
if kwota <= 0:
raise InvalidOperation
except (InvalidOperation, ValueError):
flash("Nieprawidłowa kwota (musi być > 0)", "danger")
return redirect(url_for("dodaj_wplate", zbiorka_id=zbiorka_id))
opis = request.form.get("opis", "")
nowa_wplata = Wplata(zbiorka_id=zb.id, kwota=kwota, opis=opis)
zb.stan += kwota # Aktualizacja stanu zbiórki
zb.stan = (zb.stan or Decimal("0")) + kwota
db.session.add(nowa_wplata)
db.session.commit()
flash("Wpłata została dodana", "success")
return redirect(url_for("admin_dashboard"))
next_url = request.args.get("next")
return redirect(next_url or url_for("transakcje_zbiorki", zbiorka_id=zb.id))
return render_template("admin/dodaj_wplate.html", zbiorka=zb)
@@ -436,8 +543,8 @@ def edytuj_stan(zbiorka_id):
zb = Zbiorka.query.get_or_404(zbiorka_id)
if request.method == "POST":
try:
nowy_stan = float(request.form["stan"])
except ValueError:
nowy_stan = Decimal(request.form.get("stan", "").replace(",", "."))
except (InvalidOperation, ValueError):
flash("Nieprawidłowa wartość kwoty", "danger")
return redirect(url_for("edytuj_stan", zbiorka_id=zbiorka_id))
zb.stan = nowy_stan
@@ -469,8 +576,6 @@ def create_admin_account():
db.session.commit()
from flask import request
@app.after_request
def apply_headers(response):
@@ -588,22 +693,28 @@ def dodaj_wydatek(zbiorka_id):
if not current_user.is_admin:
flash("Brak uprawnień", "danger")
return redirect(url_for("index"))
zb = Zbiorka.query.get_or_404(zbiorka_id)
if request.method == "POST":
try:
kwota = float(request.form["kwota"])
kwota = Decimal(request.form.get("kwota", "").replace(",", "."))
if kwota <= 0:
raise ValueError
except (KeyError, ValueError):
flash("Nieprawidłowa kwota", "danger")
raise InvalidOperation
except (InvalidOperation, ValueError):
flash("Nieprawidłowa kwota (musi być > 0)", "danger")
return redirect(url_for("dodaj_wydatek", zbiorka_id=zbiorka_id))
opis = request.form.get("opis", "")
nowy_wydatek = Wydatek(zbiorka_id=zb.id, kwota=kwota, opis=opis)
zb.stan -= kwota
zb.stan = (zb.stan or Decimal("0")) - kwota
db.session.add(nowy_wydatek)
db.session.commit()
flash("Wydatek został dodany", "success")
return redirect(url_for("admin_dashboard"))
next_url = request.args.get("next")
return redirect(next_url or url_for("transakcje_zbiorki", zbiorka_id=zb.id))
return render_template("admin/dodaj_wydatek.html", zbiorka=zb)
@@ -646,6 +757,97 @@ def robots():
return robots_txt, 200, {"Content-Type": "text/plain"}
@app.route("/admin/zbiorka/<int:zbiorka_id>/transakcje")
@login_required
def transakcje_zbiorki(zbiorka_id):
if not current_user.is_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
zb = Zbiorka.query.get_or_404(zbiorka_id)
aktywnosci = (
[{"typ": "wpłata", "id": w.id, "kwota": w.kwota, "opis": w.opis, "data": w.data} for w in zb.wplaty] +
[{"typ": "wydatek","id": x.id, "kwota": x.kwota,"opis": x.opis,"data": x.data} for x in zb.wydatki]
)
aktywnosci.sort(key=lambda a: a["data"], reverse=True)
return render_template("admin/transakcje.html", zbiorka=zb, aktywnosci=aktywnosci)
@app.route("/admin/wplata/<int:wplata_id>/zapisz", methods=["POST"])
@login_required
def zapisz_wplate(wplata_id):
if not current_user.is_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
w = Wplata.query.get_or_404(wplata_id)
zb = w.zbiorka
try:
nowa_kwota = Decimal(request.form.get("kwota", "").replace(",", "."))
if nowa_kwota <= 0:
raise InvalidOperation
except (InvalidOperation, ValueError):
flash("Nieprawidłowa kwota (musi być > 0)", "danger")
return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id))
delta = nowa_kwota - (w.kwota or Decimal("0"))
w.kwota = nowa_kwota
w.opis = request.form.get("opis", "")
zb.stan = (zb.stan or Decimal("0")) + delta
db.session.commit()
flash("Wpłata zaktualizowana", "success")
return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id))
@app.route("/admin/wplata/<int:wplata_id>/usun", methods=["POST"])
@login_required
def usun_wplate(wplata_id):
if not current_user.is_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
w = Wplata.query.get_or_404(wplata_id)
zb = w.zbiorka
zb.stan -= w.kwota
db.session.delete(w)
db.session.commit()
flash("Wpłata usunięta", "success")
return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id))
@app.route("/admin/wydatek/<int:wydatek_id>/zapisz", methods=["POST"])
@login_required
def zapisz_wydatek(wydatek_id):
if not current_user.is_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
x = Wydatek.query.get_or_404(wydatek_id)
zb = x.zbiorka
try:
nowa_kwota = Decimal(request.form.get("kwota", "").replace(",", "."))
if nowa_kwota <= 0:
raise InvalidOperation
except (InvalidOperation, ValueError):
flash("Nieprawidłowa kwota (musi być > 0)", "danger")
return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id))
delta = nowa_kwota - (x.kwota or Decimal("0"))
x.kwota = nowa_kwota
x.opis = request.form.get("opis", "")
# wydatki zmniejszają stan; jeżeli delta>0, stan spada bardziej
zb.stan = (zb.stan or Decimal("0")) - delta
db.session.commit()
flash("Wydatek zaktualizowany", "success")
return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id))
@app.route("/admin/wydatek/<int:wydatek_id>/usun", methods=["POST"])
@login_required
def usun_wydatek(wydatek_id):
if not current_user.is_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
x = Wydatek.query.get_or_404(wydatek_id)
zb = x.zbiorka
zb.stan += x.kwota
db.session.delete(x)
db.session.commit()
flash("Wydatek usunięty", "success")
return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id))
@app.route("/favicon.ico")
def favicon():
return "", 204