Updated iamge prompt flow adn frotnend backend
This commit is contained in:
@@ -151,9 +151,9 @@ class GenerateImagePromptsFunction(BaseAIFunction):
|
||||
prompts_created = 0
|
||||
|
||||
with transaction.atomic():
|
||||
# Save featured image prompt
|
||||
# Save featured image prompt - use content instead of task
|
||||
Images.objects.update_or_create(
|
||||
task=content.task,
|
||||
content=content,
|
||||
image_type='featured',
|
||||
defaults={
|
||||
'prompt': parsed['featured_prompt'],
|
||||
@@ -171,7 +171,7 @@ class GenerateImagePromptsFunction(BaseAIFunction):
|
||||
heading = h2_headings[idx] if idx < len(h2_headings) else f"Section {idx + 1}"
|
||||
|
||||
Images.objects.update_or_create(
|
||||
task=content.task,
|
||||
content=content,
|
||||
image_type='in_article',
|
||||
position=idx + 1,
|
||||
defaults={
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Generated manually for adding content ForeignKey to Images model
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def migrate_task_to_content(apps, schema_editor):
|
||||
"""Migrate existing Images to use content instead of task"""
|
||||
Images = apps.get_model('writer', 'Images')
|
||||
Content = apps.get_model('writer', 'Content')
|
||||
|
||||
# Update images that have a task with a content_record
|
||||
for image in Images.objects.filter(task__isnull=False, content__isnull=True):
|
||||
try:
|
||||
# Try to get content via task.content_record
|
||||
task = image.task
|
||||
if task:
|
||||
try:
|
||||
content = Content.objects.get(task=task)
|
||||
image.content = content
|
||||
image.save(update_fields=['content'])
|
||||
except Content.DoesNotExist:
|
||||
# If content doesn't exist, leave content as null
|
||||
pass
|
||||
except Exception:
|
||||
# If any error occurs, leave content as null
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('writer', '0006_update_status_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Make task field nullable first
|
||||
migrations.AlterField(
|
||||
model_name='images',
|
||||
name='task',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text='The task this image belongs to (legacy, use content instead)',
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='images',
|
||||
to='writer.tasks'
|
||||
),
|
||||
),
|
||||
# Add content field
|
||||
migrations.AddField(
|
||||
model_name='images',
|
||||
name='content',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text='The content this image belongs to (preferred)',
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='images',
|
||||
to='writer.content'
|
||||
),
|
||||
),
|
||||
# Update ordering
|
||||
migrations.AlterModelOptions(
|
||||
name='images',
|
||||
options={'ordering': ['content', 'position', '-created_at'], 'verbose_name': 'Image', 'verbose_name_plural': 'Images'},
|
||||
),
|
||||
# Add new indexes
|
||||
migrations.AddIndex(
|
||||
model_name='images',
|
||||
index=models.Index(fields=['content', 'image_type'], name='igny8_image_content_image_type_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='images',
|
||||
index=models.Index(fields=['content', 'position'], name='igny8_image_content_position_idx'),
|
||||
),
|
||||
# Data migration: populate content field from task.content_record
|
||||
migrations.RunPython(
|
||||
code=migrate_task_to_content,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -138,7 +138,7 @@ class Content(SiteSectorBaseModel):
|
||||
|
||||
|
||||
class Images(SiteSectorBaseModel):
|
||||
"""Images model for task-related images (featured, desktop, mobile)"""
|
||||
"""Images model for content-related images (featured, desktop, mobile, in-article)"""
|
||||
|
||||
IMAGE_TYPE_CHOICES = [
|
||||
('featured', 'Featured Image'),
|
||||
@@ -147,11 +147,21 @@ class Images(SiteSectorBaseModel):
|
||||
('in_article', 'In-Article Image'),
|
||||
]
|
||||
|
||||
content = models.ForeignKey(
|
||||
Content,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='images',
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="The content this image belongs to (preferred)"
|
||||
)
|
||||
task = models.ForeignKey(
|
||||
Tasks,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='images',
|
||||
help_text="The task this image belongs to"
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="The task this image belongs to (legacy, use content instead)"
|
||||
)
|
||||
image_type = models.CharField(max_length=50, choices=IMAGE_TYPE_CHOICES, default='featured')
|
||||
image_url = models.URLField(blank=True, null=True, help_text="URL of the generated/stored image")
|
||||
@@ -164,23 +174,33 @@ class Images(SiteSectorBaseModel):
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_images'
|
||||
ordering = ['task', 'position', '-created_at']
|
||||
ordering = ['content', 'position', '-created_at']
|
||||
verbose_name = 'Image'
|
||||
verbose_name_plural = 'Images'
|
||||
indexes = [
|
||||
models.Index(fields=['content', 'image_type']),
|
||||
models.Index(fields=['task', 'image_type']),
|
||||
models.Index(fields=['status']),
|
||||
models.Index(fields=['content', 'position']),
|
||||
models.Index(fields=['task', 'position']),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Automatically set account, site, and sector from task"""
|
||||
if self.task:
|
||||
"""Automatically set account, site, and sector from content or task"""
|
||||
# Prefer content over task
|
||||
if self.content:
|
||||
self.account = self.content.account
|
||||
self.site = self.content.site
|
||||
self.sector = self.content.sector
|
||||
elif self.task:
|
||||
self.account = self.task.account
|
||||
self.site = self.task.site
|
||||
self.sector = self.task.sector
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.task.title} - {self.image_type}"
|
||||
content_title = self.content.title if self.content else None
|
||||
task_title = self.task.title if self.task else None
|
||||
title = content_title or task_title or 'Unknown'
|
||||
return f"{title} - {self.image_type}"
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ class TasksSerializer(serializers.ModelSerializer):
|
||||
class ImagesSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Images model"""
|
||||
task_title = serializers.SerializerMethodField()
|
||||
content_title = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Images
|
||||
@@ -118,6 +119,8 @@ class ImagesSerializer(serializers.ModelSerializer):
|
||||
'id',
|
||||
'task_id',
|
||||
'task_title',
|
||||
'content_id',
|
||||
'content_title',
|
||||
'image_type',
|
||||
'image_url',
|
||||
'image_path',
|
||||
@@ -139,6 +142,38 @@ class ImagesSerializer(serializers.ModelSerializer):
|
||||
except Tasks.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_content_title(self, obj):
|
||||
"""Get content title"""
|
||||
if obj.content:
|
||||
return obj.content.title or obj.content.meta_title
|
||||
return None
|
||||
|
||||
|
||||
class ContentImageSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for individual image in grouped content images"""
|
||||
class Meta:
|
||||
model = Images
|
||||
fields = [
|
||||
'id',
|
||||
'image_type',
|
||||
'image_url',
|
||||
'image_path',
|
||||
'prompt',
|
||||
'status',
|
||||
'position',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]
|
||||
|
||||
|
||||
class ContentImagesGroupSerializer(serializers.Serializer):
|
||||
"""Serializer for grouped content images - one row per content"""
|
||||
content_id = serializers.IntegerField()
|
||||
content_title = serializers.CharField()
|
||||
featured_image = ContentImageSerializer(allow_null=True)
|
||||
in_article_images = ContentImageSerializer(many=True)
|
||||
overall_status = serializers.CharField() # 'pending', 'partial', 'complete', 'failed'
|
||||
|
||||
|
||||
class ContentSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -2,7 +2,8 @@ from rest_framework import viewsets, filters, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db import transaction
|
||||
from django.db import transaction, models
|
||||
from django.db.models import Q
|
||||
from igny8_core.api.base import SiteSectorModelViewSet
|
||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from .models import Tasks, Images, Content
|
||||
@@ -348,15 +349,15 @@ class TasksViewSet(SiteSectorModelViewSet):
|
||||
|
||||
class ImagesViewSet(SiteSectorModelViewSet):
|
||||
"""
|
||||
ViewSet for managing task images
|
||||
ViewSet for managing content images
|
||||
"""
|
||||
queryset = Images.objects.all()
|
||||
serializer_class = ImagesSerializer
|
||||
|
||||
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
|
||||
ordering_fields = ['created_at', 'position']
|
||||
ordering = ['task', 'position', '-created_at']
|
||||
filterset_fields = ['task_id', 'image_type', 'status']
|
||||
ordering = ['content', 'position', '-created_at']
|
||||
filterset_fields = ['task_id', 'content_id', 'image_type', 'status']
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""Override to automatically set account"""
|
||||
@@ -432,6 +433,86 @@ class ImagesViewSet(SiteSectorModelViewSet):
|
||||
'error': f'Failed to start image generation: {str(e)}',
|
||||
'type': 'TaskError'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='content_images', url_name='content_images')
|
||||
def content_images(self, request):
|
||||
"""Get images grouped by content - one row per content with featured and in-article images"""
|
||||
from .serializers import ContentImagesGroupSerializer, ContentImageSerializer
|
||||
|
||||
account = getattr(request, 'account', None)
|
||||
|
||||
# Get all content that has images (either directly or via task)
|
||||
# First, get content with direct image links
|
||||
queryset = Content.objects.filter(images__isnull=False)
|
||||
if account:
|
||||
queryset = queryset.filter(account=account)
|
||||
|
||||
# Also get content from images linked via task
|
||||
task_linked_images = Images.objects.filter(task__isnull=False, content__isnull=True)
|
||||
if account:
|
||||
task_linked_images = task_linked_images.filter(account=account)
|
||||
|
||||
# Get content IDs from task-linked images
|
||||
task_content_ids = set()
|
||||
for image in task_linked_images:
|
||||
if image.task and hasattr(image.task, 'content_record'):
|
||||
try:
|
||||
content = image.task.content_record
|
||||
if content:
|
||||
task_content_ids.add(content.id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Combine both sets of content IDs
|
||||
content_ids = set(queryset.values_list('id', flat=True).distinct())
|
||||
content_ids.update(task_content_ids)
|
||||
|
||||
# Build grouped response
|
||||
grouped_data = []
|
||||
for content_id in content_ids:
|
||||
try:
|
||||
content = Content.objects.get(id=content_id)
|
||||
# Get images linked directly to content OR via task
|
||||
content_images = Images.objects.filter(
|
||||
Q(content=content) | Q(task=content.task)
|
||||
).order_by('position')
|
||||
|
||||
# Get featured image
|
||||
featured_image = content_images.filter(image_type='featured').first()
|
||||
|
||||
# Get in-article images (sorted by position)
|
||||
in_article_images = list(content_images.filter(image_type='in_article').order_by('position'))
|
||||
|
||||
# Determine overall status
|
||||
all_images = list(content_images)
|
||||
if not all_images:
|
||||
overall_status = 'pending'
|
||||
elif all(img.status == 'generated' for img in all_images):
|
||||
overall_status = 'complete'
|
||||
elif any(img.status == 'failed' for img in all_images):
|
||||
overall_status = 'failed'
|
||||
elif any(img.status == 'generated' for img in all_images):
|
||||
overall_status = 'partial'
|
||||
else:
|
||||
overall_status = 'pending'
|
||||
|
||||
grouped_data.append({
|
||||
'content_id': content.id,
|
||||
'content_title': content.title or content.meta_title or f"Content #{content.id}",
|
||||
'featured_image': ContentImageSerializer(featured_image).data if featured_image else None,
|
||||
'in_article_images': [ContentImageSerializer(img).data for img in in_article_images],
|
||||
'overall_status': overall_status,
|
||||
})
|
||||
except Content.DoesNotExist:
|
||||
continue
|
||||
|
||||
# Sort by content title
|
||||
grouped_data.sort(key=lambda x: x['content_title'])
|
||||
|
||||
return Response({
|
||||
'count': len(grouped_data),
|
||||
'results': grouped_data
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ContentViewSet(SiteSectorModelViewSet):
|
||||
|
||||
Reference in New Issue
Block a user