nordabiz/blueprints/admin/routes_zopk_dashboard.py
Maciej Pienczyn 4181a2e760 refactor: Migrate access control from is_admin to role-based system
Replace ~170 manual `if not current_user.is_admin` checks with:
- @role_required(SystemRole.ADMIN) for user management, security, ZOPK
- @role_required(SystemRole.OFFICE_MANAGER) for content management
- current_user.can_access_admin_panel() for admin UI access
- current_user.can_moderate_forum() for forum moderation
- current_user.can_edit_company(id) for company permissions

Add @office_manager_required decorator shortcut.
Add SQL migration to sync existing users' role field.

Role hierarchy: UNAFFILIATED(10) < MEMBER(20) < EMPLOYEE(30) < MANAGER(40) < OFFICE_MANAGER(50) < ADMIN(100)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:05:22 +01:00

133 lines
5.1 KiB
Python

"""
ZOPK Dashboard Routes - Admin blueprint
Migrated from app.py as part of the blueprint refactoring.
Contains the main ZOPK dashboard route.
"""
from datetime import datetime
from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from database import (
SessionLocal,
SystemRole,
ZOPKProject,
ZOPKStakeholder,
ZOPKNews,
ZOPKResource,
ZOPKNewsFetchJob
)
from utils.decorators import role_required
from . import bp
@bp.route('/zopk')
@login_required
@role_required(SystemRole.ADMIN)
def admin_zopk():
"""Admin dashboard for ZOPK management"""
db = SessionLocal()
try:
# Pagination and filtering parameters
page = request.args.get('page', 1, type=int)
per_page = 20
status_filter = request.args.get('status', 'pending') # pending, approved, rejected, all
min_year = request.args.get('min_year', 2024, type=int) # ZOPK started in 2024
show_old = request.args.get('show_old', 'false') == 'true'
# ZOPK project started in 2024 - news from before this year are likely irrelevant
min_date = datetime(min_year, 1, 1) if not show_old else None
# Stats
stats = {
'total_projects': db.query(ZOPKProject).count(),
'total_stakeholders': db.query(ZOPKStakeholder).count(),
'total_news': db.query(ZOPKNews).count(),
'pending_news': db.query(ZOPKNews).filter(ZOPKNews.status == 'pending').count(),
'approved_news': db.query(ZOPKNews).filter(ZOPKNews.status.in_(['approved', 'auto_approved'])).count(),
'rejected_news': db.query(ZOPKNews).filter(ZOPKNews.status == 'rejected').count(),
'total_resources': db.query(ZOPKResource).count(),
# Count old news (before min_year) - likely irrelevant
'old_news': db.query(ZOPKNews).filter(
ZOPKNews.status == 'pending',
ZOPKNews.published_at < datetime(min_year, 1, 1)
).count() if not show_old else 0,
# AI evaluation stats
'ai_relevant': db.query(ZOPKNews).filter(ZOPKNews.ai_relevant == True).count(),
'ai_not_relevant': db.query(ZOPKNews).filter(ZOPKNews.ai_relevant == False).count(),
'ai_not_evaluated': db.query(ZOPKNews).filter(
ZOPKNews.status == 'pending',
ZOPKNews.ai_relevant.is_(None)
).count(),
# Items with ai_relevant but missing score (need upgrade to 1-5 stars)
'ai_missing_score': db.query(ZOPKNews).filter(
ZOPKNews.ai_relevant.isnot(None),
ZOPKNews.ai_relevance_score.is_(None)
).count()
}
# Build news query with filters
news_query = db.query(ZOPKNews)
# Status filter (including AI-based filters)
if status_filter == 'pending':
news_query = news_query.filter(ZOPKNews.status == 'pending')
elif status_filter == 'approved':
news_query = news_query.filter(ZOPKNews.status.in_(['approved', 'auto_approved']))
elif status_filter == 'rejected':
news_query = news_query.filter(ZOPKNews.status == 'rejected')
elif status_filter == 'ai_relevant':
# AI evaluated as relevant (regardless of status)
news_query = news_query.filter(ZOPKNews.ai_relevant == True)
elif status_filter == 'ai_not_relevant':
# AI evaluated as NOT relevant
news_query = news_query.filter(ZOPKNews.ai_relevant == False)
elif status_filter == 'ai_not_evaluated':
# Not yet evaluated by AI
news_query = news_query.filter(ZOPKNews.ai_relevant.is_(None))
# 'all' - no status filter
# Date filter - exclude old news by default
if min_date and not show_old:
news_query = news_query.filter(
(ZOPKNews.published_at >= min_date) | (ZOPKNews.published_at.is_(None))
)
# Order and count
total_news_filtered = news_query.count()
total_pages = (total_news_filtered + per_page - 1) // per_page
# Paginate
news_items = news_query.order_by(
ZOPKNews.published_at.desc().nullslast(),
ZOPKNews.created_at.desc()
).offset((page - 1) * per_page).limit(per_page).all()
# All projects
projects = db.query(ZOPKProject).order_by(ZOPKProject.sort_order).all()
# Recent fetch jobs
fetch_jobs = db.query(ZOPKNewsFetchJob).order_by(
ZOPKNewsFetchJob.created_at.desc()
).limit(5).all()
return render_template('admin/zopk_dashboard.html',
stats=stats,
news_items=news_items,
projects=projects,
fetch_jobs=fetch_jobs,
# Pagination
current_page=page,
total_pages=total_pages,
total_news_filtered=total_news_filtered,
per_page=per_page,
# Filters
status_filter=status_filter,
min_year=min_year,
show_old=show_old
)
finally:
db.close()