Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
from django.contrib import admin
from igny8_core.admin.base import SiteSectorAdminMixin
from .models import Tasks, Images, Content
@admin.register(Tasks)
class TasksAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_display = ['title', 'site', 'sector', 'status', 'cluster', 'content_type', 'word_count', 'created_at']
list_filter = ['status', 'content_type', 'content_structure', 'site', 'sector']
search_fields = ['title', 'keywords']
ordering = ['-created_at']
def get_site_display(self, obj):
"""Safely get site name"""
try:
return obj.site.name if obj.site else '-'
except:
return '-'
get_site_display.short_description = 'Site'
def get_sector_display(self, obj):
"""Safely get sector name"""
try:
return obj.sector.name if obj.sector else '-'
except:
return '-'
def get_cluster_display(self, obj):
"""Safely get cluster name"""
try:
return obj.cluster.name if obj.cluster else '-'
except:
return '-'
get_cluster_display.short_description = 'Cluster'
@admin.register(Images)
class ImagesAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_display = ['task', 'site', 'sector', 'image_type', 'status', 'position', 'created_at']
list_filter = ['image_type', 'status', 'site', 'sector']
search_fields = ['task__title']
ordering = ['task', 'position', '-created_at']
def get_site_display(self, obj):
"""Safely get site name"""
try:
return obj.site.name if obj.site else '-'
except:
return '-'
get_site_display.short_description = 'Site'
def get_sector_display(self, obj):
"""Safely get sector name"""
try:
return obj.sector.name if obj.sector else '-'
except:
return '-'
@admin.register(Content)
class ContentAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_display = ['task', 'site', 'sector', 'word_count', 'generated_at', 'updated_at']
list_filter = ['generated_at', 'site', 'sector']
search_fields = ['task__title']
ordering = ['-generated_at']
def get_site_display(self, obj):
"""Safely get site name"""
try:
return obj.site.name if obj.site else '-'
except:
return '-'
get_site_display.short_description = 'Site'
def get_sector_display(self, obj):
"""Safely get sector name"""
try:
return obj.sector.name if obj.sector else '-'
except:
return '-'

View File

@@ -0,0 +1,8 @@
from django.apps import AppConfig
class WriterConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'igny8_core.modules.writer'
verbose_name = 'Writer Module'

View File

@@ -0,0 +1,96 @@
# Generated by Django 5.2.7 on 2025-11-03 13:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('igny8_core_auth', '0003_alter_user_role'),
('planner', '0003_alter_clusters_sector_alter_clusters_site_and_more'),
]
operations = [
migrations.CreateModel(
name='Tasks',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(db_index=True, max_length=255)),
('description', models.TextField(blank=True, null=True)),
('keywords', models.CharField(blank=True, max_length=500)),
('content_structure', models.CharField(choices=[('cluster_hub', 'Cluster Hub'), ('landing_page', 'Landing Page'), ('pillar_page', 'Pillar Page'), ('supporting_page', 'Supporting Page')], default='blog_post', max_length=50)),
('content_type', models.CharField(choices=[('blog_post', 'Blog Post'), ('article', 'Article'), ('guide', 'Guide'), ('tutorial', 'Tutorial')], default='blog_post', max_length=50)),
('status', models.CharField(choices=[('queued', 'Queued'), ('in_progress', 'In Progress'), ('draft', 'Draft'), ('review', 'Review'), ('published', 'Published'), ('completed', 'Completed')], default='queued', max_length=50)),
('content', models.TextField(blank=True, null=True)),
('word_count', models.IntegerField(default=0)),
('meta_title', models.CharField(blank=True, max_length=255, null=True)),
('meta_description', models.TextField(blank=True, null=True)),
('assigned_post_id', models.IntegerField(blank=True, null=True)),
('post_url', models.URLField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('cluster', models.ForeignKey(blank=True, limit_choices_to={'sector': models.F('sector')}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='planner.clusters')),
('idea', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tasks', to='planner.contentideas')),
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_tasks',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TaskImages',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image_type', models.CharField(choices=[('featured', 'Featured Image'), ('desktop', 'Desktop Image'), ('mobile', 'Mobile Image'), ('in_article', 'In-Article Image')], default='featured', max_length=50)),
('image_url', models.URLField(blank=True, null=True)),
('image_path', models.CharField(blank=True, max_length=500, null=True)),
('prompt', models.TextField(blank=True, null=True)),
('status', models.CharField(default='pending', max_length=50)),
('position', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('sector', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='writer.tasks')),
],
options={
'db_table': 'igny8_task_images',
'ordering': ['task', 'position', '-created_at'],
},
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['title'], name='igny8_tasks_title_adc50b_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['status'], name='igny8_tasks_status_706869_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['cluster'], name='igny8_tasks_cluster_ac6cd4_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['content_type'], name='igny8_tasks_content_343539_idx'),
),
migrations.AddIndex(
model_name='tasks',
index=models.Index(fields=['site', 'sector'], name='igny8_tasks_site_id_9880f4_idx'),
),
migrations.AddIndex(
model_name='taskimages',
index=models.Index(fields=['task', 'image_type'], name='igny8_task__task_id_143db4_idx'),
),
migrations.AddIndex(
model_name='taskimages',
index=models.Index(fields=['status'], name='igny8_task__status_4f869f_idx'),
),
]

View File

@@ -0,0 +1,63 @@
"""Rename TaskImages, add Content model, and keyword links"""
from django.core.validators import MinValueValidator
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('planner', '0004_add_keyword_objects_to_contentideas'),
('writer', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='TaskImages',
new_name='Images',
),
migrations.AlterModelTable(
name='images',
table='igny8_images',
),
migrations.AlterModelOptions(
name='images',
options={'ordering': ['task', 'position', '-created_at']},
),
migrations.AddField(
model_name='tasks',
name='keyword_objects',
field=models.ManyToManyField(blank=True, help_text='Individual keywords linked to this task', related_name='tasks', to='planner.keywords'),
),
migrations.CreateModel(
name='Content',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('html_content', models.TextField(help_text='Final AI-generated HTML content')),
('word_count', models.IntegerField(default=0, help_text='Word count of generated content', validators=[MinValueValidator(0)])),
('metadata', models.JSONField(default=dict, help_text='Additional metadata (SEO, structure, etc.)')),
('generated_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('sector', models.ForeignKey(on_delete=models.CASCADE, related_name='content_set', to='igny8_core_auth.sector')),
('site', models.ForeignKey(on_delete=models.CASCADE, related_name='content_set', to='igny8_core_auth.site')),
('task', models.OneToOneField(help_text='The task this content belongs to', on_delete=models.CASCADE, related_name='content_record', to='writer.tasks')),
('tenant', models.ForeignKey(on_delete=models.CASCADE, related_name='content_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_content',
'ordering': ['-generated_at'],
},
),
migrations.AddIndex(
model_name='images',
index=models.Index(fields=['task', 'position'], name='igny8_images_task_id_pos_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['task'], name='igny8_content_task_id_idx'),
),
migrations.AddIndex(
model_name='content',
index=models.Index(fields=['generated_at'], name='igny8_content_generated_idx'),
),
]

View File

@@ -0,0 +1,110 @@
# Generated by Django 5.2.8 on 2025-11-07 10:06
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
('writer', '0002_rename_taskimages_add_content_and_keywords'),
]
operations = [
migrations.AlterModelOptions(
name='content',
options={'ordering': ['-generated_at'], 'verbose_name': 'Content', 'verbose_name_plural': 'Contents'},
),
migrations.AlterModelOptions(
name='images',
options={'ordering': ['task', 'position', '-created_at'], 'verbose_name': 'Image', 'verbose_name_plural': 'Images'},
),
migrations.AlterModelOptions(
name='tasks',
options={'ordering': ['-created_at'], 'verbose_name': 'Task', 'verbose_name_plural': 'Tasks'},
),
migrations.RenameIndex(
model_name='content',
new_name='igny8_conte_task_id_712988_idx',
old_name='igny8_content_task_id_idx',
),
migrations.RenameIndex(
model_name='content',
new_name='igny8_conte_generat_7128df_idx',
old_name='igny8_content_generated_idx',
),
migrations.RenameIndex(
model_name='images',
new_name='igny8_image_task_id_c80e87_idx',
old_name='igny8_task__task_id_143db4_idx',
),
migrations.RenameIndex(
model_name='images',
new_name='igny8_image_status_4a4de2_idx',
old_name='igny8_task__status_4f869f_idx',
),
migrations.RenameIndex(
model_name='images',
new_name='igny8_image_task_id_6340e6_idx',
old_name='igny8_images_task_id_pos_idx',
),
migrations.AddField(
model_name='content',
name='created_at',
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
preserve_default=False,
),
migrations.AlterField(
model_name='content',
name='sector',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.sector'),
),
migrations.AlterField(
model_name='content',
name='site',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.site'),
),
migrations.AlterField(
model_name='content',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant'),
),
migrations.AlterField(
model_name='content',
name='word_count',
field=models.IntegerField(default=0, validators=[django.core.validators.MinValueValidator(0)]),
),
migrations.AlterField(
model_name='images',
name='image_path',
field=models.CharField(blank=True, help_text='Local path if stored locally', max_length=500, null=True),
),
migrations.AlterField(
model_name='images',
name='image_url',
field=models.URLField(blank=True, help_text='URL of the generated/stored image', null=True),
),
migrations.AlterField(
model_name='images',
name='position',
field=models.IntegerField(default=0, help_text='Position for in-article images ordering'),
),
migrations.AlterField(
model_name='images',
name='prompt',
field=models.TextField(blank=True, help_text='Image generation prompt used', null=True),
),
migrations.AlterField(
model_name='images',
name='status',
field=models.CharField(default='pending', help_text='Status: pending, generated, failed', max_length=50),
),
migrations.AlterField(
model_name='images',
name='task',
field=models.ForeignKey(help_text='The task this image belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='images', to='writer.tasks'),
),
]

View File

@@ -0,0 +1,178 @@
from django.db import models
from django.core.validators import MinValueValidator
from igny8_core.auth.models import SiteSectorBaseModel
from igny8_core.modules.planner.models import Clusters, ContentIdeas, Keywords
class Tasks(SiteSectorBaseModel):
"""Tasks model for content generation queue"""
STATUS_CHOICES = [
('queued', 'Queued'),
('in_progress', 'In Progress'),
('draft', 'Draft'),
('review', 'Review'),
('published', 'Published'),
('completed', 'Completed'),
]
CONTENT_STRUCTURE_CHOICES = [
('cluster_hub', 'Cluster Hub'),
('landing_page', 'Landing Page'),
('pillar_page', 'Pillar Page'),
('supporting_page', 'Supporting Page'),
]
CONTENT_TYPE_CHOICES = [
('blog_post', 'Blog Post'),
('article', 'Article'),
('guide', 'Guide'),
('tutorial', 'Tutorial'),
]
title = models.CharField(max_length=255, db_index=True)
description = models.TextField(blank=True, null=True)
keywords = models.CharField(max_length=500, blank=True) # Comma-separated keywords (legacy)
cluster = models.ForeignKey(
Clusters,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='tasks',
limit_choices_to={'sector': models.F('sector')}
)
keyword_objects = models.ManyToManyField(
Keywords,
blank=True,
related_name='tasks',
help_text="Individual keywords linked to this task"
)
idea = models.ForeignKey(
ContentIdeas,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='tasks'
)
content_structure = models.CharField(max_length=50, choices=CONTENT_STRUCTURE_CHOICES, default='blog_post')
content_type = models.CharField(max_length=50, choices=CONTENT_TYPE_CHOICES, default='blog_post')
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='queued')
# Content fields
content = models.TextField(blank=True, null=True) # Generated content
word_count = models.IntegerField(default=0)
# SEO fields
meta_title = models.CharField(max_length=255, blank=True, null=True)
meta_description = models.TextField(blank=True, null=True)
# WordPress integration
assigned_post_id = models.IntegerField(null=True, blank=True) # WordPress post ID if published
post_url = models.URLField(blank=True, null=True) # WordPress post URL
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'igny8_tasks'
ordering = ['-created_at']
verbose_name = 'Task'
verbose_name_plural = 'Tasks'
indexes = [
models.Index(fields=['title']),
models.Index(fields=['status']),
models.Index(fields=['cluster']),
models.Index(fields=['content_type']),
models.Index(fields=['site', 'sector']),
]
def __str__(self):
return self.title
class Content(SiteSectorBaseModel):
"""
Content model for storing final AI-generated article content.
Separated from Task for content versioning and storage optimization.
"""
task = models.OneToOneField(
Tasks,
on_delete=models.CASCADE,
related_name='content_record',
help_text="The task this content belongs to"
)
html_content = models.TextField(help_text="Final AI-generated HTML content")
word_count = models.IntegerField(default=0, validators=[MinValueValidator(0)])
metadata = models.JSONField(default=dict, help_text="Additional metadata (SEO, structure, etc.)")
generated_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'igny8_content'
ordering = ['-generated_at']
verbose_name = 'Content'
verbose_name_plural = 'Contents'
indexes = [
models.Index(fields=['task']),
models.Index(fields=['generated_at']),
]
def save(self, *args, **kwargs):
"""Automatically set account, site, and sector from task"""
if 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"Content for {self.task.title}"
class Images(SiteSectorBaseModel):
"""Images model for task-related images (featured, desktop, mobile)"""
IMAGE_TYPE_CHOICES = [
('featured', 'Featured Image'),
('desktop', 'Desktop Image'),
('mobile', 'Mobile Image'),
('in_article', 'In-Article Image'),
]
task = models.ForeignKey(
Tasks,
on_delete=models.CASCADE,
related_name='images',
help_text="The task this image belongs to"
)
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")
image_path = models.CharField(max_length=500, blank=True, null=True, help_text="Local path if stored locally")
prompt = models.TextField(blank=True, null=True, help_text="Image generation prompt used")
status = models.CharField(max_length=50, default='pending', help_text="Status: pending, generated, failed")
position = models.IntegerField(default=0, help_text="Position for in-article images ordering")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'igny8_images'
ordering = ['task', 'position', '-created_at']
verbose_name = 'Image'
verbose_name_plural = 'Images'
indexes = [
models.Index(fields=['task', 'image_type']),
models.Index(fields=['status']),
models.Index(fields=['task', 'position']),
]
def save(self, *args, **kwargs):
"""Automatically set account, site, and sector from task"""
if 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}"

View File

@@ -0,0 +1,136 @@
from rest_framework import serializers
from .models import Tasks, Images, Content
from igny8_core.modules.planner.models import Clusters, ContentIdeas
class TasksSerializer(serializers.ModelSerializer):
"""Serializer for Tasks model"""
cluster_name = serializers.SerializerMethodField()
sector_name = serializers.SerializerMethodField()
idea_title = serializers.SerializerMethodField()
site_id = serializers.IntegerField(write_only=True, required=False)
sector_id = serializers.IntegerField(write_only=True, required=False)
class Meta:
model = Tasks
fields = [
'id',
'title',
'description',
'keywords',
'cluster_id',
'cluster_name',
'sector_name',
'idea_id',
'idea_title',
'content_structure',
'content_type',
'status',
'content',
'word_count',
'meta_title',
'meta_description',
'assigned_post_id',
'post_url',
'created_at',
'updated_at',
'site_id',
'sector_id',
'account_id',
]
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
def get_cluster_name(self, obj):
"""Get cluster name from Clusters model"""
if obj.cluster_id:
try:
cluster = Clusters.objects.get(id=obj.cluster_id)
return cluster.name
except Clusters.DoesNotExist:
return None
return None
def get_sector_name(self, obj):
"""Get sector name from Sector model"""
if obj.sector_id:
try:
from igny8_core.auth.models import Sector
sector = Sector.objects.get(id=obj.sector_id)
return sector.name
except Sector.DoesNotExist:
return None
return None
def get_idea_title(self, obj):
"""Get idea title from ContentIdeas model"""
if obj.idea_id:
try:
idea = ContentIdeas.objects.get(id=obj.idea_id)
return idea.idea_title
except ContentIdeas.DoesNotExist:
return None
return None
class ImagesSerializer(serializers.ModelSerializer):
"""Serializer for Images model"""
task_title = serializers.SerializerMethodField()
class Meta:
model = Images
fields = [
'id',
'task_id',
'task_title',
'image_type',
'image_url',
'image_path',
'prompt',
'status',
'position',
'created_at',
'updated_at',
'account_id',
]
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
def get_task_title(self, obj):
"""Get task title"""
if obj.task_id:
try:
task = Tasks.objects.get(id=obj.task_id)
return task.title
except Tasks.DoesNotExist:
return None
return None
class ContentSerializer(serializers.ModelSerializer):
"""Serializer for Content model"""
task_title = serializers.SerializerMethodField()
class Meta:
model = Content
fields = [
'id',
'task_id',
'task_title',
'html_content',
'word_count',
'metadata',
'generated_at',
'updated_at',
'account_id',
]
read_only_fields = ['id', 'generated_at', 'updated_at', 'account_id']
def get_task_title(self, obj):
"""Get task title"""
if obj.task_id:
try:
task = Tasks.objects.get(id=obj.task_id)
return task.title
except Tasks.DoesNotExist:
return None
return None

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TasksViewSet, ImagesViewSet, ContentViewSet
router = DefaultRouter()
router.register(r'tasks', TasksViewSet, basename='task')
router.register(r'images', ImagesViewSet, basename='image')
router.register(r'content', ContentViewSet, basename='content')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -0,0 +1,407 @@
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 igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.pagination import CustomPageNumberPagination
from .models import Tasks, Images, Content
from .serializers import TasksSerializer, ImagesSerializer, ContentSerializer
class TasksViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing tasks with CRUD operations
"""
queryset = Tasks.objects.all()
serializer_class = TasksSerializer
pagination_class = CustomPageNumberPagination # Explicitly use custom pagination
# DRF filtering configuration
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
# Search configuration
search_fields = ['title', 'keywords']
# Ordering configuration
ordering_fields = ['title', 'created_at', 'word_count', 'status']
ordering = ['-created_at'] # Default ordering (newest first)
# Filter configuration
filterset_fields = ['status', 'cluster_id', 'content_type', 'content_structure']
def perform_create(self, serializer):
"""Require explicit site_id and sector_id - no defaults."""
user = getattr(self.request, 'user', None)
try:
query_params = getattr(self.request, 'query_params', None)
if query_params is None:
query_params = getattr(self.request, 'GET', {})
except AttributeError:
query_params = {}
site_id = serializer.validated_data.get('site_id') or query_params.get('site_id')
sector_id = serializer.validated_data.get('sector_id') or query_params.get('sector_id')
from igny8_core.auth.models import Site, Sector
from rest_framework.exceptions import ValidationError
# Site ID is REQUIRED
if not site_id:
raise ValidationError("site_id is required. Please select a site.")
try:
site = Site.objects.get(id=site_id)
except Site.DoesNotExist:
raise ValidationError(f"Site with id {site_id} does not exist")
# Sector ID is REQUIRED
if not sector_id:
raise ValidationError("sector_id is required. Please select a sector.")
try:
sector = Sector.objects.get(id=sector_id)
if sector.site_id != site_id:
raise ValidationError(f"Sector does not belong to the selected site")
except Sector.DoesNotExist:
raise ValidationError(f"Sector with id {sector_id} does not exist")
serializer.validated_data.pop('site_id', None)
serializer.validated_data.pop('sector_id', None)
account = getattr(self.request, 'account', None)
if not account and user and user.is_authenticated and user.account:
account = user.account
if not account:
account = site.account
serializer.save(account=account, site=site, sector=sector)
@action(detail=False, methods=['post'], url_path='bulk_delete', url_name='bulk_delete')
def bulk_delete(self, request):
"""Bulk delete tasks"""
ids = request.data.get('ids', [])
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
queryset = self.get_queryset()
deleted_count, _ = queryset.filter(id__in=ids).delete()
return Response({'deleted_count': deleted_count}, status=status.HTTP_200_OK)
@action(detail=False, methods=['post'], url_path='bulk_update', url_name='bulk_update')
def bulk_update(self, request):
"""Bulk update task status"""
ids = request.data.get('ids', [])
status_value = request.data.get('status')
if not ids:
return Response({'error': 'No IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
if not status_value:
return Response({'error': 'No status provided'}, status=status.HTTP_400_BAD_REQUEST)
queryset = self.get_queryset()
updated_count = queryset.filter(id__in=ids).update(status=status_value)
return Response({'updated_count': updated_count}, status=status.HTTP_200_OK)
@action(detail=False, methods=['post'], url_path='auto_generate_content', url_name='auto_generate_content')
def auto_generate_content(self, request):
"""Auto-generate content for tasks using AI"""
import logging
from django.db import OperationalError, DatabaseError, IntegrityError
from django.core.exceptions import ValidationError
logger = logging.getLogger(__name__)
try:
ids = request.data.get('ids', [])
if not ids:
logger.warning("auto_generate_content: No IDs provided")
return Response({
'error': 'No IDs provided',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
if len(ids) > 10:
logger.warning(f"auto_generate_content: Too many IDs provided: {len(ids)}")
return Response({
'error': 'Maximum 10 tasks allowed for content generation',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
logger.info(f"auto_generate_content: Processing {len(ids)} task IDs: {ids}")
# Get account
account = getattr(request, 'account', None)
account_id = account.id if account else None
logger.info(f"auto_generate_content: Account ID: {account_id}")
# Validate task IDs exist in database before proceeding
try:
queryset = self.get_queryset()
existing_tasks = queryset.filter(id__in=ids)
existing_count = existing_tasks.count()
existing_ids = list(existing_tasks.values_list('id', flat=True))
logger.info(f"auto_generate_content: Found {existing_count} existing tasks out of {len(ids)} requested")
logger.info(f"auto_generate_content: Existing task IDs: {existing_ids}")
if existing_count == 0:
logger.error(f"auto_generate_content: No tasks found for IDs: {ids}")
return Response({
'error': f'No tasks found for the provided IDs: {ids}',
'type': 'NotFound',
'requested_ids': ids
}, status=status.HTTP_404_NOT_FOUND)
if existing_count < len(ids):
missing_ids = set(ids) - set(existing_ids)
logger.warning(f"auto_generate_content: Some task IDs not found: {missing_ids}")
# Continue with existing tasks, but log warning
except (OperationalError, DatabaseError) as db_error:
logger.error("=" * 80)
logger.error("DATABASE ERROR: Failed to query tasks")
logger.error(f" - Error type: {type(db_error).__name__}")
logger.error(f" - Error message: {str(db_error)}")
logger.error(f" - Requested IDs: {ids}")
logger.error(f" - Account ID: {account_id}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Database error while querying tasks: {str(db_error)}',
'type': 'OperationalError',
'details': 'Failed to retrieve tasks from database. Please check database connection and try again.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# Try to queue Celery task, fall back to synchronous if Celery not available
try:
from .tasks import auto_generate_content_task
if hasattr(auto_generate_content_task, 'delay'):
# Celery is available - queue async task
logger.info(f"auto_generate_content: Queuing Celery task for {len(ids)} tasks")
try:
task = auto_generate_content_task.delay(ids, account_id=account_id)
logger.info(f"auto_generate_content: Celery task queued successfully: {task.id}")
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Content generation started'
}, status=status.HTTP_200_OK)
except Exception as celery_error:
logger.error("=" * 80)
logger.error("CELERY ERROR: Failed to queue task")
logger.error(f" - Error type: {type(celery_error).__name__}")
logger.error(f" - Error message: {str(celery_error)}")
logger.error(f" - Task IDs: {ids}")
logger.error(f" - Account ID: {account_id}")
logger.error("=" * 80, exc_info=True)
# Fall back to synchronous execution
logger.info("auto_generate_content: Falling back to synchronous execution")
result = auto_generate_content_task(ids, account_id=account_id)
if result.get('success'):
return Response({
'success': True,
'tasks_updated': result.get('tasks_updated', 0),
'message': 'Content generated successfully (synchronous)'
}, status=status.HTTP_200_OK)
else:
return Response({
'error': result.get('error', 'Content generation failed'),
'type': 'TaskExecutionError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
else:
# Celery not available - execute synchronously
logger.info(f"auto_generate_content: Executing synchronously (Celery not available)")
result = auto_generate_content_task(ids, account_id=account_id)
if result.get('success'):
logger.info(f"auto_generate_content: Synchronous execution successful: {result.get('tasks_updated', 0)} tasks updated")
return Response({
'success': True,
'tasks_updated': result.get('tasks_updated', 0),
'message': 'Content generated successfully'
}, status=status.HTTP_200_OK)
else:
logger.error(f"auto_generate_content: Synchronous execution failed: {result.get('error', 'Unknown error')}")
return Response({
'error': result.get('error', 'Content generation failed'),
'type': 'TaskExecutionError'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except ImportError as import_error:
logger.error(f"auto_generate_content: ImportError - tasks module not available: {str(import_error)}")
# Tasks module not available - update status only
try:
queryset = self.get_queryset()
tasks = queryset.filter(id__in=ids, status__in=['queued', 'in_progress'])
updated_count = tasks.update(status='draft', content='[AI content generation not available]')
logger.info(f"auto_generate_content: Updated {updated_count} tasks (AI generation not available)")
return Response({
'updated_count': updated_count,
'message': 'Tasks updated (AI generation not available)'
}, status=status.HTTP_200_OK)
except (OperationalError, DatabaseError) as db_error:
logger.error("=" * 80)
logger.error("DATABASE ERROR: Failed to update tasks")
logger.error(f" - Error type: {type(db_error).__name__}")
logger.error(f" - Error message: {str(db_error)}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Database error while updating tasks: {str(db_error)}',
'type': 'OperationalError',
'details': 'Failed to update tasks in database. Please check database connection.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except (OperationalError, DatabaseError) as db_error:
logger.error("=" * 80)
logger.error("DATABASE ERROR: Failed during task execution")
logger.error(f" - Error type: {type(db_error).__name__}")
logger.error(f" - Error message: {str(db_error)}")
logger.error(f" - Task IDs: {ids}")
logger.error(f" - Account ID: {account_id}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Database error during content generation: {str(db_error)}',
'type': 'OperationalError',
'details': 'A database operation failed. This may be due to connection issues, constraint violations, or data integrity problems. Check the logs for more details.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except IntegrityError as integrity_error:
logger.error("=" * 80)
logger.error("INTEGRITY ERROR: Data integrity violation")
logger.error(f" - Error message: {str(integrity_error)}")
logger.error(f" - Task IDs: {ids}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Data integrity error: {str(integrity_error)}',
'type': 'IntegrityError',
'details': 'The operation violated database constraints. This may indicate missing required relationships or invalid data.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except ValidationError as validation_error:
logger.error(f"auto_generate_content: ValidationError: {str(validation_error)}")
return Response({
'error': f'Validation error: {str(validation_error)}',
'type': 'ValidationError'
}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error("=" * 80)
logger.error("UNEXPECTED ERROR in auto_generate_content")
logger.error(f" - Error type: {type(e).__name__}")
logger.error(f" - Error message: {str(e)}")
logger.error(f" - Task IDs: {ids}")
logger.error(f" - Account ID: {account_id}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Unexpected error: {str(e)}',
'type': type(e).__name__,
'details': 'An unexpected error occurred. Please check the logs for more details.'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as outer_error:
logger.error("=" * 80)
logger.error("CRITICAL ERROR: Outer exception handler")
logger.error(f" - Error type: {type(outer_error).__name__}")
logger.error(f" - Error message: {str(outer_error)}")
logger.error("=" * 80, exc_info=True)
return Response({
'error': f'Critical error: {str(outer_error)}',
'type': type(outer_error).__name__
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ImagesViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing task 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']
def perform_create(self, serializer):
"""Override to automatically set account"""
account = getattr(self.request, 'account', None)
if account:
serializer.save(account=account)
else:
serializer.save()
@action(detail=False, methods=['post'], url_path='auto_generate', url_name='auto_generate_images')
def auto_generate_images(self, request):
"""Auto-generate images for tasks using AI"""
task_ids = request.data.get('task_ids', [])
if not task_ids:
return Response({'error': 'No task IDs provided'}, status=status.HTTP_400_BAD_REQUEST)
if len(task_ids) > 10:
return Response({'error': 'Maximum 10 tasks allowed for image generation'}, status=status.HTTP_400_BAD_REQUEST)
# Get account
account = getattr(request, 'account', None)
account_id = account.id if account else None
# Try to queue Celery task, fall back to synchronous if Celery not available
try:
from .tasks import auto_generate_images_task
if hasattr(auto_generate_images_task, 'delay'):
# Celery is available - queue async task
task = auto_generate_images_task.delay(task_ids, account_id=account_id)
return Response({
'success': True,
'task_id': str(task.id),
'message': 'Image generation started'
}, status=status.HTTP_200_OK)
else:
# Celery not available - execute synchronously
result = auto_generate_images_task(task_ids, account_id=account_id)
if result.get('success'):
return Response({
'success': True,
'images_created': result.get('images_created', 0),
'message': result.get('message', 'Image generation completed')
}, status=status.HTTP_200_OK)
else:
return Response({
'error': result.get('error', 'Image generation failed')
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except ImportError:
# Tasks module not available
return Response({
'error': 'Image generation task not available'
}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
class ContentViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing task content
"""
queryset = Content.objects.all()
serializer_class = ContentSerializer
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
ordering_fields = ['generated_at', 'updated_at']
ordering = ['-generated_at']
filterset_fields = ['task_id']
def perform_create(self, serializer):
"""Override to automatically set account"""
account = getattr(self.request, 'account', None)
if account:
serializer.save(account=account)
else:
serializer.save()