nordabiz/blueprints/public/routes_announcements.py
Maciej Pienczyn 7b31e6ba44
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
security(permissions): restrict guest access to members-only areas
- Forum: add @forum_access_required to ALL public routes (read+write)
- Reports: add @member_required to all report routes
- Announcements: add @member_required to list and detail
- Education: add @member_required to all routes
- Calendar: guests can VIEW all events but cannot RSVP (public+members_only)
- PEJ and ZOPK remain accessible (as intended for outreach)

UNAFFILIATED users (registered but not Izba members) are now properly
restricted from internal community features.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 16:23:56 +01:00

149 lines
5.2 KiB
Python

"""
Announcements Routes - Public blueprint
Migrated from app.py as part of the blueprint refactoring.
Contains public-facing announcement routes for logged-in members.
"""
import logging
from datetime import datetime
from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from utils.decorators import member_required
from sqlalchemy import desc, func, or_
from sqlalchemy.dialects.postgresql import array as pg_array
from database import SessionLocal, Announcement, AnnouncementRead, User
from . import bp
logger = logging.getLogger(__name__)
# ============================================================
# PUBLIC ANNOUNCEMENTS PAGE
# ============================================================
@bp.route('/ogloszenia')
@login_required
@member_required
def announcements_list():
"""Strona z listą ogłoszeń dla zalogowanych członków"""
db = SessionLocal()
try:
page = request.args.get('page', 1, type=int)
category = request.args.get('category', '')
per_page = 12
# Base query: published and not expired
query = db.query(Announcement).filter(
Announcement.status == 'published',
or_(
Announcement.expires_at.is_(None),
Announcement.expires_at > datetime.now()
)
)
# Filter by category (supports both single category and categories array)
# Use PostgreSQL @> operator for array contains
if category and category in Announcement.CATEGORIES:
query = query.filter(Announcement.categories.op('@>')(pg_array([category])))
# Sort: pinned first, then by published_at desc
query = query.order_by(
desc(Announcement.is_pinned),
desc(Announcement.published_at)
)
# Pagination
total = query.count()
total_pages = (total + per_page - 1) // per_page
announcements = query.offset((page - 1) * per_page).limit(per_page).all()
return render_template('announcements/list.html',
announcements=announcements,
current_category=category,
categories=Announcement.CATEGORIES,
category_labels=Announcement.CATEGORY_LABELS,
page=page,
total_pages=total_pages,
total=total)
finally:
db.close()
@bp.route('/ogloszenia/<slug>')
@login_required
@member_required
def announcement_detail(slug):
"""Szczegóły ogłoszenia dla zalogowanych członków"""
db = SessionLocal()
try:
announcement = db.query(Announcement).filter(
Announcement.slug == slug,
Announcement.status == 'published',
or_(
Announcement.expires_at.is_(None),
Announcement.expires_at > datetime.now()
)
).first()
if not announcement:
flash('Nie znaleziono ogłoszenia lub zostało usunięte.', 'error')
return redirect(url_for('announcements_list'))
# Increment views counter
announcement.views_count = (announcement.views_count or 0) + 1
# Record read by current user (if not already recorded)
existing_read = db.query(AnnouncementRead).filter(
AnnouncementRead.announcement_id == announcement.id,
AnnouncementRead.user_id == current_user.id
).first()
if not existing_read:
new_read = AnnouncementRead(
announcement_id=announcement.id,
user_id=current_user.id
)
db.add(new_read)
db.commit()
# Get readers (users who read this announcement)
readers = db.query(AnnouncementRead).filter(
AnnouncementRead.announcement_id == announcement.id
).order_by(desc(AnnouncementRead.read_at)).all()
# Get total registered users count for percentage calculation
total_users = db.query(func.count(User.id)).filter(
User.is_active == True,
User.is_verified == True
).scalar() or 1
readers_count = len(readers)
read_percentage = round((readers_count / total_users) * 100, 1) if total_users > 0 else 0
# Get other recent announcements for sidebar
other_announcements = db.query(Announcement).filter(
Announcement.status == 'published',
Announcement.id != announcement.id,
or_(
Announcement.expires_at.is_(None),
Announcement.expires_at > datetime.now()
)
).order_by(desc(Announcement.published_at)).limit(5).all()
return render_template('announcements/detail.html',
announcement=announcement,
other_announcements=other_announcements,
category_labels=Announcement.CATEGORY_LABELS,
readers=readers,
readers_count=readers_count,
total_users=total_users,
read_percentage=read_percentage)
finally:
db.close()