nordabiz/blueprints/admin/routes_announcements.py
Maciej Pienczyn 54c1878d66 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>
2026-01-31 09:41:49 +01:00

354 lines
13 KiB
Python

"""
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()