logging system
This commit is contained in:
317
LOGGING-REFERENCE.md
Normal file
317
LOGGING-REFERENCE.md
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
# IGNY8 Publish/Sync Logging System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Comprehensive file-based logging system for tracking all WordPress publish/sync workflows with site-specific stamps.
|
||||||
|
|
||||||
|
## Log Format
|
||||||
|
All logs include site identification stamps in the format: `[site-id-domain]`
|
||||||
|
|
||||||
|
Example: `[16-homeg8.com]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend Logs (IGNY8 SaaS Platform)
|
||||||
|
|
||||||
|
### Location
|
||||||
|
```
|
||||||
|
/data/app/igny8/backend/logs/publish-sync-logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Files
|
||||||
|
|
||||||
|
#### 1. `publish-sync.log`
|
||||||
|
**Purpose:** Main workflow logging for content publishing
|
||||||
|
|
||||||
|
**Contains:**
|
||||||
|
- Complete publish workflow from start to finish
|
||||||
|
- Step-by-step progress (STEP 1, STEP 2, etc.)
|
||||||
|
- Content preparation (categories, tags, images, SEO)
|
||||||
|
- Success/failure status
|
||||||
|
- Timing information (duration in milliseconds)
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
```
|
||||||
|
[2025-12-01 00:45:23] [INFO] ================================================================================
|
||||||
|
[2025-12-01 00:45:23] [INFO] [16-homeg8.com] 🎯 PUBLISH WORKFLOW STARTED
|
||||||
|
[2025-12-01 00:45:23] [INFO] [16-homeg8.com] Content ID: 123
|
||||||
|
[2025-12-01 00:45:23] [INFO] [16-homeg8.com] STEP 1: Loading content and integration from database...
|
||||||
|
[2025-12-01 00:45:23] [INFO] [16-homeg8.com] ✅ Content loaded: 'Article Title'
|
||||||
|
...
|
||||||
|
[2025-12-01 00:45:25] [INFO] [16-homeg8.com] 🎉 PUBLISH WORKFLOW COMPLETED SUCCESSFULLY
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. `wordpress-api.log`
|
||||||
|
**Purpose:** HTTP API communication with WordPress sites
|
||||||
|
|
||||||
|
**Contains:**
|
||||||
|
- API requests (method, endpoint, headers, payload)
|
||||||
|
- API responses (status code, body)
|
||||||
|
- API errors and timeouts
|
||||||
|
- Request/response timing
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
```
|
||||||
|
[2025-12-01 00:45:24] [INFO] [16-homeg8.com] API REQUEST: POST https://homeg8.com/wp-json/igny8/v1/publish
|
||||||
|
[2025-12-01 00:45:24] [INFO] [16-homeg8.com] Headers: X-IGNY8-API-Key: ***abcd
|
||||||
|
[2025-12-01 00:45:24] [INFO] [16-homeg8.com] Payload: {...}
|
||||||
|
[2025-12-01 00:45:25] [INFO] [16-homeg8.com] API RESPONSE: 201
|
||||||
|
[2025-12-01 00:45:25] [INFO] [16-homeg8.com] Response body: {"success":true...}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. `webhooks.log`
|
||||||
|
**Purpose:** Webhook events received from WordPress
|
||||||
|
|
||||||
|
**Contains:**
|
||||||
|
- Webhook POST requests from WordPress
|
||||||
|
- Status updates from WordPress to IGNY8
|
||||||
|
- Metadata synchronization events
|
||||||
|
- Authentication verification
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
```
|
||||||
|
[2025-12-01 00:45:30] [INFO] [16-homeg8.com] 📥 WORDPRESS STATUS WEBHOOK RECEIVED
|
||||||
|
[2025-12-01 00:45:30] [INFO] [16-homeg8.com] Content ID: 123
|
||||||
|
[2025-12-01 00:45:30] [INFO] [16-homeg8.com] Post ID: 456
|
||||||
|
[2025-12-01 00:45:30] [INFO] [16-homeg8.com] Post Status: publish
|
||||||
|
```
|
||||||
|
|
||||||
|
### Viewing Backend Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View all publish logs
|
||||||
|
tail -f /data/app/igny8/backend/logs/publish-sync-logs/publish-sync.log
|
||||||
|
|
||||||
|
# View API communication
|
||||||
|
tail -f /data/app/igny8/backend/logs/publish-sync-logs/wordpress-api.log
|
||||||
|
|
||||||
|
# View webhook events
|
||||||
|
tail -f /data/app/igny8/backend/logs/publish-sync-logs/webhooks.log
|
||||||
|
|
||||||
|
# Search for specific site
|
||||||
|
grep "\[16-homeg8.com\]" /data/app/igny8/backend/logs/publish-sync-logs/publish-sync.log
|
||||||
|
|
||||||
|
# Search for errors
|
||||||
|
grep "\[ERROR\]" /data/app/igny8/backend/logs/publish-sync-logs/*.log
|
||||||
|
|
||||||
|
# View last 100 lines of all logs
|
||||||
|
tail -n 100 /data/app/igny8/backend/logs/publish-sync-logs/*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WordPress Plugin Logs
|
||||||
|
|
||||||
|
### Location
|
||||||
|
```
|
||||||
|
/wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
On your server:
|
||||||
|
```
|
||||||
|
/home/u276331481/domains/homeg8.com/public_html/wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Files
|
||||||
|
|
||||||
|
#### 1. `publish-sync.log`
|
||||||
|
**Purpose:** WordPress post creation and synchronization
|
||||||
|
|
||||||
|
**Contains:**
|
||||||
|
- Post creation workflow
|
||||||
|
- Categories/tags assignment
|
||||||
|
- Featured image processing
|
||||||
|
- SEO metadata updates
|
||||||
|
- Gallery image handling
|
||||||
|
- Success/failure status
|
||||||
|
|
||||||
|
**Format:**
|
||||||
|
```
|
||||||
|
[2025-12-01 00:45:25] [INFO] ================================================================================
|
||||||
|
[2025-12-01 00:45:25] [INFO] [16-homeg8.com] 🎯 CREATE WORDPRESS POST FROM IGNY8
|
||||||
|
[2025-12-01 00:45:25] [INFO] [16-homeg8.com] Content ID: 123
|
||||||
|
[2025-12-01 00:45:25] [INFO] [16-homeg8.com] STEP: Processing 3 categories
|
||||||
|
[2025-12-01 00:45:25] [INFO] [16-homeg8.com] ✅ Assigned 3 categories to post 456
|
||||||
|
...
|
||||||
|
[2025-12-01 00:45:26] [INFO] [16-homeg8.com] 🎉 POST CREATION COMPLETED SUCCESSFULLY
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. `wordpress-api.log`
|
||||||
|
**Purpose:** WordPress outgoing API calls to IGNY8
|
||||||
|
|
||||||
|
**Contains:**
|
||||||
|
- API requests from WordPress to IGNY8
|
||||||
|
- Task updates
|
||||||
|
- Status synchronization
|
||||||
|
- API responses
|
||||||
|
|
||||||
|
#### 3. `webhooks.log`
|
||||||
|
**Purpose:** Outgoing webhook calls from WordPress to IGNY8
|
||||||
|
|
||||||
|
**Contains:**
|
||||||
|
- Status webhook sends
|
||||||
|
- Metadata webhook sends
|
||||||
|
- Response handling
|
||||||
|
|
||||||
|
### Viewing WordPress Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On the WordPress server
|
||||||
|
cd /home/u276331481/domains/homeg8.com/public_html/wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/
|
||||||
|
|
||||||
|
# View all publish logs
|
||||||
|
tail -f publish-sync.log
|
||||||
|
|
||||||
|
# View API communication
|
||||||
|
tail -f wordpress-api.log
|
||||||
|
|
||||||
|
# View webhook events
|
||||||
|
tail -f webhooks.log
|
||||||
|
|
||||||
|
# Search for specific content ID
|
||||||
|
grep "Content ID: 123" publish-sync.log
|
||||||
|
|
||||||
|
# View last 50 lines
|
||||||
|
tail -n 50 publish-sync.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Log Retention
|
||||||
|
|
||||||
|
### Automatic Rotation
|
||||||
|
- **Max file size:** 10 MB per log file
|
||||||
|
- **Backup count:** 10 rotated files kept
|
||||||
|
- **Total storage:** ~100 MB per log type (10 files × 10 MB)
|
||||||
|
|
||||||
|
### Rotated Files
|
||||||
|
```
|
||||||
|
publish-sync.log # Current log
|
||||||
|
publish-sync.log.1 # Previous rotation
|
||||||
|
publish-sync.log.2 # Older rotation
|
||||||
|
...
|
||||||
|
publish-sync.log.10 # Oldest rotation (auto-deleted when new rotation created)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting Guide
|
||||||
|
|
||||||
|
### 1. Content Not Publishing
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
```bash
|
||||||
|
# Backend - Publishing workflow
|
||||||
|
grep "PUBLISH WORKFLOW" /data/app/igny8/backend/logs/publish-sync-logs/publish-sync.log | tail -20
|
||||||
|
|
||||||
|
# Backend - API errors
|
||||||
|
grep "\[ERROR\]" /data/app/igny8/backend/logs/publish-sync-logs/wordpress-api.log | tail -20
|
||||||
|
|
||||||
|
# WordPress - Post creation
|
||||||
|
tail -50 /wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/publish-sync.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Missing Categories/Tags
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
```bash
|
||||||
|
# WordPress logs - Category processing
|
||||||
|
grep "Processing.*categories" /wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/publish-sync.log
|
||||||
|
|
||||||
|
# WordPress logs - Tag processing
|
||||||
|
grep "Processing.*tags" /wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/publish-sync.log
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Status Not Syncing
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
```bash
|
||||||
|
# Backend - Webhook reception
|
||||||
|
grep "WEBHOOK RECEIVED" /data/app/igny8/backend/logs/publish-sync-logs/webhooks.log | tail -20
|
||||||
|
|
||||||
|
# WordPress - Webhook sending
|
||||||
|
grep "webhook" /wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/webhooks.log | tail -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. API Connection Issues
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
```bash
|
||||||
|
# Backend - API requests/responses
|
||||||
|
grep "API REQUEST\|API RESPONSE\|API ERROR" /data/app/igny8/backend/logs/publish-sync-logs/wordpress-api.log | tail -30
|
||||||
|
|
||||||
|
# Look for timeouts
|
||||||
|
grep "timeout" /data/app/igny8/backend/logs/publish-sync-logs/*.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Log Patterns
|
||||||
|
|
||||||
|
### Successful Publish
|
||||||
|
```
|
||||||
|
[INFO] [16-homeg8.com] 🎯 PUBLISH WORKFLOW STARTED
|
||||||
|
[INFO] [16-homeg8.com] STEP 1: Loading content...
|
||||||
|
[INFO] [16-homeg8.com] STEP 2: Checking if already published...
|
||||||
|
[INFO] [16-homeg8.com] STEP 3: Generating excerpt...
|
||||||
|
[INFO] [16-homeg8.com] STEP 4: Loading taxonomy mappings...
|
||||||
|
[INFO] [16-homeg8.com] STEP 5: Extracting keywords...
|
||||||
|
[INFO] [16-homeg8.com] STEP 6: Loading images...
|
||||||
|
[INFO] [16-homeg8.com] STEP 7: Building WordPress API payload...
|
||||||
|
[INFO] [16-homeg8.com] STEP 8: Sending POST request...
|
||||||
|
[INFO] [16-homeg8.com] ✅ WordPress responded: HTTP 201
|
||||||
|
[INFO] [16-homeg8.com] STEP 9: Processing WordPress response...
|
||||||
|
[INFO] [16-homeg8.com] ✅ WordPress post created successfully
|
||||||
|
[INFO] [16-homeg8.com] 🎉 PUBLISH WORKFLOW COMPLETED SUCCESSFULLY
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Error
|
||||||
|
```
|
||||||
|
[ERROR] [16-homeg8.com] ❌ Unexpected status code: 500
|
||||||
|
[ERROR] [16-homeg8.com] Response: {"code":"internal_server_error"...}
|
||||||
|
[ERROR] [16-homeg8.com] API ERROR: WordPress API error: HTTP 500
|
||||||
|
```
|
||||||
|
|
||||||
|
### Missing Fields
|
||||||
|
```
|
||||||
|
[WARNING] [16-homeg8.com] ⚠️ No categories in content_data
|
||||||
|
[WARNING] [16-homeg8.com] ⚠️ No featured image in content_data
|
||||||
|
[WARNING] [16-homeg8.com] ⚠️ No SEO description in content_data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accessing Logs via Debug Page
|
||||||
|
|
||||||
|
The frontend Debug Status page will display these logs in real-time (coming next).
|
||||||
|
|
||||||
|
**URL:** https://app.igny8.com/settings/debug-status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Log Cleanup
|
||||||
|
|
||||||
|
### Manual Cleanup
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
rm -f /data/app/igny8/backend/logs/publish-sync-logs/*.log*
|
||||||
|
|
||||||
|
# WordPress (on your server)
|
||||||
|
rm -f /home/u276331481/domains/homeg8.com/public_html/wp-content/plugins/igny8-ai-os/logs/publish-sync-logs/*.log*
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keep Recent Logs Only
|
||||||
|
```bash
|
||||||
|
# Keep only last 1000 lines
|
||||||
|
tail -n 1000 /data/app/igny8/backend/logs/publish-sync-logs/publish-sync.log > /tmp/temp.log
|
||||||
|
mv /tmp/temp.log /data/app/igny8/backend/logs/publish-sync-logs/publish-sync.log
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All timestamps are in server local time
|
||||||
|
- Site stamps format: `[site-id-domain]`
|
||||||
|
- Emoji indicators: 🎯 (start), ✅ (success), ❌ (error), ⚠️ (warning), 🎉 (completion)
|
||||||
|
- Logs are written in real-time during publish workflows
|
||||||
|
- File rotation happens automatically at 10MB per file
|
||||||
@@ -15,6 +15,7 @@ from igny8_core.business.content.models import Content
|
|||||||
from igny8_core.business.integration.models import SiteIntegration, SyncEvent
|
from igny8_core.business.integration.models import SiteIntegration, SyncEvent
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
webhook_logger = logging.getLogger('webhooks')
|
||||||
|
|
||||||
|
|
||||||
class NoThrottle(BaseThrottle):
|
class NoThrottle(BaseThrottle):
|
||||||
@@ -46,14 +47,22 @@ def wordpress_status_webhook(request):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
webhook_logger.info("="*80)
|
||||||
|
webhook_logger.info("📥 WORDPRESS STATUS WEBHOOK RECEIVED")
|
||||||
|
webhook_logger.info(f" Headers: {dict(request.headers)}")
|
||||||
|
webhook_logger.info(f" Body: {request.data}")
|
||||||
|
webhook_logger.info("="*80)
|
||||||
|
|
||||||
# Validate API key
|
# Validate API key
|
||||||
api_key = request.headers.get('X-IGNY8-API-KEY') or request.headers.get('Authorization', '').replace('Bearer ', '')
|
api_key = request.headers.get('X-IGNY8-API-KEY') or request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||||
if not api_key:
|
if not api_key:
|
||||||
|
webhook_logger.error(" ❌ Missing API key in request headers")
|
||||||
return error_response(
|
return error_response(
|
||||||
error='Missing API key',
|
error='Missing API key',
|
||||||
status_code=http_status.HTTP_401_UNAUTHORIZED,
|
status_code=http_status.HTTP_401_UNAUTHORIZED,
|
||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
webhook_logger.info(f" ✅ API key present: ***{api_key[-4:]}")
|
||||||
|
|
||||||
# Get webhook data
|
# Get webhook data
|
||||||
data = request.data
|
data = request.data
|
||||||
@@ -63,10 +72,16 @@ def wordpress_status_webhook(request):
|
|||||||
post_url = data.get('post_url')
|
post_url = data.get('post_url')
|
||||||
site_url = data.get('site_url')
|
site_url = data.get('site_url')
|
||||||
|
|
||||||
logger.info(f"[wordpress_status_webhook] Received webhook: content_id={content_id}, post_id={post_id}, status={post_status}")
|
webhook_logger.info(f"STEP 1: Parsing webhook data...")
|
||||||
|
webhook_logger.info(f" - Content ID: {content_id}")
|
||||||
|
webhook_logger.info(f" - Post ID: {post_id}")
|
||||||
|
webhook_logger.info(f" - Post Status: {post_status}")
|
||||||
|
webhook_logger.info(f" - Post URL: {post_url}")
|
||||||
|
webhook_logger.info(f" - Site URL: {site_url}")
|
||||||
|
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
if not content_id or not post_id or not post_status:
|
if not content_id or not post_id or not post_status:
|
||||||
|
webhook_logger.error(" ❌ Missing required fields")
|
||||||
return error_response(
|
return error_response(
|
||||||
error='Missing required fields: content_id, post_id, post_status',
|
error='Missing required fields: content_id, post_id, post_status',
|
||||||
status_code=http_status.HTTP_400_BAD_REQUEST,
|
status_code=http_status.HTTP_400_BAD_REQUEST,
|
||||||
|
|||||||
@@ -515,3 +515,68 @@ CELERY_TASK_TIME_LIMIT = 30 * 60 # 30 minutes
|
|||||||
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 25 minutes
|
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 25 minutes
|
||||||
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
|
||||||
CELERY_WORKER_MAX_TASKS_PER_CHILD = 1000
|
CELERY_WORKER_MAX_TASKS_PER_CHILD = 1000
|
||||||
|
|
||||||
|
# Publish/Sync Logging Configuration
|
||||||
|
PUBLISH_SYNC_LOG_DIR = os.path.join(BASE_DIR, 'logs', 'publish-sync-logs')
|
||||||
|
os.makedirs(PUBLISH_SYNC_LOG_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '[{asctime}] [{levelname}] [{name}] {message}',
|
||||||
|
'style': '{',
|
||||||
|
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||||
|
},
|
||||||
|
'publish_sync': {
|
||||||
|
'format': '[{asctime}] [{levelname}] {message}',
|
||||||
|
'style': '{',
|
||||||
|
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'verbose',
|
||||||
|
},
|
||||||
|
'publish_sync_file': {
|
||||||
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
|
'filename': os.path.join(PUBLISH_SYNC_LOG_DIR, 'publish-sync.log'),
|
||||||
|
'maxBytes': 10 * 1024 * 1024, # 10 MB
|
||||||
|
'backupCount': 10,
|
||||||
|
'formatter': 'publish_sync',
|
||||||
|
},
|
||||||
|
'wordpress_api_file': {
|
||||||
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
|
'filename': os.path.join(PUBLISH_SYNC_LOG_DIR, 'wordpress-api.log'),
|
||||||
|
'maxBytes': 10 * 1024 * 1024, # 10 MB
|
||||||
|
'backupCount': 10,
|
||||||
|
'formatter': 'publish_sync',
|
||||||
|
},
|
||||||
|
'webhook_file': {
|
||||||
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
|
'filename': os.path.join(PUBLISH_SYNC_LOG_DIR, 'webhooks.log'),
|
||||||
|
'maxBytes': 10 * 1024 * 1024, # 10 MB
|
||||||
|
'backupCount': 10,
|
||||||
|
'formatter': 'publish_sync',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'publish_sync': {
|
||||||
|
'handlers': ['console', 'publish_sync_file'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'wordpress_api': {
|
||||||
|
'handlers': ['console', 'wordpress_api_file'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'webhooks': {
|
||||||
|
'handlers': ['console', 'webhook_file'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
IGNY8 Content Publishing Celery Tasks
|
IGNY8 Content Publishing Celery Tasks - WITH COMPREHENSIVE LOGGING
|
||||||
|
|
||||||
Handles automated publishing of content from IGNY8 to WordPress sites.
|
Handles automated publishing of content from IGNY8 to WordPress sites.
|
||||||
|
All workflow steps are logged to files for debugging and monitoring.
|
||||||
"""
|
"""
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -9,15 +10,21 @@ from django.utils import timezone
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
|
import time
|
||||||
|
import json
|
||||||
from typing import Dict, List, Any, Optional
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
# Standard logger
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
# Dedicated file loggers
|
||||||
|
publish_logger = logging.getLogger('publish_sync')
|
||||||
|
api_logger = logging.getLogger('wordpress_api')
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True, max_retries=3)
|
@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]:
|
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
|
Publish a single content item to WordPress with comprehensive logging
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content_id: IGNY8 content ID
|
content_id: IGNY8 content ID
|
||||||
@@ -27,104 +34,147 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
|
|||||||
Returns:
|
Returns:
|
||||||
Dict with success status and details
|
Dict with success status and details
|
||||||
"""
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from igny8_core.business.content.models import Content
|
from igny8_core.business.content.models import Content, ContentTaxonomyMap
|
||||||
from igny8_core.business.integration.models import SiteIntegration
|
from igny8_core.business.integration.models import SiteIntegration, SyncEvent
|
||||||
|
from igny8_core.modules.writer.models import Images
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 🎯 Celery task started: content_id={content_id}, site_integration_id={site_integration_id}")
|
publish_logger.info("="*80)
|
||||||
|
publish_logger.info(f"🎯 PUBLISH WORKFLOW STARTED")
|
||||||
|
publish_logger.info(f" Content ID: {content_id}")
|
||||||
|
publish_logger.info(f" Site Integration ID: {site_integration_id}")
|
||||||
|
publish_logger.info(f" Task ID: {task_id}")
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
|
||||||
# Get content and site integration
|
# STEP 1: Load content and integration from database
|
||||||
try:
|
try:
|
||||||
|
publish_logger.info(f"STEP 1: Loading content and integration from database...")
|
||||||
content = Content.objects.get(id=content_id)
|
content = Content.objects.get(id=content_id)
|
||||||
logger.info(f"[publish_content_to_wordpress] 📄 Content loaded: title='{content.title}'")
|
|
||||||
site_integration = SiteIntegration.objects.get(id=site_integration_id)
|
site_integration = SiteIntegration.objects.get(id=site_integration_id)
|
||||||
logger.info(f"[publish_content_to_wordpress] 🔌 Integration loaded: platform={site_integration.platform}, site={site_integration.site.name}")
|
|
||||||
|
# Extract site info for logging context
|
||||||
|
site_id = site_integration.site.id if site_integration.site else 'unknown'
|
||||||
|
site_domain = site_integration.base_url.replace('https://', '').replace('http://', '').split('/')[0] if site_integration.base_url else 'unknown'
|
||||||
|
log_prefix = f"[{site_id}-{site_domain}]"
|
||||||
|
|
||||||
|
publish_logger.info(f" ✅ Content loaded:")
|
||||||
|
publish_logger.info(f" {log_prefix} Title: '{content.title}'")
|
||||||
|
publish_logger.info(f" {log_prefix} Status: {content.status}")
|
||||||
|
publish_logger.info(f" {log_prefix} Type: {content.content_type}")
|
||||||
|
publish_logger.info(f" {log_prefix} Site: {content.site.name if content.site else 'None'}")
|
||||||
|
publish_logger.info(f" {log_prefix} Account: {content.account.name if content.account else 'None'}")
|
||||||
|
|
||||||
|
publish_logger.info(f" ✅ Integration loaded:")
|
||||||
|
publish_logger.info(f" {log_prefix} Platform: {site_integration.platform}")
|
||||||
|
publish_logger.info(f" {log_prefix} Site: {site_integration.site.name}")
|
||||||
|
publish_logger.info(f" {log_prefix} Base URL: {site_integration.base_url}")
|
||||||
|
publish_logger.info(f" {log_prefix} API Key: {'***' + site_integration.api_key[-4:] if site_integration.api_key else 'None'}")
|
||||||
except (Content.DoesNotExist, SiteIntegration.DoesNotExist) as e:
|
except (Content.DoesNotExist, SiteIntegration.DoesNotExist) as e:
|
||||||
logger.error(f"[publish_content_to_wordpress] ❌ Content or site integration not found: {e}")
|
publish_logger.error(f" ❌ Database lookup failed: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
# Check if content is already published
|
# STEP 2: Check if already published
|
||||||
|
publish_logger.info(f"{log_prefix} STEP 2: Checking if content is already published...")
|
||||||
if content.external_id:
|
if content.external_id:
|
||||||
logger.info(f"[publish_content_to_wordpress] ⚠️ Content {content_id} already published: external_id={content.external_id}")
|
publish_logger.info(f" {log_prefix} ⚠️ Content already published:")
|
||||||
|
publish_logger.info(f" {log_prefix} External ID: {content.external_id}")
|
||||||
|
publish_logger.info(f" {log_prefix} External URL: {content.external_url}")
|
||||||
|
publish_logger.info(f" {log_prefix} ⏭️ Skipping publish workflow")
|
||||||
return {"success": True, "message": "Already published", "external_id": content.external_id}
|
return {"success": True, "message": "Already published", "external_id": content.external_id}
|
||||||
|
else:
|
||||||
|
publish_logger.info(f" {log_prefix} ✅ Content not yet published, proceeding...")
|
||||||
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 📦 Preparing content payload...")
|
# STEP 3: Generate excerpt from HTML content
|
||||||
logger.info(f"[publish_content_to_wordpress] Content title: '{content.title}'")
|
publish_logger.info(f"{log_prefix} STEP 3: Generating excerpt from content HTML...")
|
||||||
logger.info(f"[publish_content_to_wordpress] Content status: '{content.status}'")
|
|
||||||
logger.info(f"[publish_content_to_wordpress] Content type: '{content.content_type}'")
|
|
||||||
|
|
||||||
# Prepare content data for WordPress
|
|
||||||
# Generate excerpt from content_html (Content model has no 'brief' field)
|
|
||||||
excerpt = ''
|
excerpt = ''
|
||||||
if content.content_html:
|
if content.content_html:
|
||||||
from django.utils.html import strip_tags
|
|
||||||
excerpt = strip_tags(content.content_html)[:150].strip()
|
excerpt = strip_tags(content.content_html)[:150].strip()
|
||||||
if len(content.content_html) > 150:
|
if len(content.content_html) > 150:
|
||||||
excerpt += '...'
|
excerpt += '...'
|
||||||
logger.info(f"[publish_content_to_wordpress] Content HTML length: {len(content.content_html)} chars")
|
publish_logger.info(f" {log_prefix} ✅ Excerpt generated:")
|
||||||
|
publish_logger.info(f" {log_prefix} Content HTML length: {len(content.content_html)} chars")
|
||||||
|
publish_logger.info(f" {log_prefix} Excerpt: {excerpt[:80]}...")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[publish_content_to_wordpress] ⚠️ No content_html found!")
|
publish_logger.warning(f" {log_prefix} ⚠️ No content_html found - excerpt will be empty")
|
||||||
|
|
||||||
# Get taxonomy terms from ContentTaxonomyMap
|
# STEP 4: Get taxonomy terms (categories)
|
||||||
from igny8_core.business.content.models import ContentTaxonomyMap
|
publish_logger.info(f"{log_prefix} STEP 4: Loading taxonomy mappings for categories...")
|
||||||
taxonomy_maps = ContentTaxonomyMap.objects.filter(content=content).select_related('taxonomy')
|
taxonomy_maps = ContentTaxonomyMap.objects.filter(content=content).select_related('taxonomy')
|
||||||
logger.info(f"[publish_content_to_wordpress] Found {taxonomy_maps.count()} taxonomy mappings")
|
publish_logger.info(f" {log_prefix} Found {taxonomy_maps.count()} taxonomy mappings")
|
||||||
|
|
||||||
# Build categories and tags arrays from taxonomy mappings
|
|
||||||
categories = []
|
categories = []
|
||||||
tags = []
|
|
||||||
for mapping in taxonomy_maps:
|
for mapping in taxonomy_maps:
|
||||||
tax = mapping.taxonomy
|
if mapping.taxonomy:
|
||||||
if tax:
|
categories.append(mapping.taxonomy.name)
|
||||||
# Add taxonomy term name to categories (will be mapped in WordPress)
|
publish_logger.info(f" {log_prefix} 📁 Category: '{mapping.taxonomy.name}'")
|
||||||
categories.append(tax.name)
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 📁 Added category: '{tax.name}'")
|
|
||||||
|
|
||||||
# Get images from Images model
|
if not categories:
|
||||||
from igny8_core.modules.writer.models import Images
|
publish_logger.warning(f" {log_prefix} ⚠️ No categories found for content")
|
||||||
featured_image_url = None
|
else:
|
||||||
gallery_images = []
|
publish_logger.info(f" {log_prefix} ✅ TOTAL categories: {len(categories)}")
|
||||||
|
|
||||||
images = Images.objects.filter(content=content).order_by('position')
|
# STEP 5: Get keywords as tags
|
||||||
logger.info(f"[publish_content_to_wordpress] Found {images.count()} images for content")
|
publish_logger.info(f"{log_prefix} STEP 5: Extracting keywords as tags...")
|
||||||
|
tags = []
|
||||||
|
|
||||||
for image in images:
|
|
||||||
if image.image_type == 'featured' and image.image_url:
|
|
||||||
featured_image_url = image.image_url
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 🖼️ Featured image: {image.image_url[:100]}")
|
|
||||||
elif image.image_type == 'in_article' and image.image_url:
|
|
||||||
gallery_images.append({
|
|
||||||
'url': image.image_url,
|
|
||||||
'alt': image.alt_text or '',
|
|
||||||
# Add primary and secondary keywords as tags
|
|
||||||
if content.primary_keyword:
|
if content.primary_keyword:
|
||||||
tags.append(content.primary_keyword)
|
tags.append(content.primary_keyword)
|
||||||
logger.info(f"[publish_content_to_wordpress] 🏷️ Primary keyword (tag): '{content.primary_keyword}'")
|
publish_logger.info(f" {log_prefix} 🏷️ Primary keyword: '{content.primary_keyword}'")
|
||||||
else:
|
else:
|
||||||
logger.info(f"[publish_content_to_wordpress] No primary keyword found")
|
publish_logger.warning(f" {log_prefix} ⚠️ No primary keyword found")
|
||||||
|
|
||||||
if content.secondary_keywords:
|
if content.secondary_keywords:
|
||||||
if isinstance(content.secondary_keywords, list):
|
if isinstance(content.secondary_keywords, list):
|
||||||
tags.extend(content.secondary_keywords)
|
tags.extend(content.secondary_keywords)
|
||||||
logger.info(f"[publish_content_to_wordpress] 🏷️ Added {len(content.secondary_keywords)} secondary keywords as tags")
|
publish_logger.info(f" {log_prefix} 🏷️ Secondary keywords (list): {content.secondary_keywords}")
|
||||||
elif isinstance(content.secondary_keywords, str):
|
elif isinstance(content.secondary_keywords, str):
|
||||||
import json
|
|
||||||
try:
|
try:
|
||||||
keywords = json.loads(content.secondary_keywords)
|
keywords = json.loads(content.secondary_keywords)
|
||||||
if isinstance(keywords, list):
|
if isinstance(keywords, list):
|
||||||
tags.extend(keywords)
|
tags.extend(keywords)
|
||||||
logger.info(f"[publish_content_to_wordpress] 🏷️ Added {len(keywords)} secondary keywords as tags (from JSON)")
|
publish_logger.info(f" {log_prefix} 🏷️ Secondary keywords (JSON): {keywords}")
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
logger.warning(f"[publish_content_to_wordpress] Failed to parse secondary_keywords as JSON")
|
publish_logger.warning(f" {log_prefix} ⚠️ Failed to parse secondary_keywords as JSON: {content.secondary_keywords}")
|
||||||
else:
|
else:
|
||||||
logger.info(f"[publish_content_to_wordpress] No secondary keywords found")
|
publish_logger.warning(f" {log_prefix} ⚠️ No secondary keywords found")
|
||||||
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 📊 TOTAL: {len(categories)} categories, {len(tags)} tags")ords = json.loads(content.secondary_keywords)
|
if not tags:
|
||||||
if isinstance(keywords, list):
|
publish_logger.warning(f" {log_prefix} ⚠️ No tags found for content")
|
||||||
tags.extend(keywords)
|
else:
|
||||||
except (json.JSONDecodeError, TypeError):
|
publish_logger.info(f" {log_prefix} ✅ TOTAL tags: {len(tags)}")
|
||||||
pass
|
|
||||||
|
|
||||||
|
# STEP 6: Get images (featured + gallery)
|
||||||
|
publish_logger.info(f"{log_prefix} STEP 6: Loading images for content...")
|
||||||
|
images = Images.objects.filter(content=content).order_by('position')
|
||||||
|
publish_logger.info(f" {log_prefix} Found {images.count()} images")
|
||||||
|
|
||||||
|
featured_image_url = None
|
||||||
|
gallery_images = []
|
||||||
|
|
||||||
|
for image in images:
|
||||||
|
if image.image_type == 'featured' and image.image_url:
|
||||||
|
featured_image_url = image.image_url
|
||||||
|
publish_logger.info(f" {log_prefix} 🖼️ Featured image: {image.image_url[:80]}...")
|
||||||
|
elif image.image_type == 'in_article' and image.image_url:
|
||||||
|
gallery_images.append({
|
||||||
|
'url': image.image_url,
|
||||||
|
'alt': image.alt_text or '',
|
||||||
|
'caption': image.caption or ''
|
||||||
|
})
|
||||||
|
publish_logger.info(f" {log_prefix} 🖼️ Gallery image {len(gallery_images)}: {image.image_url[:60]}...")
|
||||||
|
|
||||||
|
if not featured_image_url:
|
||||||
|
publish_logger.warning(f" {log_prefix} ⚠️ No featured image found")
|
||||||
|
if not gallery_images:
|
||||||
|
publish_logger.info(f" {log_prefix} ℹ️ No gallery images found")
|
||||||
|
else:
|
||||||
|
publish_logger.info(f" {log_prefix} ✅ Gallery images: {len(gallery_images)}")
|
||||||
|
|
||||||
|
# STEP 7: Prepare content payload
|
||||||
|
publish_logger.info(f"{log_prefix} STEP 7: Building WordPress API payload...")
|
||||||
content_data = {
|
content_data = {
|
||||||
'content_id': content.id,
|
'content_id': content.id,
|
||||||
'task_id': task_id,
|
'task_id': task_id,
|
||||||
@@ -132,173 +182,245 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
|
|||||||
'content_html': content.content_html or '',
|
'content_html': content.content_html or '',
|
||||||
'excerpt': excerpt,
|
'excerpt': excerpt,
|
||||||
'status': 'publish',
|
'status': 'publish',
|
||||||
# Content model has no author field - use site default author in WordPress
|
|
||||||
'author_email': None,
|
'author_email': None,
|
||||||
'author_name': None,
|
'author_name': None,
|
||||||
# Content model has no published_at - WordPress will use current time
|
|
||||||
'published_at': None,
|
'published_at': None,
|
||||||
# Use correct Content model field names
|
|
||||||
'seo_title': content.meta_title or '',
|
'seo_title': content.meta_title or '',
|
||||||
'seo_description': content.meta_description or '',
|
'seo_description': content.meta_description or '',
|
||||||
'primary_keyword': content.primary_keyword or '',
|
'primary_keyword': content.primary_keyword or '',
|
||||||
'secondary_keywords': content.secondary_keywords or [],
|
'secondary_keywords': content.secondary_keywords or [],
|
||||||
# Send featured image URL from Images model
|
|
||||||
'featured_image_url': featured_image_url,
|
'featured_image_url': featured_image_url,
|
||||||
'gallery_images': gallery_images,
|
'gallery_images': gallery_images,
|
||||||
# Send cluster and sector IDs (Content has ForeignKey to cluster, not many-to-many)
|
|
||||||
'cluster_id': content.cluster.id if content.cluster else None,
|
'cluster_id': content.cluster.id if content.cluster else None,
|
||||||
'sector_id': content.sector.id if content.sector else None,
|
'sector_id': content.sector.id if content.sector else None,
|
||||||
# Send categories and tags from taxonomy mappings and keywords
|
|
||||||
'categories': categories,
|
'categories': categories,
|
||||||
'tags': tags,
|
'tags': tags,
|
||||||
# Keep for backward compatibility
|
|
||||||
'sectors': [],
|
'sectors': [],
|
||||||
'clusters': []
|
'clusters': []
|
||||||
logger.info(f"[publish_content_to_wordpress] 🚀 POSTing to WordPress: {wordpress_url}")
|
}
|
||||||
logger.info(f"[publish_content_to_wordpress] 📦 Payload summary:")
|
|
||||||
logger.info(f" - Categories: {categories}")
|
|
||||||
logger.info(f" - Tags: {tags}")
|
|
||||||
logger.info(f" - Featured image: {'Yes' if featured_image_url else 'No'}")
|
|
||||||
logger.info(f" - Gallery images: {len(gallery_images)}")
|
|
||||||
logger.info(f" - SEO title: {'Yes' if content_data.get('seo_title') else 'No'}")
|
|
||||||
logger.info(f" - SEO description: {'Yes' if content_data.get('seo_description') else 'No'}")
|
|
||||||
|
|
||||||
response = requests.post(
|
publish_logger.info(f" {log_prefix} ✅ Payload built:")
|
||||||
wordpress_url,
|
publish_logger.info(f" {log_prefix} Title: {content.title}")
|
||||||
json=content_data,
|
publish_logger.info(f" {log_prefix} Content HTML: {len(content_data['content_html'])} chars")
|
||||||
headers=headers,
|
publish_logger.info(f" {log_prefix} Categories: {len(categories)}")
|
||||||
timeout=30
|
publish_logger.info(f" {log_prefix} Tags: {len(tags)}")
|
||||||
# Update external_id and external_url for unified Content model
|
publish_logger.info(f" {log_prefix} Featured image: {'Yes' if featured_image_url else 'No'}")
|
||||||
old_status = content.status
|
publish_logger.info(f" {log_prefix} Gallery images: {len(gallery_images)}")
|
||||||
content.external_id = wp_data.get('post_id')
|
publish_logger.info(f" {log_prefix} SEO title: {'Yes' if content_data['seo_title'] else 'No'}")
|
||||||
content.external_url = wp_data.get('post_url')
|
publish_logger.info(f" {log_prefix} SEO description: {'Yes' if content_data['seo_description'] else 'No'}")
|
||||||
content.status = 'published'
|
publish_logger.info(f" {log_prefix} Cluster ID: {content_data['cluster_id']}")
|
||||||
content.save(update_fields=[
|
publish_logger.info(f" {log_prefix} Sector ID: {content_data['sector_id']}")
|
||||||
'external_id', 'external_url', 'status', 'updated_at'
|
|
||||||
])
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 💾 Content model updated:")
|
|
||||||
logger.info(f" - Status: '{old_status}' → 'published'")
|
|
||||||
logger.info(f" - External ID: {content.external_id}")
|
|
||||||
logger.info(f" - External URL: {content.external_url}")
|
|
||||||
wordpress_url,
|
|
||||||
json=content_data,
|
|
||||||
headers=headers,
|
|
||||||
timeout=30
|
|
||||||
)
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 📬 WordPress response: status={response.status_code}")
|
|
||||||
|
|
||||||
# Track start time for duration measurement
|
# STEP 8: Send API request to WordPress
|
||||||
import time
|
wordpress_url = f"{site_integration.base_url}/wp-json/igny8/v1/publish"
|
||||||
from igny8_core.business.integration.models import SyncEvent
|
headers = {
|
||||||
start_time = time.time()
|
'X-IGNY8-API-Key': site_integration.api_key,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
publish_logger.info(f"{log_prefix} STEP 8: Sending POST request to WordPress...")
|
||||||
|
api_logger.info(f"{log_prefix} API REQUEST: POST {wordpress_url}")
|
||||||
|
api_logger.info(f" {log_prefix} Headers: X-IGNY8-API-Key: ***{site_integration.api_key[-4:]}")
|
||||||
|
api_logger.info(f" {log_prefix} Payload: {json.dumps(content_data, indent=2)[:500]}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
wordpress_url,
|
||||||
|
json=content_data,
|
||||||
|
headers=headers,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
api_logger.info(f"{log_prefix} API RESPONSE: {response.status_code}")
|
||||||
|
api_logger.info(f" {log_prefix} Response body: {response.text[:500]}")
|
||||||
|
publish_logger.info(f" {log_prefix} ✅ WordPress responded: HTTP {response.status_code}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout as e:
|
||||||
|
error_msg = f"WordPress API timeout after 30s: {str(e)}"
|
||||||
|
api_logger.error(f"{log_prefix} API ERROR: {error_msg}")
|
||||||
|
publish_logger.error(f" {log_prefix} ❌ Request timed out after 30 seconds")
|
||||||
|
|
||||||
|
# Log failure event
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='error',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Timeout publishing '{content.title}' to WordPress",
|
||||||
|
success=False,
|
||||||
|
content_id=content.id,
|
||||||
|
error_message=error_msg,
|
||||||
|
duration_ms=int((time.time() - start_time) * 1000)
|
||||||
|
)
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
error_msg = f"WordPress API request failed: {str(e)}"
|
||||||
|
api_logger.error(f"{log_prefix} API ERROR: {error_msg}")
|
||||||
|
publish_logger.error(f" {log_prefix} ❌ Request exception: {str(e)}")
|
||||||
|
|
||||||
|
# Log failure event
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='error',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Request error publishing '{content.title}' to WordPress",
|
||||||
|
success=False,
|
||||||
|
content_id=content.id,
|
||||||
|
error_message=error_msg,
|
||||||
|
duration_ms=int((time.time() - start_time) * 1000)
|
||||||
|
)
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
|
# STEP 9: Process response
|
||||||
|
publish_logger.info(f"{log_prefix} STEP 9: Processing WordPress response...")
|
||||||
|
|
||||||
if response.status_code == 201:
|
if response.status_code == 201:
|
||||||
# Success
|
# SUCCESS - Post created
|
||||||
wp_data = response.json().get('data', {})
|
publish_logger.info(f" {log_prefix} ✅ WordPress post created successfully (HTTP 201)")
|
||||||
wp_status = wp_data.get('post_status', 'publish')
|
|
||||||
logger.info(f"[publish_content_to_wordpress] ✅ WordPress post created successfully: post_id={wp_data.get('post_id')}, status={wp_status}")
|
|
||||||
|
|
||||||
# Update external_id, external_url, and wordpress_status in Content model
|
|
||||||
content.external_id = str(wp_data.get('post_id'))
|
|
||||||
content.external_url = wp_data.get('post_url')
|
|
||||||
content.status = 'published'
|
|
||||||
|
|
||||||
# Add wordpress_status field to Content model metadata
|
|
||||||
if not hasattr(content, 'metadata') or content.metadata is None:
|
|
||||||
content.metadata = {}
|
|
||||||
content.metadata['wordpress_status'] = wp_status
|
|
||||||
|
|
||||||
content.save(update_fields=[
|
|
||||||
'external_id', 'external_url', 'status', 'metadata', 'updated_at'
|
|
||||||
])
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 💾 Content model updated:")
|
|
||||||
logger.info(f" - External ID: {content.external_id}")
|
|
||||||
logger.info(f" - External URL: {content.external_url}")
|
|
||||||
logger.info(f" - Status: published")
|
|
||||||
logger.info(f" - WordPress Status: {wp_status}")
|
|
||||||
|
|
||||||
# Log sync event
|
|
||||||
duration_ms = int((time.time() - start_time) * 1000)
|
|
||||||
SyncEvent.objects.create(
|
|
||||||
integration=site_integration,
|
|
||||||
site=content.site,
|
|
||||||
account=content.account,
|
|
||||||
event_type='publish',
|
|
||||||
action='content_publish',
|
|
||||||
description=f"Published content '{content.title}' to WordPress",
|
|
||||||
success=True,
|
|
||||||
content_id=content.id,
|
|
||||||
external_id=str(content.external_id),
|
|
||||||
details={
|
|
||||||
'post_url': content.external_url,
|
|
||||||
'wordpress_status': wp_status,
|
|
||||||
'categories': categories,
|
|
||||||
'tags': tags,
|
|
||||||
'has_featured_image': bool(featured_image_url),
|
|
||||||
'gallery_images_count': len(gallery_images),
|
|
||||||
},
|
|
||||||
duration_ms=duration_ms
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"[publish_content_to_wordpress] 🎉 Successfully published content {content_id} to WordPress post {content.external_id}")
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"external_id": content.external_id,
|
|
||||||
"external_url": content.external_url,
|
|
||||||
"wordpress_status": wp_status
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_data = response.json()
|
||||||
|
wp_data = response_data.get('data', {})
|
||||||
|
wp_status = wp_data.get('post_status', 'publish')
|
||||||
|
|
||||||
|
publish_logger.info(f" {log_prefix} Post ID: {wp_data.get('post_id')}")
|
||||||
|
publish_logger.info(f" {log_prefix} Post URL: {wp_data.get('post_url')}")
|
||||||
|
publish_logger.info(f" {log_prefix} Post status: {wp_status}")
|
||||||
|
|
||||||
|
# Update Content model
|
||||||
|
publish_logger.info(f"{log_prefix} STEP 10: Updating IGNY8 Content model...")
|
||||||
|
content.external_id = str(wp_data.get('post_id'))
|
||||||
|
content.external_url = wp_data.get('post_url')
|
||||||
|
content.status = 'published'
|
||||||
|
|
||||||
|
if not hasattr(content, 'metadata') or content.metadata is None:
|
||||||
|
content.metadata = {}
|
||||||
|
content.metadata['wordpress_status'] = wp_status
|
||||||
|
|
||||||
|
content.save(update_fields=['external_id', 'external_url', 'status', 'metadata', 'updated_at'])
|
||||||
|
|
||||||
|
publish_logger.info(f" {log_prefix} ✅ Content model updated:")
|
||||||
|
publish_logger.info(f" {log_prefix} External ID: {content.external_id}")
|
||||||
|
publish_logger.info(f" {log_prefix} External URL: {content.external_url}")
|
||||||
|
publish_logger.info(f" {log_prefix} Status: published")
|
||||||
|
publish_logger.info(f" {log_prefix} WordPress status: {wp_status}")
|
||||||
|
|
||||||
|
# Log success event
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='publish',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Published content '{content.title}' to WordPress",
|
||||||
|
success=True,
|
||||||
|
content_id=content.id,
|
||||||
|
external_id=str(content.external_id),
|
||||||
|
details={
|
||||||
|
'post_url': content.external_url,
|
||||||
|
'wordpress_status': wp_status,
|
||||||
|
'categories': categories,
|
||||||
|
'tags': tags,
|
||||||
|
'has_featured_image': bool(featured_image_url),
|
||||||
|
'gallery_images_count': len(gallery_images),
|
||||||
|
},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
publish_logger.info(f"="*80)
|
||||||
|
publish_logger.info(f"{log_prefix} 🎉 PUBLISH WORKFLOW COMPLETED SUCCESSFULLY")
|
||||||
|
publish_logger.info(f" {log_prefix} Duration: {duration_ms}ms")
|
||||||
|
publish_logger.info(f" {log_prefix} WordPress Post ID: {content.external_id}")
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"external_id": content.external_id,
|
||||||
|
"external_url": content.external_url,
|
||||||
|
"wordpress_status": wp_status
|
||||||
|
}
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, KeyError) as e:
|
||||||
|
error_msg = f"Failed to parse WordPress response: {str(e)}"
|
||||||
|
publish_logger.error(f" ❌ Response parsing error: {error_msg}")
|
||||||
|
publish_logger.error(f" Raw response: {response.text[:200]}")
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
elif response.status_code == 409:
|
elif response.status_code == 409:
|
||||||
# Content already exists
|
# CONFLICT - Post already exists
|
||||||
wp_data = response.json().get('data', {})
|
publish_logger.info(f" {log_prefix} ⚠️ Content already exists in WordPress (HTTP 409)")
|
||||||
wp_status = wp_data.get('post_status', 'publish')
|
|
||||||
content.external_id = str(wp_data.get('post_id'))
|
|
||||||
content.external_url = wp_data.get('post_url')
|
|
||||||
content.status = 'published'
|
|
||||||
|
|
||||||
# Update wordpress_status in metadata
|
|
||||||
if not hasattr(content, 'metadata') or content.metadata is None:
|
|
||||||
content.metadata = {}
|
|
||||||
content.metadata['wordpress_status'] = wp_status
|
|
||||||
|
|
||||||
content.save(update_fields=[
|
|
||||||
'external_id', 'external_url', 'status', 'metadata', 'updated_at'
|
|
||||||
])
|
|
||||||
|
|
||||||
# Log sync event
|
|
||||||
duration_ms = int((time.time() - start_time) * 1000)
|
|
||||||
SyncEvent.objects.create(
|
|
||||||
integration=site_integration,
|
|
||||||
site=content.site,
|
|
||||||
account=content.account,
|
|
||||||
event_type='sync',
|
|
||||||
action='content_publish',
|
|
||||||
description=f"Content '{content.title}' already exists in WordPress",
|
|
||||||
success=True,
|
|
||||||
content_id=content.id,
|
|
||||||
external_id=str(content.external_id),
|
|
||||||
details={
|
|
||||||
'post_url': content.external_url,
|
|
||||||
'wordpress_status': wp_status,
|
|
||||||
'already_exists': True,
|
|
||||||
},
|
|
||||||
duration_ms=duration_ms
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Content {content_id} already exists on WordPress")
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "Content already exists",
|
|
||||||
"external_id": content.external_id,
|
|
||||||
"wordpress_status": wp_status
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_data = response.json()
|
||||||
|
wp_data = response_data.get('data', {})
|
||||||
|
wp_status = wp_data.get('post_status', 'publish')
|
||||||
|
|
||||||
|
publish_logger.info(f" {log_prefix} Existing Post ID: {wp_data.get('post_id')}")
|
||||||
|
publish_logger.info(f" {log_prefix} Post URL: {wp_data.get('post_url')}")
|
||||||
|
|
||||||
|
# Update Content model
|
||||||
|
content.external_id = str(wp_data.get('post_id'))
|
||||||
|
content.external_url = wp_data.get('post_url')
|
||||||
|
content.status = 'published'
|
||||||
|
|
||||||
|
if not hasattr(content, 'metadata') or content.metadata is None:
|
||||||
|
content.metadata = {}
|
||||||
|
content.metadata['wordpress_status'] = wp_status
|
||||||
|
|
||||||
|
content.save(update_fields=['external_id', 'external_url', 'status', 'metadata', 'updated_at'])
|
||||||
|
|
||||||
|
# Log sync event
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='sync',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Content '{content.title}' already exists in WordPress",
|
||||||
|
success=True,
|
||||||
|
content_id=content.id,
|
||||||
|
external_id=str(content.external_id),
|
||||||
|
details={
|
||||||
|
'post_url': content.external_url,
|
||||||
|
'wordpress_status': wp_status,
|
||||||
|
'already_exists': True,
|
||||||
|
},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
publish_logger.info(f"="*80)
|
||||||
|
publish_logger.info(f"{log_prefix} ✅ PUBLISH WORKFLOW COMPLETED (already exists)")
|
||||||
|
publish_logger.info(f" {log_prefix} Duration: {duration_ms}ms")
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Content already exists",
|
||||||
|
"external_id": content.external_id,
|
||||||
|
"wordpress_status": wp_status
|
||||||
|
}
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, KeyError) as e:
|
||||||
|
error_msg = f"Failed to parse 409 response: {str(e)}"
|
||||||
|
publish_logger.error(f" ❌ Response parsing error: {error_msg}")
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Error
|
# ERROR - Unexpected status code
|
||||||
error_msg = f"WordPress API error: {response.status_code} - {response.text}"
|
error_msg = f"WordPress API error: HTTP {response.status_code}"
|
||||||
logger.error(f"[publish_content_to_wordpress] ❌ {error_msg}")
|
publish_logger.error(f" {log_prefix} ❌ Unexpected status code: {response.status_code}")
|
||||||
|
publish_logger.error(f" {log_prefix} Response: {response.text[:500]}")
|
||||||
|
|
||||||
# Log sync event for failure
|
api_logger.error(f"{log_prefix} API ERROR: {error_msg}")
|
||||||
|
api_logger.error(f" {log_prefix} Full response: {response.text}")
|
||||||
|
|
||||||
|
# Log failure event
|
||||||
duration_ms = int((time.time() - start_time) * 1000)
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
SyncEvent.objects.create(
|
SyncEvent.objects.create(
|
||||||
integration=site_integration,
|
integration=site_integration,
|
||||||
@@ -312,47 +434,57 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
|
|||||||
error_message=error_msg,
|
error_message=error_msg,
|
||||||
details={
|
details={
|
||||||
'status_code': response.status_code,
|
'status_code': response.status_code,
|
||||||
'response_text': response.text[:500], # Limit length
|
'response_text': response.text[:500],
|
||||||
},
|
},
|
||||||
duration_ms=duration_ms
|
duration_ms=duration_ms
|
||||||
)
|
)
|
||||||
|
|
||||||
# Retry logic
|
# Retry logic
|
||||||
if self.request.retries < self.max_retries:
|
if self.request.retries < self.max_retries:
|
||||||
# Exponential backoff: 1min, 5min, 15min
|
|
||||||
countdown = 60 * (5 ** self.request.retries)
|
countdown = 60 * (5 ** self.request.retries)
|
||||||
logger.warning(f"[publish_content_to_wordpress] 🔄 Retrying (attempt {self.request.retries + 1}/{self.max_retries}) in {countdown}s")
|
publish_logger.warning(f" {log_prefix} 🔄 Scheduling retry (attempt {self.request.retries + 1}/{self.max_retries}) in {countdown}s")
|
||||||
raise self.retry(countdown=countdown, exc=Exception(error_msg))
|
raise self.retry(countdown=countdown, exc=Exception(error_msg))
|
||||||
else:
|
else:
|
||||||
# Max retries reached - mark as failed
|
publish_logger.error(f" {log_prefix} ❌ Max retries reached, giving up")
|
||||||
logger.error(f"[publish_content_to_wordpress] ❌ Max retries reached, giving up")
|
publish_logger.info(f"="*80)
|
||||||
|
publish_logger.error(f"{log_prefix} ❌ PUBLISH WORKFLOW FAILED")
|
||||||
|
publish_logger.info(f" {log_prefix} Duration: {duration_ms}ms")
|
||||||
|
publish_logger.info("="*80)
|
||||||
return {"success": False, "error": error_msg}
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[publish_content_to_wordpress] ❌ Exception during publish: {str(e)}", exc_info=True)
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
# Try to use log_prefix if available
|
||||||
|
try:
|
||||||
|
prefix = log_prefix
|
||||||
|
except:
|
||||||
|
prefix = "[unknown-site]"
|
||||||
|
|
||||||
# Log sync event for exception
|
publish_logger.error(f"="*80)
|
||||||
|
publish_logger.error(f"{prefix} 💥 PUBLISH WORKFLOW EXCEPTION")
|
||||||
|
publish_logger.error(f" {prefix} Exception type: {type(e).__name__}")
|
||||||
|
publish_logger.error(f" {prefix} Exception message: {str(e)}")
|
||||||
|
publish_logger.error(f" {prefix} Duration: {duration_ms}ms")
|
||||||
|
publish_logger.error("="*80, exc_info=True)
|
||||||
|
|
||||||
|
# Try to log sync event
|
||||||
try:
|
try:
|
||||||
from igny8_core.business.integration.models import SyncEvent
|
from igny8_core.business.integration.models import SyncEvent
|
||||||
import time
|
|
||||||
|
|
||||||
SyncEvent.objects.create(
|
SyncEvent.objects.create(
|
||||||
integration=site_integration,
|
integration=site_integration,
|
||||||
site=content.site,
|
site=content.site,
|
||||||
account=content.account,
|
account=content.account,
|
||||||
event_type='error',
|
event_type='error',
|
||||||
action='content_publish',
|
action='content_publish',
|
||||||
description=f"Exception while publishing content '{content.title}' to WordPress",
|
description=f"Exception publishing '{content.title}' to WordPress",
|
||||||
success=False,
|
success=False,
|
||||||
content_id=content.id,
|
content_id=content.id,
|
||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
details={
|
details={'exception_type': type(e).__name__},
|
||||||
'exception_type': type(e).__name__,
|
duration_ms=duration_ms
|
||||||
'traceback': str(e),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
except Exception as log_error:
|
except Exception as log_error:
|
||||||
logger.error(f"Failed to log sync event: {str(log_error)}")
|
publish_logger.error(f"Failed to log sync event: {str(log_error)}")
|
||||||
|
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
@@ -361,133 +493,39 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
|
|||||||
def process_pending_wordpress_publications() -> Dict[str, Any]:
|
def process_pending_wordpress_publications() -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Process all content items pending WordPress publication
|
Process all content items pending WordPress publication
|
||||||
Runs every 5 minutes
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from igny8_core.business.content.models import Content
|
from igny8_core.business.content.models import Content
|
||||||
from igny8_core.business.integration.models import SiteIntegration
|
from igny8_core.business.integration.models import SiteIntegration
|
||||||
|
|
||||||
# Find content marked for WordPress publishing (status = published, external_id = empty)
|
|
||||||
pending_content = Content.objects.filter(
|
pending_content = Content.objects.filter(
|
||||||
status='published',
|
status='published',
|
||||||
external_id__isnull=True
|
external_id__isnull=True
|
||||||
).select_related('site', 'sector', 'cluster')
|
).select_related('site', 'sector', 'cluster')
|
||||||
|
|
||||||
if not pending_content.exists():
|
if not pending_content.exists():
|
||||||
logger.info("No content pending WordPress publication")
|
publish_logger.info("No content pending WordPress publication")
|
||||||
return {"success": True, "processed": 0}
|
return {"success": True, "processed": 0}
|
||||||
|
|
||||||
# Get active WordPress integrations
|
|
||||||
active_integrations = SiteIntegration.objects.filter(
|
active_integrations = SiteIntegration.objects.filter(
|
||||||
platform='wordpress',
|
platform='wordpress',
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if not active_integrations.exists():
|
if not active_integrations.exists():
|
||||||
logger.warning("No active WordPress integrations found")
|
publish_logger.warning("No active WordPress integrations found")
|
||||||
return {"success": False, "error": "No active WordPress integrations"}
|
return {"success": False, "error": "No active WordPress integrations"}
|
||||||
|
|
||||||
processed = 0
|
processed = 0
|
||||||
|
for content in pending_content[:50]:
|
||||||
for content in pending_content[:50]: # Process max 50 at a time
|
|
||||||
for integration in active_integrations.filter(site=content.site):
|
for integration in active_integrations.filter(site=content.site):
|
||||||
# Queue individual publish task
|
publish_content_to_wordpress.delay(content.id, integration.id)
|
||||||
publish_content_to_wordpress.delay(
|
|
||||||
content.id,
|
|
||||||
integration.id
|
|
||||||
)
|
|
||||||
processed += 1
|
processed += 1
|
||||||
break # Only queue with first matching integration
|
break
|
||||||
|
|
||||||
logger.info(f"Queued {processed} content items for WordPress publication")
|
publish_logger.info(f"Queued {processed} content items for WordPress publication")
|
||||||
return {"success": True, "processed": processed}
|
return {"success": True, "processed": processed}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing pending WordPress publications: {str(e)}", exc_info=True)
|
publish_logger.error(f"Error processing pending publications: {str(e)}", exc_info=True)
|
||||||
return {"success": False, "error": 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.business.content.models import Content
|
|
||||||
from igny8_core.business.integration.models import SiteIntegration
|
|
||||||
|
|
||||||
site_integration = SiteIntegration.objects.get(id=site_integration_id)
|
|
||||||
content_items = Content.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
|
|
||||||
if content.external_id:
|
|
||||||
results["skipped"] += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Queue individual publish task
|
|
||||||
publish_content_to_wordpress.delay(
|
|
||||||
content.id,
|
|
||||||
site_integration.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
|
|
||||||
|
|
||||||
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)}", exc_info=True)
|
|
||||||
return {"success": False, "error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def wordpress_status_reconciliation() -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Daily task to verify published content still exists on WordPress
|
|
||||||
Checks for discrepancies and fixes them
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from igny8_core.business.content.models import Content
|
|
||||||
|
|
||||||
# Get content marked as published
|
|
||||||
published_content = Content.objects.filter(
|
|
||||||
external_id__isnull=False
|
|
||||||
)[:100] # Limit to prevent timeouts
|
|
||||||
|
|
||||||
logger.info(f"Status reconciliation: Checking {len(published_content)} published items")
|
|
||||||
return {"success": True, "checked": len(published_content)}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error in status reconciliation: {str(e)}", exc_info=True)
|
|
||||||
return {"success": False, "error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def retry_failed_wordpress_publications() -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Retry failed WordPress publications (runs daily)
|
|
||||||
For future use when we implement failure tracking
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
logger.info("Retry task: No failure tracking currently implemented")
|
|
||||||
return {"success": True, "retried": 0}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error retrying failed publications: {str(e)}", exc_info=True)
|
|
||||||
return {"success": False, "error": str(e)}
|
|
||||||
508
backend/igny8_core/tasks/wordpress_publishing_backup.py
Normal file
508
backend/igny8_core/tasks/wordpress_publishing_backup.py
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
"""
|
||||||
|
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__)
|
||||||
|
publish_logger = logging.getLogger('publish_sync')
|
||||||
|
api_logger = logging.getLogger('wordpress_api')
|
||||||
|
|
||||||
|
|
||||||
|
@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.business.content.models import Content
|
||||||
|
from igny8_core.business.integration.models import SiteIntegration
|
||||||
|
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
publish_logger.info(f"🎯 PUBLISH WORKFLOW STARTED")
|
||||||
|
publish_logger.info(f" Content ID: {content_id}")
|
||||||
|
publish_logger.info(f" Site Integration ID: {site_integration_id}")
|
||||||
|
publish_logger.info(f" Task ID: {task_id}")
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
|
||||||
|
# Get content and site integration
|
||||||
|
try:
|
||||||
|
publish_logger.info(f"STEP 1: Loading content and integration from database...")
|
||||||
|
content = Content.objects.get(id=content_id)
|
||||||
|
publish_logger.info(f" ✅ Content loaded: '{content.title}'")
|
||||||
|
publish_logger.info(f" - Status: {content.status}")
|
||||||
|
publish_logger.info(f" - Type: {content.content_type}")
|
||||||
|
publish_logger.info(f" - Site: {content.site.name if content.site else 'None'}")
|
||||||
|
|
||||||
|
site_integration = SiteIntegration.objects.get(id=site_integration_id)
|
||||||
|
publish_logger.info(f" ✅ Integration loaded:")
|
||||||
|
publish_logger.info(f" - Platform: {site_integration.platform}")
|
||||||
|
publish_logger.info(f" - Site: {site_integration.site.name}")
|
||||||
|
publish_logger.info(f" - Base URL: {site_integration.base_url}")
|
||||||
|
except (Content.DoesNotExist, SiteIntegration.DoesNotExist) as e:
|
||||||
|
publish_logger.error(f" ❌ Database lookup failed: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
# Check if content is already published
|
||||||
|
if content.external_id:
|
||||||
|
logger.info(f"[publish_content_to_wordpress] ⚠️ Content {content_id} already published: external_id={content.external_id}")
|
||||||
|
return {"success": True, "message": "Already published", "external_id": content.external_id}
|
||||||
|
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 📦 Preparing content payload...")
|
||||||
|
logger.info(f"[publish_content_to_wordpress] Content title: '{content.title}'")
|
||||||
|
logger.info(f"[publish_content_to_wordpress] Content status: '{content.status}'")
|
||||||
|
logger.info(f"[publish_content_to_wordpress] Content type: '{content.content_type}'")
|
||||||
|
|
||||||
|
# Prepare content data for WordPress
|
||||||
|
# Generate excerpt from content_html (Content model has no 'brief' field)
|
||||||
|
excerpt = ''
|
||||||
|
if content.content_html:
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
excerpt = strip_tags(content.content_html)[:150].strip()
|
||||||
|
if len(content.content_html) > 150:
|
||||||
|
excerpt += '...'
|
||||||
|
logger.info(f"[publish_content_to_wordpress] Content HTML length: {len(content.content_html)} chars")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[publish_content_to_wordpress] ⚠️ No content_html found!")
|
||||||
|
|
||||||
|
# Get taxonomy terms from ContentTaxonomyMap
|
||||||
|
from igny8_core.business.content.models import ContentTaxonomyMap
|
||||||
|
taxonomy_maps = ContentTaxonomyMap.objects.filter(content=content).select_related('taxonomy')
|
||||||
|
logger.info(f"[publish_content_to_wordpress] Found {taxonomy_maps.count()} taxonomy mappings")
|
||||||
|
|
||||||
|
# Build categories and tags arrays from taxonomy mappings
|
||||||
|
categories = []
|
||||||
|
tags = []
|
||||||
|
for mapping in taxonomy_maps:
|
||||||
|
tax = mapping.taxonomy
|
||||||
|
if tax:
|
||||||
|
# Add taxonomy term name to categories (will be mapped in WordPress)
|
||||||
|
categories.append(tax.name)
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 📁 Added category: '{tax.name}'")
|
||||||
|
|
||||||
|
# Get images from Images model
|
||||||
|
from igny8_core.modules.writer.models import Images
|
||||||
|
featured_image_url = None
|
||||||
|
gallery_images = []
|
||||||
|
|
||||||
|
images = Images.objects.filter(content=content).order_by('position')
|
||||||
|
logger.info(f"[publish_content_to_wordpress] Found {images.count()} images for content")
|
||||||
|
|
||||||
|
for image in images:
|
||||||
|
if image.image_type == 'featured' and image.image_url:
|
||||||
|
featured_image_url = image.image_url
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 🖼️ Featured image: {image.image_url[:100]}")
|
||||||
|
elif image.image_type == 'in_article' and image.image_url:
|
||||||
|
gallery_images.append({
|
||||||
|
'url': image.image_url,
|
||||||
|
'alt': image.alt_text or '',
|
||||||
|
# Add primary and secondary keywords as tags
|
||||||
|
if content.primary_keyword:
|
||||||
|
tags.append(content.primary_keyword)
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 🏷️ Primary keyword (tag): '{content.primary_keyword}'")
|
||||||
|
else:
|
||||||
|
logger.info(f"[publish_content_to_wordpress] No primary keyword found")
|
||||||
|
|
||||||
|
if content.secondary_keywords:
|
||||||
|
if isinstance(content.secondary_keywords, list):
|
||||||
|
tags.extend(content.secondary_keywords)
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 🏷️ Added {len(content.secondary_keywords)} secondary keywords as tags")
|
||||||
|
elif isinstance(content.secondary_keywords, str):
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
keywords = json.loads(content.secondary_keywords)
|
||||||
|
if isinstance(keywords, list):
|
||||||
|
tags.extend(keywords)
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 🏷️ Added {len(keywords)} secondary keywords as tags (from JSON)")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
logger.warning(f"[publish_content_to_wordpress] Failed to parse secondary_keywords as JSON")
|
||||||
|
else:
|
||||||
|
logger.info(f"[publish_content_to_wordpress] No secondary keywords found")
|
||||||
|
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 📊 TOTAL: {len(categories)} categories, {len(tags)} tags")ords = json.loads(content.secondary_keywords)
|
||||||
|
if isinstance(keywords, list):
|
||||||
|
tags.extend(keywords)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
content_data = {
|
||||||
|
'content_id': content.id,
|
||||||
|
'task_id': task_id,
|
||||||
|
'title': content.title,
|
||||||
|
'content_html': content.content_html or '',
|
||||||
|
'excerpt': excerpt,
|
||||||
|
'status': 'publish',
|
||||||
|
# Content model has no author field - use site default author in WordPress
|
||||||
|
'author_email': None,
|
||||||
|
'author_name': None,
|
||||||
|
# Content model has no published_at - WordPress will use current time
|
||||||
|
'published_at': None,
|
||||||
|
# Use correct Content model field names
|
||||||
|
'seo_title': content.meta_title or '',
|
||||||
|
'seo_description': content.meta_description or '',
|
||||||
|
'primary_keyword': content.primary_keyword or '',
|
||||||
|
'secondary_keywords': content.secondary_keywords or [],
|
||||||
|
# Send featured image URL from Images model
|
||||||
|
'featured_image_url': featured_image_url,
|
||||||
|
'gallery_images': gallery_images,
|
||||||
|
# Send cluster and sector IDs (Content has ForeignKey to cluster, not many-to-many)
|
||||||
|
'cluster_id': content.cluster.id if content.cluster else None,
|
||||||
|
'sector_id': content.sector.id if content.sector else None,
|
||||||
|
# Send categories and tags from taxonomy mappings and keywords
|
||||||
|
'categories': categories,
|
||||||
|
'tags': tags,
|
||||||
|
# Keep for backward compatibility
|
||||||
|
'sectors': [],
|
||||||
|
'clusters': []
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 🚀 POSTing to WordPress: {wordpress_url}")
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 📦 Payload summary:")
|
||||||
|
logger.info(f" - Categories: {categories}")
|
||||||
|
logger.info(f" - Tags: {tags}")
|
||||||
|
logger.info(f" - Featured image: {'Yes' if featured_image_url else 'No'}")
|
||||||
|
logger.info(f" - Gallery images: {len(gallery_images)}")
|
||||||
|
logger.info(f" - SEO title: {'Yes' if content_data.get('seo_title') else 'No'}")
|
||||||
|
logger.info(f" - SEO description: {'Yes' if content_data.get('seo_description') else 'No'}")
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
wordpress_url,
|
||||||
|
json=content_data,
|
||||||
|
headers=headers,
|
||||||
|
timeout=30
|
||||||
|
# Update external_id and external_url for unified Content model
|
||||||
|
old_status = content.status
|
||||||
|
content.external_id = wp_data.get('post_id')
|
||||||
|
content.external_url = wp_data.get('post_url')
|
||||||
|
content.status = 'published'
|
||||||
|
content.save(update_fields=[
|
||||||
|
'external_id', 'external_url', 'status', 'updated_at'
|
||||||
|
])
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 💾 Content model updated:")
|
||||||
|
logger.info(f" - Status: '{old_status}' → 'published'")
|
||||||
|
logger.info(f" - External ID: {content.external_id}")
|
||||||
|
logger.info(f" - External URL: {content.external_url}")
|
||||||
|
wordpress_url,
|
||||||
|
json=content_data,
|
||||||
|
headers=headers,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 📬 WordPress response: status={response.status_code}")
|
||||||
|
|
||||||
|
# Track start time for duration measurement
|
||||||
|
import time
|
||||||
|
from igny8_core.business.integration.models import SyncEvent
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
if response.status_code == 201:
|
||||||
|
# Success
|
||||||
|
wp_data = response.json().get('data', {})
|
||||||
|
wp_status = wp_data.get('post_status', 'publish')
|
||||||
|
logger.info(f"[publish_content_to_wordpress] ✅ WordPress post created successfully: post_id={wp_data.get('post_id')}, status={wp_status}")
|
||||||
|
|
||||||
|
# Update external_id, external_url, and wordpress_status in Content model
|
||||||
|
content.external_id = str(wp_data.get('post_id'))
|
||||||
|
content.external_url = wp_data.get('post_url')
|
||||||
|
content.status = 'published'
|
||||||
|
|
||||||
|
# Add wordpress_status field to Content model metadata
|
||||||
|
if not hasattr(content, 'metadata') or content.metadata is None:
|
||||||
|
content.metadata = {}
|
||||||
|
content.metadata['wordpress_status'] = wp_status
|
||||||
|
|
||||||
|
content.save(update_fields=[
|
||||||
|
'external_id', 'external_url', 'status', 'metadata', 'updated_at'
|
||||||
|
])
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 💾 Content model updated:")
|
||||||
|
logger.info(f" - External ID: {content.external_id}")
|
||||||
|
logger.info(f" - External URL: {content.external_url}")
|
||||||
|
logger.info(f" - Status: published")
|
||||||
|
logger.info(f" - WordPress Status: {wp_status}")
|
||||||
|
|
||||||
|
# Log sync event
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='publish',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Published content '{content.title}' to WordPress",
|
||||||
|
success=True,
|
||||||
|
content_id=content.id,
|
||||||
|
external_id=str(content.external_id),
|
||||||
|
details={
|
||||||
|
'post_url': content.external_url,
|
||||||
|
'wordpress_status': wp_status,
|
||||||
|
'categories': categories,
|
||||||
|
'tags': tags,
|
||||||
|
'has_featured_image': bool(featured_image_url),
|
||||||
|
'gallery_images_count': len(gallery_images),
|
||||||
|
},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"[publish_content_to_wordpress] 🎉 Successfully published content {content_id} to WordPress post {content.external_id}")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"external_id": content.external_id,
|
||||||
|
"external_url": content.external_url,
|
||||||
|
"wordpress_status": wp_status
|
||||||
|
}
|
||||||
|
|
||||||
|
elif response.status_code == 409:
|
||||||
|
# Content already exists
|
||||||
|
wp_data = response.json().get('data', {})
|
||||||
|
wp_status = wp_data.get('post_status', 'publish')
|
||||||
|
content.external_id = str(wp_data.get('post_id'))
|
||||||
|
content.external_url = wp_data.get('post_url')
|
||||||
|
content.status = 'published'
|
||||||
|
|
||||||
|
# Update wordpress_status in metadata
|
||||||
|
if not hasattr(content, 'metadata') or content.metadata is None:
|
||||||
|
content.metadata = {}
|
||||||
|
content.metadata['wordpress_status'] = wp_status
|
||||||
|
|
||||||
|
content.save(update_fields=[
|
||||||
|
'external_id', 'external_url', 'status', 'metadata', 'updated_at'
|
||||||
|
])
|
||||||
|
|
||||||
|
# Log sync event
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='sync',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Content '{content.title}' already exists in WordPress",
|
||||||
|
success=True,
|
||||||
|
content_id=content.id,
|
||||||
|
external_id=str(content.external_id),
|
||||||
|
details={
|
||||||
|
'post_url': content.external_url,
|
||||||
|
'wordpress_status': wp_status,
|
||||||
|
'already_exists': True,
|
||||||
|
},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Content {content_id} already exists on WordPress")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Content already exists",
|
||||||
|
"external_id": content.external_id,
|
||||||
|
"wordpress_status": wp_status
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Error
|
||||||
|
error_msg = f"WordPress API error: {response.status_code} - {response.text}"
|
||||||
|
logger.error(f"[publish_content_to_wordpress] ❌ {error_msg}")
|
||||||
|
|
||||||
|
# Log sync event for failure
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='error',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Failed to publish content '{content.title}' to WordPress",
|
||||||
|
success=False,
|
||||||
|
content_id=content.id,
|
||||||
|
error_message=error_msg,
|
||||||
|
details={
|
||||||
|
'status_code': response.status_code,
|
||||||
|
'response_text': response.text[:500], # Limit length
|
||||||
|
},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retry logic
|
||||||
|
if self.request.retries < self.max_retries:
|
||||||
|
# Exponential backoff: 1min, 5min, 15min
|
||||||
|
countdown = 60 * (5 ** self.request.retries)
|
||||||
|
logger.warning(f"[publish_content_to_wordpress] 🔄 Retrying (attempt {self.request.retries + 1}/{self.max_retries}) in {countdown}s")
|
||||||
|
raise self.retry(countdown=countdown, exc=Exception(error_msg))
|
||||||
|
else:
|
||||||
|
# Max retries reached - mark as failed
|
||||||
|
logger.error(f"[publish_content_to_wordpress] ❌ Max retries reached, giving up")
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[publish_content_to_wordpress] ❌ Exception during publish: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
# Log sync event for exception
|
||||||
|
try:
|
||||||
|
from igny8_core.business.integration.models import SyncEvent
|
||||||
|
import time
|
||||||
|
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='error',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Exception while publishing content '{content.title}' to WordPress",
|
||||||
|
success=False,
|
||||||
|
content_id=content.id,
|
||||||
|
error_message=str(e),
|
||||||
|
details={
|
||||||
|
'exception_type': type(e).__name__,
|
||||||
|
'traceback': str(e),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as log_error:
|
||||||
|
logger.error(f"Failed to log sync event: {str(log_error)}")
|
||||||
|
|
||||||
|
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.business.content.models import Content
|
||||||
|
from igny8_core.business.integration.models import SiteIntegration
|
||||||
|
|
||||||
|
# Find content marked for WordPress publishing (status = published, external_id = empty)
|
||||||
|
pending_content = Content.objects.filter(
|
||||||
|
status='published',
|
||||||
|
external_id__isnull=True
|
||||||
|
).select_related('site', 'sector', 'cluster')
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
if not active_integrations.exists():
|
||||||
|
logger.warning("No active WordPress integrations found")
|
||||||
|
return {"success": False, "error": "No active WordPress integrations"}
|
||||||
|
|
||||||
|
processed = 0
|
||||||
|
|
||||||
|
for content in pending_content[:50]: # Process max 50 at a time
|
||||||
|
for integration in active_integrations.filter(site=content.site):
|
||||||
|
# Queue individual publish task
|
||||||
|
publish_content_to_wordpress.delay(
|
||||||
|
content.id,
|
||||||
|
integration.id
|
||||||
|
)
|
||||||
|
processed += 1
|
||||||
|
break # Only queue with first matching integration
|
||||||
|
|
||||||
|
logger.info(f"Queued {processed} content items for WordPress publication")
|
||||||
|
return {"success": True, "processed": processed}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing pending WordPress publications: {str(e)}", exc_info=True)
|
||||||
|
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.business.content.models import Content
|
||||||
|
from igny8_core.business.integration.models import SiteIntegration
|
||||||
|
|
||||||
|
site_integration = SiteIntegration.objects.get(id=site_integration_id)
|
||||||
|
content_items = Content.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
|
||||||
|
if content.external_id:
|
||||||
|
results["skipped"] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Queue individual publish task
|
||||||
|
publish_content_to_wordpress.delay(
|
||||||
|
content.id,
|
||||||
|
site_integration.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
|
||||||
|
|
||||||
|
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)}", exc_info=True)
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def wordpress_status_reconciliation() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Daily task to verify published content still exists on WordPress
|
||||||
|
Checks for discrepancies and fixes them
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from igny8_core.business.content.models import Content
|
||||||
|
|
||||||
|
# Get content marked as published
|
||||||
|
published_content = Content.objects.filter(
|
||||||
|
external_id__isnull=False
|
||||||
|
)[:100] # Limit to prevent timeouts
|
||||||
|
|
||||||
|
logger.info(f"Status reconciliation: Checking {len(published_content)} published items")
|
||||||
|
return {"success": True, "checked": len(published_content)}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in status reconciliation: {str(e)}", exc_info=True)
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def retry_failed_wordpress_publications() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Retry failed WordPress publications (runs daily)
|
||||||
|
For future use when we implement failure tracking
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("Retry task: No failure tracking currently implemented")
|
||||||
|
return {"success": True, "retried": 0}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrying failed publications: {str(e)}", exc_info=True)
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
519
backend/igny8_core/tasks/wordpress_publishing_new.py
Normal file
519
backend/igny8_core/tasks/wordpress_publishing_new.py
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
"""
|
||||||
|
IGNY8 Content Publishing Celery Tasks - WITH COMPREHENSIVE LOGGING
|
||||||
|
|
||||||
|
Handles automated publishing of content from IGNY8 to WordPress sites.
|
||||||
|
All workflow steps are logged to files for debugging and monitoring.
|
||||||
|
"""
|
||||||
|
from celery import shared_task
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Any, Optional
|
||||||
|
|
||||||
|
# Standard logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
# Dedicated file loggers
|
||||||
|
publish_logger = logging.getLogger('publish_sync')
|
||||||
|
api_logger = logging.getLogger('wordpress_api')
|
||||||
|
|
||||||
|
|
||||||
|
@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 with comprehensive logging
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from igny8_core.business.content.models import Content, ContentTaxonomyMap
|
||||||
|
from igny8_core.business.integration.models import SiteIntegration, SyncEvent
|
||||||
|
from igny8_core.modules.writer.models import Images
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
publish_logger.info(f"🎯 PUBLISH WORKFLOW STARTED")
|
||||||
|
publish_logger.info(f" Content ID: {content_id}")
|
||||||
|
publish_logger.info(f" Site Integration ID: {site_integration_id}")
|
||||||
|
publish_logger.info(f" Task ID: {task_id}")
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
|
||||||
|
# STEP 1: Load content and integration from database
|
||||||
|
try:
|
||||||
|
publish_logger.info(f"STEP 1: Loading content and integration from database...")
|
||||||
|
content = Content.objects.get(id=content_id)
|
||||||
|
publish_logger.info(f" ✅ Content loaded:")
|
||||||
|
publish_logger.info(f" - Title: '{content.title}'")
|
||||||
|
publish_logger.info(f" - Status: {content.status}")
|
||||||
|
publish_logger.info(f" - Type: {content.content_type}")
|
||||||
|
publish_logger.info(f" - Site: {content.site.name if content.site else 'None'}")
|
||||||
|
publish_logger.info(f" - Account: {content.account.name if content.account else 'None'}")
|
||||||
|
|
||||||
|
site_integration = SiteIntegration.objects.get(id=site_integration_id)
|
||||||
|
publish_logger.info(f" ✅ Integration loaded:")
|
||||||
|
publish_logger.info(f" - Platform: {site_integration.platform}")
|
||||||
|
publish_logger.info(f" - Site: {site_integration.site.name}")
|
||||||
|
publish_logger.info(f" - Base URL: {site_integration.base_url}")
|
||||||
|
publish_logger.info(f" - API Key: {'***' + site_integration.api_key[-4:] if site_integration.api_key else 'None'}")
|
||||||
|
except (Content.DoesNotExist, SiteIntegration.DoesNotExist) as e:
|
||||||
|
publish_logger.error(f" ❌ Database lookup failed: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
# STEP 2: Check if already published
|
||||||
|
publish_logger.info(f"STEP 2: Checking if content is already published...")
|
||||||
|
if content.external_id:
|
||||||
|
publish_logger.info(f" ⚠️ Content already published:")
|
||||||
|
publish_logger.info(f" - External ID: {content.external_id}")
|
||||||
|
publish_logger.info(f" - External URL: {content.external_url}")
|
||||||
|
publish_logger.info(f" ⏭️ Skipping publish workflow")
|
||||||
|
return {"success": True, "message": "Already published", "external_id": content.external_id}
|
||||||
|
else:
|
||||||
|
publish_logger.info(f" ✅ Content not yet published, proceeding...")
|
||||||
|
|
||||||
|
# STEP 3: Generate excerpt from HTML content
|
||||||
|
publish_logger.info(f"STEP 3: Generating excerpt from content HTML...")
|
||||||
|
excerpt = ''
|
||||||
|
if content.content_html:
|
||||||
|
excerpt = strip_tags(content.content_html)[:150].strip()
|
||||||
|
if len(content.content_html) > 150:
|
||||||
|
excerpt += '...'
|
||||||
|
publish_logger.info(f" ✅ Excerpt generated:")
|
||||||
|
publish_logger.info(f" - Content HTML length: {len(content.content_html)} chars")
|
||||||
|
publish_logger.info(f" - Excerpt: {excerpt[:80]}...")
|
||||||
|
else:
|
||||||
|
publish_logger.warning(f" ⚠️ No content_html found - excerpt will be empty")
|
||||||
|
|
||||||
|
# STEP 4: Get taxonomy terms (categories)
|
||||||
|
publish_logger.info(f"STEP 4: Loading taxonomy mappings for categories...")
|
||||||
|
taxonomy_maps = ContentTaxonomyMap.objects.filter(content=content).select_related('taxonomy')
|
||||||
|
publish_logger.info(f" Found {taxonomy_maps.count()} taxonomy mappings")
|
||||||
|
|
||||||
|
categories = []
|
||||||
|
for mapping in taxonomy_maps:
|
||||||
|
if mapping.taxonomy:
|
||||||
|
categories.append(mapping.taxonomy.name)
|
||||||
|
publish_logger.info(f" 📁 Category: '{mapping.taxonomy.name}'")
|
||||||
|
|
||||||
|
if not categories:
|
||||||
|
publish_logger.warning(f" ⚠️ No categories found for content")
|
||||||
|
else:
|
||||||
|
publish_logger.info(f" ✅ TOTAL categories: {len(categories)}")
|
||||||
|
|
||||||
|
# STEP 5: Get keywords as tags
|
||||||
|
publish_logger.info(f"STEP 5: Extracting keywords as tags...")
|
||||||
|
tags = []
|
||||||
|
|
||||||
|
if content.primary_keyword:
|
||||||
|
tags.append(content.primary_keyword)
|
||||||
|
publish_logger.info(f" 🏷️ Primary keyword: '{content.primary_keyword}'")
|
||||||
|
else:
|
||||||
|
publish_logger.warning(f" ⚠️ No primary keyword found")
|
||||||
|
|
||||||
|
if content.secondary_keywords:
|
||||||
|
if isinstance(content.secondary_keywords, list):
|
||||||
|
tags.extend(content.secondary_keywords)
|
||||||
|
publish_logger.info(f" 🏷️ Secondary keywords (list): {content.secondary_keywords}")
|
||||||
|
elif isinstance(content.secondary_keywords, str):
|
||||||
|
try:
|
||||||
|
keywords = json.loads(content.secondary_keywords)
|
||||||
|
if isinstance(keywords, list):
|
||||||
|
tags.extend(keywords)
|
||||||
|
publish_logger.info(f" 🏷️ Secondary keywords (JSON): {keywords}")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
publish_logger.warning(f" ⚠️ Failed to parse secondary_keywords as JSON: {content.secondary_keywords}")
|
||||||
|
else:
|
||||||
|
publish_logger.warning(f" ⚠️ No secondary keywords found")
|
||||||
|
|
||||||
|
if not tags:
|
||||||
|
publish_logger.warning(f" ⚠️ No tags found for content")
|
||||||
|
else:
|
||||||
|
publish_logger.info(f" ✅ TOTAL tags: {len(tags)}")
|
||||||
|
|
||||||
|
# STEP 6: Get images (featured + gallery)
|
||||||
|
publish_logger.info(f"STEP 6: Loading images for content...")
|
||||||
|
images = Images.objects.filter(content=content).order_by('position')
|
||||||
|
publish_logger.info(f" Found {images.count()} images")
|
||||||
|
|
||||||
|
featured_image_url = None
|
||||||
|
gallery_images = []
|
||||||
|
|
||||||
|
for image in images:
|
||||||
|
if image.image_type == 'featured' and image.image_url:
|
||||||
|
featured_image_url = image.image_url
|
||||||
|
publish_logger.info(f" 🖼️ Featured image: {image.image_url[:80]}...")
|
||||||
|
elif image.image_type == 'in_article' and image.image_url:
|
||||||
|
gallery_images.append({
|
||||||
|
'url': image.image_url,
|
||||||
|
'alt': image.alt_text or '',
|
||||||
|
'caption': image.caption or ''
|
||||||
|
})
|
||||||
|
publish_logger.info(f" 🖼️ Gallery image {len(gallery_images)}: {image.image_url[:60]}...")
|
||||||
|
|
||||||
|
if not featured_image_url:
|
||||||
|
publish_logger.warning(f" ⚠️ No featured image found")
|
||||||
|
if not gallery_images:
|
||||||
|
publish_logger.info(f" ℹ️ No gallery images found")
|
||||||
|
else:
|
||||||
|
publish_logger.info(f" ✅ Gallery images: {len(gallery_images)}")
|
||||||
|
|
||||||
|
# STEP 7: Prepare content payload
|
||||||
|
publish_logger.info(f"STEP 7: Building WordPress API payload...")
|
||||||
|
content_data = {
|
||||||
|
'content_id': content.id,
|
||||||
|
'task_id': task_id,
|
||||||
|
'title': content.title,
|
||||||
|
'content_html': content.content_html or '',
|
||||||
|
'excerpt': excerpt,
|
||||||
|
'status': 'publish',
|
||||||
|
'author_email': None,
|
||||||
|
'author_name': None,
|
||||||
|
'published_at': None,
|
||||||
|
'seo_title': content.meta_title or '',
|
||||||
|
'seo_description': content.meta_description or '',
|
||||||
|
'primary_keyword': content.primary_keyword or '',
|
||||||
|
'secondary_keywords': content.secondary_keywords or [],
|
||||||
|
'featured_image_url': featured_image_url,
|
||||||
|
'gallery_images': gallery_images,
|
||||||
|
'cluster_id': content.cluster.id if content.cluster else None,
|
||||||
|
'sector_id': content.sector.id if content.sector else None,
|
||||||
|
'categories': categories,
|
||||||
|
'tags': tags,
|
||||||
|
'sectors': [],
|
||||||
|
'clusters': []
|
||||||
|
}
|
||||||
|
|
||||||
|
publish_logger.info(f" ✅ Payload built:")
|
||||||
|
publish_logger.info(f" - Title: {content.title}")
|
||||||
|
publish_logger.info(f" - Content HTML: {len(content_data['content_html'])} chars")
|
||||||
|
publish_logger.info(f" - Categories: {len(categories)}")
|
||||||
|
publish_logger.info(f" - Tags: {len(tags)}")
|
||||||
|
publish_logger.info(f" - Featured image: {'Yes' if featured_image_url else 'No'}")
|
||||||
|
publish_logger.info(f" - Gallery images: {len(gallery_images)}")
|
||||||
|
publish_logger.info(f" - SEO title: {'Yes' if content_data['seo_title'] else 'No'}")
|
||||||
|
publish_logger.info(f" - SEO description: {'Yes' if content_data['seo_description'] else 'No'}")
|
||||||
|
publish_logger.info(f" - Cluster ID: {content_data['cluster_id']}")
|
||||||
|
publish_logger.info(f" - Sector ID: {content_data['sector_id']}")
|
||||||
|
|
||||||
|
# STEP 8: Send API request to WordPress
|
||||||
|
wordpress_url = f"{site_integration.base_url}/wp-json/igny8/v1/publish"
|
||||||
|
headers = {
|
||||||
|
'X-IGNY8-API-Key': site_integration.api_key,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
publish_logger.info(f"STEP 8: Sending POST request to WordPress...")
|
||||||
|
api_logger.info(f"API REQUEST: POST {wordpress_url}")
|
||||||
|
api_logger.info(f" Headers: X-IGNY8-API-Key: ***{site_integration.api_key[-4:]}")
|
||||||
|
api_logger.info(f" Payload: {json.dumps(content_data, indent=2)[:500]}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
wordpress_url,
|
||||||
|
json=content_data,
|
||||||
|
headers=headers,
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
|
api_logger.info(f"API RESPONSE: {response.status_code}")
|
||||||
|
api_logger.info(f" Response body: {response.text[:500]}")
|
||||||
|
publish_logger.info(f" ✅ WordPress responded: HTTP {response.status_code}")
|
||||||
|
|
||||||
|
except requests.exceptions.Timeout as e:
|
||||||
|
error_msg = f"WordPress API timeout after 30s: {str(e)}"
|
||||||
|
api_logger.error(f"API ERROR: {error_msg}")
|
||||||
|
publish_logger.error(f" ❌ Request timed out after 30 seconds")
|
||||||
|
|
||||||
|
# Log failure event
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='error',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Timeout publishing '{content.title}' to WordPress",
|
||||||
|
success=False,
|
||||||
|
content_id=content.id,
|
||||||
|
error_message=error_msg,
|
||||||
|
duration_ms=int((time.time() - start_time) * 1000)
|
||||||
|
)
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
error_msg = f"WordPress API request failed: {str(e)}"
|
||||||
|
api_logger.error(f"API ERROR: {error_msg}")
|
||||||
|
publish_logger.error(f" ❌ Request exception: {str(e)}")
|
||||||
|
|
||||||
|
# Log failure event
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='error',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Request error publishing '{content.title}' to WordPress",
|
||||||
|
success=False,
|
||||||
|
content_id=content.id,
|
||||||
|
error_message=error_msg,
|
||||||
|
duration_ms=int((time.time() - start_time) * 1000)
|
||||||
|
)
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
|
# STEP 9: Process response
|
||||||
|
publish_logger.info(f"STEP 9: Processing WordPress response...")
|
||||||
|
|
||||||
|
if response.status_code == 201:
|
||||||
|
# SUCCESS - Post created
|
||||||
|
publish_logger.info(f" ✅ WordPress post created successfully (HTTP 201)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_data = response.json()
|
||||||
|
wp_data = response_data.get('data', {})
|
||||||
|
wp_status = wp_data.get('post_status', 'publish')
|
||||||
|
|
||||||
|
publish_logger.info(f" - Post ID: {wp_data.get('post_id')}")
|
||||||
|
publish_logger.info(f" - Post URL: {wp_data.get('post_url')}")
|
||||||
|
publish_logger.info(f" - Post status: {wp_status}")
|
||||||
|
|
||||||
|
# Update Content model
|
||||||
|
publish_logger.info(f"STEP 10: Updating IGNY8 Content model...")
|
||||||
|
content.external_id = str(wp_data.get('post_id'))
|
||||||
|
content.external_url = wp_data.get('post_url')
|
||||||
|
content.status = 'published'
|
||||||
|
|
||||||
|
if not hasattr(content, 'metadata') or content.metadata is None:
|
||||||
|
content.metadata = {}
|
||||||
|
content.metadata['wordpress_status'] = wp_status
|
||||||
|
|
||||||
|
content.save(update_fields=['external_id', 'external_url', 'status', 'metadata', 'updated_at'])
|
||||||
|
|
||||||
|
publish_logger.info(f" ✅ Content model updated:")
|
||||||
|
publish_logger.info(f" - External ID: {content.external_id}")
|
||||||
|
publish_logger.info(f" - External URL: {content.external_url}")
|
||||||
|
publish_logger.info(f" - Status: published")
|
||||||
|
publish_logger.info(f" - WordPress status: {wp_status}")
|
||||||
|
|
||||||
|
# Log success event
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='publish',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Published content '{content.title}' to WordPress",
|
||||||
|
success=True,
|
||||||
|
content_id=content.id,
|
||||||
|
external_id=str(content.external_id),
|
||||||
|
details={
|
||||||
|
'post_url': content.external_url,
|
||||||
|
'wordpress_status': wp_status,
|
||||||
|
'categories': categories,
|
||||||
|
'tags': tags,
|
||||||
|
'has_featured_image': bool(featured_image_url),
|
||||||
|
'gallery_images_count': len(gallery_images),
|
||||||
|
},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
publish_logger.info(f"="*80)
|
||||||
|
publish_logger.info(f"🎉 PUBLISH WORKFLOW COMPLETED SUCCESSFULLY")
|
||||||
|
publish_logger.info(f" Duration: {duration_ms}ms")
|
||||||
|
publish_logger.info(f" WordPress Post ID: {content.external_id}")
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"external_id": content.external_id,
|
||||||
|
"external_url": content.external_url,
|
||||||
|
"wordpress_status": wp_status
|
||||||
|
}
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, KeyError) as e:
|
||||||
|
error_msg = f"Failed to parse WordPress response: {str(e)}"
|
||||||
|
publish_logger.error(f" ❌ Response parsing error: {error_msg}")
|
||||||
|
publish_logger.error(f" Raw response: {response.text[:200]}")
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
|
elif response.status_code == 409:
|
||||||
|
# CONFLICT - Post already exists
|
||||||
|
publish_logger.info(f" ⚠️ Content already exists in WordPress (HTTP 409)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response_data = response.json()
|
||||||
|
wp_data = response_data.get('data', {})
|
||||||
|
wp_status = wp_data.get('post_status', 'publish')
|
||||||
|
|
||||||
|
publish_logger.info(f" - Existing Post ID: {wp_data.get('post_id')}")
|
||||||
|
publish_logger.info(f" - Post URL: {wp_data.get('post_url')}")
|
||||||
|
|
||||||
|
# Update Content model
|
||||||
|
content.external_id = str(wp_data.get('post_id'))
|
||||||
|
content.external_url = wp_data.get('post_url')
|
||||||
|
content.status = 'published'
|
||||||
|
|
||||||
|
if not hasattr(content, 'metadata') or content.metadata is None:
|
||||||
|
content.metadata = {}
|
||||||
|
content.metadata['wordpress_status'] = wp_status
|
||||||
|
|
||||||
|
content.save(update_fields=['external_id', 'external_url', 'status', 'metadata', 'updated_at'])
|
||||||
|
|
||||||
|
# Log sync event
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='sync',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Content '{content.title}' already exists in WordPress",
|
||||||
|
success=True,
|
||||||
|
content_id=content.id,
|
||||||
|
external_id=str(content.external_id),
|
||||||
|
details={
|
||||||
|
'post_url': content.external_url,
|
||||||
|
'wordpress_status': wp_status,
|
||||||
|
'already_exists': True,
|
||||||
|
},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
publish_logger.info(f"="*80)
|
||||||
|
publish_logger.info(f"✅ PUBLISH WORKFLOW COMPLETED (already exists)")
|
||||||
|
publish_logger.info(f" Duration: {duration_ms}ms")
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Content already exists",
|
||||||
|
"external_id": content.external_id,
|
||||||
|
"wordpress_status": wp_status
|
||||||
|
}
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, KeyError) as e:
|
||||||
|
error_msg = f"Failed to parse 409 response: {str(e)}"
|
||||||
|
publish_logger.error(f" ❌ Response parsing error: {error_msg}")
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# ERROR - Unexpected status code
|
||||||
|
error_msg = f"WordPress API error: HTTP {response.status_code}"
|
||||||
|
publish_logger.error(f" ❌ Unexpected status code: {response.status_code}")
|
||||||
|
publish_logger.error(f" Response: {response.text[:500]}")
|
||||||
|
|
||||||
|
api_logger.error(f"API ERROR: {error_msg}")
|
||||||
|
api_logger.error(f" Full response: {response.text}")
|
||||||
|
|
||||||
|
# Log failure event
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='error',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Failed to publish content '{content.title}' to WordPress",
|
||||||
|
success=False,
|
||||||
|
content_id=content.id,
|
||||||
|
error_message=error_msg,
|
||||||
|
details={
|
||||||
|
'status_code': response.status_code,
|
||||||
|
'response_text': response.text[:500],
|
||||||
|
},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retry logic
|
||||||
|
if self.request.retries < self.max_retries:
|
||||||
|
countdown = 60 * (5 ** self.request.retries)
|
||||||
|
publish_logger.warning(f" 🔄 Scheduling retry (attempt {self.request.retries + 1}/{self.max_retries}) in {countdown}s")
|
||||||
|
raise self.retry(countdown=countdown, exc=Exception(error_msg))
|
||||||
|
else:
|
||||||
|
publish_logger.error(f" ❌ Max retries reached, giving up")
|
||||||
|
publish_logger.info(f"="*80)
|
||||||
|
publish_logger.error(f"❌ PUBLISH WORKFLOW FAILED")
|
||||||
|
publish_logger.info(f" Duration: {duration_ms}ms")
|
||||||
|
publish_logger.info("="*80)
|
||||||
|
return {"success": False, "error": error_msg}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
duration_ms = int((time.time() - start_time) * 1000)
|
||||||
|
publish_logger.error(f"="*80)
|
||||||
|
publish_logger.error(f"💥 PUBLISH WORKFLOW EXCEPTION")
|
||||||
|
publish_logger.error(f" Exception type: {type(e).__name__}")
|
||||||
|
publish_logger.error(f" Exception message: {str(e)}")
|
||||||
|
publish_logger.error(f" Duration: {duration_ms}ms")
|
||||||
|
publish_logger.error("="*80, exc_info=True)
|
||||||
|
|
||||||
|
# Try to log sync event
|
||||||
|
try:
|
||||||
|
from igny8_core.business.integration.models import SyncEvent
|
||||||
|
SyncEvent.objects.create(
|
||||||
|
integration=site_integration,
|
||||||
|
site=content.site,
|
||||||
|
account=content.account,
|
||||||
|
event_type='error',
|
||||||
|
action='content_publish',
|
||||||
|
description=f"Exception publishing '{content.title}' to WordPress",
|
||||||
|
success=False,
|
||||||
|
content_id=content.id,
|
||||||
|
error_message=str(e),
|
||||||
|
details={'exception_type': type(e).__name__},
|
||||||
|
duration_ms=duration_ms
|
||||||
|
)
|
||||||
|
except Exception as log_error:
|
||||||
|
publish_logger.error(f"Failed to log sync event: {str(log_error)}")
|
||||||
|
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def process_pending_wordpress_publications() -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Process all content items pending WordPress publication
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from igny8_core.business.content.models import Content
|
||||||
|
from igny8_core.business.integration.models import SiteIntegration
|
||||||
|
|
||||||
|
pending_content = Content.objects.filter(
|
||||||
|
status='published',
|
||||||
|
external_id__isnull=True
|
||||||
|
).select_related('site', 'sector', 'cluster')
|
||||||
|
|
||||||
|
if not pending_content.exists():
|
||||||
|
publish_logger.info("No content pending WordPress publication")
|
||||||
|
return {"success": True, "processed": 0}
|
||||||
|
|
||||||
|
active_integrations = SiteIntegration.objects.filter(
|
||||||
|
platform='wordpress',
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not active_integrations.exists():
|
||||||
|
publish_logger.warning("No active WordPress integrations found")
|
||||||
|
return {"success": False, "error": "No active WordPress integrations"}
|
||||||
|
|
||||||
|
processed = 0
|
||||||
|
for content in pending_content[:50]:
|
||||||
|
for integration in active_integrations.filter(site=content.site):
|
||||||
|
publish_content_to_wordpress.delay(content.id, integration.id)
|
||||||
|
processed += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
publish_logger.info(f"Queued {processed} content items for WordPress publication")
|
||||||
|
return {"success": True, "processed": processed}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
publish_logger.error(f"Error processing pending publications: {str(e)}", exc_info=True)
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
@@ -81,6 +81,7 @@ class Igny8Bridge {
|
|||||||
private function load_dependencies() {
|
private function load_dependencies() {
|
||||||
// Core classes
|
// Core classes
|
||||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/functions.php';
|
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/functions.php';
|
||||||
|
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-logger.php'; // Load logger first
|
||||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-api.php';
|
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-api.php';
|
||||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-site.php';
|
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-site.php';
|
||||||
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-rest-api.php';
|
require_once IGNY8_BRIDGE_PLUGIN_DIR . 'includes/class-igny8-rest-api.php';
|
||||||
|
|||||||
@@ -114,6 +114,15 @@ class Igny8API {
|
|||||||
return !empty($this->access_token);
|
return !empty($this->access_token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the API base URL
|
||||||
|
*
|
||||||
|
* @return string API base URL
|
||||||
|
*/
|
||||||
|
public function get_api_base() {
|
||||||
|
return $this->base_url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse unified API response
|
* Parse unified API response
|
||||||
*
|
*
|
||||||
|
|||||||
221
igny8-wp-plugin/includes/class-igny8-logger.php
Normal file
221
igny8-wp-plugin/includes/class-igny8-logger.php
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* IGNY8 File Logger
|
||||||
|
*
|
||||||
|
* Provides file-based logging for all publish/sync workflows
|
||||||
|
*
|
||||||
|
* @package IGNY8_Bridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Igny8_Logger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log directory path
|
||||||
|
*/
|
||||||
|
private static $log_dir = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize logger
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
// Set log directory to plugin root/logs/publish-sync-logs
|
||||||
|
self::$log_dir = dirname(dirname(__FILE__)) . '/logs/publish-sync-logs';
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!file_exists(self::$log_dir)) {
|
||||||
|
wp_mkdir_p(self::$log_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory is writable
|
||||||
|
if (!is_writable(self::$log_dir)) {
|
||||||
|
@chmod(self::$log_dir, 0755);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write log message to file
|
||||||
|
*
|
||||||
|
* @param string $message Log message
|
||||||
|
* @param string $level Log level (INFO, WARNING, ERROR)
|
||||||
|
* @param string $log_file Log file name (without .log extension)
|
||||||
|
*/
|
||||||
|
public static function log($message, $level = 'INFO', $log_file = 'publish-sync') {
|
||||||
|
if (self::$log_dir === null) {
|
||||||
|
self::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = current_time('Y-m-d H:i:s');
|
||||||
|
$formatted_message = "[{$timestamp}] [{$level}] {$message}\n";
|
||||||
|
|
||||||
|
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||||
|
|
||||||
|
// Append to log file
|
||||||
|
error_log($formatted_message, 3, $file_path);
|
||||||
|
|
||||||
|
// Also log to WordPress debug.log if WP_DEBUG is enabled
|
||||||
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
|
error_log("[IGNY8] [{$level}] {$message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log info message
|
||||||
|
*/
|
||||||
|
public static function info($message, $log_file = 'publish-sync') {
|
||||||
|
self::log($message, 'INFO', $log_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log warning message
|
||||||
|
*/
|
||||||
|
public static function warning($message, $log_file = 'publish-sync') {
|
||||||
|
self::log($message, 'WARNING', $log_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log error message
|
||||||
|
*/
|
||||||
|
public static function error($message, $log_file = 'publish-sync') {
|
||||||
|
self::log($message, 'ERROR', $log_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log API request
|
||||||
|
*/
|
||||||
|
public static function api_request($method, $endpoint, $data = null) {
|
||||||
|
$message = "API REQUEST: {$method} {$endpoint}";
|
||||||
|
if ($data) {
|
||||||
|
$message .= "\n Data: " . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
self::log($message, 'INFO', 'wordpress-api');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log API response
|
||||||
|
*/
|
||||||
|
public static function api_response($status_code, $body) {
|
||||||
|
$message = "API RESPONSE: HTTP {$status_code}";
|
||||||
|
if ($body) {
|
||||||
|
$body_str = is_string($body) ? $body : json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||||
|
$message .= "\n Body: " . substr($body_str, 0, 500);
|
||||||
|
}
|
||||||
|
self::log($message, 'INFO', 'wordpress-api');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log API error
|
||||||
|
*/
|
||||||
|
public static function api_error($error_message) {
|
||||||
|
self::log("API ERROR: {$error_message}", 'ERROR', 'wordpress-api');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log webhook event
|
||||||
|
*/
|
||||||
|
public static function webhook($event_type, $data) {
|
||||||
|
$message = "WEBHOOK EVENT: {$event_type}";
|
||||||
|
if ($data) {
|
||||||
|
$message .= "\n Data: " . json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
self::log($message, 'INFO', 'webhooks');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log workflow separator
|
||||||
|
*/
|
||||||
|
public static function separator($title = '') {
|
||||||
|
$line = str_repeat('=', 80);
|
||||||
|
self::log($line);
|
||||||
|
if ($title) {
|
||||||
|
self::log($title);
|
||||||
|
self::log($line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get log file contents
|
||||||
|
*
|
||||||
|
* @param string $log_file Log file name
|
||||||
|
* @param int $lines Number of lines to read (default 100, 0 for all)
|
||||||
|
* @return string Log contents
|
||||||
|
*/
|
||||||
|
public static function get_log_contents($log_file = 'publish-sync', $lines = 100) {
|
||||||
|
if (self::$log_dir === null) {
|
||||||
|
self::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||||
|
|
||||||
|
if (!file_exists($file_path)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($lines === 0) {
|
||||||
|
return file_get_contents($file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read last N lines efficiently
|
||||||
|
$file = new SplFileObject($file_path, 'r');
|
||||||
|
$file->seek(PHP_INT_MAX);
|
||||||
|
$total_lines = $file->key() + 1;
|
||||||
|
|
||||||
|
$start_line = max(0, $total_lines - $lines);
|
||||||
|
$file->seek($start_line);
|
||||||
|
|
||||||
|
$content = '';
|
||||||
|
while (!$file->eof()) {
|
||||||
|
$content .= $file->fgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear log file
|
||||||
|
*/
|
||||||
|
public static function clear_log($log_file = 'publish-sync') {
|
||||||
|
if (self::$log_dir === null) {
|
||||||
|
self::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
$file_path = self::$log_dir . '/' . $log_file . '.log';
|
||||||
|
|
||||||
|
if (file_exists($file_path)) {
|
||||||
|
@unlink($file_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all log files
|
||||||
|
*/
|
||||||
|
public static function get_log_files() {
|
||||||
|
if (self::$log_dir === null) {
|
||||||
|
self::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir(self::$log_dir)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = glob(self::$log_dir . '/*.log');
|
||||||
|
$log_files = array();
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$log_files[] = array(
|
||||||
|
'name' => basename($file, '.log'),
|
||||||
|
'path' => $file,
|
||||||
|
'size' => filesize($file),
|
||||||
|
'modified' => filemtime($file),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $log_files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
Igny8_Logger::init();
|
||||||
@@ -71,19 +71,28 @@ function igny8_cache_task_brief($task_id, $post_id, $api = null) {
|
|||||||
* @return int|WP_Error WordPress post ID or error
|
* @return int|WP_Error WordPress post ID or error
|
||||||
*/
|
*/
|
||||||
function igny8_create_wordpress_post_from_task($content_data, $allowed_post_types = array()) {
|
function igny8_create_wordpress_post_from_task($content_data, $allowed_post_types = array()) {
|
||||||
error_log('========== IGNY8 CREATE WP POST ==========');
|
// Get site information for logging
|
||||||
error_log('Content ID: ' . (isset($content_data['content_id']) ? $content_data['content_id'] : 'N/A'));
|
$site_id = get_option('igny8_site_id', 'unknown');
|
||||||
error_log('Task ID: ' . (isset($content_data['task_id']) ? $content_data['task_id'] : 'N/A'));
|
$site_domain = parse_url(home_url(), PHP_URL_HOST);
|
||||||
error_log('Title: ' . (isset($content_data['title']) ? $content_data['title'] : 'N/A'));
|
$log_prefix = "[{$site_id}-{$site_domain}]";
|
||||||
|
|
||||||
|
Igny8_Logger::separator("{$log_prefix} 🎯 CREATE WORDPRESS POST FROM IGNY8");
|
||||||
|
Igny8_Logger::info("{$log_prefix} Content ID: " . ($content_data['content_id'] ?? 'N/A'));
|
||||||
|
Igny8_Logger::info("{$log_prefix} Task ID: " . ($content_data['task_id'] ?? 'N/A'));
|
||||||
|
Igny8_Logger::info("{$log_prefix} Title: " . ($content_data['title'] ?? 'N/A'));
|
||||||
|
Igny8_Logger::separator();
|
||||||
|
|
||||||
$api = new Igny8API();
|
$api = new Igny8API();
|
||||||
|
|
||||||
if (!$api->is_authenticated()) {
|
if (!$api->is_authenticated()) {
|
||||||
error_log('IGNY8: NOT AUTHENTICATED - Aborting post creation');
|
Igny8_Logger::error("{$log_prefix} ❌ NOT AUTHENTICATED - Aborting post creation");
|
||||||
return new WP_Error('igny8_not_authenticated', 'IGNY8 API not authenticated');
|
return new WP_Error('igny8_not_authenticated', 'IGNY8 API not authenticated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Igny8_Logger::info("{$log_prefix} ✅ API authenticated");
|
||||||
|
|
||||||
$post_type = igny8_resolve_post_type_for_task($content_data);
|
$post_type = igny8_resolve_post_type_for_task($content_data);
|
||||||
|
Igny8_Logger::info("{$log_prefix} STEP 1: Resolved post type: {$post_type}");
|
||||||
|
|
||||||
if (!empty($allowed_post_types) && !in_array($post_type, $allowed_post_types, true)) {
|
if (!empty($allowed_post_types) && !in_array($post_type, $allowed_post_types, true)) {
|
||||||
return new WP_Error('igny8_post_type_disabled', sprintf('Post type %s is disabled for automation', $post_type));
|
return new WP_Error('igny8_post_type_disabled', sprintf('Post type %s is disabled for automation', $post_type));
|
||||||
@@ -204,80 +213,80 @@ function igny8_create_wordpress_post_from_task($content_data, $allowed_post_type
|
|||||||
|
|
||||||
// Handle categories
|
// Handle categories
|
||||||
if (!empty($content_data['categories'])) {
|
if (!empty($content_data['categories'])) {
|
||||||
error_log('IGNY8: Processing ' . count($content_data['categories']) . ' categories');
|
Igny8_Logger::info("{$log_prefix} STEP: Processing " . count($content_data['categories']) . " categories");
|
||||||
$category_ids = igny8_process_categories($content_data['categories'], $post_id);
|
$category_ids = igny8_process_categories($content_data['categories'], $post_id);
|
||||||
if (!empty($category_ids)) {
|
if (!empty($category_ids)) {
|
||||||
wp_set_post_terms($post_id, $category_ids, 'category');
|
wp_set_post_terms($post_id, $category_ids, 'category');
|
||||||
error_log('IGNY8: ✅ Assigned ' . count($category_ids) . ' categories to post ' . $post_id);
|
Igny8_Logger::info("{$log_prefix} ✅ Assigned " . count($category_ids) . " categories to post {$post_id}");
|
||||||
} else {
|
} else {
|
||||||
error_log('IGNY8: ⚠️ No category IDs returned from processing');
|
Igny8_Logger::warning("{$log_prefix} ⚠️ No category IDs returned from processing");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log('IGNY8: ⚠️ No categories in content_data');
|
Igny8_Logger::warning("{$log_prefix} ⚠️ No categories in content_data");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tags
|
// Handle tags
|
||||||
if (!empty($content_data['tags'])) {
|
if (!empty($content_data['tags'])) {
|
||||||
error_log('IGNY8: Processing ' . count($content_data['tags']) . ' tags');
|
Igny8_Logger::info("{$log_prefix} STEP: Processing " . count($content_data['tags']) . " tags");
|
||||||
$tag_ids = igny8_process_tags($content_data['tags'], $post_id);
|
$tag_ids = igny8_process_tags($content_data['tags'], $post_id);
|
||||||
if (!empty($tag_ids)) {
|
if (!empty($tag_ids)) {
|
||||||
wp_set_post_terms($post_id, $tag_ids, 'post_tag');
|
wp_set_post_terms($post_id, $tag_ids, 'post_tag');
|
||||||
error_log('IGNY8: ✅ Assigned ' . count($tag_ids) . ' tags to post ' . $post_id);
|
Igny8_Logger::info("{$log_prefix} ✅ Assigned " . count($tag_ids) . " tags to post {$post_id}");
|
||||||
} else {
|
} else {
|
||||||
error_log('IGNY8: ⚠️ No tag IDs returned from processing');
|
Igny8_Logger::warning("{$log_prefix} ⚠️ No tag IDs returned from processing");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log('IGNY8: ⚠️ No tags in content_data');
|
Igny8_Logger::warning("{$log_prefix} ⚠️ No tags in content_data");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle featured image
|
// Handle featured image
|
||||||
if (!empty($content_data['featured_image'])) {
|
if (!empty($content_data['featured_image'])) {
|
||||||
error_log('IGNY8: Setting featured image from featured_image field');
|
Igny8_Logger::info("{$log_prefix} STEP: Setting featured image from featured_image field");
|
||||||
igny8_set_featured_image($post_id, $content_data['featured_image']);
|
igny8_set_featured_image($post_id, $content_data['featured_image']);
|
||||||
} elseif (!empty($content_data['featured_image_url'])) {
|
} elseif (!empty($content_data['featured_image_url'])) {
|
||||||
error_log('IGNY8: Setting featured image from featured_image_url field: ' . $content_data['featured_image_url']);
|
Igny8_Logger::info("{$log_prefix} STEP: Setting featured image from URL: " . $content_data['featured_image_url']);
|
||||||
igny8_set_featured_image($post_id, $content_data['featured_image_url']);
|
igny8_set_featured_image($post_id, $content_data['featured_image_url']);
|
||||||
} else {
|
} else {
|
||||||
error_log('IGNY8: ⚠️ No featured image in content_data');
|
Igny8_Logger::warning("{$log_prefix} ⚠️ No featured image in content_data");
|
||||||
}
|
}
|
||||||
// Handle meta title and meta description (SEO)
|
// Handle meta title and meta description (SEO)
|
||||||
if (!empty($content_data['meta_title'])) {
|
if (!empty($content_data['meta_title'])) {
|
||||||
error_log('IGNY8: Setting SEO meta title: ' . $content_data['meta_title']);
|
Igny8_Logger::info("{$log_prefix} STEP: Setting SEO meta title: " . $content_data['meta_title']);
|
||||||
update_post_meta($post_id, '_yoast_wpseo_title', $content_data['meta_title']);
|
update_post_meta($post_id, '_yoast_wpseo_title', $content_data['meta_title']);
|
||||||
update_post_meta($post_id, '_seopress_titles_title', $content_data['meta_title']);
|
update_post_meta($post_id, '_seopress_titles_title', $content_data['meta_title']);
|
||||||
update_post_meta($post_id, '_aioseo_title', $content_data['meta_title']);
|
update_post_meta($post_id, '_aioseo_title', $content_data['meta_title']);
|
||||||
// Generic meta
|
// Generic meta
|
||||||
update_post_meta($post_id, '_igny8_meta_title', $content_data['meta_title']);
|
update_post_meta($post_id, '_igny8_meta_title', $content_data['meta_title']);
|
||||||
} elseif (!empty($content_data['seo_title'])) {
|
} elseif (!empty($content_data['seo_title'])) {
|
||||||
error_log('IGNY8: Setting SEO meta title from seo_title: ' . $content_data['seo_title']);
|
Igny8_Logger::info("{$log_prefix} STEP: Setting SEO meta title from seo_title: " . $content_data['seo_title']);
|
||||||
update_post_meta($post_id, '_yoast_wpseo_title', $content_data['seo_title']);
|
update_post_meta($post_id, '_yoast_wpseo_title', $content_data['seo_title']);
|
||||||
update_post_meta($post_id, '_seopress_titles_title', $content_data['seo_title']);
|
update_post_meta($post_id, '_seopress_titles_title', $content_data['seo_title']);
|
||||||
update_post_meta($post_id, '_aioseo_title', $content_data['seo_title']);
|
update_post_meta($post_id, '_aioseo_title', $content_data['seo_title']);
|
||||||
update_post_meta($post_id, '_igny8_meta_title', $content_data['seo_title']);
|
update_post_meta($post_id, '_igny8_meta_title', $content_data['seo_title']);
|
||||||
} else {
|
} else {
|
||||||
error_log('IGNY8: ⚠️ No SEO title in content_data');
|
Igny8_Logger::warning("{$log_prefix} ⚠️ No SEO title in content_data");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($content_data['meta_description'])) {
|
if (!empty($content_data['meta_description'])) {
|
||||||
error_log('IGNY8: Setting SEO meta description');
|
Igny8_Logger::info("{$log_prefix} STEP: Setting SEO meta description");
|
||||||
update_post_meta($post_id, '_yoast_wpseo_metadesc', $content_data['meta_description']);
|
update_post_meta($post_id, '_yoast_wpseo_metadesc', $content_data['meta_description']);
|
||||||
update_post_meta($post_id, '_seopress_titles_desc', $content_data['meta_description']);
|
update_post_meta($post_id, '_seopress_titles_desc', $content_data['meta_description']);
|
||||||
update_post_meta($post_id, '_aioseo_description', $content_data['meta_description']);
|
update_post_meta($post_id, '_aioseo_description', $content_data['meta_description']);
|
||||||
// Generic meta
|
// Generic meta
|
||||||
update_post_meta($post_id, '_igny8_meta_description', $content_data['meta_description']);
|
update_post_meta($post_id, '_igny8_meta_description', $content_data['meta_description']);
|
||||||
} elseif (!empty($content_data['seo_description'])) {
|
} elseif (!empty($content_data['seo_description'])) {
|
||||||
error_log('IGNY8: Setting SEO meta description from seo_description');
|
Igny8_Logger::info("{$log_prefix} STEP: Setting SEO meta description from seo_description");
|
||||||
update_post_meta($post_id, '_yoast_wpseo_metadesc', $content_data['seo_description']);
|
update_post_meta($post_id, '_yoast_wpseo_metadesc', $content_data['seo_description']);
|
||||||
update_post_meta($post_id, '_seopress_titles_desc', $content_data['seo_description']);
|
update_post_meta($post_id, '_seopress_titles_desc', $content_data['seo_description']);
|
||||||
update_post_meta($post_id, '_aioseo_description', $content_data['seo_description']);
|
update_post_meta($post_id, '_aioseo_description', $content_data['seo_description']);
|
||||||
update_post_meta($post_id, '_igny8_meta_description', $content_data['seo_description']);
|
update_post_meta($post_id, '_igny8_meta_description', $content_data['seo_description']);
|
||||||
} else {
|
} else {
|
||||||
error_log('IGNY8: ⚠️ No SEO description in content_data');
|
Igny8_Logger::warning("{$log_prefix} ⚠️ No SEO description in content_data");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle gallery images
|
// Handle gallery images
|
||||||
if (!empty($content_data['gallery_images'])) {
|
if (!empty($content_data['gallery_images'])) {
|
||||||
error_log('IGNY8: Setting gallery with ' . count($content_data['gallery_images']) . ' images');
|
Igny8_Logger::info("{$log_prefix} STEP: Setting gallery with " . count($content_data['gallery_images']) . " images");
|
||||||
igny8_set_gallery_images($post_id, $content_data['gallery_images']);
|
igny8_set_gallery_images($post_id, $content_data['gallery_images']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,9 +320,9 @@ function igny8_create_wordpress_post_from_task($content_data, $allowed_post_type
|
|||||||
$response = $api->put("/writer/tasks/{$content_data['task_id']}/", $update_data);
|
$response = $api->put("/writer/tasks/{$content_data['task_id']}/", $update_data);
|
||||||
|
|
||||||
if ($response['success']) {
|
if ($response['success']) {
|
||||||
error_log("IGNY8: Updated task {$content_data['task_id']} with WordPress post {$post_id} (status: {$wp_status})");
|
Igny8_Logger::info("{$log_prefix} ✅ Updated IGNY8 task {$content_data['task_id']} with WordPress post {$post_id} (status: {$wp_status})");
|
||||||
} else {
|
} else {
|
||||||
error_log("IGNY8: Failed to update task: " . ($response['error'] ?? 'Unknown error'));
|
Igny8_Logger::error("{$log_prefix} ❌ Failed to update IGNY8 task: " . ($response['error'] ?? 'Unknown error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +334,12 @@ function igny8_create_wordpress_post_from_task($content_data, $allowed_post_type
|
|||||||
// Send status webhook to IGNY8
|
// Send status webhook to IGNY8
|
||||||
igny8_send_status_webhook($post_id, $content_data, $wp_status);
|
igny8_send_status_webhook($post_id, $content_data, $wp_status);
|
||||||
|
|
||||||
|
Igny8_Logger::separator("{$log_prefix} 🎉 POST CREATION COMPLETED SUCCESSFULLY");
|
||||||
|
Igny8_Logger::info("{$log_prefix} WordPress Post ID: {$post_id}");
|
||||||
|
Igny8_Logger::info("{$log_prefix} Post URL: " . get_permalink($post_id));
|
||||||
|
Igny8_Logger::info("{$log_prefix} Post Status: {$wp_status}");
|
||||||
|
Igny8_Logger::separator();
|
||||||
|
|
||||||
return $post_id;
|
return $post_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user