fixes for idea render and other
This commit is contained in:
@@ -354,13 +354,19 @@ class SectorInline(TabularInline):
|
|||||||
|
|
||||||
def get_keywords_count(self, obj):
|
def get_keywords_count(self, obj):
|
||||||
if obj.pk:
|
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
|
return 0
|
||||||
get_keywords_count.short_description = 'Keywords'
|
get_keywords_count.short_description = 'Keywords'
|
||||||
|
|
||||||
def get_clusters_count(self, obj):
|
def get_clusters_count(self, obj):
|
||||||
if obj.pk:
|
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
|
return 0
|
||||||
get_clusters_count.short_description = 'Clusters'
|
get_clusters_count.short_description = 'Clusters'
|
||||||
|
|
||||||
@@ -404,8 +410,8 @@ class SiteAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|||||||
|
|
||||||
def get_api_key_display(self, obj):
|
def get_api_key_display(self, obj):
|
||||||
"""Display API key with copy button"""
|
"""Display API key with copy button"""
|
||||||
|
from django.utils.html import format_html
|
||||||
if obj.wp_api_key:
|
if obj.wp_api_key:
|
||||||
from django.utils.html import format_html
|
|
||||||
return format_html(
|
return format_html(
|
||||||
'<div style="display:flex; align-items:center; gap:10px;">'
|
'<div style="display:flex; align-items:center; gap:10px;">'
|
||||||
'<code style="background:#f0f0f0; padding:5px 10px; border-radius:3px;">{}</code>'
|
'<code style="background:#f0f0f0; padding:5px 10px; border-radius:3px;">{}</code>'
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ class Keywords(SoftDeletableModel, SiteSectorBaseModel):
|
|||||||
objects = SoftDeleteManager()
|
objects = SoftDeleteManager()
|
||||||
all_objects = models.Manager()
|
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
|
@property
|
||||||
def keyword(self):
|
def keyword(self):
|
||||||
"""Get keyword text from seed_keyword"""
|
"""Get keyword text from seed_keyword"""
|
||||||
|
|||||||
@@ -215,9 +215,15 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
# Save with all required fields explicitly
|
# Save with all required fields explicitly
|
||||||
serializer.save(account=account, site=site, sector=sector)
|
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')
|
@action(detail=False, methods=['POST'], url_path='bulk_delete', url_name='bulk_delete')
|
||||||
def bulk_delete(self, request):
|
def bulk_delete(self, request):
|
||||||
"""Bulk delete keywords"""
|
"""Bulk delete keywords - uses hard delete to avoid unique constraint issues"""
|
||||||
ids = request.data.get('ids', [])
|
ids = request.data.get('ids', [])
|
||||||
if not ids:
|
if not ids:
|
||||||
return error_response(
|
return error_response(
|
||||||
@@ -229,7 +235,10 @@ class KeywordViewSet(SiteSectorModelViewSet):
|
|||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
items_to_delete = queryset.filter(id__in=ids)
|
items_to_delete = queryset.filter(id__in=ids)
|
||||||
deleted_count = items_to_delete.count()
|
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)
|
return success_response(data={'deleted_count': deleted_count}, request=request)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,39 @@ function formatContentOutline(content: any): string {
|
|||||||
|
|
||||||
let html = '<div class="content-outline">';
|
let html = '<div class="content-outline">';
|
||||||
|
|
||||||
|
// NEW FORMAT: Handle overview + outline structure
|
||||||
|
if (content.overview && content.outline) {
|
||||||
|
// Display overview
|
||||||
|
html += '<div class="outline-intro">';
|
||||||
|
html += `<div class="outline-paragraph"><strong>Overview:</strong> ${escapeHTML(content.overview)}</div>`;
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Display intro focus if available
|
||||||
|
if (content.outline.intro_focus) {
|
||||||
|
html += '<div class="outline-section">';
|
||||||
|
html += `<h3 class="section-heading">Introduction Focus</h3>`;
|
||||||
|
html += `<div class="section-details">${escapeHTML(content.outline.intro_focus)}</div>`;
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display main sections
|
||||||
|
if (content.outline.main_sections && Array.isArray(content.outline.main_sections)) {
|
||||||
|
content.outline.main_sections.forEach((section: any) => {
|
||||||
|
html += '<div class="outline-section">';
|
||||||
|
if (section.h2_topic) {
|
||||||
|
html += `<h3 class="section-heading">${escapeHTML(section.h2_topic)}</h3>`;
|
||||||
|
}
|
||||||
|
if (section.coverage) {
|
||||||
|
html += `<div class="section-details">${escapeHTML(section.coverage)}</div>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle introduction section - can be object or string
|
// Handle introduction section - can be object or string
|
||||||
if (content.introduction) {
|
if (content.introduction) {
|
||||||
html += '<div class="outline-intro">';
|
html += '<div class="outline-intro">';
|
||||||
@@ -176,7 +209,7 @@ const HTMLContentRenderer: React.FC<HTMLContentRendererProps> = ({
|
|||||||
// If content is already an object (dict), use it directly
|
// If content is already an object (dict), use it directly
|
||||||
if (typeof content === 'object' && content !== null) {
|
if (typeof content === 'object' && content !== null) {
|
||||||
// Check for any known structure format
|
// 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);
|
return formatContentOutline(content);
|
||||||
}
|
}
|
||||||
// If it's an object but not structured, try to format it
|
// If it's an object but not structured, try to format it
|
||||||
@@ -225,7 +258,7 @@ const HTMLContentRenderer: React.FC<HTMLContentRendererProps> = ({
|
|||||||
}
|
}
|
||||||
// Use extracted content as-is (will be processed below)
|
// Use extracted content as-is (will be processed below)
|
||||||
content = extractedContent;
|
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
|
// It's a content outline structure
|
||||||
return formatContentOutline(parsed);
|
return formatContentOutline(parsed);
|
||||||
}
|
}
|
||||||
@@ -238,7 +271,7 @@ const HTMLContentRenderer: React.FC<HTMLContentRendererProps> = ({
|
|||||||
// Try to parse as JSON (content outline from GPT-4o mini) - for non-brace-starting JSON
|
// Try to parse as JSON (content outline from GPT-4o mini) - for non-brace-starting JSON
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(content);
|
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);
|
return formatContentOutline(parsed);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user