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>
750 lines
28 KiB
Python
750 lines
28 KiB
Python
"""
|
|
Group Messages Routes
|
|
=====================
|
|
|
|
Group chat creation, viewing, and messaging.
|
|
"""
|
|
|
|
import os
|
|
from datetime import datetime
|
|
|
|
from flask import render_template, request, redirect, url_for, flash, jsonify
|
|
from flask_login import login_required, current_user
|
|
|
|
from . import bp
|
|
from sqlalchemy.orm import joinedload
|
|
from sqlalchemy import func
|
|
from database import (SessionLocal, User, Company, UserCompanyPermissions,
|
|
MessageGroup, MessageGroupMember, GroupMessage,
|
|
MessageAttachment, UserNotification, UserBlock)
|
|
from extensions import limiter
|
|
from utils.helpers import sanitize_html
|
|
from utils.decorators import member_required
|
|
from email_service import send_email, build_message_notification_email
|
|
from message_upload_service import MessageUploadService
|
|
|
|
|
|
def _get_active_norda_members(db, exclude_user_id):
|
|
"""Pobierz aktywnych członków Nordy do wyboru."""
|
|
users_with_companies = db.query(
|
|
User,
|
|
Company.name.label('company_name')
|
|
).outerjoin(
|
|
UserCompanyPermissions,
|
|
UserCompanyPermissions.user_id == User.id
|
|
).outerjoin(
|
|
Company,
|
|
(Company.id == UserCompanyPermissions.company_id) & (Company.status == 'active')
|
|
).filter(
|
|
User.is_active == True,
|
|
User.is_verified == True,
|
|
User.id != exclude_user_id
|
|
).order_by(User.name).all()
|
|
|
|
seen_ids = set()
|
|
users = []
|
|
for user, company_name in users_with_companies:
|
|
if user.id not in seen_ids:
|
|
seen_ids.add(user.id)
|
|
user._company_name = company_name
|
|
users.append(user)
|
|
return users
|
|
|
|
|
|
def _check_group_access(db, group_id, user_id):
|
|
"""Sprawdź czy użytkownik jest członkiem grupy. Zwraca (group, membership) lub (None, None)."""
|
|
group = db.query(MessageGroup).filter(MessageGroup.id == group_id).first()
|
|
if not group:
|
|
return None, None
|
|
membership = db.query(MessageGroupMember).filter(
|
|
MessageGroupMember.group_id == group_id,
|
|
MessageGroupMember.user_id == user_id
|
|
).first()
|
|
if not membership:
|
|
return None, None
|
|
return group, membership
|
|
|
|
|
|
def _check_block_for_group(db, group_id, target_user_id):
|
|
"""Sprawdź czy dodanie użytkownika do grupy jest zablokowane przez UserBlock."""
|
|
member_ids = [m.user_id for m in db.query(MessageGroupMember.user_id).filter(
|
|
MessageGroupMember.group_id == group_id
|
|
).all()]
|
|
block = db.query(UserBlock).filter(
|
|
((UserBlock.user_id == target_user_id) & (UserBlock.blocked_user_id.in_(member_ids))) |
|
|
((UserBlock.user_id.in_(member_ids)) & (UserBlock.blocked_user_id == target_user_id))
|
|
).first()
|
|
return block is not None
|
|
|
|
|
|
@bp.route('/wiadomosci/nowa-grupa')
|
|
@login_required
|
|
@member_required
|
|
def group_compose():
|
|
"""Formularz tworzenia grupy"""
|
|
db = SessionLocal()
|
|
try:
|
|
users = _get_active_norda_members(db, current_user.id)
|
|
return render_template('messages/group_compose.html', users=users)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/wiadomosci/grupa/utworz', methods=['POST'])
|
|
@login_required
|
|
@member_required
|
|
def group_create():
|
|
"""Utwórz grupę i wyślij pierwszą wiadomość"""
|
|
name = request.form.get('name', '').strip()
|
|
content = sanitize_html(request.form.get('content', '').strip())
|
|
member_ids = request.form.getlist('members', type=int)
|
|
|
|
if not content:
|
|
flash('Treść pierwszej wiadomości jest wymagana.', 'error')
|
|
return redirect(url_for('.group_compose'))
|
|
|
|
if not member_ids:
|
|
flash('Wybierz co najmniej jedną osobę.', 'error')
|
|
return redirect(url_for('.group_compose'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Verify all members exist and are active Norda members
|
|
valid_members = db.query(User).filter(
|
|
User.id.in_(member_ids),
|
|
User.is_active == True,
|
|
User.is_verified == True
|
|
).all()
|
|
|
|
if not valid_members:
|
|
flash('Nie znaleziono wybranych użytkowników.', 'error')
|
|
return redirect(url_for('.group_compose'))
|
|
|
|
# Check blocks
|
|
for member in valid_members:
|
|
block = db.query(UserBlock).filter(
|
|
((UserBlock.user_id == current_user.id) & (UserBlock.blocked_user_id == member.id)) |
|
|
((UserBlock.user_id == member.id) & (UserBlock.blocked_user_id == current_user.id))
|
|
).first()
|
|
if block:
|
|
flash(f'Nie można dodać użytkownika {member.name or member.email} do grupy.', 'error')
|
|
return redirect(url_for('.group_compose'))
|
|
|
|
# Create group
|
|
group = MessageGroup(
|
|
name=name if name else None,
|
|
is_named=bool(name),
|
|
owner_id=current_user.id
|
|
)
|
|
db.add(group)
|
|
db.flush()
|
|
|
|
# Add owner as member
|
|
owner_member = MessageGroupMember(
|
|
group_id=group.id,
|
|
user_id=current_user.id,
|
|
role='owner',
|
|
last_read_at=datetime.now()
|
|
)
|
|
db.add(owner_member)
|
|
|
|
# Add selected members
|
|
for member in valid_members:
|
|
if member.id != current_user.id:
|
|
gm = MessageGroupMember(
|
|
group_id=group.id,
|
|
user_id=member.id,
|
|
role='member',
|
|
added_by_id=current_user.id
|
|
)
|
|
db.add(gm)
|
|
|
|
# Create first message
|
|
msg = GroupMessage(
|
|
group_id=group.id,
|
|
sender_id=current_user.id,
|
|
content=content
|
|
)
|
|
db.add(msg)
|
|
db.flush()
|
|
|
|
# Process attachments
|
|
if request.files.getlist('attachments'):
|
|
upload_service = MessageUploadService(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
files = [f for f in request.files.getlist('attachments') if f and f.filename]
|
|
if files:
|
|
valid_files, errors = upload_service.validate_files(files)
|
|
if errors:
|
|
db.rollback()
|
|
for err in errors:
|
|
flash(err, 'error')
|
|
return redirect(url_for('.group_compose'))
|
|
for f, filename, ext, size, file_content in valid_files:
|
|
stored_filename, _ = upload_service.save_file(file_content, ext)
|
|
att = MessageAttachment(
|
|
group_message_id=msg.id,
|
|
filename=filename,
|
|
stored_filename=stored_filename,
|
|
file_size=size,
|
|
mime_type=upload_service.get_mime_type(ext)
|
|
)
|
|
db.add(att)
|
|
|
|
group.updated_at = datetime.now()
|
|
|
|
# Notifications for all members
|
|
sender_name = current_user.name or current_user.email.split('@')[0]
|
|
group_display = name if name else 'Nowa grupa'
|
|
for member in valid_members:
|
|
if member.id != current_user.id:
|
|
notif = UserNotification(
|
|
user_id=member.id,
|
|
title=f'Dodano do grupy: {group_display}',
|
|
message=f'{sender_name} utworzył(a) grupę i wysłał(a) pierwszą wiadomość',
|
|
notification_type='message',
|
|
related_type='group',
|
|
related_id=group.id,
|
|
action_url=url_for('.group_view', group_id=group.id)
|
|
)
|
|
db.add(notif)
|
|
|
|
# Email notification
|
|
if member.notify_email_messages != False and member.email:
|
|
try:
|
|
message_url = url_for('.group_view', group_id=group.id, _external=True)
|
|
settings_url = url_for('auth.konto_prywatnosc', _external=True)
|
|
preview = (content[:200] + '...') if len(content) > 200 else content
|
|
email_html, email_text = build_message_notification_email(
|
|
sender_name=sender_name,
|
|
subject=f'Grupa: {group_display}',
|
|
content_preview=preview,
|
|
message_url=message_url,
|
|
settings_url=settings_url
|
|
)
|
|
send_email(
|
|
to=[member.email],
|
|
subject=f'Nowa grupa: {group_display} — Norda Biznes',
|
|
body_text=email_text,
|
|
body_html=email_html,
|
|
email_type='message_notification',
|
|
user_id=member.id,
|
|
recipient_name=member.name
|
|
)
|
|
except Exception:
|
|
import logging
|
|
logging.getLogger(__name__).warning(f"Failed to send group email to {member.email}")
|
|
|
|
db.commit()
|
|
flash('Grupa utworzona!', 'success')
|
|
return redirect(url_for('.group_view', group_id=group.id))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/wiadomosci/grupa/<int:group_id>')
|
|
@login_required
|
|
@member_required
|
|
def group_view(group_id):
|
|
"""Widok czatu grupowego"""
|
|
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'))
|
|
|
|
# Load messages with senders and attachments
|
|
messages = db.query(GroupMessage).options(
|
|
joinedload(GroupMessage.sender),
|
|
joinedload(GroupMessage.attachments)
|
|
).filter(
|
|
GroupMessage.group_id == group_id
|
|
).order_by(GroupMessage.created_at.asc()).all()
|
|
|
|
# Load members with user details
|
|
members = db.query(MessageGroupMember).options(
|
|
joinedload(MessageGroupMember.user)
|
|
).filter(
|
|
MessageGroupMember.group_id == group_id
|
|
).order_by(MessageGroupMember.role, MessageGroupMember.joined_at).all()
|
|
|
|
# Build read receipts: for each message, find who has read up to that point
|
|
# Show avatar at the LAST message they've read (like Teams/WhatsApp)
|
|
read_receipts = {} # message_id -> list of (user, avatar_path)
|
|
if messages:
|
|
other_members = [m for m in members if m.user_id != current_user.id]
|
|
for m in other_members:
|
|
if not m.last_read_at:
|
|
continue
|
|
# Find the last message this member has read
|
|
last_read_msg = None
|
|
for msg in messages:
|
|
if msg.created_at <= m.last_read_at:
|
|
last_read_msg = msg
|
|
else:
|
|
break
|
|
if last_read_msg:
|
|
if last_read_msg.id not in read_receipts:
|
|
read_receipts[last_read_msg.id] = []
|
|
read_receipts[last_read_msg.id].append(m.user)
|
|
|
|
# Mark as read
|
|
membership.last_read_at = datetime.now()
|
|
db.commit()
|
|
|
|
return render_template('messages/group_view.html',
|
|
group=group,
|
|
messages=messages,
|
|
members=members,
|
|
membership=membership,
|
|
read_receipts=read_receipts
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/wiadomosci/grupa/<int:group_id>/wyslij', methods=['POST'])
|
|
@login_required
|
|
@member_required
|
|
def group_send(group_id):
|
|
"""Wyślij wiadomość do grupy"""
|
|
content = sanitize_html(request.form.get('content', '').strip())
|
|
|
|
if not content:
|
|
flash('Treść jest wymagana.', 'error')
|
|
return redirect(url_for('.group_view', group_id=group_id))
|
|
|
|
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 = GroupMessage(
|
|
group_id=group_id,
|
|
sender_id=current_user.id,
|
|
content=content
|
|
)
|
|
db.add(msg)
|
|
db.flush()
|
|
|
|
# Process attachments
|
|
if request.files.getlist('attachments'):
|
|
upload_service = MessageUploadService(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
files = [f for f in request.files.getlist('attachments') if f and f.filename]
|
|
if files:
|
|
valid_files, errors = upload_service.validate_files(files)
|
|
if errors:
|
|
db.rollback()
|
|
for err in errors:
|
|
flash(err, 'error')
|
|
return redirect(url_for('.group_view', group_id=group_id))
|
|
for f, filename, ext, size, file_content in valid_files:
|
|
stored_filename, _ = upload_service.save_file(file_content, ext)
|
|
att = MessageAttachment(
|
|
group_message_id=msg.id,
|
|
filename=filename,
|
|
stored_filename=stored_filename,
|
|
file_size=size,
|
|
mime_type=upload_service.get_mime_type(ext)
|
|
)
|
|
db.add(att)
|
|
|
|
group.updated_at = datetime.now()
|
|
membership.last_read_at = datetime.now()
|
|
|
|
# Notify all other members
|
|
sender_name = current_user.name or current_user.email.split('@')[0]
|
|
group_display = group.name or group.display_name
|
|
other_members = db.query(MessageGroupMember).options(
|
|
joinedload(MessageGroupMember.user)
|
|
).filter(
|
|
MessageGroupMember.group_id == group_id,
|
|
MessageGroupMember.user_id != current_user.id
|
|
).all()
|
|
|
|
for m in other_members:
|
|
notif = UserNotification(
|
|
user_id=m.user_id,
|
|
title=f'{group_display} — nowa wiadomość',
|
|
message=f'{sender_name}: {content[:80]}...' if len(content) > 80 else f'{sender_name}: {content}',
|
|
notification_type='message',
|
|
related_type='group',
|
|
related_id=group.id,
|
|
action_url=url_for('.group_view', group_id=group_id)
|
|
)
|
|
db.add(notif)
|
|
|
|
if m.user and m.user.notify_email_messages != False and m.user.email:
|
|
try:
|
|
message_url = url_for('.group_view', group_id=group_id, _external=True)
|
|
settings_url = url_for('auth.konto_prywatnosc', _external=True)
|
|
preview = (content[:200] + '...') if len(content) > 200 else content
|
|
email_html, email_text = build_message_notification_email(
|
|
sender_name=sender_name,
|
|
subject=f'Grupa: {group_display}',
|
|
content_preview=preview,
|
|
message_url=message_url,
|
|
settings_url=settings_url
|
|
)
|
|
send_email(
|
|
to=[m.user.email],
|
|
subject=f'{group_display} — nowa wiadomość od {sender_name}',
|
|
body_text=email_text,
|
|
body_html=email_html,
|
|
email_type='message_notification',
|
|
user_id=m.user_id,
|
|
recipient_name=m.user.name
|
|
)
|
|
except Exception:
|
|
import logging
|
|
logging.getLogger(__name__).warning(f"Failed to send group email to {m.user.email}")
|
|
|
|
db.commit()
|
|
return redirect(url_for('.group_view', group_id=group_id))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/api/grupa/<int:group_id>/nowe', methods=['GET'])
|
|
@limiter.exempt
|
|
@login_required
|
|
@member_required
|
|
def group_poll_messages(group_id):
|
|
"""API: Pobierz nowe wiadomości po danym ID (polling)"""
|
|
after_id = request.args.get('after', 0, type=int)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
group, membership = _check_group_access(db, group_id, current_user.id)
|
|
if not group:
|
|
return jsonify({'messages': []})
|
|
|
|
new_msgs = db.query(GroupMessage).options(
|
|
joinedload(GroupMessage.sender)
|
|
).filter(
|
|
GroupMessage.group_id == group_id,
|
|
GroupMessage.id > after_id
|
|
).order_by(GroupMessage.created_at.asc()).all()
|
|
|
|
# Update read timestamp
|
|
if new_msgs:
|
|
membership.last_read_at = datetime.now()
|
|
db.commit()
|
|
|
|
# Build read receipts for new messages
|
|
members = db.query(MessageGroupMember).options(
|
|
joinedload(MessageGroupMember.user)
|
|
).filter(
|
|
MessageGroupMember.group_id == group_id,
|
|
MessageGroupMember.user_id != current_user.id
|
|
).all()
|
|
|
|
read_receipts = {}
|
|
for m in members:
|
|
if not m.last_read_at:
|
|
continue
|
|
for msg in reversed(new_msgs):
|
|
if msg.created_at <= m.last_read_at:
|
|
if msg.id not in read_receipts:
|
|
read_receipts[msg.id] = []
|
|
read_receipts[msg.id].append({
|
|
'name': m.user.name or m.user.email.split('@')[0],
|
|
'avatar_url': ('/static/' + m.user.avatar_path) if m.user.avatar_path else None,
|
|
'initial': (m.user.name or m.user.email)[0].upper()
|
|
})
|
|
break
|
|
|
|
result = []
|
|
for msg in new_msgs:
|
|
sender = msg.sender
|
|
result.append({
|
|
'id': msg.id,
|
|
'sender_id': msg.sender_id,
|
|
'sender_name': sender.name or sender.email.split('@')[0] if sender else 'Ktoś',
|
|
'sender_avatar': ('/static/' + sender.avatar_path) if sender and sender.avatar_path else None,
|
|
'sender_initial': (sender.name or sender.email)[0].upper() if sender else '?',
|
|
'is_me': msg.sender_id == current_user.id,
|
|
'content': msg.content,
|
|
'time': msg.created_at.strftime('%d.%m.%Y %H:%M'),
|
|
'read_by': read_receipts.get(msg.id, [])
|
|
})
|
|
|
|
return jsonify({'messages': result})
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# GROUP MANAGEMENT ROUTES
|
|
# ============================================================
|
|
|
|
@bp.route('/wiadomosci/grupa/<int:group_id>/zarzadzaj')
|
|
@login_required
|
|
@member_required
|
|
def group_manage(group_id):
|
|
"""Panel zarządzania członkami grupy"""
|
|
db = SessionLocal()
|
|
try:
|
|
group, membership = _check_group_access(db, group_id, current_user.id)
|
|
if not group or not membership.can_manage_members:
|
|
flash('Brak uprawnień do zarządzania grupą.', 'error')
|
|
return redirect(url_for('.group_view', group_id=group_id))
|
|
|
|
members = db.query(MessageGroupMember).options(
|
|
joinedload(MessageGroupMember.user)
|
|
).filter(
|
|
MessageGroupMember.group_id == group_id
|
|
).order_by(MessageGroupMember.role, MessageGroupMember.joined_at).all()
|
|
|
|
available_users = _get_active_norda_members(db, current_user.id)
|
|
member_ids = {m.user_id for m in members}
|
|
available_users = [u for u in available_users if u.id not in member_ids]
|
|
|
|
return render_template('messages/group_manage.html',
|
|
group=group,
|
|
members=members,
|
|
membership=membership,
|
|
available_users=available_users
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/wiadomosci/grupa/<int:group_id>/dodaj-czlonka', methods=['POST'])
|
|
@login_required
|
|
@member_required
|
|
def group_add_member(group_id):
|
|
"""Dodaj osobę do grupy"""
|
|
user_id = request.form.get('user_id', type=int)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
group, membership = _check_group_access(db, group_id, current_user.id)
|
|
if not group or not membership.can_manage_members:
|
|
flash('Brak uprawnień.', 'error')
|
|
return redirect(url_for('.group_view', group_id=group_id))
|
|
|
|
if not user_id:
|
|
flash('Nie wybrano użytkownika.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
# Check user exists and is active
|
|
user = db.query(User).filter(User.id == user_id, User.is_active == True).first()
|
|
if not user:
|
|
flash('Użytkownik nie istnieje.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
# Check not already member
|
|
existing = db.query(MessageGroupMember).filter(
|
|
MessageGroupMember.group_id == group_id,
|
|
MessageGroupMember.user_id == user_id
|
|
).first()
|
|
if existing:
|
|
flash('Użytkownik jest już członkiem grupy.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
# Check blocks
|
|
if _check_block_for_group(db, group_id, user_id):
|
|
flash('Nie można dodać tego użytkownika do grupy.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
new_member = MessageGroupMember(
|
|
group_id=group_id,
|
|
user_id=user_id,
|
|
role='member',
|
|
added_by_id=current_user.id
|
|
)
|
|
db.add(new_member)
|
|
|
|
# Notification
|
|
adder_name = current_user.name or current_user.email.split('@')[0]
|
|
group_display = group.name or group.display_name
|
|
notif = UserNotification(
|
|
user_id=user_id,
|
|
title=f'Dodano do grupy: {group_display}',
|
|
message=f'{adder_name} dodał(a) Cię do grupy',
|
|
notification_type='message',
|
|
related_type='group',
|
|
related_id=group.id,
|
|
action_url=url_for('.group_view', group_id=group_id)
|
|
)
|
|
db.add(notif)
|
|
|
|
db.commit()
|
|
flash(f'Dodano {user.name or user.email} do grupy.', 'success')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/wiadomosci/grupa/<int:group_id>/usun-czlonka', methods=['POST'])
|
|
@login_required
|
|
@member_required
|
|
def group_remove_member(group_id):
|
|
"""Usuń osobę z grupy"""
|
|
user_id = request.form.get('user_id', type=int)
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
group, membership = _check_group_access(db, group_id, current_user.id)
|
|
if not group or not membership.can_manage_members:
|
|
flash('Brak uprawnień.', 'error')
|
|
return redirect(url_for('.group_view', group_id=group_id))
|
|
|
|
if user_id == group.owner_id:
|
|
flash('Nie można usunąć właściciela grupy.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
target = db.query(MessageGroupMember).filter(
|
|
MessageGroupMember.group_id == group_id,
|
|
MessageGroupMember.user_id == user_id
|
|
).first()
|
|
|
|
if not target:
|
|
flash('Użytkownik nie jest członkiem grupy.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
# Moderator cannot remove another moderator (only owner can)
|
|
if target.role == 'moderator' and not membership.is_owner:
|
|
flash('Tylko właściciel może usunąć moderatora.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
db.delete(target)
|
|
db.commit()
|
|
flash(f'Usunięto {user.name or user.email if user else "użytkownika"} z grupy.', 'success')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/wiadomosci/grupa/<int:group_id>/zmien-role', methods=['POST'])
|
|
@login_required
|
|
@member_required
|
|
def group_change_role(group_id):
|
|
"""Zmień rolę członka (tylko owner)"""
|
|
user_id = request.form.get('user_id', type=int)
|
|
new_role = request.form.get('role')
|
|
|
|
if new_role not in ('moderator', 'member'):
|
|
flash('Nieprawidłowa rola.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
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 zmieniać role.', 'error')
|
|
return redirect(url_for('.group_view', group_id=group_id))
|
|
|
|
target = db.query(MessageGroupMember).filter(
|
|
MessageGroupMember.group_id == group_id,
|
|
MessageGroupMember.user_id == user_id
|
|
).first()
|
|
|
|
if not target or target.is_owner:
|
|
flash('Nie można zmienić roli tego użytkownika.', 'error')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
|
|
target.role = new_role
|
|
db.commit()
|
|
|
|
role_label = 'moderatora' if new_role == 'moderator' else 'uczestnika'
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
flash(f'{user.name or user.email if user else "Użytkownik"} — rola zmieniona na {role_label}.', 'success')
|
|
return redirect(url_for('.group_manage', group_id=group_id))
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@bp.route('/wiadomosci/grupa/<int:group_id>/edytuj', methods=['POST'])
|
|
@login_required
|
|
@member_required
|
|
def group_edit(group_id):
|
|
"""Edytuj nazwę i opis grupy (tylko owner)"""
|
|
name = request.form.get('name', '').strip()
|
|
description = request.form.get('description', '').strip()
|
|
|
|
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 edytować grupę.', 'error')
|
|
return redirect(url_for('.group_view', group_id=group_id))
|
|
|
|
group.name = name if name else None
|
|
group.is_named = bool(name)
|
|
group.description = description if description else None
|
|
db.commit()
|
|
flash('Grupa zaktualizowana.', 'success')
|
|
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()
|