Refactor content status terminology and enhance cluster serializers with idea and content counts
This commit is contained in:
@@ -198,7 +198,7 @@ class GenerateContentFunction(BaseAIFunction):
|
|||||||
tags = parsed.get('tags', [])
|
tags = parsed.get('tags', [])
|
||||||
categories = parsed.get('categories', [])
|
categories = parsed.get('categories', [])
|
||||||
# Content status should always be 'draft' for newly generated content
|
# Content status should always be 'draft' for newly generated content
|
||||||
# Status can only be changed manually to 'review' or 'published'
|
# Status can only be changed manually to 'review' or 'publish'
|
||||||
content_status = 'draft'
|
content_status = 'draft'
|
||||||
else:
|
else:
|
||||||
# Plain text response (legacy)
|
# Plain text response (legacy)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Clusters, Keywords
|
from .models import Clusters, Keywords, ContentIdeas
|
||||||
from django.db.models import Count, Sum, Avg
|
from django.db.models import Count, Sum, Avg
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +9,8 @@ class ClusterSerializer(serializers.ModelSerializer):
|
|||||||
volume = serializers.SerializerMethodField()
|
volume = serializers.SerializerMethodField()
|
||||||
difficulty = serializers.SerializerMethodField()
|
difficulty = serializers.SerializerMethodField()
|
||||||
sector_name = serializers.SerializerMethodField()
|
sector_name = serializers.SerializerMethodField()
|
||||||
|
ideas_count = serializers.SerializerMethodField()
|
||||||
|
content_count = serializers.SerializerMethodField()
|
||||||
site_id = serializers.IntegerField(write_only=True, required=False)
|
site_id = serializers.IntegerField(write_only=True, required=False)
|
||||||
sector_id = serializers.IntegerField(write_only=True, required=False)
|
sector_id = serializers.IntegerField(write_only=True, required=False)
|
||||||
|
|
||||||
@@ -22,6 +24,8 @@ class ClusterSerializer(serializers.ModelSerializer):
|
|||||||
'volume',
|
'volume',
|
||||||
'difficulty',
|
'difficulty',
|
||||||
'mapped_pages',
|
'mapped_pages',
|
||||||
|
'ideas_count',
|
||||||
|
'content_count',
|
||||||
'status',
|
'status',
|
||||||
'sector_name',
|
'sector_name',
|
||||||
'site_id',
|
'site_id',
|
||||||
@@ -89,6 +93,22 @@ class ClusterSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return round(result['avg_difficulty'] or 0, 1) # Round to 1 decimal place
|
return round(result['avg_difficulty'] or 0, 1) # Round to 1 decimal place
|
||||||
|
|
||||||
|
def get_ideas_count(self, obj):
|
||||||
|
"""Count content ideas linked to this cluster"""
|
||||||
|
if hasattr(obj, '_ideas_count'):
|
||||||
|
return obj._ideas_count
|
||||||
|
|
||||||
|
return ContentIdeas.objects.filter(keyword_cluster_id=obj.id).count()
|
||||||
|
|
||||||
|
def get_content_count(self, obj):
|
||||||
|
"""Count generated content items linked to this cluster via tasks"""
|
||||||
|
if hasattr(obj, '_content_count'):
|
||||||
|
return obj._content_count
|
||||||
|
|
||||||
|
from igny8_core.modules.writer.models import Content
|
||||||
|
|
||||||
|
return Content.objects.filter(task__cluster_id=obj.id).count()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def prefetch_keyword_stats(cls, clusters):
|
def prefetch_keyword_stats(cls, clusters):
|
||||||
@@ -137,12 +157,34 @@ class ClusterSerializer(serializers.ModelSerializer):
|
|||||||
for stat in keyword_stats
|
for stat in keyword_stats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Prefetch idea counts
|
||||||
|
idea_counts = (
|
||||||
|
ContentIdeas.objects
|
||||||
|
.filter(keyword_cluster_id__in=cluster_ids)
|
||||||
|
.values('keyword_cluster_id')
|
||||||
|
.annotate(count=Count('id'))
|
||||||
|
)
|
||||||
|
idea_stats = {item['keyword_cluster_id']: item['count'] for item in idea_counts}
|
||||||
|
|
||||||
|
# Prefetch content counts (through writer.Tasks -> Content)
|
||||||
|
from igny8_core.modules.writer.models import Content
|
||||||
|
|
||||||
|
content_counts = (
|
||||||
|
Content.objects
|
||||||
|
.filter(task__cluster_id__in=cluster_ids)
|
||||||
|
.values('task__cluster_id')
|
||||||
|
.annotate(count=Count('id'))
|
||||||
|
)
|
||||||
|
content_stats = {item['task__cluster_id']: item['count'] for item in content_counts}
|
||||||
|
|
||||||
# Attach stats to each cluster object
|
# Attach stats to each cluster object
|
||||||
for cluster in clusters:
|
for cluster in clusters:
|
||||||
cluster_stats = stats_dict.get(cluster.id, {'count': 0, 'volume': 0, 'difficulty': 0})
|
cluster_stats = stats_dict.get(cluster.id, {'count': 0, 'volume': 0, 'difficulty': 0})
|
||||||
cluster._keywords_count = cluster_stats['count']
|
cluster._keywords_count = cluster_stats['count']
|
||||||
cluster._volume = cluster_stats['volume']
|
cluster._volume = cluster_stats['volume']
|
||||||
cluster._difficulty = cluster_stats['difficulty']
|
cluster._difficulty = cluster_stats['difficulty']
|
||||||
|
cluster._ideas_count = idea_stats.get(cluster.id, 0)
|
||||||
|
cluster._content_count = content_stats.get(cluster.id, 0)
|
||||||
|
|
||||||
return clusters
|
return clusters
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_content_status_forward(apps, schema_editor):
|
||||||
|
Content = apps.get_model('writer', 'Content')
|
||||||
|
Content.objects.filter(status='published').update(status='publish')
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_content_status_backward(apps, schema_editor):
|
||||||
|
Content = apps.get_model('writer', 'Content')
|
||||||
|
Content.objects.filter(status='publish').update(status='published')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('writer', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tasks',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[('queued', 'Queued'), ('completed', 'Completed')],
|
||||||
|
default='queued',
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='content',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[('draft', 'Draft'), ('review', 'Review'), ('publish', 'Publish')],
|
||||||
|
default='draft',
|
||||||
|
help_text='Content workflow status (draft, review, publish)',
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_content_status_forward,
|
||||||
|
migrate_content_status_backward,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -109,9 +109,9 @@ class Content(SiteSectorBaseModel):
|
|||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('draft', 'Draft'),
|
('draft', 'Draft'),
|
||||||
('review', 'Review'),
|
('review', 'Review'),
|
||||||
('published', 'Published'),
|
('publish', 'Publish'),
|
||||||
]
|
]
|
||||||
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='draft', help_text="Content workflow status (draft, review, published)")
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='draft', help_text="Content workflow status (draft, review, publish)")
|
||||||
generated_at = models.DateTimeField(auto_now_add=True)
|
generated_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
|||||||
@@ -263,8 +263,8 @@ class TasksViewSet(SiteSectorModelViewSet):
|
|||||||
# Tasks module not available - update status only
|
# Tasks module not available - update status only
|
||||||
try:
|
try:
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
tasks = queryset.filter(id__in=ids, status__in=['queued', 'in_progress'])
|
tasks = queryset.filter(id__in=ids, status='queued')
|
||||||
updated_count = tasks.update(status='draft', content='[AI content generation not available]')
|
updated_count = tasks.update(status='completed', content='[AI content generation not available]')
|
||||||
|
|
||||||
logger.info(f"auto_generate_content: Updated {updated_count} tasks (AI generation not available)")
|
logger.info(f"auto_generate_content: Updated {updated_count} tasks (AI generation not available)")
|
||||||
return Response({
|
return Response({
|
||||||
|
|||||||
@@ -93,8 +93,6 @@ export const bulkActionModalConfigs: Record<string, BulkActionModalConfig> = {
|
|||||||
itemNamePlural: 'tasks',
|
itemNamePlural: 'tasks',
|
||||||
statusOptions: [
|
statusOptions: [
|
||||||
{ value: 'queued', label: 'Queued' },
|
{ value: 'queued', label: 'Queued' },
|
||||||
{ value: 'draft', label: 'Draft' },
|
|
||||||
{ value: 'in_progress', label: 'In Progress' },
|
|
||||||
{ value: 'completed', label: 'Completed' },
|
{ value: 'completed', label: 'Completed' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -114,7 +112,7 @@ export const bulkActionModalConfigs: Record<string, BulkActionModalConfig> = {
|
|||||||
statusOptions: [
|
statusOptions: [
|
||||||
{ value: 'draft', label: 'Draft' },
|
{ value: 'draft', label: 'Draft' },
|
||||||
{ value: 'review', label: 'Review' },
|
{ value: 'review', label: 'Review' },
|
||||||
{ value: 'published', label: 'Published' },
|
{ value: 'publish', label: 'Publish' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -131,8 +129,9 @@ export const bulkActionModalConfigs: Record<string, BulkActionModalConfig> = {
|
|||||||
confirmText: 'Update Status',
|
confirmText: 'Update Status',
|
||||||
itemNamePlural: 'published content items',
|
itemNamePlural: 'published content items',
|
||||||
statusOptions: [
|
statusOptions: [
|
||||||
{ value: 'published', label: 'Published' },
|
{ value: 'publish', label: 'Publish' },
|
||||||
{ value: 'archived', label: 'Archived' },
|
{ value: 'review', label: 'Review' },
|
||||||
|
{ value: 'draft', label: 'Draft' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -120,6 +120,13 @@ export const createClustersPageConfig = (
|
|||||||
width: '120px',
|
width: '120px',
|
||||||
render: (value: number) => value.toLocaleString(),
|
render: (value: number) => value.toLocaleString(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'ideas_count',
|
||||||
|
label: 'Ideas',
|
||||||
|
sortable: false,
|
||||||
|
width: '120px',
|
||||||
|
render: (value: number) => value.toLocaleString(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'volume',
|
key: 'volume',
|
||||||
label: 'Volume',
|
label: 'Volume',
|
||||||
@@ -170,8 +177,8 @@ export const createClustersPageConfig = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'mapped_pages',
|
key: 'content_count',
|
||||||
label: 'Mapped Pages',
|
label: 'Content',
|
||||||
sortable: false,
|
sortable: false,
|
||||||
width: '120px',
|
width: '120px',
|
||||||
render: (value: number) => value.toLocaleString(),
|
render: (value: number) => value.toLocaleString(),
|
||||||
|
|||||||
@@ -148,20 +148,18 @@ export const createTasksPageConfig = (
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
sortField: 'status',
|
sortField: 'status',
|
||||||
render: (value: string) => {
|
render: (value: string) => {
|
||||||
const statusColors: Record<string, 'success' | 'warning' | 'error' | 'info'> = {
|
const statusColors: Record<string, 'success' | 'warning'> = {
|
||||||
'queued': 'warning',
|
queued: 'warning',
|
||||||
'in_progress': 'info',
|
completed: 'success',
|
||||||
'draft': 'warning',
|
|
||||||
'review': 'info',
|
|
||||||
'published': 'success',
|
|
||||||
'completed': 'success',
|
|
||||||
};
|
};
|
||||||
|
const label = value ? value.replace('_', ' ') : '';
|
||||||
|
const formatted = label ? label.charAt(0).toUpperCase() + label.slice(1) : '';
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
color={statusColors[value] || 'warning'}
|
color={statusColors[value] || 'warning'}
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{value?.replace('_', ' ') || value}
|
{formatted}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -193,10 +191,6 @@ export const createTasksPageConfig = (
|
|||||||
options: [
|
options: [
|
||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Status' },
|
||||||
{ value: 'queued', label: 'Queued' },
|
{ value: 'queued', label: 'Queued' },
|
||||||
{ value: 'in_progress', label: 'In Progress' },
|
|
||||||
{ value: 'draft', label: 'Draft' },
|
|
||||||
{ value: 'review', label: 'Review' },
|
|
||||||
{ value: 'published', label: 'Published' },
|
|
||||||
{ value: 'completed', label: 'Completed' },
|
{ value: 'completed', label: 'Completed' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -318,10 +312,6 @@ export const createTasksPageConfig = (
|
|||||||
handlers.setFormData({ ...handlers.formData, status: value }),
|
handlers.setFormData({ ...handlers.formData, status: value }),
|
||||||
options: [
|
options: [
|
||||||
{ value: 'queued', label: 'Queued' },
|
{ value: 'queued', label: 'Queued' },
|
||||||
{ value: 'in_progress', label: 'In Progress' },
|
|
||||||
{ value: 'draft', label: 'Draft' },
|
|
||||||
{ value: 'review', label: 'Review' },
|
|
||||||
{ value: 'published', label: 'Published' },
|
|
||||||
{ value: 'completed', label: 'Completed' },
|
{ value: 'completed', label: 'Completed' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -340,16 +330,10 @@ export const createTasksPageConfig = (
|
|||||||
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length,
|
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'In Progress',
|
label: 'Completed',
|
||||||
value: 0,
|
|
||||||
accentColor: 'blue' as const,
|
|
||||||
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'in_progress').length,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Published',
|
|
||||||
value: 0,
|
value: 0,
|
||||||
accentColor: 'green' as const,
|
accentColor: 'green' as const,
|
||||||
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'published').length,
|
calculate: (data) => data.tasks.filter((t: Task) => t.status === 'completed').length,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import HTMLContentRenderer from '../../components/common/HTMLContentRenderer';
|
|||||||
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = {
|
const statusColors: Record<string, 'warning' | 'info' | 'success' | 'primary'> = {
|
||||||
draft: 'warning',
|
draft: 'warning',
|
||||||
review: 'info',
|
review: 'info',
|
||||||
published: 'success',
|
publish: 'success',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Content() {
|
export default function Content() {
|
||||||
@@ -176,7 +176,7 @@ export default function Content() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
>
|
>
|
||||||
{item.status?.replace('_', ' ') || 'draft'}
|
{(item.status || 'draft').replace('_', ' ').replace(/^\w/, (c) => c.toUpperCase())}
|
||||||
</Badge>
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4 align-top text-gray-600 dark:text-gray-400">
|
<td className="px-5 py-4 align-top text-gray-600 dark:text-gray-400">
|
||||||
|
|||||||
@@ -477,6 +477,8 @@ export interface Cluster {
|
|||||||
volume: number;
|
volume: number;
|
||||||
difficulty: number; // Average difficulty of keywords in cluster
|
difficulty: number; // Average difficulty of keywords in cluster
|
||||||
mapped_pages: number;
|
mapped_pages: number;
|
||||||
|
ideas_count: number;
|
||||||
|
content_count: number;
|
||||||
status: string;
|
status: string;
|
||||||
sector_name?: string | null; // Optional: populated by serializer
|
sector_name?: string | null; // Optional: populated by serializer
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user