nordabiz/blueprints/admin/routes_social.py
Maciej Pienczyn 7f77d7ebcd
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
feat(security): Restrict audit access to single designated user
Audits (SEO, IT, GBP, Social Media) are now visible only to the
designated audit owner (maciej.pienczyn@inpi.pl). All other users,
including admins, see 404 for audit routes and no audit links in
navigation. KRS Audit and Digital Maturity remain unchanged.

Adds /admin/access-overview panel showing the access matrix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 12:31:10 +01:00

306 lines
12 KiB
Python

"""
Admin Social Media Routes
==========================
Social media analytics and audit dashboards.
"""
import logging
from datetime import datetime, timedelta
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from sqlalchemy import func, distinct, or_
from . import bp
from database import (
SessionLocal, Company, Category, CompanySocialMedia, SystemRole
)
from utils.decorators import role_required, is_audit_owner
logger = logging.getLogger(__name__)
# ============================================================
# SOCIAL MEDIA ANALYTICS DASHBOARD
# ============================================================
@bp.route('/social-media')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_social_media():
"""Admin dashboard for social media analytics"""
db = SessionLocal()
try:
# Total counts per platform
platform_stats = db.query(
CompanySocialMedia.platform,
func.count(CompanySocialMedia.id).label('count'),
func.count(distinct(CompanySocialMedia.company_id)).label('companies')
).filter(
CompanySocialMedia.is_valid == True
).group_by(CompanySocialMedia.platform).all()
# Companies with each platform combination
company_platforms = db.query(
Company.id,
Company.name,
Company.slug,
func.array_agg(distinct(CompanySocialMedia.platform)).label('platforms')
).outerjoin(
CompanySocialMedia,
(Company.id == CompanySocialMedia.company_id) & (CompanySocialMedia.is_valid == True)
).group_by(Company.id, Company.name, Company.slug).all()
# Analysis
total_companies = len(company_platforms)
companies_with_sm = [c for c in company_platforms if c.platforms and c.platforms[0] is not None]
companies_without_sm = [c for c in company_platforms if not c.platforms or c.platforms[0] is None]
# Platform combinations
platform_combos_raw = {}
for c in companies_with_sm:
platforms = sorted([p for p in c.platforms if p]) if c.platforms else []
key = ', '.join(platforms) if platforms else 'Brak'
if key not in platform_combos_raw:
platform_combos_raw[key] = []
platform_combos_raw[key].append({'id': c.id, 'name': c.name, 'slug': c.slug})
# Sort by number of companies (descending)
platform_combos = dict(sorted(platform_combos_raw.items(), key=lambda x: len(x[1]), reverse=True))
# Only Facebook
only_facebook = [c for c in companies_with_sm if set(c.platforms) == {'facebook'}]
# Only LinkedIn
only_linkedin = [c for c in companies_with_sm if set(c.platforms) == {'linkedin'}]
# Only Instagram
only_instagram = [c for c in companies_with_sm if set(c.platforms) == {'instagram'}]
# Has all major (FB + LI + IG)
has_all_major = [c for c in companies_with_sm if {'facebook', 'linkedin', 'instagram'}.issubset(set(c.platforms or []))]
# Get all social media entries with company info for detailed view
all_entries = db.query(
CompanySocialMedia,
Company.name.label('company_name'),
Company.slug.label('company_slug')
).join(Company).order_by(
Company.name, CompanySocialMedia.platform
).all()
# Freshness analysis
now = datetime.now()
fresh_30d = db.query(func.count(CompanySocialMedia.id)).filter(
CompanySocialMedia.verified_at >= now - timedelta(days=30)
).scalar()
stale_90d = db.query(func.count(CompanySocialMedia.id)).filter(
CompanySocialMedia.verified_at < now - timedelta(days=90)
).scalar()
return render_template('admin/social_media.html',
platform_stats=platform_stats,
total_companies=total_companies,
companies_with_sm=len(companies_with_sm),
companies_without_sm=companies_without_sm,
platform_combos=platform_combos,
only_facebook=only_facebook,
only_linkedin=only_linkedin,
only_instagram=only_instagram,
has_all_major=has_all_major,
all_entries=all_entries,
fresh_30d=fresh_30d,
stale_90d=stale_90d,
now=now
)
finally:
db.close()
# ============================================================
# SOCIAL MEDIA AUDIT DASHBOARD
# ============================================================
@bp.route('/social-audit')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_social_audit():
"""
Admin dashboard for Social Media audit overview.
Displays:
- Summary stats (coverage per platform, total profiles)
- Platform coverage with progress bars
- Sortable table with platform icons per company
- Followers aggregate statistics
"""
if not is_audit_owner():
from flask import abort
abort(404)
db = SessionLocal()
try:
# Platform definitions
platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'twitter', 'tiktok']
# Total companies count
total_companies = db.query(func.count(Company.id)).filter(Company.status == 'active').scalar()
# Get all companies with their social media profiles
companies_query = db.query(
Company.id,
Company.name,
Company.slug,
Company.website,
Category.name.label('category_name')
).outerjoin(
Category,
Company.category_id == Category.id
).filter(
Company.status == 'active'
).order_by(Company.name).all()
# Get social media data per company (valid profiles)
social_data = db.query(
CompanySocialMedia.company_id,
CompanySocialMedia.platform,
CompanySocialMedia.url,
CompanySocialMedia.followers_count,
CompanySocialMedia.verified_at,
CompanySocialMedia.is_valid,
CompanySocialMedia.check_status
).filter(
or_(
CompanySocialMedia.is_valid == True,
CompanySocialMedia.check_status == 'needs_verification'
)
).all()
# Group social media by company + collect needs_verification items
company_social = {}
needs_verification_items = []
for sm in social_data:
if sm.company_id not in company_social:
company_social[sm.company_id] = {}
company_social[sm.company_id][sm.platform] = {
'url': sm.url,
'followers': sm.followers_count or 0,
'verified_at': sm.verified_at,
'needs_verification': sm.check_status == 'needs_verification'
}
if sm.check_status == 'needs_verification':
needs_verification_items.append({
'company_id': sm.company_id,
'platform': sm.platform,
'url': sm.url
})
# Build companies list with social media info
companies = []
for row in companies_query:
sm_data = company_social.get(row.id, {})
total_followers = sum(p.get('followers', 0) for p in sm_data.values())
platform_count = len(sm_data)
# Get last verified date across all platforms
verified_dates = [p.get('verified_at') for p in sm_data.values() if p.get('verified_at')]
last_verified = max(verified_dates) if verified_dates else None
# Generate recommendations
recommendations = []
needs_verify_platforms = [p for p, d in sm_data.items() if d.get('needs_verification')]
if not sm_data:
recommendations.append('Brak profili social media')
else:
for nv_platform in needs_verify_platforms:
recommendations.append(f'{nv_platform.capitalize()}: do weryfikacji')
fb = sm_data.get('facebook')
if not fb:
recommendations.append('Brak Facebook')
elif fb.get('url') and 'profile.php?id=' in fb['url']:
recommendations.append('Facebook: zmien adres na nazwe firmy')
if 'instagram' not in sm_data:
recommendations.append('Brak Instagram')
if 'linkedin' not in sm_data:
recommendations.append('Brak LinkedIn')
if 'youtube' not in sm_data:
recommendations.append('Brak YouTube')
companies.append({
'id': row.id,
'name': row.name,
'slug': row.slug,
'website': row.website,
'category': row.category_name,
'platforms': sm_data,
'platform_count': platform_count,
'total_followers': total_followers,
'last_verified': last_verified,
'has_facebook': 'facebook' in sm_data,
'has_instagram': 'instagram' in sm_data,
'has_linkedin': 'linkedin' in sm_data,
'has_youtube': 'youtube' in sm_data,
'has_twitter': 'twitter' in sm_data,
'has_tiktok': 'tiktok' in sm_data,
'has_needs_verification': len(needs_verify_platforms) > 0,
'recommendations': recommendations
})
# Platform statistics
platform_stats = {}
for platform in platforms:
count = db.query(func.count(distinct(CompanySocialMedia.company_id))).filter(
CompanySocialMedia.platform == platform,
CompanySocialMedia.is_valid == True
).scalar() or 0
platform_stats[platform] = {
'count': count,
'percent': round(count / total_companies * 100) if total_companies > 0 else 0
}
# Summary stats
companies_with_sm = len([c for c in companies if c['platform_count'] > 0])
companies_without_sm = total_companies - companies_with_sm
total_profiles = sum(c['platform_count'] for c in companies)
total_followers = sum(c['total_followers'] for c in companies)
# Top followers (top 10 companies by total followers)
top_followers = sorted([c for c in companies if c['total_followers'] > 0],
key=lambda x: x['total_followers'], reverse=True)[:10]
# Resolve company names for needs_verification items
company_names = {row.id: row.name for row in companies_query}
for item in needs_verification_items:
item['company_name'] = company_names.get(item['company_id'], 'Nieznana')
stats = {
'total_companies': total_companies,
'companies_with_sm': companies_with_sm,
'companies_without_sm': companies_without_sm,
'total_profiles': total_profiles,
'total_followers': total_followers,
'needs_verification_count': len(needs_verification_items),
'platform_stats': platform_stats
}
# Get unique categories
categories = sorted(set(c['category'] for c in companies if c['category']))
# Convert to objects for template
class CompanyRow:
def __init__(self, data):
for key, value in data.items():
setattr(self, key, value)
companies_objects = [CompanyRow(c) for c in companies]
top_followers_objects = [CompanyRow(c) for c in top_followers]
return render_template('admin/social_audit_dashboard.html',
companies=companies_objects,
stats=stats,
needs_verification=needs_verification_items,
categories=categories,
platforms=platforms,
top_followers=top_followers_objects,
now=datetime.now()
)
finally:
db.close()