8 Phases refactor
This commit is contained in:
@@ -1,530 +0,0 @@
|
||||
"""
|
||||
Sites Renderer Adapter
|
||||
Phase 5: Sites Renderer & Publishing
|
||||
Stage 4: Enhanced with Stage 3 metadata (clusters, taxonomies, internal links)
|
||||
|
||||
Adapter for deploying sites to IGNY8 Sites renderer.
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, Optional, List
|
||||
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(BaseAdapter):
|
||||
"""
|
||||
Adapter for deploying sites to IGNY8 Sites renderer.
|
||||
Writes site definitions to filesystem for Sites container to serve.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.sites_data_path = os.getenv('SITES_DATA_PATH', '/data/app/sites-data')
|
||||
|
||||
def deploy(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
|
||||
"""
|
||||
Deploy site blueprint to Sites renderer.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance to deploy
|
||||
|
||||
Returns:
|
||||
dict: Deployment result with status and deployment record
|
||||
"""
|
||||
try:
|
||||
# Create deployment record
|
||||
deployment = DeploymentRecord.objects.create(
|
||||
account=site_blueprint.account,
|
||||
site=site_blueprint.site,
|
||||
sector=site_blueprint.sector,
|
||||
site_blueprint=site_blueprint,
|
||||
version=site_blueprint.version,
|
||||
status='deploying'
|
||||
)
|
||||
|
||||
# Build site definition
|
||||
site_definition = self._build_site_definition(site_blueprint)
|
||||
|
||||
# Write to filesystem
|
||||
deployment_path = self._write_site_definition(
|
||||
site_blueprint,
|
||||
site_definition,
|
||||
deployment.version
|
||||
)
|
||||
|
||||
# Update deployment record
|
||||
deployment.status = 'deployed'
|
||||
deployment.deployed_version = site_blueprint.version
|
||||
deployment.deployment_url = self._get_deployment_url(site_blueprint)
|
||||
deployment.metadata = {
|
||||
'deployment_path': str(deployment_path),
|
||||
'site_definition': site_definition
|
||||
}
|
||||
deployment.save()
|
||||
|
||||
# Update blueprint
|
||||
site_blueprint.deployed_version = site_blueprint.version
|
||||
site_blueprint.status = 'deployed'
|
||||
site_blueprint.save(update_fields=['deployed_version', 'status', 'updated_at'])
|
||||
|
||||
logger.info(
|
||||
f"[SitesRendererAdapter] Successfully deployed site {site_blueprint.id} v{deployment.version}"
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'deployment_id': deployment.id,
|
||||
'version': deployment.version,
|
||||
'deployment_url': deployment.deployment_url,
|
||||
'deployment_path': str(deployment_path)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[SitesRendererAdapter] Error deploying site {site_blueprint.id}: {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Update deployment record with error
|
||||
if 'deployment' in locals():
|
||||
deployment.status = 'failed'
|
||||
deployment.error_message = str(e)
|
||||
deployment.save()
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def _build_site_definition(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
|
||||
"""
|
||||
Build site definition JSON from blueprint.
|
||||
Merges actual Content from Writer into PageBlueprint blocks.
|
||||
Stage 4: Enhanced with Stage 3 metadata (clusters, taxonomies, internal links).
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
|
||||
Returns:
|
||||
dict: Site definition structure
|
||||
"""
|
||||
from igny8_core.business.content.models import Tasks, Content, ContentClusterMap
|
||||
|
||||
# Get all pages
|
||||
pages = []
|
||||
content_id_to_page = {} # Map content IDs to pages for metadata lookup
|
||||
|
||||
for page in site_blueprint.pages.all().order_by('order'):
|
||||
# Get blocks from blueprint (placeholders)
|
||||
blocks = page.blocks_json or []
|
||||
page_metadata = {
|
||||
'content_type': page.content_type if hasattr(page, 'content_type') else None,
|
||||
'cluster_id': None,
|
||||
'cluster_name': None,
|
||||
'content_structure': None,
|
||||
'taxonomy_terms': [], # Changed from taxonomy_id/taxonomy_name to list of terms
|
||||
'internal_links': []
|
||||
}
|
||||
|
||||
# Try to find actual Content from Writer
|
||||
# PageBlueprint -> Task (by title pattern) -> Content
|
||||
task_title = f"[Site Builder] {page.title or page.slug.replace('-', ' ').title()}"
|
||||
task = Tasks.objects.filter(
|
||||
account=page.account,
|
||||
site=page.site,
|
||||
sector=page.sector,
|
||||
title=task_title
|
||||
).first()
|
||||
|
||||
# If task exists, get its Content
|
||||
if task and hasattr(task, 'content_record'):
|
||||
content = task.content_record
|
||||
# If content is published, merge its blocks
|
||||
if content and content.status == 'publish' and content.json_blocks:
|
||||
# Merge Content.json_blocks into PageBlueprint.blocks_json
|
||||
# Content blocks take precedence over blueprint placeholders
|
||||
blocks = content.json_blocks
|
||||
logger.info(
|
||||
f"[SitesRendererAdapter] Using published Content blocks for page {page.slug} "
|
||||
f"(Content ID: {content.id})"
|
||||
)
|
||||
elif content and content.status == 'publish' and content.html_content:
|
||||
# If no json_blocks but has html_content, convert to text block
|
||||
blocks = [{
|
||||
'type': 'text',
|
||||
'data': {
|
||||
'content': content.html_content,
|
||||
'title': content.title or page.title
|
||||
}
|
||||
}]
|
||||
logger.info(
|
||||
f"[SitesRendererAdapter] Converted HTML content to text block for page {page.slug}"
|
||||
)
|
||||
|
||||
# Stage 4: Add Stage 3 metadata if content exists
|
||||
if content:
|
||||
content_id_to_page[content.id] = page.slug
|
||||
|
||||
# Get cluster mapping
|
||||
cluster_map = ContentClusterMap.objects.filter(content=content).first()
|
||||
if cluster_map and cluster_map.cluster:
|
||||
page_metadata['cluster_id'] = cluster_map.cluster.id
|
||||
page_metadata['cluster_name'] = cluster_map.cluster.name
|
||||
page_metadata['content_structure'] = cluster_map.role or task.content_structure if task else None
|
||||
|
||||
# Get taxonomy terms using M2M relationship
|
||||
taxonomy_terms = content.taxonomy_terms.all()
|
||||
if taxonomy_terms.exists():
|
||||
page_metadata['taxonomy_terms'] = [
|
||||
{'id': term.id, 'name': term.name, 'type': term.taxonomy_type}
|
||||
for term in taxonomy_terms
|
||||
]
|
||||
|
||||
# Get internal links from content
|
||||
if content.internal_links:
|
||||
page_metadata['internal_links'] = content.internal_links
|
||||
|
||||
# Use content_type if available
|
||||
if content.content_type:
|
||||
page_metadata['content_type'] = content.content_type
|
||||
|
||||
# Fallback to task metadata if content not found
|
||||
if task and not page_metadata.get('cluster_id'):
|
||||
if task.cluster:
|
||||
page_metadata['cluster_id'] = task.cluster.id
|
||||
page_metadata['cluster_name'] = task.cluster.name
|
||||
page_metadata['content_structure'] = task.content_structure
|
||||
if task.taxonomy:
|
||||
page_metadata['taxonomy_id'] = task.taxonomy.id
|
||||
page_metadata['taxonomy_name'] = task.taxonomy.name
|
||||
if task.content_type:
|
||||
page_metadata['content_type'] = task.content_type
|
||||
|
||||
pages.append({
|
||||
'id': page.id,
|
||||
'slug': page.slug,
|
||||
'title': page.title,
|
||||
'type': page.type,
|
||||
'blocks': blocks,
|
||||
'status': page.status,
|
||||
'metadata': page_metadata, # Stage 4: Add metadata
|
||||
})
|
||||
|
||||
# Stage 4: Build navigation with cluster grouping
|
||||
navigation = self._build_navigation_with_metadata(site_blueprint, pages)
|
||||
|
||||
# Stage 4: Build taxonomy tree for breadcrumbs
|
||||
taxonomy_tree = self._build_taxonomy_tree(site_blueprint)
|
||||
|
||||
# Build site definition
|
||||
definition = {
|
||||
'id': site_blueprint.id,
|
||||
'name': site_blueprint.name,
|
||||
'description': site_blueprint.description,
|
||||
'version': site_blueprint.version,
|
||||
'layout': site_blueprint.structure_json.get('layout', 'default'),
|
||||
'theme': site_blueprint.structure_json.get('theme', {}),
|
||||
'navigation': navigation, # Stage 4: Enhanced navigation
|
||||
'taxonomy_tree': taxonomy_tree, # Stage 4: Taxonomy tree for breadcrumbs
|
||||
'pages': pages,
|
||||
'config': site_blueprint.config_json,
|
||||
'created_at': site_blueprint.created_at.isoformat(),
|
||||
'updated_at': site_blueprint.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
return definition
|
||||
|
||||
def _build_navigation_with_metadata(
|
||||
self,
|
||||
site_blueprint: SiteBlueprint,
|
||||
pages: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Build navigation structure with cluster grouping.
|
||||
Stage 4: Groups pages by cluster for better navigation.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
pages: List of page dictionaries
|
||||
|
||||
Returns:
|
||||
List of navigation items
|
||||
"""
|
||||
# If explicit navigation exists in structure_json, use it
|
||||
explicit_nav = site_blueprint.structure_json.get('navigation', [])
|
||||
if explicit_nav:
|
||||
return explicit_nav
|
||||
|
||||
# Otherwise, build navigation from pages grouped by cluster
|
||||
navigation = []
|
||||
|
||||
# Group pages by cluster
|
||||
cluster_groups = {}
|
||||
ungrouped_pages = []
|
||||
|
||||
for page in pages:
|
||||
if page.get('status') in ['published', 'ready']:
|
||||
cluster_id = page.get('metadata', {}).get('cluster_id')
|
||||
if cluster_id:
|
||||
if cluster_id not in cluster_groups:
|
||||
cluster_groups[cluster_id] = {
|
||||
'cluster_id': cluster_id,
|
||||
'cluster_name': page.get('metadata', {}).get('cluster_name', 'Unknown'),
|
||||
'pages': []
|
||||
}
|
||||
cluster_groups[cluster_id]['pages'].append({
|
||||
'slug': page['slug'],
|
||||
'title': page['title'],
|
||||
'type': page['type']
|
||||
})
|
||||
else:
|
||||
ungrouped_pages.append({
|
||||
'slug': page['slug'],
|
||||
'title': page['title'],
|
||||
'type': page['type']
|
||||
})
|
||||
|
||||
# Add cluster groups to navigation
|
||||
for cluster_group in cluster_groups.values():
|
||||
navigation.append({
|
||||
'type': 'cluster',
|
||||
'name': cluster_group['cluster_name'],
|
||||
'items': cluster_group['pages']
|
||||
})
|
||||
|
||||
# Add ungrouped pages
|
||||
if ungrouped_pages:
|
||||
navigation.extend(ungrouped_pages)
|
||||
|
||||
return navigation if navigation else [
|
||||
{'slug': page['slug'], 'title': page['title']}
|
||||
for page in pages
|
||||
if page.get('status') in ['published', 'ready']
|
||||
]
|
||||
|
||||
def _build_taxonomy_tree(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
|
||||
"""
|
||||
Build taxonomy tree structure for breadcrumbs.
|
||||
Stage 4: Creates hierarchical taxonomy structure.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
|
||||
Returns:
|
||||
dict: Taxonomy tree structure
|
||||
"""
|
||||
taxonomies = site_blueprint.taxonomies.all()
|
||||
|
||||
tree = {
|
||||
'categories': [],
|
||||
'tags': [],
|
||||
'product_categories': [],
|
||||
'product_attributes': []
|
||||
}
|
||||
|
||||
for taxonomy in taxonomies:
|
||||
taxonomy_item = {
|
||||
'id': taxonomy.id,
|
||||
'name': taxonomy.name,
|
||||
'slug': taxonomy.slug,
|
||||
'type': taxonomy.taxonomy_type,
|
||||
'description': taxonomy.description
|
||||
}
|
||||
|
||||
if taxonomy.taxonomy_type in ['blog_category', 'product_category']:
|
||||
category_key = 'product_categories' if 'product' in taxonomy.taxonomy_type else 'categories'
|
||||
tree[category_key].append(taxonomy_item)
|
||||
elif taxonomy.taxonomy_type in ['blog_tag', 'product_tag']:
|
||||
tag_key = 'product_tags' if 'product' in taxonomy.taxonomy_type else 'tags'
|
||||
if tag_key not in tree:
|
||||
tree[tag_key] = []
|
||||
tree[tag_key].append(taxonomy_item)
|
||||
elif taxonomy.taxonomy_type == 'product_attribute':
|
||||
tree['product_attributes'].append(taxonomy_item)
|
||||
|
||||
return tree
|
||||
|
||||
def _write_site_definition(
|
||||
self,
|
||||
site_blueprint: SiteBlueprint,
|
||||
site_definition: Dict[str, Any],
|
||||
version: int
|
||||
) -> Path:
|
||||
"""
|
||||
Write site definition to filesystem.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
site_definition: Site definition dict
|
||||
version: Version number
|
||||
|
||||
Returns:
|
||||
Path: Deployment path
|
||||
"""
|
||||
# Build path: /data/app/sites-data/clients/{site_id}/v{version}/
|
||||
site_id = site_blueprint.site.id
|
||||
deployment_dir = Path(self.sites_data_path) / 'clients' / str(site_id) / f'v{version}'
|
||||
deployment_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write site.json
|
||||
site_json_path = deployment_dir / 'site.json'
|
||||
with open(site_json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(site_definition, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Write pages
|
||||
pages_dir = deployment_dir / 'pages'
|
||||
pages_dir.mkdir(exist_ok=True)
|
||||
|
||||
for page in site_definition.get('pages', []):
|
||||
page_json_path = pages_dir / f"{page['slug']}.json"
|
||||
with open(page_json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(page, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Ensure assets directory exists
|
||||
assets_dir = deployment_dir / 'assets'
|
||||
assets_dir.mkdir(exist_ok=True)
|
||||
(assets_dir / 'images').mkdir(exist_ok=True)
|
||||
(assets_dir / 'documents').mkdir(exist_ok=True)
|
||||
(assets_dir / 'media').mkdir(exist_ok=True)
|
||||
|
||||
logger.info(f"[SitesRendererAdapter] Wrote site definition to {deployment_dir}")
|
||||
|
||||
return deployment_dir
|
||||
|
||||
def _get_deployment_url(self, site_blueprint: SiteBlueprint) -> str:
|
||||
"""
|
||||
Get deployment URL for site.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
|
||||
Returns:
|
||||
str: Deployment URL
|
||||
"""
|
||||
site_id = site_blueprint.site.id
|
||||
|
||||
# Get Sites Renderer URL from environment or use default
|
||||
sites_renderer_host = os.getenv('SITES_RENDERER_HOST', '31.97.144.105')
|
||||
sites_renderer_port = os.getenv('SITES_RENDERER_PORT', '8024')
|
||||
sites_renderer_protocol = os.getenv('SITES_RENDERER_PROTOCOL', 'http')
|
||||
|
||||
# Construct URL: http://31.97.144.105:8024/{site_id}
|
||||
# Sites Renderer routes: /:siteId/* -> SiteRenderer component
|
||||
return f"{sites_renderer_protocol}://{sites_renderer_host}:{sites_renderer_port}/{site_id}"
|
||||
|
||||
# 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': {}
|
||||
}
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
"""
|
||||
Deployment Readiness Service
|
||||
Stage 4: Checks if site blueprint is ready for deployment
|
||||
|
||||
Validates cluster coverage, content validation, sync status, and taxonomy completeness.
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from igny8_core.business.site_building.models import SiteBlueprint
|
||||
from igny8_core.business.content.services.validation_service import ContentValidationService
|
||||
from igny8_core.business.integration.services.sync_health_service import SyncHealthService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeploymentReadinessService:
|
||||
"""
|
||||
Service for checking deployment readiness.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.validation_service = ContentValidationService()
|
||||
self.sync_health_service = SyncHealthService()
|
||||
|
||||
def check_readiness(self, site_blueprint_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Check if site blueprint is ready for deployment.
|
||||
|
||||
Args:
|
||||
site_blueprint_id: SiteBlueprint ID
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'ready': bool,
|
||||
'checks': {
|
||||
'cluster_coverage': bool,
|
||||
'content_validation': bool,
|
||||
'sync_status': bool,
|
||||
'taxonomy_completeness': bool
|
||||
},
|
||||
'errors': List[str],
|
||||
'warnings': List[str],
|
||||
'details': {
|
||||
'cluster_coverage': dict,
|
||||
'content_validation': dict,
|
||||
'sync_status': dict,
|
||||
'taxonomy_completeness': dict
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
blueprint = SiteBlueprint.objects.get(id=site_blueprint_id)
|
||||
except SiteBlueprint.DoesNotExist:
|
||||
return {
|
||||
'ready': False,
|
||||
'checks': {},
|
||||
'errors': [f'SiteBlueprint {site_blueprint_id} not found'],
|
||||
'warnings': [],
|
||||
'details': {}
|
||||
}
|
||||
|
||||
checks = {}
|
||||
errors = []
|
||||
warnings = []
|
||||
details = {}
|
||||
|
||||
# Check 1: Cluster Coverage
|
||||
cluster_check = self._check_cluster_coverage(blueprint)
|
||||
checks['cluster_coverage'] = cluster_check['ready']
|
||||
details['cluster_coverage'] = cluster_check
|
||||
if not cluster_check['ready']:
|
||||
errors.extend(cluster_check.get('errors', []))
|
||||
if cluster_check.get('warnings'):
|
||||
warnings.extend(cluster_check['warnings'])
|
||||
|
||||
# Check 2: Content Validation
|
||||
content_check = self._check_content_validation(blueprint)
|
||||
checks['content_validation'] = content_check['ready']
|
||||
details['content_validation'] = content_check
|
||||
if not content_check['ready']:
|
||||
errors.extend(content_check.get('errors', []))
|
||||
if content_check.get('warnings'):
|
||||
warnings.extend(content_check['warnings'])
|
||||
|
||||
# Check 3: Sync Status (if WordPress integration exists)
|
||||
sync_check = self._check_sync_status(blueprint)
|
||||
checks['sync_status'] = sync_check['ready']
|
||||
details['sync_status'] = sync_check
|
||||
if not sync_check['ready']:
|
||||
warnings.extend(sync_check.get('warnings', []))
|
||||
if sync_check.get('errors'):
|
||||
errors.extend(sync_check['errors'])
|
||||
|
||||
# Check 4: Taxonomy Completeness
|
||||
taxonomy_check = self._check_taxonomy_completeness(blueprint)
|
||||
checks['taxonomy_completeness'] = taxonomy_check['ready']
|
||||
details['taxonomy_completeness'] = taxonomy_check
|
||||
if not taxonomy_check['ready']:
|
||||
warnings.extend(taxonomy_check.get('warnings', []))
|
||||
if taxonomy_check.get('errors'):
|
||||
errors.extend(taxonomy_check['errors'])
|
||||
|
||||
# Overall readiness: all critical checks must pass
|
||||
ready = (
|
||||
checks.get('cluster_coverage', False) and
|
||||
checks.get('content_validation', False)
|
||||
)
|
||||
|
||||
return {
|
||||
'ready': ready,
|
||||
'checks': checks,
|
||||
'errors': errors,
|
||||
'warnings': warnings,
|
||||
'details': details
|
||||
}
|
||||
|
||||
def _check_cluster_coverage(self, blueprint: SiteBlueprint) -> Dict[str, Any]:
|
||||
"""
|
||||
Check if all clusters have required coverage.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'ready': bool,
|
||||
'total_clusters': int,
|
||||
'covered_clusters': int,
|
||||
'incomplete_clusters': List[Dict],
|
||||
'errors': List[str],
|
||||
'warnings': List[str]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
cluster_links = blueprint.cluster_links.all()
|
||||
total_clusters = cluster_links.count()
|
||||
|
||||
if total_clusters == 0:
|
||||
return {
|
||||
'ready': False,
|
||||
'total_clusters': 0,
|
||||
'covered_clusters': 0,
|
||||
'incomplete_clusters': [],
|
||||
'errors': ['No clusters attached to blueprint'],
|
||||
'warnings': []
|
||||
}
|
||||
|
||||
incomplete_clusters = []
|
||||
covered_count = 0
|
||||
|
||||
for cluster_link in cluster_links:
|
||||
if cluster_link.coverage_status == 'complete':
|
||||
covered_count += 1
|
||||
else:
|
||||
incomplete_clusters.append({
|
||||
'cluster_id': cluster_link.cluster_id,
|
||||
'cluster_name': getattr(cluster_link.cluster, 'name', 'Unknown'),
|
||||
'status': cluster_link.coverage_status,
|
||||
'role': cluster_link.role
|
||||
})
|
||||
|
||||
ready = covered_count == total_clusters
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
if not ready:
|
||||
if covered_count == 0:
|
||||
errors.append('No clusters have complete coverage')
|
||||
else:
|
||||
warnings.append(
|
||||
f'{total_clusters - covered_count} of {total_clusters} clusters need coverage'
|
||||
)
|
||||
|
||||
return {
|
||||
'ready': ready,
|
||||
'total_clusters': total_clusters,
|
||||
'covered_clusters': covered_count,
|
||||
'incomplete_clusters': incomplete_clusters,
|
||||
'errors': errors,
|
||||
'warnings': warnings
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking cluster coverage: {e}", exc_info=True)
|
||||
return {
|
||||
'ready': False,
|
||||
'total_clusters': 0,
|
||||
'covered_clusters': 0,
|
||||
'incomplete_clusters': [],
|
||||
'errors': [f'Error checking cluster coverage: {str(e)}'],
|
||||
'warnings': []
|
||||
}
|
||||
|
||||
def _check_content_validation(self, blueprint: SiteBlueprint) -> Dict[str, Any]:
|
||||
"""
|
||||
Check if all published content passes validation.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'ready': bool,
|
||||
'total_content': int,
|
||||
'valid_content': int,
|
||||
'invalid_content': List[Dict],
|
||||
'errors': List[str],
|
||||
'warnings': List[str]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from igny8_core.business.content.models import Content, Tasks
|
||||
|
||||
# Get all content associated with this blueprint
|
||||
# Content is linked via Tasks -> PageBlueprint -> SiteBlueprint
|
||||
page_ids = blueprint.pages.values_list('id', flat=True)
|
||||
|
||||
# Find tasks that match page blueprints
|
||||
tasks = Tasks.objects.filter(
|
||||
account=blueprint.account,
|
||||
site=blueprint.site,
|
||||
sector=blueprint.sector
|
||||
)
|
||||
|
||||
# Filter tasks that might be related to this blueprint
|
||||
# (This is a simplified check - in practice, tasks should have blueprint reference)
|
||||
content_items = Content.objects.filter(
|
||||
task__in=tasks,
|
||||
status='publish',
|
||||
source='igny8'
|
||||
)
|
||||
|
||||
total_content = content_items.count()
|
||||
|
||||
if total_content == 0:
|
||||
return {
|
||||
'ready': True, # No content to validate is OK
|
||||
'total_content': 0,
|
||||
'valid_content': 0,
|
||||
'invalid_content': [],
|
||||
'errors': [],
|
||||
'warnings': ['No published content found for validation']
|
||||
}
|
||||
|
||||
invalid_content = []
|
||||
valid_count = 0
|
||||
|
||||
for content in content_items:
|
||||
errors = self.validation_service.validate_for_publish(content)
|
||||
if errors:
|
||||
invalid_content.append({
|
||||
'content_id': content.id,
|
||||
'title': content.title or 'Untitled',
|
||||
'errors': errors
|
||||
})
|
||||
else:
|
||||
valid_count += 1
|
||||
|
||||
ready = len(invalid_content) == 0
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
if not ready:
|
||||
errors.append(
|
||||
f'{len(invalid_content)} of {total_content} content items have validation errors'
|
||||
)
|
||||
|
||||
return {
|
||||
'ready': ready,
|
||||
'total_content': total_content,
|
||||
'valid_content': valid_count,
|
||||
'invalid_content': invalid_content,
|
||||
'errors': errors,
|
||||
'warnings': warnings
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking content validation: {e}", exc_info=True)
|
||||
return {
|
||||
'ready': False,
|
||||
'total_content': 0,
|
||||
'valid_content': 0,
|
||||
'invalid_content': [],
|
||||
'errors': [f'Error checking content validation: {str(e)}'],
|
||||
'warnings': []
|
||||
}
|
||||
|
||||
def _check_sync_status(self, blueprint: SiteBlueprint) -> Dict[str, Any]:
|
||||
"""
|
||||
Check sync status for WordPress integrations.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'ready': bool,
|
||||
'has_integration': bool,
|
||||
'sync_status': str,
|
||||
'mismatch_count': int,
|
||||
'errors': List[str],
|
||||
'warnings': List[str]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from igny8_core.business.integration.models import SiteIntegration
|
||||
|
||||
integrations = SiteIntegration.objects.filter(
|
||||
site=blueprint.site,
|
||||
is_active=True,
|
||||
platform='wordpress'
|
||||
)
|
||||
|
||||
if not integrations.exists():
|
||||
return {
|
||||
'ready': True, # No WordPress integration is OK
|
||||
'has_integration': False,
|
||||
'sync_status': None,
|
||||
'mismatch_count': 0,
|
||||
'errors': [],
|
||||
'warnings': []
|
||||
}
|
||||
|
||||
# Get sync status from SyncHealthService
|
||||
sync_status = self.sync_health_service.get_sync_status(blueprint.site.id)
|
||||
|
||||
overall_status = sync_status.get('overall_status', 'error')
|
||||
is_healthy = overall_status == 'healthy'
|
||||
|
||||
# Count total mismatches
|
||||
mismatch_count = sum(
|
||||
i.get('mismatch_count', 0) for i in sync_status.get('integrations', [])
|
||||
)
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
if not is_healthy:
|
||||
if overall_status == 'error':
|
||||
errors.append('WordPress sync has errors')
|
||||
else:
|
||||
warnings.append('WordPress sync has warnings')
|
||||
|
||||
if mismatch_count > 0:
|
||||
warnings.append(f'{mismatch_count} sync mismatches detected')
|
||||
|
||||
# Sync status doesn't block deployment, but should be warned
|
||||
return {
|
||||
'ready': True, # Sync issues are warnings, not blockers
|
||||
'has_integration': True,
|
||||
'sync_status': overall_status,
|
||||
'mismatch_count': mismatch_count,
|
||||
'errors': errors,
|
||||
'warnings': warnings
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking sync status: {e}", exc_info=True)
|
||||
return {
|
||||
'ready': True, # Don't block on sync check errors
|
||||
'has_integration': False,
|
||||
'sync_status': None,
|
||||
'mismatch_count': 0,
|
||||
'errors': [],
|
||||
'warnings': [f'Could not check sync status: {str(e)}']
|
||||
}
|
||||
|
||||
def _check_taxonomy_completeness(self, blueprint: SiteBlueprint) -> Dict[str, Any]:
|
||||
"""
|
||||
Check if taxonomies are complete for the site type.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'ready': bool,
|
||||
'total_taxonomies': int,
|
||||
'required_taxonomies': List[str],
|
||||
'missing_taxonomies': List[str],
|
||||
'errors': List[str],
|
||||
'warnings': List[str]
|
||||
}
|
||||
"""
|
||||
try:
|
||||
taxonomies = blueprint.taxonomies.all()
|
||||
total_taxonomies = taxonomies.count()
|
||||
|
||||
# Determine required taxonomies based on site type
|
||||
site_type = blueprint.site.site_type if hasattr(blueprint.site, 'site_type') else None
|
||||
|
||||
required_types = []
|
||||
if site_type == 'blog':
|
||||
required_types = ['blog_category', 'blog_tag']
|
||||
elif site_type == 'ecommerce':
|
||||
required_types = ['product_category', 'product_tag', 'product_attribute']
|
||||
elif site_type == 'company':
|
||||
required_types = ['service_category']
|
||||
|
||||
existing_types = set(taxonomies.values_list('taxonomy_type', flat=True))
|
||||
missing_types = set(required_types) - existing_types
|
||||
|
||||
ready = len(missing_types) == 0
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
if not ready:
|
||||
warnings.append(
|
||||
f'Missing required taxonomies for {site_type} site: {", ".join(missing_types)}'
|
||||
)
|
||||
|
||||
if total_taxonomies == 0:
|
||||
warnings.append('No taxonomies defined')
|
||||
|
||||
return {
|
||||
'ready': ready,
|
||||
'total_taxonomies': total_taxonomies,
|
||||
'required_taxonomies': required_types,
|
||||
'missing_taxonomies': list(missing_types),
|
||||
'errors': errors,
|
||||
'warnings': warnings
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking taxonomy completeness: {e}", exc_info=True)
|
||||
return {
|
||||
'ready': True, # Don't block on taxonomy check errors
|
||||
'total_taxonomies': 0,
|
||||
'required_taxonomies': [],
|
||||
'missing_taxonomies': [],
|
||||
'errors': [],
|
||||
'warnings': [f'Could not check taxonomy completeness: {str(e)}']
|
||||
}
|
||||
|
||||
@@ -1,140 +1,17 @@
|
||||
"""
|
||||
Deployment Service
|
||||
Phase 5: Sites Renderer & Publishing
|
||||
Deployment Service - DEPRECATED
|
||||
|
||||
Manages deployment lifecycle for sites.
|
||||
Legacy SiteBlueprint deployment functionality removed.
|
||||
Use WordPress integration sync for publishing.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from igny8_core.business.site_building.models import SiteBlueprint
|
||||
from igny8_core.business.publishing.models import DeploymentRecord
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeploymentService:
|
||||
"""
|
||||
Service for managing site deployment lifecycle.
|
||||
DEPRECATED: Legacy SiteBlueprint deployment service.
|
||||
Use integration sync services instead.
|
||||
"""
|
||||
|
||||
def get_status(self, site_blueprint: SiteBlueprint) -> Optional[DeploymentRecord]:
|
||||
"""
|
||||
Get current deployment status for a site.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
|
||||
Returns:
|
||||
DeploymentRecord or None
|
||||
"""
|
||||
return DeploymentRecord.objects.filter(
|
||||
site_blueprint=site_blueprint,
|
||||
status='deployed'
|
||||
).order_by('-deployed_at').first()
|
||||
|
||||
def get_latest_deployment(
|
||||
self,
|
||||
site_blueprint: SiteBlueprint
|
||||
) -> Optional[DeploymentRecord]:
|
||||
"""
|
||||
Get latest deployment record (any status).
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
|
||||
Returns:
|
||||
DeploymentRecord or None
|
||||
"""
|
||||
return DeploymentRecord.objects.filter(
|
||||
site_blueprint=site_blueprint
|
||||
).order_by('-created_at').first()
|
||||
|
||||
def rollback(
|
||||
self,
|
||||
site_blueprint: SiteBlueprint,
|
||||
target_version: int
|
||||
) -> dict:
|
||||
"""
|
||||
Rollback site to a previous version.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
target_version: Version to rollback to
|
||||
|
||||
Returns:
|
||||
dict: Rollback result
|
||||
"""
|
||||
try:
|
||||
# Find deployment record for target version
|
||||
target_deployment = DeploymentRecord.objects.filter(
|
||||
site_blueprint=site_blueprint,
|
||||
version=target_version,
|
||||
status='deployed'
|
||||
).first()
|
||||
|
||||
if not target_deployment:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Deployment for version {target_version} not found'
|
||||
}
|
||||
|
||||
# Create new deployment record for rollback
|
||||
rollback_deployment = DeploymentRecord.objects.create(
|
||||
account=site_blueprint.account,
|
||||
site=site_blueprint.site,
|
||||
sector=site_blueprint.sector,
|
||||
site_blueprint=site_blueprint,
|
||||
version=target_version,
|
||||
status='deployed',
|
||||
deployed_version=target_version,
|
||||
deployment_url=target_deployment.deployment_url,
|
||||
metadata={
|
||||
'rollback_from': site_blueprint.version,
|
||||
'rollback_to': target_version
|
||||
}
|
||||
)
|
||||
|
||||
# Update blueprint
|
||||
site_blueprint.deployed_version = target_version
|
||||
site_blueprint.save(update_fields=['deployed_version', 'updated_at'])
|
||||
|
||||
logger.info(
|
||||
f"[DeploymentService] Rolled back site {site_blueprint.id} to version {target_version}"
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'deployment_id': rollback_deployment.id,
|
||||
'version': target_version
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[DeploymentService] Error rolling back site {site_blueprint.id}: {str(e)}",
|
||||
exc_info=True
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}
|
||||
|
||||
def list_deployments(
|
||||
self,
|
||||
site_blueprint: SiteBlueprint
|
||||
) -> list:
|
||||
"""
|
||||
List all deployments for a site.
|
||||
|
||||
Args:
|
||||
site_blueprint: SiteBlueprint instance
|
||||
|
||||
Returns:
|
||||
list: List of DeploymentRecord instances
|
||||
"""
|
||||
return list(
|
||||
DeploymentRecord.objects.filter(
|
||||
site_blueprint=site_blueprint
|
||||
).order_by('-created_at')
|
||||
)
|
||||
|
||||
pass
|
||||
|
||||
@@ -368,10 +368,8 @@ class PublisherService:
|
||||
Adapter instance or None
|
||||
"""
|
||||
# Lazy import to avoid circular dependencies
|
||||
if destination == 'sites':
|
||||
from igny8_core.business.publishing.services.adapters.sites_renderer_adapter import SitesRendererAdapter
|
||||
return SitesRendererAdapter()
|
||||
elif destination == 'wordpress':
|
||||
# REMOVED: 'sites' destination (SitesRendererAdapter) - legacy SiteBlueprint functionality
|
||||
if destination == 'wordpress':
|
||||
from igny8_core.business.publishing.services.adapters.wordpress_adapter import WordPressAdapter
|
||||
return WordPressAdapter()
|
||||
elif destination == 'shopify':
|
||||
|
||||
Reference in New Issue
Block a user