- Introduced a new scheduled task for executing automation rules every 5 minutes in the Celery beat schedule. - Updated URL routing to include a new endpoint for automation-related functionalities. - Refactored imports in various modules to align with the new business layer structure, ensuring backward compatibility for billing models, exceptions, and services.
206 lines
7.8 KiB
Python
206 lines
7.8 KiB
Python
from django.db import models
|
|
from django.core.validators import MinValueValidator
|
|
from igny8_core.auth.models import SiteSectorBaseModel
|
|
|
|
|
|
class Tasks(SiteSectorBaseModel):
|
|
"""Tasks model for content generation queue"""
|
|
|
|
STATUS_CHOICES = [
|
|
('queued', 'Queued'),
|
|
('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(
|
|
'planning.Clusters',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='tasks',
|
|
limit_choices_to={'sector': models.F('sector')}
|
|
)
|
|
keyword_objects = models.ManyToManyField(
|
|
'planning.Keywords',
|
|
blank=True,
|
|
related_name='tasks',
|
|
help_text="Individual keywords linked to this task"
|
|
)
|
|
idea = models.ForeignKey(
|
|
'planning.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.)")
|
|
title = models.CharField(max_length=255, blank=True, null=True)
|
|
meta_title = models.CharField(max_length=255, blank=True, null=True)
|
|
meta_description = models.TextField(blank=True, null=True)
|
|
primary_keyword = models.CharField(max_length=255, blank=True, null=True)
|
|
secondary_keywords = models.JSONField(default=list, blank=True, help_text="List of secondary keywords")
|
|
tags = models.JSONField(default=list, blank=True, help_text="List of tags")
|
|
categories = models.JSONField(default=list, blank=True, help_text="List of categories")
|
|
STATUS_CHOICES = [
|
|
('draft', 'Draft'),
|
|
('review', 'Review'),
|
|
('publish', 'Publish'),
|
|
]
|
|
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)
|
|
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 content-related images (featured, desktop, mobile, in-article)"""
|
|
|
|
IMAGE_TYPE_CHOICES = [
|
|
('featured', 'Featured Image'),
|
|
('desktop', 'Desktop Image'),
|
|
('mobile', 'Mobile Image'),
|
|
('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',
|
|
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.CharField(max_length=500, 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 = ['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 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):
|
|
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}"
|
|
|