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>
326 lines
13 KiB
Python
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)
|