Merge pull request 'permissions' (#11) from permissions into master

Reviewed-on: #11
This commit is contained in:
gru
2025-09-14 19:12:55 +02:00
10 changed files with 952 additions and 362 deletions

921
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -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ą';
}
});
}

View File

@@ -0,0 +1,179 @@
{% extends 'base.html' %}
{% block title %}Zarządzanie dostępem do list{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2 class="mb-2">🔐{% if list_id %} Zarządzanie dostępem listy #{{ list_id }}{% else %} Zarządzanie dostępem do list {% endif %}</h2>
<div>
{% if list_id %}
<a href="{{ url_for('admin_lists_access') }}" class="btn btn-outline-light me-2">Powrót do wszystkich list</a>
{% endif %}
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<form method="post">
<input type="hidden" name="action" value="save_changes">
<div class="table-responsive">
<table class="table table-dark align-middle sortable">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Nazwa listy</th>
<th scope="col">Właściciel</th>
<th scope="col">Utworzono</th>
<th scope="col">Statusy</th>
<th scope="col">Udostępnianie</th>
<th scope="col">Uprawnienia</th>
</tr>
</thead>
<tbody>
{% for l in lists %}
<tr>
<td>
{{ l.id }}
<input type="hidden" name="visible_ids" value="{{ l.id }}">
</td>
<td class="fw-bold align-middle">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title }}</a>
</td>
<td>
{% if l.owner %}
👤 {{ l.owner.username }} ({{ l.owner.id }})
{% else %}-{% endif %}
</td>
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
<td style="min-width: 220px;">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pub_{{ l.id }}"
name="is_public_{{ l.id }}" {% if l.is_public %}checked{% endif %}>
<label class="form-check-label" for="pub_{{ l.id }}">🌐 Publiczna</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="tmp_{{ l.id }}"
name="is_temporary_{{ l.id }}" {% if l.is_temporary %}checked{% endif %}>
<label class="form-check-label" for="tmp_{{ l.id }}">⏳ Tymczasowa</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="arc_{{ l.id }}"
name="is_archived_{{ l.id }}" {% if l.is_archived %}checked{% endif %}>
<label class="form-check-label" for="arc_{{ l.id }}">📦 Archiwalna</label>
</div>
</td>
<td style="min-width: 220px;">
{% if l.share_token %}
{% set share_url = url_for('shared_list', token=l.share_token, _external=True) %}
<div class="d-flex align-items-center gap-2">
<div class="flex-grow-1 text-truncate mono" title="{{ share_url }}">
{{ share_url }}
</div>
</div>
<div class="text-info small">
{% if l.is_public %}Lista widoczna publicznie{% else %}Lista dostępna przez
link/uprawnienia{%
endif %}
</div>
{% else %}
<div class="text-warning small">Brak tokenu</div>
{% endif %}
</td>
<td style="min-width: 320px;">
<ul class="list-group list-group-flush mb-2">
{% for u in permitted_by_list.get(l.id, []) %}
<li
class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center border-secondary">
<div>
<span class="fw-semibold">@{{ u.username }}</span>
</div>
<form method="post" class="m-0"
onsubmit="return confirm('Odebrać dostęp @{{ u.username }}?');">
<input type="hidden" name="action" value="revoke">
<input type="hidden" name="target_list_id" value="{{ l.id }}">
<input type="hidden" name="revoke_user_id" value="{{ u.id }}">
<button type="submit" class="btn btn-sm btn-outline-danger">🚫
Odbierz</button>
</form>
</li>
{% endfor %}
{% if permitted_by_list.get(l.id, [])|length == 0 %}
<li class="list-group-item bg-dark text-white border-secondary">
<div class="text-warning small">Brak dodanych uprawnień.</div>
</li>
{% endif %}
</ul>
<!-- Nadawanie dostępu -->
<form method="post" class="m-0">
<input type="hidden" name="action" value="grant">
<input type="hidden" name="target_list_id" value="{{ l.id }}">
<div class="input-group input-group-sm">
<input type="text" name="grant_username"
class="form-control bg-dark text-white border-secondary"
placeholder="nazwa użytkownika">
<button type="submit" class="btn btn-outline-light"> Dodaj</button>
</div>
</form>
</td>
</tr>
{% endfor %}
{% if lists|length == 0 %}
<tr>
<td colspan="7" class="text-center py-4">Brak list do wyświetlenia</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz zmiany</button>
</div>
</form>
</div>
</div>
{% if not list_id %}
<hr>
<div class="d-flex justify-content-between align-items-center mt-4">
<form method="get" class="d-flex align-items-center">
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
<select id="per_page" name="per_page" class="form-select form-select-sm me-2"
onchange="this.form.page.value = 1; this.form.submit();">
<option value="25" {% if per_page==25 %}selected{% endif %}>25</option>
<option value="50" {% if per_page==50 %}selected{% endif %}>50</option>
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
</select>
<input type="hidden" name="page" value="{{ page }}">
</form>
<nav aria-label="Nawigacja stron">
<ul class="pagination pagination-dark mb-0">
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
<a class="page-link"
href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page - 1 }}">«</a>
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
<a class="page-link"
href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page + 1 }}">»</a>
</li>
</ul>
</nav>
</div>
{% endif %}
{% endblock %}

View File

@@ -11,9 +11,10 @@
<div class="card-body p-2">
<div class="d-flex flex-wrap gap-2">
<a href="{{ url_for('list_users') }}" class="btn btn-outline-light btn-sm">👥 Użytkownicy</a>
<a href="{{ url_for('admin_receipts', id='all') }}" class="btn btn-outline-light btn-sm">📸 Paragony</a>
<a href="{{ url_for('admin_receipts') }}" class="btn btn-outline-light btn-sm">📸 Paragony</a>
<a href="{{ url_for('list_products') }}" class="btn btn-outline-light btn-sm">🛍️ Produkty</a>
<a href="{{ url_for('admin_mass_edit_categories') }}" class="btn btn-outline-light btn-sm">🗂 Kategorie</a>
<a href="{{ url_for('admin_lists_access') }}" class="btn btn-outline-light btn-sm">🔐 Uprawnienia</a>
</div>
</div>
</div>
@@ -217,7 +218,7 @@
<strong>{{ month_str|replace('-', ' / ') }}</strong>
{% endif %}
</h3>
<form method="post" action="{{ url_for('admin_delete_list') }}">
<form method="post" action="{{ url_for('admin_delete_list') }}" onsubmit="return confirm('Na pewno usunąć tę listę?')" class="d-inline">
<div class="table-responsive">
<table class="table table-dark align-middle sortable">
<thead>
@@ -299,11 +300,6 @@
title="Podgląd produktów">
👁️
</button>
<form method="post" action="{{ url_for('admin_delete_list') }}"
onsubmit="return confirm('Na pewno usunąć tę listę?')" class="d-inline">
<input type="hidden" name="single_list_id" value="{{ l.id }}">
<button type="submit" class="btn btn-sm btn-outline-light" title="Usuń">🗑️</button>
</form>
</div>
</td>
</tr>

View File

@@ -117,6 +117,32 @@
value="{{ request.url_root }}share/{{ list.share_token }}">
</div>
<!-- Dostęp / uprawnienia -->
<div class="mb-4 border-top pt-3 mt-4">
<h5 class="mb-3">🔐 Użytkownicy z dostępem</h5>
<a class="btn btn-outline-warning btn-sm mb-3"
href="{{ url_for('admin_lists_access', list_id=list.id) }}">
⚙️ Edytuj uprawnienia
</a>
{% if permitted_users %}
<ul class="list-group list-group-flush mb-3">
{% for u in permitted_users %}
<li
class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center border-secondary">
<div>
<span class="fw-semibold">@{{ u.username }}</span>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div class="text-warning small">Brak dodatkowych uprawnień.</div>
{% endif %}
</div>
<button type="submit" class="btn btn-outline-light btn-sm me-2">💾 Zapisz zmiany</button>
</form>

View File

@@ -20,21 +20,28 @@
{{ (page_filesize / 1024) | round(1) }} kB
{% endif %}
</strong>
|
Łącznie:
<strong>
{% if total_filesize >= 1024*1024 %}
{{ (total_filesize / 1024 / 1024) | round(2) }} MB
{% else %}
{{ (total_filesize / 1024) | round(1) }} kB
{% if not (id != 'all' and (id|string).isdigit()) %}
| Łącznie:
<strong>
{% if total_filesize >= 1024*1024 %}
{{ (total_filesize / 1024 / 1024) | round(2) }} MB
{% else %}
{{ (total_filesize / 1024) | round(1) }} kB
{% endif %}
</strong>
{% endif %}
</strong>
</p>
<div>
<a href="{{ url_for('recalculate_filesizes_all') }}" class="btn btn-outline-light me-2">
Przelicz rozmiary plików
</a>
{% if id is string and id.isdigit() and id|int > 0 %}
<a href="{{ url_for('admin_receipts', id='all') }}" class="btn btn-outline-light me-2">
Pokaż wszystkie paragony
</a>
{% else %}
<a href="{{ url_for('recalculate_filesizes_all') }}" class="btn btn-outline-light me-2">
Przelicz rozmiary plików
</a>
{% endif %}
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
</div>
@@ -118,8 +125,8 @@
</div>
</div>
{% if id == 'all' %}
<hr>
<div class="d-flex justify-content-between align-items-center mt-4">
<form method="get" class="d-flex align-items-center">
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
@@ -149,7 +156,7 @@
</ul>
</nav>
</div>
{% endif %}
{% if orphan_files and request.path.endswith('/all') %}
<hr class="my-4">

View File

@@ -24,13 +24,13 @@
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="public" name="is_public" {% if list.is_public
%}checked{% endif %}>
<label class="form-check-label" for="public">🌐 Publiczna</label>
<label class="form-check-label" for="public">🌐 Publiczna (czyli mogą zobaczyć goście)</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="temporary" name="is_temporary" {% if list.is_temporary
%}checked{% endif %}>
<label class="form-check-label" for="temporary">⏳ Tymczasowa</label>
<label class="form-check-label" for="temporary">⏳ Tymczasowa (ustaw date wygasania)</label>
</div>
<div class="form-check form-switch">
@@ -85,17 +85,81 @@
{% endfor %}
</select>
</div>
<!-- Przyciski -->
<div class="btn-group mt-4" role="group">
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz</button>
<a href="{{ url_for('main_page') }}" class="btn btn-sm btn-outline-light">❌ Anuluj</a>
</div>
</form>
</div>
</div>
<!-- DOSTĘP DO LISTY -->
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
<div class="card-body">
<h5 class="mb-3">🔐 Dostęp do listy</h5>
<!-- Link udostępniania -->
<div class="mb-4">
<label class="form-label">🔗 Link udostępniania (wejście przez link daje dostęp; zalogowani dostają
uprawnienia na stałę po kliknięciu w link)</label>
{% if list.share_token %}
<div class="input-group mb-3">
<input type="text" class="form-control bg-dark text-white border-secondary"
readonly value="{{ url_for('shared_list', token=list.share_token, _external=True) }}"
id="sharedListUrl" aria-label="Udostępniony link">
<a class="btn btn-outline-light" href="{{ url_for('shared_list', token=list.share_token) }}" target="_blank"
title="Otwórz">Otwórz
</a>
</div>
{% else %}
<div class="text-warning small">Brak tokenu udostępniania.</div>
{% endif %}
<div class="text-info small">Ustawienie „🌐 Publiczna” nie jest wymagane dla dostępu z linku.</div>
</div>
<form method="post" class="m-0">
<div class="row g-3 align-items-end mb-4">
<div class="col-md-6">
<label for="grant_username" class="form-label"> Nadaj dostęp użytkownikowi (login)</label>
<input type="text" name="grant_username" id="grant_username"
class="form-control bg-dark text-white border-secondary rounded" placeholder="np. marek">
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-outline-light w-100"> Dodaj</button>
</div>
<!-- opcjonalnie, żeby rozróżnić akcje po stronie serwera -->
<input type="hidden" name="action" value="grant">
<!-- opcjonalnie zachowanie powrotu -->
<input type="hidden" name="next" value="{{ request.path }}">
</div>
</form>
<!-- Lista uprawnionych -->
<div class="mb-3">
<label class="form-label">👥 Użytkownicy z dostępem</label>
{% if permitted_users and permitted_users|length > 0 %}
<ul class="list-group list-group-flush">
{% for u in permitted_users %}
<li class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center border-secondary">
<div>
<span class="fw-semibold">@{{ u.username }}</span>
</div>
<form method="post" onsubmit="return confirm('Odebrać dostęp użytkownikowi @{{ u.username }}?');">
<input type="hidden" name="revoke_user_id" value="{{ u.id }}">
<button type="submit" class="btn btn-sm btn-outline-danger">🚫 Odbierz uprawnienia</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}<br>
<div class="text-warning small">Brak dodanych uprawnień.</div>
{% endif %}
</div>
</div>
</div>
{% if receipts %}
<hr class="my-4">
<h5>Paragony przypisane do tej listy</h5>

View File

@@ -13,7 +13,7 @@
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="showAllLists" {% if show_all %}checked{% endif %}>
<label class="form-check-label ms-2 text-white" for="showAllLists">
Pokaż wszystkie publiczne listy innych
Uwzględnij listy udostępnione dla mnie i publiczne
</label>
</div>
</div>

View File

@@ -35,30 +35,33 @@
<div class="mb-2">
<strong id="share-header">
{% 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 %}
</strong>
<span id="share-url" class="badge rounded-pill bg-secondary text-wrap"
style="font-size: 0.7rem; {% if not list.is_public %}display: none;{% endif %}">
<span id="share-url" class="badge rounded-pill bg-secondary text-wrap" style="font-size: 0.7rem;">
{{ request.url_root }}share/{{ list.share_token }}
</span>
</div>
<div class="d-flex flex-column flex-md-row gap-2">
<button id="copyBtn" class="btn btn-success btn-sm flex-fill"
onclick="copyLink('{{ request.url_root }}share/{{ list.share_token }}')" {% if not list.is_public %}disabled{%
endif %}>
onclick="copyLink('{{ request.url_root }}share/{{ list.share_token }}')">
📋 Skopiuj / Udostępnij
</button>
<button id="toggleVisibilityBtn" class="btn btn-outline-light btn-sm flex-fill"
onclick="toggleVisibility({{ list.id }})">
{% if list.is_public %}
🙈 Ukryj listę
🙈 Ustaw niepubliczną
{% else %}
👁️ Udostępnij ponownie
🐵 Uczyń publiczną
{% endif %}
</button>
<a href="{{ url_for('edit_my_list', list_id=list.id, next=url_for('view_list', list_id=list.id)) }}"
class="btn btn-outline-info btn-sm flex-fill">
Nadaj dostęp
</a>
</div>
</div>
</div>

View File

@@ -63,7 +63,7 @@
Twoje listy
<button type="button" class="btn btn-sm btn-outline-light ms-2" data-bs-toggle="modal"
data-bs-target="#archivedModal">
📁 Zarchiwizowane
🗄️ Zarchiwizowane
</button>
</h3>
{% if user_lists %}
@@ -87,17 +87,17 @@
<div class="btn-group mt-2 mt-md-0" role="group">
<a href="{{ url_for('view_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">📄 Otwórz</a>
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">📂 Otwórz</a>
<a href="{{ url_for('shared_list', token=l.share_token) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">✏️ Odznaczaj</a>
<a href="{{ url_for('copy_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">📋 Kopiuj</a>
<a href="{{ url_for('edit_my_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">⚙️ Ustawienia</a>
<a href="{{ url_for('toggle_visibility', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">
{% if l.is_public %}🙈 Ukryj{% else %}👁️ Odkryj{% endif %}
{% if l.is_public %}🙈 Ukryj{% else %}🐵 Odkryj{% endif %}
</a>
<a href="{{ url_for('edit_my_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">⚙️ Ustawienia</a>
</div>
</div>
@@ -135,21 +135,21 @@
{% endif %}
{% endif %}
<h3 class="mt-4">Publiczne listy innych użytkowników</h3>
{% if public_lists %}
<h3 class="mt-4">Udostępnione i publiczne listy innych użytkowników</h3>
{% set lists_to_show = accessible_lists %}
{% if lists_to_show %}
<ul class="list-group">
{% 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 %}
<li class="list-group-item bg-dark text-white">
<div class="d-flex justify-content-between align-items-center flex-wrap w-100">
<span class="fw-bold">
{{ l.title }} (Autor: {{ l.owner.username }})
{{ l.title }} (Autor: {{ l.owner.username if l.owner else '—' }})
{% for cat in l.category_badges %}
<span class="badge rounded-pill text-dark ms-1" style="background-color: {{ cat.color }};
font-size: 0.56rem;
opacity: 0.85;">
font-size: 0.56rem; opacity: 0.85;">
{{ cat.name }}
</span>
{% endfor %}
@@ -158,37 +158,31 @@
<a href="{{ url_for('shared_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">✏️ Odznaczaj</a>
</div>
<div class="progress progress-dark progress-thin mt-2 position-relative">
{# Kupione #}
<div class="progress-bar bg-success" role="progressbar"
style="width: {{ (purchased_count / total_count * 100) if total_count > 0 else 0 }}%" aria-valuemin="0"
aria-valuemax="100"></div>
{# Niekupione #}
{% set not_purchased_count = l.not_purchased_count if l.total_count else 0 %}
<div class="progress-bar bg-warning" role="progressbar"
style="width: {{ (not_purchased_count / total_count * 100) if total_count > 0 else 0 }}%" aria-valuemin="0"
aria-valuemax="100"></div>
{# Pozostałe #}
<div class="progress-bar bg-transparent" role="progressbar"
style="width: {{ 100 - ((purchased_count + not_purchased_count) / total_count * 100) if total_count > 0 else 100 }}%"
aria-valuemin="0" aria-valuemax="100"></div>
<span class="progress-label small fw-bold
{% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
<span class="progress-label small fw-bold {% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
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 %}
</span>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<p><span class="badge rounded-pill bg-secondary opacity-75">Brak dostępnych list publicznych do wyświetlenia</span></p>
<p><span class="badge rounded-pill bg-secondary opacity-75">Brak list do wyświetlenia</span></p>
{% endif %}
<div class="modal fade" id="archivedModal" tabindex="-1" aria-labelledby="archivedModalLabel" aria-hidden="true">