refactor: Migrate announcements routes to blueprints
- Create new blueprints/admin/routes_announcements.py - Move 6 announcements routes to blueprint - Update templates to use full blueprint names - Add endpoint aliases for backward compatibility Phase 6.2d - Announcements routes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
82162874b8
commit
54c1878d66
36
app.py
36
app.py
@ -10691,9 +10691,9 @@ def generate_slug(title):
|
||||
return text[:200] # Limit slug length
|
||||
|
||||
|
||||
@app.route('/admin/announcements')
|
||||
@login_required
|
||||
def admin_announcements():
|
||||
# @app.route('/admin/announcements') # MOVED TO admin.admin_announcements
|
||||
# @login_required
|
||||
def _old_admin_announcements():
|
||||
"""Admin panel - lista ogłoszeń"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
@ -10737,9 +10737,9 @@ def admin_announcements():
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/announcements/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def admin_announcements_new():
|
||||
# @app.route('/admin/announcements/new', methods=['GET', 'POST']) # MOVED TO admin.admin_announcements_new
|
||||
# @login_required
|
||||
def _old_admin_announcements_new():
|
||||
"""Admin panel - nowe ogłoszenie"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
@ -10822,9 +10822,9 @@ def admin_announcements_new():
|
||||
category_labels=Announcement.CATEGORY_LABELS)
|
||||
|
||||
|
||||
@app.route('/admin/announcements/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def admin_announcements_edit(id):
|
||||
# @app.route('/admin/announcements/<int:id>/edit', methods=['GET', 'POST']) # MOVED TO admin.admin_announcements_edit
|
||||
# @login_required
|
||||
def _old_admin_announcements_edit(id):
|
||||
"""Admin panel - edycja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
@ -10909,9 +10909,9 @@ def admin_announcements_edit(id):
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/announcements/<int:id>/publish', methods=['POST'])
|
||||
@login_required
|
||||
def admin_announcements_publish(id):
|
||||
# @app.route('/admin/announcements/<int:id>/publish', methods=['POST']) # MOVED TO admin.admin_announcements_publish
|
||||
# @login_required
|
||||
def _old_admin_announcements_publish(id):
|
||||
"""Publikacja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
@ -10950,9 +10950,9 @@ def admin_announcements_publish(id):
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/announcements/<int:id>/archive', methods=['POST'])
|
||||
@login_required
|
||||
def admin_announcements_archive(id):
|
||||
# @app.route('/admin/announcements/<int:id>/archive', methods=['POST']) # MOVED TO admin.admin_announcements_archive
|
||||
# @login_required
|
||||
def _old_admin_announcements_archive(id):
|
||||
"""Archiwizacja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
@ -10980,9 +10980,9 @@ def admin_announcements_archive(id):
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/announcements/<int:id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def admin_announcements_delete(id):
|
||||
# @app.route('/admin/announcements/<int:id>/delete', methods=['POST']) # MOVED TO admin.admin_announcements_delete
|
||||
# @login_required
|
||||
def _old_admin_announcements_delete(id):
|
||||
"""Usunięcie ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
@ -246,6 +246,13 @@ def register_blueprints(app):
|
||||
'resolve_security_alert': 'admin.resolve_security_alert',
|
||||
'unlock_account': 'admin.unlock_account',
|
||||
'api_geoip_stats': 'admin.api_geoip_stats',
|
||||
# Announcements (Phase 6.2d)
|
||||
'admin_announcements': 'admin.admin_announcements',
|
||||
'admin_announcements_new': 'admin.admin_announcements_new',
|
||||
'admin_announcements_edit': 'admin.admin_announcements_edit',
|
||||
'admin_announcements_publish': 'admin.admin_announcements_publish',
|
||||
'admin_announcements_archive': 'admin.admin_announcements_archive',
|
||||
'admin_announcements_delete': 'admin.admin_announcements_delete',
|
||||
})
|
||||
logger.info("Created admin endpoint aliases")
|
||||
except ImportError as e:
|
||||
|
||||
@ -14,3 +14,4 @@ from . import routes_audits # noqa: E402, F401
|
||||
from . import routes_status # noqa: E402, F401
|
||||
from . import routes_social # noqa: E402, F401
|
||||
from . import routes_security # noqa: E402, F401
|
||||
from . import routes_announcements # noqa: E402, F401
|
||||
|
||||
353
blueprints/admin/routes_announcements.py
Normal file
353
blueprints/admin/routes_announcements.py
Normal file
@ -0,0 +1,353 @@
|
||||
"""
|
||||
Admin Announcements Routes
|
||||
===========================
|
||||
|
||||
Announcements management (ogłoszenia) for admin panel.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
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, Announcement
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_slug(title):
|
||||
"""
|
||||
Generate URL-friendly slug from title.
|
||||
Uses unidecode for proper Polish character handling.
|
||||
"""
|
||||
try:
|
||||
from unidecode import unidecode
|
||||
text = unidecode(title.lower())
|
||||
except ImportError:
|
||||
# Fallback without unidecode
|
||||
text = title.lower()
|
||||
replacements = {
|
||||
'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n',
|
||||
'ó': 'o', 'ś': 's', 'ź': 'z', 'ż': 'z'
|
||||
}
|
||||
for pl, en in replacements.items():
|
||||
text = text.replace(pl, en)
|
||||
|
||||
# Remove special characters, replace spaces with hyphens
|
||||
text = re.sub(r'[^\w\s-]', '', text)
|
||||
text = re.sub(r'[-\s]+', '-', text).strip('-')
|
||||
return text[:200] # Limit slug length
|
||||
|
||||
|
||||
# ============================================================
|
||||
# ANNOUNCEMENTS MANAGEMENT
|
||||
# ============================================================
|
||||
|
||||
@bp.route('/announcements')
|
||||
@login_required
|
||||
def admin_announcements():
|
||||
"""Admin panel - lista ogłoszeń"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# Filters
|
||||
status_filter = request.args.get('status', 'all')
|
||||
category_filter = request.args.get('category', 'all')
|
||||
|
||||
query = db.query(Announcement)
|
||||
|
||||
if status_filter != 'all':
|
||||
query = query.filter(Announcement.status == status_filter)
|
||||
if category_filter != 'all':
|
||||
from sqlalchemy.dialects.postgresql import array as pg_array
|
||||
query = query.filter(Announcement.categories.op('@>')(pg_array([category_filter])))
|
||||
|
||||
# Sort: pinned first, then by created_at desc
|
||||
query = query.order_by(
|
||||
Announcement.is_pinned.desc(),
|
||||
Announcement.created_at.desc()
|
||||
)
|
||||
|
||||
announcements = query.all()
|
||||
|
||||
return render_template('admin/announcements.html',
|
||||
announcements=announcements,
|
||||
now=datetime.now(),
|
||||
status_filter=status_filter,
|
||||
category_filter=category_filter,
|
||||
categories=Announcement.CATEGORIES,
|
||||
category_labels=Announcement.CATEGORY_LABELS,
|
||||
statuses=Announcement.STATUSES,
|
||||
status_labels=Announcement.STATUS_LABELS)
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/announcements/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def admin_announcements_new():
|
||||
"""Admin panel - nowe ogłoszenie"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
if request.method == 'POST':
|
||||
db = SessionLocal()
|
||||
try:
|
||||
title = request.form.get('title', '').strip()
|
||||
excerpt = request.form.get('excerpt', '').strip()
|
||||
content = request.form.get('content', '').strip()
|
||||
categories = request.form.getlist('categories')
|
||||
if not categories:
|
||||
categories = ['internal'] # Default category
|
||||
category = categories[0] # Backwards compatibility
|
||||
image_url = request.form.get('image_url', '').strip() or None
|
||||
external_link = request.form.get('external_link', '').strip() or None
|
||||
is_featured = 'is_featured' in request.form
|
||||
is_pinned = 'is_pinned' in request.form
|
||||
|
||||
# Handle expires_at
|
||||
expires_at_str = request.form.get('expires_at', '').strip()
|
||||
expires_at = None
|
||||
if expires_at_str:
|
||||
try:
|
||||
expires_at = datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Generate unique slug
|
||||
base_slug = generate_slug(title)
|
||||
slug = base_slug
|
||||
counter = 1
|
||||
while db.query(Announcement).filter(Announcement.slug == slug).first():
|
||||
slug = f"{base_slug}-{counter}"
|
||||
counter += 1
|
||||
|
||||
# Determine status based on button clicked
|
||||
action = request.form.get('action', 'draft')
|
||||
status = 'published' if action == 'publish' else 'draft'
|
||||
published_at = datetime.now() if status == 'published' else None
|
||||
|
||||
announcement = Announcement(
|
||||
title=title,
|
||||
slug=slug,
|
||||
excerpt=excerpt or None,
|
||||
content=content,
|
||||
category=category,
|
||||
categories=categories,
|
||||
image_url=image_url,
|
||||
external_link=external_link,
|
||||
status=status,
|
||||
published_at=published_at,
|
||||
expires_at=expires_at,
|
||||
is_featured=is_featured,
|
||||
is_pinned=is_pinned,
|
||||
created_by=current_user.id
|
||||
)
|
||||
|
||||
db.add(announcement)
|
||||
db.commit()
|
||||
|
||||
flash(f'Ogłoszenie zostało {"opublikowane" if status == "published" else "zapisane jako szkic"}.', 'success')
|
||||
return redirect(url_for('admin.admin_announcements'))
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating announcement: {e}")
|
||||
flash(f'Błąd podczas tworzenia ogłoszenia: {e}', 'error')
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# GET request - show form
|
||||
return render_template('admin/announcements_form.html',
|
||||
announcement=None,
|
||||
categories=Announcement.CATEGORIES,
|
||||
category_labels=Announcement.CATEGORY_LABELS)
|
||||
|
||||
|
||||
@bp.route('/announcements/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def admin_announcements_edit(id):
|
||||
"""Admin panel - edycja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('dashboard'))
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
announcement = db.query(Announcement).filter(Announcement.id == id).first()
|
||||
if not announcement:
|
||||
flash('Nie znaleziono ogłoszenia.', 'error')
|
||||
return redirect(url_for('admin.admin_announcements'))
|
||||
|
||||
if request.method == 'POST':
|
||||
announcement.title = request.form.get('title', '').strip()
|
||||
announcement.excerpt = request.form.get('excerpt', '').strip() or None
|
||||
announcement.content = request.form.get('content', '').strip()
|
||||
categories = request.form.getlist('categories')
|
||||
if not categories:
|
||||
categories = ['internal'] # Default category
|
||||
announcement.categories = categories
|
||||
announcement.category = categories[0] # Backwards compatibility
|
||||
announcement.image_url = request.form.get('image_url', '').strip() or None
|
||||
announcement.external_link = request.form.get('external_link', '').strip() or None
|
||||
announcement.is_featured = 'is_featured' in request.form
|
||||
announcement.is_pinned = 'is_pinned' in request.form
|
||||
|
||||
# Handle expires_at
|
||||
expires_at_str = request.form.get('expires_at', '').strip()
|
||||
if expires_at_str:
|
||||
try:
|
||||
announcement.expires_at = datetime.strptime(expires_at_str, '%Y-%m-%dT%H:%M')
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
announcement.expires_at = None
|
||||
|
||||
# Regenerate slug if title changed significantly
|
||||
new_slug = generate_slug(announcement.title)
|
||||
if new_slug != announcement.slug.split('-')[0]: # Check if base changed
|
||||
base_slug = new_slug
|
||||
slug = base_slug
|
||||
counter = 1
|
||||
while db.query(Announcement).filter(
|
||||
Announcement.slug == slug,
|
||||
Announcement.id != id
|
||||
).first():
|
||||
slug = f"{base_slug}-{counter}"
|
||||
counter += 1
|
||||
announcement.slug = slug
|
||||
|
||||
# Handle status change
|
||||
action = request.form.get('action', 'save')
|
||||
if action == 'publish' and announcement.status != 'published':
|
||||
announcement.status = 'published'
|
||||
announcement.published_at = datetime.now()
|
||||
elif action == 'archive':
|
||||
announcement.status = 'archived'
|
||||
elif action == 'draft':
|
||||
announcement.status = 'draft'
|
||||
|
||||
announcement.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
flash('Zmiany zostały zapisane.', 'success')
|
||||
return redirect(url_for('admin.admin_announcements'))
|
||||
|
||||
# GET request - show form
|
||||
return render_template('admin/announcements_form.html',
|
||||
announcement=announcement,
|
||||
categories=Announcement.CATEGORIES,
|
||||
category_labels=Announcement.CATEGORY_LABELS)
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error editing announcement {id}: {e}")
|
||||
flash(f'Błąd: {e}', 'error')
|
||||
return redirect(url_for('admin.admin_announcements'))
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/announcements/<int:id>/publish', methods=['POST'])
|
||||
@login_required
|
||||
def admin_announcements_publish(id):
|
||||
"""Publikacja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
announcement = db.query(Announcement).filter(Announcement.id == id).first()
|
||||
if not announcement:
|
||||
return jsonify({'success': False, 'error': 'Nie znaleziono ogłoszenia'}), 404
|
||||
|
||||
announcement.status = 'published'
|
||||
if not announcement.published_at:
|
||||
announcement.published_at = datetime.now()
|
||||
announcement.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
# Notify all users about new announcement
|
||||
try:
|
||||
from utils.notifications import notify_all_users_announcement
|
||||
notify_count = notify_all_users_announcement(
|
||||
announcement_id=announcement.id,
|
||||
title=announcement.title,
|
||||
category=announcement.category
|
||||
)
|
||||
logger.info(f"Sent {notify_count} notifications for announcement: {announcement.title}")
|
||||
return jsonify({'success': True, 'message': f'Ogłoszenie zostało opublikowane. Wysłano {notify_count} powiadomień.'})
|
||||
except ImportError:
|
||||
return jsonify({'success': True, 'message': 'Ogłoszenie zostało opublikowane.'})
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error publishing announcement {id}: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/announcements/<int:id>/archive', methods=['POST'])
|
||||
@login_required
|
||||
def admin_announcements_archive(id):
|
||||
"""Archiwizacja ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
announcement = db.query(Announcement).filter(Announcement.id == id).first()
|
||||
if not announcement:
|
||||
return jsonify({'success': False, 'error': 'Nie znaleziono ogłoszenia'}), 404
|
||||
|
||||
announcement.status = 'archived'
|
||||
announcement.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': 'Ogłoszenie zostało zarchiwizowane'})
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error archiving announcement {id}: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/announcements/<int:id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def admin_announcements_delete(id):
|
||||
"""Usunięcie ogłoszenia"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
announcement = db.query(Announcement).filter(Announcement.id == id).first()
|
||||
if not announcement:
|
||||
return jsonify({'success': False, 'error': 'Nie znaleziono ogłoszenia'}), 404
|
||||
|
||||
db.delete(announcement)
|
||||
db.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': 'Ogłoszenie zostało usunięte'})
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting announcement {id}: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@ -196,7 +196,7 @@
|
||||
<div class="container">
|
||||
<div class="admin-header">
|
||||
<h1>Zarzadzanie Ogloszeniami</h1>
|
||||
<a href="{{ url_for('admin_announcements_new') }}" class="btn btn-primary">
|
||||
<a href="{{ url_for('admin.admin_announcements_new') }}" class="btn btn-primary">
|
||||
+ Nowe ogloszenie
|
||||
</a>
|
||||
</div>
|
||||
@ -245,7 +245,7 @@
|
||||
{% for ann in announcements %}
|
||||
<tr>
|
||||
<td class="title-cell">
|
||||
<a href="{{ url_for('admin_announcements_edit', id=ann.id) }}">
|
||||
<a href="{{ url_for('admin.admin_announcements_edit', id=ann.id) }}">
|
||||
{{ ann.title }}
|
||||
</a>
|
||||
{% if ann.is_pinned %}<span class="pinned-icon" title="Przypiete">📌</span>{% endif %}
|
||||
@ -271,7 +271,7 @@
|
||||
<td>{{ ann.created_at.strftime('%Y-%m-%d %H:%M') if ann.created_at else '-' }}</td>
|
||||
<td class="views-count">{{ ann.views_count or 0 }}</td>
|
||||
<td class="actions-cell">
|
||||
<a href="{{ url_for('admin_announcements_edit', id=ann.id) }}" class="btn btn-secondary btn-small">
|
||||
<a href="{{ url_for('admin.admin_announcements_edit', id=ann.id) }}" class="btn btn-secondary btn-small">
|
||||
Edytuj
|
||||
</a>
|
||||
{% if ann.status == 'draft' %}
|
||||
@ -334,7 +334,7 @@
|
||||
function applyFilters() {
|
||||
const status = document.getElementById('status-filter').value;
|
||||
const category = document.getElementById('category-filter').value;
|
||||
let url = '{{ url_for("admin_announcements") }}?';
|
||||
let url = '{{ url_for("admin.admin_announcements") }}?';
|
||||
if (status !== 'all') url += 'status=' + status + '&';
|
||||
if (category !== 'all') url += 'category=' + category + '&';
|
||||
window.location.href = url;
|
||||
|
||||
@ -265,7 +265,7 @@
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="btn-group">
|
||||
<a href="{{ url_for('admin_announcements') }}" class="btn btn-secondary">Anuluj</a>
|
||||
<a href="{{ url_for('admin.admin_announcements') }}" class="btn btn-secondary">Anuluj</a>
|
||||
|
||||
{% if announcement %}
|
||||
<!-- Edit mode -->
|
||||
|
||||
Loading…
Reference in New Issue
Block a user