- Introduced new fields in the Content model for source tracking and sync status, including external references and optimization fields. - Updated the services module to include new content generation and pipeline services for better organization and clarity.
253 lines
9.6 KiB
Python
253 lines
9.6 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(
|
|
'planner.Clusters',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='tasks',
|
|
limit_choices_to={'sector': models.F('sector')}
|
|
)
|
|
keyword_objects = models.ManyToManyField(
|
|
'planner.Keywords',
|
|
blank=True,
|
|
related_name='tasks',
|
|
help_text="Individual keywords linked to this task"
|
|
)
|
|
idea = models.ForeignKey(
|
|
'planner.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:
|
|
app_label = 'writer'
|
|
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)
|
|
|
|
# Phase 4: Source tracking
|
|
SOURCE_CHOICES = [
|
|
('igny8', 'IGNY8 Generated'),
|
|
('wordpress', 'WordPress Synced'),
|
|
('shopify', 'Shopify Synced'),
|
|
('custom', 'Custom API Synced'),
|
|
]
|
|
source = models.CharField(
|
|
max_length=50,
|
|
choices=SOURCE_CHOICES,
|
|
default='igny8',
|
|
db_index=True,
|
|
help_text="Source of the content"
|
|
)
|
|
|
|
SYNC_STATUS_CHOICES = [
|
|
('native', 'Native IGNY8 Content'),
|
|
('imported', 'Imported from External'),
|
|
('synced', 'Synced from External'),
|
|
]
|
|
sync_status = models.CharField(
|
|
max_length=50,
|
|
choices=SYNC_STATUS_CHOICES,
|
|
default='native',
|
|
db_index=True,
|
|
help_text="Sync status of the content"
|
|
)
|
|
|
|
# External reference fields
|
|
external_id = models.CharField(max_length=255, blank=True, null=True, help_text="External platform ID")
|
|
external_url = models.URLField(blank=True, null=True, help_text="External platform URL")
|
|
sync_metadata = models.JSONField(default=dict, blank=True, help_text="Platform-specific sync metadata")
|
|
|
|
# Phase 4: Linking fields
|
|
internal_links = models.JSONField(default=list, blank=True, help_text="Internal links added by linker")
|
|
linker_version = models.IntegerField(default=0, help_text="Version of linker processing")
|
|
|
|
# Phase 4: Optimization fields
|
|
optimizer_version = models.IntegerField(default=0, help_text="Version of optimizer processing")
|
|
optimization_scores = models.JSONField(default=dict, blank=True, help_text="Optimization scores (SEO, readability, engagement)")
|
|
|
|
class Meta:
|
|
app_label = 'writer'
|
|
db_table = 'igny8_content'
|
|
ordering = ['-generated_at']
|
|
verbose_name = 'Content'
|
|
verbose_name_plural = 'Contents'
|
|
indexes = [
|
|
models.Index(fields=['task']),
|
|
models.Index(fields=['generated_at']),
|
|
models.Index(fields=['source']),
|
|
models.Index(fields=['sync_status']),
|
|
models.Index(fields=['source', 'sync_status']),
|
|
]
|
|
|
|
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:
|
|
app_label = 'writer'
|
|
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}"
|
|
|