From 10ec7fb33b6b625f8bc3e75dd66008407dabd039 Mon Sep 17 00:00:00 2001 From: alorig <220087330+alorig@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:04:24 +0500 Subject: [PATCH] Revert "123" This reverts commit 5f25631329bd466d111ef437aca2baa6e4e81a2a. --- WORDPRESS_PUBLISH_UI_CHANGES.md | 14 +- .../igny8_core/api/wordpress_publishing.py | 112 ++++---- .../igny8_core/tasks/wordpress_publishing.py | 37 ++- backend/igny8_core/urls.py | 1 - .../igny8_core/urls/wordpress_publishing.py | 6 - .../WordPressPublish/BulkWordPressPublish.tsx | 268 ------------------ .../src/components/WordPressPublish/index.ts | 1 - .../src/config/pages/table-actions.config.tsx | 9 +- frontend/src/pages/Writer/Images.tsx | 90 +++--- 9 files changed, 133 insertions(+), 405 deletions(-) delete mode 100644 frontend/src/components/WordPressPublish/BulkWordPressPublish.tsx diff --git a/WORDPRESS_PUBLISH_UI_CHANGES.md b/WORDPRESS_PUBLISH_UI_CHANGES.md index a358ab0c..8db37392 100644 --- a/WORDPRESS_PUBLISH_UI_CHANGES.md +++ b/WORDPRESS_PUBLISH_UI_CHANGES.md @@ -1,18 +1,10 @@ -# WordPress Publishing UI Implementation Summary +# WordPress Publishing UI Update Summary ## Changes Made -### 🚀 **IMPLEMENTED** WordPress Publishing on Images Page +### 🚀 **MOVED** WordPress Publishing from Content Page to Images Page -**Reasoning**: Images page contains complete content with generated images, making it the optimal place for publishing. Content page removed to prevent premature publishing. - -### 🔧 **Fixed Backend API Issues** -- **Problem**: `/api/wordpress/publish/` endpoint was missing from URL configuration -- **Solution**: Added WordPress publishing URLs to main Django URL configuration -- **Problem**: Model imports were incorrect (ContentPost vs Content) -- **Solution**: Updated all references to use correct Content and SiteIntegration models -- **Problem**: Field names didn't match actual Content model fields -- **Solution**: Updated to use `sync_status`, `external_id`, `external_url` instead of `wordpress_*` fields +**Reasoning**: Content page only contains text content without generated images, making it premature to publish. Images page contains complete content with generated images, making it the optimal place for publishing. ### 📍 **WordPress Publishing Now Available On Images Page** diff --git a/backend/igny8_core/api/wordpress_publishing.py b/backend/igny8_core/api/wordpress_publishing.py index 1c700819..f2dd09c1 100644 --- a/backend/igny8_core/api/wordpress_publishing.py +++ b/backend/igny8_core/api/wordpress_publishing.py @@ -10,57 +10,16 @@ from django.shortcuts import get_object_or_404 from django.utils import timezone from typing import Dict, Any, List -from igny8_core.business.content.models import Content -from igny8_core.business.integration.models import SiteIntegration +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 simple_publish_content(request) -> Response: - """ - Simple publish endpoint that gets content_id from POST body - POST /api/wordpress/publish/ - Body: {"content_id": "123"} - """ - content_id = request.data.get('content_id') - if not content_id: - return Response( - { - 'success': False, - 'message': 'content_id is required', - 'error': 'missing_content_id' - }, - status=status.HTTP_400_BAD_REQUEST - ) - - try: - content_id = int(content_id) - except (ValueError, TypeError): - return Response( - { - 'success': False, - 'message': 'Invalid content_id format', - 'error': 'invalid_content_id' - }, - status=status.HTTP_400_BAD_REQUEST - ) - - # Call the main publish function - return publish_single_content_by_id(request, content_id) - - @api_view(['POST']) @permission_classes([IsAuthenticated]) def publish_single_content(request, content_id: int) -> Response: - """URL-based publish endpoint""" - return publish_single_content_by_id(request, content_id) - - -def publish_single_content_by_id(request, content_id: int) -> Response: """ Publish a single content item to WordPress @@ -73,7 +32,7 @@ def publish_single_content_by_id(request, content_id: int) -> Response: } """ try: - content = get_object_or_404(Content, id=content_id) + content = get_object_or_404(ContentPost, id=content_id) # Check permissions if not request.user.has_perm('content.change_contentpost'): @@ -86,23 +45,47 @@ def publish_single_content_by_id(request, content_id: int) -> Response: status=status.HTTP_403_FORBIDDEN ) - # Check if already published - if content.sync_status == 'success': + # 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.external_id, - 'wordpress_post_url': content.external_url, + 'wordpress_post_id': content.wordpress_post_id, + 'wordpress_post_url': content.wordpress_post_url, 'status': 'already_published' } } ) # Check if currently syncing - if content.sync_status == 'syncing': + if content.wordpress_sync_status == 'syncing': return Response( { 'success': False, @@ -113,7 +96,7 @@ def publish_single_content_by_id(request, content_id: int) -> Response: ) # Validate content is ready for publishing - if not content.title or not content.content_html: + if not content.title or not (content.content_html or content.content): return Response( { 'success': False, @@ -123,23 +106,34 @@ def publish_single_content_by_id(request, content_id: int) -> Response: status=status.HTTP_400_BAD_REQUEST ) - # For now, just simulate successful publishing (simplified version) - content.sync_status = 'success' - content.external_id = f'wp_{content.id}' - content.external_url = f'https://example-site.com/posts/{content.id}/' - content.save(update_fields=['sync_status', 'external_id', 'external_url']) + # 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 published to WordPress successfully', + 'message': 'Content queued for WordPress publishing', 'data': { 'content_id': content.id, - 'wordpress_post_id': content.external_id, - 'wordpress_post_url': content.external_url, - 'status': 'published' + 'site_integration_id': site_integration.id, + 'task_id': task_result.id, + 'status': 'queued' } - } + }, + status=status.HTTP_202_ACCEPTED ) except Exception as e: diff --git a/backend/igny8_core/tasks/wordpress_publishing.py b/backend/igny8_core/tasks/wordpress_publishing.py index ea5a7dcf..a564d8e5 100644 --- a/backend/igny8_core/tasks/wordpress_publishing.py +++ b/backend/igny8_core/tasks/wordpress_publishing.py @@ -28,46 +28,45 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int Dict with success status and details """ try: - from igny8_core.business.content.models import Content - from igny8_core.business.integration.models import SiteIntegration + from igny8_core.models import ContentPost, SiteIntegration # Get content and site integration try: - content = Content.objects.get(id=content_id) + content = ContentPost.objects.get(id=content_id) site_integration = SiteIntegration.objects.get(id=site_integration_id) - except (Content.DoesNotExist, SiteIntegration.DoesNotExist) as e: + 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.sync_status == 'success': + 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.external_id} + return {"success": True, "message": "Already published", "wordpress_post_id": content.wordpress_post_id} - if content.sync_status == 'syncing': + 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.sync_status = 'syncing' - content.save(update_fields=['sync_status']) + 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, - 'excerpt': '', # Content model doesn't have brief field + 'content_html': content.content_html or content.content, + 'excerpt': content.brief or '', 'status': 'publish', - 'author_email': None, # Content model doesn't have author field - 'author_name': None, # Content model doesn't have author field - 'published_at': None, # Content model doesn't have published_at field - 'seo_title': content.meta_title or '', - 'seo_description': content.meta_description or '', - 'featured_image_url': None, # Content model doesn't have featured_image field - 'sectors': [], # Content model doesn't have sectors field - 'clusters': [{'id': content.cluster.id, 'name': content.cluster.name}] if content.cluster else [], + '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', []) } diff --git a/backend/igny8_core/urls.py b/backend/igny8_core/urls.py index 502eb1a9..c1cd374f 100644 --- a/backend/igny8_core/urls.py +++ b/backend/igny8_core/urls.py @@ -35,7 +35,6 @@ urlpatterns = [ path('api/v1/optimizer/', include('igny8_core.modules.optimizer.urls')), # Optimizer endpoints path('api/v1/publisher/', include('igny8_core.modules.publisher.urls')), # Publisher endpoints path('api/v1/integration/', include('igny8_core.modules.integration.urls')), # Integration endpoints - path('api/wordpress/', include('igny8_core.urls.wordpress_publishing')), # WordPress publishing endpoints # OpenAPI Schema and Documentation path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), diff --git a/backend/igny8_core/urls/wordpress_publishing.py b/backend/igny8_core/urls/wordpress_publishing.py index 7080979b..4a6e6765 100644 --- a/backend/igny8_core/urls/wordpress_publishing.py +++ b/backend/igny8_core/urls/wordpress_publishing.py @@ -3,7 +3,6 @@ URL configuration for WordPress publishing endpoints """ from django.urls import path from igny8_core.api.wordpress_publishing import ( - simple_publish_content, publish_single_content, bulk_publish_content, get_wordpress_status, @@ -12,11 +11,6 @@ from igny8_core.api.wordpress_publishing import ( ) urlpatterns = [ - # Simple publish endpoint (expects content_id in POST body) - path('publish/', - simple_publish_content, - name='simple_publish_content'), - # Single content publishing path('content//publish-to-wordpress/', publish_single_content, diff --git a/frontend/src/components/WordPressPublish/BulkWordPressPublish.tsx b/frontend/src/components/WordPressPublish/BulkWordPressPublish.tsx deleted file mode 100644 index 0ebe0f43..00000000 --- a/frontend/src/components/WordPressPublish/BulkWordPressPublish.tsx +++ /dev/null @@ -1,268 +0,0 @@ -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 { fetchAPI } from '../../services/api'; - -interface BulkWordPressPublishProps { - contentItems: Array<{ - content_id: number; - content_title: string; - overall_status: 'pending' | 'partial' | 'complete' | 'failed'; - }>; - onPublishComplete?: (results: { success: string[], failed: string[] }) => void; -} - -interface PublishResult { - id: number; - title: string; - status: 'success' | 'failed' | 'pending'; - message?: string; -} - -export const BulkWordPressPublish: React.FC = ({ - contentItems, - onPublishComplete -}) => { - const [open, setOpen] = useState(false); - const [publishing, setPublishing] = useState(false); - const [results, setResults] = useState([]); - - // Filter items that are ready to publish - const readyToPublish = contentItems.filter(item => - item.overall_status === 'complete' - ); - - const handleBulkPublish = async () => { - if (readyToPublish.length === 0) return; - - setPublishing(true); - setResults([]); - - let successCount = 0; - let failedCount = 0; - const publishResults: PublishResult[] = []; - - // Process each item individually - for (const item of readyToPublish) { - try { - const response = await fetchAPI(`/api/wordpress/publish/`, { - method: 'POST', - body: JSON.stringify({ - content_id: item.content_id.toString() - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (response.success) { - successCount++; - publishResults.push({ - id: item.content_id, - title: item.content_title, - status: 'success', - message: 'Published successfully' - }); - } else { - failedCount++; - publishResults.push({ - id: item.content_id, - title: item.content_title, - status: 'failed', - message: response.message || 'Publishing failed' - }); - } - } catch (error: any) { - failedCount++; - publishResults.push({ - id: item.content_id, - title: item.content_title, - status: 'failed', - message: error.message || 'Network error' - }); - } - } - - setResults(publishResults); - setPublishing(false); - - // Notify parent component - if (onPublishComplete) { - const success = publishResults.filter(r => r.status === 'success').map(r => r.id.toString()); - const failed = publishResults.filter(r => r.status === 'failed').map(r => r.id.toString()); - onPublishComplete({ success, failed }); - } - }; - - 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 ( - <> - - - - - Bulk Publish to WordPress - - - - {!publishing && results.length === 0 && ( - <> - - Ready to publish {readyToPublish.length} content items to WordPress: - - - - Only content with generated images will be published. - - - - - {readyToPublish.map((item, index) => ( -
- - - - {index < readyToPublish.length - 1 && } -
- ))} -
-
- - )} - - {publishing && ( - - - - Publishing {readyToPublish.length} items to WordPress... - - - )} - - {!publishing && results.length > 0 && ( - <> - - {successCount > 0 && ( - - ✓ Successfully published {successCount} items - - )} - - {failedCount > 0 && ( - - ✗ Failed to publish {failedCount} items - - )} - - - - Results: - - - - - {results.map((result, index) => ( -
- - - {result.status === 'success' ? ( - - ) : ( - - )} - - - - {index < results.length - 1 && } -
- ))} -
-
- - )} -
- - - {!publishing && results.length === 0 && ( - <> - - - - )} - - {publishing && ( - - )} - - {!publishing && results.length > 0 && ( - - )} - -
- - ); -}; \ No newline at end of file diff --git a/frontend/src/components/WordPressPublish/index.ts b/frontend/src/components/WordPressPublish/index.ts index 77482a94..a28104ec 100644 --- a/frontend/src/components/WordPressPublish/index.ts +++ b/frontend/src/components/WordPressPublish/index.ts @@ -1,3 +1,2 @@ export { WordPressPublish } from './WordPressPublish'; -export { BulkWordPressPublish } from './BulkWordPressPublish'; export type { WordPressPublishProps } from './WordPressPublish'; \ No newline at end of file diff --git a/frontend/src/config/pages/table-actions.config.tsx b/frontend/src/config/pages/table-actions.config.tsx index d59c0034..99b6a39e 100644 --- a/frontend/src/config/pages/table-actions.config.tsx +++ b/frontend/src/config/pages/table-actions.config.tsx @@ -338,7 +338,14 @@ const tableActionsConfigs: Record = { variant: 'primary', }, ], - bulkActions: [], + bulkActions: [ + { + key: 'bulk_publish_wordpress', + label: 'Publish Ready to WordPress', + icon: , + variant: 'success', + }, + ], }, // Default config (fallback) default: { diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index eb67c6e8..0f544871 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -14,6 +14,7 @@ import { bulkUpdateImagesStatus, ContentImage, fetchAPI, + publishContent, } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, DownloadIcon, BoltIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; @@ -24,7 +25,6 @@ import { useResourceDebug } from '../../hooks/useResourceDebug'; import PageHeader from '../../components/common/PageHeader'; import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; import { Modal } from '../../components/ui/modal'; -import { BulkWordPressPublish } from '../../components/WordPressPublish'; export default function Images() { const toast = useToast(); @@ -208,8 +208,50 @@ 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 + const readyItems = images + .filter(item => ids.includes(item.content_id.toString())) + .filter(item => item.overall_status === 'complete'); + + 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 { + let successCount = 0; + let failedCount = 0; + const errors: string[] = []; + + // Process each item individually using the existing publishContent function + for (const item of readyItems) { + try { + await publishContent(item.content_id); + successCount++; + } catch (error: any) { + failedCount++; + errors.push(`${item.content_title}: ${error.message}`); + } + } + + 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(); + } 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) => { @@ -218,25 +260,13 @@ export default function Images() { setStatusUpdateRecordName(row.content_title || `Content #${row.content_id}`); setIsStatusModalOpen(true); } else if (action === 'publish_wordpress') { - // Handle WordPress publishing for individual item using WordPress API endpoint + // Handle WordPress publishing for individual item using existing publishContent function try { - const response = await fetchAPI(`/api/wordpress/publish/`, { - method: 'POST', - body: JSON.stringify({ - content_id: row.content_id.toString() - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (response.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.message}`); - } + // Use the existing publishContent function from the API + const result = await publishContent(row.content_id); + toast.success(`Successfully published "${row.content_title}" to WordPress! View at: ${result.external_url}`); + // Reload images to reflect the updated WordPress status + loadImages(); } catch (error: any) { console.error('WordPress publish error:', error); toast.error(`Failed to publish to WordPress: ${error.message || 'Network error'}`); @@ -501,24 +531,6 @@ export default function Images() { title="Content Images" badge={{ icon: , color: 'orange' }} navigation={} - actions={ - ({ - content_id: item.content_id, - content_title: item.content_title, - overall_status: item.overall_status - }))} - onPublishComplete={(results) => { - if (results.success.length > 0) { - toast.success(`Published ${results.success.length} items successfully`); - } - if (results.failed.length > 0) { - toast.error(`Failed to publish ${results.failed.length} items`); - } - loadImages(); - }} - /> - } />