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:
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user