feat: Implement WordPress publishing and unpublishing actions
- Added conditional visibility for table actions based on content state (published/draft). - Introduced `publishContent` and `unpublishContent` API functions for handling WordPress integration. - Updated `Content` component to manage publish/unpublish actions with appropriate error handling and success notifications. - Refactored `PostEditor` to remove deprecated SEO fields and consolidate taxonomy management. - Enhanced `TablePageTemplate` to filter row actions based on visibility conditions. - Updated backend API to support publishing and unpublishing content with proper status updates and external references.
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
Generate Content AI Function
|
||||
Extracted from modules/writer/tasks.py
|
||||
STAGE 3: Updated to use final Stage 1 Content schema
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, List, Any
|
||||
from django.db import transaction
|
||||
from igny8_core.ai.base import BaseAIFunction
|
||||
from igny8_core.modules.writer.models import Tasks, Content as TaskContent
|
||||
from igny8_core.modules.writer.models import Tasks, Content
|
||||
from igny8_core.ai.ai_core import AICore
|
||||
from igny8_core.ai.validators import validate_tasks_exist
|
||||
from igny8_core.ai.prompts import PromptRegistry
|
||||
@@ -62,11 +62,10 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
if account:
|
||||
queryset = queryset.filter(account=account)
|
||||
|
||||
# Preload all relationships to avoid N+1 queries
|
||||
# Stage 3: Include taxonomy and keyword_objects for metadata
|
||||
# STAGE 3: Preload relationships - taxonomy_term instead of taxonomy
|
||||
tasks = list(queryset.select_related(
|
||||
'account', 'site', 'sector', 'cluster', 'idea', 'taxonomy'
|
||||
).prefetch_related('keyword_objects'))
|
||||
'account', 'site', 'sector', 'cluster', 'taxonomy_term'
|
||||
).prefetch_related('keywords'))
|
||||
|
||||
if not tasks:
|
||||
raise ValueError("No tasks found")
|
||||
@@ -74,9 +73,8 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
return tasks
|
||||
|
||||
def build_prompt(self, data: Any, account=None) -> str:
|
||||
"""Build content generation prompt for a single task using registry"""
|
||||
"""STAGE 3: Build content generation prompt using final Task schema"""
|
||||
if isinstance(data, list):
|
||||
# For now, handle single task (will be called per task)
|
||||
if not data:
|
||||
raise ValueError("No tasks provided")
|
||||
task = data[0]
|
||||
@@ -90,33 +88,9 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
if task.description:
|
||||
idea_data += f"Description: {task.description}\n"
|
||||
|
||||
# Handle idea description (might be JSON or plain text)
|
||||
if task.idea and task.idea.description:
|
||||
description = task.idea.description
|
||||
try:
|
||||
import json
|
||||
parsed_desc = json.loads(description)
|
||||
if isinstance(parsed_desc, dict):
|
||||
formatted_desc = "Content Outline:\n\n"
|
||||
if 'H2' in parsed_desc:
|
||||
for h2_section in parsed_desc['H2']:
|
||||
formatted_desc += f"## {h2_section.get('heading', '')}\n"
|
||||
if 'subsections' in h2_section:
|
||||
for h3_section in h2_section['subsections']:
|
||||
formatted_desc += f"### {h3_section.get('subheading', '')}\n"
|
||||
formatted_desc += f"Content Type: {h3_section.get('content_type', '')}\n"
|
||||
formatted_desc += f"Details: {h3_section.get('details', '')}\n\n"
|
||||
description = formatted_desc
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass # Use as plain text
|
||||
|
||||
idea_data += f"Outline: {description}\n"
|
||||
|
||||
if task.idea:
|
||||
idea_data += f"Structure: {task.idea.content_structure or task.content_structure or 'blog_post'}\n"
|
||||
idea_data += f"Type: {task.idea.content_type or task.content_type or 'blog_post'}\n"
|
||||
if task.idea.estimated_word_count:
|
||||
idea_data += f"Estimated Word Count: {task.idea.estimated_word_count}\n"
|
||||
# Add content type and structure from task
|
||||
idea_data += f"Content Type: {task.content_type or 'post'}\n"
|
||||
idea_data += f"Content Structure: {task.content_structure or 'article'}\n"
|
||||
|
||||
# Build cluster data string
|
||||
cluster_data = ''
|
||||
@@ -124,56 +98,21 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
cluster_data = f"Cluster Name: {task.cluster.name or ''}\n"
|
||||
if task.cluster.description:
|
||||
cluster_data += f"Description: {task.cluster.description}\n"
|
||||
cluster_data += f"Status: {task.cluster.status or 'active'}\n"
|
||||
|
||||
# Stage 3: Build cluster role context
|
||||
cluster_role_data = ''
|
||||
if hasattr(task, 'cluster_role') and task.cluster_role:
|
||||
role_descriptions = {
|
||||
'hub': 'Hub Page - Main authoritative resource for this topic cluster. Should be comprehensive, overview-focused, and link to supporting content.',
|
||||
'supporting': 'Supporting Page - Detailed content that supports the hub page. Focus on specific aspects, use cases, or subtopics.',
|
||||
'attribute': 'Attribute Page - Content focused on specific attributes, features, or specifications. Include detailed comparisons and specifications.',
|
||||
}
|
||||
role_desc = role_descriptions.get(task.cluster_role, f'Role: {task.cluster_role}')
|
||||
cluster_role_data = f"Cluster Role: {role_desc}\n"
|
||||
|
||||
# Stage 3: Build taxonomy context
|
||||
# STAGE 3: Build taxonomy context (from taxonomy_term FK)
|
||||
taxonomy_data = ''
|
||||
if hasattr(task, 'taxonomy') and task.taxonomy:
|
||||
taxonomy_data = f"Taxonomy: {task.taxonomy.name or ''}\n"
|
||||
if task.taxonomy.taxonomy_type:
|
||||
taxonomy_data += f"Taxonomy Type: {task.taxonomy.get_taxonomy_type_display() or task.taxonomy.taxonomy_type}\n"
|
||||
if task.taxonomy.description:
|
||||
taxonomy_data += f"Description: {task.taxonomy.description}\n"
|
||||
if task.taxonomy_term:
|
||||
taxonomy_data = f"Taxonomy: {task.taxonomy_term.name or ''}\n"
|
||||
if task.taxonomy_term.taxonomy_type:
|
||||
taxonomy_data += f"Type: {task.taxonomy_term.get_taxonomy_type_display()}\n"
|
||||
|
||||
# Stage 3: Build attributes context from keywords
|
||||
attributes_data = ''
|
||||
if hasattr(task, 'keyword_objects') and task.keyword_objects.exists():
|
||||
attribute_list = []
|
||||
for keyword in task.keyword_objects.all():
|
||||
if hasattr(keyword, 'attribute_values') and keyword.attribute_values:
|
||||
if isinstance(keyword.attribute_values, dict):
|
||||
for attr_name, attr_value in keyword.attribute_values.items():
|
||||
attribute_list.append(f"{attr_name}: {attr_value}")
|
||||
elif isinstance(keyword.attribute_values, list):
|
||||
for attr_item in keyword.attribute_values:
|
||||
if isinstance(attr_item, dict):
|
||||
for attr_name, attr_value in attr_item.items():
|
||||
attribute_list.append(f"{attr_name}: {attr_value}")
|
||||
else:
|
||||
attribute_list.append(str(attr_item))
|
||||
|
||||
if attribute_list:
|
||||
attributes_data = "Product/Service Attributes:\n"
|
||||
attributes_data += "\n".join(f"- {attr}" for attr in attribute_list) + "\n"
|
||||
|
||||
# Build keywords string
|
||||
keywords_data = task.keywords or ''
|
||||
if not keywords_data and task.idea:
|
||||
keywords_data = task.idea.target_keywords or ''
|
||||
# STAGE 3: Build keywords context (from keywords M2M)
|
||||
keywords_data = ''
|
||||
if task.keywords.exists():
|
||||
keyword_list = [kw.keyword for kw in task.keywords.all()]
|
||||
keywords_data = "Keywords: " + ", ".join(keyword_list) + "\n"
|
||||
|
||||
# Get prompt from registry with context
|
||||
# Stage 3: Include cluster_role, taxonomy, and attributes in context
|
||||
prompt = PromptRegistry.get_prompt(
|
||||
function_name='generate_content',
|
||||
account=account,
|
||||
@@ -181,9 +120,7 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
context={
|
||||
'IDEA': idea_data,
|
||||
'CLUSTER': cluster_data,
|
||||
'CLUSTER_ROLE': cluster_role_data,
|
||||
'TAXONOMY': taxonomy_data,
|
||||
'ATTRIBUTES': attributes_data,
|
||||
'KEYWORDS': keywords_data,
|
||||
}
|
||||
)
|
||||
@@ -222,7 +159,10 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
progress_tracker=None,
|
||||
step_tracker=None
|
||||
) -> Dict:
|
||||
"""Save content to task - handles both JSON and plain text responses"""
|
||||
"""
|
||||
STAGE 3: Save content using final Stage 1 Content model schema.
|
||||
Creates independent Content record (no OneToOne to Task).
|
||||
"""
|
||||
if isinstance(original_data, list):
|
||||
task = original_data[0] if original_data else None
|
||||
else:
|
||||
@@ -236,113 +176,50 @@ class GenerateContentFunction(BaseAIFunction):
|
||||
# JSON response with structured fields
|
||||
content_html = parsed.get('content', '')
|
||||
title = parsed.get('title') or task.title
|
||||
meta_title = parsed.get('meta_title') or title or task.title
|
||||
meta_description = parsed.get('meta_description', '')
|
||||
word_count = parsed.get('word_count', 0)
|
||||
primary_keyword = parsed.get('primary_keyword', '')
|
||||
secondary_keywords = parsed.get('secondary_keywords', [])
|
||||
tags = parsed.get('tags', [])
|
||||
categories = parsed.get('categories', [])
|
||||
# Content status should always be 'draft' for newly generated content
|
||||
# Status can only be changed manually to 'review' or 'publish'
|
||||
content_status = 'draft'
|
||||
else:
|
||||
# Plain text response (legacy)
|
||||
# Plain text response
|
||||
content_html = str(parsed)
|
||||
title = task.title
|
||||
meta_title = task.meta_title or task.title
|
||||
meta_description = task.meta_description or (task.description or '')[:160] if task.description else ''
|
||||
word_count = 0
|
||||
primary_keyword = ''
|
||||
secondary_keywords = []
|
||||
tags = []
|
||||
categories = []
|
||||
content_status = 'draft'
|
||||
|
||||
# Calculate word count if not provided
|
||||
if not word_count and content_html:
|
||||
# Calculate word count
|
||||
word_count = 0
|
||||
if content_html:
|
||||
text_for_counting = re.sub(r'<[^>]+>', '', content_html)
|
||||
word_count = len(text_for_counting.split())
|
||||
|
||||
# Ensure related content record exists
|
||||
content_record, _created = TaskContent.objects.get_or_create(
|
||||
task=task,
|
||||
defaults={
|
||||
'account': task.account,
|
||||
'site': task.site,
|
||||
'sector': task.sector,
|
||||
'html_content': content_html or '',
|
||||
'word_count': word_count or 0,
|
||||
'status': 'draft',
|
||||
},
|
||||
|
||||
# STAGE 3: Create independent Content record using final schema
|
||||
content_record = Content.objects.create(
|
||||
# Core fields
|
||||
title=title,
|
||||
content_html=content_html or '',
|
||||
cluster=task.cluster,
|
||||
content_type=task.content_type,
|
||||
content_structure=task.content_structure,
|
||||
# Source and status
|
||||
source='igny8',
|
||||
status='draft',
|
||||
# Site/Sector/Account
|
||||
account=task.account,
|
||||
site=task.site,
|
||||
sector=task.sector,
|
||||
)
|
||||
|
||||
# Update content fields
|
||||
if content_html:
|
||||
content_record.html_content = content_html
|
||||
content_record.word_count = word_count or content_record.word_count or 0
|
||||
content_record.title = title
|
||||
content_record.meta_title = meta_title
|
||||
content_record.meta_description = meta_description
|
||||
content_record.primary_keyword = primary_keyword or ''
|
||||
if isinstance(secondary_keywords, list):
|
||||
content_record.secondary_keywords = secondary_keywords
|
||||
elif secondary_keywords:
|
||||
content_record.secondary_keywords = [secondary_keywords]
|
||||
else:
|
||||
content_record.secondary_keywords = []
|
||||
if isinstance(tags, list):
|
||||
content_record.tags = tags
|
||||
elif tags:
|
||||
content_record.tags = [tags]
|
||||
else:
|
||||
content_record.tags = []
|
||||
if isinstance(categories, list):
|
||||
content_record.categories = categories
|
||||
elif categories:
|
||||
content_record.categories = [categories]
|
||||
else:
|
||||
content_record.categories = []
|
||||
|
||||
# Always set status to 'draft' for newly generated content
|
||||
# Status can only be: draft, review, published (changed manually)
|
||||
content_record.status = 'draft'
|
||||
|
||||
# Merge any extra fields into metadata (non-standard keys)
|
||||
if isinstance(parsed, dict):
|
||||
excluded_keys = {
|
||||
'content',
|
||||
'title',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
'primary_keyword',
|
||||
'secondary_keywords',
|
||||
'tags',
|
||||
'categories',
|
||||
'word_count',
|
||||
'status',
|
||||
}
|
||||
extra_meta = {k: v for k, v in parsed.items() if k not in excluded_keys}
|
||||
existing_meta = content_record.metadata or {}
|
||||
existing_meta.update(extra_meta)
|
||||
content_record.metadata = existing_meta
|
||||
|
||||
# Align foreign keys to ensure consistency
|
||||
content_record.account = task.account
|
||||
content_record.site = task.site
|
||||
content_record.sector = task.sector
|
||||
content_record.task = task
|
||||
|
||||
content_record.save()
|
||||
|
||||
# Update task status - keep task data intact but mark as completed
|
||||
|
||||
# Link taxonomy terms from task if available
|
||||
if task.taxonomy_term:
|
||||
content_record.taxonomy_terms.add(task.taxonomy_term)
|
||||
|
||||
# Link all keywords from task as taxonomy terms (if they have taxonomy mappings)
|
||||
# This is optional - keywords are M2M on Task, not directly on Content
|
||||
|
||||
# STAGE 3: Update task status to completed
|
||||
task.status = 'completed'
|
||||
task.save(update_fields=['status', 'updated_at'])
|
||||
|
||||
|
||||
return {
|
||||
'count': 1,
|
||||
'tasks_updated': 1,
|
||||
'word_count': content_record.word_count,
|
||||
'content_id': content_record.id,
|
||||
'task_id': task.id,
|
||||
'word_count': word_count,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user