refactor: Faza 6.2e - Social Media do blueprintu admin
Przeniesiono 2 trasy do blueprints/admin/routes_social.py: - admin_social_media (analytics dashboard) - admin_social_audit (audit dashboard) Zaktualizowano szablony: - base.html, dashboard.html, social_audit_dashboard.html Dodano aliasy dla kompatybilności wstecznej. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
61a32d9bfa
commit
9dedab2929
12
app.py
12
app.py
@ -6059,9 +6059,9 @@ def digital_maturity_dashboard():
|
||||
db.close()
|
||||
|
||||
|
||||
@app.route('/admin/social-media')
|
||||
@login_required
|
||||
def admin_social_media():
|
||||
# @app.route('/admin/social-media') # MOVED TO admin.admin_social_media
|
||||
# @login_required
|
||||
def _old_admin_social_media():
|
||||
"""Admin dashboard for social media analytics"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
@ -6160,9 +6160,9 @@ def admin_social_media():
|
||||
# SOCIAL MEDIA AUDIT ADMIN DASHBOARD
|
||||
# ============================================================
|
||||
|
||||
@app.route('/admin/social-audit')
|
||||
@login_required
|
||||
def admin_social_audit():
|
||||
# @app.route('/admin/social-audit') # MOVED TO admin.admin_social_audit
|
||||
# @login_required
|
||||
def _old_admin_social_audit():
|
||||
"""
|
||||
Admin dashboard for Social Media audit overview.
|
||||
|
||||
|
||||
@ -233,6 +233,9 @@ def register_blueprints(app):
|
||||
'admin_status': 'admin.admin_status',
|
||||
'admin_health': 'admin.admin_health',
|
||||
'debug_panel': 'admin.debug_panel',
|
||||
# Social Media (Phase 6.2e)
|
||||
'admin_social_media': 'admin.admin_social_media',
|
||||
'admin_social_audit': 'admin.admin_social_audit',
|
||||
})
|
||||
logger.info("Created admin endpoint aliases")
|
||||
except ImportError as e:
|
||||
|
||||
@ -12,3 +12,4 @@ bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
from . import routes # noqa: E402, F401
|
||||
from . import routes_audits # noqa: E402, F401
|
||||
from . import routes_status # noqa: E402, F401
|
||||
from . import routes_social # noqa: E402, F401
|
||||
|
||||
266
blueprints/admin/routes_social.py
Normal file
266
blueprints/admin/routes_social.py
Normal file
@ -0,0 +1,266 @@
|
||||
"""
|
||||
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
|
||||
|
||||
from . import bp
|
||||
from database import (
|
||||
SessionLocal, Company, Category, CompanySocialMedia
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# SOCIAL MEDIA ANALYTICS DASHBOARD
|
||||
# ============================================================
|
||||
|
||||
@bp.route('/social-media')
|
||||
@login_required
|
||||
def admin_social_media():
|
||||
"""Admin dashboard for social media analytics"""
|
||||
if not current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('public.dashboard'))
|
||||
|
||||
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
|
||||
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 current_user.is_admin:
|
||||
flash('Brak uprawnień do tej strony.', 'error')
|
||||
return redirect(url_for('public.dashboard'))
|
||||
|
||||
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
|
||||
social_data = db.query(
|
||||
CompanySocialMedia.company_id,
|
||||
CompanySocialMedia.platform,
|
||||
CompanySocialMedia.url,
|
||||
CompanySocialMedia.followers_count,
|
||||
CompanySocialMedia.verified_at,
|
||||
CompanySocialMedia.is_valid
|
||||
).filter(
|
||||
CompanySocialMedia.is_valid == True
|
||||
).all()
|
||||
|
||||
# Group social media by company
|
||||
company_social = {}
|
||||
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
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
# 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]
|
||||
|
||||
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,
|
||||
'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,
|
||||
categories=categories,
|
||||
platforms=platforms,
|
||||
top_followers=top_followers_objects,
|
||||
now=datetime.now()
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
@ -468,7 +468,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a href="{{ url_for('admin_social_media') }}" class="btn btn-outline btn-sm">
|
||||
<a href="{{ url_for('admin.admin_social_media') }}" class="btn btn-outline btn-sm">
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||
</svg>
|
||||
|
||||
@ -1244,7 +1244,7 @@
|
||||
</svg>
|
||||
Kalendarz
|
||||
</a>
|
||||
<a href="{{ url_for('admin_social_media') }}">
|
||||
<a href="{{ url_for('admin.admin_social_media') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z"/>
|
||||
</svg>
|
||||
@ -1305,7 +1305,7 @@
|
||||
</svg>
|
||||
Audyt GBP
|
||||
</a>
|
||||
<a href="{{ url_for('admin_social_audit') }}">
|
||||
<a href="{{ url_for('admin.admin_social_audit') }}">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z"/>
|
||||
</svg>
|
||||
|
||||
@ -398,7 +398,7 @@
|
||||
<span class="btn btn-sm btn-info">Zarządzaj</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ url_for('admin_social_media') }}" class="admin-function-card">
|
||||
<a href="{{ url_for('admin.admin_social_media') }}" class="admin-function-card">
|
||||
<svg fill="none" stroke="#8b5cf6" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"/>
|
||||
</svg>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user