Refactor content status terminology and enhance cluster serializers with idea and content counts

This commit is contained in:
Desktop
2025-11-11 18:51:32 +05:00
parent b321c99089
commit a7880c3818
10 changed files with 118 additions and 39 deletions

View File

@@ -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)

View File

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

View File

@@ -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,
),
]

View File

@@ -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)

View File

@@ -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({

View File

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

View File

@@ -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(),

View File

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

View File

@@ -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">

View File

@@ -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;