Section 3-8 - #MIgration Runs -

Multiple Migfeat: Update publishing terminology and add publishing settings

- Changed references from "WordPress" to "Site" across multiple components for consistency.
- Introduced a new "Publishing" tab in Site Settings to manage automatic content approval and publishing behavior.
- Added publishing settings model to the backend with fields for auto-approval, auto-publish, and publishing limits.
- Implemented Celery tasks for scheduling and processing automated content publishing.
- Enhanced Writer Dashboard to include metrics for content published to the site and scheduled for publishing.
This commit is contained in:
IGNY8 VPS (Salman)
2026-01-01 07:10:03 +00:00
parent f81fffc9a6
commit 0340016932
21 changed files with 1200 additions and 36 deletions

View File

@@ -1476,11 +1476,36 @@ class AutomationService:
This stage automatically approves content in 'review' status and
marks it as 'approved' (ready for publishing to WordPress).
Respects PublishingSettings:
- If auto_approval_enabled is False, skip approval and keep content in 'review'
"""
stage_number = 7
stage_name = "Review → Approved"
start_time = time.time()
# Check publishing settings for auto-approval
from igny8_core.business.integration.models import PublishingSettings
publishing_settings, _ = PublishingSettings.get_or_create_for_site(self.site)
if not publishing_settings.auto_approval_enabled:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, "Auto-approval is disabled for this site - skipping Stage 7"
)
self.run.stage_7_result = {
'ready_for_review': 0,
'approved_count': 0,
'content_ids': [],
'skipped': True,
'reason': 'auto_approval_disabled'
}
self.run.status = 'completed'
self.run.completed_at = datetime.now()
self.run.save()
cache.delete(f'automation_lock_{self.site.id}')
return
# Query content ready for review
ready_for_review = Content.objects.filter(
site=self.site,
@@ -1602,13 +1627,55 @@ class AutomationService:
stage_number, approved_count, time_elapsed, 0
)
# Check if auto-publish is enabled and queue approved content for publishing
published_count = 0
if publishing_settings.auto_publish_enabled and approved_count > 0:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, f"Auto-publish enabled - queuing {len(content_ids)} content items for publishing"
)
# Get WordPress integration for this site
from igny8_core.business.integration.models import SiteIntegration
wp_integration = SiteIntegration.objects.filter(
site=self.site,
platform='wordpress',
is_active=True
).first()
if wp_integration:
from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
for content_id in content_ids:
try:
# Queue publish task
publish_content_to_wordpress.delay(
content_id=content_id,
site_integration_id=wp_integration.id
)
published_count += 1
except Exception as e:
logger.error(f"[AutomationService] Failed to queue publish for content {content_id}: {str(e)}")
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, f"Queued {published_count} content items for WordPress publishing"
)
else:
self.logger.log_stage_progress(
self.run.run_id, self.account.id, self.site.id,
stage_number, "No active WordPress integration found - skipping auto-publish"
)
self.run.stage_7_result = {
'ready_for_review': total_count,
'review_total': total_count,
'approved_count': approved_count,
'content_ids': content_ids,
'time_elapsed': time_elapsed,
'in_progress': False
'in_progress': False,
'auto_published_count': published_count if publishing_settings.auto_publish_enabled else 0,
'auto_publish_enabled': publishing_settings.auto_publish_enabled,
}
self.run.status = 'completed'
self.run.completed_at = datetime.now()
@@ -1617,7 +1684,8 @@ class AutomationService:
# Release lock
cache.delete(f'automation_lock_{self.site.id}')
logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved (ready for publishing)")
logger.info(f"[AutomationService] Stage 7 complete: {approved_count} content pieces approved" +
(f", {published_count} queued for publishing" if published_count > 0 else " (ready for publishing)"))
def pause_automation(self):
"""Pause current automation run"""

View File

@@ -282,6 +282,33 @@ class Content(SoftDeletableModel, SiteSectorBaseModel):
help_text="Content status"
)
# Publishing scheduler fields
SITE_STATUS_CHOICES = [
('not_published', 'Not Published'),
('scheduled', 'Scheduled'),
('publishing', 'Publishing'),
('published', 'Published'),
('failed', 'Failed'),
]
site_status = models.CharField(
max_length=50,
choices=SITE_STATUS_CHOICES,
default='not_published',
db_index=True,
help_text="External site publishing status"
)
scheduled_publish_at = models.DateTimeField(
null=True,
blank=True,
db_index=True,
help_text="Scheduled time for publishing to external site"
)
site_status_updated_at = models.DateTimeField(
null=True,
blank=True,
help_text="Last time site_status was changed"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.9 on 2026-01-01 06:37
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0018_add_country_remove_intent_seedkeyword'),
('integration', '0002_add_sync_event_model'),
]
operations = [
migrations.CreateModel(
name='PublishingSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('auto_approval_enabled', models.BooleanField(default=True, help_text="Automatically approve content after review (moves to 'approved' status)")),
('auto_publish_enabled', models.BooleanField(default=True, help_text='Automatically publish approved content to the external site')),
('daily_publish_limit', models.PositiveIntegerField(default=3, help_text='Maximum number of articles to publish per day', validators=[django.core.validators.MinValueValidator(1)])),
('weekly_publish_limit', models.PositiveIntegerField(default=15, help_text='Maximum number of articles to publish per week', validators=[django.core.validators.MinValueValidator(1)])),
('monthly_publish_limit', models.PositiveIntegerField(default=50, help_text='Maximum number of articles to publish per month', validators=[django.core.validators.MinValueValidator(1)])),
('publish_days', models.JSONField(default=list, help_text='Days of the week to publish (mon, tue, wed, thu, fri, sat, sun)')),
('publish_time_slots', models.JSONField(default=list, help_text="Times of day to publish (HH:MM format, e.g., ['09:00', '14:00', '18:00'])")),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
('site', models.OneToOneField(help_text='Site these publishing settings belong to', on_delete=django.db.models.deletion.CASCADE, related_name='publishing_settings', to='igny8_core_auth.site')),
],
options={
'verbose_name': 'Publishing Settings',
'verbose_name_plural': 'Publishing Settings',
'db_table': 'igny8_publishing_settings',
},
),
]

View File

@@ -244,3 +244,100 @@ class SyncEvent(AccountBaseModel):
def __str__(self):
return f"{self.get_event_type_display()} - {self.description[:50]}"
class PublishingSettings(AccountBaseModel):
"""
Site-level publishing configuration settings.
Controls automatic approval, publishing limits, and scheduling.
"""
DEFAULT_PUBLISH_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
DEFAULT_TIME_SLOTS = ['09:00', '14:00', '18:00']
site = models.OneToOneField(
'igny8_core_auth.Site',
on_delete=models.CASCADE,
related_name='publishing_settings',
help_text="Site these publishing settings belong to"
)
# Auto-approval settings
auto_approval_enabled = models.BooleanField(
default=True,
help_text="Automatically approve content after review (moves to 'approved' status)"
)
# Auto-publish settings
auto_publish_enabled = models.BooleanField(
default=True,
help_text="Automatically publish approved content to the external site"
)
# Publishing limits
daily_publish_limit = models.PositiveIntegerField(
default=3,
validators=[MinValueValidator(1)],
help_text="Maximum number of articles to publish per day"
)
weekly_publish_limit = models.PositiveIntegerField(
default=15,
validators=[MinValueValidator(1)],
help_text="Maximum number of articles to publish per week"
)
monthly_publish_limit = models.PositiveIntegerField(
default=50,
validators=[MinValueValidator(1)],
help_text="Maximum number of articles to publish per month"
)
# Publishing schedule
publish_days = models.JSONField(
default=list,
help_text="Days of the week to publish (mon, tue, wed, thu, fri, sat, sun)"
)
publish_time_slots = models.JSONField(
default=list,
help_text="Times of day to publish (HH:MM format, e.g., ['09:00', '14:00', '18:00'])"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'integration'
db_table = 'igny8_publishing_settings'
verbose_name = 'Publishing Settings'
verbose_name_plural = 'Publishing Settings'
def __str__(self):
return f"Publishing Settings for {self.site.name}"
def save(self, *args, **kwargs):
"""Set defaults for JSON fields if empty"""
if not self.publish_days:
self.publish_days = self.DEFAULT_PUBLISH_DAYS
if not self.publish_time_slots:
self.publish_time_slots = self.DEFAULT_TIME_SLOTS
super().save(*args, **kwargs)
@classmethod
def get_or_create_for_site(cls, site):
"""Get or create publishing settings for a site with defaults"""
settings, created = cls.objects.get_or_create(
site=site,
defaults={
'account': site.account,
'auto_approval_enabled': True,
'auto_publish_enabled': True,
'daily_publish_limit': 3,
'weekly_publish_limit': 15,
'monthly_publish_limit': 50,
'publish_days': cls.DEFAULT_PUBLISH_DAYS,
'publish_time_slots': cls.DEFAULT_TIME_SLOTS,
}
)
return settings, created

View File

@@ -19,6 +19,9 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
# Load task modules from all registered Django apps.
app.autodiscover_tasks()
# Explicitly import tasks from igny8_core/tasks directory
app.autodiscover_tasks(['igny8_core.tasks'])
# Celery Beat schedule for periodic tasks
app.conf.beat_schedule = {
'replenish-monthly-credits': {
@@ -39,6 +42,15 @@ app.conf.beat_schedule = {
'task': 'automation.check_scheduled_automations',
'schedule': crontab(minute=0), # Every hour at :00
},
# Publishing Scheduler Tasks
'schedule-approved-content': {
'task': 'publishing.schedule_approved_content',
'schedule': crontab(minute=0), # Every hour at :00
},
'process-scheduled-publications': {
'task': 'publishing.process_scheduled_publications',
'schedule': crontab(minute='*/5'), # Every 5 minutes
},
# Maintenance: purge expired soft-deleted records daily at 3:15 AM
'purge-soft-deleted-records': {
'task': 'igny8_core.purge_soft_deleted',

View File

@@ -5,7 +5,7 @@ Phase 6: Site Integration & Multi-Destination Publishing
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from igny8_core.modules.integration.views import IntegrationViewSet
from igny8_core.modules.integration.views import IntegrationViewSet, PublishingSettingsViewSet
from igny8_core.modules.integration.webhooks import (
wordpress_status_webhook,
wordpress_metadata_webhook,
@@ -14,9 +14,19 @@ from igny8_core.modules.integration.webhooks import (
router = DefaultRouter()
router.register(r'integrations', IntegrationViewSet, basename='integration')
# Create PublishingSettings ViewSet instance
publishing_settings_viewset = PublishingSettingsViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
})
urlpatterns = [
path('', include(router.urls)),
# Site-level publishing settings
path('sites/<int:site_id>/publishing-settings/', publishing_settings_viewset, name='publishing-settings'),
# Webhook endpoints
path('webhooks/wordpress/status/', wordpress_status_webhook, name='wordpress-status-webhook'),
path('webhooks/wordpress/metadata/', wordpress_metadata_webhook, name='wordpress-metadata-webhook'),

View File

@@ -838,5 +838,148 @@ class IntegrationViewSet(SiteSectorModelViewSet):
}, request=request)
# PublishingSettings ViewSet
from rest_framework import serializers, viewsets
from igny8_core.business.integration.models import PublishingSettings
class PublishingSettingsSerializer(serializers.ModelSerializer):
"""Serializer for PublishingSettings model"""
class Meta:
model = PublishingSettings
fields = [
'id',
'site',
'auto_approval_enabled',
'auto_publish_enabled',
'daily_publish_limit',
'weekly_publish_limit',
'monthly_publish_limit',
'publish_days',
'publish_time_slots',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'site', 'created_at', 'updated_at']
@extend_schema_view(
retrieve=extend_schema(tags=['Integration']),
update=extend_schema(tags=['Integration']),
partial_update=extend_schema(tags=['Integration']),
)
class PublishingSettingsViewSet(viewsets.ViewSet):
"""
ViewSet for managing site-level publishing settings.
GET /api/v1/integration/sites/{site_id}/publishing-settings/
PUT /api/v1/integration/sites/{site_id}/publishing-settings/
PATCH /api/v1/integration/sites/{site_id}/publishing-settings/
"""
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'integration'
throttle_classes = [DebugScopedRateThrottle]
def _get_site(self, site_id, request):
"""Get site and verify user has access"""
from igny8_core.auth.models import Site
try:
site = Site.objects.get(id=int(site_id))
# Check if user has access to this site (same account)
if hasattr(request, 'account') and site.account != request.account:
return None
return site
except (Site.DoesNotExist, ValueError, TypeError):
return None
@extend_schema(tags=['Integration'])
def retrieve(self, request, site_id=None):
"""
Get publishing settings for a site.
Creates default settings if they don't exist.
"""
site = self._get_site(site_id, request)
if not site:
return error_response(
'Site not found or access denied',
None,
status.HTTP_404_NOT_FOUND,
request
)
# Get or create settings with defaults
settings, created = PublishingSettings.get_or_create_for_site(site)
serializer = PublishingSettingsSerializer(settings)
return success_response(
data=serializer.data,
message='Publishing settings retrieved' + (' (created with defaults)' if created else ''),
request=request
)
@extend_schema(tags=['Integration'])
def update(self, request, site_id=None):
"""
Update publishing settings for a site (full update).
"""
site = self._get_site(site_id, request)
if not site:
return error_response(
'Site not found or access denied',
None,
status.HTTP_404_NOT_FOUND,
request
)
# Get or create settings
settings, _ = PublishingSettings.get_or_create_for_site(site)
serializer = PublishingSettingsSerializer(settings, data=request.data)
if serializer.is_valid():
serializer.save()
return success_response(
data=serializer.data,
message='Publishing settings updated',
request=request
)
return error_response(
'Validation failed',
serializer.errors,
status.HTTP_400_BAD_REQUEST,
request
)
@extend_schema(tags=['Integration'])
def partial_update(self, request, site_id=None):
"""
Partially update publishing settings for a site.
"""
site = self._get_site(site_id, request)
if not site:
return error_response(
'Site not found or access denied',
None,
status.HTTP_404_NOT_FOUND,
request
)
# Get or create settings
settings, _ = PublishingSettings.get_or_create_for_site(site)
serializer = PublishingSettingsSerializer(settings, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return success_response(
data=serializer.data,
message='Publishing settings updated',
request=request
)
return error_response(
'Validation failed',
serializer.errors,
status.HTTP_400_BAD_REQUEST,
request
)

View File

@@ -0,0 +1,49 @@
# Generated migration for publishing scheduler fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('writer', '0014_add_approved_status'),
]
operations = [
migrations.AddField(
model_name='content',
name='site_status',
field=models.CharField(
choices=[
('not_published', 'Not Published'),
('scheduled', 'Scheduled'),
('publishing', 'Publishing'),
('published', 'Published'),
('failed', 'Failed'),
],
db_index=True,
default='not_published',
help_text='External site publishing status',
max_length=50,
),
),
migrations.AddField(
model_name='content',
name='scheduled_publish_at',
field=models.DateTimeField(
blank=True,
db_index=True,
help_text='Scheduled time for publishing to external site',
null=True,
),
),
migrations.AddField(
model_name='content',
name='site_status_updated_at',
field=models.DateTimeField(
blank=True,
help_text='Last time site_status was changed',
null=True,
),
),
]

View File

@@ -186,6 +186,9 @@ class ContentSerializer(serializers.ModelSerializer):
'external_url',
'source',
'status',
'site_status',
'scheduled_publish_at',
'site_status_updated_at',
'word_count',
'sector_name',
'site_id',
@@ -197,7 +200,7 @@ class ContentSerializer(serializers.ModelSerializer):
'created_at',
'updated_at',
]
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id', 'site_status', 'scheduled_publish_at', 'site_status_updated_at']
def validate(self, attrs):
"""Ensure required fields for Content creation"""

View File

@@ -0,0 +1,11 @@
"""
IGNY8 Celery Tasks
This module contains all Celery background tasks for the application.
"""
# Import all task modules to ensure they're registered with Celery
from igny8_core.tasks.plan_limits import *
from igny8_core.tasks.backup import *
from igny8_core.tasks.wordpress_publishing import *
from igny8_core.tasks.publishing_scheduler import *

View File

@@ -0,0 +1,361 @@
"""
IGNY8 Publishing Scheduler Tasks
Celery tasks for scheduling and processing automated content publishing:
1. schedule_approved_content: Runs hourly - schedules approved content for publishing
2. process_scheduled_publications: Runs every 5 minutes - processes scheduled content
"""
from celery import shared_task
from django.utils import timezone
from datetime import datetime, timedelta
import logging
from typing import Dict, Any
logger = logging.getLogger(__name__)
@shared_task(name='publishing.schedule_approved_content')
def schedule_approved_content() -> Dict[str, Any]:
"""
Hourly task that schedules approved content for publishing based on PublishingSettings.
For each site with PublishingSettings.auto_publish_enabled:
1. Gets all content with status='approved' and site_status='not_published'
2. Calculates next available publish slots based on:
- publish_days (which days are allowed)
- publish_time_slots (which times are allowed)
- daily/weekly/monthly limits
3. Sets scheduled_publish_at and site_status='scheduled'
Returns:
Dict with scheduling results per site
"""
from igny8_core.business.content.models import Content
from igny8_core.business.integration.models import PublishingSettings
from igny8_core.auth.models import Site
results = {
'sites_processed': 0,
'content_scheduled': 0,
'errors': [],
'details': []
}
try:
# Get all sites with auto_publish enabled
sites_with_settings = PublishingSettings.objects.filter(auto_publish_enabled=True)
for settings in sites_with_settings:
site = settings.site
site_result = {
'site_id': site.id,
'site_name': site.name,
'scheduled_count': 0
}
try:
# Get approved content that's not yet scheduled
pending_content = Content.objects.filter(
site=site,
status='approved',
site_status='not_published',
scheduled_publish_at__isnull=True
).order_by('created_at')
if not pending_content.exists():
logger.debug(f"Site {site.id}: No pending content to schedule")
results['details'].append(site_result)
results['sites_processed'] += 1
continue
# Calculate available slots
available_slots = _calculate_available_slots(settings, site)
# Assign slots to content
for i, content in enumerate(pending_content):
if i >= len(available_slots):
logger.info(f"Site {site.id}: No more slots available (limit reached)")
break
scheduled_time = available_slots[i]
content.scheduled_publish_at = scheduled_time
content.site_status = 'scheduled'
content.site_status_updated_at = timezone.now()
content.save(update_fields=['scheduled_publish_at', 'site_status', 'site_status_updated_at'])
site_result['scheduled_count'] += 1
results['content_scheduled'] += 1
logger.info(f"Scheduled content {content.id} for {scheduled_time}")
results['details'].append(site_result)
results['sites_processed'] += 1
except Exception as e:
error_msg = f"Error processing site {site.id}: {str(e)}"
logger.error(error_msg)
results['errors'].append(error_msg)
logger.info(f"Publishing scheduler completed: {results['content_scheduled']} content items scheduled across {results['sites_processed']} sites")
return results
except Exception as e:
error_msg = f"Fatal error in schedule_approved_content: {str(e)}"
logger.error(error_msg)
results['errors'].append(error_msg)
return results
def _calculate_available_slots(settings: 'PublishingSettings', site: 'Site') -> list:
"""
Calculate available publishing time slots based on settings and limits.
Args:
settings: PublishingSettings instance
site: Site instance
Returns:
List of datetime objects representing available slots
"""
from igny8_core.business.content.models import Content
now = timezone.now()
slots = []
# Get configured days and times
publish_days = settings.publish_days or ['mon', 'tue', 'wed', 'thu', 'fri']
publish_times = settings.publish_time_slots or ['09:00', '14:00', '18:00']
# Day name mapping
day_map = {
'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3,
'fri': 4, 'sat': 5, 'sun': 6
}
allowed_days = [day_map.get(d.lower(), -1) for d in publish_days]
allowed_days = [d for d in allowed_days if d >= 0]
# Parse time slots
time_slots = []
for time_str in publish_times:
try:
hour, minute = map(int, time_str.split(':'))
time_slots.append((hour, minute))
except (ValueError, AttributeError):
continue
if not time_slots:
time_slots = [(9, 0), (14, 0), (18, 0)]
# Calculate limits
daily_limit = settings.daily_publish_limit
weekly_limit = settings.weekly_publish_limit
monthly_limit = settings.monthly_publish_limit
# Count existing scheduled/published content
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
week_start = today_start - timedelta(days=now.weekday())
month_start = today_start.replace(day=1)
daily_count = Content.objects.filter(
site=site,
site_status__in=['scheduled', 'publishing', 'published'],
scheduled_publish_at__gte=today_start
).count()
weekly_count = Content.objects.filter(
site=site,
site_status__in=['scheduled', 'publishing', 'published'],
scheduled_publish_at__gte=week_start
).count()
monthly_count = Content.objects.filter(
site=site,
site_status__in=['scheduled', 'publishing', 'published'],
scheduled_publish_at__gte=month_start
).count()
# Generate slots for next 30 days
current_date = now.date()
slots_per_day = {} # Track slots used per day
for day_offset in range(30):
check_date = current_date + timedelta(days=day_offset)
# Check if day is allowed
if check_date.weekday() not in allowed_days:
continue
for hour, minute in time_slots:
slot_time = timezone.make_aware(
datetime.combine(check_date, datetime.min.time().replace(hour=hour, minute=minute))
)
# Skip if in the past
if slot_time <= now:
continue
# Check daily limit
day_key = check_date.isoformat()
slots_this_day = slots_per_day.get(day_key, 0)
if daily_limit and (daily_count + slots_this_day) >= daily_limit:
continue
# Check weekly limit
slot_week_start = slot_time - timedelta(days=slot_time.weekday())
if slot_week_start.date() == week_start.date():
scheduled_in_week = weekly_count + len([s for s in slots if s >= week_start])
if weekly_limit and scheduled_in_week >= weekly_limit:
continue
# Check monthly limit
if slot_time.month == now.month and slot_time.year == now.year:
scheduled_in_month = monthly_count + len([s for s in slots if s.month == now.month])
if monthly_limit and scheduled_in_month >= monthly_limit:
continue
slots.append(slot_time)
slots_per_day[day_key] = slots_per_day.get(day_key, 0) + 1
# Limit total slots to prevent memory issues
if len(slots) >= 100:
return slots
return slots
@shared_task(name='publishing.process_scheduled_publications')
def process_scheduled_publications() -> Dict[str, Any]:
"""
Every 5 minutes: Process content scheduled for publishing.
Finds all content where:
- site_status = 'scheduled'
- scheduled_publish_at <= now
For each, triggers the WordPress publishing task.
Returns:
Dict with processing results
"""
from igny8_core.business.content.models import Content
from igny8_core.business.integration.models import SiteIntegration
from igny8_core.tasks.wordpress_publishing import publish_content_to_wordpress
results = {
'processed': 0,
'published': 0,
'failed': 0,
'errors': []
}
now = timezone.now()
try:
# Get all scheduled content that's due
due_content = Content.objects.filter(
site_status='scheduled',
scheduled_publish_at__lte=now
).select_related('site', 'task')
for content in due_content:
results['processed'] += 1
try:
# 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'])
# 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_updated_at = timezone.now()
content.save(update_fields=['site_status', 'site_status_updated_at'])
results['failed'] += 1
results['errors'].append(error_msg)
continue
# Queue the WordPress publishing task
task_id = content.task_id if hasattr(content, 'task') and content.task else None
publish_content_to_wordpress.delay(
content_id=content.id,
site_integration_id=site_integration.id,
task_id=task_id
)
logger.info(f"Queued content {content.id} for WordPress publishing")
results['published'] += 1
except Exception as e:
error_msg = f"Error processing content {content.id}: {str(e)}"
logger.error(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)
logger.info(f"Processing completed: {results['published']}/{results['processed']} published successfully")
return results
except Exception as e:
error_msg = f"Fatal error in process_scheduled_publications: {str(e)}"
logger.error(error_msg)
results['errors'].append(error_msg)
return results
@shared_task(name='publishing.update_content_site_status')
def update_content_site_status(content_id: int, new_status: str, external_id: str = None, external_url: str = None) -> Dict[str, Any]:
"""
Update content site_status after WordPress publishing completes.
Called by the WordPress publishing task upon success or failure.
Args:
content_id: Content ID to update
new_status: New site_status ('published' or 'failed')
external_id: WordPress post ID (on success)
external_url: WordPress post URL (on success)
Returns:
Dict with update result
"""
from igny8_core.business.content.models import Content
try:
content = Content.objects.get(id=content_id)
content.site_status = new_status
content.site_status_updated_at = timezone.now()
if external_id:
content.external_id = external_id
if external_url:
content.external_url = external_url
update_fields = ['site_status', 'site_status_updated_at']
if external_id:
update_fields.append('external_id')
if external_url:
update_fields.append('external_url')
content.save(update_fields=update_fields)
logger.info(f"Updated content {content_id} site_status to {new_status}")
return {'success': True, 'content_id': content_id, 'new_status': new_status}
except Content.DoesNotExist:
error_msg = f"Content {content_id} not found"
logger.error(error_msg)
return {'success': False, 'error': error_msg}
except Exception as e:
error_msg = f"Error updating content {content_id}: {str(e)}"
logger.error(error_msg)
return {'success': False, 'error': error_msg}

View File

@@ -354,12 +354,14 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
content.external_id = str(wp_data.get('post_id'))
content.external_url = wp_data.get('post_url')
content.status = 'published'
content.site_status = 'published'
content.site_status_updated_at = timezone.now()
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'])
content.save(update_fields=['external_id', 'external_url', 'status', 'site_status', 'site_status_updated_at', 'metadata', 'updated_at'])
publish_logger.info(f" {log_prefix} ✅ Content model updated:")
publish_logger.info(f" {log_prefix} External ID: {content.external_id}")
@@ -425,12 +427,14 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
content.external_id = str(wp_data.get('post_id'))
content.external_url = wp_data.get('post_url')
content.status = 'published'
content.site_status = 'published'
content.site_status_updated_at = timezone.now()
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'])
content.save(update_fields=['external_id', 'external_url', 'status', 'site_status', 'site_status_updated_at', 'metadata', 'updated_at'])
# Log sync event
duration_ms = int((time.time() - start_time) * 1000)
@@ -525,6 +529,16 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
publish_logger.error(f" {prefix} Duration: {duration_ms}ms")
publish_logger.error("="*80, exc_info=True)
# Update site_status to failed if content was loaded
try:
if content and content.id:
content.site_status = 'failed'
content.site_status_updated_at = timezone.now()
content.save(update_fields=['site_status', 'site_status_updated_at'])
publish_logger.info(f"Updated content {content.id} site_status to 'failed'")
except Exception as update_error:
publish_logger.error(f"Failed to update site_status: {str(update_error)}")
# Try to log sync event
try:
from igny8_core.business.integration.models import SyncEvent