Implement content synchronization from WordPress and Shopify in ContentSyncService

- Added methods to sync content from WordPress and Shopify to IGNY8, including error handling and logging.
- Introduced internal methods for fetching posts from WordPress and products from Shopify.
- Updated IntegrationService to include a method for retrieving active integrations for a site.
- Enhanced test cases to cover new functionality and ensure proper setup of test data, including industry and sector associations.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-18 02:07:22 +00:00
parent 873f97ea3f
commit 51c3986e01
12 changed files with 583 additions and 64 deletions

View File

@@ -7,16 +7,18 @@ Adapter for deploying sites to IGNY8 Sites renderer.
import logging
import json
import os
from typing import Dict, Any
from typing import Dict, Any, Optional
from pathlib import Path
from datetime import datetime
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.publishing.models import DeploymentRecord
from igny8_core.business.publishing.services.adapters.base_adapter import BaseAdapter
logger = logging.getLogger(__name__)
class SitesRendererAdapter:
class SitesRendererAdapter(BaseAdapter):
"""
Adapter for deploying sites to IGNY8 Sites renderer.
Writes site definitions to filesystem for Sites container to serve.
@@ -200,4 +202,113 @@ class SitesRendererAdapter:
# For now, return placeholder
site_id = site_blueprint.site.id
return f"https://{site_id}.igny8.com" # Placeholder
# BaseAdapter interface implementation
def publish(
self,
content: Any,
destination_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Publish content to destination (implements BaseAdapter interface).
Args:
content: SiteBlueprint to publish
destination_config: Destination-specific configuration
Returns:
dict: Publishing result
"""
if not isinstance(content, SiteBlueprint):
return {
'success': False,
'error': 'SitesRendererAdapter only accepts SiteBlueprint instances'
}
result = self.deploy(content)
if result.get('success'):
return {
'success': True,
'external_id': str(result.get('deployment_id')),
'url': result.get('deployment_url'),
'published_at': datetime.now(),
'metadata': {
'deployment_path': result.get('deployment_path'),
'version': result.get('version')
}
}
else:
return {
'success': False,
'error': result.get('error'),
'metadata': {}
}
def test_connection(
self,
config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Test connection to Sites renderer (implements BaseAdapter interface).
Args:
config: Destination configuration
Returns:
dict: Connection test result
"""
sites_data_path = config.get('sites_data_path', os.getenv('SITES_DATA_PATH', '/data/app/sites-data'))
try:
path = Path(sites_data_path)
if path.exists() and path.is_dir():
return {
'success': True,
'message': 'Sites data directory is accessible',
'details': {'path': str(path)}
}
else:
return {
'success': False,
'message': f'Sites data directory does not exist: {sites_data_path}',
'details': {}
}
except Exception as e:
return {
'success': False,
'message': f'Error accessing sites data directory: {str(e)}',
'details': {}
}
def get_status(
self,
published_id: str,
config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Get publishing status for published content (implements BaseAdapter interface).
Args:
published_id: Deployment record ID
config: Destination configuration
Returns:
dict: Status information
"""
try:
deployment = DeploymentRecord.objects.get(id=published_id)
return {
'status': deployment.status,
'url': deployment.deployment_url,
'updated_at': deployment.updated_at,
'metadata': deployment.metadata or {}
}
except DeploymentRecord.DoesNotExist:
return {
'status': 'not_found',
'url': None,
'updated_at': None,
'metadata': {}
}

View File

@@ -53,14 +53,15 @@ class WordPressAdapter(BaseAdapter):
client = self._get_client(destination_config)
# Extract content data
if hasattr(content, 'title') and hasattr(content, 'content'):
if hasattr(content, 'title'):
# Content model instance
title = content.title
content_html = content.content
# Try different possible attribute names for content
content_html = getattr(content, 'html_content', None) or getattr(content, 'content', None) or ''
elif isinstance(content, dict):
# Dict with content data
title = content.get('title', '')
content_html = content.get('content', '')
content_html = content.get('html_content') or content.get('content', '')
else:
raise ValueError(f"Unsupported content type: {type(content)}")
@@ -76,14 +77,19 @@ class WordPressAdapter(BaseAdapter):
featured_image_url=featured_image_url
)
if result.get('success'):
# Handle different response formats (for compatibility with mocks and real API)
if result.get('success') or result.get('id') or result.get('post_id'):
# Extract post ID from various possible fields
post_id = result.get('post_id') or result.get('id') or result.get('ID')
url = result.get('url') or result.get('link')
return {
'success': True,
'external_id': str(result.get('post_id')),
'url': result.get('url'),
'external_id': str(post_id) if post_id else None,
'url': url,
'published_at': datetime.now(),
'metadata': {
'post_id': result.get('post_id'),
'post_id': post_id,
'status': status
}
}
@@ -94,7 +100,7 @@ class WordPressAdapter(BaseAdapter):
'url': None,
'published_at': None,
'metadata': {
'error': result.get('error')
'error': result.get('error', 'Unknown error')
}
}

View File

@@ -5,7 +5,7 @@ Phase 6: Site Integration & Multi-Destination Publishing
from django.test import TestCase
from unittest.mock import Mock, patch
from igny8_core.auth.models import Account, Site, Sector
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.publishing.services.adapters.base_adapter import BaseAdapter
from igny8_core.business.publishing.services.adapters.sites_renderer_adapter import SitesRendererAdapter
from igny8_core.business.publishing.services.adapters.wordpress_adapter import WordPressAdapter
@@ -17,15 +17,56 @@ class AdapterPatternTestCase(TestCase):
def setUp(self):
"""Set up test data"""
self.account = Account.objects.create(name="Test Account")
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site"
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)
@@ -79,15 +120,16 @@ class AdapterPatternTestCase(TestCase):
adapter = WordPressAdapter()
config = {
'url': 'https://example.com',
'site_url': 'https://example.com',
'username': 'test',
'password': 'test'
'app_password': 'test'
}
with patch('igny8_core.utils.wordpress.WordPressClient') as mock_client:
# Patch WordPressClient at the point where it's used in the adapter
with patch('igny8_core.business.publishing.services.adapters.wordpress_adapter.WordPressClient') as mock_client_class:
mock_instance = Mock()
mock_instance.create_post.return_value = {'id': 123, 'link': 'https://example.com/post/123'}
mock_client.return_value = mock_instance
mock_client_class.return_value = mock_instance
result = adapter.publish(content, config)

View File

@@ -5,7 +5,7 @@ Phase 5: Sites Renderer & Publishing
from django.test import TestCase
from django.utils import timezone
from igny8_core.auth.models import Account, Site, Sector
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.publishing.models import DeploymentRecord
from igny8_core.business.publishing.services.deployment_service import DeploymentService
@@ -16,15 +16,56 @@ class DeploymentServiceTestCase(TestCase):
def setUp(self):
"""Set up test data"""
self.account = Account.objects.create(name="Test Account")
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site"
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)

View File

@@ -6,7 +6,7 @@ from django.test import TestCase
from django.utils import timezone
from unittest.mock import Mock, patch
from igny8_core.auth.models import Account, Site, Sector
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
from igny8_core.business.publishing.services.publisher_service import PublisherService
@@ -17,18 +17,56 @@ class PublisherServiceTestCase(TestCase):
def setUp(self):
"""Set up test data"""
from igny8_core.business.site_building.tests.base import SiteBuilderTestBase
# Create plan first
self.plan = Plan.objects.create(
name="Test Plan",
slug="test-plan",
price=0,
credits_per_month=1000
)
# Create user first (Account needs owner)
self.user = User.objects.create_user(
username='testuser',
email='test@test.com',
password='testpass123',
role='owner'
)
# Create account with owner
self.account = Account.objects.create(
name="Test Account",
slug="test-account",
plan=self.plan,
owner=self.user
)
# Update user to have account
self.user.account = self.account
self.user.save()
# Create industry and sector
self.industry = Industry.objects.create(
name="Test Industry",
slug="test-industry"
)
self.industry_sector = IndustrySector.objects.create(
industry=self.industry,
name="Test Sector",
slug="test-sector"
)
# Use SiteBuilderTestBase pattern if available, otherwise create manually
self.account = Account.objects.create(name="Test Account")
self.site = Site.objects.create(
account=self.account,
name="Test Site",
slug="test-site"
slug="test-site",
industry=self.industry
)
self.sector = Sector.objects.create(
account=self.account,
site=self.site,
industry_sector=self.industry_sector,
name="Test Sector",
slug="test-sector"
)
@@ -43,12 +81,11 @@ class PublisherServiceTestCase(TestCase):
def test_publish_to_sites_creates_deployment_record(self):
"""Test: Deployment works end-to-end"""
with patch('igny8_core.business.publishing.services.adapters.sites_renderer_adapter.SitesRendererAdapter.deploy') as mock_deploy:
mock_deploy.return_value = {
'success': True,
'deployment_url': 'https://test-site.igny8.com',
'version': 1
}
# Don't mock deploy - let it run to create the deployment record
# But mock the filesystem operations to avoid actual file writes
with patch('igny8_core.business.publishing.services.adapters.sites_renderer_adapter.Path.mkdir'), \
patch('igny8_core.business.publishing.services.adapters.sites_renderer_adapter.open', create=True) as mock_open:
mock_file = mock_open.return_value.__enter__.return_value
result = self.service.publish_to_sites(self.blueprint)