feat(pej): add public routes — index, local-content, news

Shared constants in pej_constants.py, endpoint aliases registered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-16 18:19:43 +01:00
parent d7c95f0d7f
commit 971c5616c3
4 changed files with 202 additions and 0 deletions

View File

@ -176,6 +176,10 @@ def register_blueprints(app):
'zopk_index': 'public.zopk_index',
'zopk_project_detail': 'public.zopk_project_detail',
'zopk_news_list': 'public.zopk_news_list',
# PEJ Public routes
'pej_index': 'public.pej_index',
'pej_local_content': 'public.pej_local_content',
'pej_news': 'public.pej_news',
# Announcements
'announcements_list': 'public.announcements_list',
'announcement_detail': 'public.announcement_detail',

View File

@ -0,0 +1,22 @@
"""Shared constants and helpers for PEJ section."""
from database import db_session, ZOPKProject
# Explicit slug list — easy to extend with SMR projects later
NUCLEAR_PROJECT_SLUGS = ['nuclear-plant']
LINK_TYPE_LABELS = {
'potential_supplier': 'Potencjalny dostawca',
'partner': 'Partner',
'investor': 'Inwestor',
'beneficiary': 'Beneficjent'
}
def get_nuclear_project_ids():
"""Return IDs of nuclear projects from ZOPK."""
projects = db_session.query(ZOPKProject.id).filter(
ZOPKProject.slug.in_(NUCLEAR_PROJECT_SLUGS),
ZOPKProject.project_type == 'energy'
).all()
return [p.id for p in projects]

View File

@ -11,5 +11,6 @@ bp = Blueprint('public', __name__)
from . import routes # noqa: E402, F401
from . import routes_zopk # noqa: E402, F401
from . import routes_pej # noqa: E402, F401
from . import routes_announcements # noqa: E402, F401
from . import routes_company_edit # noqa: E402, F401

View File

@ -0,0 +1,175 @@
"""PEJ (nuclear energy) section routes — filtered lens on ZOPK data."""
import math
from flask import render_template, request, abort
from flask_login import login_required
from sqlalchemy import func
from . import bp
from database import (
db_session, ZOPKNews, ZOPKMilestone,
ZOPKCompanyLink, Company, Announcement, Category
)
from blueprints.pej_constants import get_nuclear_project_ids, LINK_TYPE_LABELS
@bp.route('/pej')
@login_required
def pej_index():
"""PEJ landing page — hero, stats, news, timeline, top companies, announcements."""
nuclear_ids = get_nuclear_project_ids()
if not nuclear_ids:
abort(404)
# Stats
companies_count = db_session.query(func.count(ZOPKCompanyLink.id)).filter(
ZOPKCompanyLink.project_id.in_(nuclear_ids),
ZOPKCompanyLink.relevance_score >= 25
).scalar() or 0
news_count = db_session.query(func.count(ZOPKNews.id)).filter(
ZOPKNews.project_id.in_(nuclear_ids),
ZOPKNews.status.in_(['approved', 'auto_approved'])
).scalar() or 0
milestones_count = db_session.query(func.count(ZOPKMilestone.id)).filter(
ZOPKMilestone.category == 'nuclear'
).scalar() or 0
# Latest news (4)
news = db_session.query(ZOPKNews).filter(
ZOPKNews.project_id.in_(nuclear_ids),
ZOPKNews.status.in_(['approved', 'auto_approved'])
).order_by(ZOPKNews.published_at.desc()).limit(4).all()
# Nuclear milestones
milestones = db_session.query(ZOPKMilestone).filter(
ZOPKMilestone.category == 'nuclear'
).order_by(ZOPKMilestone.target_date.asc()).all()
# Top 6 companies by relevance
top_companies = db_session.query(ZOPKCompanyLink, Company).join(
Company, ZOPKCompanyLink.company_id == Company.id
).filter(
ZOPKCompanyLink.project_id.in_(nuclear_ids),
ZOPKCompanyLink.relevance_score >= 25,
Company.status == 'active'
).order_by(ZOPKCompanyLink.relevance_score.desc()).limit(6).all()
# PEJ announcements (status='published' in Announcement model)
announcements = db_session.query(Announcement).filter(
Announcement.categories.contains(['pej']),
Announcement.status == 'published'
).order_by(Announcement.created_at.desc()).limit(3).all()
return render_template('pej/index.html',
companies_count=companies_count,
news_count=news_count,
milestones_count=milestones_count,
news=news,
milestones=milestones,
top_companies=top_companies,
announcements=announcements,
link_type_labels=LINK_TYPE_LABELS
)
@bp.route('/pej/local-content')
@login_required
def pej_local_content():
"""Full list of Norda companies matched to nuclear projects."""
nuclear_ids = get_nuclear_project_ids()
if not nuclear_ids:
abort(404)
page = request.args.get('page', 1, type=int)
per_page = 20
category_filter = request.args.get('category', 0, type=int)
link_type_filter = request.args.get('link_type', '')
search_query = request.args.get('q', '')
query = db_session.query(ZOPKCompanyLink, Company).join(
Company, ZOPKCompanyLink.company_id == Company.id
).filter(
ZOPKCompanyLink.project_id.in_(nuclear_ids),
ZOPKCompanyLink.relevance_score >= 25,
Company.status == 'active'
)
if category_filter:
query = query.filter(Company.category_id == category_filter)
if link_type_filter:
query = query.filter(ZOPKCompanyLink.link_type == link_type_filter)
if search_query:
query = query.filter(Company.name.ilike(f'%{search_query}%'))
total = query.count()
results = query.order_by(
ZOPKCompanyLink.relevance_score.desc()
).offset((page - 1) * per_page).limit(per_page).all()
# Get distinct categories for filter dropdown
category_ids = db_session.query(Company.category_id).join(
ZOPKCompanyLink, Company.id == ZOPKCompanyLink.company_id
).filter(
ZOPKCompanyLink.project_id.in_(nuclear_ids),
ZOPKCompanyLink.relevance_score >= 25,
Company.status == 'active',
Company.category_id.isnot(None)
).distinct().all()
category_ids = [c[0] for c in category_ids if c[0]]
categories = db_session.query(Category).filter(
Category.id.in_(category_ids)
).order_by(Category.name).all() if category_ids else []
link_types = db_session.query(ZOPKCompanyLink.link_type).filter(
ZOPKCompanyLink.project_id.in_(nuclear_ids),
ZOPKCompanyLink.relevance_score >= 25
).distinct().all()
link_types = sorted([lt[0] for lt in link_types if lt[0]])
total_pages = math.ceil(total / per_page) if total > 0 else 1
return render_template('pej/local_content.html',
results=results,
total=total,
page=page,
per_page=per_page,
total_pages=total_pages,
categories=categories,
link_types=link_types,
link_type_labels=LINK_TYPE_LABELS,
category_filter=category_filter,
link_type_filter=link_type_filter,
search_query=search_query
)
@bp.route('/pej/aktualnosci')
@login_required
def pej_news():
"""Nuclear news list with pagination."""
nuclear_ids = get_nuclear_project_ids()
if not nuclear_ids:
abort(404)
page = request.args.get('page', 1, type=int)
per_page = 20
query = db_session.query(ZOPKNews).filter(
ZOPKNews.project_id.in_(nuclear_ids),
ZOPKNews.status.in_(['approved', 'auto_approved'])
).order_by(ZOPKNews.published_at.desc())
total = query.count()
news = query.offset((page - 1) * per_page).limit(per_page).all()
total_pages = math.ceil(total / per_page) if total > 0 else 1
return render_template('pej/news.html',
news=news,
page=page,
total=total,
total_pages=total_pages
)