From ca5451c7957a96e4d1d6bf9cbed58ed0d63740b6 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Mon, 1 Dec 2025 08:54:52 +0000 Subject: [PATCH] fixes --- backend/igny8_core/auth/admin.py | 4 +- backend/igny8_core/auth/serializers.py | 2 +- backend/igny8_core/business/content/models.py | 1 + ...alter_publishingrecord_content_and_more.py | 25 +++ .../publishing/services/publisher_service.py | 14 +- .../igny8_core/modules/integration/views.py | 7 + ...metadata_content_external_type_and_more.py | 151 ++++++++++++++++++ 7 files changed, 196 insertions(+), 8 deletions(-) create mode 100644 backend/igny8_core/business/publishing/migrations/0002_alter_publishingrecord_content_and_more.py create mode 100644 backend/igny8_core/modules/writer/migrations/0011_content_external_metadata_content_external_type_and_more.py diff --git a/backend/igny8_core/auth/admin.py b/backend/igny8_core/auth/admin.py index 9bb277de..58c4d5b1 100644 --- a/backend/igny8_core/auth/admin.py +++ b/backend/igny8_core/auth/admin.py @@ -117,8 +117,8 @@ class SiteAdmin(AccountAdminMixin, admin.ModelAdmin): 'fields': ('name', 'slug', 'account', 'domain', 'description', 'industry', 'site_type', 'hosting_type', 'status', 'is_active') }), ('WordPress Integration', { - 'fields': ('wp_url', 'wp_username', 'wp_app_password', 'get_api_key_display'), - 'description': 'Legacy WordPress integration fields. For WordPress sites using the IGNY8 WP Bridge plugin.' + 'fields': ('get_api_key_display',), + 'description': 'WordPress integration API key. Use SiteIntegration model for full integration settings.' }), ('SEO Metadata', { 'fields': ('seo_metadata',), diff --git a/backend/igny8_core/auth/serializers.py b/backend/igny8_core/auth/serializers.py index 23a1c35a..04d64890 100644 --- a/backend/igny8_core/auth/serializers.py +++ b/backend/igny8_core/auth/serializers.py @@ -68,7 +68,7 @@ class SiteSerializer(serializers.ModelSerializer): fields = [ 'id', 'name', 'slug', 'domain', 'description', 'industry', 'industry_name', 'industry_slug', - 'is_active', 'status', 'wp_url', 'wp_username', 'wp_api_key', + 'is_active', 'status', 'site_type', 'hosting_type', 'seo_metadata', 'sectors_count', 'active_sectors_count', 'selected_sectors', 'can_add_sectors', diff --git a/backend/igny8_core/business/content/models.py b/backend/igny8_core/business/content/models.py index f0daa8ff..73aae514 100644 --- a/backend/igny8_core/business/content/models.py +++ b/backend/igny8_core/business/content/models.py @@ -217,6 +217,7 @@ class Content(SiteSectorBaseModel): external_id = models.CharField(max_length=255, blank=True, null=True, db_index=True, help_text="WordPress/external platform post ID") external_url = models.URLField(blank=True, null=True, help_text="WordPress/external platform URL") external_type = models.CharField(max_length=100, blank=True, null=True, help_text="WordPress post type (post, page, product, etc.)") + external_metadata = models.JSONField(blank=True, null=True, default=dict, help_text="External platform metadata (WordPress term IDs, etc.)") sync_status = models.CharField(max_length=50, blank=True, null=True, help_text="Sync status with WordPress") # Source tracking diff --git a/backend/igny8_core/business/publishing/migrations/0002_alter_publishingrecord_content_and_more.py b/backend/igny8_core/business/publishing/migrations/0002_alter_publishingrecord_content_and_more.py new file mode 100644 index 00000000..68c3f21f --- /dev/null +++ b/backend/igny8_core/business/publishing/migrations/0002_alter_publishingrecord_content_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.8 on 2025-12-01 08:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('igny8_core_auth', '0003_add_sync_event_model'), + ('publishing', '0001_initial'), + ('writer', '0010_add_review_status_to_content'), + ] + + operations = [ + migrations.AlterField( + model_name='publishingrecord', + name='content', + field=models.ForeignKey(blank=True, help_text='Content being published', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publishing_records', to='writer.content'), + ), + migrations.AddIndex( + model_name='deploymentrecord', + index=models.Index(fields=['site', 'status'], name='igny8_deplo_site_id_0de333_idx'), + ), + ] diff --git a/backend/igny8_core/business/publishing/services/publisher_service.py b/backend/igny8_core/business/publishing/services/publisher_service.py index b0339eb8..7e89f1b6 100644 --- a/backend/igny8_core/business/publishing/services/publisher_service.py +++ b/backend/igny8_core/business/publishing/services/publisher_service.py @@ -173,11 +173,15 @@ class PublisherService: # NEW: Save term_ids to external_metadata if available if result.get('metadata') and result['metadata'].get('term_ids'): - if not content.external_metadata: - content.external_metadata = {} - content.external_metadata['wordpress_term_ids'] = result['metadata']['term_ids'] - logger.info(f"[PublisherService._publish_to_destination] 🏷️ Saved term_ids to external_metadata: {result['metadata']['term_ids']}") - content.save(update_fields=['status', 'external_id', 'external_url', 'external_metadata', 'updated_at']) + if not hasattr(content, 'external_metadata'): + logger.warning(f"[PublisherService._publish_to_destination] ⚠️ Content model missing external_metadata field - run migrations") + content.save(update_fields=['status', 'external_id', 'external_url', 'updated_at']) + else: + if content.external_metadata is None: + content.external_metadata = {} + content.external_metadata['wordpress_term_ids'] = result['metadata']['term_ids'] + logger.info(f"[PublisherService._publish_to_destination] 🏷️ Saved term_ids to external_metadata: {result['metadata']['term_ids']}") + content.save(update_fields=['status', 'external_id', 'external_url', 'external_metadata', 'updated_at']) else: content.save(update_fields=['status', 'external_id', 'external_url', 'updated_at']) diff --git a/backend/igny8_core/modules/integration/views.py b/backend/igny8_core/modules/integration/views.py index 7a9b5511..af102089 100644 --- a/backend/igny8_core/modules/integration/views.py +++ b/backend/igny8_core/modules/integration/views.py @@ -55,6 +55,13 @@ class IntegrationViewSet(SiteSectorModelViewSet): from rest_framework import serializers class SiteIntegrationSerializer(serializers.ModelSerializer): + api_key = serializers.SerializerMethodField() + + def get_api_key(self, obj): + """Return the API key from encrypted credentials""" + credentials = obj.get_credentials() + return credentials.get('api_key', '') + def validate(self, data): """ Custom validation for WordPress integrations. diff --git a/backend/igny8_core/modules/writer/migrations/0011_content_external_metadata_content_external_type_and_more.py b/backend/igny8_core/modules/writer/migrations/0011_content_external_metadata_content_external_type_and_more.py new file mode 100644 index 00000000..9cb683e6 --- /dev/null +++ b/backend/igny8_core/modules/writer/migrations/0011_content_external_metadata_content_external_type_and_more.py @@ -0,0 +1,151 @@ +# Generated by Django 5.2.8 on 2025-12-01 08:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('planner', '0005_field_rename_implementation'), + ('writer', '0010_add_review_status_to_content'), + ] + + operations = [ + migrations.AddField( + model_name='content', + name='external_metadata', + field=models.JSONField(blank=True, default=dict, help_text='External platform metadata (WordPress term IDs, etc.)', null=True), + ), + migrations.AddField( + model_name='content', + name='external_type', + field=models.CharField(blank=True, help_text='WordPress post type (post, page, product, etc.)', max_length=100, null=True), + ), + migrations.AddField( + model_name='content', + name='meta_description', + field=models.TextField(blank=True, help_text='SEO meta description', null=True), + ), + migrations.AddField( + model_name='content', + name='meta_title', + field=models.CharField(blank=True, help_text='SEO meta title', max_length=255, null=True), + ), + migrations.AddField( + model_name='content', + name='primary_keyword', + field=models.CharField(blank=True, help_text='Primary SEO keyword', max_length=255, null=True), + ), + migrations.AddField( + model_name='content', + name='secondary_keywords', + field=models.JSONField(blank=True, default=list, help_text='Secondary SEO keywords'), + ), + migrations.AddField( + model_name='content', + name='sync_status', + field=models.CharField(blank=True, help_text='Sync status with WordPress', max_length=50, null=True), + ), + migrations.AddField( + model_name='content', + name='word_count', + field=models.IntegerField(default=0, help_text='Actual word count of content (calculated from HTML)'), + ), + migrations.AddField( + model_name='contenttaxonomy', + name='count', + field=models.IntegerField(default=0, help_text='Number of times this term is used'), + ), + migrations.AddField( + model_name='contenttaxonomy', + name='description', + field=models.TextField(blank=True, default='', help_text='Taxonomy term description'), + ), + migrations.AddField( + model_name='contenttaxonomy', + name='metadata', + field=models.JSONField(blank=True, default=dict, help_text='Additional metadata (AI generation details, etc.)'), + ), + migrations.AddField( + model_name='contenttaxonomy', + name='sync_status', + field=models.CharField(blank=True, default='', help_text='Sync status with external platform', max_length=50), + ), + migrations.AddField( + model_name='tasks', + name='idea', + field=models.ForeignKey(blank=True, db_column='idea_id', help_text='Optional content idea reference', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='planner.contentideas'), + ), + migrations.AlterField( + model_name='content', + name='content_structure', + field=models.CharField(choices=[('article', 'Article'), ('guide', 'Guide'), ('comparison', 'Comparison'), ('review', 'Review'), ('listicle', 'Listicle'), ('landing_page', 'Landing Page'), ('business_page', 'Business Page'), ('service_page', 'Service Page'), ('general', 'General'), ('cluster_hub', 'Cluster Hub'), ('product_page', 'Product Page'), ('category_archive', 'Category Archive'), ('tag_archive', 'Tag Archive'), ('attribute_archive', 'Attribute Archive')], db_index=True, default='article', help_text='Content structure/format based on content type', max_length=50), + ), + migrations.AlterField( + model_name='content', + name='content_type', + field=models.CharField(choices=[('post', 'Post'), ('page', 'Page'), ('product', 'Product'), ('taxonomy', 'Taxonomy')], db_index=True, default='post', help_text='Content type: post, page, product, taxonomy', max_length=50), + ), + migrations.AlterField( + model_name='contenttaxonomy', + name='external_id', + field=models.IntegerField(blank=True, db_index=True, help_text='WordPress term_id for sync', null=True), + ), + migrations.AlterField( + model_name='contenttaxonomy', + name='external_taxonomy', + field=models.CharField(blank=True, default='', help_text='WordPress taxonomy slug (category, post_tag)', max_length=100), + ), + migrations.AlterField( + model_name='contenttaxonomy', + name='taxonomy_type', + field=models.CharField(choices=[('category', 'Category'), ('tag', 'Tag')], db_index=True, help_text='Type of taxonomy (category or tag)', max_length=50), + ), + migrations.AlterField( + model_name='tasks', + name='content_structure', + field=models.CharField(blank=True, choices=[('article', 'Article'), ('guide', 'Guide'), ('comparison', 'Comparison'), ('review', 'Review'), ('listicle', 'Listicle'), ('landing_page', 'Landing Page'), ('business_page', 'Business Page'), ('service_page', 'Service Page'), ('general', 'General'), ('cluster_hub', 'Cluster Hub'), ('product_page', 'Product Page'), ('category_archive', 'Category Archive'), ('tag_archive', 'Tag Archive'), ('attribute_archive', 'Attribute Archive')], db_index=True, default='article', help_text='Content structure: article, guide, comparison, review, listicle, landing_page, etc.', max_length=100, null=True), + ), + migrations.AlterField( + model_name='tasks', + name='content_type', + field=models.CharField(blank=True, choices=[('post', 'Post'), ('page', 'Page'), ('product', 'Product'), ('taxonomy', 'Taxonomy')], db_index=True, default='post', help_text='Content type: post, page, product, taxonomy', max_length=100, null=True), + ), + migrations.RemoveField( + model_name='tasks', + name='keywords', + ), + migrations.AlterField( + model_name='tasks', + name='taxonomy_term', + field=models.ForeignKey(blank=True, db_column='taxonomy_id', help_text='Optional taxonomy term assignment', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='writer.contenttaxonomy'), + ), + migrations.CreateModel( + name='ContentTaxonomyRelation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('content', models.ForeignKey(db_column='content_id', on_delete=django.db.models.deletion.CASCADE, to='writer.content')), + ('taxonomy', models.ForeignKey(db_column='taxonomy_id', on_delete=django.db.models.deletion.CASCADE, to='writer.contenttaxonomy')), + ], + options={ + 'db_table': 'igny8_content_taxonomy_relations', + 'unique_together': {('content', 'taxonomy')}, + }, + ), + migrations.AlterField( + model_name='content', + name='taxonomy_terms', + field=models.ManyToManyField(blank=True, help_text='Associated taxonomy terms (categories, tags, attributes)', related_name='contents', through='writer.ContentTaxonomyRelation', to='writer.contenttaxonomy'), + ), + migrations.DeleteModel( + name='ContentTaxonomyMap', + ), + migrations.AddField( + model_name='tasks', + name='keywords', + field=models.TextField(blank=True, help_text='Comma-separated keywords for this task', null=True), + ), + ]