nordabiz/blueprints/admin/routes_social_publisher.py
Maciej Pienczyn 4a033f0d81 feat: Add Social Media Publisher module (MVP)
Admin panel module for publishing posts on NORDA chamber Facebook page.
Includes AI content generation (Gemini), post workflow (draft/approved/
scheduled/published), Facebook Graph API publishing, and engagement tracking.

New: migration 070, SocialPost/SocialMediaConfig models, publisher service,
admin routes with AJAX, 3 templates (list/form/settings).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 12:08:29 +01:00

326 lines
13 KiB
Python

"""
Admin Social Publisher Routes
==============================
Social media publishing management for NORDA Business Chamber.
"""
import logging
import os
from datetime import datetime
from flask import render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from . import bp
from database import SessionLocal, SocialPost, SocialMediaConfig, Company, NordaEvent, SystemRole
from utils.decorators import role_required
logger = logging.getLogger(__name__)
# ============================================================
# SOCIAL PUBLISHER - LIST & DASHBOARD
# ============================================================
@bp.route('/social-publisher')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def social_publisher_list():
"""Lista postow social media z filtrami."""
from services.social_publisher_service import social_publisher, POST_TYPES
status_filter = request.args.get('status', 'all')
type_filter = request.args.get('type', 'all')
db = SessionLocal()
try:
query = db.query(SocialPost).order_by(SocialPost.created_at.desc())
if status_filter != 'all':
query = query.filter(SocialPost.status == status_filter)
if type_filter != 'all':
query = query.filter(SocialPost.post_type == type_filter)
posts = query.limit(100).all()
stats = social_publisher.get_stats()
return render_template('admin/social_publisher.html',
posts=posts,
stats=stats,
post_types=POST_TYPES,
status_filter=status_filter,
type_filter=type_filter)
finally:
db.close()
# ============================================================
# SOCIAL PUBLISHER - CREATE / EDIT FORM
# ============================================================
@bp.route('/social-publisher/new', methods=['GET', 'POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def social_publisher_new():
"""Tworzenie nowego posta."""
from services.social_publisher_service import social_publisher, POST_TYPES
db = SessionLocal()
try:
companies = db.query(Company).filter(Company.is_active == True).order_by(Company.name).all()
events = db.query(NordaEvent).order_by(NordaEvent.event_date.desc()).limit(20).all()
if request.method == 'POST':
action = request.form.get('action', 'draft')
post_type = request.form.get('post_type', 'chamber_news')
content = request.form.get('content', '').strip()
hashtags = request.form.get('hashtags', '').strip()
company_id = request.form.get('company_id', type=int)
event_id = request.form.get('event_id', type=int)
if not content:
flash('Treść posta jest wymagana.', 'danger')
return render_template('admin/social_publisher_form.html',
post=None, companies=companies, events=events,
post_types=POST_TYPES)
post = social_publisher.create_post(
post_type=post_type,
content=content,
hashtags=hashtags or None,
company_id=company_id,
event_id=event_id,
user_id=current_user.id,
)
if action == 'publish':
success, message = social_publisher.publish_post(post.id)
flash(message, 'success' if success else 'danger')
else:
flash('Post utworzony (szkic).', 'success')
return redirect(url_for('admin.social_publisher_edit', post_id=post.id))
return render_template('admin/social_publisher_form.html',
post=None, companies=companies, events=events,
post_types=POST_TYPES)
finally:
db.close()
@bp.route('/social-publisher/<int:post_id>/edit', methods=['GET', 'POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def social_publisher_edit(post_id):
"""Edycja istniejacego posta."""
from services.social_publisher_service import social_publisher, POST_TYPES
db = SessionLocal()
try:
post = db.query(SocialPost).filter(SocialPost.id == post_id).first()
if not post:
flash('Post nie znaleziony.', 'danger')
return redirect(url_for('admin.social_publisher_list'))
companies = db.query(Company).filter(Company.is_active == True).order_by(Company.name).all()
events = db.query(NordaEvent).order_by(NordaEvent.event_date.desc()).limit(20).all()
if request.method == 'POST':
action = request.form.get('action', 'save')
# Handle non-edit actions
if action == 'approve':
result = social_publisher.approve_post(post_id, current_user.id)
flash('Post zatwierdzony.' if result else 'Nie można zatwierdzić posta.',
'success' if result else 'danger')
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
if action == 'publish':
success, message = social_publisher.publish_post(post_id)
flash(message, 'success' if success else 'danger')
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
if action == 'delete':
if social_publisher.delete_post(post_id):
flash('Post usunięty.', 'success')
return redirect(url_for('admin.social_publisher_list'))
flash('Nie można usunąć posta.', 'danger')
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
# Default: save/update
content = request.form.get('content', '').strip()
hashtags = request.form.get('hashtags', '').strip()
post_type = request.form.get('post_type')
company_id = request.form.get('company_id', type=int)
event_id = request.form.get('event_id', type=int)
if not content:
flash('Treść posta jest wymagana.', 'danger')
return render_template('admin/social_publisher_form.html',
post=post, companies=companies, events=events,
post_types=POST_TYPES)
social_publisher.update_post(
post_id=post_id,
content=content,
hashtags=hashtags or None,
post_type=post_type,
company_id=company_id,
event_id=event_id,
)
flash('Post zaktualizowany.', 'success')
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
return render_template('admin/social_publisher_form.html',
post=post, companies=companies, events=events,
post_types=POST_TYPES)
finally:
db.close()
# ============================================================
# SOCIAL PUBLISHER - ACTIONS
# ============================================================
@bp.route('/social-publisher/<int:post_id>/approve', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def social_publisher_approve(post_id):
"""Zatwierdz post."""
from services.social_publisher_service import social_publisher
result = social_publisher.approve_post(post_id, current_user.id)
if result:
flash('Post zatwierdzony.', 'success')
else:
flash('Nie można zatwierdzić posta.', 'danger')
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
@bp.route('/social-publisher/<int:post_id>/publish', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def social_publisher_publish(post_id):
"""Opublikuj post na Facebook."""
from services.social_publisher_service import social_publisher
success, message = social_publisher.publish_post(post_id)
# AJAX response
if request.headers.get('X-CSRFToken') and not request.form:
return jsonify({'success': success, 'message': message, 'error': None if success else message})
flash(message, 'success' if success else 'danger')
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
@bp.route('/social-publisher/<int:post_id>/delete', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def social_publisher_delete(post_id):
"""Usuń post."""
from services.social_publisher_service import social_publisher
success = social_publisher.delete_post(post_id)
# AJAX response
if request.headers.get('X-CSRFToken') and not request.form:
return jsonify({'success': success, 'error': None if success else 'Nie można usunąć posta.'})
flash('Post usunięty.' if success else 'Nie można usunąć posta.', 'success' if success else 'danger')
return redirect(url_for('admin.social_publisher_list'))
@bp.route('/social-publisher/<int:post_id>/refresh-engagement', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def social_publisher_refresh_engagement(post_id):
"""Odswiez metryki engagement z Facebook."""
from services.social_publisher_service import social_publisher
result = social_publisher.refresh_engagement(post_id)
if result:
flash('Engagement zaktualizowany.', 'success')
else:
flash('Nie udało się pobrać engagement.', 'warning')
return redirect(url_for('admin.social_publisher_edit', post_id=post_id))
# ============================================================
# SOCIAL PUBLISHER - AI GENERATION (AJAX)
# ============================================================
@bp.route('/social-publisher/generate', methods=['POST'])
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def social_publisher_generate():
"""Generuj tresc posta AI (AJAX endpoint)."""
from services.social_publisher_service import social_publisher
post_type = request.json.get('post_type')
company_id = request.json.get('company_id')
event_id = request.json.get('event_id')
custom_context = request.json.get('custom_context', {})
try:
# Build context
context = {}
if company_id:
context.update(social_publisher.get_company_context(int(company_id)))
if event_id:
context.update(social_publisher.get_event_context(int(event_id)))
context.update(custom_context)
# Fill defaults for missing fields
defaults = {
'company_name': '', 'category': '', 'city': 'Wejherowo',
'description': '', 'website': '',
'event_title': '', 'event_date': '', 'event_location': '',
'event_description': '', 'event_topics': '', 'attendees_count': '',
'topic': '', 'source': '', 'facts': '', 'details': '',
}
for key, default in defaults.items():
context.setdefault(key, default)
content, model = social_publisher.generate_content(post_type, context)
return jsonify({'success': True, 'content': content, 'model': model})
except Exception as e:
logger.error(f"AI generation failed: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# ============================================================
# SOCIAL PUBLISHER - SETTINGS
# ============================================================
@bp.route('/social-publisher/settings', methods=['GET', 'POST'])
@login_required
@role_required(SystemRole.ADMIN)
def social_publisher_settings():
"""Konfiguracja Facebook (tylko admin)."""
from services.social_publisher_service import social_publisher
config = social_publisher.get_fb_config()
if request.method == 'POST':
page_id = request.form.get('page_id', '').strip()
page_name = request.form.get('page_name', '').strip()
access_token = request.form.get('access_token', '').strip()
debug_mode = request.form.get('debug_mode') == 'on'
if not page_id or not access_token:
flash('Page ID i Access Token są wymagane.', 'danger')
else:
social_publisher.save_fb_config(
page_id=page_id,
page_name=page_name,
access_token=access_token,
debug_mode=debug_mode,
user_id=current_user.id,
)
flash('Konfiguracja Facebook zapisana.', 'success')
return redirect(url_for('admin.social_publisher_settings'))
return render_template('admin/social_publisher_settings.html', config=config)