From 9f826c92f805d09ed81d12720a7051e6d5013eec Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Wed, 17 Dec 2025 05:58:13 +0000 Subject: [PATCH] fixes for idea render and other --- backend/igny8_core/auth/admin.py | 12 ++++-- .../igny8_core/business/planning/models.py | 7 ++++ backend/igny8_core/modules/planner/views.py | 13 ++++++- .../components/common/HTMLContentRenderer.tsx | 39 +++++++++++++++++-- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/backend/igny8_core/auth/admin.py b/backend/igny8_core/auth/admin.py index dba61dd3..8b054ac0 100644 --- a/backend/igny8_core/auth/admin.py +++ b/backend/igny8_core/auth/admin.py @@ -354,13 +354,19 @@ class SectorInline(TabularInline): def get_keywords_count(self, obj): if obj.pk: - return getattr(obj, 'keywords_set', obj.keywords_set).count() + try: + return obj.keywords_set.count() + except (AttributeError, Exception): + return 0 return 0 get_keywords_count.short_description = 'Keywords' def get_clusters_count(self, obj): if obj.pk: - return getattr(obj, 'clusters_set', obj.clusters_set).count() + try: + return obj.clusters_set.count() + except (AttributeError, Exception): + return 0 return 0 get_clusters_count.short_description = 'Clusters' @@ -404,8 +410,8 @@ class SiteAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin): def get_api_key_display(self, obj): """Display API key with copy button""" + from django.utils.html import format_html if obj.wp_api_key: - from django.utils.html import format_html return format_html( '
' '{}' diff --git a/backend/igny8_core/business/planning/models.py b/backend/igny8_core/business/planning/models.py index be6cb1d4..65651e11 100644 --- a/backend/igny8_core/business/planning/models.py +++ b/backend/igny8_core/business/planning/models.py @@ -108,6 +108,13 @@ class Keywords(SoftDeletableModel, SiteSectorBaseModel): objects = SoftDeleteManager() all_objects = models.Manager() + def soft_delete(self, user=None, reason=None, retention_days=None): + """Override soft_delete to clear seed_keyword FK to prevent PROTECT issues""" + # Clear the seed_keyword FK before soft delete to prevent cascade protection issues + # This allows SeedKeywords to be managed independently after Keywords are deleted + self.seed_keyword = None + super().soft_delete(user=user, reason=reason, retention_days=retention_days) + @property def keyword(self): """Get keyword text from seed_keyword""" diff --git a/backend/igny8_core/modules/planner/views.py b/backend/igny8_core/modules/planner/views.py index 3919757a..9b84b763 100644 --- a/backend/igny8_core/modules/planner/views.py +++ b/backend/igny8_core/modules/planner/views.py @@ -215,9 +215,15 @@ class KeywordViewSet(SiteSectorModelViewSet): # Save with all required fields explicitly serializer.save(account=account, site=site, sector=sector) + def destroy(self, request, *args, **kwargs): + """Override destroy to use hard delete for keywords""" + instance = self.get_object() + instance.hard_delete() + return Response(status=status.HTTP_204_NO_CONTENT) + @action(detail=False, methods=['POST'], url_path='bulk_delete', url_name='bulk_delete') def bulk_delete(self, request): - """Bulk delete keywords""" + """Bulk delete keywords - uses hard delete to avoid unique constraint issues""" ids = request.data.get('ids', []) if not ids: return error_response( @@ -229,7 +235,10 @@ class KeywordViewSet(SiteSectorModelViewSet): queryset = self.get_queryset() items_to_delete = queryset.filter(id__in=ids) deleted_count = items_to_delete.count() - items_to_delete.delete() # Soft delete via SoftDeletableModel + + # Hard delete to avoid unique constraint violations when re-adding same keywords + for item in items_to_delete: + item.hard_delete() return success_response(data={'deleted_count': deleted_count}, request=request) diff --git a/frontend/src/components/common/HTMLContentRenderer.tsx b/frontend/src/components/common/HTMLContentRenderer.tsx index c20f5c6b..938ce3f1 100644 --- a/frontend/src/components/common/HTMLContentRenderer.tsx +++ b/frontend/src/components/common/HTMLContentRenderer.tsx @@ -28,6 +28,39 @@ function formatContentOutline(content: any): string { let html = '
'; + // NEW FORMAT: Handle overview + outline structure + if (content.overview && content.outline) { + // Display overview + html += '
'; + html += `
Overview: ${escapeHTML(content.overview)}
`; + html += '
'; + + // Display intro focus if available + if (content.outline.intro_focus) { + html += '
'; + html += `

Introduction Focus

`; + html += `
${escapeHTML(content.outline.intro_focus)}
`; + html += '
'; + } + + // Display main sections + if (content.outline.main_sections && Array.isArray(content.outline.main_sections)) { + content.outline.main_sections.forEach((section: any) => { + html += '
'; + if (section.h2_topic) { + html += `

${escapeHTML(section.h2_topic)}

`; + } + if (section.coverage) { + html += `
${escapeHTML(section.coverage)}
`; + } + html += '
'; + }); + } + + html += '
'; + return html; + } + // Handle introduction section - can be object or string if (content.introduction) { html += '
'; @@ -176,7 +209,7 @@ const HTMLContentRenderer: React.FC = ({ // If content is already an object (dict), use it directly if (typeof content === 'object' && content !== null) { // Check for any known structure format - if (content.H2 || content.H3 || content.introduction || content.sections) { + if (content.overview || content.outline || content.H2 || content.H3 || content.introduction || content.sections) { return formatContentOutline(content); } // If it's an object but not structured, try to format it @@ -225,7 +258,7 @@ const HTMLContentRenderer: React.FC = ({ } // Use extracted content as-is (will be processed below) content = extractedContent; - } else if (parsed.H2 || parsed.H3 || parsed.introduction || parsed.sections) { + } else if (parsed.overview || parsed.outline || parsed.H2 || parsed.H3 || parsed.introduction || parsed.sections) { // It's a content outline structure return formatContentOutline(parsed); } @@ -238,7 +271,7 @@ const HTMLContentRenderer: React.FC = ({ // Try to parse as JSON (content outline from GPT-4o mini) - for non-brace-starting JSON try { const parsed = JSON.parse(content); - if (typeof parsed === 'object' && (parsed.H2 || parsed.H3 || parsed.introduction || parsed.sections)) { + if (typeof parsed === 'object' && (parsed.overview || parsed.outline || parsed.H2 || parsed.H3 || parsed.introduction || parsed.sections)) { return formatContentOutline(parsed); } } catch {