feat: Implement WordPress publishing and unpublishing actions
- Added conditional visibility for table actions based on content state (published/draft). - Introduced `publishContent` and `unpublishContent` API functions for handling WordPress integration. - Updated `Content` component to manage publish/unpublish actions with appropriate error handling and success notifications. - Refactored `PostEditor` to remove deprecated SEO fields and consolidate taxonomy management. - Enhanced `TablePageTemplate` to filter row actions based on visibility conditions. - Updated backend API to support publishing and unpublishing content with proper status updates and external references.
This commit is contained in:
@@ -1011,23 +1011,39 @@ class ContentIdeasViewSet(SiteSectorModelViewSet):
|
||||
|
||||
created_tasks = []
|
||||
for idea in ideas:
|
||||
# Stage 3: Inherit metadata from idea
|
||||
# STAGE 3: Map idea fields to final Task schema
|
||||
# Map site_entity_type → content_type
|
||||
content_type = idea.site_entity_type or 'post'
|
||||
|
||||
# Map cluster_role → content_structure
|
||||
# hub → article, supporting → guide, attribute → comparison
|
||||
role_to_structure = {
|
||||
'hub': 'article',
|
||||
'supporting': 'guide',
|
||||
'attribute': 'comparison',
|
||||
}
|
||||
content_structure = role_to_structure.get(idea.cluster_role, 'article')
|
||||
|
||||
# Create task with Stage 1 final fields
|
||||
task = Tasks.objects.create(
|
||||
title=idea.idea_title,
|
||||
description=idea.description or '',
|
||||
keywords=idea.target_keywords or '',
|
||||
cluster=idea.keyword_cluster,
|
||||
idea=idea,
|
||||
content_type=content_type,
|
||||
content_structure=content_structure,
|
||||
taxonomy_term=None, # Can be set later if taxonomy is available
|
||||
status='queued',
|
||||
account=idea.account,
|
||||
site=idea.site,
|
||||
sector=idea.sector,
|
||||
# Stage 3: Inherit entity metadata (use standardized fields)
|
||||
entity_type=(idea.site_entity_type or 'post'),
|
||||
taxonomy=idea.taxonomy,
|
||||
cluster_role=(idea.cluster_role or 'hub'),
|
||||
)
|
||||
|
||||
# Link keywords from idea to task
|
||||
if idea.keyword_objects.exists():
|
||||
task.keywords.set(idea.keyword_objects.all())
|
||||
|
||||
created_tasks.append(task.id)
|
||||
|
||||
# Update idea status
|
||||
idea.status = 'scheduled'
|
||||
idea.save()
|
||||
|
||||
@@ -761,22 +761,34 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
@action(detail=True, methods=['post'], url_path='publish', url_name='publish', permission_classes=[IsAuthenticatedAndActive, IsEditorOrAbove])
|
||||
def publish(self, request, pk=None):
|
||||
"""
|
||||
Stage 1: Publish content to WordPress site.
|
||||
STAGE 3: Publish content to WordPress site.
|
||||
Prevents duplicate publishing and updates external_id/external_url.
|
||||
|
||||
POST /api/v1/writer/content/{id}/publish/
|
||||
{
|
||||
"site_id": 1 // WordPress site to publish to
|
||||
"site_id": 1, // Optional - defaults to content's site
|
||||
"status": "publish" // Optional - draft or publish
|
||||
}
|
||||
"""
|
||||
import requests
|
||||
from igny8_core.auth.models import Site
|
||||
from igny8_core.business.publishing.services.adapters.wordpress_adapter import WordPressAdapter
|
||||
|
||||
content = self.get_object()
|
||||
site_id = request.data.get('site_id')
|
||||
|
||||
# STAGE 3: Prevent duplicate publishing
|
||||
if content.external_id:
|
||||
return error_response(
|
||||
error='Content already published. Use WordPress to update or unpublish first.',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request,
|
||||
errors={'external_id': [f'Already published with ID: {content.external_id}']}
|
||||
)
|
||||
|
||||
# Get site (use content's site if not specified)
|
||||
site_id = request.data.get('site_id') or content.site_id
|
||||
if not site_id:
|
||||
return error_response(
|
||||
error='site_id is required',
|
||||
error='site_id is required or content must have a site',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
@@ -790,50 +802,40 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
request=request
|
||||
)
|
||||
|
||||
# Build WordPress API payload
|
||||
wp_payload = {
|
||||
'title': content.title,
|
||||
'content': content.content_html,
|
||||
'status': 'publish',
|
||||
'meta': {
|
||||
'_igny8_content_id': str(content.id),
|
||||
'_igny8_cluster_id': str(content.cluster_id) if content.cluster_id else '',
|
||||
'_igny8_content_type': content.content_type,
|
||||
'_igny8_content_structure': content.content_structure,
|
||||
},
|
||||
}
|
||||
# Get WordPress credentials from site metadata
|
||||
wp_credentials = site.metadata.get('wordpress', {}) if site.metadata else {}
|
||||
wp_url = wp_credentials.get('url') or site.url
|
||||
wp_username = wp_credentials.get('username')
|
||||
wp_app_password = wp_credentials.get('app_password')
|
||||
|
||||
# Add taxonomy terms if present
|
||||
if content.taxonomy_terms.exists():
|
||||
wp_categories = []
|
||||
wp_tags = []
|
||||
for term in content.taxonomy_terms.all():
|
||||
if term.taxonomy_type == 'category' and term.external_id:
|
||||
wp_categories.append(int(term.external_id))
|
||||
elif term.taxonomy_type == 'post_tag' and term.external_id:
|
||||
wp_tags.append(int(term.external_id))
|
||||
|
||||
if wp_categories:
|
||||
wp_payload['categories'] = wp_categories
|
||||
if wp_tags:
|
||||
wp_payload['tags'] = wp_tags
|
||||
if not wp_username or not wp_app_password:
|
||||
return error_response(
|
||||
error='WordPress credentials not configured for this site',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request,
|
||||
errors={'credentials': ['Missing WordPress username or app password in site settings']}
|
||||
)
|
||||
|
||||
# Call WordPress REST API (using site's WP credentials)
|
||||
try:
|
||||
# TODO: Get WP credentials from site.metadata or environment
|
||||
wp_url = site.url
|
||||
wp_endpoint = f'{wp_url}/wp-json/wp/v2/posts'
|
||||
|
||||
# Placeholder - real implementation needs proper auth
|
||||
# response = requests.post(wp_endpoint, json=wp_payload, auth=(wp_user, wp_password))
|
||||
# response.raise_for_status()
|
||||
# wp_post_data = response.json()
|
||||
|
||||
# For now, mark as published and return success
|
||||
# Use WordPress adapter to publish
|
||||
adapter = WordPressAdapter()
|
||||
wp_status = request.data.get('status', 'publish') # draft or publish
|
||||
|
||||
result = adapter.publish(
|
||||
content=content,
|
||||
destination_config={
|
||||
'site_url': wp_url,
|
||||
'username': wp_username,
|
||||
'app_password': wp_app_password,
|
||||
'status': wp_status,
|
||||
}
|
||||
)
|
||||
|
||||
if result.get('success'):
|
||||
# STAGE 3: Update content with external references
|
||||
content.external_id = result.get('external_id')
|
||||
content.external_url = result.get('url')
|
||||
content.status = 'published'
|
||||
content.external_id = '12345'
|
||||
content.external_url = f'{wp_url}/?p=12345'
|
||||
content.save()
|
||||
content.save(update_fields=['external_id', 'external_url', 'status', 'updated_at'])
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
@@ -841,18 +843,55 @@ class ContentViewSet(SiteSectorModelViewSet):
|
||||
'status': content.status,
|
||||
'external_id': content.external_id,
|
||||
'external_url': content.external_url,
|
||||
'message': 'Content published to WordPress (placeholder implementation)',
|
||||
},
|
||||
message='Content published successfully',
|
||||
message='Content published to WordPress successfully',
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
else:
|
||||
return error_response(
|
||||
error=f'Failed to publish to WordPress: {str(e)}',
|
||||
error=f"Failed to publish to WordPress: {result.get('metadata', {}).get('error', 'Unknown error')}",
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='unpublish', url_name='unpublish', permission_classes=[IsAuthenticatedAndActive, IsEditorOrAbove])
|
||||
def unpublish(self, request, pk=None):
|
||||
"""
|
||||
STAGE 3: Unpublish content - clear external references and revert to draft.
|
||||
Note: This does NOT delete the WordPress post, only clears the link.
|
||||
|
||||
POST /api/v1/writer/content/{id}/unpublish/
|
||||
"""
|
||||
content = self.get_object()
|
||||
|
||||
if not content.external_id:
|
||||
return error_response(
|
||||
error='Content is not published',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Store the old values for response
|
||||
old_external_id = content.external_id
|
||||
old_external_url = content.external_url
|
||||
|
||||
# Clear external references and revert status
|
||||
content.external_id = None
|
||||
content.external_url = None
|
||||
content.status = 'draft'
|
||||
content.save(update_fields=['external_id', 'external_url', 'status', 'updated_at'])
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'content_id': content.id,
|
||||
'status': content.status,
|
||||
'was_external_id': old_external_id,
|
||||
'was_external_url': old_external_url,
|
||||
},
|
||||
message='Content unpublished successfully. WordPress post was not deleted.',
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='generate_image_prompts', url_name='generate_image_prompts')
|
||||
def generate_image_prompts(self, request):
|
||||
"""Generate image prompts for content records - same pattern as other AI functions"""
|
||||
|
||||
Reference in New Issue
Block a user