pubslihign and scheduling plan updated
This commit is contained in:
@@ -1518,6 +1518,77 @@ class ContentViewSet(SiteSectorModelViewSet):
|
|||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='reschedule', url_name='reschedule', permission_classes=[IsAuthenticatedAndActive, IsEditorOrAbove])
|
||||||
|
def reschedule(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
Reschedule failed or published content for republishing.
|
||||||
|
Updates scheduled_publish_at and sets site_status back to 'scheduled'.
|
||||||
|
|
||||||
|
POST /api/v1/writer/content/{id}/reschedule/
|
||||||
|
{
|
||||||
|
"scheduled_at": "2026-01-20T14:00:00Z" // ISO 8601 datetime
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
from django.utils import timezone
|
||||||
|
from dateutil import parser
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
content = self.get_object()
|
||||||
|
|
||||||
|
# Get scheduled time from request
|
||||||
|
scheduled_at_str = request.data.get('scheduled_at')
|
||||||
|
if not scheduled_at_str:
|
||||||
|
return error_response(
|
||||||
|
error='scheduled_at is required (ISO 8601 datetime)',
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse datetime
|
||||||
|
try:
|
||||||
|
scheduled_at = parser.isoparse(scheduled_at_str)
|
||||||
|
if scheduled_at.tzinfo is None:
|
||||||
|
scheduled_at = timezone.make_aware(scheduled_at)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
return error_response(
|
||||||
|
error=f'Invalid datetime format: {str(e)}. Use ISO 8601 format.',
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure datetime is in the future
|
||||||
|
if scheduled_at <= timezone.now():
|
||||||
|
return error_response(
|
||||||
|
error='Scheduled time must be in the future',
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store old values for logging
|
||||||
|
old_status = content.site_status
|
||||||
|
old_scheduled_at = content.scheduled_publish_at
|
||||||
|
|
||||||
|
# Update content - allow rescheduling from any state
|
||||||
|
content.site_status = 'scheduled'
|
||||||
|
content.scheduled_publish_at = scheduled_at
|
||||||
|
content.site_status_updated_at = timezone.now()
|
||||||
|
content.save(update_fields=['site_status', 'scheduled_publish_at', 'site_status_updated_at', 'updated_at'])
|
||||||
|
|
||||||
|
logger.info(f"[ContentViewSet.reschedule] Content {content.id} rescheduled from {old_status} (was {old_scheduled_at}) to {scheduled_at}")
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
data={
|
||||||
|
'content_id': content.id,
|
||||||
|
'site_status': content.site_status,
|
||||||
|
'scheduled_publish_at': content.scheduled_publish_at.isoformat(),
|
||||||
|
'previous_status': old_status,
|
||||||
|
'was_scheduled_for': old_scheduled_at.isoformat() if old_scheduled_at else None,
|
||||||
|
},
|
||||||
|
message=f'Content rescheduled for {scheduled_at.strftime("%Y-%m-%d %H:%M")}',
|
||||||
|
request=request
|
||||||
|
)
|
||||||
|
|
||||||
@action(detail=False, methods=['post'], url_path='generate_image_prompts', url_name='generate_image_prompts')
|
@action(detail=False, methods=['post'], url_path='generate_image_prompts', url_name='generate_image_prompts')
|
||||||
def generate_image_prompts(self, request):
|
def generate_image_prompts(self, request):
|
||||||
"""Generate image prompts for content records - same pattern as other AI functions"""
|
"""Generate image prompts for content records - same pattern as other AI functions"""
|
||||||
|
|||||||
@@ -232,14 +232,15 @@ def process_scheduled_publications() -> Dict[str, Any]:
|
|||||||
- site_status = 'scheduled'
|
- site_status = 'scheduled'
|
||||||
- scheduled_publish_at <= now
|
- scheduled_publish_at <= now
|
||||||
|
|
||||||
For each, triggers the WordPress publishing task.
|
For each, triggers publishing via PublisherService (current system).
|
||||||
|
|
||||||
|
UPDATED: Uses Site.wp_api_key directly (no SiteIntegration needed).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with processing results
|
Dict with processing results
|
||||||
"""
|
"""
|
||||||
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.publishing.services.publisher_service import PublisherService
|
||||||
from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
|
|
||||||
|
|
||||||
results = {
|
results = {
|
||||||
'processed': 0,
|
'processed': 0,
|
||||||
@@ -249,33 +250,25 @@ def process_scheduled_publications() -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
publisher_service = PublisherService()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get all scheduled content that's due
|
# Get all scheduled content that's due
|
||||||
due_content = Content.objects.filter(
|
due_content = Content.objects.filter(
|
||||||
site_status='scheduled',
|
site_status='scheduled',
|
||||||
scheduled_publish_at__lte=now
|
scheduled_publish_at__lte=now
|
||||||
).select_related('site', 'sector', 'cluster')
|
).select_related('site', 'sector', 'cluster', 'account')
|
||||||
|
|
||||||
|
logger.info(f"[process_scheduled_publications] Found {due_content.count()} content items due for publishing")
|
||||||
|
|
||||||
for content in due_content:
|
for content in due_content:
|
||||||
results['processed'] += 1
|
results['processed'] += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Update status to publishing
|
# Validate prerequisites
|
||||||
content.site_status = 'publishing'
|
if not content.site:
|
||||||
content.site_status_updated_at = timezone.now()
|
error_msg = f"Content {content.id} has no site assigned"
|
||||||
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
logger.error(f"[process_scheduled_publications] {error_msg}")
|
||||||
|
|
||||||
# Get site integration
|
|
||||||
site_integration = SiteIntegration.objects.filter(
|
|
||||||
site=content.site,
|
|
||||||
platform='wordpress',
|
|
||||||
is_active=True
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not site_integration:
|
|
||||||
error_msg = f"No active WordPress integration for site {content.site_id}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
content.site_status = 'failed'
|
content.site_status = 'failed'
|
||||||
content.site_status_updated_at = timezone.now()
|
content.site_status_updated_at = timezone.now()
|
||||||
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
||||||
@@ -283,30 +276,67 @@ def process_scheduled_publications() -> Dict[str, Any]:
|
|||||||
results['errors'].append(error_msg)
|
results['errors'].append(error_msg)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Queue the WordPress publishing task
|
# Check WordPress configuration on Site
|
||||||
publish_content_to_wordpress.delay(
|
if not content.site.wp_api_key:
|
||||||
|
error_msg = f"Site '{content.site.name}' (ID: {content.site.id}) has no WordPress API key configured"
|
||||||
|
logger.error(f"[process_scheduled_publications] {error_msg}")
|
||||||
|
content.site_status = 'failed'
|
||||||
|
content.site_status_updated_at = timezone.now()
|
||||||
|
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
||||||
|
results['failed'] += 1
|
||||||
|
results['errors'].append(error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not content.site.domain:
|
||||||
|
error_msg = f"Site '{content.site.name}' (ID: {content.site.id}) has no domain configured"
|
||||||
|
logger.error(f"[process_scheduled_publications] {error_msg}")
|
||||||
|
content.site_status = 'failed'
|
||||||
|
content.site_status_updated_at = timezone.now()
|
||||||
|
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
||||||
|
results['failed'] += 1
|
||||||
|
results['errors'].append(error_msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update status to publishing
|
||||||
|
content.site_status = 'publishing'
|
||||||
|
content.site_status_updated_at = timezone.now()
|
||||||
|
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
||||||
|
|
||||||
|
# Publish via PublisherService (current system)
|
||||||
|
logger.info(f"[process_scheduled_publications] Publishing content {content.id} '{content.title}' to {content.site.domain}")
|
||||||
|
publish_result = publisher_service.publish_content(
|
||||||
content_id=content.id,
|
content_id=content.id,
|
||||||
site_integration_id=site_integration.id
|
destinations=['wordpress'],
|
||||||
|
account=content.account
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Queued content {content.id} for WordPress publishing")
|
if publish_result.get('success'):
|
||||||
results['published'] += 1
|
logger.info(f"[process_scheduled_publications] ✅ Successfully published content {content.id}")
|
||||||
|
results['published'] += 1
|
||||||
|
else:
|
||||||
|
error_msg = f"Publishing failed for content {content.id}: {publish_result.get('error', 'Unknown error')}"
|
||||||
|
logger.error(f"[process_scheduled_publications] ❌ {error_msg}")
|
||||||
|
content.site_status = 'failed'
|
||||||
|
content.site_status_updated_at = timezone.now()
|
||||||
|
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
||||||
|
results['failed'] += 1
|
||||||
|
results['errors'].append(error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Error processing content {content.id}: {str(e)}"
|
error_msg = f"Error processing content {content.id}: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(f"[process_scheduled_publications] ❌ {error_msg}", exc_info=True)
|
||||||
content.site_status = 'failed'
|
content.site_status = 'failed'
|
||||||
content.site_status_updated_at = timezone.now()
|
content.site_status_updated_at = timezone.now()
|
||||||
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
content.save(update_fields=['site_status', 'site_status_updated_at'])
|
||||||
results['failed'] += 1
|
results['failed'] += 1
|
||||||
results['errors'].append(error_msg)
|
results['errors'].append(error_msg)
|
||||||
|
|
||||||
logger.info(f"Processing completed: {results['published']}/{results['processed']} published successfully")
|
logger.info(f"[process_scheduled_publications] ✅ Completed: {results['published']}/{results['processed']} published successfully, {results['failed']} failed")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Fatal error in process_scheduled_publications: {str(e)}"
|
error_msg = f"Fatal error in process_scheduled_publications: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(f"[process_scheduled_publications] ❌ {error_msg}", exc_info=True)
|
||||||
results['errors'].append(error_msg)
|
results['errors'].append(error_msg)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Scheduled Content Publishing Workflow
|
# Scheduled Content Publishing Workflow
|
||||||
|
|
||||||
**Last Updated:** January 12, 2026
|
**Last Updated:** January 16, 2026
|
||||||
**Module:** Publishing / Automation
|
**Module:** Publishing / Automation
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -9,6 +9,12 @@
|
|||||||
|
|
||||||
IGNY8 provides automated content publishing to WordPress sites. Content goes through a scheduling process before being published at the designated time.
|
IGNY8 provides automated content publishing to WordPress sites. Content goes through a scheduling process before being published at the designated time.
|
||||||
|
|
||||||
|
**Current System (v2.0):**
|
||||||
|
- WordPress credentials stored directly on `Site` model (`wp_api_key`, `domain`)
|
||||||
|
- No `SiteIntegration` model required
|
||||||
|
- Publishing via `PublisherService` (not legacy Celery tasks)
|
||||||
|
- API endpoint: `POST /api/v1/publisher/publish`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Content Lifecycle for Publishing
|
## Content Lifecycle for Publishing
|
||||||
@@ -115,34 +121,38 @@ Content has **TWO separate status fields**:
|
|||||||
- `site_status='scheduled'`
|
- `site_status='scheduled'`
|
||||||
- `scheduled_publish_at <= now`
|
- `scheduled_publish_at <= now`
|
||||||
2. For each content item:
|
2. For each content item:
|
||||||
|
- Validates WordPress configuration exists on Site (`wp_api_key`, `domain`)
|
||||||
- Updates `site_status='publishing'`
|
- Updates `site_status='publishing'`
|
||||||
- Gets the site's WordPress integration
|
- Calls `PublisherService.publish_content()` directly (synchronous)
|
||||||
- Queues `publish_content_to_wordpress` Celery task
|
- Updates `site_status='published'` on success or `'failed'` on error
|
||||||
3. Logs results and any errors
|
3. Logs results and any errors
|
||||||
|
|
||||||
|
**Current Implementation (v2.0):**
|
||||||
|
- Uses `PublisherService` (current system)
|
||||||
|
- Gets config from `Site.wp_api_key` and `Site.domain`
|
||||||
|
- No `SiteIntegration` required
|
||||||
|
- Synchronous publishing (not queued to Celery)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. `publish_content_to_wordpress`
|
### 3. `PublisherService.publish_content`
|
||||||
|
|
||||||
**Type:** On-demand Celery task (queued by `process_scheduled_publications`)
|
**Type:** Service method (called by scheduler)
|
||||||
**Task Name:** `publishing.publish_content_to_wordpress`
|
**File:** `backend/igny8_core/business/publishing/services/publisher_service.py`
|
||||||
**File:** `backend/igny8_core/tasks/wordpress_publishing.py`
|
|
||||||
|
|
||||||
#### What It Does:
|
#### What It Does:
|
||||||
1. **Load Content & Integration** - Gets content and WordPress credentials
|
1. **Load Content** - Gets content by ID
|
||||||
2. **Check Already Published** - Skips if `external_id` exists
|
2. **Get WordPress Config** - Reads from `Site.wp_api_key` and `Site.domain`
|
||||||
3. **Generate Excerpt** - Creates excerpt from HTML content
|
3. **Call Adapter** - Uses `WordPressAdapter` to handle API communication
|
||||||
4. **Get Taxonomy Terms** - Loads categories and tags from `ContentTaxonomy`
|
4. **WordPress API Call** - POSTs to `{domain}/wp-json/igny8/v1/publish`
|
||||||
5. **Get Images** - Loads featured image and gallery images
|
5. **Update Content** - Sets `external_id`, `external_url`, `site_status='published'`
|
||||||
6. **Build API Payload** - Constructs WordPress REST API payload
|
6. **Create Record** - Logs in `PublishingRecord` model
|
||||||
7. **Call WordPress API** - POSTs to WordPress via IGNY8 Bridge plugin
|
|
||||||
8. **Update Content** - Sets `external_id`, `external_url`, `site_status='published'`
|
|
||||||
9. **Log Sync Event** - Records in `SyncEvent` model
|
|
||||||
|
|
||||||
#### WordPress Connection:
|
#### WordPress Connection:
|
||||||
- Uses the IGNY8 WordPress Bridge plugin installed on the site
|
- Uses the IGNY8 WordPress Bridge plugin installed on the site
|
||||||
- API endpoint: `{site_url}/wp-json/igny8-bridge/v1/publish`
|
- API endpoint: `{site.domain}/wp-json/igny8/v1/publish`
|
||||||
- Authentication: API key stored in `Site.wp_api_key`
|
- Authentication: API key from `Site.wp_api_key`
|
||||||
|
- **No SiteIntegration model needed**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -201,15 +211,39 @@ app.conf.beat_schedule = {
|
|||||||
|
|
||||||
## Manual Publishing
|
## Manual Publishing
|
||||||
|
|
||||||
Content can also be published immediately via:
|
Content can be published immediately or rescheduled via API:
|
||||||
|
|
||||||
### API Endpoint
|
### Publish Now
|
||||||
```
|
```
|
||||||
POST /api/v1/content/{content_id}/publish/
|
POST /api/v1/publisher/publish
|
||||||
|
{
|
||||||
|
"content_id": 123,
|
||||||
|
"destinations": ["wordpress"]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Admin Action
|
### Schedule for Later
|
||||||
In Django Admin, select content and use "Publish to WordPress" action.
|
```
|
||||||
|
POST /api/v1/writer/content/{id}/schedule/
|
||||||
|
{
|
||||||
|
"scheduled_at": "2026-01-20T14:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reschedule Failed Content
|
||||||
|
```
|
||||||
|
POST /api/v1/writer/content/{id}/reschedule/
|
||||||
|
{
|
||||||
|
"scheduled_at": "2026-01-20T15:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Reschedule works from any `site_status` (failed, published, scheduled, etc.)
|
||||||
|
|
||||||
|
### Unschedule
|
||||||
|
```
|
||||||
|
POST /api/v1/writer/content/{id}/unschedule/
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -267,16 +301,40 @@ process_scheduled_publications()
|
|||||||
|
|
||||||
| Error | Cause | Solution |
|
| Error | Cause | Solution |
|
||||||
|-------|-------|----------|
|
|-------|-------|----------|
|
||||||
| No active WordPress integration | Site doesn't have WordPress connected | Configure integration in Site settings |
|
| No WordPress API key | Site.wp_api_key is empty | Configure API key in Site settings |
|
||||||
|
| No domain configured | Site.domain is empty | Set domain in Site settings |
|
||||||
| API key invalid/expired | WordPress API key issue | Regenerate API key in WordPress plugin |
|
| API key invalid/expired | WordPress API key issue | Regenerate API key in WordPress plugin |
|
||||||
| Connection timeout | WordPress site unreachable | Check site availability |
|
| Connection timeout | WordPress site unreachable | Check site availability |
|
||||||
| Plugin not active | IGNY8 Bridge plugin disabled | Enable plugin in WordPress |
|
| Plugin not active | IGNY8 Bridge plugin disabled | Enable plugin in WordPress |
|
||||||
| Content already published | Duplicate publish attempt | Check `external_id` field |
|
| Content already published | Duplicate publish attempt | Use reschedule to republish |
|
||||||
|
|
||||||
### Retry Policy
|
### Retry Policy
|
||||||
- `publish_content_to_wordpress` has `max_retries=3`
|
|
||||||
- Automatic retry on transient failures
|
|
||||||
- Failed content marked with `site_status='failed'`
|
- Failed content marked with `site_status='failed'`
|
||||||
|
- Use the `reschedule` action to retry publishing
|
||||||
|
- Can reschedule from any status (failed, published, etc.)
|
||||||
|
|
||||||
|
### Rescheduling Failed Content
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Via API
|
||||||
|
POST /api/v1/writer/content/{id}/reschedule/
|
||||||
|
{
|
||||||
|
"scheduled_at": "2026-01-20T15:00:00Z"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Via Django shell
|
||||||
|
from igny8_core.business.content.models import Content
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
failed_content = Content.objects.filter(site_status='failed')
|
||||||
|
for content in failed_content:
|
||||||
|
# Reschedule for 1 hour from now
|
||||||
|
content.site_status = 'scheduled'
|
||||||
|
content.scheduled_publish_at = timezone.now() + timedelta(hours=1)
|
||||||
|
content.site_status_updated_at = timezone.now()
|
||||||
|
content.save(update_fields=['site_status', 'scheduled_publish_at', 'site_status_updated_at'])
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -294,19 +352,37 @@ process_scheduled_publications()
|
|||||||
1. Check Celery Beat is running: `docker compose logs igny8_celery_beat`
|
1. Check Celery Beat is running: `docker compose logs igny8_celery_beat`
|
||||||
2. Check Celery Worker is running: `docker compose logs igny8_celery_worker`
|
2. Check Celery Worker is running: `docker compose logs igny8_celery_worker`
|
||||||
3. Look for errors in worker logs
|
3. Look for errors in worker logs
|
||||||
4. Verify WordPress integration is active
|
4. Verify Site has `wp_api_key` configured
|
||||||
5. Test WordPress API connectivity
|
5. Verify Site has `domain` configured
|
||||||
|
6. Test WordPress API connectivity
|
||||||
|
|
||||||
### Resetting Failed Content
|
### Resetting Failed Content
|
||||||
|
|
||||||
```python
|
Use the reschedule API endpoint:
|
||||||
# Reset failed content to try again
|
|
||||||
from igny8_core.business.content.models import Content
|
|
||||||
|
|
||||||
Content.objects.filter(site_status='failed').update(
|
```bash
|
||||||
site_status='not_published',
|
# Reschedule single content
|
||||||
scheduled_publish_at=None
|
curl -X POST https://api.igny8.com/api/v1/writer/content/344/reschedule/ \
|
||||||
)
|
-H "Authorization: Api-Key YOUR_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"scheduled_at": "2026-01-20T15:00:00Z"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via Django shell:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Reset all failed content to reschedule in 1 hour
|
||||||
|
from igny8_core.business.content.models import Content
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
failed_content = Content.objects.filter(site_status='failed')
|
||||||
|
for content in failed_content:
|
||||||
|
content.site_status = 'scheduled'
|
||||||
|
content.scheduled_publish_at = timezone.now() + timedelta(hours=1)
|
||||||
|
content.site_status_updated_at = timezone.now()
|
||||||
|
content.save(update_fields=['site_status', 'scheduled_publish_at', 'site_status_updated_at'])
|
||||||
|
print(f"Rescheduled content {content.id}")
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -318,29 +394,12 @@ Content.objects.filter(site_status='failed').update(
|
|||||||
│ IGNY8 Backend │
|
│ IGNY8 Backend │
|
||||||
├─────────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────────┤
|
||||||
│ │
|
│ │
|
||||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
│ ┌───────Validate Site.wp_api_key & domain │ │
|
||||||
│ │ Celery Beat │ │ Celery Worker │ │
|
│ │ - Call PublisherService.publish_content() │ │
|
||||||
│ │ │ │ │ │
|
|
||||||
│ │ Sends tasks at │───▶│ Executes tasks │ │
|
|
||||||
│ │ scheduled times │ │ │ │
|
|
||||||
│ └──────────────────┘ └────────┬─────────┘ │
|
|
||||||
│ │ │
|
|
||||||
│ ▼ │
|
|
||||||
│ ┌────────────────────────────────────────────────────────┐ │
|
|
||||||
│ │ Publishing Tasks │ │
|
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ 1. schedule_approved_content (hourly) │ │
|
│ │ 3. PublisherService │ │
|
||||||
│ │ - Find approved content │ │
|
│ │ - Get config from Site model │ │
|
||||||
│ │ - Calculate publish slots │ │
|
│ │ - Call WordPressAdapter │ │
|
||||||
│ │ - Set scheduled_publish_at │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ 2. process_scheduled_publications (every 5 min) │ │
|
|
||||||
│ │ - Find due content │ │
|
|
||||||
│ │ - Queue publish_content_to_wordpress │ │
|
|
||||||
│ │ │ │
|
|
||||||
│ │ 3. publish_content_to_wordpress │ │
|
|
||||||
│ │ - Build API payload │ │
|
|
||||||
│ │ - Call WordPress REST API │ │
|
|
||||||
│ │ - Update content status │ │
|
│ │ - Update content status │ │
|
||||||
│ └────────────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
@@ -353,6 +412,68 @@ Content.objects.filter(site_status='failed').update(
|
|||||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
│ │ IGNY8 Bridge Plugin │ │
|
│ │ IGNY8 Bridge Plugin │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
|
│ │ /wp-json/igny8/v1/publish │ │
|
||||||
|
│ │ - Receives content payload │ │
|
||||||
|
│ │ - Creates/updates WordPress post │ │
|
||||||
|
│ │ - Handles images, categories, tags │ │
|
||||||
|
│ │ - Returns post ID and URL │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Schedule Content
|
||||||
|
```http
|
||||||
|
POST /api/v1/writer/content/{id}/schedule/
|
||||||
|
Authorization: Api-Key YOUR_KEY
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"scheduled_at": "2026-01-20T14:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reschedule Content (Failed or Any Status)
|
||||||
|
```http
|
||||||
|
POST /api/v1/writer/content/{id}/reschedule/
|
||||||
|
Authorization: Api-Key YOUR_KEY
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"scheduled_at": "2026-01-20T15:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unschedule Content
|
||||||
|
```http
|
||||||
|
POST /api/v1/writer/content/{id}/unschedule/
|
||||||
|
Authorization: Api-Key YOUR_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Publish Immediately
|
||||||
|
```http
|
||||||
|
POST /api/v1/publisher/publish
|
||||||
|
Authorization: Api-Key YOUR_KEY
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"content_id": 123,
|
||||||
|
"destinations": ["wordpress"]
|
||||||
|
}│
|
||||||
|
│ └────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
└───────────────────────────────────┼─────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼ HTTPS
|
||||||
|
┌───────────────────────────────────────────────────────────────┐
|
||||||
|
│ WordPress Site │
|
||||||
|
├───────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ IGNY8 Bridge Plugin │ │
|
||||||
|
│ │ │ │
|
||||||
│ │ /wp-json/igny8-bridge/v1/publish │ │
|
│ │ /wp-json/igny8-bridge/v1/publish │ │
|
||||||
│ │ - Receives content payload │ │
|
│ │ - Receives content payload │ │
|
||||||
│ │ - Creates/updates WordPress post │ │
|
│ │ - Creates/updates WordPress post │ │
|
||||||
|
|||||||
1595
docs/plans/PUBLISHING-PROGRESS-AND-SCHEDULING-UX-PLAN.md
Normal file
1595
docs/plans/PUBLISHING-PROGRESS-AND-SCHEDULING-UX-PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user