feat(migrations): Rename indexes and update global integration settings fields for improved clarity and functionality
feat(admin): Add API monitoring, debug console, and system health templates for enhanced admin interface docs: Add AI system cleanup summary and audit report detailing architecture, token management, and recommendations docs: Introduce credits and tokens system guide outlining configuration, data flow, and monitoring strategies
This commit is contained in:
@@ -1,3 +1,19 @@
|
||||
"""
|
||||
IGNY8 System Module
|
||||
"""
|
||||
# Avoid circular imports - don't import models at module level
|
||||
# Models are automatically discovered by Django
|
||||
|
||||
__all__ = [
|
||||
# Account-based models
|
||||
'AIPrompt',
|
||||
'IntegrationSettings',
|
||||
'AuthorProfile',
|
||||
'Strategy',
|
||||
# Global settings models
|
||||
'GlobalIntegrationSettings',
|
||||
'AccountIntegrationOverride',
|
||||
'GlobalAIPrompt',
|
||||
'GlobalAuthorProfile',
|
||||
'GlobalStrategy',
|
||||
]
|
||||
|
||||
@@ -5,6 +5,12 @@ from django.contrib import admin
|
||||
from unfold.admin import ModelAdmin
|
||||
from igny8_core.admin.base import AccountAdminMixin, Igny8ModelAdmin
|
||||
from .models import AIPrompt, IntegrationSettings, AuthorProfile, Strategy
|
||||
from .global_settings_models import (
|
||||
GlobalIntegrationSettings,
|
||||
GlobalAIPrompt,
|
||||
GlobalAuthorProfile,
|
||||
GlobalStrategy,
|
||||
)
|
||||
|
||||
from django.contrib import messages
|
||||
from import_export.admin import ExportMixin, ImportExportMixin
|
||||
@@ -52,8 +58,8 @@ except ImportError:
|
||||
@admin.register(AIPrompt)
|
||||
class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
resource_class = AIPromptResource
|
||||
list_display = ['id', 'prompt_type', 'account', 'is_active', 'updated_at']
|
||||
list_filter = ['prompt_type', 'is_active', 'account']
|
||||
list_display = ['id', 'prompt_type', 'account', 'is_customized', 'is_active', 'updated_at']
|
||||
list_filter = ['prompt_type', 'is_active', 'is_customized', 'account']
|
||||
search_fields = ['prompt_type']
|
||||
readonly_fields = ['created_at', 'updated_at', 'default_prompt']
|
||||
actions = [
|
||||
@@ -64,10 +70,11 @@ class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
'fields': ('account', 'prompt_type', 'is_active')
|
||||
'fields': ('account', 'prompt_type', 'is_active', 'is_customized')
|
||||
}),
|
||||
('Prompt Content', {
|
||||
'fields': ('prompt_value', 'default_prompt')
|
||||
'fields': ('prompt_value', 'default_prompt'),
|
||||
'description': 'Customize prompt_value or reset to default_prompt'
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at')
|
||||
@@ -94,14 +101,14 @@ class AIPromptAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
bulk_deactivate.short_description = 'Deactivate selected prompts'
|
||||
|
||||
def bulk_reset_to_default(self, request, queryset):
|
||||
"""Reset selected prompts to their global defaults"""
|
||||
count = 0
|
||||
for prompt in queryset:
|
||||
if prompt.default_prompt:
|
||||
prompt.prompt_value = prompt.default_prompt
|
||||
prompt.save()
|
||||
prompt.reset_to_default()
|
||||
count += 1
|
||||
self.message_user(request, f'{count} AI prompt(s) reset to default values.', messages.SUCCESS)
|
||||
bulk_reset_to_default.short_description = 'Reset to default values'
|
||||
self.message_user(request, f'{count} prompt(s) reset to default.', messages.SUCCESS)
|
||||
bulk_reset_to_default.short_description = 'Reset selected prompts to global default'
|
||||
|
||||
|
||||
class IntegrationSettingsResource(resources.ModelResource):
|
||||
@@ -114,36 +121,42 @@ class IntegrationSettingsResource(resources.ModelResource):
|
||||
|
||||
@admin.register(IntegrationSettings)
|
||||
class IntegrationSettingsAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
"""
|
||||
Admin for per-account integration setting overrides.
|
||||
|
||||
IMPORTANT: This stores ONLY model selection and parameter overrides.
|
||||
API keys come from GlobalIntegrationSettings and cannot be overridden.
|
||||
Free plan users cannot create these - they must use global defaults.
|
||||
"""
|
||||
resource_class = IntegrationSettingsResource
|
||||
list_display = ['id', 'integration_type', 'account', 'is_active', 'updated_at']
|
||||
list_filter = ['integration_type', 'is_active', 'account']
|
||||
search_fields = ['integration_type']
|
||||
search_fields = ['integration_type', 'account__name']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
actions = [
|
||||
'bulk_activate',
|
||||
'bulk_deactivate',
|
||||
'bulk_test_connection',
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
('Basic Info', {
|
||||
'fields': ('account', 'integration_type', 'is_active')
|
||||
}),
|
||||
('Configuration', {
|
||||
('Configuration Overrides', {
|
||||
'fields': ('config',),
|
||||
'description': 'JSON configuration containing API keys and settings. Example: {"apiKey": "sk-...", "model": "gpt-4.1", "enabled": true}'
|
||||
'description': (
|
||||
'JSON overrides for model/parameter selection. '
|
||||
'Fields: model, temperature, max_tokens, image_size, image_quality, etc. '
|
||||
'Leave null to use global defaults. '
|
||||
'Example: {"model": "gpt-4", "temperature": 0.8, "max_tokens": 4000} '
|
||||
'WARNING: NEVER store API keys here - they come from GlobalIntegrationSettings'
|
||||
)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at')
|
||||
}),
|
||||
)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Make config readonly when viewing to prevent accidental exposure"""
|
||||
if obj: # Editing existing object
|
||||
return self.readonly_fields + ['config']
|
||||
return self.readonly_fields
|
||||
|
||||
def get_account_display(self, obj):
|
||||
"""Safely get account name"""
|
||||
try:
|
||||
@@ -312,4 +325,119 @@ class StrategyAdmin(ImportExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||
strategy_copy.save()
|
||||
count += 1
|
||||
self.message_user(request, f'{count} strategy/strategies cloned.', messages.SUCCESS)
|
||||
bulk_clone.short_description = 'Clone selected strategies'
|
||||
bulk_clone.short_description = 'Clone selected strategies'
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GLOBAL SETTINGS ADMIN - Platform-wide defaults
|
||||
# =============================================================================
|
||||
|
||||
@admin.register(GlobalIntegrationSettings)
|
||||
class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
|
||||
"""Admin for global integration settings (singleton)"""
|
||||
list_display = ["id", "is_active", "last_updated", "updated_by"]
|
||||
readonly_fields = ["last_updated"]
|
||||
|
||||
fieldsets = (
|
||||
("OpenAI Settings", {
|
||||
"fields": ("openai_api_key", "openai_model", "openai_temperature", "openai_max_tokens"),
|
||||
"description": "Global OpenAI configuration used by all accounts (unless overridden)"
|
||||
}),
|
||||
("DALL-E Settings", {
|
||||
"fields": ("dalle_api_key", "dalle_model", "dalle_size", "dalle_quality", "dalle_style"),
|
||||
"description": "Global DALL-E image generation configuration"
|
||||
}),
|
||||
("Anthropic Settings", {
|
||||
"fields": ("anthropic_api_key", "anthropic_model"),
|
||||
"description": "Global Anthropic Claude configuration"
|
||||
}),
|
||||
("Runware Settings", {
|
||||
"fields": ("runware_api_key",),
|
||||
"description": "Global Runware image generation configuration"
|
||||
}),
|
||||
("Status", {
|
||||
"fields": ("is_active", "last_updated", "updated_by")
|
||||
}),
|
||||
)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Only allow one instance (singleton pattern)"""
|
||||
return not GlobalIntegrationSettings.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Dont allow deletion of singleton"""
|
||||
return False
|
||||
|
||||
@admin.register(GlobalAIPrompt)
|
||||
class GlobalAIPromptAdmin(ExportMixin, Igny8ModelAdmin):
|
||||
"""Admin for global AI prompt templates"""
|
||||
list_display = ["prompt_type", "version", "is_active", "last_updated"]
|
||||
list_filter = ["is_active", "prompt_type", "version"]
|
||||
search_fields = ["prompt_type", "description"]
|
||||
readonly_fields = ["last_updated", "created_at"]
|
||||
|
||||
fieldsets = (
|
||||
("Basic Info", {
|
||||
"fields": ("prompt_type", "description", "is_active", "version")
|
||||
}),
|
||||
("Prompt Content", {
|
||||
"fields": ("prompt_value", "variables"),
|
||||
"description": "Variables should be a list of variable names used in the prompt"
|
||||
}),
|
||||
("Timestamps", {
|
||||
"fields": ("created_at", "last_updated")
|
||||
}),
|
||||
)
|
||||
|
||||
actions = ["increment_version"]
|
||||
|
||||
def increment_version(self, request, queryset):
|
||||
"""Increment version for selected prompts"""
|
||||
for prompt in queryset:
|
||||
prompt.version += 1
|
||||
prompt.save()
|
||||
self.message_user(request, f"{queryset.count()} prompt(s) version incremented.", messages.SUCCESS)
|
||||
increment_version.short_description = "Increment version"
|
||||
|
||||
|
||||
@admin.register(GlobalAuthorProfile)
|
||||
class GlobalAuthorProfileAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
"""Admin for global author profile templates"""
|
||||
list_display = ["name", "category", "tone", "language", "is_active", "created_at"]
|
||||
list_filter = ["is_active", "category", "tone", "language"]
|
||||
search_fields = ["name", "description"]
|
||||
readonly_fields = ["created_at", "updated_at"]
|
||||
|
||||
fieldsets = (
|
||||
("Basic Info", {
|
||||
"fields": ("name", "description", "category", "is_active")
|
||||
}),
|
||||
("Writing Style", {
|
||||
"fields": ("tone", "language", "structure_template")
|
||||
}),
|
||||
("Timestamps", {
|
||||
"fields": ("created_at", "updated_at")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(GlobalStrategy)
|
||||
class GlobalStrategyAdmin(ImportExportMixin, Igny8ModelAdmin):
|
||||
"""Admin for global strategy templates"""
|
||||
list_display = ["name", "category", "is_active", "created_at"]
|
||||
list_filter = ["is_active", "category"]
|
||||
search_fields = ["name", "description"]
|
||||
readonly_fields = ["created_at", "updated_at"]
|
||||
|
||||
fieldsets = (
|
||||
("Basic Info", {
|
||||
"fields": ("name", "description", "category", "is_active")
|
||||
}),
|
||||
("Strategy Configuration", {
|
||||
"fields": ("prompt_types", "section_logic")
|
||||
}),
|
||||
("Timestamps", {
|
||||
"fields": ("created_at", "updated_at")
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
270
backend/igny8_core/modules/system/global_settings_models.py
Normal file
270
backend/igny8_core/modules/system/global_settings_models.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""
|
||||
Global settings models - Platform-wide defaults
|
||||
These models store system-wide defaults that all accounts use.
|
||||
Accounts can override model selection and parameters (but NOT API keys).
|
||||
"""
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class GlobalIntegrationSettings(models.Model):
|
||||
"""
|
||||
Platform-wide API keys and default integration settings.
|
||||
Singleton pattern - only ONE instance exists (pk=1).
|
||||
|
||||
IMPORTANT:
|
||||
- API keys stored here are used by ALL accounts (no exceptions)
|
||||
- Model selections and parameters are defaults
|
||||
- Accounts can override model/params via IntegrationSettings model
|
||||
- Free plan: Cannot override, must use these defaults
|
||||
- Starter/Growth/Scale: Can override model, temperature, tokens, etc.
|
||||
"""
|
||||
|
||||
# OpenAI Settings (for text generation)
|
||||
openai_api_key = models.CharField(
|
||||
max_length=500,
|
||||
blank=True,
|
||||
help_text="Platform OpenAI API key - used by ALL accounts"
|
||||
)
|
||||
openai_model = models.CharField(
|
||||
max_length=100,
|
||||
default='gpt-4-turbo-preview',
|
||||
help_text="Default text generation model (accounts can override if plan allows)"
|
||||
)
|
||||
openai_temperature = models.FloatField(
|
||||
default=0.7,
|
||||
help_text="Default temperature 0.0-2.0 (accounts can override if plan allows)"
|
||||
)
|
||||
openai_max_tokens = models.IntegerField(
|
||||
default=8192,
|
||||
help_text="Default max tokens for responses (accounts can override if plan allows)"
|
||||
)
|
||||
|
||||
# DALL-E Settings (for image generation)
|
||||
dalle_api_key = models.CharField(
|
||||
max_length=500,
|
||||
blank=True,
|
||||
help_text="Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)"
|
||||
)
|
||||
dalle_model = models.CharField(
|
||||
max_length=100,
|
||||
default='dall-e-3',
|
||||
help_text="Default DALL-E model (accounts can override if plan allows)"
|
||||
)
|
||||
dalle_size = models.CharField(
|
||||
max_length=20,
|
||||
default='1024x1024',
|
||||
help_text="Default image size (accounts can override if plan allows)"
|
||||
)
|
||||
dalle_quality = models.CharField(
|
||||
max_length=20,
|
||||
default='standard',
|
||||
choices=[('standard', 'Standard'), ('hd', 'HD')],
|
||||
help_text="Default image quality (accounts can override if plan allows)"
|
||||
)
|
||||
dalle_style = models.CharField(
|
||||
max_length=20,
|
||||
default='vivid',
|
||||
choices=[('vivid', 'Vivid'), ('natural', 'Natural')],
|
||||
help_text="Default image style (accounts can override if plan allows)"
|
||||
)
|
||||
|
||||
# Anthropic Settings (for Claude)
|
||||
anthropic_api_key = models.CharField(
|
||||
max_length=500,
|
||||
blank=True,
|
||||
help_text="Platform Anthropic API key - used by ALL accounts"
|
||||
)
|
||||
anthropic_model = models.CharField(
|
||||
max_length=100,
|
||||
default='claude-3-sonnet-20240229',
|
||||
help_text="Default Anthropic model (accounts can override if plan allows)"
|
||||
)
|
||||
|
||||
# Runware Settings (alternative image generation)
|
||||
runware_api_key = models.CharField(
|
||||
max_length=500,
|
||||
blank=True,
|
||||
help_text="Platform Runware API key - used by ALL accounts"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
is_active = models.BooleanField(default=True)
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
updated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='global_settings_updates'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_integration_settings'
|
||||
verbose_name = "Global Integration Settings"
|
||||
verbose_name_plural = "Global Integration Settings"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Enforce singleton - always use pk=1
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""Get or create the singleton instance"""
|
||||
obj, created = cls.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
def __str__(self):
|
||||
return "Global Integration Settings"
|
||||
|
||||
|
||||
|
||||
|
||||
class GlobalAIPrompt(models.Model):
|
||||
"""
|
||||
Platform-wide default AI prompt templates.
|
||||
All accounts use these by default. Accounts can save overrides which are stored
|
||||
in the AIPrompt model with the default_prompt field preserving this global 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'),
|
||||
('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,
|
||||
unique=True,
|
||||
help_text="Type of AI operation this prompt is for"
|
||||
)
|
||||
prompt_value = models.TextField(help_text="Default prompt template")
|
||||
description = models.TextField(blank=True, help_text="Description of what this prompt does")
|
||||
variables = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of variables used in the prompt (e.g., {keyword}, {industry})"
|
||||
)
|
||||
is_active = models.BooleanField(default=True, db_index=True)
|
||||
version = models.IntegerField(default=1, help_text="Prompt version for tracking changes")
|
||||
last_updated = models.DateTimeField(auto_now=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_ai_prompts'
|
||||
verbose_name = "Global AI Prompt"
|
||||
verbose_name_plural = "Global AI Prompts"
|
||||
ordering = ['prompt_type']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_prompt_type_display()} (v{self.version})"
|
||||
|
||||
|
||||
class GlobalAuthorProfile(models.Model):
|
||||
"""
|
||||
Platform-wide author persona templates.
|
||||
All accounts can clone these profiles and customize them.
|
||||
"""
|
||||
|
||||
CATEGORY_CHOICES = [
|
||||
('saas', 'SaaS/B2B'),
|
||||
('ecommerce', 'E-commerce'),
|
||||
('blog', 'Blog/Publishing'),
|
||||
('technical', 'Technical'),
|
||||
('creative', 'Creative'),
|
||||
('news', 'News/Media'),
|
||||
('academic', 'Academic'),
|
||||
]
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
help_text="Profile name (e.g., 'SaaS B2B Professional')"
|
||||
)
|
||||
description = models.TextField(help_text="Description of the writing style")
|
||||
tone = models.CharField(
|
||||
max_length=100,
|
||||
help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical')"
|
||||
)
|
||||
language = models.CharField(
|
||||
max_length=50,
|
||||
default='en',
|
||||
help_text="Language code"
|
||||
)
|
||||
structure_template = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Structure template defining content sections"
|
||||
)
|
||||
category = models.CharField(
|
||||
max_length=50,
|
||||
choices=CATEGORY_CHOICES,
|
||||
help_text="Profile category"
|
||||
)
|
||||
is_active = models.BooleanField(default=True, db_index=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_author_profiles'
|
||||
verbose_name = "Global Author Profile"
|
||||
verbose_name_plural = "Global Author Profiles"
|
||||
ordering = ['category', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_category_display()})"
|
||||
|
||||
|
||||
class GlobalStrategy(models.Model):
|
||||
"""
|
||||
Platform-wide content strategy templates.
|
||||
All accounts can clone these strategies and customize them.
|
||||
"""
|
||||
|
||||
CATEGORY_CHOICES = [
|
||||
('blog', 'Blog Content'),
|
||||
('ecommerce', 'E-commerce'),
|
||||
('saas', 'SaaS/B2B'),
|
||||
('news', 'News/Media'),
|
||||
('technical', 'Technical Documentation'),
|
||||
('marketing', 'Marketing Content'),
|
||||
]
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
help_text="Strategy name"
|
||||
)
|
||||
description = models.TextField(help_text="Description of the content strategy")
|
||||
prompt_types = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of prompt types to use"
|
||||
)
|
||||
section_logic = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Section logic configuration"
|
||||
)
|
||||
category = models.CharField(
|
||||
max_length=50,
|
||||
choices=CATEGORY_CHOICES,
|
||||
help_text="Strategy category"
|
||||
)
|
||||
is_active = models.BooleanField(default=True, db_index=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'igny8_global_strategies'
|
||||
verbose_name = "Global Strategy"
|
||||
verbose_name_plural = "Global Strategies"
|
||||
ordering = ['category', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_category_display()})"
|
||||
@@ -10,7 +10,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.throttles import DebugScopedRateThrottle
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper
|
||||
from igny8_core.api.permissions import IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,12 +30,12 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
Following reference plugin pattern: WordPress uses update_option() for igny8_api_settings
|
||||
We store in IntegrationSettings model with account isolation
|
||||
|
||||
IMPORTANT: Integration settings are system-wide (configured by super users/developers)
|
||||
Normal users don't configure their own API keys - they use the system account settings via fallback
|
||||
Integration settings configured through Django admin interface.
|
||||
Normal users can view settings but only Admin/Owner roles can modify.
|
||||
|
||||
NOTE: Class-level permissions are [IsAuthenticatedAndActive, HasTenantAccess] only.
|
||||
Individual actions override with IsSystemAccountOrDeveloper where needed (save, test).
|
||||
task_progress and get_image_generation_settings need to be accessible to all authenticated users.
|
||||
Individual actions override with IsAdminOrOwner where needed (save, test).
|
||||
task_progress and get_image_generation_settings accessible to all authenticated users.
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
|
||||
@@ -46,11 +46,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
Override permissions based on action.
|
||||
- list, retrieve: authenticated users with tenant access (read-only)
|
||||
- update, save, test: system accounts/developers only (write operations)
|
||||
- update, save, test: Admin/Owner roles only (write operations)
|
||||
- task_progress, get_image_generation_settings: all authenticated users
|
||||
"""
|
||||
if self.action in ['update', 'save_post', 'test_connection']:
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper]
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner]
|
||||
else:
|
||||
permission_classes = self.permission_classes
|
||||
return [permission() for permission in permission_classes]
|
||||
@@ -91,7 +91,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
||||
return self.save_settings(request, pk)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='test', url_name='test',
|
||||
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper])
|
||||
permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsAdminOrOwner])
|
||||
def test_connection(self, request, pk=None):
|
||||
"""
|
||||
Test API connection for OpenAI or Runware
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# Generated migration for global settings models
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('igny8_core_auth', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Create GlobalIntegrationSettings
|
||||
migrations.CreateModel(
|
||||
name='GlobalIntegrationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('openai_api_key', models.CharField(blank=True, help_text='Global OpenAI API key used by all accounts (unless overridden)', max_length=500)),
|
||||
('openai_model', models.CharField(default='gpt-4-turbo-preview', help_text='Default OpenAI model for text generation', max_length=100)),
|
||||
('openai_temperature', models.FloatField(default=0.7, help_text='Temperature for OpenAI text generation (0.0 to 2.0)')),
|
||||
('openai_max_tokens', models.IntegerField(default=4000, help_text='Maximum tokens for OpenAI responses')),
|
||||
('dalle_api_key', models.CharField(blank=True, help_text='Global DALL-E API key (can be same as OpenAI key)', max_length=500)),
|
||||
('dalle_model', models.CharField(default='dall-e-3', help_text='DALL-E model version', max_length=100)),
|
||||
('dalle_size', models.CharField(default='1024x1024', help_text='Default image size for DALL-E', max_length=20)),
|
||||
('dalle_quality', models.CharField(choices=[('standard', 'Standard'), ('hd', 'HD')], default='standard', help_text='Image quality for DALL-E 3', max_length=20)),
|
||||
('dalle_style', models.CharField(choices=[('vivid', 'Vivid'), ('natural', 'Natural')], default='vivid', help_text='Image style for DALL-E 3', max_length=20)),
|
||||
('anthropic_api_key', models.CharField(blank=True, help_text='Global Anthropic Claude API key', max_length=500)),
|
||||
('anthropic_model', models.CharField(default='claude-3-sonnet-20240229', help_text='Default Anthropic Claude model', max_length=100)),
|
||||
('runware_api_key', models.CharField(blank=True, help_text='Global Runware API key for image generation', max_length=500)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='global_settings_updates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Integration Settings',
|
||||
'verbose_name_plural': 'Global Integration Settings',
|
||||
'db_table': 'igny8_global_integration_settings',
|
||||
},
|
||||
),
|
||||
|
||||
# Create AccountIntegrationOverride
|
||||
migrations.CreateModel(
|
||||
name='AccountIntegrationOverride',
|
||||
fields=[
|
||||
('account', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='integration_override', serialize=False, to='igny8_core_auth.account')),
|
||||
('use_own_keys', models.BooleanField(default=False, help_text="Use account's own API keys instead of global settings")),
|
||||
('openai_api_key', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('openai_model', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('openai_temperature', models.FloatField(blank=True, null=True)),
|
||||
('openai_max_tokens', models.IntegerField(blank=True, null=True)),
|
||||
('dalle_api_key', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('dalle_model', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('dalle_size', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('dalle_quality', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('dalle_style', models.CharField(blank=True, max_length=20, null=True)),
|
||||
('anthropic_api_key', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('anthropic_model', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('runware_api_key', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Account Integration Override',
|
||||
'verbose_name_plural': 'Account Integration Overrides',
|
||||
'db_table': 'igny8_account_integration_override',
|
||||
},
|
||||
),
|
||||
|
||||
# Create GlobalAIPrompt
|
||||
migrations.CreateModel(
|
||||
name='GlobalAIPrompt',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('prompt_type', models.CharField(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'), ('product_generation', 'Product Content Generation'), ('service_generation', 'Service Page Generation'), ('taxonomy_generation', 'Taxonomy Generation')], help_text='Type of AI operation this prompt is for', max_length=50, unique=True)),
|
||||
('prompt_value', models.TextField(help_text='Default prompt template')),
|
||||
('description', models.TextField(blank=True, help_text='Description of what this prompt does')),
|
||||
('variables', models.JSONField(default=list, help_text='List of variables used in the prompt (e.g., {keyword}, {industry})')),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('version', models.IntegerField(default=1, help_text='Prompt version for tracking changes')),
|
||||
('last_updated', models.DateTimeField(auto_now=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global AI Prompt',
|
||||
'verbose_name_plural': 'Global AI Prompts',
|
||||
'db_table': 'igny8_global_ai_prompts',
|
||||
'ordering': ['prompt_type'],
|
||||
},
|
||||
),
|
||||
|
||||
# Create GlobalAuthorProfile
|
||||
migrations.CreateModel(
|
||||
name='GlobalAuthorProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text="Profile name (e.g., 'SaaS B2B Professional')", max_length=255, unique=True)),
|
||||
('description', models.TextField(help_text='Description of the writing style')),
|
||||
('tone', models.CharField(help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical')", max_length=100)),
|
||||
('language', models.CharField(default='en', help_text='Language code', max_length=50)),
|
||||
('structure_template', models.JSONField(default=dict, help_text='Structure template defining content sections')),
|
||||
('category', models.CharField(choices=[('saas', 'SaaS/B2B'), ('ecommerce', 'E-commerce'), ('blog', 'Blog/Publishing'), ('technical', 'Technical'), ('creative', 'Creative'), ('news', 'News/Media'), ('academic', 'Academic')], help_text='Profile category', max_length=50)),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Author Profile',
|
||||
'verbose_name_plural': 'Global Author Profiles',
|
||||
'db_table': 'igny8_global_author_profiles',
|
||||
'ordering': ['category', 'name'],
|
||||
},
|
||||
),
|
||||
|
||||
# Create GlobalStrategy
|
||||
migrations.CreateModel(
|
||||
name='GlobalStrategy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Strategy name', max_length=255, unique=True)),
|
||||
('description', models.TextField(help_text='Description of the content strategy')),
|
||||
('prompt_types', models.JSONField(default=list, help_text='List of prompt types to use')),
|
||||
('section_logic', models.JSONField(default=dict, help_text='Section logic configuration')),
|
||||
('category', models.CharField(choices=[('blog', 'Blog Content'), ('ecommerce', 'E-commerce'), ('saas', 'SaaS/B2B'), ('news', 'News/Media'), ('technical', 'Technical Documentation'), ('marketing', 'Marketing Content')], help_text='Strategy category', max_length=50)),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Global Strategy',
|
||||
'verbose_name_plural': 'Global Strategies',
|
||||
'db_table': 'igny8_global_strategies',
|
||||
'ordering': ['category', 'name'],
|
||||
},
|
||||
),
|
||||
|
||||
# Update AIPrompt model - remove default_prompt, add is_customized
|
||||
migrations.RemoveField(
|
||||
model_name='aiprompt',
|
||||
name='default_prompt',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='aiprompt',
|
||||
name='is_customized',
|
||||
field=models.BooleanField(default=False, help_text='True if account customized the prompt, False if using global default'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='aiprompt',
|
||||
index=models.Index(fields=['is_customized'], name='igny8_ai_pr_is_cust_idx'),
|
||||
),
|
||||
|
||||
# Update AuthorProfile - add is_custom and cloned_from
|
||||
migrations.AddField(
|
||||
model_name='authorprofile',
|
||||
name='is_custom',
|
||||
field=models.BooleanField(default=False, help_text='True if created by account, False if cloned from global template'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='authorprofile',
|
||||
name='cloned_from',
|
||||
field=models.ForeignKey(blank=True, help_text='Reference to the global template this was cloned from', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cloned_instances', to='system.globalauthorprofile'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='authorprofile',
|
||||
index=models.Index(fields=['is_custom'], name='igny8_autho_is_cust_idx'),
|
||||
),
|
||||
|
||||
# Update Strategy - add is_custom and cloned_from
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='is_custom',
|
||||
field=models.BooleanField(default=False, help_text='True if created by account, False if cloned from global template'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='strategy',
|
||||
name='cloned_from',
|
||||
field=models.ForeignKey(blank=True, help_text='Reference to the global template this was cloned from', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cloned_instances', to='system.globalstrategy'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='strategy',
|
||||
index=models.Index(fields=['is_custom'], name='igny8_strat_is_cust_idx'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,108 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-20 12:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0003_fix_global_settings_architecture'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameIndex(
|
||||
model_name='aiprompt',
|
||||
new_name='igny8_ai_pr_is_cust_5d7a72_idx',
|
||||
old_name='igny8_ai_pr_is_cust_idx',
|
||||
),
|
||||
migrations.RenameIndex(
|
||||
model_name='authorprofile',
|
||||
new_name='igny8_autho_is_cust_d163e6_idx',
|
||||
old_name='igny8_autho_is_cust_idx',
|
||||
),
|
||||
migrations.RenameIndex(
|
||||
model_name='strategy',
|
||||
new_name='igny8_strat_is_cust_4b3c4b_idx',
|
||||
old_name='igny8_strat_is_cust_idx',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aiprompt',
|
||||
name='default_prompt',
|
||||
field=models.TextField(blank=True, help_text='Global default prompt - used for reset to default'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aiprompt',
|
||||
name='prompt_value',
|
||||
field=models.TextField(help_text='Current prompt text (customized or default)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='anthropic_api_key',
|
||||
field=models.CharField(blank=True, help_text='Platform Anthropic API key - used by ALL accounts', max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='anthropic_model',
|
||||
field=models.CharField(default='claude-3-sonnet-20240229', help_text='Default Anthropic model (accounts can override if plan allows)', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_api_key',
|
||||
field=models.CharField(blank=True, help_text='Platform DALL-E API key - used by ALL accounts (can be same as OpenAI)', max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_model',
|
||||
field=models.CharField(default='dall-e-3', help_text='Default DALL-E model (accounts can override if plan allows)', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_quality',
|
||||
field=models.CharField(choices=[('standard', 'Standard'), ('hd', 'HD')], default='standard', help_text='Default image quality (accounts can override if plan allows)', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_size',
|
||||
field=models.CharField(default='1024x1024', help_text='Default image size (accounts can override if plan allows)', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='dalle_style',
|
||||
field=models.CharField(choices=[('vivid', 'Vivid'), ('natural', 'Natural')], default='vivid', help_text='Default image style (accounts can override if plan allows)', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_api_key',
|
||||
field=models.CharField(blank=True, help_text='Platform OpenAI API key - used by ALL accounts', max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_max_tokens',
|
||||
field=models.IntegerField(default=8192, help_text='Default max tokens for responses (accounts can override if plan allows)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_model',
|
||||
field=models.CharField(default='gpt-4-turbo-preview', help_text='Default text generation model (accounts can override if plan allows)', max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='openai_temperature',
|
||||
field=models.FloatField(default=0.7, help_text='Default temperature 0.0-2.0 (accounts can override if plan allows)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='runware_api_key',
|
||||
field=models.CharField(blank=True, help_text='Platform Runware API key - used by ALL accounts', max_length=500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='integrationsettings',
|
||||
name='config',
|
||||
field=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.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='integrationsettings',
|
||||
name='integration_type',
|
||||
field=models.CharField(choices=[('openai', 'OpenAI'), ('dalle', 'DALL-E'), ('anthropic', 'Anthropic'), ('runware', 'Runware')], db_index=True, max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -11,7 +11,12 @@ from .settings_models import (
|
||||
|
||||
|
||||
class AIPrompt(AccountBaseModel):
|
||||
"""AI Prompt templates for various AI operations"""
|
||||
"""
|
||||
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'),
|
||||
@@ -28,8 +33,15 @@ class AIPrompt(AccountBaseModel):
|
||||
]
|
||||
|
||||
prompt_type = models.CharField(max_length=50, choices=PROMPT_TYPE_CHOICES, db_index=True)
|
||||
prompt_value = models.TextField(help_text="The prompt template text")
|
||||
default_prompt = models.TextField(help_text="Default prompt value (for reset)")
|
||||
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)
|
||||
@@ -41,24 +53,77 @@ class AIPrompt(AccountBaseModel):
|
||||
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"""
|
||||
if self.default_prompt:
|
||||
self.prompt_value = self.default_prompt
|
||||
self.is_customized = False
|
||||
self.save()
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_prompt_type_display()}"
|
||||
status = "Custom" if self.is_customized else "Default"
|
||||
return f"{self.get_prompt_type_display()} ({status})"
|
||||
|
||||
|
||||
class IntegrationSettings(AccountBaseModel):
|
||||
"""Integration settings for OpenAI, Runware, GSC, etc."""
|
||||
"""
|
||||
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'),
|
||||
('gsc', 'Google Search Console'),
|
||||
('image_generation', 'Image Generation Service'),
|
||||
]
|
||||
|
||||
integration_type = models.CharField(max_length=50, choices=INTEGRATION_TYPE_CHOICES, db_index=True)
|
||||
config = models.JSONField(default=dict, help_text="Integration configuration (API keys, settings, etc.)")
|
||||
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)
|
||||
@@ -80,6 +145,7 @@ class IntegrationSettings(AccountBaseModel):
|
||||
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')")
|
||||
@@ -93,6 +159,18 @@ class AuthorProfile(AccountBaseModel):
|
||||
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)
|
||||
@@ -105,16 +183,19 @@ class AuthorProfile(AccountBaseModel):
|
||||
indexes = [
|
||||
models.Index(fields=['account', 'is_active']),
|
||||
models.Index(fields=['name']),
|
||||
models.Index(fields=['is_custom']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
account = getattr(self, 'account', None)
|
||||
return f"{self.name} ({account.name if account else 'No Account'})"
|
||||
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")
|
||||
@@ -135,6 +216,18 @@ class Strategy(AccountBaseModel):
|
||||
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)
|
||||
@@ -148,8 +241,10 @@ class Strategy(AccountBaseModel):
|
||||
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'
|
||||
return f"{self.name} ({sector_name})"
|
||||
status = "Custom" if self.is_custom else "Template"
|
||||
return f"{self.name} ({status}) - {sector_name}"
|
||||
|
||||
Reference in New Issue
Block a user