listy, i inne funkcje
This commit is contained in:
268
app.py
268
app.py
@@ -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
|
||||
|
Reference in New Issue
Block a user