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 = '