Files

399 lines
14 KiB
Python

"""
System module models - for global settings and prompts
"""
from django.db import models
from django.conf import settings
from igny8_core.auth.models import AccountBaseModel
# Import settings models
from .settings_models import (
SystemSettings, AccountSettings, UserSettings, ModuleSettings, ModuleEnableSettings, AISettings
)
class IntegrationProvider(models.Model):
"""
Centralized storage for ALL external service API keys.
Per final-model-schemas.md:
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| provider_id | CharField(50) PK | Yes | openai, runware, stripe, paypal, resend |
| display_name | CharField(100) | Yes | Human-readable name |
| provider_type | CharField(20) | Yes | ai / payment / email / storage |
| api_key | CharField(500) | No | Primary API key |
| api_secret | CharField(500) | No | Secondary secret (Stripe, PayPal) |
| webhook_secret | CharField(500) | No | Webhook signing secret |
| api_endpoint | URLField | No | Custom endpoint (optional) |
| config | JSONField | No | Provider-specific config |
| is_active | BooleanField | Yes | Enable/disable provider |
| is_sandbox | BooleanField | Yes | Test mode flag |
| updated_by | FK(User) | No | Audit trail |
| created_at | DateTime | Auto | |
| updated_at | DateTime | Auto | |
"""
PROVIDER_TYPE_CHOICES = [
('ai', 'AI Provider'),
('payment', 'Payment Gateway'),
('email', 'Email Service'),
('storage', 'Storage Service'),
]
# Primary Key
provider_id = models.CharField(
max_length=50,
unique=True,
primary_key=True,
help_text="Unique identifier (e.g., 'openai', 'stripe', 'resend')"
)
# Display name
display_name = models.CharField(
max_length=100,
help_text="Human-readable name"
)
# Provider type
provider_type = models.CharField(
max_length=20,
choices=PROVIDER_TYPE_CHOICES,
default='ai',
db_index=True,
help_text="ai / payment / email / storage"
)
# Authentication
api_key = models.CharField(
max_length=500,
blank=True,
help_text="Primary API key"
)
api_secret = models.CharField(
max_length=500,
blank=True,
help_text="Secondary secret (Stripe, PayPal)"
)
webhook_secret = models.CharField(
max_length=500,
blank=True,
help_text="Webhook signing secret"
)
# Endpoints
api_endpoint = models.URLField(
blank=True,
help_text="Custom endpoint (optional)"
)
# Configuration
config = models.JSONField(
default=dict,
blank=True,
help_text="Provider-specific config"
)
# Status
is_active = models.BooleanField(
default=True,
db_index=True,
help_text="Enable/disable provider"
)
is_sandbox = models.BooleanField(
default=False,
help_text="Test mode flag"
)
# Audit
updated_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='integration_provider_updates',
help_text="Audit trail"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'igny8_integration_providers'
verbose_name = 'Integration Provider'
verbose_name_plural = 'Integration Providers'
ordering = ['provider_type', 'display_name']
def __str__(self):
status = "Active" if self.is_active else "Inactive"
mode = "(Sandbox)" if self.is_sandbox else ""
return f"{self.display_name} - {status} {mode}".strip()
@classmethod
def get_provider(cls, provider_id: str):
"""Get provider by ID, returns None if not found or inactive"""
try:
return cls.objects.get(provider_id=provider_id, is_active=True)
except cls.DoesNotExist:
return None
@classmethod
def get_api_key(cls, provider_id: str) -> str:
"""Get API key for a provider"""
provider = cls.get_provider(provider_id)
return provider.api_key if provider else ""
@classmethod
def get_providers_by_type(cls, provider_type: str):
"""Get all active providers of a type"""
return cls.objects.filter(provider_type=provider_type, is_active=True)
class AIPrompt(AccountBaseModel):
"""
Account-specific AI Prompt templates.
Stores global default in default_prompt, current value in prompt_value.
When user saves an override, prompt_value changes but default_prompt stays.
Reset copies default_prompt back to prompt_value.
"""
PROMPT_TYPE_CHOICES = [
('clustering', 'Clustering'),
('ideas', 'Ideas Generation'),
('content_generation', 'Content Generation'),
('image_prompt_extraction', 'Image Prompt Extraction'),
('image_prompt_template', 'Image Prompt Template'),
('negative_prompt', 'Negative Prompt'),
('site_structure_generation', 'Site Structure Generation'), # Phase 7: Site Builder prompts
# Phase 8: Universal Content Types
('product_generation', 'Product Content Generation'),
('service_generation', 'Service Page Generation'),
('taxonomy_generation', 'Taxonomy Generation'),
]
prompt_type = models.CharField(max_length=50, choices=PROMPT_TYPE_CHOICES, db_index=True)
prompt_value = models.TextField(help_text="Current prompt text (customized or default)")
default_prompt = models.TextField(
blank=True,
help_text="Global default prompt - used for reset to default"
)
is_customized = models.BooleanField(
default=False,
help_text="True if account customized the prompt, False if using global default"
)
is_active = models.BooleanField(default=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'igny8_ai_prompts'
ordering = ['prompt_type']
unique_together = [['account', 'prompt_type']] # Each account can have one prompt per type
indexes = [
models.Index(fields=['prompt_type']),
models.Index(fields=['account', 'prompt_type']),
models.Index(fields=['is_customized']),
]
@classmethod
def get_effective_prompt(cls, account, prompt_type):
"""
Get the effective prompt for an account.
Returns account-specific prompt if exists and customized.
Otherwise returns global default.
"""
from .global_settings_models import GlobalAIPrompt
# Try to get account-specific prompt
try:
account_prompt = cls.objects.get(account=account, prompt_type=prompt_type, is_active=True)
# If customized, use account's version
if account_prompt.is_customized:
return account_prompt.prompt_value
# If not customized, use default_prompt from account record or global
return account_prompt.default_prompt or account_prompt.prompt_value
except cls.DoesNotExist:
pass
# Fallback to global prompt
try:
global_prompt = GlobalAIPrompt.objects.get(prompt_type=prompt_type, is_active=True)
return global_prompt.prompt_value
except GlobalAIPrompt.DoesNotExist:
return None
def reset_to_default(self):
"""Reset prompt to global default from GlobalAIPrompt"""
from .global_settings_models import GlobalAIPrompt
try:
global_prompt = GlobalAIPrompt.objects.get(
prompt_type=self.prompt_type,
is_active=True
)
self.prompt_value = global_prompt.prompt_value
self.default_prompt = global_prompt.prompt_value
self.is_customized = False
self.save()
except GlobalAIPrompt.DoesNotExist:
raise ValueError(
f"Cannot reset: Global prompt '{self.prompt_type}' not found. "
f"Please configure it in Django admin at: /admin/system/globalaiprompt/"
)
def __str__(self):
status = "Custom" if self.is_customized else "Default"
return f"{self.get_prompt_type_display()} ({status})"
class IntegrationSettings(AccountBaseModel):
"""
Per-account integration settings overrides.
IMPORTANT: This model stores ONLY model selection and parameter overrides.
API keys are NEVER stored here - they come from GlobalIntegrationSettings.
Free plan: Cannot create overrides, must use global defaults
Starter/Growth/Scale plans: Can override model, temperature, tokens, image settings
NULL values in config mean "use global default"
"""
INTEGRATION_TYPE_CHOICES = [
('openai', 'OpenAI'),
('dalle', 'DALL-E'),
('anthropic', 'Anthropic'),
('runware', 'Runware'),
]
integration_type = models.CharField(max_length=50, choices=INTEGRATION_TYPE_CHOICES, db_index=True)
config = models.JSONField(
default=dict,
help_text=(
"Model and parameter overrides only. Fields: model, temperature, max_tokens, "
"image_size, image_quality, etc. NULL = use global default. "
"NEVER store API keys here."
)
)
is_active = models.BooleanField(default=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'igny8_integration_settings'
unique_together = [['account', 'integration_type']]
ordering = ['integration_type']
indexes = [
models.Index(fields=['integration_type']),
models.Index(fields=['account', 'integration_type']),
]
def __str__(self):
account = getattr(self, 'account', None)
return f"{self.get_integration_type_display()} - {account.name if account else 'No Account'}"
class AuthorProfile(AccountBaseModel):
"""
Writing style profiles - tone, language, structure templates.
Can be cloned from global templates or created from scratch.
Examples: "SaaS B2B Informative", "E-commerce Product Descriptions", etc.
"""
name = models.CharField(max_length=255, help_text="Profile name (e.g., 'SaaS B2B Informative')")
description = models.TextField(blank=True, help_text="Description of the writing style")
tone = models.CharField(
max_length=100,
help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical', 'Conversational')"
)
language = models.CharField(max_length=50, default='en', help_text="Language code (e.g., 'en', 'es', 'fr')")
structure_template = models.JSONField(
default=dict,
help_text="Structure template defining content sections and their order"
)
is_custom = models.BooleanField(
default=False,
help_text="True if created by account, False if cloned from global template"
)
cloned_from = models.ForeignKey(
'system.GlobalAuthorProfile',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='cloned_instances',
help_text="Reference to the global template this was cloned from"
)
is_active = models.BooleanField(default=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'igny8_author_profiles'
ordering = ['name']
verbose_name = 'Author Profile'
verbose_name_plural = 'Author Profiles'
indexes = [
models.Index(fields=['account', 'is_active']),
models.Index(fields=['name']),
models.Index(fields=['is_custom']),
]
def __str__(self):
account = getattr(self, 'account', None)
status = "Custom" if self.is_custom else "Template"
return f"{self.name} ({status}) - {account.name if account else 'No Account'}"
class Strategy(AccountBaseModel):
"""
Defined content strategies per sector, integrating prompt types, section logic, etc.
Can be cloned from global templates or created from scratch.
Links together prompts, author profiles, and sector-specific content strategies.
"""
name = models.CharField(max_length=255, help_text="Strategy name")
description = models.TextField(blank=True, help_text="Description of the content strategy")
sector = models.ForeignKey(
'igny8_core_auth.Sector',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='strategies',
help_text="Optional: Link strategy to a specific sector"
)
prompt_types = models.JSONField(
default=list,
help_text="List of prompt types to use (e.g., ['clustering', 'ideas', 'content_generation'])"
)
section_logic = models.JSONField(
default=dict,
help_text="Section logic configuration defining content structure and flow"
)
is_custom = models.BooleanField(
default=False,
help_text="True if created by account, False if cloned from global template"
)
cloned_from = models.ForeignKey(
'system.GlobalStrategy',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='cloned_instances',
help_text="Reference to the global template this was cloned from"
)
is_active = models.BooleanField(default=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'igny8_strategies'
ordering = ['name']
verbose_name = 'Strategy'
verbose_name_plural = 'Strategies'
indexes = [
models.Index(fields=['account', 'is_active']),
models.Index(fields=['account', 'sector']),
models.Index(fields=['name']),
models.Index(fields=['is_custom']),
]
def __str__(self):
sector_name = self.sector.name if self.sector else 'Global'
status = "Custom" if self.is_custom else "Template"
return f"{self.name} ({status}) - {sector_name}"