blurpritn adn site builde cleanup

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-01 02:22:02 +00:00
parent 3f2385d4d9
commit a7a772a78c
33 changed files with 1591 additions and 1308 deletions

View File

@@ -272,16 +272,13 @@ class Content(SiteSectorBaseModel):
class ContentTaxonomy(SiteSectorBaseModel):
"""
Universal taxonomy model for WordPress and IGNY8 cluster-based taxonomies.
Supports categories, tags, product attributes, and cluster mappings.
Simplified taxonomy model for AI-generated categories and tags.
Directly linked to Content via many-to-many relationship.
"""
TAXONOMY_TYPE_CHOICES = [
('category', 'Category'),
('tag', 'Tag'),
('product_category', 'Product Category'),
('product_attribute', 'Product Attribute'),
('cluster', 'Cluster Taxonomy'),
]
name = models.CharField(max_length=255, db_index=True, help_text="Term name")
@@ -290,7 +287,7 @@ class ContentTaxonomy(SiteSectorBaseModel):
max_length=50,
choices=TAXONOMY_TYPE_CHOICES,
db_index=True,
help_text="Type of taxonomy"
help_text="Type of taxonomy (category or tag)"
)
# WordPress/external platform sync fields
@@ -298,25 +295,19 @@ class ContentTaxonomy(SiteSectorBaseModel):
max_length=100,
blank=True,
default='',
help_text="WordPress taxonomy slug (category, post_tag, product_cat, pa_*) - empty for cluster taxonomies"
help_text="WordPress taxonomy slug (category, post_tag)"
)
external_id = models.IntegerField(
null=True,
blank=True,
db_index=True,
help_text="WordPress term_id - null for cluster taxonomies"
help_text="WordPress term_id for sync"
)
description = models.TextField(
blank=True,
default='',
help_text="Taxonomy term description"
)
sync_status = models.CharField(
max_length=50,
blank=True,
default='',
help_text="Synchronization status with external platforms"
)
count = models.IntegerField(
default=0,
help_text="Number of times this term is used"
@@ -324,7 +315,7 @@ class ContentTaxonomy(SiteSectorBaseModel):
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional metadata for the taxonomy term"
help_text="Additional metadata (AI generation details, etc.)"
)
created_at = models.DateTimeField(auto_now_add=True)
@@ -483,59 +474,6 @@ class ContentClusterMap(SiteSectorBaseModel):
return f"{self.cluster.name} ({self.get_role_display()})"
class ContentTaxonomyMap(SiteSectorBaseModel):
"""Maps content entities to blueprint taxonomies for syncing/publishing."""
SOURCE_CHOICES = [
('blueprint', 'Blueprint'),
('manual', 'Manual'),
('import', 'Import'),
]
content = models.ForeignKey(
Content,
on_delete=models.CASCADE,
related_name='taxonomy_mappings',
null=True,
blank=True,
)
task = models.ForeignKey(
Tasks,
on_delete=models.CASCADE,
related_name='taxonomy_mappings',
null=True,
blank=True,
)
taxonomy = models.ForeignKey(
'site_building.SiteBlueprintTaxonomy',
on_delete=models.CASCADE,
related_name='content_mappings',
)
source = models.CharField(max_length=50, choices=SOURCE_CHOICES, default='blueprint')
metadata = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'writer'
db_table = 'igny8_content_taxonomy_map'
unique_together = [['content', 'taxonomy']]
indexes = [
models.Index(fields=['taxonomy']),
models.Index(fields=['content', 'taxonomy']),
models.Index(fields=['task', 'taxonomy']),
]
def save(self, *args, **kwargs):
provider = self.content or self.task
if provider:
self.account = provider.account
self.site = provider.site
self.sector = provider.sector
super().save(*args, **kwargs)
def __str__(self):
return f"{self.taxonomy.name}"
class ContentAttribute(SiteSectorBaseModel):

View File

@@ -1,6 +1,7 @@
"""
Metadata Mapping Service
Stage 3: Persists cluster/taxonomy/attribute mappings from Tasks to Content
Stage 3: Persists cluster/attribute mappings from Tasks to Content
Legacy: ContentTaxonomyMap removed - taxonomy now uses Content.taxonomy_terms M2M relationship
"""
import logging
from typing import Optional
@@ -10,9 +11,9 @@ from igny8_core.business.content.models import (
Tasks,
Content,
ContentClusterMap,
ContentTaxonomyMap,
ContentAttributeMap,
)
# Removed: ContentTaxonomyMap - replaced by Content.taxonomy_terms ManyToManyField
logger = logging.getLogger(__name__)

View File

@@ -80,16 +80,16 @@ class CandidateEngine:
def _score_candidates(self, content: Content, candidates: List[Content]) -> List[Dict]:
"""Score candidates based on relevance"""
from igny8_core.business.content.models import ContentClusterMap, ContentTaxonomyMap
from igny8_core.business.content.models import ContentClusterMap
# Stage 3: Get cluster mappings for content
content_clusters = set(
ContentClusterMap.objects.filter(content=content)
.values_list('cluster_id', flat=True)
)
# Taxonomy matching using Content.taxonomy_terms M2M relationship
content_taxonomies = set(
ContentTaxonomyMap.objects.filter(content=content)
.values_list('taxonomy_id', flat=True)
content.taxonomy_terms.values_list('id', flat=True)
)
scored = []
@@ -106,10 +106,9 @@ class CandidateEngine:
if cluster_overlap:
score += 50 * len(cluster_overlap) # High weight for cluster matches
# Stage 3: Taxonomy matching
# Stage 3: Taxonomy matching using M2M relationship
candidate_taxonomies = set(
ContentTaxonomyMap.objects.filter(content=candidate)
.values_list('taxonomy_id', flat=True)
candidate.taxonomy_terms.values_list('id', flat=True)
)
taxonomy_overlap = content_taxonomies & candidate_taxonomies
if taxonomy_overlap:

View File

@@ -102,9 +102,8 @@ class ContentAnalyzer:
return ContentClusterMap.objects.filter(content=content).exists()
def _has_taxonomy_mapping(self, content: Content) -> bool:
"""Stage 3: Check if content has taxonomy mapping"""
from igny8_core.business.content.models import ContentTaxonomyMap
return ContentTaxonomyMap.objects.filter(content=content).exists()
"""Stage 3: Check if content has taxonomy mapping (categories/tags)"""
return content.taxonomy_terms.exists()
def _calculate_seo_score(self, content: Content) -> float:
"""Calculate SEO score (0-100)"""

View File

@@ -191,14 +191,7 @@ class ContentIdeas(SiteSectorBaseModel):
related_name='ideas',
limit_choices_to={'sector': models.F('sector')}
)
taxonomy = models.ForeignKey(
'site_building.SiteBlueprintTaxonomy',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='content_ideas',
help_text="Optional taxonomy association when derived from blueprint planning"
)
# REMOVED: taxonomy FK to SiteBlueprintTaxonomy (legacy blueprint functionality)
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
estimated_word_count = models.IntegerField(default=1000)
content_type = models.CharField(

View File

@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
('site_building', '0001_initial'),
# ('site_building', '0001_initial'), # REMOVED: SiteBuilder deprecated
('writer', '0001_initial'),
]
@@ -31,12 +31,12 @@ class Migration(migrations.Migration):
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site')),
('site_blueprint', models.ForeignKey(help_text='Site blueprint being deployed', on_delete=django.db.models.deletion.CASCADE, related_name='deployments', to='site_building.siteblueprint')),
# ('site_blueprint', ...) REMOVED: SiteBuilder deprecated
],
options={
'db_table': 'igny8_deployment_records',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['site_blueprint', 'status'], name='igny8_deplo_site_bl_14c185_idx'), models.Index(fields=['site_blueprint', 'version'], name='igny8_deplo_site_bl_34f669_idx'), models.Index(fields=['status'], name='igny8_deplo_status_5cb014_idx'), models.Index(fields=['account', 'status'], name='igny8_deplo_tenant__4de41d_idx')],
'indexes': [models.Index(fields=['status'], name='igny8_deplo_status_5cb014_idx'), models.Index(fields=['account', 'status'], name='igny8_deplo_tenant__4de41d_idx')],
},
),
migrations.CreateModel(
@@ -56,12 +56,12 @@ class Migration(migrations.Migration):
('content', models.ForeignKey(blank=True, help_text='Content being published (if publishing content)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publishing_records', to='writer.content')),
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site')),
('site_blueprint', models.ForeignKey(blank=True, help_text='Site blueprint being published (if publishing site)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publishing_records', to='site_building.siteblueprint')),
# ('site_blueprint', ...) REMOVED: SiteBuilder deprecated
],
options={
'db_table': 'igny8_publishing_records',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['destination', 'status'], name='igny8_publi_destina_5706a3_idx'), models.Index(fields=['content', 'destination'], name='igny8_publi_content_3688ba_idx'), models.Index(fields=['site_blueprint', 'destination'], name='igny8_publi_site_bl_963f5d_idx'), models.Index(fields=['account', 'status'], name='igny8_publi_tenant__2e0749_idx')],
'indexes': [models.Index(fields=['destination', 'status'], name='igny8_publi_destina_5706a3_idx'), models.Index(fields=['content', 'destination'], name='igny8_publi_content_3688ba_idx'), models.Index(fields=['account', 'status'], name='igny8_publi_tenant__2e0749_idx')],
},
),
]

View File

@@ -18,22 +18,14 @@ class PublishingRecord(SiteSectorBaseModel):
('failed', 'Failed'),
]
# Content or SiteBlueprint reference (one must be set)
# Content reference
content = models.ForeignKey(
'writer.Content',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='publishing_records',
help_text="Content being published (if publishing content)"
)
site_blueprint = models.ForeignKey(
'site_building.SiteBlueprint',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='publishing_records',
help_text="Site blueprint being published (if publishing site)"
help_text="Content being published"
)
# Destination information
@@ -80,18 +72,17 @@ class PublishingRecord(SiteSectorBaseModel):
indexes = [
models.Index(fields=['destination', 'status']),
models.Index(fields=['content', 'destination']),
models.Index(fields=['site_blueprint', 'destination']),
models.Index(fields=['account', 'status']),
]
def __str__(self):
target = self.content or self.site_blueprint
return f"{target}{self.destination} ({self.get_status_display()})"
return f"{self.content}{self.destination} ({self.get_status_display()})"
class DeploymentRecord(SiteSectorBaseModel):
"""
Track site deployments to Sites renderer.
Legacy model - SiteBlueprint functionality removed.
"""
STATUS_CHOICES = [
@@ -102,12 +93,7 @@ class DeploymentRecord(SiteSectorBaseModel):
('rolled_back', 'Rolled Back'),
]
site_blueprint = models.ForeignKey(
'site_building.SiteBlueprint',
on_delete=models.CASCADE,
related_name='deployments',
help_text="Site blueprint being deployed"
)
# Legacy: site_blueprint field removed - now using site from SiteSectorBaseModel directly
# Version tracking
version = models.IntegerField(
@@ -148,12 +134,12 @@ class DeploymentRecord(SiteSectorBaseModel):
db_table = 'igny8_deployment_records'
ordering = ['-created_at']
indexes = [
models.Index(fields=['site_blueprint', 'status']),
models.Index(fields=['site_blueprint', 'version']),
models.Index(fields=['status']),
models.Index(fields=['account', 'status']),
models.Index(fields=['site', 'status']),
]
def __str__(self):
return f"{self.site_blueprint.name} v{self.version} ({self.get_status_display()})"
return f"Deployment v{self.version} for {self.site.name if self.site else 'Unknown'} ({self.get_status_display()})"

View File

@@ -115,7 +115,7 @@ class SitesRendererAdapter(BaseAdapter):
Returns:
dict: Site definition structure
"""
from igny8_core.business.content.models import Tasks, Content, ContentClusterMap, ContentTaxonomyMap
from igny8_core.business.content.models import Tasks, Content, ContentClusterMap
# Get all pages
pages = []
@@ -129,8 +129,7 @@ class SitesRendererAdapter(BaseAdapter):
'cluster_id': None,
'cluster_name': None,
'content_structure': None,
'taxonomy_id': None,
'taxonomy_name': None,
'taxonomy_terms': [], # Changed from taxonomy_id/taxonomy_name to list of terms
'internal_links': []
}
@@ -180,11 +179,13 @@ class SitesRendererAdapter(BaseAdapter):
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 mapping
taxonomy_map = ContentTaxonomyMap.objects.filter(content=content).first()
if taxonomy_map and taxonomy_map.taxonomy:
page_metadata['taxonomy_id'] = taxonomy_map.taxonomy.id
page_metadata['taxonomy_name'] = taxonomy_map.taxonomy.name
# 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:

View File

@@ -1,22 +1,24 @@
"""
Publisher Service
Phase 5: Sites Renderer & Publishing
Phase 5: Content Publishing
Main publishing orchestrator for content and sites.
Main publishing orchestrator for content.
Legacy: SiteBlueprint publishing removed.
"""
import logging
from typing import Optional, List, Dict, Any
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
from igny8_core.business.site_building.models import SiteBlueprint
# Removed: from igny8_core.business.site_building.models import SiteBlueprint
logger = logging.getLogger(__name__)
class PublisherService:
"""
Main publishing service for content and sites.
Main publishing service for content.
Routes to appropriate adapters based on destination.
Legacy: SiteBlueprint publishing removed.
"""
def __init__(self):
@@ -27,36 +29,6 @@ class PublisherService:
"""Lazy load adapters to avoid circular imports"""
pass # Will be implemented when adapters are created
def publish_to_sites(self, site_blueprint: SiteBlueprint) -> Dict[str, Any]:
"""
Publish site to Sites renderer.
Args:
site_blueprint: SiteBlueprint instance to deploy
Returns:
dict: Deployment result with status and deployment record
"""
from igny8_core.business.publishing.services.adapters.sites_renderer_adapter import SitesRendererAdapter
adapter = SitesRendererAdapter()
return adapter.deploy(site_blueprint)
def get_deployment_status(self, site_blueprint: SiteBlueprint) -> Optional[DeploymentRecord]:
"""
Get deployment status for a site.
Args:
site_blueprint: SiteBlueprint instance
Returns:
DeploymentRecord or None
"""
from igny8_core.business.publishing.services.deployment_service import DeploymentService
service = DeploymentService()
return service.get_status(site_blueprint)
def publish_content(
self,
content_id: int,
@@ -216,7 +188,7 @@ class PublisherService:
Publish content to multiple destinations.
Args:
content: Content instance or SiteBlueprint
content: Content instance
destinations: List of destination configs, e.g.:
[
{'platform': 'wordpress', 'site_url': '...', 'username': '...', 'app_password': '...'},
@@ -272,8 +244,7 @@ class PublisherService:
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,
content=content if hasattr(content, 'id') else None,
destination=platform,
status='published' if result.get('success') else 'failed',
destination_id=result.get('external_id'),
@@ -319,7 +290,7 @@ class PublisherService:
Publish content using site integrations.
Args:
content: Content instance or SiteBlueprint
content: Content instance
site: Site instance
account: Account instance
platforms: Optional list of platforms to publish to (all active if None)

View File

@@ -1,250 +1,12 @@
"""
Admin interface for Site Builder models
Admin interface for Site Building
Legacy SiteBlueprint admin removed - models deprecated.
"""
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from igny8_core.admin.base import SiteSectorAdminMixin
from .models import (
SiteBlueprint,
PageBlueprint,
BusinessType,
AudienceProfile,
BrandPersonality,
HeroImageryDirection,
)
class PageBlueprintInline(admin.TabularInline):
"""Inline admin for Page Blueprints within Site Blueprint"""
model = PageBlueprint
extra = 0
fields = ['slug', 'title', 'type', 'status', 'order']
readonly_fields = ['slug', 'title', 'type', 'status', 'order']
can_delete = False
show_change_link = True
@admin.register(SiteBlueprint)
class SiteBlueprintAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
"""Admin interface for Site Blueprints"""
list_display = [
'name',
'get_site_display',
'get_sector_display',
'status',
'hosting_type',
'version',
'get_pages_count',
'created_at',
]
list_filter = ['status', 'hosting_type', 'site', 'sector', 'account', 'created_at']
search_fields = ['name', 'description', 'site__name', 'sector__name']
readonly_fields = ['created_at', 'updated_at', 'version', 'deployed_version']
ordering = ['-created_at']
inlines = [PageBlueprintInline]
fieldsets = (
('Basic Information', {
'fields': ('name', 'description', 'account', 'site', 'sector')
}),
('Status & Configuration', {
'fields': ('status', 'hosting_type', 'version', 'deployed_version')
}),
('Configuration Data', {
'fields': ('config_json',),
'classes': ('collapse',),
}),
('Structure Data', {
'fields': ('structure_json',),
'classes': ('collapse',),
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',),
}),
)
def get_site_display(self, obj):
"""Safely get site name"""
try:
if obj.site:
url = reverse('admin:igny8_core_auth_site_change', args=[obj.site.id])
return format_html('<a href="{}">{}</a>', url, obj.site.name)
return '-'
except:
return '-'
get_site_display.short_description = 'Site'
def get_sector_display(self, obj):
"""Safely get sector name"""
try:
return obj.sector.name if obj.sector else '-'
except:
return '-'
get_sector_display.short_description = 'Sector'
def get_pages_count(self, obj):
"""Get count of pages for this blueprint"""
try:
count = obj.pages.count()
if count > 0:
url = reverse('admin:site_building_pageblueprint_changelist')
return format_html('<a href="{}?site_blueprint__id__exact={}">{} pages</a>', url, obj.id, count)
return '0 pages'
except:
return '0 pages'
get_pages_count.short_description = 'Pages'
@admin.register(PageBlueprint)
class PageBlueprintAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
"""Admin interface for Page Blueprints"""
list_display = [
'title',
'slug',
'get_site_blueprint_display',
'type',
'status',
'order',
'get_site_display',
'get_sector_display',
'created_at',
]
list_filter = ['status', 'type', 'site_blueprint', 'site', 'sector', 'created_at']
search_fields = ['title', 'slug', 'site_blueprint__name', 'site__name']
readonly_fields = ['created_at', 'updated_at']
ordering = ['site_blueprint', 'order', 'created_at']
fieldsets = (
('Basic Information', {
'fields': ('site_blueprint', 'title', 'slug', 'type', 'status', 'order')
}),
('Account & Site', {
'fields': ('account', 'site', 'sector'),
'classes': ('collapse',),
}),
('Content Blocks', {
'fields': ('blocks_json',),
'classes': ('collapse',),
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',),
}),
)
def get_site_blueprint_display(self, obj):
"""Safely get site blueprint name with link"""
try:
if obj.site_blueprint:
url = reverse('admin:site_building_siteblueprint_change', args=[obj.site_blueprint.id])
return format_html('<a href="{}">{}</a>', url, obj.site_blueprint.name)
return '-'
except:
return '-'
get_site_blueprint_display.short_description = 'Site Blueprint'
def get_site_display(self, obj):
"""Safely get site name"""
try:
if obj.site:
url = reverse('admin:igny8_core_auth_site_change', args=[obj.site.id])
return format_html('<a href="{}">{}</a>', url, obj.site.name)
return '-'
except:
return '-'
get_site_display.short_description = 'Site'
def get_sector_display(self, obj):
"""Safely get sector name"""
try:
return obj.sector.name if obj.sector else '-'
except:
return '-'
get_sector_display.short_description = 'Sector'
@admin.register(BusinessType)
class BusinessTypeAdmin(admin.ModelAdmin):
"""Admin interface for Business Types"""
list_display = ['name', 'description', 'is_active', 'order', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['name', 'description']
list_editable = ['is_active', 'order']
ordering = ['order', 'name']
fieldsets = (
('Basic Information', {
'fields': ('name', 'description', 'is_active', 'order')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',),
}),
)
readonly_fields = ['created_at', 'updated_at']
@admin.register(AudienceProfile)
class AudienceProfileAdmin(admin.ModelAdmin):
"""Admin interface for Audience Profiles"""
list_display = ['name', 'description', 'is_active', 'order', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['name', 'description']
list_editable = ['is_active', 'order']
ordering = ['order', 'name']
fieldsets = (
('Basic Information', {
'fields': ('name', 'description', 'is_active', 'order')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',),
}),
)
readonly_fields = ['created_at', 'updated_at']
@admin.register(BrandPersonality)
class BrandPersonalityAdmin(admin.ModelAdmin):
"""Admin interface for Brand Personalities"""
list_display = ['name', 'description', 'is_active', 'order', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['name', 'description']
list_editable = ['is_active', 'order']
ordering = ['order', 'name']
fieldsets = (
('Basic Information', {
'fields': ('name', 'description', 'is_active', 'order')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',),
}),
)
readonly_fields = ['created_at', 'updated_at']
@admin.register(HeroImageryDirection)
class HeroImageryDirectionAdmin(admin.ModelAdmin):
"""Admin interface for Hero Imagery Directions"""
list_display = ['name', 'description', 'is_active', 'order', 'created_at']
list_filter = ['is_active', 'created_at']
search_fields = ['name', 'description']
list_editable = ['is_active', 'order']
ordering = ['order', 'name']
fieldsets = (
('Basic Information', {
'fields': ('name', 'description', 'is_active', 'order')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',),
}),
)
readonly_fields = ['created_at', 'updated_at']
# All SiteBuilder admin classes removed:
# - SiteBlueprintAdmin
# - PageBlueprintAdmin
# - BusinessTypeAdmin, AudienceProfileAdmin, BrandPersonalityAdmin, HeroImageryDirectionAdmin
#
# Site Builder functionality has been deprecated and removed from the system.

View File

@@ -0,0 +1,53 @@
# Generated manually on 2025-12-01
# Remove SiteBlueprint, PageBlueprint, SiteBlueprintCluster, and SiteBlueprintTaxonomy models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('site_building', '0001_initial'), # Changed from 0002_initial
]
operations = [
# Drop tables in reverse dependency order
migrations.RunSQL(
sql=[
# Drop foreign key constraints first
"ALTER TABLE igny8_publishing_records DROP CONSTRAINT IF EXISTS igny8_publishing_recor_site_blueprint_id_9f4e8c7a_fk_igny8_sit CASCADE;",
"ALTER TABLE igny8_deployment_records DROP CONSTRAINT IF EXISTS igny8_deployment_recor_site_blueprint_id_3a2b7c1d_fk_igny8_sit CASCADE;",
# Drop the tables
"DROP TABLE IF EXISTS igny8_site_blueprint_taxonomies CASCADE;",
"DROP TABLE IF EXISTS igny8_site_blueprint_clusters CASCADE;",
"DROP TABLE IF EXISTS igny8_page_blueprints CASCADE;",
"DROP TABLE IF EXISTS igny8_site_blueprints CASCADE;",
"DROP TABLE IF EXISTS igny8_site_builder_business_types CASCADE;",
"DROP TABLE IF EXISTS igny8_site_builder_audience_profiles CASCADE;",
"DROP TABLE IF EXISTS igny8_site_builder_brand_personalities CASCADE;",
"DROP TABLE IF EXISTS igny8_site_builder_hero_imagery CASCADE;",
],
reverse_sql=[
# Reverse migration not supported - this is a destructive operation
"SELECT 1;"
],
),
# Also drop the site_blueprint_id column from PublishingRecord
migrations.RunSQL(
sql=[
"ALTER TABLE igny8_publishing_records DROP COLUMN IF EXISTS site_blueprint_id CASCADE;",
"DROP INDEX IF EXISTS igny8_publishing_recor_site_blueprint_id_des_b7c4e5f8_idx;",
],
reverse_sql=["SELECT 1;"],
),
# Drop the site_blueprint_id column from DeploymentRecord
migrations.RunSQL(
sql=[
"ALTER TABLE igny8_deployment_records DROP COLUMN IF EXISTS site_blueprint_id CASCADE;",
],
reverse_sql=["SELECT 1;"],
),
]

View File

@@ -1,346 +1,15 @@
"""
Site Builder Models
Phase 3: Site Builder
Site Building Models
Legacy SiteBuilder module has been removed.
This file is kept for backwards compatibility with migrations.
"""
from django.db import models
from django.core.validators import MinValueValidator
from igny8_core.auth.models import SiteSectorBaseModel
class SiteBlueprint(SiteSectorBaseModel):
"""
Site Blueprint model for storing AI-generated site structures.
"""
STATUS_CHOICES = [
('draft', 'Draft'),
('generating', 'Generating'),
('ready', 'Ready'),
('deployed', 'Deployed'),
]
HOSTING_TYPE_CHOICES = [
('igny8_sites', 'IGNY8 Sites'),
('wordpress', 'WordPress'),
('shopify', 'Shopify'),
('multi', 'Multiple Destinations'),
]
name = models.CharField(max_length=255, help_text="Site name")
description = models.TextField(blank=True, null=True, help_text="Site description")
# Site configuration (from wizard)
config_json = models.JSONField(
default=dict,
help_text="Wizard configuration: business_type, style, objectives, etc."
)
# Generated structure (from AI)
structure_json = models.JSONField(
default=dict,
help_text="AI-generated structure: pages, layout, theme, etc."
)
# Status tracking
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft',
db_index=True,
help_text="Blueprint status"
)
# Hosting configuration
hosting_type = models.CharField(
max_length=50,
choices=HOSTING_TYPE_CHOICES,
default='igny8_sites',
help_text="Target hosting platform"
)
# Version tracking
version = models.IntegerField(default=1, validators=[MinValueValidator(1)], help_text="Blueprint version")
deployed_version = models.IntegerField(null=True, blank=True, help_text="Currently deployed version")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'site_building'
db_table = 'igny8_site_blueprints'
ordering = ['-created_at']
verbose_name = 'Site Blueprint'
verbose_name_plural = 'Site Blueprints'
indexes = [
models.Index(fields=['status']),
models.Index(fields=['hosting_type']),
models.Index(fields=['site', 'sector']),
models.Index(fields=['account', 'status']),
]
def __str__(self):
return f"{self.name} ({self.get_status_display()})"
class PageBlueprint(SiteSectorBaseModel):
"""
Page Blueprint model for storing individual page definitions.
"""
PAGE_TYPE_CHOICES = [
('home', 'Home'),
('about', 'About'),
('services', 'Services'),
('products', 'Products'),
('blog', 'Blog'),
('contact', 'Contact'),
('custom', 'Custom'),
]
STATUS_CHOICES = [
('draft', 'Draft'),
('generating', 'Generating'),
('ready', 'Ready'),
('published', 'Published'),
]
site_blueprint = models.ForeignKey(
SiteBlueprint,
on_delete=models.CASCADE,
related_name='pages',
help_text="The site blueprint this page belongs to"
)
slug = models.SlugField(max_length=255, help_text="Page URL slug")
title = models.CharField(max_length=255, help_text="Page title")
# Page type
type = models.CharField(
max_length=50,
choices=PAGE_TYPE_CHOICES,
default='custom',
help_text="Page type"
)
# Page content (blocks)
blocks_json = models.JSONField(
default=list,
help_text="Page content blocks: [{'type': 'hero', 'data': {...}}, ...]"
)
# Status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='draft',
db_index=True,
help_text="Page status"
)
# Order
order = models.IntegerField(default=0, help_text="Page order in navigation")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'site_building'
db_table = 'igny8_page_blueprints'
ordering = ['order', 'created_at']
verbose_name = 'Page Blueprint'
verbose_name_plural = 'Page Blueprints'
unique_together = [['site_blueprint', 'slug']]
indexes = [
models.Index(fields=['site_blueprint', 'status']),
models.Index(fields=['type']),
models.Index(fields=['site_blueprint', 'order']),
]
def save(self, *args, **kwargs):
"""Automatically set account, site, and sector from site_blueprint"""
if self.site_blueprint:
self.account = self.site_blueprint.account
self.site = self.site_blueprint.site
self.sector = self.site_blueprint.sector
super().save(*args, **kwargs)
def __str__(self):
return f"{self.title} ({self.site_blueprint.name})"
class SiteBlueprintCluster(SiteSectorBaseModel):
"""
Mapping table that connects planner clusters to site blueprints
with role/coverage metadata.
"""
ROLE_CHOICES = [
('hub', 'Hub Page'),
('supporting', 'Supporting Page'),
('attribute', 'Attribute Page'),
]
COVERAGE_CHOICES = [
('pending', 'Pending'),
('in_progress', 'In Progress'),
('complete', 'Complete'),
]
site_blueprint = models.ForeignKey(
SiteBlueprint,
on_delete=models.CASCADE,
related_name='cluster_links',
help_text="Site blueprint that is planning coverage for the cluster",
)
cluster = models.ForeignKey(
'planner.Clusters',
on_delete=models.CASCADE,
related_name='blueprint_links',
help_text="Planner cluster being mapped into the site blueprint",
)
role = models.CharField(max_length=50, choices=ROLE_CHOICES, default='hub')
coverage_status = models.CharField(max_length=50, choices=COVERAGE_CHOICES, default='pending')
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional coverage metadata (target pages, keyword counts, ai hints)",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'site_building'
db_table = 'igny8_site_blueprint_clusters'
unique_together = [['site_blueprint', 'cluster', 'role']]
verbose_name = 'Site Blueprint Cluster'
verbose_name_plural = 'Site Blueprint Clusters'
indexes = [
models.Index(fields=['site_blueprint', 'cluster']),
models.Index(fields=['site_blueprint', 'coverage_status']),
models.Index(fields=['cluster', 'role']),
]
def save(self, *args, **kwargs):
if self.site_blueprint:
self.account = self.site_blueprint.account
self.site = self.site_blueprint.site
self.sector = self.site_blueprint.sector
super().save(*args, **kwargs)
def __str__(self):
return f"{self.site_blueprint.name}{self.cluster.name}"
class SiteBlueprintTaxonomy(SiteSectorBaseModel):
"""
Taxonomy blueprint entity for categories/tags/product attributes tied to clusters.
"""
TAXONOMY_TYPE_CHOICES = [
('blog_category', 'Blog Category'),
('blog_tag', 'Blog Tag'),
('product_category', 'Product Category'),
('product_tag', 'Product Tag'),
('product_attribute', 'Product Attribute'),
('service_category', 'Service Category'),
]
site_blueprint = models.ForeignKey(
SiteBlueprint,
on_delete=models.CASCADE,
related_name='taxonomies',
help_text="Site blueprint this taxonomy belongs to",
)
name = models.CharField(max_length=255, help_text="Display name")
slug = models.SlugField(max_length=255, help_text="Slug/identifier within the site blueprint")
taxonomy_type = models.CharField(max_length=50, choices=TAXONOMY_TYPE_CHOICES, default='blog_category')
description = models.TextField(blank=True, null=True)
metadata = models.JSONField(default=dict, blank=True, help_text="Additional taxonomy metadata or AI hints")
clusters = models.ManyToManyField(
'planner.Clusters',
blank=True,
related_name='blueprint_taxonomies',
help_text="Planner clusters that this taxonomy maps to",
)
external_reference = models.CharField(
max_length=255,
blank=True,
null=True,
help_text="External system ID (WordPress/WooCommerce/etc.)",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'site_building'
db_table = 'igny8_site_blueprint_taxonomies'
unique_together = [['site_blueprint', 'slug']]
verbose_name = 'Site Blueprint Taxonomy'
verbose_name_plural = 'Site Blueprint Taxonomies'
indexes = [
models.Index(fields=['site_blueprint', 'taxonomy_type']),
models.Index(fields=['taxonomy_type']),
]
def save(self, *args, **kwargs):
if self.site_blueprint:
self.account = self.site_blueprint.account
self.site = self.site_blueprint.site
self.sector = self.site_blueprint.sector
super().save(*args, **kwargs)
def __str__(self):
return f"{self.name} ({self.get_taxonomy_type_display()})"
class SiteBuilderOption(models.Model):
"""
Base model for Site Builder dropdown metadata.
"""
name = models.CharField(max_length=120, unique=True)
description = models.CharField(max_length=255, blank=True)
is_active = models.BooleanField(default=True)
order = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
ordering = ['order', 'name']
def __str__(self):
return self.name
class BusinessType(SiteBuilderOption):
class Meta(SiteBuilderOption.Meta):
app_label = 'site_building'
db_table = 'igny8_site_builder_business_types'
verbose_name = 'Business Type'
verbose_name_plural = 'Business Types'
class AudienceProfile(SiteBuilderOption):
class Meta(SiteBuilderOption.Meta):
app_label = 'site_building'
db_table = 'igny8_site_builder_audience_profiles'
verbose_name = 'Audience Profile'
verbose_name_plural = 'Audience Profiles'
class BrandPersonality(SiteBuilderOption):
class Meta(SiteBuilderOption.Meta):
app_label = 'site_building'
db_table = 'igny8_site_builder_brand_personalities'
verbose_name = 'Brand Personality'
verbose_name_plural = 'Brand Personalities'
class HeroImageryDirection(SiteBuilderOption):
class Meta(SiteBuilderOption.Meta):
app_label = 'site_building'
db_table = 'igny8_site_builder_hero_imagery'
verbose_name = 'Hero Imagery Direction'
verbose_name_plural = 'Hero Imagery Directions'
# All SiteBuilder models have been removed:
# - SiteBlueprint
# - PageBlueprint
# - SiteBlueprintCluster
# - SiteBlueprintTaxonomy
# - BusinessType, AudienceProfile, BrandPersonality, HeroImageryDirection
#
# Taxonomy functionality moved to ContentTaxonomy model in business/content/models.py

View File

@@ -11,15 +11,11 @@ class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
('planner', '0001_initial'),
('site_building', '0001_initial'),
# ('site_building', '0001_initial'), # REMOVED: SiteBuilder deprecated
]
operations = [
migrations.AddField(
model_name='contentideas',
name='taxonomy',
field=models.ForeignKey(blank=True, help_text='Optional taxonomy association when derived from blueprint planning', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='content_ideas', to='site_building.siteblueprinttaxonomy'),
),
# REMOVED: ContentIdeas.taxonomy FK to SiteBlueprintTaxonomy (legacy blueprint)
migrations.AddField(
model_name='keywords',
name='account',

View File

@@ -8,7 +8,7 @@ class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0002_add_wp_api_key_to_site'),
('planner', '0004_remove_clusters_igny8_clust_context_0d6bd7_idx_and_more'),
('site_building', '0001_initial'),
# ('site_building', '0001_initial'), # REMOVED: SiteBuilder deprecated
]
operations = [

View File

@@ -4,7 +4,7 @@ from django.conf import settings
from .models import Keywords, Clusters, ContentIdeas
from igny8_core.auth.models import SeedKeyword
from igny8_core.auth.serializers import SeedKeywordSerializer
from igny8_core.business.site_building.models import SiteBlueprintTaxonomy
# Removed: from igny8_core.business.site_building.models import SiteBlueprintTaxonomy
class KeywordSerializer(serializers.ModelSerializer):
@@ -209,10 +209,13 @@ class ContentIdeasSerializer(serializers.ModelSerializer):
return None
def get_taxonomy_name(self, obj):
"""Legacy: SiteBlueprintTaxonomy removed - taxonomy now in ContentTaxonomy"""
if obj.taxonomy_id:
try:
taxonomy = SiteBlueprintTaxonomy.objects.get(id=obj.taxonomy_id)
from igny8_core.business.content.models import ContentTaxonomy
taxonomy = ContentTaxonomy.objects.get(id=obj.taxonomy_id)
return taxonomy.name
except SiteBlueprintTaxonomy.DoesNotExist:
except ContentTaxonomy.DoesNotExist:
return None
return None

View File

@@ -18,27 +18,25 @@ from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
from igny8_core.business.publishing.services.publisher_service import PublisherService
from igny8_core.business.publishing.services.deployment_readiness_service import DeploymentReadinessService
from igny8_core.business.site_building.models import SiteBlueprint
class PublishingRecordViewSet(SiteSectorModelViewSet):
"""
ViewSet for PublishingRecord model.
"""
queryset = PublishingRecord.objects.select_related('content', 'site_blueprint')
queryset = PublishingRecord.objects.select_related('content')
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
throttle_classes = [DebugScopedRateThrottle]
def get_serializer_class(self):
# Will be created in next step
# Dynamically create serializer
from rest_framework import serializers
class PublishingRecordSerializer(serializers.ModelSerializer):
class Meta:
model = PublishingRecord
fields = '__all__'
exclude = [] # Legacy: site_blueprint field removed from model
return PublishingRecordSerializer
@@ -46,27 +44,29 @@ class PublishingRecordViewSet(SiteSectorModelViewSet):
class DeploymentRecordViewSet(SiteSectorModelViewSet):
"""
ViewSet for DeploymentRecord model.
Legacy: SiteBlueprint functionality removed.
"""
queryset = DeploymentRecord.objects.select_related('site_blueprint')
queryset = DeploymentRecord.objects.all()
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
throttle_classes = [DebugScopedRateThrottle]
def get_serializer_class(self):
# Will be created in next step
# Dynamically create serializer
from rest_framework import serializers
class DeploymentRecordSerializer(serializers.ModelSerializer):
class Meta:
model = DeploymentRecord
fields = '__all__'
exclude = [] # Legacy: site_blueprint field removed from model
return DeploymentRecordSerializer
class PublisherViewSet(viewsets.ViewSet):
"""
Publisher actions for publishing content and sites.
Publisher actions for publishing content.
Legacy SiteBlueprint publishing removed - only content publishing supported.
"""
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
throttle_scope = 'publisher'
@@ -75,29 +75,33 @@ class PublisherViewSet(viewsets.ViewSet):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.publisher_service = PublisherService()
self.readiness_service = DeploymentReadinessService()
@action(detail=False, methods=['post'], url_path='publish')
def publish(self, request):
"""
Publish content or site to destinations.
Publish content to destinations.
Request body:
{
"content_id": 123, # Optional: content to publish
"site_blueprint_id": 456, # Optional: site to publish
"destinations": ["wordpress", "sites"] # Required: list of destinations
"content_id": 123, # Required: content to publish
"destinations": ["wordpress"] # Required: list of destinations
}
"""
import logging
logger = logging.getLogger(__name__)
content_id = request.data.get('content_id')
site_blueprint_id = request.data.get('site_blueprint_id')
destinations = request.data.get('destinations', [])
logger.info(f"[PublisherViewSet.publish] 🚀 Publish request received: content_id={content_id}, destinations={destinations}")
if not content_id:
return error_response(
'content_id is required',
status.HTTP_400_BAD_REQUEST,
request
)
if not destinations:
return error_response(
'destinations is required',
@@ -107,136 +111,26 @@ class PublisherViewSet(viewsets.ViewSet):
account = request.account
if site_blueprint_id:
# Publish site
try:
blueprint = SiteBlueprint.objects.get(id=site_blueprint_id, account=account)
except SiteBlueprint.DoesNotExist:
return error_response(
f'Site blueprint {site_blueprint_id} not found',
status.HTTP_404_NOT_FOUND,
request
)
if 'sites' in destinations:
result = self.publisher_service.publish_to_sites(blueprint)
return success_response(result, request=request)
else:
return error_response(
'Site publishing only supports "sites" destination',
status.HTTP_400_BAD_REQUEST,
request
)
elif content_id:
# Publish content
logger.info(f"[PublisherViewSet.publish] 📝 Publishing content {content_id} to {destinations}")
result = self.publisher_service.publish_content(
content_id,
destinations,
account
)
logger.info(f"[PublisherViewSet.publish] {'' if result.get('success') else ''} Publish result: {result}")
return success_response(result, request=request)
else:
return error_response(
'Either content_id or site_blueprint_id is required',
status.HTTP_400_BAD_REQUEST,
request
)
@action(detail=False, methods=['get'], url_path='blueprints/(?P<blueprint_id>[^/.]+)/readiness')
def deployment_readiness(self, request, blueprint_id):
"""
Check deployment readiness for a site blueprint.
Stage 4: Pre-deployment validation checks.
GET /api/v1/publisher/blueprints/{blueprint_id}/readiness/
"""
account = request.account
try:
blueprint = SiteBlueprint.objects.get(id=blueprint_id, account=account)
except SiteBlueprint.DoesNotExist:
return error_response(
f'Site blueprint {blueprint_id} not found',
status.HTTP_404_NOT_FOUND,
request
)
readiness = self.readiness_service.check_readiness(blueprint_id)
return success_response(readiness, request=request)
@action(detail=False, methods=['post'], url_path='deploy/(?P<blueprint_id>[^/.]+)')
def deploy(self, request, blueprint_id):
"""
Deploy site blueprint to Sites renderer.
Stage 4: Enhanced with readiness check (optional).
POST /api/v1/publisher/deploy/{blueprint_id}/
Request body (optional):
{
"skip_readiness_check": false # Set to true to skip readiness validation
}
"""
account = request.account
try:
blueprint = SiteBlueprint.objects.get(id=blueprint_id, account=account)
except SiteBlueprint.DoesNotExist:
return error_response(
f'Site blueprint {blueprint_id} not found',
status.HTTP_404_NOT_FOUND,
request
)
# Stage 4: Optional readiness check
skip_check = request.data.get('skip_readiness_check', False)
if not skip_check:
readiness = self.readiness_service.check_readiness(blueprint_id)
if not readiness.get('ready'):
return error_response(
{
'message': 'Site is not ready for deployment',
'readiness': readiness
},
status.HTTP_400_BAD_REQUEST,
request
)
result = self.publisher_service.publish_to_sites(blueprint)
response_status = status.HTTP_202_ACCEPTED if result.get('success') else status.HTTP_400_BAD_REQUEST
return success_response(result, request=request, status_code=response_status)
# Publish content
logger.info(f"[PublisherViewSet.publish] 📝 Publishing content {content_id} to {destinations}")
result = self.publisher_service.publish_content(
content_id,
destinations,
account
)
logger.info(f"[PublisherViewSet.publish] {'' if result.get('success') else ''} Publish result: {result}")
return success_response(result, request=request)
@action(detail=False, methods=['get'], url_path='status/(?P<id>[^/.]+)')
def get_status(self, request, id):
"""
Get publishing/deployment status.
Get publishing status.
GET /api/v1/publisher/status/{id}/
"""
account = request.account
# Try deployment record first
try:
deployment = DeploymentRecord.objects.get(id=id, account=account)
return success_response({
'type': 'deployment',
'status': deployment.status,
'version': deployment.version,
'deployed_version': deployment.deployed_version,
'deployment_url': deployment.deployment_url,
'deployed_at': deployment.deployed_at,
'error_message': deployment.error_message,
}, request=request)
except DeploymentRecord.DoesNotExist:
pass
# Try publishing record
# Get publishing record
try:
publishing = PublishingRecord.objects.get(id=id, account=account)
return success_response({

View File

@@ -12,7 +12,7 @@ class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
('planner', '0001_initial'),
('site_building', '0001_initial'),
# ('site_building', '0001_initial'), # REMOVED: SiteBuilder deprecated
]
operations = [

View File

@@ -53,11 +53,11 @@ INSTALLED_APPS = [
'igny8_core.modules.system.apps.SystemConfig',
'igny8_core.modules.billing.apps.BillingConfig',
'igny8_core.modules.automation.apps.AutomationConfig',
'igny8_core.business.site_building.apps.SiteBuildingConfig',
# 'igny8_core.business.site_building.apps.SiteBuildingConfig', # REMOVED: SiteBuilder/Blueprint deprecated
'igny8_core.business.optimization.apps.OptimizationConfig',
'igny8_core.business.publishing.apps.PublishingConfig',
'igny8_core.business.integration.apps.IntegrationConfig',
'igny8_core.modules.site_builder.apps.SiteBuilderConfig',
# 'igny8_core.modules.site_builder.apps.SiteBuilderConfig', # REMOVED: SiteBuilder deprecated
'igny8_core.modules.linker.apps.LinkerConfig',
'igny8_core.modules.optimizer.apps.OptimizerConfig',
'igny8_core.modules.publisher.apps.PublisherConfig',

View File

@@ -37,7 +37,7 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
start_time = time.time()
try:
from igny8_core.business.content.models import Content, ContentTaxonomyMap
from igny8_core.business.content.models import Content
from igny8_core.business.integration.models import SiteIntegration, SyncEvent
from igny8_core.modules.writer.models import Images
from django.utils.html import strip_tags
@@ -100,47 +100,75 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
else:
publish_logger.warning(f" {log_prefix} ⚠️ No content_html found - excerpt will be empty")
# STEP 4: Get taxonomy terms (categories)
publish_logger.info(f"{log_prefix} STEP 4: Loading taxonomy mappings for categories...")
taxonomy_maps = ContentTaxonomyMap.objects.filter(content=content).select_related('taxonomy')
publish_logger.info(f" {log_prefix} Found {taxonomy_maps.count()} taxonomy mappings")
# STEP 4: Get taxonomy terms (categories and tags)
publish_logger.info(f"{log_prefix} STEP 4: Loading taxonomy terms from Content.taxonomy_terms...")
categories = []
for mapping in taxonomy_maps:
if mapping.taxonomy:
categories.append(mapping.taxonomy.name)
publish_logger.info(f" {log_prefix} 📁 Category: '{mapping.taxonomy.name}'")
# Get categories from ContentTaxonomy many-to-many relationship
# This is the CORRECT way - matching ContentSerializer.get_categories()
categories = [
term.name
for term in content.taxonomy_terms.filter(taxonomy_type='category')
]
publish_logger.info(f" {log_prefix} Found {len(categories)} categories from taxonomy_terms")
for cat in categories:
publish_logger.info(f" {log_prefix} 📁 Category: '{cat}'")
# FALLBACK: If no categories from taxonomy_terms, use Cluster as category (matches UI display)
if not categories and content.cluster:
categories.append(content.cluster.name)
publish_logger.info(f" {log_prefix} 📁 Category (fallback from cluster): '{content.cluster.name}'")
if not categories:
publish_logger.warning(f" {log_prefix} ⚠️ No categories found for content")
else:
publish_logger.info(f" {log_prefix} ✅ TOTAL categories: {len(categories)}")
# STEP 5: Get keywords as tags
publish_logger.info(f"{log_prefix} STEP 5: Extracting keywords as tags...")
tags = []
# STEP 5: Get tags from taxonomy_terms AND keywords
publish_logger.info(f"{log_prefix} STEP 5: Loading tags from taxonomy_terms and keywords...")
# First, get tags from ContentTaxonomy many-to-many relationship
# This matches ContentSerializer.get_tags()
tags = [
term.name
for term in content.taxonomy_terms.filter(taxonomy_type='tag')
]
publish_logger.info(f" {log_prefix} Found {len(tags)} tags from taxonomy_terms")
for tag in tags:
publish_logger.info(f" {log_prefix} 🏷️ Tag: '{tag}'")
# Add primary keyword as tag (if not already in tags)
if content.primary_keyword:
tags.append(content.primary_keyword)
publish_logger.info(f" {log_prefix} 🏷️ Primary keyword: '{content.primary_keyword}'")
if content.primary_keyword not in tags:
tags.append(content.primary_keyword)
publish_logger.info(f" {log_prefix} 🏷️ Primary keyword: '{content.primary_keyword}'")
else:
publish_logger.warning(f" {log_prefix} ⚠️ No primary keyword found")
# Add secondary keywords as tags (if not already in tags)
if content.secondary_keywords:
if isinstance(content.secondary_keywords, list):
tags.extend(content.secondary_keywords)
for keyword in content.secondary_keywords:
if keyword not in tags:
tags.append(keyword)
publish_logger.info(f" {log_prefix} 🏷️ Secondary keywords (list): {content.secondary_keywords}")
elif isinstance(content.secondary_keywords, str):
try:
keywords = json.loads(content.secondary_keywords)
if isinstance(keywords, list):
tags.extend(keywords)
for keyword in keywords:
if keyword not in tags:
tags.append(keyword)
publish_logger.info(f" {log_prefix} 🏷️ Secondary keywords (JSON): {keywords}")
except (json.JSONDecodeError, TypeError):
publish_logger.warning(f" {log_prefix} ⚠️ Failed to parse secondary_keywords as JSON: {content.secondary_keywords}")
else:
publish_logger.warning(f" {log_prefix} ⚠️ No secondary keywords found")
publish_logger.info(f" {log_prefix} ✅ TOTAL tags: {len(tags)}")
if not tags:
publish_logger.warning(f" {log_prefix} ⚠️ No tags found for content")
else:
@@ -154,17 +182,30 @@ def publish_content_to_wordpress(self, content_id: int, site_integration_id: int
featured_image_url = None
gallery_images = []
def convert_image_path_to_url(image_path):
"""Convert local image path to public URL"""
if not image_path:
return None
# Convert: /data/app/igny8/frontend/public/images/... -> https://app.igny8.com/images/...
if '/frontend/public/' in image_path:
relative_path = image_path.split('/frontend/public/')[-1]
return f"https://app.igny8.com/{relative_path}"
return image_path
for image in images:
if image.image_type == 'featured' and image.image_url:
featured_image_url = image.image_url
publish_logger.info(f" {log_prefix} 🖼️ Featured image: {image.image_url[:80]}...")
elif image.image_type == 'in_article' and image.image_url:
# Use image_path (local file) and convert to public URL
image_url = convert_image_path_to_url(image.image_path) if hasattr(image, 'image_path') and image.image_path else None
if image.image_type == 'featured' and image_url:
featured_image_url = image_url
publish_logger.info(f" {log_prefix} 🖼️ Featured image: {image_url[:80]}...")
elif image.image_type == 'in_article' and image_url:
gallery_images.append({
'url': image.image_url,
'alt': image.alt_text or '',
'caption': image.caption or ''
'url': image_url,
'alt': getattr(image, 'alt', '') or '',
'caption': getattr(image, 'caption', '') or ''
})
publish_logger.info(f" {log_prefix} 🖼️ Gallery image {len(gallery_images)}: {image.image_url[:60]}...")
publish_logger.info(f" {log_prefix} 🖼️ Gallery image {len(gallery_images)}: {image_url[:60]}...")
if not featured_image_url:
publish_logger.warning(f" {log_prefix} ⚠️ No featured image found")

View File

@@ -39,7 +39,7 @@ urlpatterns = [
path('api/v1/auth/', include('igny8_core.auth.urls')), # Auth endpoints
path('api/v1/planner/', include('igny8_core.modules.planner.urls')),
path('api/v1/writer/', include('igny8_core.modules.writer.urls')),
path('api/v1/site-builder/', include('igny8_core.modules.site_builder.urls')),
# Site Builder module removed - legacy blueprint functionality deprecated
path('api/v1/system/', include('igny8_core.modules.system.urls')),
path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Billing endpoints
path('api/v1/automation/', include('igny8_core.modules.automation.urls')), # Automation endpoints