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
- 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>
149 lines
5.2 KiB
Python
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()
|