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:
alorig
2025-11-26 01:24:58 +05:00
parent ba842d8332
commit 53ea0c34ce
13 changed files with 1249 additions and 417 deletions

View File

@@ -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,
}