Updated iamge prompt flow adn frotnend backend

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-11 18:10:18 +00:00
parent fa696064e2
commit a1b21f39f6
10 changed files with 611 additions and 196 deletions

View File

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

View File

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

View File

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

View File

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

View File

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