refactor: Extract forum blueprint (Phase 3)

- Create blueprints/forum/ with 10 routes:
  - forum_index, forum_new_topic, forum_topic, forum_reply
  - admin_forum, admin_forum_pin, admin_forum_lock
  - admin_forum_delete_topic, admin_forum_delete_reply
  - admin_forum_change_status
- Register forum blueprint with backward-compatible aliases
- Remove dead code from app.py (-422 lines)
- app.py: 13,820 → 13,398 lines (-3.1%)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-31 07:42:18 +01:00
parent e06d3b172d
commit ad2262388b
5 changed files with 497 additions and 424 deletions

422
app.py
View File

@ -1088,428 +1088,6 @@ def health_full():
# The routes below have been migrated to the public blueprint.
# They are commented out but preserved for reference.
# See: blueprints/public/routes.py
# ============================================================
# FORUM ROUTES
# ============================================================
@app.route('/forum')
@login_required
def forum_index():
"""Forum - list of topics with category/status filters"""
page = request.args.get('page', 1, type=int)
per_page = 20
category_filter = request.args.get('category', '')
status_filter = request.args.get('status', '')
db = SessionLocal()
try:
# Build query with optional filters
query = db.query(ForumTopic)
if category_filter and category_filter in ForumTopic.CATEGORIES:
query = query.filter(ForumTopic.category == category_filter)
if status_filter and status_filter in ForumTopic.STATUSES:
query = query.filter(ForumTopic.status == status_filter)
# Order by pinned first, then by last activity
query = query.order_by(
ForumTopic.is_pinned.desc(),
ForumTopic.updated_at.desc()
)
total_topics = query.count()
topics = query.limit(per_page).offset((page - 1) * per_page).all()
return render_template(
'forum/index.html',
topics=topics,
page=page,
per_page=per_page,
total_topics=total_topics,
total_pages=(total_topics + per_page - 1) // per_page,
category_filter=category_filter,
status_filter=status_filter,
categories=ForumTopic.CATEGORIES,
statuses=ForumTopic.STATUSES,
category_labels=ForumTopic.CATEGORY_LABELS,
status_labels=ForumTopic.STATUS_LABELS
)
finally:
db.close()
@app.route('/forum/nowy', methods=['GET', 'POST'])
@login_required
def forum_new_topic():
"""Create new forum topic with category and attachments"""
if request.method == 'POST':
title = sanitize_input(request.form.get('title', ''), 255)
content = request.form.get('content', '').strip()
category = request.form.get('category', 'question')
# Validate category
if category not in ForumTopic.CATEGORIES:
category = 'question'
if not title or len(title) < 5:
flash('Tytuł musi mieć co najmniej 5 znaków.', 'error')
return render_template('forum/new_topic.html',
categories=ForumTopic.CATEGORIES,
category_labels=ForumTopic.CATEGORY_LABELS)
if not content or len(content) < 10:
flash('Treść musi mieć co najmniej 10 znaków.', 'error')
return render_template('forum/new_topic.html',
categories=ForumTopic.CATEGORIES,
category_labels=ForumTopic.CATEGORY_LABELS)
db = SessionLocal()
try:
topic = ForumTopic(
title=title,
content=content,
author_id=current_user.id,
category=category
)
db.add(topic)
db.commit()
db.refresh(topic)
# Handle file upload
if 'attachment' in request.files:
file = request.files['attachment']
if file and file.filename:
is_valid, error_msg = FileUploadService.validate_file(file)
if is_valid:
stored_filename, rel_path, file_size, mime_type = FileUploadService.save_file(file, 'topic')
attachment = ForumAttachment(
attachment_type='topic',
topic_id=topic.id,
original_filename=file.filename,
stored_filename=stored_filename,
file_extension=stored_filename.rsplit('.', 1)[-1],
file_size=file_size,
mime_type=mime_type,
uploaded_by=current_user.id
)
db.add(attachment)
db.commit()
else:
flash(f'Załącznik: {error_msg}', 'warning')
flash('Temat został utworzony.', 'success')
return redirect(url_for('forum_topic', topic_id=topic.id))
finally:
db.close()
return render_template('forum/new_topic.html',
categories=ForumTopic.CATEGORIES,
category_labels=ForumTopic.CATEGORY_LABELS)
@app.route('/forum/<int:topic_id>')
@login_required
def forum_topic(topic_id):
"""View forum topic with replies"""
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
flash('Temat nie istnieje.', 'error')
return redirect(url_for('forum_index'))
# Increment view count (handle NULL)
topic.views_count = (topic.views_count or 0) + 1
db.commit()
return render_template('forum/topic.html',
topic=topic,
category_labels=ForumTopic.CATEGORY_LABELS,
status_labels=ForumTopic.STATUS_LABELS)
finally:
db.close()
@app.route('/forum/<int:topic_id>/odpowiedz', methods=['POST'])
@login_required
def forum_reply(topic_id):
"""Add reply to forum topic with optional attachment"""
content = request.form.get('content', '').strip()
if not content or len(content) < 3:
flash('Odpowiedź musi mieć co najmniej 3 znaki.', 'error')
return redirect(url_for('forum_topic', topic_id=topic_id))
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
flash('Temat nie istnieje.', 'error')
return redirect(url_for('forum_index'))
if topic.is_locked:
flash('Ten temat jest zamknięty.', 'error')
return redirect(url_for('forum_topic', topic_id=topic_id))
reply = ForumReply(
topic_id=topic_id,
author_id=current_user.id,
content=content
)
db.add(reply)
db.commit()
db.refresh(reply)
# Handle multiple file uploads (max 10)
MAX_ATTACHMENTS = 10
files = request.files.getlist('attachments[]')
if not files:
# Fallback for single file upload (backward compatibility)
files = request.files.getlist('attachment')
uploaded_count = 0
errors = []
for file in files[:MAX_ATTACHMENTS]:
if file and file.filename:
is_valid, error_msg = FileUploadService.validate_file(file)
if is_valid:
stored_filename, rel_path, file_size, mime_type = FileUploadService.save_file(file, 'reply')
attachment = ForumAttachment(
attachment_type='reply',
reply_id=reply.id,
original_filename=file.filename,
stored_filename=stored_filename,
file_extension=stored_filename.rsplit('.', 1)[-1],
file_size=file_size,
mime_type=mime_type,
uploaded_by=current_user.id
)
db.add(attachment)
uploaded_count += 1
else:
errors.append(f'{file.filename}: {error_msg}')
if uploaded_count > 0:
db.commit()
if errors:
flash(f'Niektóre załączniki nie zostały dodane: {"; ".join(errors)}', 'warning')
# Update topic updated_at
topic.updated_at = datetime.now()
db.commit()
flash('Odpowiedź dodana.', 'success')
return redirect(url_for('forum_topic', topic_id=topic_id))
finally:
db.close()
# ============================================================
# FORUM ADMIN ROUTES
# ============================================================
@app.route('/admin/forum')
@login_required
def admin_forum():
"""Admin panel for forum moderation"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('forum_index'))
db = SessionLocal()
try:
# Get all topics with stats
topics = db.query(ForumTopic).order_by(
ForumTopic.created_at.desc()
).all()
# Get recent replies
recent_replies = db.query(ForumReply).order_by(
ForumReply.created_at.desc()
).limit(50).all()
# Stats
total_topics = len(topics)
total_replies = db.query(ForumReply).count()
pinned_count = sum(1 for t in topics if t.is_pinned)
locked_count = sum(1 for t in topics if t.is_locked)
# Category and status stats
category_counts = {}
status_counts = {}
for t in topics:
cat = t.category or 'question'
status = t.status or 'new'
category_counts[cat] = category_counts.get(cat, 0) + 1
status_counts[status] = status_counts.get(status, 0) + 1
return render_template(
'admin/forum.html',
topics=topics,
recent_replies=recent_replies,
total_topics=total_topics,
total_replies=total_replies,
pinned_count=pinned_count,
locked_count=locked_count,
category_counts=category_counts,
status_counts=status_counts,
categories=ForumTopic.CATEGORIES,
statuses=ForumTopic.STATUSES,
category_labels=ForumTopic.CATEGORY_LABELS,
status_labels=ForumTopic.STATUS_LABELS
)
finally:
db.close()
@app.route('/admin/forum/topic/<int:topic_id>/pin', methods=['POST'])
@login_required
def admin_forum_pin(topic_id):
"""Toggle topic pin status"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
topic.is_pinned = not topic.is_pinned
db.commit()
logger.info(f"Admin {current_user.email} {'pinned' if topic.is_pinned else 'unpinned'} topic #{topic_id}")
return jsonify({
'success': True,
'is_pinned': topic.is_pinned,
'message': f"Temat {'przypięty' if topic.is_pinned else 'odpięty'}"
})
finally:
db.close()
@app.route('/admin/forum/topic/<int:topic_id>/lock', methods=['POST'])
@login_required
def admin_forum_lock(topic_id):
"""Toggle topic lock status"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
topic.is_locked = not topic.is_locked
db.commit()
logger.info(f"Admin {current_user.email} {'locked' if topic.is_locked else 'unlocked'} topic #{topic_id}")
return jsonify({
'success': True,
'is_locked': topic.is_locked,
'message': f"Temat {'zamknięty' if topic.is_locked else 'otwarty'}"
})
finally:
db.close()
@app.route('/admin/forum/topic/<int:topic_id>/delete', methods=['POST'])
@login_required
def admin_forum_delete_topic(topic_id):
"""Delete topic and all its replies"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
topic_title = topic.title
db.delete(topic) # Cascade deletes replies
db.commit()
logger.info(f"Admin {current_user.email} deleted topic #{topic_id}: {topic_title}")
return jsonify({
'success': True,
'message': 'Temat usunięty'
})
finally:
db.close()
@app.route('/admin/forum/reply/<int:reply_id>/delete', methods=['POST'])
@login_required
def admin_forum_delete_reply(reply_id):
"""Delete a reply"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
reply = db.query(ForumReply).filter(ForumReply.id == reply_id).first()
if not reply:
return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404
topic_id = reply.topic_id
db.delete(reply)
db.commit()
logger.info(f"Admin {current_user.email} deleted reply #{reply_id} from topic #{topic_id}")
return jsonify({
'success': True,
'message': 'Odpowiedź usunięta'
})
finally:
db.close()
@app.route('/admin/forum/topic/<int:topic_id>/status', methods=['POST'])
@login_required
def admin_forum_change_status(topic_id):
"""Change topic status (admin only)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {}
new_status = data.get('status')
note = data.get('note', '').strip()
if not new_status or new_status not in ForumTopic.STATUSES:
return jsonify({'success': False, 'error': 'Nieprawidłowy status'}), 400
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
old_status = topic.status
topic.status = new_status
topic.status_changed_by = current_user.id
topic.status_changed_at = datetime.now()
if note:
topic.status_note = note
db.commit()
logger.info(f"Admin {current_user.email} changed topic #{topic_id} status: {old_status} -> {new_status}")
return jsonify({
'success': True,
'status': new_status,
'status_label': ForumTopic.STATUS_LABELS.get(new_status, new_status),
'message': f"Status zmieniony na: {ForumTopic.STATUS_LABELS.get(new_status, new_status)}"
})
finally:
db.close()
# ============================================================
# RECOMMENDATIONS ADMIN ROUTES
# ============================================================

View File

@ -120,7 +120,32 @@ def register_blueprints(app):
except Exception as e:
logger.error(f"Error registering public blueprint: {e}")
# Phase 3-10: Future blueprints will be added here
# Phase 3: Forum blueprint
try:
from blueprints.forum import bp as forum_bp
app.register_blueprint(forum_bp)
logger.info("Registered blueprint: forum")
# Create aliases for backward compatibility
_create_endpoint_aliases(app, forum_bp, {
'forum_index': 'forum.forum_index',
'forum_new_topic': 'forum.forum_new_topic',
'forum_topic': 'forum.forum_topic',
'forum_reply': 'forum.forum_reply',
'admin_forum': 'forum.admin_forum',
'admin_forum_pin': 'forum.admin_forum_pin',
'admin_forum_lock': 'forum.admin_forum_lock',
'admin_forum_delete_topic': 'forum.admin_forum_delete_topic',
'admin_forum_delete_reply': 'forum.admin_forum_delete_reply',
'admin_forum_change_status': 'forum.admin_forum_change_status',
})
logger.info("Created forum endpoint aliases")
except ImportError as e:
logger.debug(f"Blueprint forum not yet available: {e}")
except Exception as e:
logger.error(f"Error registering forum blueprint: {e}")
# Phase 4-10: Future blueprints will be added here
def _create_endpoint_aliases(app, blueprint, aliases):

View File

@ -0,0 +1,12 @@
"""
Forum Blueprint
===============
Forum routes: topics, replies, admin moderation.
"""
from flask import Blueprint
bp = Blueprint('forum', __name__)
from . import routes # noqa: E402, F401

450
blueprints/forum/routes.py Normal file
View File

@ -0,0 +1,450 @@
"""
Forum Routes
============
Forum topics, replies, and admin moderation.
"""
import logging
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, ForumTopic, ForumReply, ForumAttachment
from utils.helpers import sanitize_input
# Logger
logger = logging.getLogger(__name__)
# Import FileUploadService (may not be available in all environments)
try:
from file_upload_service import FileUploadService
FILE_UPLOAD_AVAILABLE = True
except ImportError:
FILE_UPLOAD_AVAILABLE = False
logger.warning("FileUploadService not available for forum")
# ============================================================
# PUBLIC FORUM ROUTES
# ============================================================
@bp.route('/forum')
@login_required
def forum_index():
"""Forum - list of topics with category/status filters"""
page = request.args.get('page', 1, type=int)
per_page = 20
category_filter = request.args.get('category', '')
status_filter = request.args.get('status', '')
db = SessionLocal()
try:
# Build query with optional filters
query = db.query(ForumTopic)
if category_filter and category_filter in ForumTopic.CATEGORIES:
query = query.filter(ForumTopic.category == category_filter)
if status_filter and status_filter in ForumTopic.STATUSES:
query = query.filter(ForumTopic.status == status_filter)
# Order by pinned first, then by last activity
query = query.order_by(
ForumTopic.is_pinned.desc(),
ForumTopic.updated_at.desc()
)
total_topics = query.count()
topics = query.limit(per_page).offset((page - 1) * per_page).all()
return render_template(
'forum/index.html',
topics=topics,
page=page,
per_page=per_page,
total_topics=total_topics,
total_pages=(total_topics + per_page - 1) // per_page,
category_filter=category_filter,
status_filter=status_filter,
categories=ForumTopic.CATEGORIES,
statuses=ForumTopic.STATUSES,
category_labels=ForumTopic.CATEGORY_LABELS,
status_labels=ForumTopic.STATUS_LABELS
)
finally:
db.close()
@bp.route('/forum/nowy', methods=['GET', 'POST'])
@login_required
def forum_new_topic():
"""Create new forum topic with category and attachments"""
if request.method == 'POST':
title = sanitize_input(request.form.get('title', ''), 255)
content = request.form.get('content', '').strip()
category = request.form.get('category', 'question')
# Validate category
if category not in ForumTopic.CATEGORIES:
category = 'question'
if not title or len(title) < 5:
flash('Tytuł musi mieć co najmniej 5 znaków.', 'error')
return render_template('forum/new_topic.html',
categories=ForumTopic.CATEGORIES,
category_labels=ForumTopic.CATEGORY_LABELS)
if not content or len(content) < 10:
flash('Treść musi mieć co najmniej 10 znaków.', 'error')
return render_template('forum/new_topic.html',
categories=ForumTopic.CATEGORIES,
category_labels=ForumTopic.CATEGORY_LABELS)
db = SessionLocal()
try:
topic = ForumTopic(
title=title,
content=content,
author_id=current_user.id,
category=category
)
db.add(topic)
db.commit()
db.refresh(topic)
# Handle file upload
if FILE_UPLOAD_AVAILABLE and 'attachment' in request.files:
file = request.files['attachment']
if file and file.filename:
is_valid, error_msg = FileUploadService.validate_file(file)
if is_valid:
stored_filename, rel_path, file_size, mime_type = FileUploadService.save_file(file, 'topic')
attachment = ForumAttachment(
attachment_type='topic',
topic_id=topic.id,
original_filename=file.filename,
stored_filename=stored_filename,
file_extension=stored_filename.rsplit('.', 1)[-1],
file_size=file_size,
mime_type=mime_type,
uploaded_by=current_user.id
)
db.add(attachment)
db.commit()
else:
flash(f'Załącznik: {error_msg}', 'warning')
flash('Temat został utworzony.', 'success')
return redirect(url_for('.forum_topic', topic_id=topic.id))
finally:
db.close()
return render_template('forum/new_topic.html',
categories=ForumTopic.CATEGORIES,
category_labels=ForumTopic.CATEGORY_LABELS)
@bp.route('/forum/<int:topic_id>')
@login_required
def forum_topic(topic_id):
"""View forum topic with replies"""
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
flash('Temat nie istnieje.', 'error')
return redirect(url_for('.forum_index'))
# Increment view count (handle NULL)
topic.views_count = (topic.views_count or 0) + 1
db.commit()
return render_template('forum/topic.html',
topic=topic,
category_labels=ForumTopic.CATEGORY_LABELS,
status_labels=ForumTopic.STATUS_LABELS)
finally:
db.close()
@bp.route('/forum/<int:topic_id>/odpowiedz', methods=['POST'])
@login_required
def forum_reply(topic_id):
"""Add reply to forum topic with optional attachment"""
content = request.form.get('content', '').strip()
if not content or len(content) < 3:
flash('Odpowiedź musi mieć co najmniej 3 znaki.', 'error')
return redirect(url_for('.forum_topic', topic_id=topic_id))
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
flash('Temat nie istnieje.', 'error')
return redirect(url_for('.forum_index'))
if topic.is_locked:
flash('Ten temat jest zamknięty.', 'error')
return redirect(url_for('.forum_topic', topic_id=topic_id))
reply = ForumReply(
topic_id=topic_id,
author_id=current_user.id,
content=content
)
db.add(reply)
db.commit()
db.refresh(reply)
# Handle multiple file uploads (max 10)
if FILE_UPLOAD_AVAILABLE:
MAX_ATTACHMENTS = 10
files = request.files.getlist('attachments[]')
if not files:
# Fallback for single file upload (backward compatibility)
files = request.files.getlist('attachment')
uploaded_count = 0
errors = []
for file in files[:MAX_ATTACHMENTS]:
if file and file.filename:
is_valid, error_msg = FileUploadService.validate_file(file)
if is_valid:
stored_filename, rel_path, file_size, mime_type = FileUploadService.save_file(file, 'reply')
attachment = ForumAttachment(
attachment_type='reply',
reply_id=reply.id,
original_filename=file.filename,
stored_filename=stored_filename,
file_extension=stored_filename.rsplit('.', 1)[-1],
file_size=file_size,
mime_type=mime_type,
uploaded_by=current_user.id
)
db.add(attachment)
uploaded_count += 1
else:
errors.append(f'{file.filename}: {error_msg}')
if uploaded_count > 0:
db.commit()
if errors:
flash(f'Niektóre załączniki nie zostały dodane: {"; ".join(errors)}', 'warning')
# Update topic updated_at
topic.updated_at = datetime.now()
db.commit()
flash('Odpowiedź dodana.', 'success')
return redirect(url_for('.forum_topic', topic_id=topic_id))
finally:
db.close()
# ============================================================
# ADMIN FORUM ROUTES
# ============================================================
@bp.route('/admin/forum')
@login_required
def admin_forum():
"""Admin panel for forum moderation"""
if not current_user.is_admin:
flash('Brak uprawnień do tej strony.', 'error')
return redirect(url_for('.forum_index'))
db = SessionLocal()
try:
# Get all topics with stats
topics = db.query(ForumTopic).order_by(
ForumTopic.created_at.desc()
).all()
# Get recent replies
recent_replies = db.query(ForumReply).order_by(
ForumReply.created_at.desc()
).limit(50).all()
# Stats
total_topics = len(topics)
total_replies = db.query(ForumReply).count()
pinned_count = sum(1 for t in topics if t.is_pinned)
locked_count = sum(1 for t in topics if t.is_locked)
# Category and status stats
category_counts = {}
status_counts = {}
for t in topics:
cat = t.category or 'question'
status = t.status or 'new'
category_counts[cat] = category_counts.get(cat, 0) + 1
status_counts[status] = status_counts.get(status, 0) + 1
return render_template(
'admin/forum.html',
topics=topics,
recent_replies=recent_replies,
total_topics=total_topics,
total_replies=total_replies,
pinned_count=pinned_count,
locked_count=locked_count,
category_counts=category_counts,
status_counts=status_counts,
categories=ForumTopic.CATEGORIES,
statuses=ForumTopic.STATUSES,
category_labels=ForumTopic.CATEGORY_LABELS,
status_labels=ForumTopic.STATUS_LABELS
)
finally:
db.close()
@bp.route('/admin/forum/topic/<int:topic_id>/pin', methods=['POST'])
@login_required
def admin_forum_pin(topic_id):
"""Toggle topic pin status"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
topic.is_pinned = not topic.is_pinned
db.commit()
logger.info(f"Admin {current_user.email} {'pinned' if topic.is_pinned else 'unpinned'} topic #{topic_id}")
return jsonify({
'success': True,
'is_pinned': topic.is_pinned,
'message': f"Temat {'przypięty' if topic.is_pinned else 'odpięty'}"
})
finally:
db.close()
@bp.route('/admin/forum/topic/<int:topic_id>/lock', methods=['POST'])
@login_required
def admin_forum_lock(topic_id):
"""Toggle topic lock status"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
topic.is_locked = not topic.is_locked
db.commit()
logger.info(f"Admin {current_user.email} {'locked' if topic.is_locked else 'unlocked'} topic #{topic_id}")
return jsonify({
'success': True,
'is_locked': topic.is_locked,
'message': f"Temat {'zamknięty' if topic.is_locked else 'otwarty'}"
})
finally:
db.close()
@bp.route('/admin/forum/topic/<int:topic_id>/delete', methods=['POST'])
@login_required
def admin_forum_delete_topic(topic_id):
"""Delete topic and all its replies"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
topic_title = topic.title
db.delete(topic) # Cascade deletes replies
db.commit()
logger.info(f"Admin {current_user.email} deleted topic #{topic_id}: {topic_title}")
return jsonify({
'success': True,
'message': 'Temat usunięty'
})
finally:
db.close()
@bp.route('/admin/forum/reply/<int:reply_id>/delete', methods=['POST'])
@login_required
def admin_forum_delete_reply(reply_id):
"""Delete a reply"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
db = SessionLocal()
try:
reply = db.query(ForumReply).filter(ForumReply.id == reply_id).first()
if not reply:
return jsonify({'success': False, 'error': 'Odpowiedź nie istnieje'}), 404
topic_id = reply.topic_id
db.delete(reply)
db.commit()
logger.info(f"Admin {current_user.email} deleted reply #{reply_id} from topic #{topic_id}")
return jsonify({
'success': True,
'message': 'Odpowiedź usunięta'
})
finally:
db.close()
@bp.route('/admin/forum/topic/<int:topic_id>/status', methods=['POST'])
@login_required
def admin_forum_change_status(topic_id):
"""Change topic status (admin only)"""
if not current_user.is_admin:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
data = request.get_json() or {}
new_status = data.get('status')
note = data.get('note', '').strip()
if not new_status or new_status not in ForumTopic.STATUSES:
return jsonify({'success': False, 'error': 'Nieprawidłowy status'}), 400
db = SessionLocal()
try:
topic = db.query(ForumTopic).filter(ForumTopic.id == topic_id).first()
if not topic:
return jsonify({'success': False, 'error': 'Temat nie istnieje'}), 404
old_status = topic.status
topic.status = new_status
topic.status_changed_by = current_user.id
topic.status_changed_at = datetime.now()
if note:
topic.status_note = note
db.commit()
logger.info(f"Admin {current_user.email} changed topic #{topic_id} status: {old_status} -> {new_status}")
return jsonify({
'success': True,
'status': new_status,
'status_label': ForumTopic.STATUS_LABELS.get(new_status, new_status),
'message': f"Status zmieniony na: {ForumTopic.STATUS_LABELS.get(new_status, new_status)}"
})
finally:
db.close()

View File

@ -128,7 +128,7 @@ Usuń funkcje z prefiksem `_old_` z app.py.
|------|--------|--------|--------|
| **1** | reports, community, education | 19 | ✅ WDROŻONA |
| **2a** | auth + public + cleanup | 31 | ✅ WDROŻONA |
| **3** | account, forum | ~25 | 🔜 Następna |
| **3** | forum (10 routes) | 10 | ✅ WDROŻONA |
| **4** | messages, notifications | ~10 | ⏳ |
| **5** | chat | ~8 | ⏳ |
| **6** | admin (8 modułów) | ~60 | ⏳ |
@ -153,10 +153,18 @@ Usuń funkcje z prefiksem `_old_` z app.py.
- `blueprints/auth/routes.py` (1,040 linii)
- `blueprints/public/routes.py` (862 linii)
### Po Fazie 3 - Forum (2026-01-31)
- app.py: 13,820 → 13,398 linii
- **Usunięto: 422 linie (3.1%)**
- Nowe pliki:
- `blueprints/forum/__init__.py`
- `blueprints/forum/routes.py` (450 linii)
### Łączna redukcja app.py
- Start: 15,570 linii
- 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)
- **Cel końcowy: ~500 linii**
---