diff --git a/app.py b/app.py index 80b27a2..9c83970 100644 --- a/app.py +++ b/app.py @@ -2219,346 +2219,6 @@ def admin_calendar_delete(event_id): db.close() -# ============================================================ -# PRIVATE MESSAGES ROUTES -# ============================================================ - -@app.route('/wiadomosci') -@login_required -def messages_inbox(): - """Skrzynka odbiorcza""" - page = request.args.get('page', 1, type=int) - per_page = 20 - - db = SessionLocal() - try: - query = db.query(PrivateMessage).filter( - PrivateMessage.recipient_id == current_user.id - ).order_by(PrivateMessage.created_at.desc()) - - total = query.count() - messages = query.limit(per_page).offset((page - 1) * per_page).all() - - unread_count = db.query(PrivateMessage).filter( - PrivateMessage.recipient_id == current_user.id, - PrivateMessage.is_read == False - ).count() - - return render_template('messages/inbox.html', - messages=messages, - page=page, - total_pages=(total + per_page - 1) // per_page, - unread_count=unread_count - ) - finally: - db.close() - - -@app.route('/wiadomosci/wyslane') -@login_required -def messages_sent(): - """Wysłane wiadomości""" - page = request.args.get('page', 1, type=int) - per_page = 20 - - db = SessionLocal() - try: - query = db.query(PrivateMessage).filter( - PrivateMessage.sender_id == current_user.id - ).order_by(PrivateMessage.created_at.desc()) - - total = query.count() - messages = query.limit(per_page).offset((page - 1) * per_page).all() - - return render_template('messages/sent.html', - messages=messages, - page=page, - total_pages=(total + per_page - 1) // per_page - ) - finally: - db.close() - - -@app.route('/wiadomosci/nowa') -@login_required -def messages_new(): - """Formularz nowej wiadomości""" - recipient_id = request.args.get('to', type=int) - - db = SessionLocal() - try: - # Lista użytkowników do wyboru - users = db.query(User).filter( - User.is_active == True, - User.is_verified == True, - User.id != current_user.id - ).order_by(User.name).all() - - recipient = None - if recipient_id: - recipient = db.query(User).filter(User.id == recipient_id).first() - - return render_template('messages/compose.html', - users=users, - recipient=recipient - ) - finally: - db.close() - - -@app.route('/wiadomosci/wyslij', methods=['POST']) -@login_required -def messages_send(): - """Wyślij wiadomość""" - recipient_id = request.form.get('recipient_id', type=int) - subject = sanitize_input(request.form.get('subject', ''), 255) - content = request.form.get('content', '').strip() - - if not recipient_id or not content: - flash('Odbiorca i treść są wymagane.', 'error') - return redirect(url_for('messages_new')) - - db = SessionLocal() - try: - recipient = db.query(User).filter(User.id == recipient_id).first() - if not recipient: - flash('Odbiorca nie istnieje.', 'error') - return redirect(url_for('messages_new')) - - # Check if either user has blocked the other - block_exists = db.query(UserBlock).filter( - ((UserBlock.user_id == current_user.id) & (UserBlock.blocked_user_id == recipient_id)) | - ((UserBlock.user_id == recipient_id) & (UserBlock.blocked_user_id == current_user.id)) - ).first() - if block_exists: - flash('Nie można wysłać wiadomości do tego użytkownika.', 'error') - return redirect(url_for('messages_new')) - - message = PrivateMessage( - sender_id=current_user.id, - recipient_id=recipient_id, - subject=subject, - content=content - ) - db.add(message) - db.commit() - - flash('Wiadomość wysłana.', 'success') - return redirect(url_for('messages_sent')) - finally: - db.close() - - -@app.route('/wiadomosci/') -@login_required -def messages_view(message_id): - """Czytaj wiadomość""" - db = SessionLocal() - try: - message = db.query(PrivateMessage).filter( - PrivateMessage.id == message_id - ).first() - - if not message: - flash('Wiadomość nie istnieje.', 'error') - return redirect(url_for('messages_inbox')) - - # Sprawdź dostęp - if message.recipient_id != current_user.id and message.sender_id != current_user.id: - flash('Brak dostępu do tej wiadomości.', 'error') - return redirect(url_for('messages_inbox')) - - # Oznacz jako przeczytaną - if message.recipient_id == current_user.id and not message.is_read: - message.is_read = True - message.read_at = datetime.now() - db.commit() - - return render_template('messages/view.html', message=message) - finally: - db.close() - - -@app.route('/wiadomosci//odpowiedz', methods=['POST']) -@login_required -def messages_reply(message_id): - """Odpowiedz na wiadomość""" - content = request.form.get('content', '').strip() - - if not content: - flash('Treść jest wymagana.', 'error') - return redirect(url_for('messages_view', message_id=message_id)) - - db = SessionLocal() - try: - original = db.query(PrivateMessage).filter( - PrivateMessage.id == message_id - ).first() - - if not original: - flash('Wiadomość nie istnieje.', 'error') - return redirect(url_for('messages_inbox')) - - # Odpowiedz do nadawcy oryginalnej wiadomości - recipient_id = original.sender_id if original.sender_id != current_user.id else original.recipient_id - - # Check if either user has blocked the other - block_exists = db.query(UserBlock).filter( - ((UserBlock.user_id == current_user.id) & (UserBlock.blocked_user_id == recipient_id)) | - ((UserBlock.user_id == recipient_id) & (UserBlock.blocked_user_id == current_user.id)) - ).first() - if block_exists: - flash('Nie można wysłać wiadomości do tego użytkownika.', 'error') - return redirect(url_for('messages_inbox')) - - reply = PrivateMessage( - sender_id=current_user.id, - recipient_id=recipient_id, - subject=f"Re: {original.subject}" if original.subject else None, - content=content, - parent_id=message_id - ) - db.add(reply) - db.commit() - - flash('Odpowiedź wysłana.', 'success') - return redirect(url_for('messages_view', message_id=message_id)) - finally: - db.close() - - -@app.route('/api/messages/unread-count') -@login_required -def api_unread_count(): - """API: Liczba nieprzeczytanych wiadomości""" - db = SessionLocal() - try: - count = db.query(PrivateMessage).filter( - PrivateMessage.recipient_id == current_user.id, - PrivateMessage.is_read == False - ).count() - return jsonify({'count': count}) - finally: - db.close() - - -# ============================================================ -# NOTIFICATIONS API ROUTES -# ============================================================ - -@app.route('/api/notifications') -@login_required -def api_notifications(): - """API: Get user notifications""" - limit = request.args.get('limit', 20, type=int) - offset = request.args.get('offset', 0, type=int) - unread_only = request.args.get('unread_only', 'false').lower() == 'true' - - db = SessionLocal() - try: - query = db.query(UserNotification).filter( - UserNotification.user_id == current_user.id - ) - - if unread_only: - query = query.filter(UserNotification.is_read == False) - - # Order by most recent first - query = query.order_by(UserNotification.created_at.desc()) - - total = query.count() - notifications = query.limit(limit).offset(offset).all() - - return jsonify({ - 'success': True, - 'notifications': [ - { - 'id': n.id, - 'title': n.title, - 'message': n.message, - 'notification_type': n.notification_type, - 'related_type': n.related_type, - 'related_id': n.related_id, - 'action_url': n.action_url, - 'is_read': n.is_read, - 'created_at': n.created_at.isoformat() if n.created_at else None - } - for n in notifications - ], - 'total': total, - 'unread_count': db.query(UserNotification).filter( - UserNotification.user_id == current_user.id, - UserNotification.is_read == False - ).count() - }) - finally: - db.close() - - -@app.route('/api/notifications//read', methods=['POST']) -@login_required -def api_notification_mark_read(notification_id): - """API: Mark notification as read""" - db = SessionLocal() - try: - notification = db.query(UserNotification).filter( - UserNotification.id == notification_id, - UserNotification.user_id == current_user.id - ).first() - - if not notification: - return jsonify({'success': False, 'error': 'Powiadomienie nie znalezione'}), 404 - - notification.mark_as_read() - db.commit() - - return jsonify({ - 'success': True, - 'message': 'Oznaczono jako przeczytane' - }) - finally: - db.close() - - -@app.route('/api/notifications/read-all', methods=['POST']) -@login_required -def api_notifications_mark_all_read(): - """API: Mark all notifications as read""" - db = SessionLocal() - try: - updated = db.query(UserNotification).filter( - UserNotification.user_id == current_user.id, - UserNotification.is_read == False - ).update({ - UserNotification.is_read: True, - UserNotification.read_at: datetime.now() - }) - db.commit() - - return jsonify({ - 'success': True, - 'message': f'Oznaczono {updated} powiadomien jako przeczytane', - 'count': updated - }) - finally: - db.close() - - -@app.route('/api/notifications/unread-count') -@login_required -def api_notifications_unread_count(): - """API: Get unread notifications count""" - db = SessionLocal() - try: - count = db.query(UserNotification).filter( - UserNotification.user_id == current_user.id, - UserNotification.is_read == False - ).count() - return jsonify({'count': count}) - finally: - db.close() - - # ============================================================ # USER ANALYTICS API ROUTES # ============================================================ diff --git a/blueprints/__init__.py b/blueprints/__init__.py index b2d6009..2433825 100644 --- a/blueprints/__init__.py +++ b/blueprints/__init__.py @@ -145,7 +145,33 @@ def register_blueprints(app): except Exception as e: logger.error(f"Error registering forum blueprint: {e}") - # Phase 4-10: Future blueprints will be added here + # Phase 4: Messages + Notifications blueprint + try: + from blueprints.messages import bp as messages_bp + app.register_blueprint(messages_bp) + logger.info("Registered blueprint: messages") + + # Create aliases for backward compatibility + _create_endpoint_aliases(app, messages_bp, { + 'messages_inbox': 'messages.messages_inbox', + 'messages_sent': 'messages.messages_sent', + 'messages_new': 'messages.messages_new', + 'messages_send': 'messages.messages_send', + 'messages_view': 'messages.messages_view', + 'messages_reply': 'messages.messages_reply', + 'api_unread_count': 'messages.api_unread_count', + 'api_notifications': 'messages.api_notifications', + 'api_notification_mark_read': 'messages.api_notification_mark_read', + 'api_notifications_mark_all_read': 'messages.api_notifications_mark_all_read', + 'api_notifications_unread_count': 'messages.api_notifications_unread_count', + }) + logger.info("Created messages endpoint aliases") + except ImportError as e: + logger.debug(f"Blueprint messages not yet available: {e}") + except Exception as e: + logger.error(f"Error registering messages blueprint: {e}") + + # Phase 5-10: Future blueprints will be added here def _create_endpoint_aliases(app, blueprint, aliases): diff --git a/blueprints/messages/__init__.py b/blueprints/messages/__init__.py new file mode 100644 index 0000000..580abb2 --- /dev/null +++ b/blueprints/messages/__init__.py @@ -0,0 +1,12 @@ +""" +Messages Blueprint +================== + +Private messages and notifications routes. +""" + +from flask import Blueprint + +bp = Blueprint('messages', __name__) + +from . import routes # noqa: E402, F401 diff --git a/blueprints/messages/routes.py b/blueprints/messages/routes.py new file mode 100644 index 0000000..0d86c00 --- /dev/null +++ b/blueprints/messages/routes.py @@ -0,0 +1,355 @@ +""" +Messages Routes +=============== + +Private messages and notifications API. +""" + +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 database import SessionLocal, User, PrivateMessage, UserNotification, UserBlock +from utils.helpers import sanitize_input + + +# ============================================================ +# PRIVATE MESSAGES ROUTES +# ============================================================ + +@bp.route('/wiadomosci') +@login_required +def messages_inbox(): + """Skrzynka odbiorcza""" + page = request.args.get('page', 1, type=int) + per_page = 20 + + db = SessionLocal() + try: + query = db.query(PrivateMessage).filter( + PrivateMessage.recipient_id == current_user.id + ).order_by(PrivateMessage.created_at.desc()) + + total = query.count() + messages = query.limit(per_page).offset((page - 1) * per_page).all() + + unread_count = db.query(PrivateMessage).filter( + PrivateMessage.recipient_id == current_user.id, + PrivateMessage.is_read == False + ).count() + + return render_template('messages/inbox.html', + messages=messages, + page=page, + total_pages=(total + per_page - 1) // per_page, + unread_count=unread_count + ) + finally: + db.close() + + +@bp.route('/wiadomosci/wyslane') +@login_required +def messages_sent(): + """Wysłane wiadomości""" + page = request.args.get('page', 1, type=int) + per_page = 20 + + db = SessionLocal() + try: + query = db.query(PrivateMessage).filter( + PrivateMessage.sender_id == current_user.id + ).order_by(PrivateMessage.created_at.desc()) + + total = query.count() + messages = query.limit(per_page).offset((page - 1) * per_page).all() + + return render_template('messages/sent.html', + messages=messages, + page=page, + total_pages=(total + per_page - 1) // per_page + ) + finally: + db.close() + + +@bp.route('/wiadomosci/nowa') +@login_required +def messages_new(): + """Formularz nowej wiadomości""" + recipient_id = request.args.get('to', type=int) + + db = SessionLocal() + try: + # Lista użytkowników do wyboru + users = db.query(User).filter( + User.is_active == True, + User.is_verified == True, + User.id != current_user.id + ).order_by(User.name).all() + + recipient = None + if recipient_id: + recipient = db.query(User).filter(User.id == recipient_id).first() + + return render_template('messages/compose.html', + users=users, + recipient=recipient + ) + finally: + db.close() + + +@bp.route('/wiadomosci/wyslij', methods=['POST']) +@login_required +def messages_send(): + """Wyślij wiadomość""" + recipient_id = request.form.get('recipient_id', type=int) + subject = sanitize_input(request.form.get('subject', ''), 255) + content = request.form.get('content', '').strip() + + if not recipient_id or not content: + flash('Odbiorca i treść są wymagane.', 'error') + return redirect(url_for('.messages_new')) + + db = SessionLocal() + try: + recipient = db.query(User).filter(User.id == recipient_id).first() + if not recipient: + flash('Odbiorca nie istnieje.', 'error') + return redirect(url_for('.messages_new')) + + # Check if either user has blocked the other + block_exists = db.query(UserBlock).filter( + ((UserBlock.user_id == current_user.id) & (UserBlock.blocked_user_id == recipient_id)) | + ((UserBlock.user_id == recipient_id) & (UserBlock.blocked_user_id == current_user.id)) + ).first() + if block_exists: + flash('Nie można wysłać wiadomości do tego użytkownika.', 'error') + return redirect(url_for('.messages_new')) + + message = PrivateMessage( + sender_id=current_user.id, + recipient_id=recipient_id, + subject=subject, + content=content + ) + db.add(message) + db.commit() + + flash('Wiadomość wysłana.', 'success') + return redirect(url_for('.messages_sent')) + finally: + db.close() + + +@bp.route('/wiadomosci/') +@login_required +def messages_view(message_id): + """Czytaj wiadomość""" + db = SessionLocal() + try: + message = db.query(PrivateMessage).filter( + PrivateMessage.id == message_id + ).first() + + if not message: + flash('Wiadomość nie istnieje.', 'error') + return redirect(url_for('.messages_inbox')) + + # Sprawdź dostęp + if message.recipient_id != current_user.id and message.sender_id != current_user.id: + flash('Brak dostępu do tej wiadomości.', 'error') + return redirect(url_for('.messages_inbox')) + + # Oznacz jako przeczytaną + if message.recipient_id == current_user.id and not message.is_read: + message.is_read = True + message.read_at = datetime.now() + db.commit() + + return render_template('messages/view.html', message=message) + finally: + db.close() + + +@bp.route('/wiadomosci//odpowiedz', methods=['POST']) +@login_required +def messages_reply(message_id): + """Odpowiedz na wiadomość""" + content = request.form.get('content', '').strip() + + if not content: + flash('Treść jest wymagana.', 'error') + return redirect(url_for('.messages_view', message_id=message_id)) + + db = SessionLocal() + try: + original = db.query(PrivateMessage).filter( + PrivateMessage.id == message_id + ).first() + + if not original: + flash('Wiadomość nie istnieje.', 'error') + return redirect(url_for('.messages_inbox')) + + # Odpowiedz do nadawcy oryginalnej wiadomości + recipient_id = original.sender_id if original.sender_id != current_user.id else original.recipient_id + + # Check if either user has blocked the other + block_exists = db.query(UserBlock).filter( + ((UserBlock.user_id == current_user.id) & (UserBlock.blocked_user_id == recipient_id)) | + ((UserBlock.user_id == recipient_id) & (UserBlock.blocked_user_id == current_user.id)) + ).first() + if block_exists: + flash('Nie można wysłać wiadomości do tego użytkownika.', 'error') + return redirect(url_for('.messages_inbox')) + + reply = PrivateMessage( + sender_id=current_user.id, + recipient_id=recipient_id, + subject=f"Re: {original.subject}" if original.subject else None, + content=content, + parent_id=message_id + ) + db.add(reply) + db.commit() + + flash('Odpowiedź wysłana.', 'success') + return redirect(url_for('.messages_view', message_id=message_id)) + finally: + db.close() + + +@bp.route('/api/messages/unread-count') +@login_required +def api_unread_count(): + """API: Liczba nieprzeczytanych wiadomości""" + db = SessionLocal() + try: + count = db.query(PrivateMessage).filter( + PrivateMessage.recipient_id == current_user.id, + PrivateMessage.is_read == False + ).count() + return jsonify({'count': count}) + finally: + db.close() + + +# ============================================================ +# NOTIFICATIONS API ROUTES +# ============================================================ + +@bp.route('/api/notifications') +@login_required +def api_notifications(): + """API: Get user notifications""" + limit = request.args.get('limit', 20, type=int) + offset = request.args.get('offset', 0, type=int) + unread_only = request.args.get('unread_only', 'false').lower() == 'true' + + db = SessionLocal() + try: + query = db.query(UserNotification).filter( + UserNotification.user_id == current_user.id + ) + + if unread_only: + query = query.filter(UserNotification.is_read == False) + + # Order by most recent first + query = query.order_by(UserNotification.created_at.desc()) + + total = query.count() + notifications = query.limit(limit).offset(offset).all() + + return jsonify({ + 'success': True, + 'notifications': [ + { + 'id': n.id, + 'title': n.title, + 'message': n.message, + 'notification_type': n.notification_type, + 'related_type': n.related_type, + 'related_id': n.related_id, + 'action_url': n.action_url, + 'is_read': n.is_read, + 'created_at': n.created_at.isoformat() if n.created_at else None + } + for n in notifications + ], + 'total': total, + 'unread_count': db.query(UserNotification).filter( + UserNotification.user_id == current_user.id, + UserNotification.is_read == False + ).count() + }) + finally: + db.close() + + +@bp.route('/api/notifications//read', methods=['POST']) +@login_required +def api_notification_mark_read(notification_id): + """API: Mark notification as read""" + db = SessionLocal() + try: + notification = db.query(UserNotification).filter( + UserNotification.id == notification_id, + UserNotification.user_id == current_user.id + ).first() + + if not notification: + return jsonify({'success': False, 'error': 'Powiadomienie nie znalezione'}), 404 + + notification.mark_as_read() + db.commit() + + return jsonify({ + 'success': True, + 'message': 'Oznaczono jako przeczytane' + }) + finally: + db.close() + + +@bp.route('/api/notifications/read-all', methods=['POST']) +@login_required +def api_notifications_mark_all_read(): + """API: Mark all notifications as read""" + db = SessionLocal() + try: + updated = db.query(UserNotification).filter( + UserNotification.user_id == current_user.id, + UserNotification.is_read == False + ).update({ + UserNotification.is_read: True, + UserNotification.read_at: datetime.now() + }) + db.commit() + + return jsonify({ + 'success': True, + 'message': f'Oznaczono {updated} powiadomien jako przeczytane', + 'count': updated + }) + finally: + db.close() + + +@bp.route('/api/notifications/unread-count') +@login_required +def api_notifications_unread_count(): + """API: Get unread notifications count""" + db = SessionLocal() + try: + count = db.query(UserNotification).filter( + UserNotification.user_id == current_user.id, + UserNotification.is_read == False + ).count() + return jsonify({'count': count}) + finally: + db.close() diff --git a/docs/REFACTORING_STATUS.md b/docs/REFACTORING_STATUS.md index be7371d..3a72ca4 100644 --- a/docs/REFACTORING_STATUS.md +++ b/docs/REFACTORING_STATUS.md @@ -129,7 +129,7 @@ Usuń funkcje z prefiksem `_old_` z app.py. | **1** | reports, community, education | 19 | ✅ WDROŻONA | | **2a** | auth + public + cleanup | 31 | ✅ WDROŻONA | | **3** | forum (10 routes) | 10 | ✅ WDROŻONA | -| **4** | messages, notifications | ~10 | ⏳ | +| **4** | messages + notifications (11 routes) | 11 | ✅ WDROŻONA | | **5** | chat | ~8 | ⏳ | | **6** | admin (8 modułów) | ~60 | ⏳ | | **7** | audits (6 modułów) | ~35 | ⏳ | @@ -165,6 +165,7 @@ Usuń funkcje z prefiksem `_old_` z app.py. - Po Fazie 1: 13,699 linii (-12.0%) - Po Fazie 2a: 13,820 linii (-11.2% od startu) - Po Fazie 3: 13,398 linii (-13.9% od startu) +- Po Fazie 4: 13,058 linii (-16.1% od startu) - **Cel końcowy: ~500 linii** ---