zmiany ux i kodowe

This commit is contained in:
Mateusz Gruszczyński
2025-09-23 10:25:11 +02:00
parent 1a423a8b92
commit bbe318f995
12 changed files with 205 additions and 71 deletions

114
app.py
View File

@@ -1,3 +1,7 @@
import markdown as md
import hashlib, os
import re
import socket
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from flask_login import (
@@ -11,29 +15,42 @@ from flask_login import (
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime, timezone
from markupsafe import Markup
from sqlalchemy import event, Numeric
from sqlalchemy import event, Numeric, select
from sqlalchemy.engine import Engine
from decimal import Decimal, InvalidOperation
import markdown as md
from flask import request, flash, abort
import os
import re
import socket
try:
from zoneinfo import ZoneInfo # Python 3.9+
except ImportError:
from backports.zoneinfo import ZoneInfo
def build_fingerprint(paths):
h = hashlib.sha256()
for base in paths:
if not os.path.exists(base):
continue
for root, _, files in os.walk(base):
for f in sorted(files):
p = os.path.join(root, f)
try:
with open(p, "rb") as fh:
h.update(fh.read())
except Exception:
continue
return h.hexdigest()[:8]
APP_VERSION = f"{datetime.now():%Y.%m.%d}+{build_fingerprint(['templates','static','app.py'])}"
app = Flask(__name__)
app.config['APP_VERSION'] = APP_VERSION
# Ładujemy konfigurację z pliku config.py
app.config.from_object("config.Config")
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")
@@ -143,7 +160,7 @@ class GlobalSettings(db.Model):
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
return db.session.get(User, int(user_id))
@event.listens_for(Engine, "connect")
@@ -235,6 +252,11 @@ def inject_globals():
}
@app.context_processor
def inject_version():
return {'APP_VERSION': app.config['APP_VERSION']}
# TRASY PUBLICZNE
@app.route("/")
def index():
@@ -255,7 +277,9 @@ def page_not_found(e):
@app.route("/zbiorka/<int:zbiorka_id>")
def zbiorka(zbiorka_id):
zb = Zbiorka.query.get_or_404(zbiorka_id)
zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None:
abort(404)
if zb.ukryta and (not current_user.is_authenticated or not current_user.is_admin):
abort(404)
@@ -364,7 +388,9 @@ def formularz_zbiorek(zbiorka_id=None):
# Tryb
is_edit = zbiorka_id is not None
zb = Zbiorka.query.get_or_404(zbiorka_id) if is_edit else None
zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None:
abort(404)
global_settings = GlobalSettings.query.first()
if request.method == "POST":
@@ -498,7 +524,9 @@ def dodaj_wplate(zbiorka_id):
flash("Brak uprawnień", "danger")
return redirect(url_for("index"))
zb = Zbiorka.query.get_or_404(zbiorka_id)
zb = db.session.get(Zbiorka, zbiorka_id) if is_edit else None
if is_edit and not zb:
abort(404)
if request.method == "POST":
try:
@@ -527,7 +555,9 @@ def usun_zbiorka(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)
zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None:
abort(404)
db.session.delete(zb)
db.session.commit()
flash("Zbiórka została usunięta", "success")
@@ -540,7 +570,9 @@ def edytuj_stan(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)
zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None:
abort(404)
if request.method == "POST":
try:
nowy_stan = Decimal(request.form.get("stan", "").replace(",", "."))
@@ -560,7 +592,9 @@ def zmien_widzialnosc(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)
zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None:
abort(404)
zb.ukryta = not zb.ukryta
db.session.commit()
flash("Zbiórka została " + ("ukryta" if zb.ukryta else "przywrócona"), "success")
@@ -579,7 +613,6 @@ def create_admin_account():
@app.after_request
def apply_headers(response):
# --- STATIC: jak wcześniej ---
if request.path.startswith("/static/"):
response.headers.pop("Content-Disposition", None)
response.headers["Vary"] = "Accept-Encoding"
@@ -616,7 +649,7 @@ def apply_headers(response):
response.headers.pop("Vary", None)
else:
response.headers["Vary"] = "Cookie, Accept-Encoding"
default_cache = app.config.get("CACHE_CONTROL_HEADER") or "private, max-age=0"
default_cache = app.config.get("CACHE_CONTROL_HEADER") or "private, no-store"
response.headers["Cache-Control"] = default_cache
if (
@@ -694,7 +727,9 @@ def dodaj_wydatek(zbiorka_id):
flash("Brak uprawnień", "danger")
return redirect(url_for("index"))
zb = Zbiorka.query.get_or_404(zbiorka_id)
zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None:
abort(404)
if request.method == "POST":
try:
@@ -734,7 +769,9 @@ def oznacz_zbiorka(zbiorka_id):
flash("Brak uprawnień do wykonania tej operacji", "danger")
return redirect(url_for("index"))
zb = Zbiorka.query.get_or_404(zbiorka_id)
zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None:
abort(404)
if "niezrealizowana" in request.path:
zb.zrealizowana = False
@@ -762,7 +799,9 @@ def robots():
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)
zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None:
abort(404)
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]
@@ -776,7 +815,9 @@ def transakcje_zbiorki(zbiorka_id):
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)
w = db.session.get(Wplata, wplata_id)
if w is None:
abort(404)
zb = w.zbiorka
try:
nowa_kwota = Decimal(request.form.get("kwota", "").replace(",", "."))
@@ -800,7 +841,9 @@ def zapisz_wplate(wplata_id):
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)
w = db.session.get(Wplata, wplata_id)
if w is None:
abort(404)
zb = w.zbiorka
zb.stan -= w.kwota
db.session.delete(w)
@@ -814,7 +857,9 @@ def usun_wplate(wplata_id):
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)
x = db.session.get(Wydatek, wydatek_id)
if x is None:
abort(404)
zb = x.zbiorka
try:
nowa_kwota = Decimal(request.form.get("kwota", "").replace(",", "."))
@@ -839,7 +884,9 @@ def zapisz_wydatek(wydatek_id):
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)
x = db.session.get(Wydatek, wydatek_id)
if x is None:
abort(404)
zb = x.zbiorka
zb.stan += x.kwota
db.session.delete(x)
@@ -867,9 +914,16 @@ if __name__ == "__main__":
with app.app_context():
db.create_all()
# Tworzenie konta głównego admina, jeśli nie istnieje
if not User.query.filter_by(is_admin=True).first():
main_admin = User(username=app.config["MAIN_ADMIN_USERNAME"], is_admin=True)
stmt = select(User).filter_by(is_admin=True)
admin = db.session.execute(stmt).scalars().first()
if not admin:
main_admin = User(
username=app.config["MAIN_ADMIN_USERNAME"],
is_admin=True
)
main_admin.set_password(app.config["MAIN_ADMIN_PASSWORD"])
db.session.add(main_admin)
db.session.commit()
app.run(debug=True)

View File

@@ -14,11 +14,26 @@
<h3 class="card-title mb-0">Dodaj wpłatę: <span class="fw-semibold">{{ zbiorka.nazwa }}</span></h3>
<div class="d-flex align-items-center gap-2">
{% if zbiorka.cel %}
<span class="badge bg-dark border" style="border-color: var(--border);">Cel: {{ zbiorka.cel|round(2) }}
PLN</span>
<span class="badge bg-dark border" style="border-color: var(--border);">
Cel: {{ zbiorka.cel|round(2) }} PLN
</span>
{% endif %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Stan: {{ zbiorka.stan|round(2) }} PLN
</span>
{% if zbiorka.cel and zbiorka.cel > 0 %}
{% set delta = zbiorka.cel - zbiorka.stan %}
{% if delta > 0 %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Brakuje: {{ delta|round(2) }} PLN
</span>
{% else %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Nadwyżka: {{ (-delta)|round(2) }} PLN
</span>
{% endif %}
{% endif %}
<span class="badge bg-dark border" style="border-color: var(--border);">Stan: {{ zbiorka.stan|round(2) }}
PLN</span>
</div>
</div>
@@ -82,5 +97,5 @@
{% endblock %}
{% block extra_scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/dodaj_wplate.js') }}"></script>
<script src="{{ url_for('static', filename='js/dodaj_wplate.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -13,14 +13,30 @@
<div
class="card-header bg-secondary text-white d-flex flex-wrap align-items-center justify-content-between gap-2">
<h3 class="card-title mb-0">Dodaj wydatek: <span class="fw-semibold">{{ zbiorka.nazwa }}</span></h3>
<div class="d-flex align-items-center gap-2">
{% if zbiorka.cel %}
<span class="badge bg-dark border" style="border-color: var(--border);">Cel: {{ zbiorka.cel|round(2) }}
PLN</span>
<div class="d-flex align-items-center flex-wrap gap-2">
{% if has_cel %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Cel: {{ zbiorka.cel|round(2) }} PLN
</span>
{% endif %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Obecnie: {{ zbiorka.stan|round(2) }} PLN
</span>
{% if has_cel %}
{% set delta = zbiorka.cel - zbiorka.stan %}
{% if delta > 0 %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Brakuje: {{ delta|round(2) }} PLN
</span>
{% else %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Nadwyżka: {{ (-delta)|round(2) }} PLN
</span>
{% endif %}
{% endif %}
<span class="badge bg-dark border" style="border-color: var(--border);">Stan: {{ zbiorka.stan|round(2)
}} PLN</span>
</div>
</div>
<div class="card-body">
@@ -59,5 +75,5 @@
{% block extra_scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/dodaj_wydatek.js') }}"></script>
<script src="{{ url_for('static', filename='js/dodaj_wydatek.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -20,11 +20,26 @@
<h3 class="card-title mb-0">Edytuj stan: <span class="fw-semibold">{{ zbiorka.nazwa }}</span></h3>
<div class="d-flex align-items-center flex-wrap gap-2">
{% if has_cel %}
<span class="badge bg-dark border" style="border-color: var(--border);">Cel: {{ zbiorka.cel|round(2) }}
PLN</span>
<span class="badge bg-dark border" style="border-color: var(--border);">
Cel: {{ zbiorka.cel|round(2) }} PLN
</span>
{% endif %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Obecnie: {{ zbiorka.stan|round(2) }} PLN
</span>
{% if has_cel %}
{% set delta = zbiorka.cel - zbiorka.stan %}
{% if delta > 0 %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Brakuje: {{ delta|round(2) }} PLN
</span>
{% else %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Nadwyżka: {{ (-delta)|round(2) }} PLN
</span>
{% endif %}
{% endif %}
<span class="badge bg-dark border" style="border-color: var(--border);">Obecnie: {{ zbiorka.stan|round(2) }}
PLN</span>
</div>
</div>
@@ -124,5 +139,5 @@
{% block extra_scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/edytuj_stan.js') }}"></script>
<script src="{{ url_for('static', filename='js/edytuj_stan.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -37,8 +37,27 @@
Cel: {{ zbiorka.cel|round(2) }} PLN
</span>
{% endif %}
{% if not zbiorka.ukryj_kwote %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Stan: {{ zbiorka.stan|round(2) }} PLN
</span>
{% if zbiorka.cel %}
{% set delta = zbiorka.cel - zbiorka.stan %}
{% if delta > 0 %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Brakuje: {{ delta|round(2) }} PLN
</span>
{% elif delta < 0 %} <span class="badge bg-dark border" style="border-color: var(--border);">
Nadwyżka: {{ (-delta)|round(2) }} PLN
</span>
{% endif %}
{% endif %}
{% endif %}
{% if zbiorka.ukryj_kwote %}
<span class="badge bg-secondary">Kwoty ukryte</span>
<span class="badge bg-secondary">Kwoty niepubliczne</span>
{% else %}
<span class="badge bg-success">Kwoty widoczne</span>
{% endif %}
@@ -46,6 +65,7 @@
{% else %}
<small class="opacity-75">Uzupełnij podstawowe dane i dane płatności</small>
{% endif %}
</div>
<div class="card-body">
@@ -82,10 +102,10 @@
<!-- SEKCJA: Lista produktów -->
<div class="mb-4">
<h6 class="text-muted mb-2">Lista produktów</h6>
<h6 class="text-muted mb-2">Lista produktów / Pozycje / Cele</h6>
<p class="text-muted small mb-3">
Wypunktuj dokładnie produkty do zakupu — podaj nazwę, opcjonalny link do sklepu i cenę.
Status domyślnie <em>Do kupienia</em>; przełącz na <em>Kupione</em> po realizacji.
Status domyślnie <code>Do kupienia</code>; przełącz na <code>Kupione</code> po realizacji.
</p>
<div class="table-responsive">
@@ -221,7 +241,6 @@
<div class="mb-4">
<h6 class="text-muted mb-2">Cel i widoczność</h6>
{# === BOX: zgodność sumy produktów z celem === #}
<div id="celSyncBox" class="alert d-none py-2 px-3 mb-3" role="alert">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<div id="celSyncMsg" class="small"></div>
@@ -268,8 +287,8 @@
{% block extra_scripts %}
{{ super() }}
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<script src="{{ url_for('static', filename='js/mde_custom.js') }}"></script>
<script src="{{ url_for('static', filename='js/formularz_zbiorek.js') }}"></script>
<script src="{{ url_for('static', filename='js/produkty_formularz.js') }}"></script>
<script src="{{ url_for('static', filename='js/kwoty_formularz.js') }}"></script>
<script src="{{ url_for('static', filename='js/mde_custom.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static', filename='js/formularz_zbiorek.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static', filename='js/produkty_formularz.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static', filename='js/kwoty_formularz.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -139,5 +139,5 @@
{% block extra_scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/transakcje.js') }}"></script>
<script src="{{ url_for('static', filename='js/transakcje.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -162,5 +162,5 @@
{% block extra_scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/ustawienia.js') }}"></script>
<script src="{{ url_for('static', filename='js/ustawienia.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>{% block title %}Aplikacja Zbiórek{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.0/dist/darkly/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}?v={{ APP_VERSION }}" />
{% block extra_head %}{% endblock %}
</head>
@@ -80,10 +80,12 @@
{{ global_settings.footer_text if global_settings and global_settings.footer_text else "© " ~ (now().year if now
else '2025') ~ " linuxiarz.pl" }}
{% endif %}
<div class="small text-muted">v{{ APP_VERSION }}</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/progress.js') }}"></script>
<script src="{{ url_for('static', filename='js/progress.js') }}?v={{ APP_VERSION }}"></script>
{% block extra_scripts %}{% endblock %}
</body>

View File

@@ -49,11 +49,24 @@ zbiórki{% endif %}{% endblock %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Stan: {{ z.stan|round(2) }} PLN
</span>
{% if z.cel > 0 %}
{% set delta = z.cel - z.stan %}
{% if delta > 0 %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Brakuje: {{ delta|round(2) }} PLN
</span>
{% elif delta < 0 %} <span class="badge bg-dark border" style="border-color: var(--border);">
Nadwyżka: {{ (-delta)|round(2) }} PLN
</span>
{% endif %}
{% endif %}
{% else %}
<span class="badge bg-secondary">Kwoty ukryte</span>
<span class="badge bg-secondary">Kwoty niepubliczne</span>
{% endif %}
</div>
<div class="mb-1">
<div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100"
aria-valuenow="{{ progress_clamped|round(2) if not z.ukryj_kwote else '' }}"

View File

@@ -51,5 +51,5 @@
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', filename='js/walidacja_logowanie.js') }}"></script>
<script src="{{ url_for('static', filename='js/walidacja_logowanie.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -57,5 +57,5 @@
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', filename='js/walidacja_rejestracja.js') }}"></script>
<script src="{{ url_for('static', filename='js/walidacja_rejestracja.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -18,7 +18,7 @@
<span class="badge rounded-pill" style="background: var(--accent); color:#111;">Zrealizowana</span>
{% endif %}
{% if zbiorka.ukryj_kwote %}
<span class="badge bg-secondary">Kwoty ukryte</span>
<span class="badge bg-secondary">Kwoty niepubliczne</span>
{% else %}
<span class="badge bg-success">Kwoty widoczne</span>
{% endif %}
@@ -264,6 +264,6 @@
{% block extra_scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/zbiorka.js') }}"></script>
<script src="{{ url_for('static', filename='js/progress.js') }}"></script>
<script src="{{ url_for('static', filename='js/zbiorka.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static', filename='js/progress.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}