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>
133 lines
5.1 KiB
Python
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()
|