Phase 6
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Base Adapter
|
||||
Phase 6: Site Integration & Multi-Destination Publishing
|
||||
|
||||
Abstract base class for publishing adapters.
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
|
||||
class BaseAdapter(ABC):
|
||||
"""
|
||||
Abstract base class for publishing adapters.
|
||||
All platform-specific adapters must inherit from this.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def publish(
|
||||
self,
|
||||
content: Any,
|
||||
destination_config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Publish content to destination.
|
||||
|
||||
Args:
|
||||
content: Content or SiteBlueprint to publish
|
||||
destination_config: Destination-specific configuration
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'external_id': str, # External platform ID
|
||||
'url': str, # Published content URL
|
||||
'published_at': datetime,
|
||||
'metadata': dict
|
||||
}
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def test_connection(
|
||||
self,
|
||||
config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Test connection to destination.
|
||||
|
||||
Args:
|
||||
config: Destination configuration
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'message': str,
|
||||
'details': dict
|
||||
}
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_status(
|
||||
self,
|
||||
published_id: str,
|
||||
config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get publishing status for published content.
|
||||
|
||||
Args:
|
||||
published_id: External platform ID
|
||||
config: Destination configuration
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'status': str, # 'published', 'draft', 'deleted', etc.
|
||||
'url': str,
|
||||
'updated_at': datetime,
|
||||
'metadata': dict
|
||||
}
|
||||
"""
|
||||
pass
|
||||
|
||||
def validate_config(
|
||||
self,
|
||||
config: Dict[str, Any]
|
||||
) -> tuple:
|
||||
"""
|
||||
Validate destination configuration.
|
||||
|
||||
Args:
|
||||
config: Destination configuration
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid, error_message)
|
||||
"""
|
||||
# Default implementation - can be overridden
|
||||
return True, None
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Shopify Adapter
|
||||
Phase 6: Site Integration & Multi-Destination Publishing
|
||||
|
||||
Adapter for publishing content to Shopify.
|
||||
Skeleton implementation - to be fully implemented in future.
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from igny8_core.business.publishing.services.adapters.base_adapter import BaseAdapter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ShopifyAdapter(BaseAdapter):
|
||||
"""
|
||||
Adapter for publishing content to Shopify.
|
||||
Skeleton implementation - full implementation pending.
|
||||
"""
|
||||
|
||||
def publish(
|
||||
self,
|
||||
content: Any,
|
||||
destination_config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Publish content to Shopify.
|
||||
|
||||
Args:
|
||||
content: Content instance or dict with content data
|
||||
destination_config: {
|
||||
'shop_domain': str,
|
||||
'access_token': str,
|
||||
'content_type': str, # 'page', 'blog_post', 'product'
|
||||
}
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'external_id': str, # Shopify resource ID
|
||||
'url': str, # Published resource URL
|
||||
'published_at': datetime,
|
||||
'metadata': dict
|
||||
}
|
||||
"""
|
||||
# TODO: Implement Shopify publishing
|
||||
logger.warning("[ShopifyAdapter] Shopify publishing not yet implemented")
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'external_id': None,
|
||||
'url': None,
|
||||
'published_at': None,
|
||||
'metadata': {
|
||||
'error': 'Shopify publishing not yet implemented'
|
||||
}
|
||||
}
|
||||
|
||||
def test_connection(
|
||||
self,
|
||||
config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Test connection to Shopify.
|
||||
|
||||
Args:
|
||||
config: {
|
||||
'shop_domain': str,
|
||||
'access_token': str
|
||||
}
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'message': str,
|
||||
'details': dict
|
||||
}
|
||||
"""
|
||||
# TODO: Implement Shopify connection testing
|
||||
logger.warning("[ShopifyAdapter] Shopify connection testing not yet implemented")
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'Shopify connection testing not yet implemented',
|
||||
'details': {}
|
||||
}
|
||||
|
||||
def get_status(
|
||||
self,
|
||||
published_id: str,
|
||||
config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get publishing status for published content.
|
||||
|
||||
Args:
|
||||
published_id: Shopify resource ID
|
||||
config: Shopify configuration
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'status': str, # 'published', 'draft', 'deleted', etc.
|
||||
'url': str,
|
||||
'updated_at': datetime,
|
||||
'metadata': dict
|
||||
}
|
||||
"""
|
||||
# TODO: Implement Shopify status retrieval
|
||||
logger.warning("[ShopifyAdapter] Shopify status retrieval not yet implemented")
|
||||
|
||||
return {
|
||||
'status': 'unknown',
|
||||
'url': None,
|
||||
'updated_at': None,
|
||||
'metadata': {
|
||||
'error': 'Shopify status retrieval not yet implemented'
|
||||
}
|
||||
}
|
||||
|
||||
def validate_config(
|
||||
self,
|
||||
config: Dict[str, Any]
|
||||
) -> tuple:
|
||||
"""
|
||||
Validate Shopify configuration.
|
||||
|
||||
Args:
|
||||
config: Shopify configuration
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid, error_message)
|
||||
"""
|
||||
required_fields = ['shop_domain', 'access_token']
|
||||
|
||||
for field in required_fields:
|
||||
if field not in config or not config[field]:
|
||||
return False, f"Missing required field: {field}"
|
||||
|
||||
# Validate domain format
|
||||
shop_domain = config.get('shop_domain', '')
|
||||
if not shop_domain.endswith('.myshopify.com'):
|
||||
return False, "shop_domain must end with .myshopify.com"
|
||||
|
||||
return True, None
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
"""
|
||||
WordPress Adapter
|
||||
Phase 6: Site Integration & Multi-Destination Publishing
|
||||
|
||||
Adapter for publishing content to WordPress.
|
||||
Refactored to use BaseAdapter interface while preserving existing functionality.
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from igny8_core.business.publishing.services.adapters.base_adapter import BaseAdapter
|
||||
from igny8_core.utils.wordpress import WordPressClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WordPressAdapter(BaseAdapter):
|
||||
"""
|
||||
Adapter for publishing content to WordPress.
|
||||
Uses WordPressClient internally to preserve existing functionality.
|
||||
"""
|
||||
|
||||
def publish(
|
||||
self,
|
||||
content: Any,
|
||||
destination_config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Publish content to WordPress.
|
||||
|
||||
Args:
|
||||
content: Content instance or dict with content data
|
||||
destination_config: {
|
||||
'site_url': str,
|
||||
'username': str,
|
||||
'app_password': str,
|
||||
'status': str (optional, default 'draft'),
|
||||
'featured_image_url': str (optional)
|
||||
}
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'external_id': str, # WordPress post ID
|
||||
'url': str, # Published post URL
|
||||
'published_at': datetime,
|
||||
'metadata': dict
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# Get WordPress client
|
||||
client = self._get_client(destination_config)
|
||||
|
||||
# Extract content data
|
||||
if hasattr(content, 'title') and hasattr(content, 'content'):
|
||||
# Content model instance
|
||||
title = content.title
|
||||
content_html = content.content
|
||||
elif isinstance(content, dict):
|
||||
# Dict with content data
|
||||
title = content.get('title', '')
|
||||
content_html = content.get('content', '')
|
||||
else:
|
||||
raise ValueError(f"Unsupported content type: {type(content)}")
|
||||
|
||||
# Get publishing options
|
||||
status = destination_config.get('status', 'draft')
|
||||
featured_image_url = destination_config.get('featured_image_url')
|
||||
|
||||
# Publish to WordPress
|
||||
result = client.create_post(
|
||||
title=title,
|
||||
content=content_html,
|
||||
status=status,
|
||||
featured_image_url=featured_image_url
|
||||
)
|
||||
|
||||
if result.get('success'):
|
||||
return {
|
||||
'success': True,
|
||||
'external_id': str(result.get('post_id')),
|
||||
'url': result.get('url'),
|
||||
'published_at': datetime.now(),
|
||||
'metadata': {
|
||||
'post_id': result.get('post_id'),
|
||||
'status': status
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'external_id': None,
|
||||
'url': None,
|
||||
'published_at': None,
|
||||
'metadata': {
|
||||
'error': result.get('error')
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[WordPressAdapter] Error publishing content: {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'external_id': None,
|
||||
'url': None,
|
||||
'published_at': None,
|
||||
'metadata': {
|
||||
'error': str(e)
|
||||
}
|
||||
}
|
||||
|
||||
def test_connection(
|
||||
self,
|
||||
config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Test connection to WordPress.
|
||||
|
||||
Args:
|
||||
config: {
|
||||
'site_url': str,
|
||||
'username': str,
|
||||
'app_password': str
|
||||
}
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'message': str,
|
||||
'details': dict
|
||||
}
|
||||
"""
|
||||
try:
|
||||
client = self._get_client(config)
|
||||
result = client.test_connection()
|
||||
|
||||
return {
|
||||
'success': result.get('success', False),
|
||||
'message': result.get('message', ''),
|
||||
'details': {
|
||||
'wp_version': result.get('wp_version')
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[WordPressAdapter] Connection test failed: {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'message': str(e),
|
||||
'details': {}
|
||||
}
|
||||
|
||||
def get_status(
|
||||
self,
|
||||
published_id: str,
|
||||
config: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get publishing status for published content.
|
||||
|
||||
Args:
|
||||
published_id: WordPress post ID
|
||||
config: WordPress configuration
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'status': str, # 'published', 'draft', 'deleted', etc.
|
||||
'url': str,
|
||||
'updated_at': datetime,
|
||||
'metadata': dict
|
||||
}
|
||||
"""
|
||||
try:
|
||||
client = self._get_client(config)
|
||||
|
||||
# Fetch post from WordPress
|
||||
response = client.session.get(
|
||||
f"{client.api_base}/posts/{published_id}"
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return {
|
||||
'status': data.get('status', 'unknown'),
|
||||
'url': data.get('link'),
|
||||
'updated_at': datetime.fromisoformat(
|
||||
data.get('modified', '').replace('Z', '+00:00')
|
||||
) if data.get('modified') else None,
|
||||
'metadata': {
|
||||
'title': data.get('title', {}).get('rendered'),
|
||||
'author': data.get('author'),
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'status': 'deleted' if response.status_code == 404 else 'unknown',
|
||||
'url': None,
|
||||
'updated_at': None,
|
||||
'metadata': {
|
||||
'error': f"HTTP {response.status_code}"
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[WordPressAdapter] Error getting status: {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
return {
|
||||
'status': 'unknown',
|
||||
'url': None,
|
||||
'updated_at': None,
|
||||
'metadata': {
|
||||
'error': str(e)
|
||||
}
|
||||
}
|
||||
|
||||
def validate_config(
|
||||
self,
|
||||
config: Dict[str, Any]
|
||||
) -> tuple:
|
||||
"""
|
||||
Validate WordPress configuration.
|
||||
|
||||
Args:
|
||||
config: WordPress configuration
|
||||
|
||||
Returns:
|
||||
tuple: (is_valid, error_message)
|
||||
"""
|
||||
required_fields = ['site_url']
|
||||
|
||||
for field in required_fields:
|
||||
if field not in config or not config[field]:
|
||||
return False, f"Missing required field: {field}"
|
||||
|
||||
# Validate URL format
|
||||
site_url = config.get('site_url', '')
|
||||
if not site_url.startswith(('http://', 'https://')):
|
||||
return False, "site_url must start with http:// or https://"
|
||||
|
||||
return True, None
|
||||
|
||||
def _get_client(self, config: Dict[str, Any]) -> WordPressClient:
|
||||
"""
|
||||
Get WordPress client from configuration.
|
||||
|
||||
Args:
|
||||
config: WordPress configuration
|
||||
|
||||
Returns:
|
||||
WordPressClient instance
|
||||
"""
|
||||
site_url = config.get('site_url')
|
||||
username = config.get('username')
|
||||
app_password = config.get('app_password')
|
||||
|
||||
return WordPressClient(site_url, username, app_password)
|
||||
|
||||
@@ -138,8 +138,24 @@ class PublisherService:
|
||||
if not adapter:
|
||||
raise ValueError(f"No adapter found for destination: {destination}")
|
||||
|
||||
# Get destination config (for now, basic config - can be extended)
|
||||
destination_config = {'account': account}
|
||||
|
||||
# If content has site, try to get integration config
|
||||
if hasattr(content, 'site') and content.site:
|
||||
from igny8_core.business.integration.models import SiteIntegration
|
||||
integration = SiteIntegration.objects.filter(
|
||||
site=content.site,
|
||||
platform=destination,
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
if integration:
|
||||
destination_config.update(integration.config_json)
|
||||
destination_config.update(integration.get_credentials())
|
||||
|
||||
# Publish via adapter
|
||||
result = adapter.publish(content, {'account': account})
|
||||
result = adapter.publish(content, destination_config)
|
||||
|
||||
# Update record
|
||||
record.status = 'published' if result.get('success') else 'failed'
|
||||
@@ -163,6 +179,156 @@ class PublisherService:
|
||||
record.save()
|
||||
raise
|
||||
|
||||
def publish_to_multiple_destinations(
|
||||
self,
|
||||
content: Any,
|
||||
destinations: List[Dict[str, Any]],
|
||||
account
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Publish content to multiple destinations.
|
||||
|
||||
Args:
|
||||
content: Content instance or SiteBlueprint
|
||||
destinations: List of destination configs, e.g.:
|
||||
[
|
||||
{'platform': 'wordpress', 'site_url': '...', 'username': '...', 'app_password': '...'},
|
||||
{'platform': 'sites'},
|
||||
{'platform': 'shopify', 'shop_domain': '...', 'access_token': '...'}
|
||||
]
|
||||
account: Account instance
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'success': bool,
|
||||
'results': list of publishing results per destination
|
||||
}
|
||||
"""
|
||||
results = []
|
||||
|
||||
for destination_config in destinations:
|
||||
platform = destination_config.get('platform')
|
||||
if not platform:
|
||||
results.append({
|
||||
'platform': 'unknown',
|
||||
'success': False,
|
||||
'error': 'Platform not specified'
|
||||
})
|
||||
continue
|
||||
|
||||
try:
|
||||
adapter = self._get_adapter(platform)
|
||||
if not adapter:
|
||||
results.append({
|
||||
'platform': platform,
|
||||
'success': False,
|
||||
'error': f'No adapter found for platform: {platform}'
|
||||
})
|
||||
continue
|
||||
|
||||
# Validate config
|
||||
is_valid, error_msg = adapter.validate_config(destination_config)
|
||||
if not is_valid:
|
||||
results.append({
|
||||
'platform': platform,
|
||||
'success': False,
|
||||
'error': error_msg or 'Invalid configuration'
|
||||
})
|
||||
continue
|
||||
|
||||
# Publish via adapter
|
||||
result = adapter.publish(content, destination_config)
|
||||
|
||||
# Create publishing record if content has site/sector
|
||||
if hasattr(content, 'site') and hasattr(content, 'sector'):
|
||||
record = PublishingRecord.objects.create(
|
||||
account=account,
|
||||
site=content.site,
|
||||
sector=content.sector,
|
||||
content=content if hasattr(content, 'id') and not isinstance(content, SiteBlueprint) else None,
|
||||
site_blueprint=content if isinstance(content, SiteBlueprint) else None,
|
||||
destination=platform,
|
||||
status='published' if result.get('success') else 'failed',
|
||||
destination_id=result.get('external_id'),
|
||||
destination_url=result.get('url'),
|
||||
published_at=result.get('published_at'),
|
||||
error_message=result.get('metadata', {}).get('error'),
|
||||
metadata=result.get('metadata', {})
|
||||
)
|
||||
result['publishing_record_id'] = record.id
|
||||
|
||||
results.append({
|
||||
'platform': platform,
|
||||
'success': result.get('success', False),
|
||||
'external_id': result.get('external_id'),
|
||||
'url': result.get('url'),
|
||||
'error': result.get('metadata', {}).get('error')
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error publishing to {platform}: {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
results.append({
|
||||
'platform': platform,
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
return {
|
||||
'success': all(r.get('success', False) for r in results),
|
||||
'results': results
|
||||
}
|
||||
|
||||
def publish_with_integrations(
|
||||
self,
|
||||
content: Any,
|
||||
site,
|
||||
account,
|
||||
platforms: Optional[List[str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Publish content using site integrations.
|
||||
|
||||
Args:
|
||||
content: Content instance or SiteBlueprint
|
||||
site: Site instance
|
||||
account: Account instance
|
||||
platforms: Optional list of platforms to publish to (all active if None)
|
||||
|
||||
Returns:
|
||||
dict: Publishing results
|
||||
"""
|
||||
from igny8_core.business.integration.models import SiteIntegration
|
||||
|
||||
# Get active integrations for site
|
||||
integrations = SiteIntegration.objects.filter(
|
||||
site=site,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
if platforms:
|
||||
integrations = integrations.filter(platform__in=platforms)
|
||||
|
||||
destinations = []
|
||||
for integration in integrations:
|
||||
config = integration.config_json.copy()
|
||||
credentials = integration.get_credentials()
|
||||
|
||||
destination_config = {
|
||||
'platform': integration.platform,
|
||||
**config,
|
||||
**credentials
|
||||
}
|
||||
destinations.append(destination_config)
|
||||
|
||||
# Also add 'sites' destination if not in platforms filter or if platforms is None
|
||||
if not platforms or 'sites' in platforms:
|
||||
destinations.append({'platform': 'sites'})
|
||||
|
||||
return self.publish_to_multiple_destinations(content, destinations, account)
|
||||
|
||||
def _get_adapter(self, destination: str):
|
||||
"""
|
||||
Get adapter for destination platform.
|
||||
@@ -178,10 +344,10 @@ class PublisherService:
|
||||
from igny8_core.business.publishing.services.adapters.sites_renderer_adapter import SitesRendererAdapter
|
||||
return SitesRendererAdapter()
|
||||
elif destination == 'wordpress':
|
||||
# Will be implemented in Phase 6
|
||||
return None
|
||||
from igny8_core.business.publishing.services.adapters.wordpress_adapter import WordPressAdapter
|
||||
return WordPressAdapter()
|
||||
elif destination == 'shopify':
|
||||
# Will be implemented in Phase 6
|
||||
return None
|
||||
from igny8_core.business.publishing.services.adapters.shopify_adapter import ShopifyAdapter
|
||||
return ShopifyAdapter()
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user