refactor stage 1
This commit is contained in:
@@ -287,3 +287,171 @@ class Images(SiteSectorBaseModel):
|
|||||||
title = content_title or task_title or 'Unknown'
|
title = content_title or task_title or 'Unknown'
|
||||||
return f"{title} - {self.image_type}"
|
return f"{title} - {self.image_type}"
|
||||||
|
|
||||||
|
|
||||||
|
class ContentClusterMap(SiteSectorBaseModel):
|
||||||
|
"""Associates generated content with planner clusters + roles."""
|
||||||
|
|
||||||
|
ROLE_CHOICES = [
|
||||||
|
('hub', 'Hub Page'),
|
||||||
|
('supporting', 'Supporting Page'),
|
||||||
|
('attribute', 'Attribute Page'),
|
||||||
|
]
|
||||||
|
|
||||||
|
SOURCE_CHOICES = [
|
||||||
|
('blueprint', 'Blueprint'),
|
||||||
|
('manual', 'Manual'),
|
||||||
|
('import', 'Import'),
|
||||||
|
]
|
||||||
|
|
||||||
|
content = models.ForeignKey(
|
||||||
|
Content,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='cluster_mappings',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
task = models.ForeignKey(
|
||||||
|
Tasks,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='cluster_mappings',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
cluster = models.ForeignKey(
|
||||||
|
'planner.Clusters',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='content_mappings',
|
||||||
|
)
|
||||||
|
role = models.CharField(max_length=50, choices=ROLE_CHOICES, default='hub')
|
||||||
|
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_cluster_map'
|
||||||
|
unique_together = [['content', 'cluster', 'role']]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['cluster', 'role']),
|
||||||
|
models.Index(fields=['content', 'role']),
|
||||||
|
models.Index(fields=['task', 'role']),
|
||||||
|
]
|
||||||
|
|
||||||
|
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.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 ContentAttributeMap(SiteSectorBaseModel):
|
||||||
|
"""Stores structured attribute data tied to content/task records."""
|
||||||
|
|
||||||
|
SOURCE_CHOICES = [
|
||||||
|
('blueprint', 'Blueprint'),
|
||||||
|
('manual', 'Manual'),
|
||||||
|
('import', 'Import'),
|
||||||
|
]
|
||||||
|
|
||||||
|
content = models.ForeignKey(
|
||||||
|
Content,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='attribute_mappings',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
task = models.ForeignKey(
|
||||||
|
Tasks,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='attribute_mappings',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=120)
|
||||||
|
value = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
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_attribute_map'
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['name']),
|
||||||
|
models.Index(fields=['content', 'name']),
|
||||||
|
models.Index(fields=['task', 'name']),
|
||||||
|
]
|
||||||
|
|
||||||
|
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):
|
||||||
|
target = self.content or self.task
|
||||||
|
return f"{target} – {self.name}"
|
||||||
|
|||||||
@@ -5,12 +5,29 @@ from igny8_core.auth.models import SiteSectorBaseModel, SeedKeyword
|
|||||||
class Clusters(SiteSectorBaseModel):
|
class Clusters(SiteSectorBaseModel):
|
||||||
"""Clusters model for keyword grouping"""
|
"""Clusters model for keyword grouping"""
|
||||||
|
|
||||||
|
CONTEXT_TYPE_CHOICES = [
|
||||||
|
('topic', 'Topic Cluster'),
|
||||||
|
('attribute', 'Attribute Cluster'),
|
||||||
|
('service_line', 'Service Line'),
|
||||||
|
]
|
||||||
|
|
||||||
name = models.CharField(max_length=255, unique=True, db_index=True)
|
name = models.CharField(max_length=255, unique=True, db_index=True)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
keywords_count = models.IntegerField(default=0)
|
keywords_count = models.IntegerField(default=0)
|
||||||
volume = models.IntegerField(default=0)
|
volume = models.IntegerField(default=0)
|
||||||
mapped_pages = models.IntegerField(default=0)
|
mapped_pages = models.IntegerField(default=0)
|
||||||
status = models.CharField(max_length=50, default='active')
|
status = models.CharField(max_length=50, default='active')
|
||||||
|
context_type = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=CONTEXT_TYPE_CHOICES,
|
||||||
|
default='topic',
|
||||||
|
help_text="Primary dimension for this cluster (topic, attribute, service line)"
|
||||||
|
)
|
||||||
|
dimension_meta = models.JSONField(
|
||||||
|
default=dict,
|
||||||
|
blank=True,
|
||||||
|
help_text="Extended metadata (taxonomy hints, attribute suggestions, coverage targets)"
|
||||||
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
@@ -24,6 +41,7 @@ class Clusters(SiteSectorBaseModel):
|
|||||||
models.Index(fields=['name']),
|
models.Index(fields=['name']),
|
||||||
models.Index(fields=['status']),
|
models.Index(fields=['status']),
|
||||||
models.Index(fields=['site', 'sector']),
|
models.Index(fields=['site', 'sector']),
|
||||||
|
models.Index(fields=['context_type']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -61,6 +79,11 @@ class Keywords(SiteSectorBaseModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
help_text="Site-specific difficulty override (uses seed_keyword.difficulty if not set)"
|
help_text="Site-specific difficulty override (uses seed_keyword.difficulty if not set)"
|
||||||
)
|
)
|
||||||
|
attribute_values = models.JSONField(
|
||||||
|
default=list,
|
||||||
|
blank=True,
|
||||||
|
help_text="Optional attribute metadata (e.g., product specs, service modifiers)"
|
||||||
|
)
|
||||||
|
|
||||||
cluster = models.ForeignKey(
|
cluster = models.ForeignKey(
|
||||||
'Clusters',
|
'Clusters',
|
||||||
@@ -154,6 +177,20 @@ class ContentIdeas(SiteSectorBaseModel):
|
|||||||
('guide', 'Guide'),
|
('guide', 'Guide'),
|
||||||
('tutorial', 'Tutorial'),
|
('tutorial', 'Tutorial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SITE_ENTITY_TYPE_CHOICES = [
|
||||||
|
('page', 'Site Page'),
|
||||||
|
('blog_post', 'Blog Post'),
|
||||||
|
('product', 'Product'),
|
||||||
|
('service', 'Service'),
|
||||||
|
('taxonomy', 'Taxonomy Page'),
|
||||||
|
]
|
||||||
|
|
||||||
|
CLUSTER_ROLE_CHOICES = [
|
||||||
|
('hub', 'Hub Page'),
|
||||||
|
('supporting', 'Supporting Page'),
|
||||||
|
('attribute', 'Attribute Page'),
|
||||||
|
]
|
||||||
|
|
||||||
idea_title = models.CharField(max_length=255, db_index=True)
|
idea_title = models.CharField(max_length=255, db_index=True)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
@@ -174,8 +211,28 @@ class ContentIdeas(SiteSectorBaseModel):
|
|||||||
related_name='ideas',
|
related_name='ideas',
|
||||||
limit_choices_to={'sector': models.F('sector')}
|
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"
|
||||||
|
)
|
||||||
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
|
||||||
estimated_word_count = models.IntegerField(default=1000)
|
estimated_word_count = models.IntegerField(default=1000)
|
||||||
|
site_entity_type = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=SITE_ENTITY_TYPE_CHOICES,
|
||||||
|
default='page',
|
||||||
|
help_text="Target entity type when promoting idea into tasks/pages"
|
||||||
|
)
|
||||||
|
cluster_role = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=CLUSTER_ROLE_CHOICES,
|
||||||
|
default='hub',
|
||||||
|
help_text="Role within the cluster-driven sitemap"
|
||||||
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
@@ -190,6 +247,8 @@ class ContentIdeas(SiteSectorBaseModel):
|
|||||||
models.Index(fields=['status']),
|
models.Index(fields=['status']),
|
||||||
models.Index(fields=['keyword_cluster']),
|
models.Index(fields=['keyword_cluster']),
|
||||||
models.Index(fields=['content_structure']),
|
models.Index(fields=['content_structure']),
|
||||||
|
models.Index(fields=['site_entity_type']),
|
||||||
|
models.Index(fields=['cluster_role']),
|
||||||
models.Index(fields=['site', 'sector']),
|
models.Index(fields=['site', 'sector']),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('planner', '0008_stage1_site_builder_fields'),
|
||||||
|
('site_building', '0002_sitebuilder_metadata'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SiteBlueprintCluster',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('role', models.CharField(choices=[('hub', 'Hub Page'), ('supporting', 'Supporting Page'), ('attribute', 'Attribute Page')], default='hub', max_length=50)),
|
||||||
|
('coverage_status', models.CharField(choices=[('pending', 'Pending'), ('in_progress', 'In Progress'), ('complete', 'Complete')], default='pending', max_length=50)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict, help_text='Additional coverage metadata (target pages, keyword counts, ai hints)')),
|
||||||
|
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprintcluster_set', to='igny8_core_auth.account')),
|
||||||
|
('cluster', models.ForeignKey(help_text='Planner cluster being mapped into the site blueprint', on_delete=django.db.models.deletion.CASCADE, related_name='blueprint_links', to='planner.clusters')),
|
||||||
|
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprintcluster_set', to='igny8_core_auth.sector')),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprintcluster_set', to='igny8_core_auth.site')),
|
||||||
|
('site_blueprint', models.ForeignKey(help_text='Site blueprint that is planning coverage for the cluster', on_delete=django.db.models.deletion.CASCADE, related_name='cluster_links', to='site_building.siteblueprint')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Site Blueprint Cluster',
|
||||||
|
'verbose_name_plural': 'Site Blueprint Clusters',
|
||||||
|
'db_table': 'igny8_site_blueprint_clusters',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
'unique_together': {('site_blueprint', 'cluster', 'role')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WorkflowState',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('current_step', models.CharField(default='business_details', max_length=50)),
|
||||||
|
('step_status', models.JSONField(blank=True, default=dict, help_text='Dictionary of step → status/progress metadata')),
|
||||||
|
('blocking_reason', models.TextField(blank=True, help_text='Human-readable explanation when blocked', null=True)),
|
||||||
|
('completed', models.BooleanField(default=False, help_text='Marks wizard completion')),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='workflowstate_set', to='igny8_core_auth.account')),
|
||||||
|
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workflowstate_set', to='igny8_core_auth.sector')),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workflowstate_set', to='igny8_core_auth.site')),
|
||||||
|
('site_blueprint', models.OneToOneField(help_text='Blueprint whose progress is being tracked', on_delete=django.db.models.deletion.CASCADE, related_name='workflow_state', to='site_building.siteblueprint')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Workflow State',
|
||||||
|
'verbose_name_plural': 'Workflow States',
|
||||||
|
'db_table': 'igny8_site_blueprint_workflow_states',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SiteBlueprintTaxonomy',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('name', models.CharField(help_text='Display name', max_length=255)),
|
||||||
|
('slug', models.SlugField(help_text='Slug/identifier within the site blueprint', max_length=255)),
|
||||||
|
('taxonomy_type', models.CharField(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')], default='blog_category', max_length=50)),
|
||||||
|
('description', models.TextField(blank=True, null=True)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict, help_text='Additional taxonomy metadata or AI hints')),
|
||||||
|
('external_reference', models.CharField(blank=True, help_text='External system ID (WordPress/WooCommerce/etc.)', max_length=255, null=True)),
|
||||||
|
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprinttaxonomy_set', to='igny8_core_auth.account')),
|
||||||
|
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprinttaxonomy_set', to='igny8_core_auth.sector')),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='siteblueprinttaxonomy_set', to='igny8_core_auth.site')),
|
||||||
|
('site_blueprint', models.ForeignKey(help_text='Site blueprint this taxonomy belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='taxonomies', to='site_building.siteblueprint')),
|
||||||
|
('clusters', models.ManyToManyField(blank=True, help_text='Planner clusters that this taxonomy maps to', related_name='blueprint_taxonomies', to='planner.clusters')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Site Blueprint Taxonomy',
|
||||||
|
'verbose_name_plural': 'Site Blueprint Taxonomies',
|
||||||
|
'db_table': 'igny8_site_blueprint_taxonomies',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
'unique_together': {('site_blueprint', 'slug')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='siteblueprintcluster',
|
||||||
|
index=models.Index(fields=['site_blueprint', 'cluster'], name='site_buildi_site_bl_4234c0_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='siteblueprintcluster',
|
||||||
|
index=models.Index(fields=['cluster', 'role'], name='site_buildi_cluster__9a078f_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='siteblueprintcluster',
|
||||||
|
index=models.Index(fields=['site_blueprint', 'coverage_status'], name='site_buildi_site_bl_459d80_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='workflowstate',
|
||||||
|
index=models.Index(fields=['site_blueprint'], name='site_buildi_site_bl_312cd0_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='workflowstate',
|
||||||
|
index=models.Index(fields=['current_step'], name='site_buildi_current_a25dce_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='workflowstate',
|
||||||
|
index=models.Index(fields=['completed'], name='site_buildi_complet_4649af_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='siteblueprinttaxonomy',
|
||||||
|
index=models.Index(fields=['site_blueprint', 'taxonomy_type'], name='site_buildi_site_bl_33fadc_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='siteblueprinttaxonomy',
|
||||||
|
index=models.Index(fields=['taxonomy_type'], name='site_buildi_taxonom_4a7dde_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -167,6 +167,178 @@ class PageBlueprint(SiteSectorBaseModel):
|
|||||||
return f"{self.title} ({self.site_blueprint.name})"
|
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 WorkflowState(SiteSectorBaseModel):
|
||||||
|
"""
|
||||||
|
Persists wizard progress + gating data for each site blueprint.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_STEP = 'business_details'
|
||||||
|
|
||||||
|
site_blueprint = models.OneToOneField(
|
||||||
|
SiteBlueprint,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='workflow_state',
|
||||||
|
help_text="Blueprint whose progress is being tracked",
|
||||||
|
)
|
||||||
|
current_step = models.CharField(max_length=50, default=DEFAULT_STEP)
|
||||||
|
step_status = models.JSONField(
|
||||||
|
default=dict,
|
||||||
|
blank=True,
|
||||||
|
help_text="Dictionary of step → status/progress metadata",
|
||||||
|
)
|
||||||
|
blocking_reason = models.TextField(blank=True, null=True, help_text="Human-readable explanation when blocked")
|
||||||
|
completed = models.BooleanField(default=False, help_text="Marks wizard completion")
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'site_building'
|
||||||
|
db_table = 'igny8_site_blueprint_workflow_states'
|
||||||
|
verbose_name = 'Workflow State'
|
||||||
|
verbose_name_plural = 'Workflow States'
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['site_blueprint']),
|
||||||
|
models.Index(fields=['current_step']),
|
||||||
|
models.Index(fields=['completed']),
|
||||||
|
]
|
||||||
|
|
||||||
|
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"Workflow for {self.site_blueprint.name} ({self.current_step})"
|
||||||
|
|
||||||
|
|
||||||
class SiteBuilderOption(models.Model):
|
class SiteBuilderOption(models.Model):
|
||||||
"""
|
"""
|
||||||
Base model for Site Builder dropdown metadata.
|
Base model for Site Builder dropdown metadata.
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ Site Building Services
|
|||||||
from igny8_core.business.site_building.services.file_management_service import SiteBuilderFileService
|
from igny8_core.business.site_building.services.file_management_service import SiteBuilderFileService
|
||||||
from igny8_core.business.site_building.services.structure_generation_service import StructureGenerationService
|
from igny8_core.business.site_building.services.structure_generation_service import StructureGenerationService
|
||||||
from igny8_core.business.site_building.services.page_generation_service import PageGenerationService
|
from igny8_core.business.site_building.services.page_generation_service import PageGenerationService
|
||||||
|
from igny8_core.business.site_building.services.workflow_state_service import WorkflowStateService
|
||||||
|
from igny8_core.business.site_building.services.taxonomy_service import TaxonomyService
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SiteBuilderFileService',
|
'SiteBuilderFileService',
|
||||||
'StructureGenerationService',
|
'StructureGenerationService',
|
||||||
'PageGenerationService',
|
'PageGenerationService',
|
||||||
|
'WorkflowStateService',
|
||||||
|
'TaxonomyService',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""
|
||||||
|
Taxonomy Service
|
||||||
|
Handles CRUD + import helpers for blueprint taxonomies.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Iterable, Optional, Sequence
|
||||||
|
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from igny8_core.business.site_building.models import SiteBlueprint, SiteBlueprintTaxonomy
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TaxonomyService:
|
||||||
|
"""High-level helper used by the wizard + sync flows."""
|
||||||
|
|
||||||
|
def create_taxonomy(
|
||||||
|
self,
|
||||||
|
site_blueprint: SiteBlueprint,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
slug: str,
|
||||||
|
taxonomy_type: str,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
metadata: Optional[dict] = None,
|
||||||
|
clusters: Optional[Sequence] = None,
|
||||||
|
external_reference: Optional[str] = None,
|
||||||
|
) -> SiteBlueprintTaxonomy:
|
||||||
|
taxonomy = SiteBlueprintTaxonomy.objects.create(
|
||||||
|
site_blueprint=site_blueprint,
|
||||||
|
name=name,
|
||||||
|
slug=slug,
|
||||||
|
taxonomy_type=taxonomy_type,
|
||||||
|
description=description or '',
|
||||||
|
metadata=metadata or {},
|
||||||
|
external_reference=external_reference,
|
||||||
|
)
|
||||||
|
if clusters:
|
||||||
|
taxonomy.clusters.set(self._normalize_cluster_ids(clusters))
|
||||||
|
return taxonomy
|
||||||
|
|
||||||
|
def update_taxonomy(
|
||||||
|
self,
|
||||||
|
taxonomy: SiteBlueprintTaxonomy,
|
||||||
|
*,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
slug: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
metadata: Optional[dict] = None,
|
||||||
|
clusters: Optional[Sequence] = None,
|
||||||
|
external_reference: Optional[str] = None,
|
||||||
|
) -> SiteBlueprintTaxonomy:
|
||||||
|
if name is not None:
|
||||||
|
taxonomy.name = name
|
||||||
|
if slug is not None:
|
||||||
|
taxonomy.slug = slug
|
||||||
|
if description is not None:
|
||||||
|
taxonomy.description = description
|
||||||
|
if metadata is not None:
|
||||||
|
taxonomy.metadata = metadata
|
||||||
|
if external_reference is not None:
|
||||||
|
taxonomy.external_reference = external_reference
|
||||||
|
taxonomy.save()
|
||||||
|
|
||||||
|
if clusters is not None:
|
||||||
|
taxonomy.clusters.set(self._normalize_cluster_ids(clusters))
|
||||||
|
return taxonomy
|
||||||
|
|
||||||
|
def map_clusters(
|
||||||
|
self,
|
||||||
|
taxonomy: SiteBlueprintTaxonomy,
|
||||||
|
clusters: Sequence,
|
||||||
|
) -> SiteBlueprintTaxonomy:
|
||||||
|
taxonomy.clusters.set(self._normalize_cluster_ids(clusters))
|
||||||
|
return taxonomy
|
||||||
|
|
||||||
|
def import_from_external(
|
||||||
|
self,
|
||||||
|
site_blueprint: SiteBlueprint,
|
||||||
|
records: Iterable[dict],
|
||||||
|
*,
|
||||||
|
default_type: str = 'blog_category',
|
||||||
|
) -> list[SiteBlueprintTaxonomy]:
|
||||||
|
"""
|
||||||
|
Import helper consumed by WordPress/WooCommerce sync flows.
|
||||||
|
Each record should contain name, slug, optional type + description.
|
||||||
|
"""
|
||||||
|
created = []
|
||||||
|
with transaction.atomic():
|
||||||
|
for record in records:
|
||||||
|
name = record.get('name')
|
||||||
|
slug = record.get('slug') or name
|
||||||
|
if not name or not slug:
|
||||||
|
logger.warning("Skipping taxonomy import with missing name/slug: %s", record)
|
||||||
|
continue
|
||||||
|
taxonomy_type = record.get('taxonomy_type') or default_type
|
||||||
|
taxonomy, _ = SiteBlueprintTaxonomy.objects.update_or_create(
|
||||||
|
site_blueprint=site_blueprint,
|
||||||
|
slug=slug,
|
||||||
|
defaults={
|
||||||
|
'name': name,
|
||||||
|
'taxonomy_type': taxonomy_type,
|
||||||
|
'description': record.get('description') or '',
|
||||||
|
'metadata': record.get('metadata') or {},
|
||||||
|
'external_reference': record.get('external_reference'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
created.append(taxonomy)
|
||||||
|
return created
|
||||||
|
|
||||||
|
def _normalize_cluster_ids(self, clusters: Sequence) -> list[int]:
|
||||||
|
"""Accept queryset/model/ids and normalize to integer IDs."""
|
||||||
|
normalized = []
|
||||||
|
for cluster in clusters:
|
||||||
|
if cluster is None:
|
||||||
|
continue
|
||||||
|
if hasattr(cluster, 'id'):
|
||||||
|
normalized.append(cluster.id)
|
||||||
|
else:
|
||||||
|
normalized.append(int(cluster))
|
||||||
|
return normalized
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Reusable validation helpers for the site builder workflow.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from igny8_core.business.site_building.models import SiteBlueprint
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_clusters_attached(site_blueprint: SiteBlueprint) -> bool:
|
||||||
|
if not site_blueprint.cluster_links.exists():
|
||||||
|
raise ValidationError("Attach at least one planner cluster before proceeding.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_taxonomies_defined(site_blueprint: SiteBlueprint) -> bool:
|
||||||
|
if not site_blueprint.taxonomies.exists():
|
||||||
|
raise ValidationError("Define or import at least one taxonomy to continue.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_sitemap_ready(site_blueprint: SiteBlueprint) -> bool:
|
||||||
|
if not site_blueprint.pages.exists():
|
||||||
|
raise ValidationError("Generate the AI sitemap before reviewing this step.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_coverage_ready(site_blueprint: SiteBlueprint) -> bool:
|
||||||
|
incomplete = site_blueprint.cluster_links.exclude(coverage_status='complete').exists()
|
||||||
|
if incomplete:
|
||||||
|
raise ValidationError("Complete coverage for all attached clusters.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_ideas_ready(site_blueprint: SiteBlueprint) -> bool:
|
||||||
|
if not site_blueprint.cluster_links.exists() or not site_blueprint.pages.exists():
|
||||||
|
raise ValidationError("Attach clusters and generate pages before sending ideas.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
Workflow State Service
|
||||||
|
Manages wizard progress + gating checks for site blueprints.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from igny8_core.business.site_building.models import SiteBlueprint, WorkflowState
|
||||||
|
from igny8_core.business.site_building.services import validators
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_STEPS: List[str] = [
|
||||||
|
'business_details',
|
||||||
|
'clusters',
|
||||||
|
'taxonomies',
|
||||||
|
'sitemap',
|
||||||
|
'coverage',
|
||||||
|
'ideas',
|
||||||
|
]
|
||||||
|
|
||||||
|
STEP_VALIDATORS = {
|
||||||
|
'clusters': validators.ensure_clusters_attached,
|
||||||
|
'taxonomies': validators.ensure_taxonomies_defined,
|
||||||
|
'sitemap': validators.ensure_sitemap_ready,
|
||||||
|
'coverage': validators.ensure_coverage_ready,
|
||||||
|
'ideas': validators.ensure_ideas_ready,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowStateService:
|
||||||
|
"""Centralizes workflow persistence + validation logic."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.enabled = getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False)
|
||||||
|
|
||||||
|
def initialize(self, site_blueprint: SiteBlueprint) -> Optional[WorkflowState]:
|
||||||
|
if not self.enabled or not site_blueprint:
|
||||||
|
return None
|
||||||
|
|
||||||
|
state, _ = WorkflowState.objects.get_or_create(
|
||||||
|
site_blueprint=site_blueprint,
|
||||||
|
defaults={
|
||||||
|
'current_step': WorkflowState.DEFAULT_STEP,
|
||||||
|
'step_status': {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def refresh_state(self, site_blueprint: SiteBlueprint) -> Optional[WorkflowState]:
|
||||||
|
"""Re-run validators to keep the state snapshot fresh."""
|
||||||
|
state = self.initialize(site_blueprint)
|
||||||
|
if not state:
|
||||||
|
return None
|
||||||
|
|
||||||
|
step_status: Dict[str, Dict[str, str]] = state.step_status or {}
|
||||||
|
blocking_reason = None
|
||||||
|
|
||||||
|
for step in DEFAULT_STEPS:
|
||||||
|
validator = STEP_VALIDATORS.get(step)
|
||||||
|
try:
|
||||||
|
if validator:
|
||||||
|
validator(site_blueprint)
|
||||||
|
step_status[step] = {'status': 'ready'}
|
||||||
|
except ValidationError as exc:
|
||||||
|
message = str(exc)
|
||||||
|
step_status[step] = {'status': 'blocked', 'message': message}
|
||||||
|
if not blocking_reason:
|
||||||
|
blocking_reason = message
|
||||||
|
|
||||||
|
state.step_status = step_status
|
||||||
|
state.blocking_reason = blocking_reason
|
||||||
|
state.completed = all(value.get('status') == 'ready' for value in step_status.values())
|
||||||
|
state.save(update_fields=['step_status', 'blocking_reason', 'completed', 'updated_at'])
|
||||||
|
return state
|
||||||
|
|
||||||
|
def update_step(
|
||||||
|
self,
|
||||||
|
site_blueprint: SiteBlueprint,
|
||||||
|
step: str,
|
||||||
|
status: str,
|
||||||
|
metadata: Optional[Dict[str, str]] = None,
|
||||||
|
) -> Optional[WorkflowState]:
|
||||||
|
"""Persist explicit step updates coming from the wizard."""
|
||||||
|
state = self.initialize(site_blueprint)
|
||||||
|
if not state:
|
||||||
|
return None
|
||||||
|
|
||||||
|
metadata = metadata or {}
|
||||||
|
step_status = state.step_status or {}
|
||||||
|
step_status[step] = {'status': status, **metadata}
|
||||||
|
|
||||||
|
if step in DEFAULT_STEPS:
|
||||||
|
state.current_step = step
|
||||||
|
|
||||||
|
state.step_status = step_status
|
||||||
|
state.blocking_reason = metadata.get('message')
|
||||||
|
state.completed = all(value.get('status') == 'ready' for value in step_status.values())
|
||||||
|
state.save(update_fields=['current_step', 'step_status', 'blocking_reason', 'completed', 'updated_at'])
|
||||||
|
logger.debug("Workflow step updated: blueprint=%s step=%s status=%s", site_blueprint.id, step, status)
|
||||||
|
return state
|
||||||
|
|
||||||
|
def validate_step(self, site_blueprint: SiteBlueprint, step: str) -> None:
|
||||||
|
"""Run validator for a single step (raises ValidationError when blocked)."""
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
validator = STEP_VALIDATORS.get(step)
|
||||||
|
if not validator:
|
||||||
|
return
|
||||||
|
|
||||||
|
validator(site_blueprint)
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#
|
||||||
|
# Generated by Django 5.2.8 on 2025-11-19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
def default_json():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def default_list():
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('site_building', '0002_sitebuilder_metadata'),
|
||||||
|
('planner', '0007_merge_20251109_2138'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clusters',
|
||||||
|
name='context_type',
|
||||||
|
field=models.CharField(choices=[('topic', 'Topic Cluster'), ('attribute', 'Attribute Cluster'), ('service_line', 'Service Line')], default='topic', help_text='Primary dimension for this cluster (topic, attribute, service line)', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clusters',
|
||||||
|
name='dimension_meta',
|
||||||
|
field=models.JSONField(blank=True, default=default_json, help_text='Extended metadata (taxonomy hints, attribute suggestions, coverage targets)'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contentideas',
|
||||||
|
name='cluster_role',
|
||||||
|
field=models.CharField(choices=[('hub', 'Hub Page'), ('supporting', 'Supporting Page'), ('attribute', 'Attribute Page')], default='hub', help_text='Role within the cluster-driven sitemap', max_length=50),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='contentideas',
|
||||||
|
name='site_entity_type',
|
||||||
|
field=models.CharField(choices=[('page', 'Site Page'), ('blog_post', 'Blog Post'), ('product', 'Product'), ('service', 'Service'), ('taxonomy', 'Taxonomy Page')], default='page', help_text='Target entity type when promoting idea into tasks/pages', max_length=50),
|
||||||
|
),
|
||||||
|
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'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='keywords',
|
||||||
|
name='attribute_values',
|
||||||
|
field=models.JSONField(blank=True, default=default_list, help_text='Optional attribute metadata (e.g., product specs, service modifiers)'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clusters',
|
||||||
|
index=models.Index(fields=['context_type'], name='planner_cl_context__2ed54f_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contentideas',
|
||||||
|
index=models.Index(fields=['site_entity_type'], name='planner_co_site_ent_d3183c_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contentideas',
|
||||||
|
index=models.Index(fields=['cluster_role'], name='planner_co_cluster__f97a65_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('site_building', '0003_workflow_and_taxonomies'),
|
||||||
|
('planner', '0008_stage1_site_builder_fields'),
|
||||||
|
('writer', '0011_add_universal_content_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ContentClusterMap',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('role', models.CharField(choices=[('hub', 'Hub Page'), ('supporting', 'Supporting Page'), ('attribute', 'Attribute Page')], default='hub', max_length=50)),
|
||||||
|
('source', models.CharField(choices=[('blueprint', 'Blueprint'), ('manual', 'Manual'), ('import', 'Import')], default='blueprint', max_length=50)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='contentclustermap_set', to='igny8_core_auth.account')),
|
||||||
|
('cluster', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_mappings', to='planner.clusters')),
|
||||||
|
('content', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cluster_mappings', to='writer.content')),
|
||||||
|
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contentclustermap_set', to='igny8_core_auth.sector')),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contentclustermap_set', to='igny8_core_auth.site')),
|
||||||
|
('task', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cluster_mappings', to='writer.tasks')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Content Cluster Map',
|
||||||
|
'verbose_name_plural': 'Content Cluster Maps',
|
||||||
|
'db_table': 'igny8_content_cluster_map',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
'unique_together': {('content', 'cluster', 'role')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ContentTaxonomyMap',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('source', models.CharField(choices=[('blueprint', 'Blueprint'), ('manual', 'Manual'), ('import', 'Import')], default='blueprint', max_length=50)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='contenttaxonomymap_set', to='igny8_core_auth.account')),
|
||||||
|
('content', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='taxonomy_mappings', to='writer.content')),
|
||||||
|
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contenttaxonomymap_set', to='igny8_core_auth.sector')),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contenttaxonomymap_set', to='igny8_core_auth.site')),
|
||||||
|
('taxonomy', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='content_mappings', to='site_building.siteblueprinttaxonomy')),
|
||||||
|
('task', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='taxonomy_mappings', to='writer.tasks')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Content Taxonomy Map',
|
||||||
|
'verbose_name_plural': 'Content Taxonomy Maps',
|
||||||
|
'db_table': 'igny8_content_taxonomy_map',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
'unique_together': {('content', 'taxonomy')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ContentAttributeMap',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('name', models.CharField(max_length=120)),
|
||||||
|
('value', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('source', models.CharField(choices=[('blueprint', 'Blueprint'), ('manual', 'Manual'), ('import', 'Import')], default='blueprint', max_length=50)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='contentattributemap_set', to='igny8_core_auth.account')),
|
||||||
|
('content', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attribute_mappings', to='writer.content')),
|
||||||
|
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contentattributemap_set', to='igny8_core_auth.sector')),
|
||||||
|
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contentattributemap_set', to='igny8_core_auth.site')),
|
||||||
|
('task', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attribute_mappings', to='writer.tasks')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Content Attribute Map',
|
||||||
|
'verbose_name_plural': 'Content Attribute Maps',
|
||||||
|
'db_table': 'igny8_content_attribute_map',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contentclustermap',
|
||||||
|
index=models.Index(fields=['cluster', 'role'], name='writer_con_cluster__d06bd6_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contentclustermap',
|
||||||
|
index=models.Index(fields=['content', 'role'], name='writer_con_content__bb02dd_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contentclustermap',
|
||||||
|
index=models.Index(fields=['task', 'role'], name='writer_con_task__role_828ce1_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contenttaxonomymap',
|
||||||
|
index=models.Index(fields=['taxonomy'], name='writer_con_taxonomy_d55410_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contenttaxonomymap',
|
||||||
|
index=models.Index(fields=['content', 'taxonomy'], name='writer_con_content__0af6a6_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contenttaxonomymap',
|
||||||
|
index=models.Index(fields=['task', 'taxonomy'], name='writer_con_task__taxon_e3bdad_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contentattributemap',
|
||||||
|
index=models.Index(fields=['name'], name='writer_con_name_a9671a_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contentattributemap',
|
||||||
|
index=models.Index(fields=['content', 'name'], name='writer_con_content__34a91e_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='contentattributemap',
|
||||||
|
index=models.Index(fields=['task', 'name'], name='writer_con_task__name_fa4a4e_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
|
|||||||
# Set IGNY8_USE_UNIFIED_EXCEPTION_HANDLER=True to enable unified exception handler
|
# Set IGNY8_USE_UNIFIED_EXCEPTION_HANDLER=True to enable unified exception handler
|
||||||
# Set IGNY8_DEBUG_THROTTLE=True to bypass rate limiting in development
|
# Set IGNY8_DEBUG_THROTTLE=True to bypass rate limiting in development
|
||||||
IGNY8_DEBUG_THROTTLE = os.getenv('IGNY8_DEBUG_THROTTLE', str(DEBUG)).lower() == 'true'
|
IGNY8_DEBUG_THROTTLE = os.getenv('IGNY8_DEBUG_THROTTLE', str(DEBUG)).lower() == 'true'
|
||||||
|
USE_SITE_BUILDER_REFACTOR = os.getenv('USE_SITE_BUILDER_REFACTOR', 'false').lower() == 'true'
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
'*', # Allow all hosts for flexibility
|
'*', # Allow all hosts for flexibility
|
||||||
|
|||||||
Reference in New Issue
Block a user