fixing issues of integration with wordpress plugin
This commit is contained in:
@@ -78,6 +78,7 @@ class SiteSerializer(serializers.ModelSerializer):
|
||||
'industry', 'industry_name', 'industry_slug',
|
||||
'is_active', 'status',
|
||||
'site_type', 'hosting_type', 'seo_metadata',
|
||||
'wp_api_key', # WordPress API key (single source of truth for integration)
|
||||
'sectors_count', 'active_sectors_count', 'selected_sectors',
|
||||
'can_add_sectors', 'keywords_count', 'has_integration',
|
||||
'created_at', 'updated_at'
|
||||
@@ -86,6 +87,7 @@ class SiteSerializer(serializers.ModelSerializer):
|
||||
# Explicitly specify required fields for clarity
|
||||
extra_kwargs = {
|
||||
'industry': {'required': True, 'error_messages': {'required': 'Industry is required when creating a site.'}},
|
||||
'wp_api_key': {'read_only': True}, # Only set via generate-api-key endpoint
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -217,6 +217,7 @@ class IntegrationService:
|
||||
dict: Connection test result with detailed health status
|
||||
"""
|
||||
import requests
|
||||
from django.utils import timezone
|
||||
|
||||
config = integration.config_json
|
||||
|
||||
@@ -324,13 +325,6 @@ class IntegrationService:
|
||||
health_checks['plugin_has_api_key']
|
||||
)
|
||||
|
||||
# Save site_url to config if successful and not already set
|
||||
if is_healthy and not config.get('site_url'):
|
||||
config['site_url'] = site_url
|
||||
integration.config_json = config
|
||||
integration.save(update_fields=['config_json'])
|
||||
logger.info(f"[IntegrationService] Saved site_url to integration {integration.id} config: {site_url}")
|
||||
|
||||
# Build response message
|
||||
if is_healthy:
|
||||
message = "✅ WordPress integration is connected and authenticated via API key"
|
||||
@@ -347,6 +341,28 @@ class IntegrationService:
|
||||
else:
|
||||
message = "❌ WordPress connection failed"
|
||||
|
||||
# Update integration status based on connection test result
|
||||
if is_healthy:
|
||||
integration.sync_status = 'success'
|
||||
integration.sync_error = None
|
||||
integration.last_sync_at = timezone.now()
|
||||
logger.info(f"[IntegrationService] Connection test passed, set sync_status to 'success' for integration {integration.id}")
|
||||
else:
|
||||
integration.sync_status = 'failed'
|
||||
integration.sync_error = message
|
||||
logger.warning(f"[IntegrationService] Connection test failed, set sync_status to 'failed' for integration {integration.id}")
|
||||
|
||||
# Save site_url to config if successful and not already set
|
||||
if is_healthy and not config.get('site_url'):
|
||||
config['site_url'] = site_url
|
||||
integration.config_json = config
|
||||
|
||||
# Save all changes to integration
|
||||
integration.save()
|
||||
|
||||
if is_healthy and not config.get('site_url'):
|
||||
logger.info(f"[IntegrationService] Saved site_url to integration {integration.id} config: {site_url}")
|
||||
|
||||
return {
|
||||
'success': is_healthy,
|
||||
'fully_functional': is_healthy,
|
||||
|
||||
@@ -127,33 +127,22 @@ class PublisherService:
|
||||
# Get destination config
|
||||
destination_config = {}
|
||||
|
||||
# If content has site, try to get integration config
|
||||
# Get WordPress config directly from Site model (no SiteIntegration needed)
|
||||
if hasattr(content, 'site') and content.site:
|
||||
from igny8_core.business.integration.models import SiteIntegration
|
||||
logger.info(f"[PublisherService._publish_to_destination] 🔍 Looking for integration: site={content.site.name}, platform={destination}")
|
||||
integration = SiteIntegration.objects.filter(
|
||||
site=content.site,
|
||||
platform=destination,
|
||||
is_active=True
|
||||
).first()
|
||||
site = content.site
|
||||
logger.info(f"[PublisherService._publish_to_destination] 🔍 Getting config from site: {site.name}")
|
||||
|
||||
if integration:
|
||||
logger.info(f"[PublisherService._publish_to_destination] ✅ Integration found: id={integration.id}")
|
||||
# Merge config_json (site_url, etc.)
|
||||
destination_config.update(integration.config_json or {})
|
||||
|
||||
# API key is stored in Site.wp_api_key (SINGLE source of truth)
|
||||
if integration.site.wp_api_key:
|
||||
destination_config['api_key'] = integration.site.wp_api_key
|
||||
|
||||
logger.info(f"[PublisherService._publish_to_destination] 🔑 Config merged: has_api_key={bool(destination_config.get('api_key'))}, has_site_url={bool(destination_config.get('site_url'))}")
|
||||
|
||||
# Ensure site_url is set (from config or from site model)
|
||||
if not destination_config.get('site_url'):
|
||||
destination_config['site_url'] = content.site.url
|
||||
logger.info(f"[PublisherService._publish_to_destination] 🌐 Using site.url: {content.site.url}")
|
||||
# API key is stored in Site.wp_api_key (SINGLE source of truth)
|
||||
if site.wp_api_key:
|
||||
destination_config['api_key'] = site.wp_api_key
|
||||
logger.info(f"[PublisherService._publish_to_destination] 🔑 API key found on site")
|
||||
else:
|
||||
logger.warning(f"[PublisherService._publish_to_destination] ⚠️ No integration found for site={content.site.name}, platform={destination}")
|
||||
logger.error(f"[PublisherService._publish_to_destination] ❌ No API key found on site {site.name}")
|
||||
raise ValueError(f"WordPress API key not configured for site {site.name}. Please generate an API key in Site Settings.")
|
||||
|
||||
# Use Site.domain as site_url
|
||||
destination_config['site_url'] = site.domain or site.url
|
||||
logger.info(f"[PublisherService._publish_to_destination] 🌐 Using site URL: {destination_config['site_url']}")
|
||||
|
||||
# Publish via adapter
|
||||
logger.info(f"[PublisherService._publish_to_destination] 🚀 Calling adapter.publish() with config keys: {list(destination_config.keys())}")
|
||||
|
||||
@@ -12,6 +12,7 @@ from igny8_core.api.base import SiteSectorModelViewSet
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.auth.models import Site
|
||||
from igny8_core.business.integration.models import SiteIntegration
|
||||
from igny8_core.business.integration.services.integration_service import IntegrationService
|
||||
from igny8_core.business.integration.services.sync_service import SyncService
|
||||
@@ -131,19 +132,23 @@ class IntegrationViewSet(SiteSectorModelViewSet):
|
||||
permission_classes=[AllowAny], throttle_classes=[NoThrottle])
|
||||
def test_connection_collection(self, request):
|
||||
"""
|
||||
Collection-level test connection endpoint for frontend convenience.
|
||||
Test WordPress connection using Site.wp_api_key (single source of truth).
|
||||
|
||||
POST /api/v1/integration/integrations/test-connection/
|
||||
|
||||
Body:
|
||||
{
|
||||
"site_id": 123,
|
||||
"api_key": "...",
|
||||
"site_url": "https://example.com"
|
||||
"site_id": 123
|
||||
}
|
||||
|
||||
Tests:
|
||||
1. WordPress site is reachable
|
||||
2. IGNY8 plugin is installed
|
||||
3. Plugin has API key configured (matching Site.wp_api_key)
|
||||
"""
|
||||
import requests as http_requests
|
||||
|
||||
site_id = request.data.get('site_id')
|
||||
api_key = request.data.get('api_key')
|
||||
site_url = request.data.get('site_url')
|
||||
|
||||
if not site_id:
|
||||
return error_response('site_id is required', None, status.HTTP_400_BAD_REQUEST, request)
|
||||
@@ -155,80 +160,146 @@ class IntegrationViewSet(SiteSectorModelViewSet):
|
||||
except (Site.DoesNotExist, ValueError, TypeError):
|
||||
return error_response('Site not found or invalid', None, status.HTTP_404_NOT_FOUND, request)
|
||||
|
||||
# Authentication: accept either authenticated user OR matching API key in body
|
||||
api_key = request.data.get('api_key') or api_key
|
||||
authenticated = False
|
||||
# If request has a valid user and belongs to same account, allow
|
||||
if hasattr(request, 'user') and getattr(request.user, 'is_authenticated', False):
|
||||
try:
|
||||
# If user has account, ensure site belongs to user's account
|
||||
if site.account == request.user.account:
|
||||
authenticated = True
|
||||
except Exception:
|
||||
# Ignore and fallback to api_key check
|
||||
pass
|
||||
# Authentication: user must be authenticated and belong to same account
|
||||
if not hasattr(request, 'user') or not getattr(request.user, 'is_authenticated', False):
|
||||
return error_response('Authentication required', None, status.HTTP_403_FORBIDDEN, request)
|
||||
|
||||
try:
|
||||
if site.account != request.user.account:
|
||||
return error_response('Site does not belong to your account', None, status.HTTP_403_FORBIDDEN, request)
|
||||
except Exception:
|
||||
return error_response('Authentication failed', None, status.HTTP_403_FORBIDDEN, request)
|
||||
|
||||
# If not authenticated via session, allow if provided api_key matches site's stored wp_api_key
|
||||
if not authenticated:
|
||||
stored_key = getattr(site, 'wp_api_key', None)
|
||||
if stored_key and api_key and str(api_key) == str(stored_key):
|
||||
authenticated = True
|
||||
elif not stored_key:
|
||||
# API key not set on site - provide helpful error message
|
||||
return error_response(
|
||||
'API key not configured for this site. Please generate an API key in the IGNY8 app and ensure it is saved to the site.',
|
||||
None,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
request
|
||||
)
|
||||
elif api_key and stored_key and str(api_key) != str(stored_key):
|
||||
# API key provided but doesn't match
|
||||
return error_response(
|
||||
'Invalid API key. The provided API key does not match the one stored for this site.',
|
||||
None,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
request
|
||||
)
|
||||
|
||||
if not authenticated:
|
||||
return error_response('Authentication credentials were not provided.', None, status.HTTP_403_FORBIDDEN, request)
|
||||
|
||||
# Try to find an existing integration for this site+platform
|
||||
integration = SiteIntegration.objects.filter(site=site, platform='wordpress').first()
|
||||
|
||||
# If not found, create and save the integration to database (for status tracking, not credentials)
|
||||
integration_created = False
|
||||
if not integration:
|
||||
integration = SiteIntegration.objects.create(
|
||||
account=site.account,
|
||||
site=site,
|
||||
platform='wordpress',
|
||||
platform_type='cms',
|
||||
config_json={'site_url': site_url} if site_url else {},
|
||||
credentials_json={}, # API key is stored in Site.wp_api_key, not here
|
||||
is_active=True,
|
||||
sync_enabled=True
|
||||
# Get stored API key from Site model (single source of truth)
|
||||
stored_api_key = site.wp_api_key
|
||||
if not stored_api_key:
|
||||
return error_response(
|
||||
'API key not configured. Please generate an API key first.',
|
||||
None,
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
request
|
||||
)
|
||||
integration_created = True
|
||||
logger.info(f"[IntegrationViewSet] Created WordPress integration {integration.id} for site {site.id}")
|
||||
|
||||
service = IntegrationService()
|
||||
# Mark this as initial connection test since API key was provided in request body
|
||||
# This allows the test to pass even if WordPress plugin hasn't stored the key yet
|
||||
is_initial_connection = bool(api_key and request.data.get('api_key'))
|
||||
result = service._test_wordpress_connection(integration, is_initial_connection=is_initial_connection)
|
||||
# Get site URL
|
||||
site_url = site.domain or site.url
|
||||
if not site_url:
|
||||
return error_response(
|
||||
'Site URL not configured',
|
||||
None,
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
request
|
||||
)
|
||||
|
||||
if result.get('success'):
|
||||
# Include integration_id in response so plugin can store it
|
||||
result['integration_id'] = integration.id
|
||||
result['integration_created'] = integration_created
|
||||
return success_response(result, request=request)
|
||||
else:
|
||||
# If test failed and we just created integration, delete it
|
||||
if integration_created:
|
||||
integration.delete()
|
||||
logger.info(f"[IntegrationViewSet] Deleted integration {integration.id} due to failed connection test")
|
||||
return error_response(result.get('message', 'Connection test failed'), None, status.HTTP_400_BAD_REQUEST, request)
|
||||
# Health check results
|
||||
health_checks = {
|
||||
'site_url_configured': True,
|
||||
'api_key_configured': True,
|
||||
'wp_rest_api_reachable': False,
|
||||
'plugin_installed': False,
|
||||
'plugin_has_api_key': False,
|
||||
'api_key_verified': False, # NEW: Verifies IGNY8 and WordPress have SAME key
|
||||
}
|
||||
issues = []
|
||||
|
||||
try:
|
||||
# Check 1: WordPress REST API reachable
|
||||
try:
|
||||
rest_response = http_requests.get(
|
||||
f"{site_url.rstrip('/')}/wp-json/",
|
||||
timeout=10
|
||||
)
|
||||
if rest_response.status_code == 200:
|
||||
health_checks['wp_rest_api_reachable'] = True
|
||||
else:
|
||||
issues.append(f"WordPress REST API not reachable: HTTP {rest_response.status_code}")
|
||||
except Exception as e:
|
||||
issues.append(f"WordPress REST API unreachable: {str(e)}")
|
||||
|
||||
# Check 2: IGNY8 Plugin installed (public status endpoint)
|
||||
try:
|
||||
status_response = http_requests.get(
|
||||
f"{site_url.rstrip('/')}/wp-json/igny8/v1/status",
|
||||
timeout=10
|
||||
)
|
||||
if status_response.status_code == 200:
|
||||
health_checks['plugin_installed'] = True
|
||||
|
||||
status_data = status_response.json()
|
||||
plugin_data = status_data.get('data', status_data)
|
||||
|
||||
if plugin_data.get('connected') or plugin_data.get('has_api_key'):
|
||||
health_checks['plugin_has_api_key'] = True
|
||||
else:
|
||||
issues.append("Plugin installed but no API key configured in WordPress")
|
||||
else:
|
||||
issues.append(f"IGNY8 plugin not found: HTTP {status_response.status_code}")
|
||||
except Exception as e:
|
||||
issues.append(f"Cannot detect IGNY8 plugin: {str(e)}")
|
||||
|
||||
# Check 3: Verify API keys MATCH by making authenticated request
|
||||
# This is the CRITICAL check - WordPress must accept our API key
|
||||
if health_checks['plugin_installed'] and health_checks['plugin_has_api_key']:
|
||||
try:
|
||||
# Make authenticated request using Site.wp_api_key to dedicated verify endpoint
|
||||
verify_response = http_requests.get(
|
||||
f"{site_url.rstrip('/')}/wp-json/igny8/v1/verify-key",
|
||||
headers={
|
||||
'X-IGNY8-API-KEY': stored_api_key,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
if verify_response.status_code == 200:
|
||||
health_checks['api_key_verified'] = True
|
||||
elif verify_response.status_code in [401, 403]:
|
||||
issues.append("API key mismatch - WordPress has different key than IGNY8. Please copy the API key from IGNY8 to WordPress plugin settings.")
|
||||
else:
|
||||
issues.append(f"API key verification failed: HTTP {verify_response.status_code}")
|
||||
except Exception as e:
|
||||
issues.append(f"API key verification request failed: {str(e)}")
|
||||
|
||||
# Determine overall status - MUST include api_key_verified for true connection
|
||||
is_healthy = (
|
||||
health_checks['api_key_configured'] and
|
||||
health_checks['wp_rest_api_reachable'] and
|
||||
health_checks['plugin_installed'] and
|
||||
health_checks['plugin_has_api_key'] and
|
||||
health_checks['api_key_verified'] # CRITICAL: keys must match
|
||||
)
|
||||
|
||||
# Build message with clear guidance
|
||||
if is_healthy:
|
||||
message = "✅ WordPress integration is fully connected and verified"
|
||||
elif not health_checks['wp_rest_api_reachable']:
|
||||
message = "❌ Cannot reach WordPress site"
|
||||
elif not health_checks['plugin_installed']:
|
||||
message = "⚠️ WordPress is reachable but IGNY8 plugin not installed"
|
||||
elif not health_checks['plugin_has_api_key']:
|
||||
message = "⚠️ Plugin installed but no API key configured in WordPress"
|
||||
elif not health_checks['api_key_verified']:
|
||||
message = "⚠️ API key mismatch - copy the API key from IGNY8 to WordPress plugin"
|
||||
else:
|
||||
message = "❌ WordPress connection failed"
|
||||
|
||||
return success_response({
|
||||
'success': is_healthy,
|
||||
'message': message,
|
||||
'site_id': site.id,
|
||||
'site_name': site.name,
|
||||
'site_url': site_url,
|
||||
'api_key_configured': bool(stored_api_key),
|
||||
'health_checks': health_checks,
|
||||
'issues': issues if issues else None,
|
||||
}, request=request)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WordPress connection test failed: {e}")
|
||||
return error_response(
|
||||
f'Connection test failed: {str(e)}',
|
||||
None,
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request
|
||||
)
|
||||
|
||||
@extend_schema(tags=['Integration'])
|
||||
@action(detail=True, methods=['post'])
|
||||
@@ -808,42 +879,71 @@ class IntegrationViewSet(SiteSectorModelViewSet):
|
||||
site.wp_api_key = api_key
|
||||
site.save(update_fields=['wp_api_key'])
|
||||
|
||||
# Get or create SiteIntegration (for integration status/config, NOT credentials)
|
||||
integration, created = SiteIntegration.objects.get_or_create(
|
||||
site=site,
|
||||
platform='wordpress',
|
||||
defaults={
|
||||
'account': site.account,
|
||||
'platform': 'wordpress',
|
||||
'platform_type': 'cms',
|
||||
'is_active': True,
|
||||
'sync_enabled': True,
|
||||
'credentials_json': {}, # Empty - API key is on Site model
|
||||
'config_json': {}
|
||||
}
|
||||
)
|
||||
|
||||
# If integration already exists, just ensure it's active
|
||||
if not created:
|
||||
integration.is_active = True
|
||||
integration.sync_enabled = True
|
||||
# Clear any old credentials_json API key (migrate to Site.wp_api_key)
|
||||
if integration.credentials_json.get('api_key'):
|
||||
integration.credentials_json = {}
|
||||
integration.save()
|
||||
|
||||
logger.info(
|
||||
f"Generated new API key for site {site.name} (ID: {site_id}), "
|
||||
f"stored in Site.wp_api_key (single source of truth)"
|
||||
)
|
||||
|
||||
# Serialize the integration with the new key
|
||||
serializer = self.get_serializer(integration)
|
||||
return success_response({
|
||||
'api_key': api_key,
|
||||
'site_id': site.id,
|
||||
'site_name': site.name,
|
||||
'site_url': site.domain or site.url,
|
||||
'message': 'API key generated successfully. WordPress integration is ready.',
|
||||
}, request=request)
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='revoke-api-key')
|
||||
def revoke_api_key(self, request):
|
||||
"""
|
||||
Revoke (delete) the API key for a site's WordPress integration.
|
||||
|
||||
POST /api/v1/integration/integrations/revoke-api-key/
|
||||
|
||||
Body:
|
||||
{
|
||||
"site_id": 5
|
||||
}
|
||||
"""
|
||||
site_id = request.data.get('site_id')
|
||||
if not site_id:
|
||||
return error_response(
|
||||
'Site ID is required',
|
||||
None,
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
request
|
||||
)
|
||||
|
||||
try:
|
||||
site = Site.objects.get(id=site_id)
|
||||
except Site.DoesNotExist:
|
||||
return error_response(
|
||||
f'Site with ID {site_id} not found',
|
||||
None,
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
request
|
||||
)
|
||||
|
||||
# Verify user has access to this site
|
||||
if site.account != request.user.account:
|
||||
return error_response(
|
||||
'You do not have permission to modify this site',
|
||||
None,
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
request
|
||||
)
|
||||
|
||||
# SINGLE SOURCE OF TRUTH: Remove API key from Site.wp_api_key
|
||||
site.wp_api_key = None
|
||||
site.save(update_fields=['wp_api_key'])
|
||||
|
||||
logger.info(
|
||||
f"Revoked API key for site {site.name} (ID: {site_id})"
|
||||
)
|
||||
|
||||
return success_response({
|
||||
'integration': serializer.data,
|
||||
'api_key': api_key,
|
||||
'message': f"API key {'generated' if created else 'regenerated'} successfully",
|
||||
'site_id': site.id,
|
||||
'site_name': site.name,
|
||||
'message': 'API key revoked successfully. WordPress integration is now disconnected.',
|
||||
}, request=request)
|
||||
|
||||
|
||||
|
||||
@@ -429,11 +429,11 @@ class ContentResource(resources.ModelResource):
|
||||
@admin.register(Content)
|
||||
class ContentAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = ContentResource
|
||||
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'word_count', 'get_taxonomy_count', 'created_at']
|
||||
list_filter = ['status', 'content_type', 'content_structure', 'source', 'site', 'sector', 'cluster', 'word_count', 'created_at']
|
||||
list_display = ['title', 'content_type', 'content_structure', 'site', 'sector', 'source', 'status', 'site_status', 'scheduled_publish_at', 'word_count', 'get_taxonomy_count', 'created_at']
|
||||
list_filter = ['status', 'site_status', 'content_type', 'content_structure', 'source', 'site', 'sector', 'cluster', 'word_count', 'created_at']
|
||||
search_fields = ['title', 'content_html', 'external_url', 'meta_title', 'primary_keyword']
|
||||
ordering = ['-created_at']
|
||||
readonly_fields = ['created_at', 'updated_at', 'word_count', 'get_tags_display', 'get_categories_display']
|
||||
readonly_fields = ['created_at', 'updated_at', 'word_count', 'site_status_updated_at', 'get_tags_display', 'get_categories_display']
|
||||
autocomplete_fields = ['cluster', 'site', 'sector']
|
||||
inlines = [ContentTaxonomyInline]
|
||||
actions = [
|
||||
@@ -449,6 +449,10 @@ class ContentAdmin(ImportExportMixin, SiteSectorAdminMixin, Igny8ModelAdmin):
|
||||
('Basic Info', {
|
||||
'fields': ('title', 'site', 'sector', 'cluster', 'status')
|
||||
}),
|
||||
('Publishing Status', {
|
||||
'fields': ('site_status', 'scheduled_publish_at', 'site_status_updated_at'),
|
||||
'description': 'WordPress/external site publishing status. Managed by automated publishing scheduler.'
|
||||
}),
|
||||
('Content Classification', {
|
||||
'fields': ('content_type', 'content_structure', 'source')
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Generated manually on 2026-01-12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def add_wordpress_plugin(apps, schema_editor):
|
||||
"""
|
||||
Add the IGNY8 WordPress Bridge plugin to the database.
|
||||
"""
|
||||
Plugin = apps.get_model('plugins', 'Plugin')
|
||||
PluginVersion = apps.get_model('plugins', 'PluginVersion')
|
||||
|
||||
# Create or update the WordPress plugin
|
||||
plugin, created = Plugin.objects.get_or_create(
|
||||
slug='igny8-wp-bridge',
|
||||
defaults={
|
||||
'name': 'IGNY8 WordPress Bridge',
|
||||
'platform': 'wordpress',
|
||||
'description': 'Connect your WordPress site to IGNY8 for AI-powered content publishing, SEO optimization, and seamless automation. Features API key authentication, automated updates, advanced template rendering, and webhook sync.',
|
||||
'homepage_url': 'https://igny8.com/docs/wordpress-integration',
|
||||
'is_active': True,
|
||||
}
|
||||
)
|
||||
|
||||
if not created:
|
||||
# Update existing plugin with latest information
|
||||
plugin.name = 'IGNY8 WordPress Bridge'
|
||||
plugin.description = 'Connect your WordPress site to IGNY8 for AI-powered content publishing, SEO optimization, and seamless automation. Features API key authentication, automated updates, advanced template rendering, and webhook sync.'
|
||||
plugin.homepage_url = 'https://igny8.com/docs/wordpress-integration'
|
||||
plugin.is_active = True
|
||||
plugin.save()
|
||||
|
||||
# Add current version (1.3.4) if it doesn't exist
|
||||
version, created = PluginVersion.objects.get_or_create(
|
||||
plugin=plugin,
|
||||
version='1.3.4',
|
||||
defaults={
|
||||
'version_code': 10304, # 1.03.04
|
||||
'status': 'released',
|
||||
'changelog': '''## Version 1.3.4 (January 12, 2026)
|
||||
|
||||
### Major Changes
|
||||
- **API Key Authentication Only**: Removed username/password authentication
|
||||
- **Simplified Integration**: Single API key for all communication
|
||||
- **Bearer Token Auth**: Uses `Authorization: Bearer {api_key}` header
|
||||
- **Webhooks Deprecated**: Removed webhook signature validation
|
||||
|
||||
### Authentication
|
||||
- API key stored in WordPress options table: `igny8_api_key`
|
||||
- Accepts both `X-IGNY8-API-KEY` header and `Authorization: Bearer` header
|
||||
- Single source of truth: Site.wp_api_key in IGNY8 backend
|
||||
|
||||
### Technical Improvements
|
||||
- Streamlined connection test endpoint
|
||||
- Improved error handling and validation
|
||||
- Better health check reporting
|
||||
- Enhanced auto-update mechanism
|
||||
|
||||
### Backward Compatibility
|
||||
- Legacy username/password fields removed
|
||||
- No migration needed for existing installations
|
||||
- API key authentication works with all IGNY8 API versions 1.0+
|
||||
''',
|
||||
'min_api_version': '1.0',
|
||||
'min_platform_version': '5.6',
|
||||
'min_php_version': '7.4',
|
||||
'file_path': 'plugins/wordpress/dist/igny8-wp-bridge-1.3.4.zip',
|
||||
'file_size': 0, # Will be updated when file is generated
|
||||
'checksum': '', # Will be updated when file is generated
|
||||
'force_update': False,
|
||||
}
|
||||
)
|
||||
|
||||
if created:
|
||||
print(f"✅ Created WordPress plugin version 1.3.4")
|
||||
else:
|
||||
print(f"ℹ️ WordPress plugin version 1.3.4 already exists")
|
||||
|
||||
|
||||
def remove_wordpress_plugin(apps, schema_editor):
|
||||
"""
|
||||
Reverse migration - remove the WordPress plugin.
|
||||
This is optional and can be commented out if you want to keep the data.
|
||||
"""
|
||||
Plugin = apps.get_model('plugins', 'Plugin')
|
||||
Plugin.objects.filter(slug='igny8-wp-bridge').delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('plugins', '0003_simplify_status_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_wordpress_plugin, remove_wordpress_plugin),
|
||||
]
|
||||
@@ -255,7 +255,7 @@ def process_scheduled_publications() -> Dict[str, Any]:
|
||||
due_content = Content.objects.filter(
|
||||
site_status='scheduled',
|
||||
scheduled_publish_at__lte=now
|
||||
).select_related('site', 'task')
|
||||
).select_related('site', 'sector', 'cluster')
|
||||
|
||||
for content in due_content:
|
||||
results['processed'] += 1
|
||||
@@ -284,11 +284,9 @@ def process_scheduled_publications() -> Dict[str, Any]:
|
||||
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
|
||||
site_integration_id=site_integration.id
|
||||
)
|
||||
|
||||
logger.info(f"Queued content {content.id} for WordPress publishing")
|
||||
|
||||
Reference in New Issue
Block a user