feat(messages): delete group and delete individual messages
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

- Group owner can delete entire group (danger zone in manage panel)
- Message author or group owner can delete individual messages (trash icon on hover)
- CASCADE deletes attachments from disk

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-20 12:26:50 +01:00
parent cda97c18bb
commit d86e77aef0
3 changed files with 99 additions and 0 deletions

View File

@ -682,3 +682,68 @@ def group_edit(group_id):
return redirect(url_for('.group_manage', group_id=group_id))
finally:
db.close()
@bp.route('/wiadomosci/grupa/<int:group_id>/wiadomosc/<int:message_id>/usun', methods=['POST'])
@login_required
@member_required
def group_delete_message(group_id, message_id):
"""Usuń wiadomość (tylko autor lub owner grupy)"""
db = SessionLocal()
try:
group, membership = _check_group_access(db, group_id, current_user.id)
if not group:
flash('Grupa nie istnieje lub nie masz dostępu.', 'error')
return redirect(url_for('.messages_inbox'))
msg = db.query(GroupMessage).filter(
GroupMessage.id == message_id,
GroupMessage.group_id == group_id
).first()
if not msg:
flash('Wiadomość nie istnieje.', 'error')
return redirect(url_for('.group_view', group_id=group_id))
# Only message author or group owner can delete
if msg.sender_id != current_user.id and not membership.is_owner:
flash('Nie masz uprawnień do usunięcia tej wiadomości.', 'error')
return redirect(url_for('.group_view', group_id=group_id))
# Delete attachments from disk
for att in msg.attachments:
try:
filepath = os.path.join('static', 'uploads', 'messages',
att.created_at.strftime('%Y'), att.created_at.strftime('%m'), att.stored_filename)
if os.path.exists(filepath):
os.remove(filepath)
except Exception:
pass
db.delete(msg)
db.commit()
flash('Wiadomość usunięta.', 'success')
return redirect(url_for('.group_view', group_id=group_id))
finally:
db.close()
@bp.route('/wiadomosci/grupa/<int:group_id>/usun', methods=['POST'])
@login_required
@member_required
def group_delete(group_id):
"""Usuń grupę (tylko owner)"""
db = SessionLocal()
try:
group, membership = _check_group_access(db, group_id, current_user.id)
if not group or not membership.is_owner:
flash('Tylko właściciel może usunąć grupę.', 'error')
return redirect(url_for('.messages_inbox'))
group_name = group.name or group.display_name
db.delete(group)
db.commit()
flash(f'Grupa "{group_name}" została usunięta.', 'success')
return redirect(url_for('.messages_inbox'))
finally:
db.close()

View File

@ -362,6 +362,15 @@
</div>
<button type="submit" class="btn btn-primary">Zapisz zmiany</button>
</form>
<div style="margin-top: var(--spacing-xl); padding-top: var(--spacing-lg); border-top: 1px solid var(--border-color, #e5e7eb);">
<h3 style="color: #dc2626; font-size: var(--font-size-sm); margin-bottom: var(--spacing-sm);">Strefa niebezpieczna</h3>
<p style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-bottom: var(--spacing-sm);">Usunięcie grupy jest nieodwracalne. Wszystkie wiadomości i załączniki zostaną usunięte.</p>
<form method="POST" action="{{ url_for('messages.group_delete', group_id=group.id) }}" onsubmit="return confirm('Czy na pewno chcesz usunąć tę grupę? Wszystkie wiadomości zostaną usunięte. Tej operacji nie można cofnąć.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn" style="background: #dc2626; color: white; border: none; font-size: var(--font-size-sm);">Usuń grupę</button>
</form>
</div>
</div>
{% endif %}

View File

@ -226,6 +226,25 @@
color: var(--text-secondary);
}
.msg-delete-btn {
background: none;
border: none;
cursor: pointer;
font-size: 12px;
opacity: 0;
transition: opacity 0.15s;
padding: 2px;
line-height: 1;
}
.msg-header:hover .msg-delete-btn {
opacity: 0.5;
}
.msg-delete-btn:hover {
opacity: 1 !important;
}
.msg-content {
background: var(--surface);
border: 1px solid var(--border-color, #e5e7eb);
@ -436,6 +455,12 @@
<div class="msg-header">
<span class="msg-sender">{% if msg.sender_id == current_user.id %}Ty{% else %}<a href="{{ url_for('public.user_profile', user_id=msg.sender_id) }}" style="color: inherit; text-decoration: none;" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">{{ msg.sender.name or msg.sender.email.split('@')[0] }}</a>{% endif %}</span>
<span class="msg-time">{{ msg.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
{% if msg.sender_id == current_user.id or membership.is_owner %}
<form method="POST" action="{{ url_for('messages.group_delete_message', group_id=group.id, message_id=msg.id) }}" style="display:inline; margin-left: 4px;" onsubmit="return confirm('Usunąć tę wiadomość?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="msg-delete-btn" title="Usuń wiadomość">🗑</button>
</form>
{% endif %}
</div>
<div class="msg-content">{{ msg.content|linkify }}</div>
{% if msg.attachments %}