- 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.
196 lines
7.1 KiB
Python
196 lines
7.1 KiB
Python
from django.db import models
|
|
from igny8_core.auth.models import SiteSectorBaseModel, SeedKeyword
|
|
|
|
|
|
class Clusters(SiteSectorBaseModel):
|
|
"""Clusters model for keyword grouping"""
|
|
|
|
name = models.CharField(max_length=255, unique=True, db_index=True)
|
|
description = models.TextField(blank=True, null=True)
|
|
keywords_count = models.IntegerField(default=0)
|
|
volume = models.IntegerField(default=0)
|
|
mapped_pages = models.IntegerField(default=0)
|
|
status = models.CharField(max_length=50, default='active')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'igny8_clusters'
|
|
ordering = ['name']
|
|
verbose_name = 'Cluster'
|
|
verbose_name_plural = 'Clusters'
|
|
indexes = [
|
|
models.Index(fields=['name']),
|
|
models.Index(fields=['status']),
|
|
models.Index(fields=['site', 'sector']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class Keywords(SiteSectorBaseModel):
|
|
"""
|
|
Keywords model for SEO keyword management.
|
|
Site-specific instances that reference global SeedKeywords.
|
|
"""
|
|
|
|
STATUS_CHOICES = [
|
|
('active', 'Active'),
|
|
('pending', 'Pending'),
|
|
('archived', 'Archived'),
|
|
]
|
|
|
|
# Required: Link to global SeedKeyword
|
|
seed_keyword = models.ForeignKey(
|
|
SeedKeyword,
|
|
on_delete=models.PROTECT, # Prevent deletion if Keywords reference it
|
|
related_name='site_keywords',
|
|
help_text="Reference to the global seed keyword"
|
|
)
|
|
|
|
# Site-specific overrides (optional)
|
|
volume_override = models.IntegerField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Site-specific volume override (uses seed_keyword.volume if not set)"
|
|
)
|
|
difficulty_override = models.IntegerField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Site-specific difficulty override (uses seed_keyword.difficulty if not set)"
|
|
)
|
|
|
|
cluster = models.ForeignKey(
|
|
'Clusters',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='keywords',
|
|
limit_choices_to={'sector': models.F('sector')} # Cluster must be in same sector
|
|
)
|
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='pending')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'igny8_keywords'
|
|
ordering = ['-created_at']
|
|
verbose_name = 'Keyword'
|
|
verbose_name_plural = 'Keywords'
|
|
unique_together = [['seed_keyword', 'site', 'sector']] # One keyword per site/sector
|
|
indexes = [
|
|
models.Index(fields=['seed_keyword']),
|
|
models.Index(fields=['status']),
|
|
models.Index(fields=['cluster']),
|
|
models.Index(fields=['site', 'sector']),
|
|
models.Index(fields=['seed_keyword', 'site', 'sector']),
|
|
]
|
|
|
|
@property
|
|
def keyword(self):
|
|
"""Get keyword text from seed_keyword"""
|
|
return self.seed_keyword.keyword if self.seed_keyword else ''
|
|
|
|
@property
|
|
def volume(self):
|
|
"""Get volume from override or seed_keyword"""
|
|
return self.volume_override if self.volume_override is not None else (self.seed_keyword.volume if self.seed_keyword else 0)
|
|
|
|
@property
|
|
def difficulty(self):
|
|
"""Get difficulty from override or seed_keyword"""
|
|
return self.difficulty_override if self.difficulty_override is not None else (self.seed_keyword.difficulty if self.seed_keyword else 0)
|
|
|
|
@property
|
|
def intent(self):
|
|
"""Get intent from seed_keyword"""
|
|
return self.seed_keyword.intent if self.seed_keyword else 'informational'
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""Validate that seed_keyword's industry/sector matches site's industry/sector"""
|
|
if self.seed_keyword and self.site and self.sector:
|
|
# Validate industry match
|
|
if self.site.industry != self.seed_keyword.industry:
|
|
from django.core.exceptions import ValidationError
|
|
raise ValidationError(
|
|
f"SeedKeyword industry ({self.seed_keyword.industry.name}) must match site industry ({self.site.industry.name})"
|
|
)
|
|
|
|
# Validate sector match (site sector's industry_sector must match seed_keyword's sector)
|
|
if self.sector.industry_sector != self.seed_keyword.sector:
|
|
from django.core.exceptions import ValidationError
|
|
raise ValidationError(
|
|
f"SeedKeyword sector ({self.seed_keyword.sector.name}) must match site sector's industry sector ({self.sector.industry_sector.name if self.sector.industry_sector else 'None'})"
|
|
)
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
return self.keyword
|
|
|
|
|
|
class ContentIdeas(SiteSectorBaseModel):
|
|
"""Content Ideas model for planning content based on keyword clusters"""
|
|
|
|
STATUS_CHOICES = [
|
|
('new', 'New'),
|
|
('scheduled', 'Scheduled'),
|
|
('published', 'Published'),
|
|
]
|
|
|
|
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'),
|
|
]
|
|
|
|
idea_title = models.CharField(max_length=255, db_index=True)
|
|
description = models.TextField(blank=True, null=True)
|
|
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')
|
|
target_keywords = models.CharField(max_length=500, blank=True) # Comma-separated keywords (legacy)
|
|
keyword_objects = models.ManyToManyField(
|
|
'Keywords',
|
|
blank=True,
|
|
related_name='content_ideas',
|
|
help_text="Individual keywords linked to this content idea"
|
|
)
|
|
keyword_cluster = models.ForeignKey(
|
|
Clusters,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='ideas',
|
|
limit_choices_to={'sector': models.F('sector')}
|
|
)
|
|
status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='new')
|
|
estimated_word_count = models.IntegerField(default=1000)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'igny8_content_ideas'
|
|
ordering = ['-created_at']
|
|
verbose_name = 'Content Idea'
|
|
verbose_name_plural = 'Content Ideas'
|
|
indexes = [
|
|
models.Index(fields=['idea_title']),
|
|
models.Index(fields=['status']),
|
|
models.Index(fields=['keyword_cluster']),
|
|
models.Index(fields=['content_structure']),
|
|
models.Index(fields=['site', 'sector']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.idea_title
|
|
|