Remove obsolete migration files and update initial migration timestamps for various modules; ensure consistency in migration history across the project.

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-20 23:32:45 +00:00
parent b38553cfc3
commit d14d6d89b1
67 changed files with 12722 additions and 3414 deletions

View File

@@ -1,7 +1,6 @@
# Generated manually for Phase 2: Automation System
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
@@ -10,7 +9,6 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
]
operations = [
@@ -18,8 +16,6 @@ class Migration(migrations.Migration):
name='AutomationRule',
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='Rule name', max_length=255)),
('description', models.TextField(blank=True, help_text='Rule description', null=True)),
('trigger', models.CharField(choices=[('schedule', 'Schedule'), ('event', 'Event'), ('manual', 'Manual')], default='manual', max_length=50)),
@@ -31,70 +27,34 @@ class Migration(migrations.Migration):
('last_executed_at', models.DateTimeField(blank=True, null=True)),
('execution_count', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)])),
('metadata', models.JSONField(default=dict, help_text='Additional metadata')),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
('site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.site')),
('sector', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.sector')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'igny8_automation_rules',
'ordering': ['-created_at'],
'verbose_name': 'Automation Rule',
'verbose_name_plural': 'Automation Rules',
'db_table': 'igny8_automation_rules',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='ScheduledTask',
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)),
('scheduled_at', models.DateTimeField(help_text='When the task is scheduled to run')),
('executed_at', models.DateTimeField(blank=True, help_text='When the task was actually executed', null=True)),
('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='pending', max_length=50)),
('result', models.JSONField(default=dict, help_text='Execution result data')),
('error_message', models.TextField(blank=True, help_text='Error message if execution failed', null=True)),
('metadata', models.JSONField(default=dict, help_text='Additional metadata')),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
('automation_rule', models.ForeignKey(help_text='The automation rule this task belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_tasks', to='automation.automationrule')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'igny8_scheduled_tasks',
'ordering': ['-scheduled_at'],
'verbose_name': 'Scheduled Task',
'verbose_name_plural': 'Scheduled Tasks',
'db_table': 'igny8_scheduled_tasks',
'ordering': ['-scheduled_at'],
},
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['trigger', 'is_active'], name='igny8_autom_trigger_123abc_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['status'], name='igny8_autom_status_456def_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['site', 'sector'], name='igny8_autom_site_id_789ghi_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['trigger', 'is_active', 'status'], name='igny8_autom_trigger_0abjkl_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['automation_rule', 'status'], name='igny8_sched_automation_123abc_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['scheduled_at', 'status'], name='igny8_sched_scheduled_456def_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['account', 'status'], name='igny8_sched_account_789ghi_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['status', 'scheduled_at'], name='igny8_sched_status_0abjkl_idx'),
),
]

View File

@@ -0,0 +1,74 @@
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('automation', '0001_initial'),
('igny8_core_auth', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='automationrule',
name='account',
field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'),
),
migrations.AddField(
model_name='automationrule',
name='sector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector'),
),
migrations.AddField(
model_name='automationrule',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site'),
),
migrations.AddField(
model_name='scheduledtask',
name='account',
field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'),
),
migrations.AddField(
model_name='scheduledtask',
name='automation_rule',
field=models.ForeignKey(help_text='The automation rule this task belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_tasks', to='automation.automationrule'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['trigger', 'is_active'], name='igny8_autom_trigger_32979f_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['status'], name='igny8_autom_status_827c0d_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['site', 'sector'], name='igny8_autom_site_id_d0a51d_idx'),
),
migrations.AddIndex(
model_name='automationrule',
index=models.Index(fields=['trigger', 'is_active', 'status'], name='igny8_autom_trigger_f3f3e2_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['automation_rule', 'status'], name='igny8_sched_automat_da6c85_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['scheduled_at', 'status'], name='igny8_sched_schedul_1e3342_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['account', 'status'], name='igny8_sched_tenant__7244a8_idx'),
),
migrations.AddIndex(
model_name='scheduledtask',
index=models.Index(fields=['status', 'scheduled_at'], name='igny8_sched_status_21f32f_idx'),
),
]

View File

@@ -1,7 +1,6 @@
# Generated by Django 5.2.8 on 2025-11-07 10:37
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
@@ -10,7 +9,6 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
]
operations = [
@@ -25,12 +23,10 @@ class Migration(migrations.Migration):
('description', models.CharField(max_length=255)),
('metadata', models.JSONField(default=dict, help_text='Additional context (AI call details, etc.)')),
('created_at', models.DateTimeField(auto_now_add=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_credit_transactions',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['tenant', 'transaction_type'], name='igny8_credi_tenant__c4281c_idx'), models.Index(fields=['tenant', 'created_at'], name='igny8_credi_tenant__69abd3_idx')],
},
),
migrations.CreateModel(
@@ -38,7 +34,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True)),
('operation_type', models.CharField(choices=[('clustering', 'Keyword Clustering'), ('ideas', 'Content Ideas Generation'), ('content', 'Content Generation'), ('images', 'Image Generation'), ('reparse', 'Content Reparse')], db_index=True, max_length=50)),
('operation_type', models.CharField(choices=[('clustering', 'Keyword Clustering'), ('idea_generation', 'Content Ideas Generation'), ('content_generation', 'Content Generation'), ('image_generation', 'Image Generation'), ('reparse', 'Content Reparse'), ('ideas', 'Content Ideas Generation'), ('content', 'Content Generation'), ('images', 'Image Generation')], db_index=True, max_length=50)),
('credits_used', models.IntegerField(validators=[django.core.validators.MinValueValidator(0)])),
('cost_usd', models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True)),
('model_used', models.CharField(blank=True, max_length=100)),
@@ -48,12 +44,10 @@ class Migration(migrations.Migration):
('related_object_id', models.IntegerField(blank=True, null=True)),
('metadata', models.JSONField(default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_credit_usage_logs',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['tenant', 'operation_type'], name='igny8_credi_tenant__3545ed_idx'), models.Index(fields=['tenant', 'created_at'], name='igny8_credi_tenant__4aee99_idx'), models.Index(fields=['tenant', 'operation_type', 'created_at'], name='igny8_credi_tenant__a00d57_idx')],
},
),
]

View File

@@ -0,0 +1,47 @@
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('billing', '0001_initial'),
('igny8_core_auth', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='credittransaction',
name='account',
field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'),
),
migrations.AddField(
model_name='creditusagelog',
name='account',
field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'),
),
migrations.AddIndex(
model_name='credittransaction',
index=models.Index(fields=['account', 'transaction_type'], name='igny8_credi_tenant__c4281c_idx'),
),
migrations.AddIndex(
model_name='credittransaction',
index=models.Index(fields=['account', 'created_at'], name='igny8_credi_tenant__69abd3_idx'),
),
migrations.AddIndex(
model_name='creditusagelog',
index=models.Index(fields=['account', 'operation_type'], name='igny8_credi_tenant__3545ed_idx'),
),
migrations.AddIndex(
model_name='creditusagelog',
index=models.Index(fields=['account', 'created_at'], name='igny8_credi_tenant__4aee99_idx'),
),
migrations.AddIndex(
model_name='creditusagelog',
index=models.Index(fields=['account', 'operation_type', 'created_at'], name='igny8_credi_tenant__a00d57_idx'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated migration to rename tenant field to account
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('billing', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='credittransaction',
old_name='tenant',
new_name='account',
),
migrations.RenameField(
model_name='creditusagelog',
old_name='tenant',
new_name='account',
),
]

View File

@@ -1,22 +0,0 @@
# Generated migration to fix tenant_id column name
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('billing', '0002_rename_tenant_to_account'),
]
operations = [
# Rename the database column from account_id to tenant_id to match model's db_column
migrations.RunSQL(
sql="ALTER TABLE igny8_credit_transactions RENAME COLUMN account_id TO tenant_id;",
reverse_sql="ALTER TABLE igny8_credit_transactions RENAME COLUMN tenant_id TO account_id;"
),
migrations.RunSQL(
sql="ALTER TABLE igny8_credit_usage_logs RENAME COLUMN account_id TO tenant_id;",
reverse_sql="ALTER TABLE igny8_credit_usage_logs RENAME COLUMN tenant_id TO account_id;"
),
]

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-11-02 21:42
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.db.models.deletion
from django.db import migrations, models
@@ -13,6 +13,24 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='Keywords',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('volume_override', models.IntegerField(blank=True, help_text='Site-specific volume override (uses seed_keyword.volume if not set)', null=True)),
('difficulty_override', models.IntegerField(blank=True, help_text='Site-specific difficulty override (uses seed_keyword.difficulty if not set)', null=True)),
('attribute_values', models.JSONField(blank=True, default=list, help_text='Optional attribute metadata (e.g., product specs, service modifiers)')),
('status', models.CharField(choices=[('active', 'Active'), ('pending', 'Pending'), ('archived', 'Archived')], default='pending', max_length=50)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Keyword',
'verbose_name_plural': 'Keywords',
'db_table': 'igny8_keywords',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Clusters',
fields=[
@@ -23,64 +41,46 @@ class Migration(migrations.Migration):
('volume', models.IntegerField(default=0)),
('mapped_pages', models.IntegerField(default=0)),
('status', models.CharField(default='active', max_length=50)),
('context_type', 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)),
('dimension_meta', models.JSONField(blank=True, default=dict, help_text='Extended metadata (taxonomy hints, attribute suggestions, coverage targets)')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('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')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'verbose_name': 'Cluster',
'verbose_name_plural': 'Clusters',
'db_table': 'igny8_clusters',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Keywords',
name='ContentIdeas',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('keyword', models.CharField(db_index=True, max_length=255)),
('volume', models.IntegerField(default=0)),
('difficulty', models.IntegerField(default=0)),
('intent', models.CharField(choices=[('informational', 'Informational'), ('navigational', 'Navigational'), ('commercial', 'Commercial'), ('transactional', 'Transactional')], default='informational', max_length=50)),
('status', models.CharField(choices=[('active', 'Active'), ('pending', 'Pending'), ('archived', 'Archived')], default='pending', max_length=50)),
('idea_title', models.CharField(db_index=True, max_length=255)),
('description', models.TextField(blank=True, null=True)),
('content_structure', models.CharField(choices=[('cluster_hub', 'Cluster Hub'), ('landing_page', 'Landing Page'), ('pillar_page', 'Pillar Page'), ('supporting_page', 'Supporting Page')], default='blog_post', max_length=50)),
('content_type', models.CharField(choices=[('blog_post', 'Blog Post'), ('article', 'Article'), ('guide', 'Guide'), ('tutorial', 'Tutorial')], default='blog_post', max_length=50)),
('target_keywords', models.CharField(blank=True, max_length=500)),
('status', models.CharField(choices=[('new', 'New'), ('scheduled', 'Scheduled'), ('published', 'Published')], default='new', max_length=50)),
('estimated_word_count', models.IntegerField(default=1000)),
('site_entity_type', 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)),
('cluster_role', 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)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('cluster', models.ForeignKey(blank=True, limit_choices_to={'sector': models.F('sector')}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='keywords', to='planner.clusters')),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
('keyword_cluster', models.ForeignKey(blank=True, limit_choices_to={'sector': models.F('sector')}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ideas', to='planner.clusters')),
('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')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_keywords',
'verbose_name': 'Content Idea',
'verbose_name_plural': 'Content Ideas',
'db_table': 'igny8_content_ideas',
'ordering': ['-created_at'],
},
),
migrations.AddIndex(
model_name='clusters',
index=models.Index(fields=['name'], name='igny8_clust_name_0f98bb_idx'),
),
migrations.AddIndex(
model_name='clusters',
index=models.Index(fields=['status'], name='igny8_clust_status_c50486_idx'),
),
migrations.AddIndex(
model_name='clusters',
index=models.Index(fields=['site', 'sector'], name='igny8_clust_site_id_a9aee3_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['keyword'], name='igny8_keywo_keyword_462bff_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['status'], name='igny8_keywo_status_9a0dd6_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['cluster'], name='igny8_keywo_cluster_d1ea95_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['site', 'sector'], name='igny8_keywo_site_id_7bcb63_idx'),
),
]

View File

@@ -1,15 +0,0 @@
# Legacy migration kept for compatibility; database schema already includes
# the account/site/sector fields as part of the initial state.
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
('planner', '0001_initial'),
]
operations = []

View File

@@ -0,0 +1,121 @@
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('igny8_core_auth', '0001_initial'),
('planner', '0001_initial'),
('site_building', '0001_initial'),
]
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'),
),
migrations.AddField(
model_name='keywords',
name='account',
field=models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account'),
),
migrations.AddField(
model_name='keywords',
name='cluster',
field=models.ForeignKey(blank=True, limit_choices_to={'sector': models.F('sector')}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='keywords', to='planner.clusters'),
),
migrations.AddField(
model_name='keywords',
name='sector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector'),
),
migrations.AddField(
model_name='keywords',
name='seed_keyword',
field=models.ForeignKey(help_text='Reference to the global seed keyword', on_delete=django.db.models.deletion.PROTECT, related_name='site_keywords', to='igny8_core_auth.seedkeyword'),
),
migrations.AddField(
model_name='keywords',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site'),
),
migrations.AddField(
model_name='contentideas',
name='keyword_objects',
field=models.ManyToManyField(blank=True, help_text='Individual keywords linked to this content idea', related_name='content_ideas', to='planner.keywords'),
),
migrations.AddIndex(
model_name='clusters',
index=models.Index(fields=['name'], name='igny8_clust_name_0f98bb_idx'),
),
migrations.AddIndex(
model_name='clusters',
index=models.Index(fields=['status'], name='igny8_clust_status_c50486_idx'),
),
migrations.AddIndex(
model_name='clusters',
index=models.Index(fields=['site', 'sector'], name='igny8_clust_site_id_a9aee3_idx'),
),
migrations.AddIndex(
model_name='clusters',
index=models.Index(fields=['context_type'], name='igny8_clust_context_0d6bd7_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['seed_keyword'], name='igny8_keywo_seed_ke_919a9d_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['status'], name='igny8_keywo_status_9a0dd6_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['cluster'], name='igny8_keywo_cluster_d1ea95_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['site', 'sector'], name='igny8_keywo_site_id_7bcb63_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['seed_keyword', 'site', 'sector'], name='igny8_keywo_seed_ke_69146b_idx'),
),
migrations.AlterUniqueTogether(
name='keywords',
unique_together={('seed_keyword', 'site', 'sector')},
),
migrations.AddIndex(
model_name='contentideas',
index=models.Index(fields=['idea_title'], name='igny8_conte_idea_ti_1e15a5_idx'),
),
migrations.AddIndex(
model_name='contentideas',
index=models.Index(fields=['status'], name='igny8_conte_status_6be5bc_idx'),
),
migrations.AddIndex(
model_name='contentideas',
index=models.Index(fields=['keyword_cluster'], name='igny8_conte_keyword_4d2151_idx'),
),
migrations.AddIndex(
model_name='contentideas',
index=models.Index(fields=['content_structure'], name='igny8_conte_content_3eede7_idx'),
),
migrations.AddIndex(
model_name='contentideas',
index=models.Index(fields=['site_entity_type'], name='igny8_conte_site_en_511349_idx'),
),
migrations.AddIndex(
model_name='contentideas',
index=models.Index(fields=['cluster_role'], name='igny8_conte_cluster_234240_idx'),
),
migrations.AddIndex(
model_name='contentideas',
index=models.Index(fields=['site', 'sector'], name='igny8_conte_site_id_03b520_idx'),
),
]

View File

@@ -1,59 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-03 13:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0003_alter_user_role'),
('planner', '0002_add_site_sector_tenant'),
]
operations = [
migrations.AlterField(
model_name='clusters',
name='sector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector'),
),
migrations.AlterField(
model_name='clusters',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site'),
),
migrations.AlterField(
model_name='keywords',
name='sector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector'),
),
migrations.AlterField(
model_name='keywords',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site'),
),
migrations.CreateModel(
name='ContentIdeas',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('idea_title', models.CharField(db_index=True, max_length=255)),
('description', models.TextField(blank=True, null=True)),
('content_structure', models.CharField(choices=[('cluster_hub', 'Cluster Hub'), ('landing_page', 'Landing Page'), ('pillar_page', 'Pillar Page'), ('supporting_page', 'Supporting Page')], default='blog_post', max_length=50)),
('content_type', models.CharField(choices=[('blog_post', 'Blog Post'), ('article', 'Article'), ('guide', 'Guide'), ('tutorial', 'Tutorial')], default='blog_post', max_length=50)),
('target_keywords', models.CharField(blank=True, max_length=500)),
('status', models.CharField(choices=[('new', 'New'), ('scheduled', 'Scheduled'), ('published', 'Published')], default='new', max_length=50)),
('estimated_word_count', models.IntegerField(default=1000)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('keyword_cluster', models.ForeignKey(blank=True, limit_choices_to={'sector': models.F('sector')}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ideas', to='planner.clusters')),
('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')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_content_ideas',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['idea_title'], name='igny8_conte_idea_ti_1e15a5_idx'), models.Index(fields=['status'], name='igny8_conte_status_6be5bc_idx'), models.Index(fields=['keyword_cluster'], name='igny8_conte_keyword_4d2151_idx'), models.Index(fields=['content_structure'], name='igny8_conte_content_3eede7_idx'), models.Index(fields=['site', 'sector'], name='igny8_conte_site_id_03b520_idx')],
},
),
]

View File

@@ -1,24 +0,0 @@
# Generated manually to fix missing ManyToMany table
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('planner', '0003_alter_clusters_sector_alter_clusters_site_and_more'),
]
operations = [
migrations.AddField(
model_name='contentideas',
name='keyword_objects',
field=models.ManyToManyField(
blank=True,
help_text='Individual keywords linked to this content idea',
related_name='content_ideas',
to='planner.keywords'
),
),
]

View File

@@ -1,16 +0,0 @@
# Generated manually for adding seed_keyword relationship to Keywords
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0010_add_seed_keyword'),
('planner', '0003_alter_clusters_sector_alter_clusters_site_and_more'),
]
# Duplicate of planner.0006_add_seed_keyword_to_keywords.
# This migration is kept as a no-op to avoid applying the schema changes twice.
operations = []

View File

@@ -1,25 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-07 10:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('planner', '0004_add_keyword_objects_to_contentideas'),
]
operations = [
migrations.AlterModelOptions(
name='clusters',
options={'ordering': ['name'], 'verbose_name': 'Cluster', 'verbose_name_plural': 'Clusters'},
),
migrations.AlterModelOptions(
name='contentideas',
options={'ordering': ['-created_at'], 'verbose_name': 'Content Idea', 'verbose_name_plural': 'Content Ideas'},
),
migrations.AlterModelOptions(
name='keywords',
options={'ordering': ['-created_at'], 'verbose_name': 'Keyword', 'verbose_name_plural': 'Keywords'},
),
]

View File

@@ -1,86 +0,0 @@
# Generated manually for adding seed_keyword relationship to Keywords
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0010_add_seed_keyword'),
('planner', '0005_alter_clusters_options_alter_contentideas_options_and_more'),
]
operations = [
# Remove old fields (keyword, volume, difficulty, intent)
migrations.RemoveField(
model_name='keywords',
name='keyword',
),
migrations.RemoveField(
model_name='keywords',
name='volume',
),
migrations.RemoveField(
model_name='keywords',
name='difficulty',
),
migrations.RemoveField(
model_name='keywords',
name='intent',
),
# Add seed_keyword FK
migrations.AddField(
model_name='keywords',
name='seed_keyword',
field=models.ForeignKey(
help_text='Reference to the global seed keyword',
on_delete=django.db.models.deletion.PROTECT,
related_name='site_keywords',
to='igny8_core_auth.seedkeyword',
null=True # Temporarily nullable for migration
),
),
# Add override fields
migrations.AddField(
model_name='keywords',
name='volume_override',
field=models.IntegerField(blank=True, help_text='Site-specific volume override (uses seed_keyword.volume if not set)', null=True),
),
migrations.AddField(
model_name='keywords',
name='difficulty_override',
field=models.IntegerField(blank=True, help_text='Site-specific difficulty override (uses seed_keyword.difficulty if not set)', null=True),
),
# Make seed_keyword required (after data migration if needed)
migrations.AlterField(
model_name='keywords',
name='seed_keyword',
field=models.ForeignKey(
help_text='Reference to the global seed keyword',
on_delete=django.db.models.deletion.PROTECT,
related_name='site_keywords',
to='igny8_core_auth.seedkeyword'
),
),
# Add unique constraint
migrations.AlterUniqueTogether(
name='keywords',
unique_together={('seed_keyword', 'site', 'sector')},
),
# Update indexes
migrations.AlterIndexTogether(
name='keywords',
index_together=set(),
),
# Add new indexes
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['seed_keyword'], name='igny8_keyw_seed_k_12345_idx'),
),
migrations.AddIndex(
model_name='keywords',
index=models.Index(fields=['seed_keyword', 'site', 'sector'], name='igny8_keyw_seed_si_67890_idx'),
),
]

View File

@@ -1,14 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-09 21:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('planner', '0004_add_seed_keyword_to_keywords'),
('planner', '0006_add_seed_keyword_to_keywords'),
]
operations = [
]

View File

@@ -1,74 +0,0 @@
#
# 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', '0003_workflow_and_taxonomies'),
('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'),
),
]

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-11-02 21:42
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.db.models.deletion
from django.conf import settings
@@ -16,42 +16,196 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='SystemLog',
name='SystemSettings',
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)),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255, unique=True)),
('value', models.JSONField(default=dict, help_text='Settings value as JSON')),
('description', models.TextField(blank=True, help_text='Description of this setting')),
('updated_at', models.DateTimeField(auto_now=True)),
('module', models.CharField(choices=[('planner', 'Planner'), ('writer', 'Writer'), ('thinker', 'Thinker'), ('ai', 'AI Pipeline'), ('wp_bridge', 'WordPress Bridge'), ('system', 'System')], db_index=True, max_length=50)),
('level', models.CharField(choices=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('success', 'Success')], db_index=True, default='info', max_length=20)),
('action', models.CharField(max_length=255)),
('message', models.TextField()),
('metadata', models.JSONField(blank=True, default=dict)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'igny8_system_logs',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['tenant', 'module', 'level', '-created_at'], name='igny8_syste_tenant__1a6cda_idx')],
'db_table': 'igny8_system_settings',
'ordering': ['key'],
'indexes': [models.Index(fields=['key'], name='igny8_syste_key_20500b_idx')],
},
),
migrations.CreateModel(
name='SystemStatus',
name='AccountSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config', models.JSONField(default=dict, help_text='Settings configuration as JSON')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
],
options={
'db_table': 'igny8_account_settings',
'ordering': ['key'],
'indexes': [models.Index(fields=['account', 'key'], name='igny8_accou_tenant__308b8f_idx')],
'unique_together': {('account', 'key')},
},
),
migrations.CreateModel(
name='AIPrompt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prompt_type', models.CharField(choices=[('clustering', 'Clustering'), ('ideas', 'Ideas Generation'), ('content_generation', 'Content Generation'), ('image_prompt_extraction', 'Image Prompt Extraction'), ('image_prompt_template', 'Image Prompt Template'), ('negative_prompt', 'Negative Prompt'), ('site_structure_generation', 'Site Structure Generation'), ('product_generation', 'Product Content Generation'), ('service_generation', 'Service Page Generation'), ('taxonomy_generation', 'Taxonomy Generation')], db_index=True, max_length=50)),
('prompt_value', models.TextField(help_text='The prompt template text')),
('default_prompt', models.TextField(help_text='Default prompt value (for reset)')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
],
options={
'db_table': 'igny8_ai_prompts',
'ordering': ['prompt_type'],
'indexes': [models.Index(fields=['prompt_type'], name='igny8_ai_pr_prompt__4b2dbe_idx'), models.Index(fields=['account', 'prompt_type'], name='igny8_ai_pr_tenant__9e7b95_idx')],
'unique_together': {('account', 'prompt_type')},
},
),
migrations.CreateModel(
name='AISettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('integration_type', models.CharField(db_index=True, help_text="Integration type (e.g., 'openai', 'runware')", max_length=50)),
('config', models.JSONField(default=dict, help_text='Integration configuration (API keys, settings, etc.)')),
('model_preferences', models.JSONField(default=dict, help_text='Model preferences per operation type')),
('cost_limits', models.JSONField(default=dict, help_text='Cost limits and budgets')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
],
options={
'db_table': 'igny8_ai_settings',
'ordering': ['integration_type'],
'indexes': [models.Index(fields=['integration_type'], name='igny8_ai_se_integra_4f0b21_idx'), models.Index(fields=['account', 'integration_type'], name='igny8_ai_se_tenant__05ae98_idx')],
'unique_together': {('account', 'integration_type')},
},
),
migrations.CreateModel(
name='AuthorProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text="Profile name (e.g., 'SaaS B2B Informative')", max_length=255)),
('description', models.TextField(blank=True, help_text='Description of the writing style')),
('tone', models.CharField(help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical', 'Conversational')", max_length=100)),
('language', models.CharField(default='en', help_text="Language code (e.g., 'en', 'es', 'fr')", max_length=50)),
('structure_template', models.JSONField(default=dict, help_text='Structure template defining content sections and their order')),
('is_active', models.BooleanField(db_index=True, default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
],
options={
'verbose_name': 'Author Profile',
'verbose_name_plural': 'Author Profiles',
'db_table': 'igny8_author_profiles',
'ordering': ['name'],
'indexes': [models.Index(fields=['account', 'is_active'], name='igny8_autho_tenant__97d2c2_idx'), models.Index(fields=['name'], name='igny8_autho_name_8295f3_idx')],
},
),
migrations.CreateModel(
name='IntegrationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('integration_type', models.CharField(choices=[('openai', 'OpenAI'), ('runware', 'Runware'), ('gsc', 'Google Search Console'), ('image_generation', 'Image Generation Service')], db_index=True, max_length=50)),
('config', models.JSONField(default=dict, help_text='Integration configuration (API keys, settings, etc.)')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
],
options={
'db_table': 'igny8_integration_settings',
'ordering': ['integration_type'],
'indexes': [models.Index(fields=['integration_type'], name='igny8_integ_integra_5e382e_idx'), models.Index(fields=['account', 'integration_type'], name='igny8_integ_tenant__5da472_idx')],
'unique_together': {('account', 'integration_type')},
},
),
migrations.CreateModel(
name='ModuleEnableSettings',
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)),
('component', models.CharField(db_index=True, max_length=100)),
('status', models.CharField(choices=[('healthy', 'Healthy'), ('warning', 'Warning'), ('error', 'Error'), ('maintenance', 'Maintenance')], default='healthy', max_length=20)),
('message', models.TextField(blank=True)),
('last_check', models.DateTimeField(auto_now=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
('planner_enabled', models.BooleanField(default=True, help_text='Enable Planner module')),
('writer_enabled', models.BooleanField(default=True, help_text='Enable Writer module')),
('thinker_enabled', models.BooleanField(default=True, help_text='Enable Thinker module')),
('automation_enabled', models.BooleanField(default=True, help_text='Enable Automation module')),
('site_builder_enabled', models.BooleanField(default=True, help_text='Enable Site Builder module')),
('linker_enabled', models.BooleanField(default=True, help_text='Enable Linker module')),
('optimizer_enabled', models.BooleanField(default=True, help_text='Enable Optimizer module')),
('publisher_enabled', models.BooleanField(default=True, help_text='Enable Publisher module')),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
],
options={
'db_table': 'igny8_system_status',
'indexes': [models.Index(fields=['tenant', 'status'], name='igny8_syste_tenant__0e1889_idx')],
'unique_together': {('tenant', 'component')},
'db_table': 'igny8_module_enable_settings',
'unique_together': {('account',)},
},
),
migrations.CreateModel(
name='ModuleSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config', models.JSONField(default=dict, help_text='Settings configuration as JSON')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('module_name', models.CharField(db_index=True, help_text="Module name (e.g., 'planner', 'writer')", max_length=100)),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
],
options={
'db_table': 'igny8_module_settings',
'ordering': ['module_name', 'key'],
'indexes': [models.Index(fields=['account', 'module_name', 'key'], name='igny8_modul_tenant__21ee25_idx'), models.Index(fields=['module_name', 'key'], name='igny8_modul_module__95373a_idx')],
'unique_together': {('account', 'module_name', 'key')},
},
),
migrations.CreateModel(
name='Strategy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Strategy name', max_length=255)),
('description', models.TextField(blank=True, help_text='Description of the content strategy')),
('prompt_types', models.JSONField(default=list, help_text="List of prompt types to use (e.g., ['clustering', 'ideas', 'content_generation'])")),
('section_logic', models.JSONField(default=dict, help_text='Section logic configuration defining content structure and flow')),
('is_active', models.BooleanField(db_index=True, default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('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(blank=True, help_text='Optional: Link strategy to a specific sector', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='strategies', to='igny8_core_auth.sector')),
],
options={
'verbose_name': 'Strategy',
'verbose_name_plural': 'Strategies',
'db_table': 'igny8_strategies',
'ordering': ['name'],
'indexes': [models.Index(fields=['account', 'is_active'], name='igny8_strat_tenant__344de9_idx'), models.Index(fields=['account', 'sector'], name='igny8_strat_tenant__279cfa_idx'), models.Index(fields=['name'], name='igny8_strat_name_8fe823_idx')],
},
),
migrations.CreateModel(
name='UserSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
('value', models.JSONField(default=dict, help_text='Settings value as JSON')),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='user_settings', to='igny8_core_auth.account')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_settings', to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'igny8_user_settings',
'ordering': ['key'],
'indexes': [models.Index(fields=['user', 'account', 'key'], name='igny8_user__user_id_ac09d9_idx'), models.Index(fields=['account', 'key'], name='igny8_user__tenant__01033d_idx')],
'unique_together': {('user', 'account', 'key')},
},
),
]

View File

@@ -1,61 +0,0 @@
# Generated migration for IntegrationSettings and AIPrompt models
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
('system', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AIPrompt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prompt_type', models.CharField(choices=[('clustering', 'Clustering'), ('ideas', 'Ideas Generation'), ('content_generation', 'Content Generation'), ('image_prompt_template', 'Image Prompt Template'), ('negative_prompt', 'Negative Prompt')], db_index=True, max_length=50)),
('prompt_value', models.TextField(help_text='The prompt template text')),
('default_prompt', models.TextField(help_text='Default prompt value (for reset)')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_ai_prompts',
'ordering': ['prompt_type'],
'unique_together': {('tenant', 'prompt_type')},
'indexes': [
models.Index(fields=['prompt_type'], name='igny8_ai_pr_prompt__idx'),
models.Index(fields=['tenant', 'prompt_type'], name='igny8_ai_pr_tenant__idx'),
],
},
),
migrations.CreateModel(
name='IntegrationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('integration_type', models.CharField(choices=[('openai', 'OpenAI'), ('runware', 'Runware'), ('gsc', 'Google Search Console')], db_index=True, max_length=50)),
('config', models.JSONField(default=dict, help_text='Integration configuration (API keys, settings, etc.)')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_integration_settings',
'ordering': ['integration_type'],
'unique_together': {('tenant', 'integration_type')},
'indexes': [
models.Index(fields=['integration_type'], name='igny8_integ_integra_idx'),
models.Index(fields=['tenant', 'integration_type'], name='igny8_integ_tenant__idx'),
],
},
),
]

View File

@@ -1,28 +0,0 @@
# Generated migration to add image_generation integration type
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0002_integration_settings_ai_prompts'),
]
operations = [
migrations.AlterField(
model_name='integrationsettings',
name='integration_type',
field=models.CharField(
choices=[
('openai', 'OpenAI'),
('runware', 'Runware'),
('gsc', 'Google Search Console'),
('image_generation', 'Image Generation Service'),
],
db_index=True,
max_length=50
),
),
]

View File

@@ -1,200 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-07 10:06
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
('system', '0003_add_image_generation_integration_type'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AISettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('integration_type', models.CharField(db_index=True, help_text="Integration type (e.g., 'openai', 'runware')", max_length=50)),
('config', models.JSONField(default=dict, help_text='Integration configuration (API keys, settings, etc.)')),
('model_preferences', models.JSONField(default=dict, help_text='Model preferences per operation type')),
('cost_limits', models.JSONField(default=dict, help_text='Cost limits and budgets')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'igny8_ai_settings',
'ordering': ['integration_type'],
},
),
migrations.CreateModel(
name='ModuleSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config', models.JSONField(default=dict, help_text='Settings configuration as JSON')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('module_name', models.CharField(db_index=True, help_text="Module name (e.g., 'planner', 'writer')", max_length=100)),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
],
options={
'db_table': 'igny8_module_settings',
'ordering': ['module_name', 'key'],
},
),
migrations.CreateModel(
name='SystemSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255, unique=True)),
('value', models.JSONField(default=dict, help_text='Settings value as JSON')),
('description', models.TextField(blank=True, help_text='Description of this setting')),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'igny8_system_settings',
'ordering': ['key'],
},
),
migrations.CreateModel(
name='TenantSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config', models.JSONField(default=dict, help_text='Settings configuration as JSON')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
],
options={
'db_table': 'igny8_tenant_settings',
'ordering': ['key'],
},
),
migrations.CreateModel(
name='UserSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
('value', models.JSONField(default=dict, help_text='Settings value as JSON')),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'igny8_user_settings',
'ordering': ['key'],
},
),
migrations.RenameIndex(
model_name='aiprompt',
new_name='igny8_ai_pr_prompt__4b2dbe_idx',
old_name='igny8_ai_pr_prompt__idx',
),
migrations.RenameIndex(
model_name='aiprompt',
new_name='igny8_ai_pr_tenant__9e7b95_idx',
old_name='igny8_ai_pr_tenant__idx',
),
migrations.RenameIndex(
model_name='integrationsettings',
new_name='igny8_integ_integra_5e382e_idx',
old_name='igny8_integ_integra_idx',
),
migrations.RenameIndex(
model_name='integrationsettings',
new_name='igny8_integ_tenant__5da472_idx',
old_name='igny8_integ_tenant__idx',
),
migrations.AlterField(
model_name='aiprompt',
name='prompt_type',
field=models.CharField(choices=[('clustering', 'Clustering'), ('ideas', 'Ideas Generation'), ('content_generation', 'Content Generation'), ('image_prompt_extraction', 'Image Prompt Extraction'), ('image_prompt_template', 'Image Prompt Template'), ('negative_prompt', 'Negative Prompt')], db_index=True, max_length=50),
),
migrations.AddField(
model_name='aisettings',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant'),
),
migrations.AddField(
model_name='modulesettings',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant'),
),
migrations.AddIndex(
model_name='systemsettings',
index=models.Index(fields=['key'], name='igny8_syste_key_20500b_idx'),
),
migrations.AddField(
model_name='tenantsettings',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant'),
),
migrations.AddField(
model_name='usersettings',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_settings', to='igny8_core_auth.tenant'),
),
migrations.AddField(
model_name='usersettings',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_settings', to=settings.AUTH_USER_MODEL),
),
migrations.RunSQL(
sql="DROP TABLE IF EXISTS igny8_system_logs CASCADE",
reverse_sql=migrations.RunSQL.noop,
),
migrations.RunSQL(
sql="DROP TABLE IF EXISTS igny8_system_status CASCADE",
reverse_sql=migrations.RunSQL.noop,
),
migrations.AddIndex(
model_name='aisettings',
index=models.Index(fields=['integration_type'], name='igny8_ai_se_integra_4f0b21_idx'),
),
migrations.AddIndex(
model_name='aisettings',
index=models.Index(fields=['tenant', 'integration_type'], name='igny8_ai_se_tenant__05ae98_idx'),
),
migrations.AlterUniqueTogether(
name='aisettings',
unique_together={('tenant', 'integration_type')},
),
migrations.AddIndex(
model_name='modulesettings',
index=models.Index(fields=['tenant', 'module_name', 'key'], name='igny8_modul_tenant__21ee25_idx'),
),
migrations.AddIndex(
model_name='modulesettings',
index=models.Index(fields=['module_name', 'key'], name='igny8_modul_module__95373a_idx'),
),
migrations.AlterUniqueTogether(
name='modulesettings',
unique_together={('tenant', 'module_name', 'key')},
),
migrations.AddIndex(
model_name='tenantsettings',
index=models.Index(fields=['tenant', 'key'], name='igny8_tenan_tenant__8ce0b3_idx'),
),
migrations.AlterUniqueTogether(
name='tenantsettings',
unique_together={('tenant', 'key')},
),
migrations.AddIndex(
model_name='usersettings',
index=models.Index(fields=['user', 'tenant', 'key'], name='igny8_user__user_id_ac09d9_idx'),
),
migrations.AddIndex(
model_name='usersettings',
index=models.Index(fields=['tenant', 'key'], name='igny8_user__tenant__01033d_idx'),
),
migrations.AlterUniqueTogether(
name='usersettings',
unique_together={('user', 'tenant', 'key')},
),
]

View File

@@ -1,77 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-07 11:34
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0010_add_seed_keyword'),
('system', '0004_aisettings_modulesettings_systemsettings_and_more'),
]
operations = [
migrations.CreateModel(
name='AuthorProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text="Profile name (e.g., 'SaaS B2B Informative')", max_length=255)),
('description', models.TextField(blank=True, help_text='Description of the writing style')),
('tone', models.CharField(help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical', 'Conversational')", max_length=100)),
('language', models.CharField(default='en', help_text="Language code (e.g., 'en', 'es', 'fr')", max_length=50)),
('structure_template', models.JSONField(default=dict, help_text='Structure template defining content sections and their order')),
('is_active', models.BooleanField(db_index=True, default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'verbose_name': 'Author Profile',
'verbose_name_plural': 'Author Profiles',
'db_table': 'igny8_author_profiles',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Strategy',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Strategy name', max_length=255)),
('description', models.TextField(blank=True, help_text='Description of the content strategy')),
('prompt_types', models.JSONField(default=list, help_text="List of prompt types to use (e.g., ['clustering', 'ideas', 'content_generation'])")),
('section_logic', models.JSONField(default=dict, help_text='Section logic configuration defining content structure and flow')),
('is_active', models.BooleanField(db_index=True, default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('sector', models.ForeignKey(blank=True, help_text='Optional: Link strategy to a specific sector', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='strategies', to='igny8_core_auth.sector')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'verbose_name': 'Strategy',
'verbose_name_plural': 'Strategies',
'db_table': 'igny8_strategies',
'ordering': ['name'],
},
),
migrations.AddIndex(
model_name='authorprofile',
index=models.Index(fields=['tenant', 'is_active'], name='igny8_autho_tenant__97d2c2_idx'),
),
migrations.AddIndex(
model_name='authorprofile',
index=models.Index(fields=['name'], name='igny8_autho_name_8295f3_idx'),
),
migrations.AddIndex(
model_name='strategy',
index=models.Index(fields=['tenant', 'is_active'], name='igny8_strat_tenant__344de9_idx'),
),
migrations.AddIndex(
model_name='strategy',
index=models.Index(fields=['tenant', 'sector'], name='igny8_strat_tenant__279cfa_idx'),
),
migrations.AddIndex(
model_name='strategy',
index=models.Index(fields=['name'], name='igny8_strat_name_8fe823_idx'),
),
]

View File

@@ -1,71 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-07 14:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('system', '0005_add_author_profile_strategy'),
]
operations = [
# Remove unique_together constraint if it exists and table exists
migrations.RunSQL(
"""
DO $$
BEGIN
-- Drop unique constraint if table and constraint exist
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'igny8_system_status'
) AND EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname LIKE '%systemstatus%tenant_id%component%'
) THEN
ALTER TABLE igny8_system_status DROP CONSTRAINT IF EXISTS igny8_system_status_tenant_id_component_key;
END IF;
END $$;
""",
reverse_sql=migrations.RunSQL.noop
),
# Only remove field if table exists
migrations.RunSQL(
"""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'igny8_system_status'
) AND EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'igny8_system_status' AND column_name = 'tenant_id'
) THEN
ALTER TABLE igny8_system_status DROP COLUMN IF EXISTS tenant_id;
END IF;
END $$;
""",
reverse_sql=migrations.RunSQL.noop
),
# Delete models only if tables exist
migrations.RunSQL(
"""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'igny8_system_logs'
) THEN
DROP TABLE IF EXISTS igny8_system_logs CASCADE;
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'igny8_system_status'
) THEN
DROP TABLE IF EXISTS igny8_system_status CASCADE;
END IF;
END $$;
""",
reverse_sql=migrations.RunSQL.noop
),
]

View File

@@ -1,39 +0,0 @@
# Generated manually for Phase 0: Module Enable Settings
# Using RunSQL to create table directly to avoid model resolution issues with new unified API model
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('system', '0006_alter_systemstatus_unique_together_and_more'),
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
]
operations = [
# Create table using raw SQL to avoid model resolution issues
# The model state is automatically discovered from models.py
migrations.RunSQL(
sql="""
CREATE TABLE IF NOT EXISTS igny8_module_enable_settings (
id BIGSERIAL PRIMARY KEY,
planner_enabled BOOLEAN NOT NULL DEFAULT TRUE,
writer_enabled BOOLEAN NOT NULL DEFAULT TRUE,
thinker_enabled BOOLEAN NOT NULL DEFAULT TRUE,
automation_enabled BOOLEAN NOT NULL DEFAULT TRUE,
site_builder_enabled BOOLEAN NOT NULL DEFAULT TRUE,
linker_enabled BOOLEAN NOT NULL DEFAULT TRUE,
optimizer_enabled BOOLEAN NOT NULL DEFAULT TRUE,
publisher_enabled BOOLEAN NOT NULL DEFAULT TRUE,
tenant_id BIGINT NOT NULL REFERENCES igny8_tenants(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS igny8_module_enable_settings_tenant_id_idx ON igny8_module_enable_settings(tenant_id);
CREATE INDEX IF NOT EXISTS igny8_module_enable_settings_account_created_idx ON igny8_module_enable_settings(tenant_id, created_at);
CREATE UNIQUE INDEX IF NOT EXISTS unique_account_module_enable_settings ON igny8_module_enable_settings(tenant_id);
""",
reverse_sql="DROP TABLE IF EXISTS igny8_module_enable_settings CASCADE;",
),
]

View File

@@ -1,31 +0,0 @@
# Generated manually for Phase 7: Prompt Management UI
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0007_add_module_enable_settings'),
]
operations = [
migrations.AlterField(
model_name='aiprompt',
name='prompt_type',
field=models.CharField(
choices=[
('clustering', 'Clustering'),
('ideas', 'Ideas Generation'),
('content_generation', 'Content Generation'),
('image_prompt_extraction', 'Image Prompt Extraction'),
('image_prompt_template', 'Image Prompt Template'),
('negative_prompt', 'Negative Prompt'),
('site_structure_generation', 'Site Structure Generation'),
],
db_index=True,
max_length=50
),
),
]

View File

@@ -1,34 +0,0 @@
# Generated manually for Phase 8: Universal Content Types
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0008_add_site_structure_generation_prompt_type'),
]
operations = [
migrations.AlterField(
model_name='aiprompt',
name='prompt_type',
field=models.CharField(
choices=[
('clustering', 'Clustering'),
('ideas', 'Ideas Generation'),
('content_generation', 'Content Generation'),
('image_prompt_extraction', 'Image Prompt Extraction'),
('image_prompt_template', 'Image Prompt Template'),
('negative_prompt', 'Negative Prompt'),
('site_structure_generation', 'Site Structure Generation'),
('product_generation', 'Product Content Generation'),
('service_generation', 'Service Page Generation'),
('taxonomy_generation', 'Taxonomy Generation'),
],
db_index=True,
max_length=50
),
),
]

View File

@@ -1,5 +1,6 @@
# Generated by Django 5.2.7 on 2025-11-03 13:22
# Generated by Django 5.2.8 on 2025-11-20 23:27
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
@@ -9,11 +10,53 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('igny8_core_auth', '0003_alter_user_role'),
('planner', '0003_alter_clusters_sector_alter_clusters_site_and_more'),
('igny8_core_auth', '0001_initial'),
('planner', '0001_initial'),
('site_building', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Content',
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)),
('html_content', models.TextField(help_text='Final AI-generated HTML content')),
('word_count', models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)])),
('metadata', models.JSONField(default=dict, help_text='Additional metadata (SEO, structure, etc.)')),
('title', models.CharField(blank=True, max_length=255, null=True)),
('meta_title', models.CharField(blank=True, max_length=255, null=True)),
('meta_description', models.TextField(blank=True, null=True)),
('primary_keyword', models.CharField(blank=True, max_length=255, null=True)),
('secondary_keywords', models.JSONField(blank=True, default=list, help_text='List of secondary keywords')),
('tags', models.JSONField(blank=True, default=list, help_text='List of tags')),
('categories', models.JSONField(blank=True, default=list, help_text='List of categories')),
('status', models.CharField(choices=[('draft', 'Draft'), ('review', 'Review'), ('publish', 'Publish')], default='draft', help_text='Content workflow status (draft, review, publish)', max_length=50)),
('generated_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('source', models.CharField(choices=[('igny8', 'IGNY8 Generated'), ('wordpress', 'WordPress Synced'), ('shopify', 'Shopify Synced'), ('custom', 'Custom API Synced')], db_index=True, default='igny8', help_text='Source of the content', max_length=50)),
('sync_status', models.CharField(choices=[('native', 'Native IGNY8 Content'), ('imported', 'Imported from External'), ('synced', 'Synced from External')], db_index=True, default='native', help_text='Sync status of the content', max_length=50)),
('external_id', models.CharField(blank=True, help_text='External platform ID', max_length=255, null=True)),
('external_url', models.URLField(blank=True, help_text='External platform URL', null=True)),
('sync_metadata', models.JSONField(blank=True, default=dict, help_text='Platform-specific sync metadata')),
('internal_links', models.JSONField(blank=True, default=list, help_text='Internal links added by linker')),
('linker_version', models.IntegerField(default=0, help_text='Version of linker processing')),
('optimizer_version', models.IntegerField(default=0, help_text='Version of optimizer processing')),
('optimization_scores', models.JSONField(blank=True, default=dict, help_text='Optimization scores (SEO, readability, engagement)')),
('entity_type', models.CharField(choices=[('blog_post', 'Blog Post'), ('article', 'Article'), ('product', 'Product'), ('service', 'Service Page'), ('taxonomy', 'Taxonomy Page'), ('page', 'Page')], db_index=True, default='blog_post', help_text='Type of content entity', max_length=50)),
('json_blocks', models.JSONField(blank=True, default=list, help_text='Structured content blocks (for products, services, taxonomies)')),
('structure_data', models.JSONField(blank=True, default=dict, help_text='Content structure data (metadata, schema, etc.)')),
('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')),
],
options={
'verbose_name': 'Content',
'verbose_name_plural': 'Contents',
'db_table': 'igny8_content',
'ordering': ['-generated_at'],
},
),
migrations.CreateModel(
name='Tasks',
fields=[
@@ -23,7 +66,9 @@ class Migration(migrations.Migration):
('keywords', models.CharField(blank=True, max_length=500)),
('content_structure', models.CharField(choices=[('cluster_hub', 'Cluster Hub'), ('landing_page', 'Landing Page'), ('pillar_page', 'Pillar Page'), ('supporting_page', 'Supporting Page')], default='blog_post', max_length=50)),
('content_type', models.CharField(choices=[('blog_post', 'Blog Post'), ('article', 'Article'), ('guide', 'Guide'), ('tutorial', 'Tutorial')], default='blog_post', max_length=50)),
('status', models.CharField(choices=[('queued', 'Queued'), ('in_progress', 'In Progress'), ('draft', 'Draft'), ('review', 'Review'), ('published', 'Published'), ('completed', 'Completed')], default='queued', max_length=50)),
('status', models.CharField(choices=[('queued', 'Queued'), ('completed', 'Completed')], default='queued', max_length=50)),
('entity_type', models.CharField(blank=True, choices=[('blog_post', 'Blog Post'), ('article', 'Article'), ('product', 'Product'), ('service', 'Service Page'), ('taxonomy', 'Taxonomy Page'), ('page', 'Page')], db_index=True, default='blog_post', help_text='Type of content entity (inherited from idea/blueprint)', max_length=50, null=True)),
('cluster_role', models.CharField(blank=True, choices=[('hub', 'Hub Page'), ('supporting', 'Supporting Page'), ('attribute', 'Attribute Page')], default='hub', help_text='Role within the cluster-driven sitemap', max_length=50, null=True)),
('content', models.TextField(blank=True, null=True)),
('word_count', models.IntegerField(default=0)),
('meta_title', models.CharField(blank=True, max_length=255, null=True)),
@@ -32,39 +77,110 @@ class Migration(migrations.Migration):
('post_url', models.URLField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
('cluster', models.ForeignKey(blank=True, limit_choices_to={'sector': models.F('sector')}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='planner.clusters')),
('idea', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='planner.contentideas')),
('keyword_objects', models.ManyToManyField(blank=True, help_text='Individual keywords linked to this task', related_name='tasks', to='planner.keywords')),
('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')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
('taxonomy', models.ForeignKey(blank=True, help_text='Taxonomy association when derived from blueprint planning', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='site_building.siteblueprinttaxonomy')),
],
options={
'verbose_name': 'Task',
'verbose_name_plural': 'Tasks',
'db_table': 'igny8_tasks',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TaskImages',
name='Images',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image_type', models.CharField(choices=[('featured', 'Featured Image'), ('desktop', 'Desktop Image'), ('mobile', 'Mobile Image'), ('in_article', 'In-Article Image')], default='featured', max_length=50)),
('image_url', models.URLField(blank=True, null=True)),
('image_path', models.CharField(blank=True, max_length=500, null=True)),
('prompt', models.TextField(blank=True, null=True)),
('status', models.CharField(default='pending', max_length=50)),
('position', models.IntegerField(default=0)),
('image_url', models.CharField(blank=True, help_text='URL of the generated/stored image', max_length=500, null=True)),
('image_path', models.CharField(blank=True, help_text='Local path if stored locally', max_length=500, null=True)),
('prompt', models.TextField(blank=True, help_text='Image generation prompt used', null=True)),
('status', models.CharField(default='pending', help_text='Status: pending, generated, failed', max_length=50)),
('position', models.IntegerField(default=0, help_text='Position for in-article images ordering')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
('content', models.ForeignKey(blank=True, help_text='The content this image belongs to (preferred)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', 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')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='writer.tasks')),
('task', models.ForeignKey(blank=True, help_text='The task this image belongs to (legacy, use content instead)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='writer.tasks')),
],
options={
'db_table': 'igny8_task_images',
'ordering': ['task', 'position', '-created_at'],
'verbose_name': 'Image',
'verbose_name_plural': 'Images',
'db_table': 'igny8_images',
'ordering': ['content', 'position', '-created_at'],
},
),
migrations.CreateModel(
name='ContentTaxonomyMap',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('source', models.CharField(choices=[('blueprint', 'Blueprint'), ('manual', 'Manual'), ('import', 'Import')], default='blueprint', max_length=50)),
('metadata', models.JSONField(blank=True, default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_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='%(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')),
('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={
'db_table': 'igny8_content_taxonomy_map',
},
),
migrations.CreateModel(
name='ContentClusterMap',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_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='%(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')),
('task', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cluster_mappings', to='writer.tasks')),
],
options={
'db_table': 'igny8_content_cluster_map',
},
),
migrations.CreateModel(
name='ContentAttributeMap',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_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='%(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')),
('task', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attribute_mappings', to='writer.tasks')),
],
options={
'db_table': 'igny8_content_attribute_map',
},
),
migrations.AddField(
model_name='content',
name='task',
field=models.OneToOneField(blank=True, help_text='The task this content belongs to', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content_record', to='writer.tasks'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['title'], name='igny8_tasks_title_adc50b_idx'),
@@ -81,16 +197,104 @@ class Migration(migrations.Migration):
model_name='tasks',
index=models.Index(fields=['content_type'], name='igny8_tasks_content_343539_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['entity_type'], name='igny8_tasks_entity__1dc185_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['cluster_role'], name='igny8_tasks_cluster_c87903_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['site', 'sector'], name='igny8_tasks_site_id_9880f4_idx'),
),
migrations.AddIndex(
model_name='taskimages',
index=models.Index(fields=['task', 'image_type'], name='igny8_task__task_id_143db4_idx'),
model_name='images',
index=models.Index(fields=['content', 'image_type'], name='igny8_image_content_830ae4_idx'),
),
migrations.AddIndex(
model_name='taskimages',
index=models.Index(fields=['status'], name='igny8_task__status_4f869f_idx'),
model_name='images',
index=models.Index(fields=['task', 'image_type'], name='igny8_image_task_id_c80e87_idx'),
),
migrations.AddIndex(
model_name='images',
index=models.Index(fields=['status'], name='igny8_image_status_4a4de2_idx'),
),
migrations.AddIndex(
model_name='images',
index=models.Index(fields=['content', 'position'], name='igny8_image_content_4ab256_idx'),
),
migrations.AddIndex(
model_name='images',
index=models.Index(fields=['task', 'position'], name='igny8_image_task_id_6340e6_idx'),
),
migrations.AddIndex(
model_name='contenttaxonomymap',
index=models.Index(fields=['taxonomy'], name='igny8_conte_taxonom_c3da58_idx'),
),
migrations.AddIndex(
model_name='contenttaxonomymap',
index=models.Index(fields=['content', 'taxonomy'], name='igny8_conte_content_9dc417_idx'),
),
migrations.AddIndex(
model_name='contenttaxonomymap',
index=models.Index(fields=['task', 'taxonomy'], name='igny8_conte_task_id_e94754_idx'),
),
migrations.AlterUniqueTogether(
name='contenttaxonomymap',
unique_together={('content', 'taxonomy')},
),
migrations.AddIndex(
model_name='contentclustermap',
index=models.Index(fields=['cluster', 'role'], name='igny8_conte_cluster_c12778_idx'),
),
migrations.AddIndex(
model_name='contentclustermap',
index=models.Index(fields=['content', 'role'], name='igny8_conte_content_9fa5cc_idx'),
),
migrations.AddIndex(
model_name='contentclustermap',
index=models.Index(fields=['task', 'role'], name='igny8_conte_task_id_821851_idx'),
),
migrations.AlterUniqueTogether(
name='contentclustermap',
unique_together={('content', 'cluster', 'role')},
),
migrations.AddIndex(
model_name='contentattributemap',
index=models.Index(fields=['name'], name='igny8_conte_name_f79e3e_idx'),
),
migrations.AddIndex(
model_name='contentattributemap',
index=models.Index(fields=['content', 'name'], name='igny8_conte_content_f255f2_idx'),
),
migrations.AddIndex(
model_name='contentattributemap',
index=models.Index(fields=['task', 'name'], name='igny8_conte_task_id_d60684_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['task'], name='igny8_conte_task_id_712988_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['generated_at'], name='igny8_conte_generat_7128df_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['source'], name='igny8_conte_source_5ef8a1_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['sync_status'], name='igny8_conte_sync_st_02d5bd_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['source', 'sync_status'], name='igny8_conte_source_df78d0_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['entity_type'], name='igny8_conte_entity__f559b3_idx'),
),
]

View File

@@ -1,63 +0,0 @@
"""Rename TaskImages, add Content model, and keyword links"""
from django.core.validators import MinValueValidator
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('planner', '0004_add_keyword_objects_to_contentideas'),
('writer', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='TaskImages',
new_name='Images',
),
migrations.AlterModelTable(
name='images',
table='igny8_images',
),
migrations.AlterModelOptions(
name='images',
options={'ordering': ['task', 'position', '-created_at']},
),
migrations.AddField(
model_name='tasks',
name='keyword_objects',
field=models.ManyToManyField(blank=True, help_text='Individual keywords linked to this task', related_name='tasks', to='planner.keywords'),
),
migrations.CreateModel(
name='Content',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('html_content', models.TextField(help_text='Final AI-generated HTML content')),
('word_count', models.IntegerField(default=0, help_text='Word count of generated content', validators=[MinValueValidator(0)])),
('metadata', models.JSONField(default=dict, help_text='Additional metadata (SEO, structure, etc.)')),
('generated_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('sector', models.ForeignKey(on_delete=models.CASCADE, related_name='content_set', to='igny8_core_auth.sector')),
('site', models.ForeignKey(on_delete=models.CASCADE, related_name='content_set', to='igny8_core_auth.site')),
('task', models.OneToOneField(help_text='The task this content belongs to', on_delete=models.CASCADE, related_name='content_record', to='writer.tasks')),
('tenant', models.ForeignKey(on_delete=models.CASCADE, related_name='content_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_content',
'ordering': ['-generated_at'],
},
),
migrations.AddIndex(
model_name='images',
index=models.Index(fields=['task', 'position'], name='igny8_images_task_id_pos_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['task'], name='igny8_content_task_id_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['generated_at'], name='igny8_content_generated_idx'),
),
]

View File

@@ -1,110 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-07 10:06
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
('writer', '0002_rename_taskimages_add_content_and_keywords'),
]
operations = [
migrations.AlterModelOptions(
name='content',
options={'ordering': ['-generated_at'], 'verbose_name': 'Content', 'verbose_name_plural': 'Contents'},
),
migrations.AlterModelOptions(
name='images',
options={'ordering': ['task', 'position', '-created_at'], 'verbose_name': 'Image', 'verbose_name_plural': 'Images'},
),
migrations.AlterModelOptions(
name='tasks',
options={'ordering': ['-created_at'], 'verbose_name': 'Task', 'verbose_name_plural': 'Tasks'},
),
migrations.RenameIndex(
model_name='content',
new_name='igny8_conte_task_id_712988_idx',
old_name='igny8_content_task_id_idx',
),
migrations.RenameIndex(
model_name='content',
new_name='igny8_conte_generat_7128df_idx',
old_name='igny8_content_generated_idx',
),
migrations.RenameIndex(
model_name='images',
new_name='igny8_image_task_id_c80e87_idx',
old_name='igny8_task__task_id_143db4_idx',
),
migrations.RenameIndex(
model_name='images',
new_name='igny8_image_status_4a4de2_idx',
old_name='igny8_task__status_4f869f_idx',
),
migrations.RenameIndex(
model_name='images',
new_name='igny8_image_task_id_6340e6_idx',
old_name='igny8_images_task_id_pos_idx',
),
migrations.AddField(
model_name='content',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AlterField(
model_name='content',
name='sector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector'),
),
migrations.AlterField(
model_name='content',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site'),
),
migrations.AlterField(
model_name='content',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant'),
),
migrations.AlterField(
model_name='content',
name='word_count',
field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='images',
name='image_path',
field=models.CharField(blank=True, help_text='Local path if stored locally', max_length=500, null=True),
),
migrations.AlterField(
model_name='images',
name='image_url',
field=models.URLField(blank=True, help_text='URL of the generated/stored image', null=True),
),
migrations.AlterField(
model_name='images',
name='position',
field=models.IntegerField(default=0, help_text='Position for in-article images ordering'),
),
migrations.AlterField(
model_name='images',
name='prompt',
field=models.TextField(blank=True, help_text='Image generation prompt used', null=True),
),
migrations.AlterField(
model_name='images',
name='status',
field=models.CharField(default='pending', help_text='Status: pending, generated, failed', max_length=50),
),
migrations.AlterField(
model_name='images',
name='task',
field=models.ForeignKey(help_text='The task this image belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='images', to='writer.tasks'),
),
]

View File

@@ -1,35 +0,0 @@
# Generated migration for adding SEO fields to Tasks model
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('writer', '0003_alter_content_options_alter_images_options_and_more'),
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
]
operations = [
migrations.AddField(
model_name='tasks',
name='primary_keyword',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='tasks',
name='secondary_keywords',
field=models.JSONField(blank=True, default=list, help_text='List of secondary keywords'),
),
migrations.AddField(
model_name='tasks',
name='tags',
field=models.JSONField(blank=True, default=list, help_text='List of tags'),
),
migrations.AddField(
model_name='tasks',
name='categories',
field=models.JSONField(blank=True, default=list, help_text='List of categories'),
),
]

View File

@@ -1,171 +0,0 @@
from django.db import migrations, models
def _normalize_list(value):
if not value:
return []
if isinstance(value, list):
return value
if isinstance(value, tuple):
return list(value)
return [value]
def forwards(apps, schema_editor):
Tasks = apps.get_model('writer', 'Tasks')
Content = apps.get_model('writer', 'Content')
for task in Tasks.objects.all():
account_id = getattr(task, 'account_id', None)
if account_id is None and getattr(task, 'account', None):
account_id = task.account.id
site_id = getattr(task, 'site_id', None)
if site_id is None and getattr(task, 'site', None):
site_id = task.site.id
sector_id = getattr(task, 'sector_id', None)
if sector_id is None and getattr(task, 'sector', None):
sector_id = task.sector.id if task.sector else None
tenant_id = getattr(task, 'tenant_id', None)
if tenant_id is None and getattr(task, 'tenant', None):
tenant_id = task.tenant.id
# Prepare defaults for new content record
defaults = {
'html_content': task.content or '',
'word_count': task.word_count or 0,
'title': getattr(task, 'title', None),
'meta_title': getattr(task, 'meta_title', None),
'meta_description': getattr(task, 'meta_description', None),
'primary_keyword': getattr(task, 'primary_keyword', None),
'secondary_keywords': _normalize_list(getattr(task, 'secondary_keywords', [])),
'tags': _normalize_list(getattr(task, 'tags', [])),
'categories': _normalize_list(getattr(task, 'categories', [])),
'status': 'draft',
}
content_record = Content.objects.filter(task_id=task.id).first()
created = False
if not content_record:
content_record = Content(task_id=task.id, **defaults)
created = True
# Update existing records with the latest information
if not created:
if task.content:
content_record.html_content = task.content
if task.word_count:
content_record.word_count = task.word_count
if getattr(task, 'title', None):
content_record.title = task.title
if getattr(task, 'meta_title', None):
content_record.meta_title = task.meta_title
if getattr(task, 'meta_description', None):
content_record.meta_description = task.meta_description
if hasattr(task, 'primary_keyword'):
content_record.primary_keyword = task.primary_keyword or ''
if hasattr(task, 'secondary_keywords'):
content_record.secondary_keywords = _normalize_list(task.secondary_keywords)
if hasattr(task, 'tags'):
content_record.tags = _normalize_list(task.tags)
if hasattr(task, 'categories'):
content_record.categories = _normalize_list(task.categories)
if not content_record.status:
content_record.status = 'draft'
# Ensure account/site/sector alignment (save() will also set this)
if account_id:
content_record.account_id = account_id
if site_id:
content_record.site_id = site_id
if sector_id:
content_record.sector_id = sector_id
if tenant_id:
content_record.tenant_id = tenant_id
# Preserve existing metadata but ensure it's a dict
metadata = content_record.metadata or {}
content_record.metadata = metadata
content_record.save()
def backwards(apps, schema_editor):
"""
Reverse data migration is intentionally left as a no-op because
reverting would require reintroducing the removed fields on Tasks.
"""
pass
class Migration(migrations.Migration):
dependencies = [
('writer', '0004_add_content_seo_fields'),
]
operations = [
migrations.AddField(
model_name='content',
name='categories',
field=models.JSONField(blank=True, default=list, help_text='List of categories'),
),
migrations.AddField(
model_name='content',
name='meta_description',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='content',
name='meta_title',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='content',
name='primary_keyword',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='content',
name='secondary_keywords',
field=models.JSONField(blank=True, default=list, help_text='List of secondary keywords'),
),
migrations.AddField(
model_name='content',
name='status',
field=models.CharField(default='draft', help_text='Content workflow status (draft, review, published, etc.)', max_length=50),
),
migrations.AddField(
model_name='content',
name='tags',
field=models.JSONField(blank=True, default=list, help_text='List of tags'),
),
migrations.AddField(
model_name='content',
name='title',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.RunPython(forwards, backwards),
migrations.RemoveField(
model_name='tasks',
name='categories',
),
migrations.RemoveField(
model_name='tasks',
name='primary_keyword',
),
migrations.RemoveField(
model_name='tasks',
name='secondary_keywords',
),
migrations.RemoveField(
model_name='tasks',
name='tags',
),
]

View File

@@ -1,60 +0,0 @@
# Generated manually to update status field choices and normalize data
from django.db import migrations, models
def normalize_statuses_forward(apps, schema_editor):
Tasks = apps.get_model('writer', 'Tasks')
Content = apps.get_model('writer', 'Content')
# Normalize task statuses to queued/completed
queued_states = {'queued', 'in_progress'}
completed_states = {'draft', 'review', 'published', 'completed'}
Tasks.objects.filter(status__in=queued_states).update(status='queued')
Tasks.objects.filter(status__in=completed_states).update(status='completed')
# Normalize content statuses: published -> publish (new choice)
Content.objects.filter(status='published').update(status='publish')
def normalize_statuses_backward(apps, schema_editor):
Tasks = apps.get_model('writer', 'Tasks')
Content = apps.get_model('writer', 'Content')
# Restore pre-normalization values
Tasks.objects.filter(status='queued').update(status='queued')
Tasks.objects.filter(status='completed').update(status='published')
Content.objects.filter(status='publish').update(status='published')
class Migration(migrations.Migration):
dependencies = [
('writer', '0005_move_content_fields_to_content'),
]
operations = [
migrations.RunPython(normalize_statuses_forward, normalize_statuses_backward),
migrations.AlterField(
model_name='tasks',
name='status',
field=models.CharField(
choices=[('queued', 'Queued'), ('completed', 'Completed')],
default='queued',
max_length=50,
),
),
migrations.AlterField(
model_name='content',
name='status',
field=models.CharField(
choices=[('draft', 'Draft'), ('review', 'Review'), ('publish', 'Publish')],
default='draft',
help_text='Content workflow status (draft, review, publish)',
max_length=50,
),
),
]

View File

@@ -1,83 +0,0 @@
# Generated manually for adding content ForeignKey to Images model
from django.db import migrations, models
import django.db.models.deletion
def migrate_task_to_content(apps, schema_editor):
"""Migrate existing Images to use content instead of task"""
Images = apps.get_model('writer', 'Images')
Content = apps.get_model('writer', 'Content')
# Update images that have a task with a content_record
for image in Images.objects.filter(task__isnull=False, content__isnull=True):
try:
# Try to get content via task.content_record
task = image.task
if task:
try:
content = Content.objects.get(task=task)
image.content = content
image.save(update_fields=['content'])
except Content.DoesNotExist:
# If content doesn't exist, leave content as null
pass
except Exception:
# If any error occurs, leave content as null
pass
class Migration(migrations.Migration):
dependencies = [
('writer', '0006_update_status_choices'),
]
operations = [
# Make task field nullable first
migrations.AlterField(
model_name='images',
name='task',
field=models.ForeignKey(
blank=True,
help_text='The task this image belongs to (legacy, use content instead)',
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='images',
to='writer.tasks'
),
),
# Add content field
migrations.AddField(
model_name='images',
name='content',
field=models.ForeignKey(
blank=True,
help_text='The content this image belongs to (preferred)',
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='images',
to='writer.content'
),
),
# Update ordering
migrations.AlterModelOptions(
name='images',
options={'ordering': ['content', 'position', '-created_at'], 'verbose_name': 'Image', 'verbose_name_plural': 'Images'},
),
# Add new indexes
migrations.AddIndex(
model_name='images',
index=models.Index(fields=['content', 'image_type'], name='igny8_image_content_image_type_idx'),
),
migrations.AddIndex(
model_name='images',
index=models.Index(fields=['content', 'position'], name='igny8_image_content_position_idx'),
),
# Data migration: populate content field from task.content_record
migrations.RunPython(
code=migrate_task_to_content,
reverse_code=migrations.RunPython.noop,
),
]

View File

@@ -1,19 +0,0 @@
# Generated migration to change image_url from URLField to CharField
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('writer', '0007_add_content_to_images'),
]
operations = [
migrations.AlterField(
model_name='images',
name='image_url',
field=models.CharField(blank=True, help_text='URL of the generated/stored image', max_length=500, null=True),
),
]

View File

@@ -1,119 +0,0 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
('writer', '0008_change_image_url_to_charfield'),
]
operations = [
migrations.AddField(
model_name='content',
name='source',
field=models.CharField(
choices=[
('igny8', 'IGNY8 Generated'),
('wordpress', 'WordPress Synced'),
('shopify', 'Shopify Synced'),
('custom', 'Custom API Synced'),
],
db_index=True,
default='igny8',
help_text='Source of the content',
max_length=50,
),
),
migrations.AddField(
model_name='content',
name='sync_status',
field=models.CharField(
choices=[
('native', 'Native IGNY8 Content'),
('imported', 'Imported from External'),
('synced', 'Synced from External'),
],
db_index=True,
default='native',
help_text='Sync status of the content',
max_length=50,
),
),
migrations.AddField(
model_name='content',
name='external_id',
field=models.CharField(
blank=True,
help_text='External platform ID',
max_length=255,
null=True,
),
),
migrations.AddField(
model_name='content',
name='external_url',
field=models.URLField(
blank=True,
help_text='External platform URL',
null=True,
),
),
migrations.AddField(
model_name='content',
name='sync_metadata',
field=models.JSONField(
blank=True,
default=dict,
help_text='Platform-specific sync metadata',
),
),
migrations.AddField(
model_name='content',
name='internal_links',
field=models.JSONField(
blank=True,
default=list,
help_text='Internal links added by linker',
),
),
migrations.AddField(
model_name='content',
name='linker_version',
field=models.IntegerField(
default=0,
help_text='Version of linker processing',
),
),
migrations.AddField(
model_name='content',
name='optimizer_version',
field=models.IntegerField(
default=0,
help_text='Version of optimizer processing',
),
),
migrations.AddField(
model_name='content',
name='optimization_scores',
field=models.JSONField(
blank=True,
default=dict,
help_text='Optimization scores (SEO, readability, engagement)',
),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['source'], name='igny8_conte_source_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['sync_status'], name='igny8_conte_sync_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['source', 'sync_status'], name='igny8_conte_source_sync_idx'),
),
]

View File

@@ -1,26 +0,0 @@
# Generated migration to make Content.task nullable for Phase 4 synced content
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('writer', '0009_add_content_site_source_fields'),
]
operations = [
migrations.AlterField(
model_name='content',
name='task',
field=models.OneToOneField(
blank=True,
help_text='The task this content belongs to',
null=True,
on_delete=models.CASCADE,
related_name='content_record',
to='writer.tasks'
),
),
]

View File

@@ -1,54 +0,0 @@
# Generated manually for Phase 8: Universal Content Types
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('writer', '0010_make_content_task_nullable'),
]
operations = [
migrations.AddField(
model_name='content',
name='entity_type',
field=models.CharField(
choices=[
('blog_post', 'Blog Post'),
('article', 'Article'),
('product', 'Product'),
('service', 'Service Page'),
('taxonomy', 'Taxonomy Page'),
('page', 'Page'),
],
db_index=True,
default='blog_post',
help_text='Type of content entity',
max_length=50
),
),
migrations.AddField(
model_name='content',
name='json_blocks',
field=models.JSONField(
blank=True,
default=list,
help_text='Structured content blocks (for products, services, taxonomies)'
),
),
migrations.AddField(
model_name='content',
name='structure_data',
field=models.JSONField(
blank=True,
default=dict,
help_text='Content structure data (metadata, schema, etc.)'
),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['entity_type'], name='igny8_conte_entity__idx'),
),
]

View File

@@ -1,252 +0,0 @@
from django.db import migrations, models
import django.db.models.deletion
def backfill_metadata_mappings_stub(apps, schema_editor):
"""
Stage 3: Backfill metadata mappings for existing Content/Task records.
This function backfills:
- ContentClusterMap records from existing Content/Task -> Cluster relationships
- ContentTaxonomyMap records from existing taxonomy associations
- ContentAttributeMap records from existing attribute data
- entity_type on Tasks from existing content_type or other fields (if field exists)
"""
Tasks = apps.get_model('writer', 'Tasks')
Content = apps.get_model('writer', 'Content')
ContentClusterMap = apps.get_model('writer', 'ContentClusterMap')
ContentTaxonomyMap = apps.get_model('writer', 'ContentTaxonomyMap')
ContentAttributeMap = apps.get_model('writer', 'ContentAttributeMap')
# Check if entity_type field exists (added in migration 0013)
task_fields = [f.name for f in Tasks._meta.get_fields()]
has_entity_type = 'entity_type' in task_fields
# Backfill Tasks: Set entity_type from content_type if field exists and not set
tasks_updated = 0
if has_entity_type:
for task in Tasks.objects.filter(entity_type__isnull=True):
# Map content_type to entity_type
entity_type_map = {
'blog_post': 'blog_post',
'article': 'article',
'guide': 'article',
'tutorial': 'article',
}
task.entity_type = entity_type_map.get(task.content_type, 'blog_post')
task.save(update_fields=['entity_type'])
tasks_updated += 1
# Backfill Content: Set entity_type from task if not set
content_updated = 0
content_fields = [f.name for f in Content._meta.get_fields()]
if 'entity_type' in content_fields:
for content in Content.objects.filter(entity_type__isnull=True):
if content.task and has_entity_type and hasattr(content.task, 'entity_type') and content.task.entity_type:
content.entity_type = content.task.entity_type
content.save(update_fields=['entity_type'])
content_updated += 1
# Backfill ContentClusterMap: Create mappings from Task->Cluster relationships
cluster_maps_created = 0
has_cluster_role = 'cluster_role' in task_fields
content_fields = [f.name for f in Content._meta.get_fields()]
for task in Tasks.objects.filter(cluster__isnull=False):
# Find all Content records for this task
contents = Content.objects.filter(task=task)
for content in contents:
# Check if mapping already exists
if not ContentClusterMap.objects.filter(
content=content,
cluster=task.cluster
).exists():
# Get cluster_role if field exists
role = 'hub' # Default
if has_cluster_role and hasattr(task, 'cluster_role') and task.cluster_role:
role = task.cluster_role
# Get account/site/sector from content or task
account_id = getattr(content, 'account_id', None) or getattr(content, 'tenant_id', None) or getattr(task, 'account_id', None) or getattr(task, 'tenant_id', None)
site_id = getattr(content, 'site_id', None) or getattr(task, 'site_id', None)
sector_id = getattr(content, 'sector_id', None) or getattr(task, 'sector_id', None)
if account_id and site_id and sector_id:
ContentClusterMap.objects.create(
content=content,
task=task,
cluster=task.cluster,
role=role,
account_id=account_id,
site_id=site_id,
sector_id=sector_id,
source='blueprint' if task.idea else 'manual',
metadata={},
)
cluster_maps_created += 1
# Backfill ContentTaxonomyMap: Create mappings from Task->Taxonomy relationships
taxonomy_maps_created = 0
has_taxonomy = 'taxonomy' in task_fields
if has_taxonomy:
for task in Tasks.objects.filter(taxonomy__isnull=False):
contents = Content.objects.filter(task=task)
for content in contents:
if not ContentTaxonomyMap.objects.filter(
content=content,
taxonomy=task.taxonomy
).exists():
# Get account/site/sector from content or task
account_id = getattr(content, 'account_id', None) or getattr(content, 'tenant_id', None) or getattr(task, 'account_id', None) or getattr(task, 'tenant_id', None)
site_id = getattr(content, 'site_id', None) or getattr(task, 'site_id', None)
sector_id = getattr(content, 'sector_id', None) or getattr(task, 'sector_id', None)
if account_id and site_id and sector_id:
ContentTaxonomyMap.objects.create(
content=content,
task=task,
taxonomy=task.taxonomy,
account_id=account_id,
site_id=site_id,
sector_id=sector_id,
source='blueprint',
metadata={},
)
taxonomy_maps_created += 1
print(f"Backfill complete:")
print(f" - Tasks entity_type updated: {tasks_updated}")
print(f" - Content entity_type updated: {content_updated}")
print(f" - Cluster mappings created: {cluster_maps_created}")
print(f" - Taxonomy mappings created: {taxonomy_maps_created}")
def reverse_backfill_metadata_mappings_stub(apps, schema_editor):
"""
Reverse operation for metadata backfill (no-op for Stage 1).
"""
pass
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.tenant')),
('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.tenant')),
('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.tenant')),
('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'),
),
# Stage 1: Data migration stub for Stage 3 backfill
migrations.RunPython(
backfill_metadata_mappings_stub,
reverse_backfill_metadata_mappings_stub,
),
]

View File

@@ -1,70 +0,0 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('writer', '0012_metadata_mapping_tables'),
('site_building', '0003_workflow_and_taxonomies'),
]
operations = [
migrations.AddField(
model_name='tasks',
name='entity_type',
field=models.CharField(
blank=True,
choices=[
('blog_post', 'Blog Post'),
('article', 'Article'),
('product', 'Product'),
('service', 'Service Page'),
('taxonomy', 'Taxonomy Page'),
('page', 'Page'),
],
db_index=True,
default='blog_post',
help_text='Type of content entity (inherited from idea/blueprint)',
max_length=50,
null=True,
),
),
migrations.AddField(
model_name='tasks',
name='taxonomy',
field=models.ForeignKey(
blank=True,
help_text='Taxonomy association when derived from blueprint planning',
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='tasks',
to='site_building.SiteBlueprintTaxonomy',
),
),
migrations.AddField(
model_name='tasks',
name='cluster_role',
field=models.CharField(
blank=True,
choices=[
('hub', 'Hub Page'),
('supporting', 'Supporting Page'),
('attribute', 'Attribute Page'),
],
default='hub',
help_text='Role within the cluster-driven sitemap',
max_length=50,
null=True,
),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['entity_type'], name='writer_tasks_entity_type_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['cluster_role'], name='writer_tasks_cluster_role_idx'),
),
]