Compare commits
2 Commits
7c4ed6a16c
...
e360c5fede
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e360c5fede | ||
|
|
3fcba76d0b |
@@ -1 +0,0 @@
|
||||
# This file has been removed to fix circular import issues
|
||||
400
backend/igny8_core/api/wordpress_publishing.py
Normal file
400
backend/igny8_core/api/wordpress_publishing.py
Normal file
@@ -0,0 +1,400 @@
|
||||
"""
|
||||
WordPress Publishing API Views
|
||||
Handles manual content publishing to WordPress sites
|
||||
"""
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from igny8_core.models import ContentPost, SiteIntegration
|
||||
from igny8_core.tasks.wordpress_publishing import (
|
||||
publish_content_to_wordpress,
|
||||
bulk_publish_content_to_wordpress
|
||||
)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def publish_single_content(request, content_id: int) -> Response:
|
||||
"""
|
||||
Publish a single content item to WordPress
|
||||
|
||||
POST /api/v1/content/{content_id}/publish-to-wordpress/
|
||||
|
||||
Body:
|
||||
{
|
||||
"site_integration_id": 123, // Optional - will use default if not provided
|
||||
"force": false // Optional - force republish even if already published
|
||||
}
|
||||
"""
|
||||
try:
|
||||
content = get_object_or_404(ContentPost, id=content_id)
|
||||
|
||||
# Check permissions
|
||||
if not request.user.has_perm('content.change_contentpost'):
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'Permission denied',
|
||||
'error': 'insufficient_permissions'
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
# Get site integration
|
||||
site_integration_id = request.data.get('site_integration_id')
|
||||
force = request.data.get('force', False)
|
||||
|
||||
if site_integration_id:
|
||||
site_integration = get_object_or_404(SiteIntegration, id=site_integration_id)
|
||||
else:
|
||||
# Get default WordPress integration for user's organization
|
||||
site_integration = SiteIntegration.objects.filter(
|
||||
platform='wordpress',
|
||||
is_active=True,
|
||||
# Add organization filter if applicable
|
||||
).first()
|
||||
|
||||
if not site_integration:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'No WordPress integration found',
|
||||
'error': 'no_integration'
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Check if already published (unless force is true)
|
||||
if not force and content.wordpress_sync_status == 'success':
|
||||
return Response(
|
||||
{
|
||||
'success': True,
|
||||
'message': 'Content already published to WordPress',
|
||||
'data': {
|
||||
'content_id': content.id,
|
||||
'wordpress_post_id': content.wordpress_post_id,
|
||||
'wordpress_post_url': content.wordpress_post_url,
|
||||
'status': 'already_published'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Check if currently syncing
|
||||
if content.wordpress_sync_status == 'syncing':
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'Content is currently being published to WordPress',
|
||||
'error': 'sync_in_progress'
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
# Validate content is ready for publishing
|
||||
if not content.title or not (content.content_html or content.content):
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'Content is incomplete - missing title or content',
|
||||
'error': 'incomplete_content'
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Set status to pending and queue the task
|
||||
content.wordpress_sync_status = 'pending'
|
||||
content.save(update_fields=['wordpress_sync_status'])
|
||||
|
||||
# Get task_id if content is associated with a writer task
|
||||
task_id = None
|
||||
if hasattr(content, 'writer_task'):
|
||||
task_id = content.writer_task.id
|
||||
|
||||
# Queue the publishing task
|
||||
task_result = publish_content_to_wordpress.delay(
|
||||
content.id,
|
||||
site_integration.id,
|
||||
task_id
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'success': True,
|
||||
'message': 'Content queued for WordPress publishing',
|
||||
'data': {
|
||||
'content_id': content.id,
|
||||
'site_integration_id': site_integration.id,
|
||||
'task_id': task_result.id,
|
||||
'status': 'queued'
|
||||
}
|
||||
},
|
||||
status=status.HTTP_202_ACCEPTED
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': f'Error queuing content for WordPress publishing: {str(e)}',
|
||||
'error': 'server_error'
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def bulk_publish_content(request) -> Response:
|
||||
"""
|
||||
Bulk publish multiple content items to WordPress
|
||||
|
||||
POST /api/v1/content/bulk-publish-to-wordpress/
|
||||
|
||||
Body:
|
||||
{
|
||||
"content_ids": [1, 2, 3, 4],
|
||||
"site_integration_id": 123, // Optional
|
||||
"force": false // Optional
|
||||
}
|
||||
"""
|
||||
try:
|
||||
content_ids = request.data.get('content_ids', [])
|
||||
site_integration_id = request.data.get('site_integration_id')
|
||||
force = request.data.get('force', False)
|
||||
|
||||
if not content_ids:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'No content IDs provided',
|
||||
'error': 'missing_content_ids'
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Check permissions
|
||||
if not request.user.has_perm('content.change_contentpost'):
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'Permission denied',
|
||||
'error': 'insufficient_permissions'
|
||||
},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
# Get site integration
|
||||
if site_integration_id:
|
||||
site_integration = get_object_or_404(SiteIntegration, id=site_integration_id)
|
||||
else:
|
||||
site_integration = SiteIntegration.objects.filter(
|
||||
platform='wordpress',
|
||||
is_active=True,
|
||||
).first()
|
||||
|
||||
if not site_integration:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'No WordPress integration found',
|
||||
'error': 'no_integration'
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Validate content items
|
||||
content_items = ContentPost.objects.filter(id__in=content_ids)
|
||||
|
||||
if content_items.count() != len(content_ids):
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'Some content items not found',
|
||||
'error': 'content_not_found'
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# Queue bulk publishing task
|
||||
task_result = bulk_publish_content_to_wordpress.delay(
|
||||
content_ids,
|
||||
site_integration.id
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'success': True,
|
||||
'message': f'{len(content_ids)} content items queued for WordPress publishing',
|
||||
'data': {
|
||||
'content_count': len(content_ids),
|
||||
'site_integration_id': site_integration.id,
|
||||
'task_id': task_result.id,
|
||||
'status': 'queued'
|
||||
}
|
||||
},
|
||||
status=status.HTTP_202_ACCEPTED
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': f'Error queuing bulk WordPress publishing: {str(e)}',
|
||||
'error': 'server_error'
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_wordpress_status(request, content_id: int) -> Response:
|
||||
"""
|
||||
Get WordPress publishing status for a content item
|
||||
|
||||
GET /api/v1/content/{content_id}/wordpress-status/
|
||||
"""
|
||||
try:
|
||||
content = get_object_or_404(ContentPost, id=content_id)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'success': True,
|
||||
'data': {
|
||||
'content_id': content.id,
|
||||
'wordpress_sync_status': content.wordpress_sync_status,
|
||||
'wordpress_post_id': content.wordpress_post_id,
|
||||
'wordpress_post_url': content.wordpress_post_url,
|
||||
'wordpress_sync_attempts': content.wordpress_sync_attempts,
|
||||
'last_wordpress_sync': content.last_wordpress_sync.isoformat() if content.last_wordpress_sync else None,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': f'Error getting WordPress status: {str(e)}',
|
||||
'error': 'server_error'
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_wordpress_integrations(request) -> Response:
|
||||
"""
|
||||
Get available WordPress integrations for publishing
|
||||
|
||||
GET /api/v1/wordpress-integrations/
|
||||
"""
|
||||
try:
|
||||
integrations = SiteIntegration.objects.filter(
|
||||
platform='wordpress',
|
||||
is_active=True,
|
||||
# Add organization filter if applicable
|
||||
).values(
|
||||
'id', 'site_name', 'site_url', 'is_active',
|
||||
'created_at', 'last_sync_at'
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'success': True,
|
||||
'data': list(integrations)
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': f'Error getting WordPress integrations: {str(e)}',
|
||||
'error': 'server_error'
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def retry_failed_wordpress_sync(request, content_id: int) -> Response:
|
||||
"""
|
||||
Retry a failed WordPress sync
|
||||
|
||||
POST /api/v1/content/{content_id}/retry-wordpress-sync/
|
||||
"""
|
||||
try:
|
||||
content = get_object_or_404(ContentPost, id=content_id)
|
||||
|
||||
if content.wordpress_sync_status != 'failed':
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'Content is not in failed status',
|
||||
'error': 'invalid_status'
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Get default WordPress integration
|
||||
site_integration = SiteIntegration.objects.filter(
|
||||
platform='wordpress',
|
||||
is_active=True,
|
||||
).first()
|
||||
|
||||
if not site_integration:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': 'No WordPress integration found',
|
||||
'error': 'no_integration'
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Reset status and retry
|
||||
content.wordpress_sync_status = 'pending'
|
||||
content.save(update_fields=['wordpress_sync_status'])
|
||||
|
||||
# Get task_id if available
|
||||
task_id = None
|
||||
if hasattr(content, 'writer_task'):
|
||||
task_id = content.writer_task.id
|
||||
|
||||
# Queue the publishing task
|
||||
task_result = publish_content_to_wordpress.delay(
|
||||
content.id,
|
||||
site_integration.id,
|
||||
task_id
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'success': True,
|
||||
'message': 'WordPress sync retry queued',
|
||||
'data': {
|
||||
'content_id': content.id,
|
||||
'task_id': task_result.id,
|
||||
'status': 'queued'
|
||||
}
|
||||
},
|
||||
status=status.HTTP_202_ACCEPTED
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return Response(
|
||||
{
|
||||
'success': False,
|
||||
'message': f'Error retrying WordPress sync: {str(e)}',
|
||||
'error': 'server_error'
|
||||
},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
385
backend/igny8_core/tasks/wordpress_publishing.py
Normal file
385
backend/igny8_core/tasks/wordpress_publishing.py
Normal file
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
IGNY8 Content Publishing Celery Tasks
|
||||
|
||||
Handles automated publishing of content from IGNY8 to WordPress sites.
|
||||
"""
|
||||
from celery import shared_task
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
import requests
|
||||
import logging
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_task(bind=True, max_retries=3)
|
||||
def publish_content_to_wordpress(self, content_id: int, site_integration_id: int, task_id: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Publish a single content item to WordPress
|
||||
|
||||
Args:
|
||||
content_id: IGNY8 content ID
|
||||
site_integration_id: WordPress site integration ID
|
||||
task_id: Optional IGNY8 task ID
|
||||
|
||||
Returns:
|
||||
Dict with success status and details
|
||||
"""
|
||||
try:
|
||||
from igny8_core.models import ContentPost, SiteIntegration
|
||||
|
||||
# Get content and site integration
|
||||
try:
|
||||
content = ContentPost.objects.get(id=content_id)
|
||||
site_integration = SiteIntegration.objects.get(id=site_integration_id)
|
||||
except (ContentPost.DoesNotExist, SiteIntegration.DoesNotExist) as e:
|
||||
logger.error(f"Content or site integration not found: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
# Check if content is ready for publishing
|
||||
if content.wordpress_sync_status == 'success':
|
||||
logger.info(f"Content {content_id} already published to WordPress")
|
||||
return {"success": True, "message": "Already published", "wordpress_post_id": content.wordpress_post_id}
|
||||
|
||||
if content.wordpress_sync_status == 'syncing':
|
||||
logger.info(f"Content {content_id} is currently syncing")
|
||||
return {"success": False, "error": "Content is currently syncing"}
|
||||
|
||||
# Update status to syncing
|
||||
content.wordpress_sync_status = 'syncing'
|
||||
content.save(update_fields=['wordpress_sync_status'])
|
||||
|
||||
# Prepare content data for WordPress
|
||||
content_data = {
|
||||
'content_id': content.id,
|
||||
'task_id': task_id,
|
||||
'title': content.title,
|
||||
'content_html': content.content_html or content.content,
|
||||
'excerpt': content.brief or '',
|
||||
'status': 'publish',
|
||||
'author_email': content.author.email if content.author else None,
|
||||
'author_name': content.author.get_full_name() if content.author else None,
|
||||
'published_at': content.published_at.isoformat() if content.published_at else None,
|
||||
'seo_title': getattr(content, 'seo_title', ''),
|
||||
'seo_description': getattr(content, 'seo_description', ''),
|
||||
'featured_image_url': content.featured_image.url if content.featured_image else None,
|
||||
'sectors': [{'id': s.id, 'name': s.name} for s in content.sectors.all()],
|
||||
'clusters': [{'id': c.id, 'name': c.name} for c in content.clusters.all()],
|
||||
'tags': getattr(content, 'tags', []),
|
||||
'focus_keywords': getattr(content, 'focus_keywords', [])
|
||||
}
|
||||
|
||||
# Call WordPress REST API
|
||||
wordpress_url = f"{site_integration.site_url}/wp-json/igny8/v1/publish-content/"
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-IGNY8-API-KEY': site_integration.api_key,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
wordpress_url,
|
||||
json=content_data,
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
# Success
|
||||
wp_data = response.json().get('data', {})
|
||||
content.wordpress_sync_status = 'success'
|
||||
content.wordpress_post_id = wp_data.get('post_id')
|
||||
content.wordpress_post_url = wp_data.get('post_url')
|
||||
content.last_wordpress_sync = timezone.now()
|
||||
content.save(update_fields=[
|
||||
'wordpress_sync_status', 'wordpress_post_id',
|
||||
'wordpress_post_url', 'last_wordpress_sync'
|
||||
])
|
||||
|
||||
logger.info(f"Successfully published content {content_id} to WordPress post {content.wordpress_post_id}")
|
||||
return {
|
||||
"success": True,
|
||||
"wordpress_post_id": content.wordpress_post_id,
|
||||
"wordpress_post_url": content.wordpress_post_url
|
||||
}
|
||||
|
||||
elif response.status_code == 409:
|
||||
# Content already exists
|
||||
wp_data = response.json().get('data', {})
|
||||
content.wordpress_sync_status = 'success'
|
||||
content.wordpress_post_id = wp_data.get('post_id')
|
||||
content.last_wordpress_sync = timezone.now()
|
||||
content.save(update_fields=[
|
||||
'wordpress_sync_status', 'wordpress_post_id', 'last_wordpress_sync'
|
||||
])
|
||||
|
||||
logger.info(f"Content {content_id} already exists on WordPress")
|
||||
return {"success": True, "message": "Content already exists", "wordpress_post_id": content.wordpress_post_id}
|
||||
|
||||
else:
|
||||
# Error
|
||||
error_msg = f"WordPress API error: {response.status_code} - {response.text}"
|
||||
logger.error(error_msg)
|
||||
|
||||
# Retry logic
|
||||
if self.request.retries < self.max_retries:
|
||||
content.wordpress_sync_attempts = (content.wordpress_sync_attempts or 0) + 1
|
||||
content.save(update_fields=['wordpress_sync_attempts'])
|
||||
|
||||
# Exponential backoff: 1min, 5min, 15min
|
||||
countdown = 60 * (5 ** self.request.retries)
|
||||
raise self.retry(countdown=countdown, exc=Exception(error_msg))
|
||||
else:
|
||||
# Max retries reached
|
||||
content.wordpress_sync_status = 'failed'
|
||||
content.last_wordpress_sync = timezone.now()
|
||||
content.save(update_fields=['wordpress_sync_status', 'last_wordpress_sync'])
|
||||
|
||||
return {"success": False, "error": error_msg}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error publishing content {content_id}: {str(e)}")
|
||||
|
||||
# Update content status on error
|
||||
try:
|
||||
content = ContentPost.objects.get(id=content_id)
|
||||
content.wordpress_sync_status = 'failed'
|
||||
content.last_wordpress_sync = timezone.now()
|
||||
content.save(update_fields=['wordpress_sync_status', 'last_wordpress_sync'])
|
||||
except:
|
||||
pass
|
||||
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@shared_task
|
||||
def process_pending_wordpress_publications() -> Dict[str, Any]:
|
||||
"""
|
||||
Process all content items pending WordPress publication
|
||||
Runs every 5 minutes
|
||||
"""
|
||||
try:
|
||||
from igny8_core.models import ContentPost, SiteIntegration
|
||||
|
||||
# Find content marked for WordPress publishing
|
||||
pending_content = ContentPost.objects.filter(
|
||||
wordpress_sync_status='pending',
|
||||
published_at__isnull=False # Only published content
|
||||
).select_related('author').prefetch_related('sectors', 'clusters')
|
||||
|
||||
if not pending_content.exists():
|
||||
logger.info("No content pending WordPress publication")
|
||||
return {"success": True, "processed": 0}
|
||||
|
||||
# Get active WordPress integrations
|
||||
active_integrations = SiteIntegration.objects.filter(
|
||||
platform='wordpress',
|
||||
is_active=True,
|
||||
api_key__isnull=False
|
||||
)
|
||||
|
||||
if not active_integrations.exists():
|
||||
logger.warning("No active WordPress integrations found")
|
||||
return {"success": False, "error": "No active WordPress integrations"}
|
||||
|
||||
processed = 0
|
||||
failed = 0
|
||||
|
||||
for content in pending_content[:50]: # Process max 50 at a time
|
||||
for integration in active_integrations:
|
||||
# Get task_id if content is associated with a task
|
||||
task_id = None
|
||||
if hasattr(content, 'writer_task'):
|
||||
task_id = content.writer_task.id
|
||||
|
||||
# Queue individual publish task
|
||||
publish_content_to_wordpress.delay(
|
||||
content.id,
|
||||
integration.id,
|
||||
task_id
|
||||
)
|
||||
processed += 1
|
||||
|
||||
logger.info(f"Queued {processed} content items for WordPress publication")
|
||||
return {"success": True, "processed": processed, "failed": failed}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing pending WordPress publications: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@shared_task
|
||||
def bulk_publish_content_to_wordpress(content_ids: List[int], site_integration_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Bulk publish multiple content items to WordPress
|
||||
Used for manual bulk operations from Content Manager
|
||||
"""
|
||||
try:
|
||||
from igny8_core.models import ContentPost, SiteIntegration
|
||||
|
||||
site_integration = SiteIntegration.objects.get(id=site_integration_id)
|
||||
content_items = ContentPost.objects.filter(id__in=content_ids)
|
||||
|
||||
results = {
|
||||
"success": True,
|
||||
"total": len(content_ids),
|
||||
"queued": 0,
|
||||
"skipped": 0,
|
||||
"errors": []
|
||||
}
|
||||
|
||||
for content in content_items:
|
||||
try:
|
||||
# Skip if already published or syncing
|
||||
if content.wordpress_sync_status in ['success', 'syncing']:
|
||||
results["skipped"] += 1
|
||||
continue
|
||||
|
||||
# Mark as pending and queue
|
||||
content.wordpress_sync_status = 'pending'
|
||||
content.save(update_fields=['wordpress_sync_status'])
|
||||
|
||||
# Get task_id if available
|
||||
task_id = None
|
||||
if hasattr(content, 'writer_task'):
|
||||
task_id = content.writer_task.id
|
||||
|
||||
# Queue individual publish task
|
||||
publish_content_to_wordpress.delay(
|
||||
content.id,
|
||||
site_integration.id,
|
||||
task_id
|
||||
)
|
||||
results["queued"] += 1
|
||||
|
||||
except Exception as e:
|
||||
results["errors"].append(f"Content {content.id}: {str(e)}")
|
||||
|
||||
if results["errors"]:
|
||||
results["success"] = len(results["errors"]) < results["total"] / 2 # Success if < 50% errors
|
||||
|
||||
logger.info(f"Bulk publish: {results['queued']} queued, {results['skipped']} skipped, {len(results['errors'])} errors")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in bulk publish: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@shared_task
|
||||
def wordpress_status_reconciliation() -> Dict[str, Any]:
|
||||
"""
|
||||
Daily task to reconcile status between IGNY8 and WordPress
|
||||
Checks for discrepancies and fixes them
|
||||
"""
|
||||
try:
|
||||
from igny8_core.models import ContentPost, SiteIntegration
|
||||
|
||||
# Get content marked as published to WordPress
|
||||
wp_content = ContentPost.objects.filter(
|
||||
wordpress_sync_status='success',
|
||||
wordpress_post_id__isnull=False
|
||||
)
|
||||
|
||||
active_integrations = SiteIntegration.objects.filter(
|
||||
platform='wordpress',
|
||||
is_active=True
|
||||
)
|
||||
|
||||
reconciled = 0
|
||||
errors = []
|
||||
|
||||
for integration in active_integrations:
|
||||
integration_content = wp_content.filter(
|
||||
# Assuming there's a way to link content to integration
|
||||
# This would depend on your data model
|
||||
)
|
||||
|
||||
for content in integration_content[:100]: # Limit to prevent timeouts
|
||||
try:
|
||||
# Check WordPress post status
|
||||
wp_url = f"{integration.site_url}/wp-json/igny8/v1/post-status/{content.id}/"
|
||||
headers = {'X-IGNY8-API-KEY': integration.api_key}
|
||||
|
||||
response = requests.get(wp_url, headers=headers, timeout=10)
|
||||
|
||||
if response.status_code == 200:
|
||||
wp_data = response.json().get('data', {})
|
||||
wp_status = wp_data.get('wordpress_status')
|
||||
|
||||
# Update if status changed
|
||||
if wp_status == 'trash' and content.wordpress_sync_status == 'success':
|
||||
content.wordpress_sync_status = 'failed'
|
||||
content.save(update_fields=['wordpress_sync_status'])
|
||||
reconciled += 1
|
||||
|
||||
elif response.status_code == 404:
|
||||
# Post not found on WordPress
|
||||
content.wordpress_sync_status = 'failed'
|
||||
content.wordpress_post_id = None
|
||||
content.wordpress_post_url = None
|
||||
content.save(update_fields=[
|
||||
'wordpress_sync_status', 'wordpress_post_id', 'wordpress_post_url'
|
||||
])
|
||||
reconciled += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Content {content.id}: {str(e)}")
|
||||
|
||||
logger.info(f"Status reconciliation: {reconciled} reconciled, {len(errors)} errors")
|
||||
return {"success": True, "reconciled": reconciled, "errors": errors}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in status reconciliation: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@shared_task
|
||||
def retry_failed_wordpress_publications() -> Dict[str, Any]:
|
||||
"""
|
||||
Retry failed WordPress publications (runs daily)
|
||||
Only retries items that failed more than 1 hour ago
|
||||
"""
|
||||
try:
|
||||
from igny8_core.models import ContentPost, SiteIntegration
|
||||
|
||||
# Find failed publications older than 1 hour
|
||||
one_hour_ago = timezone.now() - timedelta(hours=1)
|
||||
failed_content = ContentPost.objects.filter(
|
||||
wordpress_sync_status='failed',
|
||||
last_wordpress_sync__lt=one_hour_ago,
|
||||
wordpress_sync_attempts__lt=5 # Max 5 total attempts
|
||||
)
|
||||
|
||||
active_integrations = SiteIntegration.objects.filter(
|
||||
platform='wordpress',
|
||||
is_active=True
|
||||
)
|
||||
|
||||
retried = 0
|
||||
|
||||
for content in failed_content[:20]: # Limit retries per run
|
||||
for integration in active_integrations:
|
||||
# Reset status and retry
|
||||
content.wordpress_sync_status = 'pending'
|
||||
content.save(update_fields=['wordpress_sync_status'])
|
||||
|
||||
task_id = None
|
||||
if hasattr(content, 'writer_task'):
|
||||
task_id = content.writer_task.id
|
||||
|
||||
publish_content_to_wordpress.delay(
|
||||
content.id,
|
||||
integration.id,
|
||||
task_id
|
||||
)
|
||||
retried += 1
|
||||
break # Only retry with first active integration
|
||||
|
||||
logger.info(f"Retried {retried} failed WordPress publications")
|
||||
return {"success": True, "retried": retried}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrying failed publications: {str(e)}")
|
||||
return {"success": False, "error": str(e)}
|
||||
38
backend/igny8_core/urls/wordpress_publishing.py
Normal file
38
backend/igny8_core/urls/wordpress_publishing.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
URL configuration for WordPress publishing endpoints
|
||||
"""
|
||||
from django.urls import path
|
||||
from igny8_core.api.wordpress_publishing import (
|
||||
publish_single_content,
|
||||
bulk_publish_content,
|
||||
get_wordpress_status,
|
||||
get_wordpress_integrations,
|
||||
retry_failed_wordpress_sync,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# Single content publishing
|
||||
path('content/<int:content_id>/publish-to-wordpress/',
|
||||
publish_single_content,
|
||||
name='publish_single_content'),
|
||||
|
||||
# Bulk content publishing
|
||||
path('content/bulk-publish-to-wordpress/',
|
||||
bulk_publish_content,
|
||||
name='bulk_publish_content'),
|
||||
|
||||
# WordPress status
|
||||
path('content/<int:content_id>/wordpress-status/',
|
||||
get_wordpress_status,
|
||||
name='get_wordpress_status'),
|
||||
|
||||
# WordPress integrations list
|
||||
path('wordpress-integrations/',
|
||||
get_wordpress_integrations,
|
||||
name='get_wordpress_integrations'),
|
||||
|
||||
# Retry failed sync
|
||||
path('content/<int:content_id>/retry-wordpress-sync/',
|
||||
retry_failed_wordpress_sync,
|
||||
name='retry_failed_wordpress_sync'),
|
||||
]
|
||||
@@ -0,0 +1,260 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography,
|
||||
Alert,
|
||||
Box,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Publish as PublishIcon,
|
||||
CheckCircle as SuccessIcon,
|
||||
Error as ErrorIcon
|
||||
} from '@mui/icons-material';
|
||||
import { api } from '../../services/api';
|
||||
|
||||
interface BulkWordPressPublishProps {
|
||||
contentItems: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
imageGenerationStatus: 'pending' | 'generating' | 'complete' | 'failed';
|
||||
wordpressStatus: 'draft' | 'publishing' | 'published' | 'failed';
|
||||
}>;
|
||||
onPublishComplete?: (results: { success: string[], failed: string[] }) => void;
|
||||
}
|
||||
|
||||
interface PublishResult {
|
||||
id: string;
|
||||
title: string;
|
||||
status: 'success' | 'failed' | 'pending';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
|
||||
contentItems,
|
||||
onPublishComplete
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const [results, setResults] = useState<PublishResult[]>([]);
|
||||
|
||||
// Filter items that are ready to publish
|
||||
const readyToPublish = contentItems.filter(item =>
|
||||
item.imageGenerationStatus === 'complete' &&
|
||||
item.wordpressStatus !== 'published' &&
|
||||
item.wordpressStatus !== 'publishing'
|
||||
);
|
||||
|
||||
const handleBulkPublish = async () => {
|
||||
if (readyToPublish.length === 0) return;
|
||||
|
||||
setPublishing(true);
|
||||
setResults([]);
|
||||
|
||||
try {
|
||||
const response = await api.post('/api/wordpress/bulk-publish/', {
|
||||
content_ids: readyToPublish.map(item => item.id)
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const publishResults: PublishResult[] = response.data.data.results.map((result: any) => ({
|
||||
id: result.content_id,
|
||||
title: readyToPublish.find(item => item.id === result.content_id)?.title || 'Unknown',
|
||||
status: result.success ? 'success' : 'failed',
|
||||
message: result.message
|
||||
}));
|
||||
|
||||
setResults(publishResults);
|
||||
|
||||
// Notify parent component
|
||||
if (onPublishComplete) {
|
||||
const success = publishResults.filter(r => r.status === 'success').map(r => r.id);
|
||||
const failed = publishResults.filter(r => r.status === 'failed').map(r => r.id);
|
||||
onPublishComplete({ success, failed });
|
||||
}
|
||||
} else {
|
||||
// Handle API error
|
||||
const failedResults: PublishResult[] = readyToPublish.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
status: 'failed',
|
||||
message: response.data.message || 'Failed to publish'
|
||||
}));
|
||||
setResults(failedResults);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Bulk publish error:', error);
|
||||
const failedResults: PublishResult[] = readyToPublish.map(item => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
status: 'failed',
|
||||
message: 'Network error or server unavailable'
|
||||
}));
|
||||
setResults(failedResults);
|
||||
} finally {
|
||||
setPublishing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!publishing) {
|
||||
setOpen(false);
|
||||
setResults([]);
|
||||
}
|
||||
};
|
||||
|
||||
const successCount = results.filter(r => r.status === 'success').length;
|
||||
const failedCount = results.filter(r => r.status === 'failed').length;
|
||||
|
||||
if (readyToPublish.length === 0) {
|
||||
return null; // Don't show button if nothing to publish
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<PublishIcon />}
|
||||
onClick={() => setOpen(true)}
|
||||
size="small"
|
||||
>
|
||||
Publish Ready ({readyToPublish.length})
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
disableEscapeKeyDown={publishing}
|
||||
>
|
||||
<DialogTitle>
|
||||
Bulk Publish to WordPress
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{!publishing && results.length === 0 && (
|
||||
<>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Ready to publish <strong>{readyToPublish.length}</strong> content items to WordPress:
|
||||
</Typography>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 2, mb: 2 }}>
|
||||
Only content with generated images and not yet published will be included.
|
||||
</Alert>
|
||||
|
||||
<Box sx={{ maxHeight: 300, overflow: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
||||
<List dense>
|
||||
{readyToPublish.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
secondary={`ID: ${item.id}`}
|
||||
/>
|
||||
</ListItem>
|
||||
{index < readyToPublish.length - 1 && <Divider />}
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{publishing && (
|
||||
<Box display="flex" alignItems="center" gap={2} py={4}>
|
||||
<CircularProgress />
|
||||
<Typography>
|
||||
Publishing {readyToPublish.length} items to WordPress...
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!publishing && results.length > 0 && (
|
||||
<>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
{successCount > 0 && (
|
||||
<Alert severity="success" sx={{ mb: 1 }}>
|
||||
✓ Successfully published {successCount} items
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{failedCount > 0 && (
|
||||
<Alert severity="error">
|
||||
✗ Failed to publish {failedCount} items
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Results:
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ maxHeight: 400, overflow: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
||||
<List dense>
|
||||
{results.map((result, index) => (
|
||||
<div key={result.id}>
|
||||
<ListItem>
|
||||
<Box display="flex" alignItems="center" width="100%">
|
||||
{result.status === 'success' ? (
|
||||
<SuccessIcon color="success" sx={{ mr: 1 }} />
|
||||
) : (
|
||||
<ErrorIcon color="error" sx={{ mr: 1 }} />
|
||||
)}
|
||||
<ListItemText
|
||||
primary={result.title}
|
||||
secondary={result.message || (result.status === 'success' ? 'Published successfully' : 'Publishing failed')}
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
{index < results.length - 1 && <Divider />}
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
{!publishing && results.length === 0 && (
|
||||
<>
|
||||
<Button onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBulkPublish}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<PublishIcon />}
|
||||
>
|
||||
Publish All ({readyToPublish.length})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{publishing && (
|
||||
<Button disabled>
|
||||
Publishing...
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!publishing && results.length > 0 && (
|
||||
<Button onClick={handleClose} variant="contained">
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
166
frontend/src/components/WordPressPublish/ContentActionsMenu.tsx
Normal file
166
frontend/src/components/WordPressPublish/ContentActionsMenu.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
MoreVert as MoreVertIcon,
|
||||
Publish as PublishIcon,
|
||||
Edit as EditIcon,
|
||||
Image as ImageIcon,
|
||||
GetApp as ExportIcon,
|
||||
Delete as DeleteIcon
|
||||
} from '@mui/icons-material';
|
||||
import { WordPressPublish } from './WordPressPublish';
|
||||
|
||||
interface ContentActionsMenuProps {
|
||||
contentId: string;
|
||||
contentTitle: string;
|
||||
imageGenerationStatus: 'pending' | 'generating' | 'complete' | 'failed';
|
||||
wordpressStatus: 'draft' | 'publishing' | 'published' | 'failed';
|
||||
onEdit?: () => void;
|
||||
onGenerateImage?: () => void;
|
||||
onExport?: () => void;
|
||||
onDelete?: () => void;
|
||||
onWordPressStatusChange?: (status: string) => void;
|
||||
}
|
||||
|
||||
export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
|
||||
contentId,
|
||||
contentTitle,
|
||||
imageGenerationStatus,
|
||||
wordpressStatus,
|
||||
onEdit,
|
||||
onGenerateImage,
|
||||
onExport,
|
||||
onDelete,
|
||||
onWordPressStatusChange
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [showWordPressDialog, setShowWordPressDialog] = useState(false);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handlePublishClick = () => {
|
||||
setShowWordPressDialog(true);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleMenuAction = (action: () => void) => {
|
||||
action();
|
||||
handleClose();
|
||||
};
|
||||
|
||||
// Check if WordPress publishing is available
|
||||
const canPublishToWordPress = imageGenerationStatus === 'complete' &&
|
||||
wordpressStatus !== 'published' &&
|
||||
wordpressStatus !== 'publishing';
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="more actions"
|
||||
id="content-actions-button"
|
||||
aria-controls={open ? 'content-actions-menu' : undefined}
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
id="content-actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'content-actions-button',
|
||||
}}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
>
|
||||
{/* WordPress Publishing - Only show if images are ready */}
|
||||
{canPublishToWordPress && (
|
||||
<>
|
||||
<MenuItem onClick={handlePublishClick}>
|
||||
<ListItemIcon>
|
||||
<PublishIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Publish to WordPress</ListItemText>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Edit Action */}
|
||||
{onEdit && (
|
||||
<MenuItem onClick={() => handleMenuAction(onEdit)}>
|
||||
<ListItemIcon>
|
||||
<EditIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Generate Image Action */}
|
||||
{onGenerateImage && (
|
||||
<MenuItem onClick={() => handleMenuAction(onGenerateImage)}>
|
||||
<ListItemIcon>
|
||||
<ImageIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Generate Image Prompts</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Export Action */}
|
||||
{onExport && (
|
||||
<MenuItem onClick={() => handleMenuAction(onExport)}>
|
||||
<ListItemIcon>
|
||||
<ExportIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Export</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Delete Action */}
|
||||
{onDelete && (
|
||||
<>
|
||||
<Divider />
|
||||
<MenuItem onClick={() => handleMenuAction(onDelete)} sx={{ color: 'error.main' }}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon fontSize="small" color="error" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
|
||||
{/* WordPress Publish Dialog */}
|
||||
{showWordPressDialog && (
|
||||
<WordPressPublish
|
||||
contentId={contentId}
|
||||
contentTitle={contentTitle}
|
||||
currentStatus={wordpressStatus}
|
||||
imageGenerationStatus={imageGenerationStatus}
|
||||
onStatusChange={onWordPressStatusChange}
|
||||
showOnlyIfImagesReady={true}
|
||||
size="medium"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -27,8 +27,10 @@ export interface WordPressPublishProps {
|
||||
contentId: string;
|
||||
contentTitle: string;
|
||||
currentStatus?: 'draft' | 'publishing' | 'published' | 'failed';
|
||||
imageGenerationStatus?: 'pending' | 'generating' | 'complete' | 'failed';
|
||||
onStatusChange?: (status: string) => void;
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
showOnlyIfImagesReady?: boolean;
|
||||
}
|
||||
|
||||
interface WordPressStatus {
|
||||
@@ -43,8 +45,10 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
contentId,
|
||||
contentTitle,
|
||||
currentStatus = 'draft',
|
||||
imageGenerationStatus = 'pending',
|
||||
onStatusChange,
|
||||
size = 'medium'
|
||||
size = 'medium',
|
||||
showOnlyIfImagesReady = false
|
||||
}) => {
|
||||
const [wpStatus, setWpStatus] = useState<WordPressStatus | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -193,7 +197,34 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
|
||||
const statusInfo = getStatusInfo();
|
||||
|
||||
|
||||
// Don't show publish button if images aren't ready and showOnlyIfImagesReady is true
|
||||
const shouldShowPublishButton = !showOnlyIfImagesReady || imageGenerationStatus === 'complete';
|
||||
|
||||
if (!shouldShowPublishButton) {
|
||||
return (
|
||||
<Tooltip title={`Images must be generated before publishing to WordPress`}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled
|
||||
size={size}
|
||||
startIcon={<PendingIcon />}
|
||||
>
|
||||
Awaiting Images
|
||||
</Button>
|
||||
{size !== 'small' && (
|
||||
<Chip
|
||||
icon={<PendingIcon />}
|
||||
label="Images Pending"
|
||||
color="warning"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const renderButton = () => {
|
||||
if (size === 'small') {
|
||||
@@ -302,7 +333,18 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
This will create a new post on your connected WordPress site with all content,
|
||||
images, categories, and SEO metadata.
|
||||
</Typography>
|
||||
|
||||
|
||||
{imageGenerationStatus === 'complete' && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
✓ Images are generated and ready for publishing
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{imageGenerationStatus !== 'complete' && showOnlyIfImagesReady && (
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
Images are still being generated. Please wait before publishing.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{wpStatus?.wordpress_sync_status === 'success' && (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export { WordPressPublish } from './WordPressPublish';
|
||||
export { BulkWordPressPublish } from './BulkWordPressPublish';
|
||||
export { ContentActionsMenu } from './ContentActionsMenu';
|
||||
export type { WordPressPublishProps } from './WordPressPublish';
|
||||
@@ -321,6 +321,18 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
},
|
||||
'/writer/images': {
|
||||
rowActions: [
|
||||
{
|
||||
key: 'publish_wordpress',
|
||||
label: 'Publish to WordPress',
|
||||
icon: <ArrowRightIcon className="w-5 h-5" />,
|
||||
variant: 'success',
|
||||
shouldShow: (row: any) => {
|
||||
// Only show if images are generated and not already published/publishing
|
||||
return row.status === 'complete' &&
|
||||
(!row.wordpress_status ||
|
||||
(row.wordpress_status !== 'published' && row.wordpress_status !== 'publishing'));
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'update_status',
|
||||
label: 'Update Status',
|
||||
@@ -328,7 +340,14 @@ const tableActionsConfigs: Record<string, TableActionsConfig> = {
|
||||
variant: 'primary',
|
||||
},
|
||||
],
|
||||
bulkActions: [],
|
||||
bulkActions: [
|
||||
{
|
||||
key: 'bulk_publish_wordpress',
|
||||
label: 'Publish Ready to WordPress',
|
||||
icon: <ArrowRightIcon className="w-5 h-5" />,
|
||||
variant: 'success',
|
||||
},
|
||||
],
|
||||
},
|
||||
// Default config (fallback)
|
||||
default: {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
generateImages,
|
||||
bulkUpdateImagesStatus,
|
||||
ContentImage,
|
||||
fetchAPI,
|
||||
api,
|
||||
} from '../../services/api';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons';
|
||||
@@ -207,8 +207,49 @@ export default function Images() {
|
||||
|
||||
// Bulk action handler
|
||||
const handleBulkAction = useCallback(async (action: string, ids: string[]) => {
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}, [toast]);
|
||||
if (action === 'bulk_publish_wordpress') {
|
||||
// Filter to only publish items that have images generated and are not already published
|
||||
const readyItems = images
|
||||
.filter(item => ids.includes(item.content_id.toString()))
|
||||
.filter(item => item.status === 'complete' &&
|
||||
(!item.wordpress_status ||
|
||||
(item.wordpress_status !== 'published' && item.wordpress_status !== 'publishing')));
|
||||
|
||||
if (readyItems.length === 0) {
|
||||
toast.warning('No items are ready for WordPress publishing. Items must have generated images and not already be published.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.post('/api/wordpress/bulk-publish/', {
|
||||
content_ids: readyItems.map(item => item.content_id.toString())
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
const results = response.data.data.results;
|
||||
const successCount = results.filter((r: any) => r.success).length;
|
||||
const failedCount = results.filter((r: any) => !r.success).length;
|
||||
|
||||
if (successCount > 0) {
|
||||
toast.success(`Successfully published ${successCount} item(s) to WordPress`);
|
||||
}
|
||||
if (failedCount > 0) {
|
||||
toast.warning(`${failedCount} item(s) failed to publish`);
|
||||
}
|
||||
|
||||
// Reload images to reflect the updated WordPress status
|
||||
loadImages();
|
||||
} else {
|
||||
toast.error(`Bulk publish failed: ${response.data.message}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Bulk WordPress publish error:', error);
|
||||
toast.error(`Failed to bulk publish to WordPress: ${error.message || 'Network error'}`);
|
||||
}
|
||||
} else {
|
||||
toast.info(`Bulk action "${action}" for ${ids.length} items`);
|
||||
}
|
||||
}, [images, toast, loadImages]);
|
||||
|
||||
// Row action handler
|
||||
const handleRowAction = useCallback(async (action: string, row: ContentImagesGroup) => {
|
||||
@@ -216,8 +257,26 @@ export default function Images() {
|
||||
setStatusUpdateContentId(row.content_id);
|
||||
setStatusUpdateRecordName(row.content_title || `Content #${row.content_id}`);
|
||||
setIsStatusModalOpen(true);
|
||||
} else if (action === 'publish_wordpress') {
|
||||
// Handle WordPress publishing for individual item
|
||||
try {
|
||||
const response = await api.post('/api/wordpress/publish/', {
|
||||
content_id: row.content_id.toString()
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success(`Successfully published "${row.content_title}" to WordPress`);
|
||||
// Reload images to reflect the updated WordPress status
|
||||
loadImages();
|
||||
} else {
|
||||
toast.error(`Failed to publish: ${response.data.message}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('WordPress publish error:', error);
|
||||
toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [loadImages, toast]);
|
||||
|
||||
// Handle status update confirmation
|
||||
const handleStatusUpdate = useCallback(async (status: string) => {
|
||||
|
||||
Reference in New Issue
Block a user