refactor: Migrate ZOPK public routes to public blueprint

- Created blueprints/public/routes_zopk.py with 3 public routes:
  - /zopk (zopk_index)
  - /zopk/projekty/<slug> (zopk_project_detail)
  - /zopk/aktualnosci (zopk_news_list)
- Added endpoint aliases for backward compatibility
- Removed ZOPK public routes from app.py

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-31 17:19:53 +01:00
parent bdae7f5309
commit ab15cf3cba
4 changed files with 195 additions and 171 deletions

174
app.py
View File

@ -6877,178 +6877,10 @@ def api_it_audit_export():
# ============================================================
# ZIELONY OKRĘG PRZEMYSŁOWY KASZUBIA (ZOPK)
# ============================================================
@app.route('/zopk')
@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK page
def zopk_index():
"""
Public knowledge base page for ZOPK.
Shows projects, stakeholders, approved news, resources, and timeline.
"""
from database import ZOPKProject, ZOPKStakeholder, ZOPKNews, ZOPKResource, ZOPKMilestone
db = SessionLocal()
try:
# Get active projects
projects = db.query(ZOPKProject).filter(
ZOPKProject.is_active == True
).order_by(ZOPKProject.sort_order, ZOPKProject.name).all()
# Get milestones for timeline (sorted by target_date)
# Show all milestones - is_verified column will be added in migration
milestones = db.query(ZOPKMilestone).order_by(
ZOPKMilestone.target_date.asc()
).all()
# Get active stakeholders
stakeholders = db.query(ZOPKStakeholder).filter(
ZOPKStakeholder.is_active == True
).order_by(ZOPKStakeholder.importance.desc(), ZOPKStakeholder.name).limit(10).all()
# Get approved news (both manually approved and AI auto-approved)
# Show more news on main page (expanded view)
news_items = db.query(ZOPKNews).filter(
ZOPKNews.status.in_(['approved', 'auto_approved'])
).order_by(ZOPKNews.published_at.desc()).limit(25).all()
# Get featured resources
resources = db.query(ZOPKResource).filter(
ZOPKResource.status == 'approved'
).order_by(ZOPKResource.sort_order, ZOPKResource.created_at.desc()).limit(12).all()
# News time-based statistics
from datetime import datetime, timedelta
now = datetime.now()
day_ago = now - timedelta(days=1)
week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30)
approved_news_filter = ZOPKNews.status.in_(['approved', 'auto_approved'])
total_news = db.query(ZOPKNews).filter(approved_news_filter).count()
news_stats = {
'total': total_news,
'last_day': db.query(ZOPKNews).filter(
approved_news_filter,
ZOPKNews.published_at >= day_ago
).count(),
'last_week': db.query(ZOPKNews).filter(
approved_news_filter,
ZOPKNews.published_at >= week_ago
).count(),
'last_month': db.query(ZOPKNews).filter(
approved_news_filter,
ZOPKNews.published_at >= month_ago
).count()
}
# General stats
stats = {
'total_projects': len(projects),
'total_news': total_news,
'total_resources': db.query(ZOPKResource).filter(ZOPKResource.status == 'approved').count(),
'total_stakeholders': db.query(ZOPKStakeholder).filter(ZOPKStakeholder.is_active == True).count()
}
return render_template('zopk/index.html',
projects=projects,
stakeholders=stakeholders,
news_items=news_items,
resources=resources,
stats=stats,
news_stats=news_stats,
milestones=milestones
)
finally:
db.close()
@app.route('/zopk/projekty/<slug>')
@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK project pages
def zopk_project_detail(slug):
"""Project detail page"""
from database import ZOPKProject, ZOPKNews, ZOPKResource, ZOPKCompanyLink
db = SessionLocal()
try:
project = db.query(ZOPKProject).filter(ZOPKProject.slug == slug).first()
if not project:
abort(404)
# Get news for this project (both manually approved and AI auto-approved)
news_items = db.query(ZOPKNews).filter(
ZOPKNews.project_id == project.id,
ZOPKNews.status.in_(['approved', 'auto_approved'])
).order_by(ZOPKNews.published_at.desc()).limit(10).all()
# Get resources for this project
resources = db.query(ZOPKResource).filter(
ZOPKResource.project_id == project.id,
ZOPKResource.status == 'approved'
).order_by(ZOPKResource.sort_order).all()
# Get Norda companies linked to this project
company_links = db.query(ZOPKCompanyLink).filter(
ZOPKCompanyLink.project_id == project.id
).order_by(ZOPKCompanyLink.relevance_score.desc()).all()
return render_template('zopk/project_detail.html',
project=project,
news_items=news_items,
resources=resources,
company_links=company_links
)
finally:
db.close()
@app.route('/zopk/aktualnosci')
@limiter.limit("60 per minute") # SECURITY: Rate limit public ZOPK news list
def zopk_news_list():
"""All ZOPK news - paginated"""
from database import ZOPKProject, ZOPKNews
db = SessionLocal()
try:
page = request.args.get('page', 1, type=int)
per_page = 20
project_slug = request.args.get('projekt')
query = db.query(ZOPKNews).filter(ZOPKNews.status.in_(['approved', 'auto_approved']))
if project_slug:
project = db.query(ZOPKProject).filter(ZOPKProject.slug == project_slug).first()
if project:
query = query.filter(ZOPKNews.project_id == project.id)
total = query.count()
news_items = query.order_by(ZOPKNews.published_at.desc()).offset(
(page - 1) * per_page
).limit(per_page).all()
total_pages = (total + per_page - 1) // per_page
# Get projects for filter
projects = db.query(ZOPKProject).filter(
ZOPKProject.is_active == True
).order_by(ZOPKProject.sort_order).all()
return render_template('zopk/news_list.html',
news_items=news_items,
projects=projects,
current_project=project_slug,
page=page,
total_pages=total_pages,
total=total
)
finally:
db.close()
# ZOPK PUBLIC ROUTES - MOVED TO blueprints/public/routes_zopk.py
# Routes: /zopk, /zopk/projekty/<slug>, /zopk/aktualnosci
# ============================================================
# ============================================================

View File

@ -113,6 +113,10 @@ def register_blueprints(app):
'connections_map': 'public.connections_map',
'dashboard': 'public.dashboard',
'release_notes': 'public.release_notes',
# ZOPK Public routes
'zopk_index': 'public.zopk_index',
'zopk_project_detail': 'public.zopk_project_detail',
'zopk_news_list': 'public.zopk_news_list',
})
logger.info("Created public endpoint aliases")
except ImportError as e:

View File

@ -10,3 +10,4 @@ from flask import Blueprint
bp = Blueprint('public', __name__)
from . import routes # noqa: E402, F401
from . import routes_zopk # noqa: E402, F401

View File

@ -0,0 +1,187 @@
"""
ZOPK Public Routes - Public blueprint
Migrated from app.py as part of the blueprint refactoring.
Contains public-facing routes for ZOPK (Zielony Okręg Przemysłowy Kaszubia).
"""
from datetime import datetime, timedelta
from flask import abort, render_template, request
from database import (
SessionLocal,
ZOPKProject,
ZOPKStakeholder,
ZOPKNews,
ZOPKResource,
ZOPKMilestone,
ZOPKCompanyLink
)
from . import bp
# Import limiter from app - will be initialized when app starts
from flask import current_app
def get_limiter():
"""Get rate limiter from current app."""
return current_app.extensions.get('limiter')
@bp.route('/zopk')
def zopk_index():
"""
Public knowledge base page for ZOPK.
Shows projects, stakeholders, approved news, resources, and timeline.
"""
db = SessionLocal()
try:
# Get active projects
projects = db.query(ZOPKProject).filter(
ZOPKProject.is_active == True
).order_by(ZOPKProject.sort_order, ZOPKProject.name).all()
# Get milestones for timeline (sorted by target_date)
milestones = db.query(ZOPKMilestone).order_by(
ZOPKMilestone.target_date.asc()
).all()
# Get active stakeholders
stakeholders = db.query(ZOPKStakeholder).filter(
ZOPKStakeholder.is_active == True
).order_by(ZOPKStakeholder.importance.desc(), ZOPKStakeholder.name).limit(10).all()
# Get approved news (both manually approved and AI auto-approved)
news_items = db.query(ZOPKNews).filter(
ZOPKNews.status.in_(['approved', 'auto_approved'])
).order_by(ZOPKNews.published_at.desc()).limit(25).all()
# Get featured resources
resources = db.query(ZOPKResource).filter(
ZOPKResource.status == 'approved'
).order_by(ZOPKResource.sort_order, ZOPKResource.created_at.desc()).limit(12).all()
# News time-based statistics
now = datetime.now()
day_ago = now - timedelta(days=1)
week_ago = now - timedelta(days=7)
month_ago = now - timedelta(days=30)
approved_news_filter = ZOPKNews.status.in_(['approved', 'auto_approved'])
total_news = db.query(ZOPKNews).filter(approved_news_filter).count()
news_stats = {
'total': total_news,
'last_day': db.query(ZOPKNews).filter(
approved_news_filter,
ZOPKNews.published_at >= day_ago
).count(),
'last_week': db.query(ZOPKNews).filter(
approved_news_filter,
ZOPKNews.published_at >= week_ago
).count(),
'last_month': db.query(ZOPKNews).filter(
approved_news_filter,
ZOPKNews.published_at >= month_ago
).count()
}
# General stats
stats = {
'total_projects': len(projects),
'total_news': total_news,
'total_resources': db.query(ZOPKResource).filter(ZOPKResource.status == 'approved').count(),
'total_stakeholders': db.query(ZOPKStakeholder).filter(ZOPKStakeholder.is_active == True).count()
}
return render_template('zopk/index.html',
projects=projects,
stakeholders=stakeholders,
news_items=news_items,
resources=resources,
stats=stats,
news_stats=news_stats,
milestones=milestones
)
finally:
db.close()
@bp.route('/zopk/projekty/<slug>')
def zopk_project_detail(slug):
"""Project detail page"""
db = SessionLocal()
try:
project = db.query(ZOPKProject).filter(ZOPKProject.slug == slug).first()
if not project:
abort(404)
# Get news for this project (both manually approved and AI auto-approved)
news_items = db.query(ZOPKNews).filter(
ZOPKNews.project_id == project.id,
ZOPKNews.status.in_(['approved', 'auto_approved'])
).order_by(ZOPKNews.published_at.desc()).limit(10).all()
# Get resources for this project
resources = db.query(ZOPKResource).filter(
ZOPKResource.project_id == project.id,
ZOPKResource.status == 'approved'
).order_by(ZOPKResource.sort_order).all()
# Get Norda companies linked to this project
company_links = db.query(ZOPKCompanyLink).filter(
ZOPKCompanyLink.project_id == project.id
).order_by(ZOPKCompanyLink.relevance_score.desc()).all()
return render_template('zopk/project_detail.html',
project=project,
news_items=news_items,
resources=resources,
company_links=company_links
)
finally:
db.close()
@bp.route('/zopk/aktualnosci')
def zopk_news_list():
"""All ZOPK news - paginated"""
db = SessionLocal()
try:
page = request.args.get('page', 1, type=int)
per_page = 20
project_slug = request.args.get('projekt')
query = db.query(ZOPKNews).filter(ZOPKNews.status.in_(['approved', 'auto_approved']))
if project_slug:
project = db.query(ZOPKProject).filter(ZOPKProject.slug == project_slug).first()
if project:
query = query.filter(ZOPKNews.project_id == project.id)
total = query.count()
news_items = query.order_by(ZOPKNews.published_at.desc()).offset(
(page - 1) * per_page
).limit(per_page).all()
total_pages = (total + per_page - 1) // per_page
# Get projects for filter
projects = db.query(ZOPKProject).filter(
ZOPKProject.is_active == True
).order_by(ZOPKProject.sort_order).all()
return render_template('zopk/news_list.html',
news_items=news_items,
projects=projects,
current_project=project_slug,
page=page,
total_pages=total_pages,
total=total
)
finally:
db.close()