blurpritn adn site builde cleanup
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)"""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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()})"
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;"],
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user