From bf1c2e2a2907792870185f8085c7fb4b69b274c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 13 Sep 2025 18:14:23 +0200 Subject: [PATCH 01/25] commit1 permissions --- app.py | 314 ++++++++++++++++++++---- static/js/functions.js | 16 +- templates/admin/admin_lists_access.html | 174 +++++++++++++ templates/admin/admin_panel.html | 2 + templates/admin/edit_list.html | 21 ++ templates/edit_my_list.html | 57 +++++ templates/list.html | 21 +- templates/main.html | 33 ++- 8 files changed, 557 insertions(+), 81 deletions(-) create mode 100644 templates/admin/admin_lists_access.html diff --git a/app.py b/app.py index ae84c49..eeb120c 100644 --- a/app.py +++ b/app.py @@ -216,7 +216,7 @@ class ShoppingList(db.Model): expires_at = db.Column(db.DateTime(timezone=True), nullable=True) owner = db.relationship("User", backref="lists", lazy=True) is_archived = db.Column(db.Boolean, default=False) - is_public = db.Column(db.Boolean, default=True) + is_public = db.Column(db.Boolean, default=False) # Relacje items = db.relationship("Item", back_populates="shopping_list", lazy="select") @@ -290,6 +290,30 @@ class Receipt(db.Model): uploaded_by_user = db.relationship("User", backref="uploaded_receipts") +class ListPermission(db.Model): + __tablename__ = "list_permission" + id = db.Column(db.Integer, primary_key=True) + list_id = db.Column( + db.Integer, + db.ForeignKey("shopping_list.id", ondelete="CASCADE"), + nullable=False, + ) + user_id = db.Column( + db.Integer, + db.ForeignKey("user.id", ondelete="CASCADE"), + nullable=False, + ) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + __table_args__ = (db.UniqueConstraint("list_id", "user_id", name="uq_list_user"),) + +ShoppingList.permitted_users = db.relationship( + "User", + secondary="list_permission", + backref=db.backref("permitted_lists", lazy="dynamic"), + lazy="dynamic", +) + + def hash_password(password): pepper = app.config["BCRYPT_PEPPER"] peppered = (password + pepper).encode("utf-8") @@ -538,6 +562,10 @@ def redirect_with_flash( flash(message, category) return redirect(url_for(endpoint)) +def user_permission_subq(user_id): + return db.session.query(ListPermission.list_id).filter( + ListPermission.user_id == user_id + ) def admin_required(f): @wraps(f) @@ -1392,21 +1420,27 @@ def favicon(): @app.route("/") def main_page(): + from sqlalchemy import func, or_, case # upewnij się, że masz te importy na górze pliku + + perm_subq = ( + user_permission_subq(current_user.id) + if current_user.is_authenticated + else None + ) + now = datetime.now(timezone.utc) month_param = request.args.get("m", None) start = end = None if month_param in (None, ""): - # brak wyboru -> domyślnie aktualny miesiąc + # domyślnie: bieżący miesiąc month_str = now.strftime("%Y-%m") start = datetime(now.year, now.month, 1, tzinfo=timezone.utc) end = (start + timedelta(days=31)).replace(day=1) - elif month_param == "all": month_str = "all" start = end = None - else: month_str = month_param try: @@ -1414,16 +1448,10 @@ def main_page(): start = datetime(year, month, 1, tzinfo=timezone.utc) end = (start + timedelta(days=31)).replace(day=1) except ValueError: + # jeśli m ma zły format – pokaż wszystko + month_str = "all" start = end = None - # dalej normalnie używasz date_filter: - def date_filter(query): - if start and end: - query = query.filter( - ShoppingList.created_at >= start, ShoppingList.created_at < end - ) - return query - def date_filter(query): if start and end: query = query.filter( @@ -1434,10 +1462,10 @@ def main_page(): if current_user.is_authenticated: user_lists = ( date_filter( - ShoppingList.query.filter_by( - owner_id=current_user.id, is_archived=False - ).filter( - (ShoppingList.expires_at == None) | (ShoppingList.expires_at > now) + ShoppingList.query.filter( + ShoppingList.owner_id == current_user.id, + ShoppingList.is_archived == False, + (ShoppingList.expires_at == None) | (ShoppingList.expires_at > now), ) ) .order_by(ShoppingList.created_at.desc()) @@ -1450,21 +1478,23 @@ def main_page(): .all() ) + # publiczne cudze + udzielone mi (po list_permission) public_lists = ( date_filter( ShoppingList.query.filter( - ShoppingList.is_public == True, ShoppingList.owner_id != current_user.id, - ( - (ShoppingList.expires_at == None) - | (ShoppingList.expires_at > now) - ), ShoppingList.is_archived == False, + (ShoppingList.expires_at == None) | (ShoppingList.expires_at > now), + or_( + ShoppingList.is_public == True, + ShoppingList.id.in_(perm_subq), + ), ) ) .order_by(ShoppingList.created_at.desc()) .all() ) + accessible_lists = public_lists # alias do szablonu: publiczne + udostępnione else: user_lists = [] archived_lists = [] @@ -1472,31 +1502,31 @@ def main_page(): date_filter( ShoppingList.query.filter( ShoppingList.is_public == True, - ( - (ShoppingList.expires_at == None) - | (ShoppingList.expires_at > now) - ), + (ShoppingList.expires_at == None) | (ShoppingList.expires_at > now), ShoppingList.is_archived == False, ) ) .order_by(ShoppingList.created_at.desc()) .all() ) + accessible_lists = public_lists # dla gościa = tylko publiczne - # Definiujemy widoczny zakres list dla tego użytkownika + # Zakres miesięcy do selektora if current_user.is_authenticated: visible_lists_query = ShoppingList.query.filter( or_( - ShoppingList.owner_id == current_user.id, ShoppingList.is_public == True + ShoppingList.owner_id == current_user.id, + ShoppingList.is_public == True, + ShoppingList.id.in_(perm_subq), ) ) else: visible_lists_query = ShoppingList.query.filter(ShoppingList.is_public == True) - # Teraz możemy bezpiecznie pobrać miesiące month_options = get_active_months_query(visible_lists_query) - all_lists = user_lists + public_lists + archived_lists + # Statystyki dla wszystkich widocznych sekcji + all_lists = user_lists + accessible_lists + archived_lists all_ids = [l.id for l in all_lists] if all_ids: @@ -1504,18 +1534,13 @@ def main_page(): db.session.query( Item.list_id, func.count(Item.id).label("total_count"), - func.sum(case((Item.purchased == True, 1), else_=0)).label( - "purchased_count" - ), - func.sum(case((Item.not_purchased == True, 1), else_=0)).label( - "not_purchased_count" - ), + func.sum(case((Item.purchased == True, 1), else_=0)).label("purchased_count"), + func.sum(case((Item.not_purchased == True, 1), else_=0)).label("not_purchased_count"), ) .filter(Item.list_id.in_(all_ids)) .group_by(Item.list_id) .all() ) - stats_map = { s.list_id: ( s.total_count or 0, @@ -1535,18 +1560,12 @@ def main_page(): ) for l in all_lists: - total_count, purchased_count, not_purchased_count = stats_map.get( - l.id, (0, 0, 0) - ) + total_count, purchased_count, not_purchased_count = stats_map.get(l.id, (0, 0, 0)) l.total_count = total_count l.purchased_count = purchased_count l.not_purchased_count = not_purchased_count l.total_expense = latest_expenses_map.get(l.id, 0) - - l.category_badges = [ - {"name": c.name, "color": category_to_color(c.name)} - for c in l.categories - ] + l.category_badges = [{"name": c.name, "color": category_to_color(c.name)} for c in l.categories] else: for l in all_lists: l.total_count = 0 @@ -1559,6 +1578,7 @@ def main_page(): "main.html", user_lists=user_lists, public_lists=public_lists, + accessible_lists=accessible_lists, archived_lists=archived_lists, now=now, timedelta=timedelta, @@ -1627,6 +1647,48 @@ def edit_my_list(list_id): next_page = request.args.get("next") or request.referrer if request.method == "POST": + grant_username = (request.form.get("grant_username") or "").strip().lower() + revoke_user_id = request.form.get("revoke_user_id") + + # ——— SZYBKIE AKCJE UPRAWNIEŃ ——— + if grant_username: + u = User.query.filter(func.lower(User.username) == grant_username).first() + if not u: + flash("Użytkownik nie istnieje.", "danger") + return redirect(request.url) + if u.id == current_user.id: + flash("Jesteś właścicielem tej listy.", "info") + return redirect(request.url) + + exists = ( + db.session.query(ListPermission.id) + .filter( + ListPermission.list_id == l.id, + ListPermission.user_id == u.id, + ) + .first() + ) + if not exists: + db.session.add(ListPermission(list_id=l.id, user_id=u.id)) + db.session.commit() + flash(f"Nadano dostęp użytkownikowi „{u.username}”.", "success") + else: + flash("Ten użytkownik już ma dostęp.", "info") + return redirect(request.url) + + if revoke_user_id: + try: + uid = int(revoke_user_id) + except ValueError: + flash("Błędny identyfikator użytkownika.", "danger") + return redirect(request.url) + + ListPermission.query.filter_by(list_id=l.id, user_id=uid).delete() + db.session.commit() + flash("Odebrano dostęp użytkownikowi.", "success") + return redirect(request.url) + # ——— KONIEC AKCJI UPRAWNIEŃ ——— + if "unarchive" in request.form: l.is_archived = False db.session.commit() @@ -1650,7 +1712,7 @@ def edit_my_list(list_id): flash("Nieprawidłowy format miesiąca", "danger") return redirect(next_page or url_for("main_page")) - new_title = request.form.get("title", "").strip() + new_title = (request.form.get("title") or "").strip() is_public = "is_public" in request.form is_temporary = "is_temporary" in request.form is_archived = "is_archived" in request.form @@ -1682,12 +1744,22 @@ def edit_my_list(list_id): flash("Zaktualizowano dane listy", "success") return redirect(next_page or url_for("main_page")) + # Użytkownicy z dostępem (do wyświetlenia w szablonie) + permitted_users = ( + db.session.query(User) + .join(ListPermission, ListPermission.user_id == User.id) + .where(ListPermission.list_id == l.id) + .order_by(User.username.asc()) + .all() + ) + return render_template( "edit_my_list.html", list=l, receipts=receipts, categories=categories, selected_categories=selected_categories_ids, + permitted_users=permitted_users, ) @@ -1996,14 +2068,38 @@ def expenses_data(): @app.route("/share/") @app.route("/guest-list/") def shared_list(token=None, list_id=None): + now = datetime.now(timezone.utc) + if token: shopping_list = ShoppingList.query.filter_by(share_token=token).first_or_404() - if not check_list_public(shopping_list): + # jeśli lista wygasła – zablokuj (spójne z resztą aplikacji) + if shopping_list.is_temporary and shopping_list.expires_at and shopping_list.expires_at <= now: + flash("Link wygasł.", "warning") return redirect(url_for("main_page")) + # >>> KLUCZOWE: pozwól wejść nawet, gdy niepubliczna (bez check_list_public) list_id = shopping_list.id + # >>> Jeśli zalogowany i nie jest właścicielem — auto-przypisz stałe uprawnienie + if current_user.is_authenticated and current_user.id != shopping_list.owner_id: + # dodaj wpis tylko jeśli go nie ma + exists = ( + db.session.query(ListPermission.id) + .filter( + ListPermission.list_id == shopping_list.id, + ListPermission.user_id == current_user.id, + ) + .first() + ) + if not exists: + db.session.add( + ListPermission(list_id=shopping_list.id, user_id=current_user.id) + ) + db.session.commit() + else: + shopping_list = ShoppingList.query.get_or_404(list_id) + total_expense = get_total_expense_for_list(list_id) shopping_list, items, receipts, expenses, total_expense = get_list_details(list_id) @@ -2031,6 +2127,7 @@ def shared_list(token=None, list_id=None): ) + @app.route("/copy/") @login_required def copy_list(list_id): @@ -2852,6 +2949,13 @@ def edit_list(list_id): joinedload(ShoppingList.categories), ], ) + permitted_users = ( + db.session.query(User) + .join(ListPermission, ListPermission.user_id == User.id) + .filter(ListPermission.list_id == shopping_list.id) + .order_by(User.username.asc()) + .all() + ) if shopping_list is None: abort(404) @@ -3029,6 +3133,7 @@ def edit_list(list_id): receipts=receipts, categories=categories, selected_categories=selected_categories_ids, + permitted_users=permitted_users, ) @@ -3309,6 +3414,123 @@ def add_suggestion(): return redirect(url_for("list_products")) +# ── Admin: zarządzanie dostępem do list ─────────────────────────────────────── +@app.route("/admin/lists-access", methods=["GET", "POST"]) +@login_required +def admin_lists_access(): + # Prosta autoryzacja admina – dostosuj do swojej aplikacji + if not getattr(current_user, "is_admin", False): + abort(403) + + # Paginacja + try: + page = int(request.args.get("page", 1)) + except ValueError: + page = 1 + try: + per_page = int(request.args.get("per_page", 25)) + except ValueError: + per_page = 25 + per_page = max(1, min(100, per_page)) + + # Filtrowanie bazowe (bez archiwalnych? – tutaj pokazujemy wszystkie) + q = ( + ShoppingList.query + .options(db.joinedload(ShoppingList.owner)) + .order_by(ShoppingList.created_at.desc()) + ) + + # POST: grant/revoke per-wiersz + zbiorcza zmiana statusów + if request.method == "POST": + action = request.form.get("action") + target_list_id = request.form.get("target_list_id", type=int) + + # Grant pojedynczy + if action == "grant" and target_list_id: + login = (request.form.get("grant_username") or "").strip().lower() + l = db.session.get(ShoppingList, target_list_id) + if not l: + flash("Lista nie istnieje.", "danger") + return redirect(request.url) + u = User.query.filter(func.lower(User.username) == login).first() + if not u: + flash("Użytkownik nie istnieje.", "danger") + return redirect(request.url) + exists = ( + db.session.query(ListPermission.id) + .filter(ListPermission.list_id == l.id, ListPermission.user_id == u.id) + .first() + ) + if not exists: + db.session.add(ListPermission(list_id=l.id, user_id=u.id)) + db.session.commit() + flash(f"Nadano dostęp „{u.username}” do listy #{l.id}.", "success") + else: + flash("Ten użytkownik już ma dostęp.", "info") + return redirect(request.url) + + # Revoke pojedynczy + if action == "revoke" and target_list_id: + uid = request.form.get("revoke_user_id", type=int) + if uid: + ListPermission.query.filter_by(list_id=target_list_id, user_id=uid).delete() + db.session.commit() + flash("Odebrano dostęp użytkownikowi.", "success") + return redirect(request.url) + + # Zbiorcze zapisy statusów (checkboxy wierszy) + if action == "save_changes": + # Zaktualizuj pola is_public / is_temporary / is_archived na podstawie POST + # Wysyłamy identyfikatory wszystkich list widocznych na stronie w ukrytym polu multiple + ids = request.form.getlist("visible_ids", type=int) + if ids: + lists = ShoppingList.query.filter(ShoppingList.id.in_(ids)).all() + posted = request.form + for l in lists: + l.is_public = (posted.get(f"is_public_{l.id}") is not None) + l.is_temporary = (posted.get(f"is_temporary_{l.id}") is not None) + l.is_archived = (posted.get(f"is_archived_{l.id}") is not None) + db.session.commit() + flash("Zapisano zmiany statusów.", "success") + return redirect(request.url) + + # Dane do tabeli + pagination = q.paginate(page=page, per_page=per_page, error_out=False) + lists = pagination.items + + # Zbierz uprawnionych per lista (1 zapytanie) + list_ids = [l.id for l in lists] + perms = ( + db.session.query( + ListPermission.list_id, + User.id.label("uid"), + User.username.label("uname"), + ) + .join(User, User.id == ListPermission.user_id) + .filter(ListPermission.list_id.in_(list_ids)) + .order_by(User.username.asc()) + .all() + ) + + permitted_by_list = {lid: [] for lid in list_ids} + for lid, uid, uname in perms: + permitted_by_list[lid].append({"id": uid, "username": uname}) + + + # Query-string do paginacji + query_string = f"per_page={per_page}" + + return render_template( + "admin/admin_lists_access.html", + lists=lists, + permitted_by_list=permitted_by_list, + page=page, + per_page=per_page, + total_pages=pagination.pages or 1, + query_string=query_string, + ) + + @app.route("/healthcheck") def healthcheck(): header_token = request.headers.get("X-Internal-Check") diff --git a/static/js/functions.js b/static/js/functions.js index 244499e..ab379c7 100644 --- a/static/js/functions.js +++ b/static/js/functions.js @@ -224,17 +224,17 @@ function toggleVisibility(listId) { const copyBtn = document.getElementById('copyBtn'); const toggleBtn = document.getElementById('toggleVisibilityBtn'); + // URL zawsze widoczny i aktywny + shareUrlSpan.style.display = 'inline'; + shareUrlSpan.textContent = data.share_url; + copyBtn.disabled = false; + if (data.is_public) { - shareHeader.textContent = '🔗 Udostępnij link:'; - shareUrlSpan.style.display = 'inline'; - shareUrlSpan.textContent = data.share_url; - copyBtn.disabled = false; + shareHeader.textContent = '🔗 Udostępnij link (lista publiczna)'; toggleBtn.innerHTML = '🙈 Ukryj listę'; } else { - shareHeader.textContent = '🙈 Lista jest ukryta. Link udostępniania nie zadziała!'; - shareUrlSpan.style.display = 'none'; - copyBtn.disabled = true; - toggleBtn.innerHTML = '👁️ Udostępnij ponownie'; + shareHeader.textContent = '🔗 Udostępnij link (widoczna tylko przez link / uprawnienia)'; + toggleBtn.innerHTML = '👁️ Uczyń publiczną'; } }); } diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html new file mode 100644 index 0000000..3110901 --- /dev/null +++ b/templates/admin/admin_lists_access.html @@ -0,0 +1,174 @@ +{% extends 'base.html' %} +{% block title %}Zarządzanie dostępem do list{% endblock %} + +{% block content %} +
+

🔐 Zarządzanie dostępem do list

+ +
+ +
+
+
+ + +
+ + + + + + + + + + + + + + {% for l in lists %} + + + + + + + + + + + + + + + + {% endfor %} + {% if lists|length == 0 %} + + + + {% endif %} + +
IDNazwa listyWłaścicielUtworzonoStatusyLink shareUprawnienia
+ {{ l.id }} + + + {{ l.title }} + + {% if l.owner %} + 👤 {{ l.owner.username }} ({{ l.owner.id }}) + {% else %}-{% endif %} + {{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }} +
+ + +
+
+ + +
+
+ + +
+
+ {% if l.share_token %} +
+ + Otwórz +
+ {% else %} + Brak tokenu + {% endif %} +
+ +
    + {% for u in permitted_by_list.get(l.id, []) %} +
  • +
    + @{{ u.username }} +
    + + + + + + +
  • + {% endfor %} + {% if permitted_by_list.get(l.id, [])|length == 0 %} +
  • +
    Brak dodatkowych uprawnień.
    +
  • + {% endif %} +
+ + +
+ + +
+ + +
+
+ +
Brak list do wyświetlenia
+
+ +
+ +
+ +
+
+ +
+
+
+ + + +
+ + +
+{% endblock %} + +{% block scripts %} +{# Opcjonalnie: skrypty sortowania/UX jeśli już je masz #} +{% endblock %} \ No newline at end of file diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 30791e7..6efdd1d 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -14,6 +14,8 @@ 📸 Paragony 🛍️ Produkty 🗂 Kategorie + 🔐 Uprawnienia list + diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index d4d63b1..e61b935 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -117,6 +117,27 @@ value="{{ request.url_root }}share/{{ list.share_token }}"> + + +
+
🔐 Użytkownicy z dostępem
+ + {% if permitted_users %} +
    + {% for u in permitted_users %} +
  • +
    + @{{ u.username }} +
    +
  • + {% endfor %} +
+ {% else %} +
Brak dodatkowych uprawnień.
+ {% endif %} +
+ diff --git a/templates/edit_my_list.html b/templates/edit_my_list.html index 88525c6..958dccb 100644 --- a/templates/edit_my_list.html +++ b/templates/edit_my_list.html @@ -86,6 +86,63 @@ + +
+
🔐 Dostęp do listy
+ + +
+ + {% if list.share_token %} +
+ + Otwórz +
+ {% else %} +
Brak tokenu udostępniania.
+ {% endif %} +
Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ + {% if permitted_users and permitted_users|length > 0 %} +
    + {% for u in permitted_users %} +
  • +
    + @{{ u.username }} +
    +
    + + +
    +
  • + {% endfor %} +
+ {% else %}
+
Brak dodatkowych uprawnień.
+ {% endif %} +
+
+
diff --git a/templates/list.html b/templates/list.html index 216475c..aa2b58c 100644 --- a/templates/list.html +++ b/templates/list.html @@ -35,31 +35,36 @@
{% if list.is_public %} - 🔗 Udostępnij link: + 🔗 Udostępnij link (lista publiczna) {% else %} - 🙈 Lista jest ukryta przed gośćmi + 🔗 Udostępnij link (widoczna przez link / uprawnienia) {% endif %} - + {{ request.url_root }}share/{{ list.share_token }}
+ + + + ➕ Nadaj dostęp +
+
diff --git a/templates/main.html b/templates/main.html index f8b126b..d2473c8 100644 --- a/templates/main.html +++ b/templates/main.html @@ -92,12 +92,13 @@ class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">✏️ Odznaczaj 📋 Kopiuj - ⚙️ Ustawienia + - {% if l.is_public %}🙈 Ukryj{% else %}👁️ Odkryj{% endif %} + {% if l.is_public %}🙈 Niepubliczna{% else %}👁️ Publiczna{% endif %} + ⚙️ Ustawienia @@ -135,21 +136,21 @@ {% endif %} {% endif %} -

Publiczne listy innych użytkowników

-{% if public_lists %} +

Udostępnione i publiczne listy innych użytkowników

+{% set lists_to_show = accessible_lists %} +{% if lists_to_show %}
    - {% for l in public_lists %} + {% for l in lists_to_show %} {% set purchased_count = l.purchased_count %} {% set total_count = l.total_count %} {% set percent = (purchased_count / total_count * 100) if total_count > 0 else 0 %}
  • - {{ l.title }} (Autor: {{ l.owner.username }}) + {{ l.title }} (Autor: {{ l.owner.username if l.owner else '—' }}) {% for cat in l.category_badges %} + font-size: 0.56rem; opacity: 0.85;"> {{ cat.name }} {% endfor %} @@ -158,37 +159,31 @@ ✏️ Odznaczaj
    +
    - {# Kupione #}
    - {# Niekupione #} {% set not_purchased_count = l.not_purchased_count if l.total_count else 0 %}
    - {# Pozostałe #}
    - + Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%) - {% if l.total_expense > 0 %} - — 💸 {{ '%.2f'|format(l.total_expense) }} PLN - {% endif %} + {% if l.total_expense > 0 %} — 💸 {{ '%.2f'|format(l.total_expense) }} PLN{% endif %}
    -
  • {% endfor %}
{% else %} -

Brak dostępnych list publicznych do wyświetlenia

+

Brak list do wyświetlenia

{% endif %} diff --git a/templates/list.html b/templates/list.html index aa2b58c..ed8f157 100644 --- a/templates/list.html +++ b/templates/list.html @@ -55,10 +55,9 @@ {% if list.is_public %} 🙈 Ustaw niepubliczną {% else %} - 👁️ Uczyń publiczną + 🐵 Uczyń publiczną {% endif %} - ➕ Nadaj dostęp diff --git a/templates/main.html b/templates/main.html index d2473c8..1c7b1e2 100644 --- a/templates/main.html +++ b/templates/main.html @@ -63,7 +63,7 @@ Twoje listy {% if user_lists %} @@ -87,15 +87,14 @@
📄 Otwórz + class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">📂 Otwórz ✏️ Odznaczaj 📋 Kopiuj - - {% if l.is_public %}🙈 Niepubliczna{% else %}👁️ Publiczna{% endif %} + {% if l.is_public %}🙈 Ukryj{% else %}🐵 Odkryj{% endif %} ⚙️ Ustawienia From 3ade00fe08466ef09569bdf54e38199176a7d775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 13 Sep 2025 22:47:02 +0200 Subject: [PATCH 06/25] commit2 permissions --- app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 57c949d..f76fffe 100644 --- a/app.py +++ b/app.py @@ -1069,7 +1069,8 @@ def resolve_range(range_type: str): elif rt in ("last30days", "last_30_days"): sd = (now - timedelta(days=30)).date().strftime("%Y-%m-%d") ed = now.date().strftime("%Y-%m-%d") - bucket = "monthly" elif rt in ("last90days", "last_90_days"): + bucket = "monthly" + elif rt in ("last90days", "last_90_days"): sd = (now - timedelta(days=90)).date().strftime("%Y-%m-%d") ed = now.date().strftime("%Y-%m-%d") bucket = "monthly" @@ -2140,7 +2141,8 @@ def expenses_data(): @app.route("/share/") -@app.route("/guest-list/") +#@app.route("/guest-list/") +@app.route("/shared/") def shared_list(token=None, list_id=None): now = datetime.now(timezone.utc) From a69bf21fbb2dd0ab5c690aa996f9b8c3599bd592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 13 Sep 2025 23:04:25 +0200 Subject: [PATCH 07/25] commit2 permissions --- app.py | 160 +++++++++++++----------- templates/admin/admin_lists_access.html | 28 +++-- templates/edit_my_list.html | 10 +- 3 files changed, 111 insertions(+), 87 deletions(-) diff --git a/app.py b/app.py index f76fffe..7b34da5 100644 --- a/app.py +++ b/app.py @@ -306,6 +306,7 @@ class ListPermission(db.Model): created_at = db.Column(db.DateTime, default=datetime.utcnow) __table_args__ = (db.UniqueConstraint("list_id", "user_id", name="uq_list_user"),) + ShoppingList.permitted_users = db.relationship( "User", secondary="list_permission", @@ -467,7 +468,7 @@ def get_total_expense_for_list(list_id, start_date=None, end_date=None): def update_list_categories_from_form(shopping_list, form): - raw_vals = form.getlist("categories") + raw_vals = form.getlist("categories") candidate_ids = set() for v in raw_vals: @@ -562,17 +563,22 @@ def redirect_with_flash( flash(message, category) return redirect(url_for(endpoint)) + def can_view_list(sl: ShoppingList) -> bool: if current_user.is_authenticated: if sl.owner_id == current_user.id: return True if sl.is_public: return True - return db.session.query(ListPermission.id).filter_by( - list_id=sl.id, user_id=current_user.id - ).first() is not None + return ( + db.session.query(ListPermission.id) + .filter_by(list_id=sl.id, user_id=current_user.id) + .first() + is not None + ) return bool(sl.is_public) + def db_bucket(col, kind: str = "month"): name = db.engine.name # 'sqlite', 'mysql', 'mariadb', 'postgresql', ... kind = (kind or "month").lower() @@ -587,9 +593,9 @@ def db_bucket(col, kind: str = "month"): if kind == "week": if name == "sqlite": - return func.printf("%s-W%s", - func.strftime("%Y", col), - func.strftime("%W", col)) + return func.printf( + "%s-W%s", func.strftime("%Y", col), func.strftime("%W", col) + ) elif name in ("mysql", "mariadb"): return func.date_format(col, "%x-W%v") else: @@ -630,6 +636,7 @@ def user_permission_subq(user_id): ListPermission.user_id == user_id ) + def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): @@ -851,6 +858,7 @@ def category_to_color(name): r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation) return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" + def get_total_expenses_grouped_by_category( show_all, range_type, start_date, end_date, user_id, category_id=None ): @@ -885,13 +893,13 @@ def get_total_expenses_grouped_by_category( except (TypeError, ValueError): pass - # ZAKRES: zawsze po created_at LISTY if start_date and end_date: try: dt_start = datetime.strptime(start_date, "%Y-%m-%d") - dt_end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) - lists_q = lists_q.filter(ShoppingList.created_at >= dt_start, - ShoppingList.created_at < dt_end) + dt_end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) + lists_q = lists_q.filter( + ShoppingList.created_at >= dt_start, ShoppingList.created_at < dt_end + ) except Exception: return {"error": "Błędne daty"} @@ -899,7 +907,6 @@ def get_total_expenses_grouped_by_category( if not lists: return {"labels": [], "datasets": []} - # SUMY: po wszystkich wydatkach tych list (bez filtra dat po Expense) list_ids = [l.id for l in lists] totals = ( db.session.query( @@ -912,7 +919,6 @@ def get_total_expenses_grouped_by_category( ) expense_map = {lid: float(total or 0) for lid, total in totals} - # bucket wg created_at LISTY def bucket_from_dt(ts: datetime) -> str: if range_type == "daily": return ts.strftime("%Y-%m-%d") @@ -948,7 +954,7 @@ def get_total_expenses_grouped_by_category( data_map[key][c.name] += total_expense labels = sorted(all_labels) - cats = sorted({cat for b in data_map.values() for cat,v in b.items() if v > 0}) + cats = sorted({cat for b in data_map.values() for cat, v in b.items() if v > 0}) datasets = [ { @@ -960,6 +966,7 @@ def get_total_expenses_grouped_by_category( ] return {"labels": labels, "datasets": datasets} + def get_total_expenses_grouped_by_list_created_at( user_only=False, admin=False, @@ -1006,13 +1013,13 @@ def get_total_expenses_grouped_by_list_created_at( except (TypeError, ValueError): pass - # ZAKRES: zawsze po created_at LISTY if start_date and end_date: try: dt_start = datetime.strptime(start_date, "%Y-%m-%d") - dt_end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) - lists_q = lists_q.filter(ShoppingList.created_at >= dt_start, - ShoppingList.created_at < dt_end) + dt_end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) + lists_q = lists_q.filter( + ShoppingList.created_at >= dt_start, ShoppingList.created_at < dt_end + ) except Exception: return {"error": "Błędne daty"} @@ -1020,7 +1027,6 @@ def get_total_expenses_grouped_by_list_created_at( if not lists: return {"labels": [], "expenses": []} - # SUMY: po wszystkich wydatkach tych list (bez filtra dat po Expense) list_ids = [l.id for l in lists] totals = ( db.session.query( @@ -1033,7 +1039,6 @@ def get_total_expenses_grouped_by_list_created_at( ) expense_map = {lid: float(total or 0) for lid, total in totals} - # bucket wg created_at LISTY def bucket_from_dt(ts: datetime) -> str: if range_type == "daily": return ts.strftime("%Y-%m-%d") @@ -1056,6 +1061,7 @@ def get_total_expenses_grouped_by_list_created_at( expenses = [round(grouped[l], 2) for l in labels] return {"labels": labels, "expenses": expenses} + def resolve_range(range_type: str): now = datetime.now(timezone.utc) sd = ed = None @@ -1069,7 +1075,7 @@ def resolve_range(range_type: str): elif rt in ("last30days", "last_30_days"): sd = (now - timedelta(days=30)).date().strftime("%Y-%m-%d") ed = now.date().strftime("%Y-%m-%d") - bucket = "monthly" + bucket = "monthly" elif rt in ("last90days", "last_90_days"): sd = (now - timedelta(days=90)).date().strftime("%Y-%m-%d") ed = now.date().strftime("%Y-%m-%d") @@ -1079,6 +1085,18 @@ def resolve_range(range_type: str): sd = first.date().strftime("%Y-%m-%d") ed = now.date().strftime("%Y-%m-%d") bucket = "monthly" + elif rt in ( + "currentmonth", + "thismonth", + "this_month", + "monthtodate", + "month_to_date", + "mtd", + ): + first = datetime(now.year, now.month, 1, tzinfo=timezone.utc) + sd = first.date().strftime("%Y-%m-%d") + ed = now.date().strftime("%Y-%m-%d") + bucket = "monthly" return sd, ed, bucket @@ -1493,9 +1511,7 @@ def favicon(): @app.route("/") def main_page(): perm_subq = ( - user_permission_subq(current_user.id) - if current_user.is_authenticated - else None + user_permission_subq(current_user.id) if current_user.is_authenticated else None ) now = datetime.now(timezone.utc) @@ -1604,8 +1620,12 @@ def main_page(): db.session.query( Item.list_id, func.count(Item.id).label("total_count"), - func.sum(case((Item.purchased == True, 1), else_=0)).label("purchased_count"), - func.sum(case((Item.not_purchased == True, 1), else_=0)).label("not_purchased_count"), + func.sum(case((Item.purchased == True, 1), else_=0)).label( + "purchased_count" + ), + func.sum(case((Item.not_purchased == True, 1), else_=0)).label( + "not_purchased_count" + ), ) .filter(Item.list_id.in_(all_ids)) .group_by(Item.list_id) @@ -1630,12 +1650,17 @@ def main_page(): ) for l in all_lists: - total_count, purchased_count, not_purchased_count = stats_map.get(l.id, (0, 0, 0)) + total_count, purchased_count, not_purchased_count = stats_map.get( + l.id, (0, 0, 0) + ) l.total_count = total_count l.purchased_count = purchased_count l.not_purchased_count = not_purchased_count l.total_expense = latest_expenses_map.get(l.id, 0) - l.category_badges = [{"name": c.name, "color": category_to_color(c.name)} for c in l.categories] + l.category_badges = [ + {"name": c.name, "color": category_to_color(c.name)} + for c in l.categories + ] else: for l in all_lists: l.total_count = 0 @@ -1987,9 +2012,9 @@ def view_list(list_id): @login_required def expenses(): start_date_str = request.args.get("start_date") - end_date_str = request.args.get("end_date") - category_id = request.args.get("category_id", type=str) - show_all = request.args.get("show_all", "true").lower() == "true" + end_date_str = request.args.get("end_date") + category_id = request.args.get("category_id", type=str) + show_all = request.args.get("show_all", "true").lower() == "true" now = datetime.now(timezone.utc) @@ -2002,10 +2027,10 @@ def expenses(): if start_date_str and end_date_str: try: start = datetime.strptime(start_date_str, "%Y-%m-%d") - end = datetime.strptime(end_date_str, "%Y-%m-%d") + timedelta(days=1) + end = datetime.strptime(end_date_str, "%Y-%m-%d") + timedelta(days=1) lists_q = lists_q.filter( ShoppingList.created_at >= start, - ShoppingList.created_at < end, + ShoppingList.created_at < end, ) except ValueError: flash("Błędny zakres dat", "danger") @@ -2024,16 +2049,16 @@ def expenses(): pass lists_filtered = ( - lists_q - .options(joinedload(ShoppingList.owner), joinedload(ShoppingList.categories)) + lists_q.options( + joinedload(ShoppingList.owner), joinedload(ShoppingList.categories) + ) .order_by(ShoppingList.created_at.desc()) .all() ) list_ids = [l.id for l in lists_filtered] or [-1] expenses = ( - Expense.query - .options( + Expense.query.options( joinedload(Expense.shopping_list).joinedload(ShoppingList.owner), joinedload(Expense.shopping_list).joinedload(ShoppingList.categories), ) @@ -2056,9 +2081,12 @@ def expenses(): totals_map = {row.lid: float(row.total_expense or 0) for row in totals_rows} categories = ( - Category.query - .join(shopping_list_category, shopping_list_category.c.category_id == Category.id) - .join(ShoppingList, ShoppingList.id == shopping_list_category.c.shopping_list_id) + Category.query.join( + shopping_list_category, shopping_list_category.c.category_id == Category.id + ) + .join( + ShoppingList, ShoppingList.id == shopping_list_category.c.shopping_list_id + ) .filter(ShoppingList.id.in_(list_ids)) .distinct() .order_by(Category.name.asc()) @@ -2068,8 +2096,8 @@ def expenses(): expense_table = [ { - "title": (e.shopping_list.title if e.shopping_list else "Nieznana"), - "amount": e.amount, + "title": (e.shopping_list.title if e.shopping_list else "Nieznana"), + "amount": e.amount, "added_at": e.added_at, } for e in expenses @@ -2102,8 +2130,8 @@ def expenses(): def expenses_data(): range_type = request.args.get("range", "monthly") start_date = request.args.get("start_date") - end_date = request.args.get("end_date") - show_all = request.args.get("show_all", "true").lower() == "true" + end_date = request.args.get("end_date") + show_all = request.args.get("show_all", "true").lower() == "true" category_id = request.args.get("category_id") by_category = request.args.get("by_category", "false").lower() == "true" @@ -2111,7 +2139,7 @@ def expenses_data(): sd, ed, bucket = resolve_range(range_type) if sd and ed: start_date = sd - end_date = ed + end_date = ed range_type = bucket if by_category: @@ -2141,7 +2169,7 @@ def expenses_data(): @app.route("/share/") -#@app.route("/guest-list/") +# @app.route("/guest-list/") @app.route("/shared/") def shared_list(token=None, list_id=None): now = datetime.now(timezone.utc) @@ -2150,7 +2178,11 @@ def shared_list(token=None, list_id=None): shopping_list = ShoppingList.query.filter_by(share_token=token).first_or_404() # jeśli lista wygasła – zablokuj (spójne z resztą aplikacji) - if shopping_list.is_temporary and shopping_list.expires_at and shopping_list.expires_at <= now: + if ( + shopping_list.is_temporary + and shopping_list.expires_at + and shopping_list.expires_at <= now + ): flash("Link wygasł.", "warning") return redirect(url_for("main_page")) @@ -2203,7 +2235,6 @@ def shared_list(token=None, list_id=None): ) - @app.route("/copy/") @login_required def copy_list(list_id): @@ -3490,15 +3521,11 @@ def add_suggestion(): return redirect(url_for("list_products")) -# ── Admin: zarządzanie dostępem do list ─────────────────────────────────────── @app.route("/admin/lists-access", methods=["GET", "POST"]) @login_required +@admin_required def admin_lists_access(): - # Prosta autoryzacja admina – dostosuj do swojej aplikacji - if not getattr(current_user, "is_admin", False): - abort(403) - # Paginacja try: page = int(request.args.get("page", 1)) except ValueError: @@ -3509,19 +3536,14 @@ def admin_lists_access(): per_page = 25 per_page = max(1, min(100, per_page)) - # Filtrowanie bazowe (bez archiwalnych? – tutaj pokazujemy wszystkie) - q = ( - ShoppingList.query - .options(db.joinedload(ShoppingList.owner)) - .order_by(ShoppingList.created_at.desc()) + q = ShoppingList.query.options(db.joinedload(ShoppingList.owner)).order_by( + ShoppingList.created_at.desc() ) - # POST: grant/revoke per-wiersz + zbiorcza zmiana statusów if request.method == "POST": action = request.form.get("action") target_list_id = request.form.get("target_list_id", type=int) - # Grant pojedynczy if action == "grant" and target_list_id: login = (request.form.get("grant_username") or "").strip().lower() l = db.session.get(ShoppingList, target_list_id) @@ -3545,36 +3567,32 @@ def admin_lists_access(): flash("Ten użytkownik już ma dostęp.", "info") return redirect(request.url) - # Revoke pojedynczy if action == "revoke" and target_list_id: uid = request.form.get("revoke_user_id", type=int) if uid: - ListPermission.query.filter_by(list_id=target_list_id, user_id=uid).delete() + ListPermission.query.filter_by( + list_id=target_list_id, user_id=uid + ).delete() db.session.commit() flash("Odebrano dostęp użytkownikowi.", "success") return redirect(request.url) - # Zbiorcze zapisy statusów (checkboxy wierszy) if action == "save_changes": - # Zaktualizuj pola is_public / is_temporary / is_archived na podstawie POST - # Wysyłamy identyfikatory wszystkich list widocznych na stronie w ukrytym polu multiple ids = request.form.getlist("visible_ids", type=int) if ids: lists = ShoppingList.query.filter(ShoppingList.id.in_(ids)).all() posted = request.form for l in lists: - l.is_public = (posted.get(f"is_public_{l.id}") is not None) - l.is_temporary = (posted.get(f"is_temporary_{l.id}") is not None) - l.is_archived = (posted.get(f"is_archived_{l.id}") is not None) + l.is_public = posted.get(f"is_public_{l.id}") is not None + l.is_temporary = posted.get(f"is_temporary_{l.id}") is not None + l.is_archived = posted.get(f"is_archived_{l.id}") is not None db.session.commit() flash("Zapisano zmiany statusów.", "success") return redirect(request.url) - # Dane do tabeli pagination = q.paginate(page=page, per_page=per_page, error_out=False) lists = pagination.items - # Zbierz uprawnionych per lista (1 zapytanie) list_ids = [l.id for l in lists] perms = ( db.session.query( @@ -3592,8 +3610,6 @@ def admin_lists_access(): for lid, uid, uname in perms: permitted_by_list[lid].append({"id": uid, "username": uname}) - - # Query-string do paginacji query_string = f"per_page={per_page}" return render_template( diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index 3110901..7517a70 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -23,7 +23,7 @@ Właściciel Utworzono Statusy - Link share + Udostępnianie Uprawnienia @@ -65,17 +65,25 @@
- + {% if l.share_token %} -
- - Otwórz + {% set share_url = url_for('shared_list', token=l.share_token, _external=True) %} +
+ 🔗 Link +
+ {{ share_url }} +
+ + 👁️ Otwórz + +
+
+ {% if l.is_public %}Widoczna publicznie{% else %}Dostępna przez link/uprawnienia{% + endif %}
{% else %} - Brak tokenu +
Brak tokenu
{% endif %} @@ -100,7 +108,7 @@ {% endfor %} {% if permitted_by_list.get(l.id, [])|length == 0 %}
  • -
    Brak dodatkowych uprawnień.
    +
    Brak dodanych uprawnień.
  • {% endif %} diff --git a/templates/edit_my_list.html b/templates/edit_my_list.html index 958dccb..fd8d1de 100644 --- a/templates/edit_my_list.html +++ b/templates/edit_my_list.html @@ -24,13 +24,13 @@
    - +
    - +
    @@ -92,8 +92,8 @@
    - + {% if list.share_token %}
    {% else %}
    -
    Brak dodatkowych uprawnień.
    +
    Brak dodanych uprawnień.
    {% endif %}
    From 4c0df73e74a4917bf1dac0c9bbf9bb57ed74588f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 13 Sep 2025 23:07:37 +0200 Subject: [PATCH 08/25] commit3 wizaulne --- templates/admin/admin_lists_access.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index 7517a70..0dc050d 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -69,17 +69,17 @@ {% if l.share_token %} {% set share_url = url_for('shared_list', token=l.share_token, _external=True) %}
    - 🔗 Link
    {{ share_url }}
    - 👁️ Otwórz + Otwórz
    - {% if l.is_public %}Widoczna publicznie{% else %}Dostępna przez link/uprawnienia{% + {% if l.is_public %}Lista widoczna publicznie{% else %}Lista dostępna przez + link/uprawnienia{% endif %}
    {% else %} From 7496442276ccff14220a63e3fa8ae31b57646c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 13 Sep 2025 23:09:09 +0200 Subject: [PATCH 09/25] commit3 wizaulne --- templates/admin/admin_lists_access.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index 0dc050d..62ec976 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -65,7 +65,7 @@
    - + {% if l.share_token %} {% set share_url = url_for('shared_list', token=l.share_token, _external=True) %}
    @@ -88,7 +88,6 @@ -
      {% for u in permitted_by_list.get(l.id, []) %}
    • Date: Sat, 13 Sep 2025 23:11:12 +0200 Subject: [PATCH 10/25] commit3 wizaulne --- templates/admin/admin_lists_access.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index 62ec976..60444c7 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -72,10 +72,6 @@
      {{ share_url }}
      - - Otwórz -
    {% if l.is_public %}Lista widoczna publicznie{% else %}Lista dostępna przez From 50af5ce44dc6bb8e8cc2a9cb72b7c472ff2a0414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 13 Sep 2025 23:14:32 +0200 Subject: [PATCH 11/25] commit4 naprawa formularza --- templates/edit_my_list.html | 115 ++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 58 deletions(-) diff --git a/templates/edit_my_list.html b/templates/edit_my_list.html index fd8d1de..305164e 100644 --- a/templates/edit_my_list.html +++ b/templates/edit_my_list.html @@ -85,64 +85,6 @@ {% endfor %}
    - - -
    -
    🔐 Dostęp do listy
    - - -
    - - {% if list.share_token %} -
    - - Otwórz -
    - {% else %} -
    Brak tokenu udostępniania.
    - {% endif %} -
    Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.
    -
    - - -
    -
    - - -
    -
    - -
    -
    - - -
    - - {% if permitted_users and permitted_users|length > 0 %} -
      - {% for u in permitted_users %} -
    • -
      - @{{ u.username }} -
      -
      - - -
      -
    • - {% endfor %} -
    - {% else %}
    -
    Brak dodanych uprawnień.
    - {% endif %} -
    -
    -
    @@ -151,7 +93,64 @@
    +
    + +
    +
    🔐 Dostęp do listy
    + + +
    + + {% if list.share_token %} +
    + + Otwórz +
    + {% else %} +
    Brak tokenu udostępniania.
    + {% endif %} +
    Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.
    +
    + + +
    +
    + + +
    +
    + +
    +
    + + +
    + + {% if permitted_users and permitted_users|length > 0 %} +
      + {% for u in permitted_users %} +
    • +
      + @{{ u.username }} +
      +
      + + +
      +
    • + {% endfor %} +
    + {% else %}
    +
    Brak dodanych uprawnień.
    + {% endif %} +
    +
    + {% if receipts %}
    From e860202af888c57efe1b931dbe78ba111a8d88dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 13 Sep 2025 23:19:34 +0200 Subject: [PATCH 12/25] commit4 naprawa formularza --- templates/edit_my_list.html | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/templates/edit_my_list.html b/templates/edit_my_list.html index 305164e..f143a8d 100644 --- a/templates/edit_my_list.html +++ b/templates/edit_my_list.html @@ -115,19 +115,22 @@ {% endif %}
    Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.
    - - -
    -
    - - +
    +
    +
    + + +
    +
    + +
    + + + +
    -
    - -
    -
    - +
    From 554340dd64a4ade36b892a178af443310abef478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 12:23:02 +0200 Subject: [PATCH 13/25] poprawki --- app.py | 27 ++++++++++--------------- templates/admin/admin_lists_access.html | 4 ---- templates/edit_my_list.html | 27 ++++++++++++++----------- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/app.py b/app.py index 7b34da5..6f81baf 100644 --- a/app.py +++ b/app.py @@ -1745,15 +1745,14 @@ def edit_my_list(list_id): grant_username = (request.form.get("grant_username") or "").strip().lower() revoke_user_id = request.form.get("revoke_user_id") - # ——— SZYBKIE AKCJE UPRAWNIEŃ ——— if grant_username: u = User.query.filter(func.lower(User.username) == grant_username).first() if not u: flash("Użytkownik nie istnieje.", "danger") - return redirect(request.url) + return redirect(next_page or request.url) if u.id == current_user.id: flash("Jesteś właścicielem tej listy.", "info") - return redirect(request.url) + return redirect(next_page or request.url) exists = ( db.session.query(ListPermission.id) @@ -1769,28 +1768,26 @@ def edit_my_list(list_id): flash(f"Nadano dostęp użytkownikowi „{u.username}”.", "success") else: flash("Ten użytkownik już ma dostęp.", "info") - return redirect(request.url) + return redirect(next_page or request.url) if revoke_user_id: try: uid = int(revoke_user_id) except ValueError: flash("Błędny identyfikator użytkownika.", "danger") - return redirect(request.url) + return redirect(next_page or request.url) ListPermission.query.filter_by(list_id=l.id, user_id=uid).delete() db.session.commit() flash("Odebrano dostęp użytkownikowi.", "success") - return redirect(request.url) - # ——— KONIEC AKCJI UPRAWNIEŃ ——— + return redirect(next_page or request.url) if "unarchive" in request.form: l.is_archived = False db.session.commit() flash(f"Lista „{l.title}” została przywrócona.", "success") - return redirect(next_page or url_for("main_page")) + return redirect(next_page or request.url) - # Pełna edycja formularza move_to_month = request.form.get("move_to_month") if move_to_month: try: @@ -1802,10 +1799,10 @@ def edit_my_list(list_id): f"Zmieniono datę utworzenia listy na {new_created_at.strftime('%Y-%m-%d')}", "success", ) - return redirect(next_page or url_for("main_page")) + return redirect(next_page or request.url) except ValueError: flash("Nieprawidłowy format miesiąca", "danger") - return redirect(next_page or url_for("main_page")) + return redirect(next_page or request.url) new_title = (request.form.get("title") or "").strip() is_public = "is_public" in request.form @@ -1816,7 +1813,7 @@ def edit_my_list(list_id): if not new_title: flash("Podaj poprawny tytuł", "danger") - return redirect(next_page or url_for("main_page")) + return redirect(next_page or request.url) l.title = new_title l.is_public = is_public @@ -1830,16 +1827,15 @@ def edit_my_list(list_id): l.expires_at = expires_dt.replace(tzinfo=timezone.utc) except ValueError: flash("Błędna data lub godzina wygasania", "danger") - return redirect(next_page or url_for("main_page")) + return redirect(next_page or request.url) else: l.expires_at = None update_list_categories_from_form(l, request.form) db.session.commit() flash("Zaktualizowano dane listy", "success") - return redirect(next_page or url_for("main_page")) + return redirect(next_page or request.url) - # Użytkownicy z dostępem (do wyświetlenia w szablonie) permitted_users = ( db.session.query(User) .join(ListPermission, ListPermission.user_id == User.id) @@ -3525,7 +3521,6 @@ def add_suggestion(): @login_required @admin_required def admin_lists_access(): - try: page = int(request.args.get("page", 1)) except ValueError: diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index 60444c7..6d2ff9e 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -170,8 +170,4 @@
    -{% endblock %} - -{% block scripts %} -{# Opcjonalnie: skrypty sortowania/UX jeśli już je masz #} {% endblock %} \ No newline at end of file diff --git a/templates/edit_my_list.html b/templates/edit_my_list.html index f143a8d..9df2646 100644 --- a/templates/edit_my_list.html +++ b/templates/edit_my_list.html @@ -90,31 +90,34 @@ ❌ Anuluj
    - - + -
    +
    🔐 Dostęp do listy
    - {% if list.share_token %} -
    - - Otwórz -
    + + {% if list.share_token %} +
    + + + +
    {% else %} -
    Brak tokenu udostępniania.
    +
    Brak tokenu udostępniania.
    {% endif %}
    Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.
    +
    @@ -131,6 +134,7 @@
    +
    @@ -154,7 +158,6 @@
    - {% if receipts %}
    Paragony przypisane do tej listy
    From ec200a3819bc25ae9b4a4a8e1eacc2969593c708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 12:41:49 +0200 Subject: [PATCH 14/25] poprawki --- app.py | 27 ++++-- templates/admin/admin_lists_access.html | 3 + templates/admin/edit_list.html | 5 ++ templates/edit_my_list.html | 114 ++++++++++++------------ templates/list.html | 1 - 5 files changed, 85 insertions(+), 65 deletions(-) diff --git a/app.py b/app.py index 6f81baf..3feec89 100644 --- a/app.py +++ b/app.py @@ -3518,9 +3518,10 @@ def add_suggestion(): @app.route("/admin/lists-access", methods=["GET", "POST"]) +@app.route("/admin/lists-access/", methods=["GET", "POST"]) @login_required @admin_required -def admin_lists_access(): +def admin_lists_access(list_id=None): try: page = int(request.args.get("page", 1)) except ValueError: @@ -3535,6 +3536,19 @@ def admin_lists_access(): ShoppingList.created_at.desc() ) + if list_id is not None: + target_list = db.session.get(ShoppingList, list_id) + if not target_list: + flash("Lista nie istnieje.", "danger") + return redirect(url_for("admin_lists_access")) + lists = [target_list] + list_ids = [list_id] + pagination = None + else: + pagination = q.paginate(page=page, per_page=per_page, error_out=False) + lists = pagination.items + list_ids = [l.id for l in lists] + if request.method == "POST": action = request.form.get("action") target_list_id = request.form.get("target_list_id", type=int) @@ -3575,9 +3589,9 @@ def admin_lists_access(): if action == "save_changes": ids = request.form.getlist("visible_ids", type=int) if ids: - lists = ShoppingList.query.filter(ShoppingList.id.in_(ids)).all() + lists_edit = ShoppingList.query.filter(ShoppingList.id.in_(ids)).all() posted = request.form - for l in lists: + for l in lists_edit: l.is_public = posted.get(f"is_public_{l.id}") is not None l.is_temporary = posted.get(f"is_temporary_{l.id}") is not None l.is_archived = posted.get(f"is_archived_{l.id}") is not None @@ -3585,10 +3599,6 @@ def admin_lists_access(): flash("Zapisano zmiany statusów.", "success") return redirect(request.url) - pagination = q.paginate(page=page, per_page=per_page, error_out=False) - lists = pagination.items - - list_ids = [l.id for l in lists] perms = ( db.session.query( ListPermission.list_id, @@ -3613,8 +3623,9 @@ def admin_lists_access(): permitted_by_list=permitted_by_list, page=page, per_page=per_page, - total_pages=pagination.pages or 1, + total_pages=pagination.pages if pagination else 1, query_string=query_string, + list_id=list_id, ) diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index 6d2ff9e..c48c59f 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -139,6 +139,7 @@
    +{% if not list_id %}
    @@ -170,4 +171,6 @@
    +{% endif %} + {% endblock %} \ No newline at end of file diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index e61b935..cd24d39 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -122,6 +122,11 @@
    🔐 Użytkownicy z dostępem
    + + ⚙️ Edytuj uprawnienia (admin) + + {% if permitted_users %}
      {% for u in permitted_users %} diff --git a/templates/edit_my_list.html b/templates/edit_my_list.html index 9df2646..e08aabd 100644 --- a/templates/edit_my_list.html +++ b/templates/edit_my_list.html @@ -96,65 +96,67 @@
      -
      🔐 Dostęp do listy
      +
      +
      🔐 Dostęp do listy
      - -
      - + +
      + - {% if list.share_token %} -
      - - - -
      - {% else %} -
      Brak tokenu udostępniania.
      - {% endif %} -
      Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.
      -
      - - -
      -
      - - -
      -
      - -
      - - - - -
      - - - -
      - - {% if permitted_users and permitted_users|length > 0 %} -
        - {% for u in permitted_users %} -
      • -
        - @{{ u.username }} + {% if list.share_token %} + -
        - - -
        -
      • - {% endfor %} -
      - {% else %}
      -
      Brak dodanych uprawnień.
      - {% endif %} + {% else %} +
      Brak tokenu udostępniania.
      + {% endif %} +
      Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.
      +
      + +
      +
      +
      + + +
      +
      + +
      + + + + +
      +
      + + +
      + + {% if permitted_users and permitted_users|length > 0 %} +
        + {% for u in permitted_users %} +
      • +
        + @{{ u.username }} +
        +
        + + +
        +
      • + {% endfor %} +
      + {% else %}
      +
      Brak dodanych uprawnień.
      + {% endif %} +
      diff --git a/templates/list.html b/templates/list.html index ed8f157..6e80d0f 100644 --- a/templates/list.html +++ b/templates/list.html @@ -63,7 +63,6 @@ ➕ Nadaj dostęp
      -
    From 736b34231a0d9f5adc21c5e630c2fa313357ca70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 12:46:30 +0200 Subject: [PATCH 15/25] poprawki --- templates/admin/edit_list.html | 2 +- templates/admin/receipts.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index cd24d39..48cb8c6 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -124,7 +124,7 @@ - ⚙️ Edytuj uprawnienia (admin) + ⚙️ Edytuj uprawnienia {% if permitted_users %} diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index 481c34f..2ce26d1 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -118,8 +118,8 @@ +{% if not list_id %}
    -
    @@ -149,7 +149,7 @@
    - +{% endif %} {% if orphan_files and request.path.endswith('/all') %}
    From b709c8252cec335651906a115d812506e7c9d187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 12:48:51 +0200 Subject: [PATCH 16/25] poprawki --- templates/admin/receipts.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index 2ce26d1..4dbdc0e 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -118,7 +118,7 @@ -{% if not list_id %} +{% if id == 'all' %}
    From 74b44dd8e891867029091b08cc609224c34e90d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 12:52:40 +0200 Subject: [PATCH 17/25] poprawki --- app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app.py b/app.py index 3feec89..0ece649 100644 --- a/app.py +++ b/app.py @@ -2847,6 +2847,7 @@ def delete_user(user_id): return redirect(url_for("list_users")) +@app.route("/admin/receipts/", defaults={'id': 'all'}) @app.route("/admin/receipts/") @login_required @admin_required From 016f9896b7e238a3c49b24159bacd16b97d245eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 12:59:15 +0200 Subject: [PATCH 18/25] poprawki --- app.py | 2 +- templates/admin/receipts.html | 32 ++++++++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app.py b/app.py index 0ece649..3d384d1 100644 --- a/app.py +++ b/app.py @@ -2847,7 +2847,7 @@ def delete_user(user_id): return redirect(url_for("list_users")) -@app.route("/admin/receipts/", defaults={'id': 'all'}) +@app.route("/admin/receipts", defaults={'id': 'all'}) @app.route("/admin/receipts/") @login_required @admin_required diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index 4dbdc0e..f45e0ed 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -20,21 +20,29 @@ {{ (page_filesize / 1024) | round(1) }} kB {% endif %} - | - Łącznie: - - {% if total_filesize >= 1024*1024 %} - {{ (total_filesize / 1024 / 1024) | round(2) }} MB - {% else %} - {{ (total_filesize / 1024) | round(1) }} kB - {% endif %} - + {% if id != 'all' and (id|string).isdigit() %} + {% else %} + | Łącznie: + + {% if total_filesize >= 1024*1024 %} + {{ (total_filesize / 1024 / 1024) | round(2) }} MB + {% else %} + {{ (total_filesize / 1024) | round(1) }} kB + {% endif %} + + {% endif %}

    From 9bff1a43b3ac86edc479226aa33de5c336c1307d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 13:03:13 +0200 Subject: [PATCH 19/25] poprawki --- templates/admin/receipts.html | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index f45e0ed..a90f165 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -20,21 +20,20 @@ {{ (page_filesize / 1024) | round(1) }} kB {% endif %} - {% if id != 'all' and (id|string).isdigit() %} - {% else %} - | Łącznie: - - {% if total_filesize >= 1024*1024 %} - {{ (total_filesize / 1024 / 1024) | round(2) }} MB - {% else %} - {{ (total_filesize / 1024) | round(1) }} kB - {% endif %} - - {% endif %} + {% if not (id != 'all' and (id|string).isdigit()) %} + | Łącznie: + + {% if total_filesize >= 1024*1024 %} + {{ (total_filesize / 1024 / 1024) | round(2) }} MB + {% else %} + {{ (total_filesize / 1024) | round(1) }} kB + {% endif %} + + {% endif %}

    - {% if id|int(value=-1) > 0 %} + {% if id is string and id.isdigit() and id|int > 0 %} Pokaż wszystkie paragony From 2a672170081dba959923c43d395d3adb5dabe9c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 13:26:28 +0200 Subject: [PATCH 20/25] poprawki --- templates/admin/admin_lists_access.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index c48c59f..cc36605 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -5,6 +5,9 @@

    🔐 Zarządzanie dostępem do list

    + {% if not list_id %} + Powrót do wszystkich list + {% endif %} ← Powrót do panelu
    From 8e96702d8e561084b22d4900b09405b0b3cbd572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 13:30:13 +0200 Subject: [PATCH 21/25] poprawki --- templates/admin/admin_lists_access.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index cc36605..8cdfc94 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -3,10 +3,10 @@ {% block content %}
    -

    🔐 Zarządzanie dostępem do list

    +

    🔐{% if list_id %} Zarządzanie dostępem do list{% else %}Zarządzanie dostępem #{{ list_id }}{% endif %}

    - {% if not list_id %} - Powrót do wszystkich list + {% if list_id %} + Powrót do wszystkich list {% endif %} ← Powrót do panelu
    From f2dafd6fe868c165cd805a12ada362ee851c46bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 13:41:37 +0200 Subject: [PATCH 22/25] poprawki --- templates/admin/admin_lists_access.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index 8cdfc94..363c73f 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -3,10 +3,10 @@ {% block content %}
    -

    🔐{% if list_id %} Zarządzanie dostępem do list{% else %}Zarządzanie dostępem #{{ list_id }}{% endif %}

    +

    🔐{% if list_id %} Zarządzanie dostępem #{{ list_id }}{% else %} Zarządzanie dostępem do list {% endif %}

    From c93194ba3e30a407cc6a141b7cdecbabf80a24fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 13:43:18 +0200 Subject: [PATCH 23/25] poprawki --- templates/admin/admin_lists_access.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/admin_lists_access.html b/templates/admin/admin_lists_access.html index 363c73f..89fb241 100644 --- a/templates/admin/admin_lists_access.html +++ b/templates/admin/admin_lists_access.html @@ -3,10 +3,10 @@ {% block content %}
    -

    🔐{% if list_id %} Zarządzanie dostępem #{{ list_id }}{% else %} Zarządzanie dostępem do list {% endif %}

    +

    🔐{% if list_id %} Zarządzanie dostępem listy #{{ list_id }}{% else %} Zarządzanie dostępem do list {% endif %}

    From 45302341e2d07058207b5cec8930c2ff8fbfd225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 18:56:08 +0200 Subject: [PATCH 24/25] wizualne --- app.py | 34 ++++++++++++++++---------------- templates/admin/admin_panel.html | 12 +++-------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/app.py b/app.py index 3d384d1..4a39ffa 100644 --- a/app.py +++ b/app.py @@ -2847,15 +2847,27 @@ def delete_user(user_id): return redirect(url_for("list_users")) -@app.route("/admin/receipts", defaults={'id': 'all'}) -@app.route("/admin/receipts/") +@app.route("/admin/receipts", methods=["GET"]) +@app.route("/admin/receipts/", methods=["GET"]) @login_required @admin_required -def admin_receipts(id): +def admin_receipts(list_id=None): try: page, per_page = get_page_args(default_per_page=24, max_per_page=200) - if id == "all": + if list_id is not None: + all_receipts = ( + Receipt.query.options(joinedload(Receipt.uploaded_by_user)) + .filter_by(list_id=list_id) + .order_by(Receipt.uploaded_at.desc()) + .all() + ) + receipts_paginated, total_items, total_pages = paginate_items( + all_receipts, page, per_page + ) + orphan_files = [] + id = list_id + else: all_filenames = { r.filename for r in Receipt.query.with_entities(Receipt.filename).all() } @@ -2868,6 +2880,7 @@ def admin_receipts(id): receipts_paginated = pagination.items total_pages = pagination.pages + id = "all" upload_folder = app.config["UPLOAD_FOLDER"] files_on_disk = set(os.listdir(upload_folder)) @@ -2878,25 +2891,12 @@ def admin_receipts(id): and f not in all_filenames and f.startswith("list_") ] - else: - list_id = int(id) - all_receipts = ( - Receipt.query.options(joinedload(Receipt.uploaded_by_user)) - .filter_by(list_id=list_id) - .order_by(Receipt.uploaded_at.desc()) - .all() - ) - receipts_paginated, total_items, total_pages = paginate_items( - all_receipts, page, per_page - ) - orphan_files = [] except ValueError: flash("Nieprawidłowe ID listy.", "danger") return redirect(url_for("admin_panel")) total_filesize = db.session.query(func.sum(Receipt.filesize)).scalar() or 0 - page_filesize = sum(r.filesize or 0 for r in receipts_paginated) query_string = urlencode({k: v for k, v in request.args.items() if k != "page"}) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 6efdd1d..d41f051 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -11,11 +11,10 @@
    @@ -219,7 +218,7 @@ — {{ month_str|replace('-', ' / ') }} {% endif %} - +
    @@ -301,11 +300,6 @@ title="Podgląd produktów"> 👁️ - - - - From eb9187a9650b6c7aa007b0814dae6324624345ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 14 Sep 2025 18:59:00 +0200 Subject: [PATCH 25/25] wizualne --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 4a39ffa..1aace7c 100644 --- a/app.py +++ b/app.py @@ -3034,7 +3034,7 @@ def admin_delete_list(): db.session.delete(lst) db.session.commit() - flash(f"Usunięto {len(ids)} list(y)", "success") + flash(f"Usunięto {len(ids)} list(e/y)", "success") return redirect(request.referrer or url_for("admin_panel"))