Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
"""
IGNY8 System Module
"""

View File

@@ -0,0 +1,164 @@
"""
System Module Admin
"""
from django.contrib import admin
from igny8_core.admin.base import AccountAdminMixin
from .models import AIPrompt, IntegrationSettings, AuthorProfile, Strategy
# Import settings admin
from .settings_admin import (
SystemSettingsAdmin, AccountSettingsAdmin, UserSettingsAdmin,
ModuleSettingsAdmin, AISettingsAdmin
)
try:
from .models import SystemLog, SystemStatus
@admin.register(SystemLog)
class SystemLogAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['id', 'account', 'module', 'level', 'action', 'message', 'created_at']
list_filter = ['module', 'level', 'created_at', 'account']
search_fields = ['message', 'action']
readonly_fields = ['created_at', 'updated_at']
date_hierarchy = 'created_at'
@admin.register(SystemStatus)
class SystemStatusAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['component', 'account', 'status', 'message', 'last_check']
list_filter = ['status', 'component', 'account']
search_fields = ['component', 'message']
readonly_fields = ['last_check']
except ImportError:
pass
@admin.register(AIPrompt)
class AIPromptAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['id', 'prompt_type', 'account', 'is_active', 'updated_at']
list_filter = ['prompt_type', 'is_active', 'account']
search_fields = ['prompt_type']
readonly_fields = ['created_at', 'updated_at', 'default_prompt']
fieldsets = (
('Basic Info', {
'fields': ('account', 'prompt_type', 'is_active')
}),
('Prompt Content', {
'fields': ('prompt_value', 'default_prompt')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at')
}),
)
def get_account_display(self, obj):
"""Safely get account name"""
try:
account = getattr(obj, 'account', None)
return account.name if account else '-'
except:
return '-'
get_account_display.short_description = 'Account'
@admin.register(IntegrationSettings)
class IntegrationSettingsAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['id', 'integration_type', 'account', 'is_active', 'updated_at']
list_filter = ['integration_type', 'is_active', 'account']
search_fields = ['integration_type']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Basic Info', {
'fields': ('account', 'integration_type', 'is_active')
}),
('Configuration', {
'fields': ('config',),
'description': 'JSON configuration containing API keys and settings. Example: {"apiKey": "sk-...", "model": "gpt-4.1", "enabled": true}'
}),
('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:
account = getattr(obj, 'account', None)
return account.name if account else '-'
except:
return '-'
get_account_display.short_description = 'Account'
@admin.register(AuthorProfile)
class AuthorProfileAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['name', 'account', 'tone', 'language', 'is_active', 'created_at']
list_filter = ['is_active', 'tone', 'language', 'account']
search_fields = ['name', 'description', 'tone']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Basic Info', {
'fields': ('account', 'name', 'description', 'is_active')
}),
('Writing Style', {
'fields': ('tone', 'language', 'structure_template')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at')
}),
)
def get_account_display(self, obj):
"""Safely get account name"""
try:
account = getattr(obj, 'account', None)
return account.name if account else '-'
except:
return '-'
get_account_display.short_description = 'Account'
@admin.register(Strategy)
class StrategyAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['name', 'account', 'sector', 'is_active', 'created_at']
list_filter = ['is_active', 'account']
search_fields = ['name', 'description']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Basic Info', {
'fields': ('account', 'name', 'description', 'sector', 'is_active')
}),
('Strategy Configuration', {
'fields': ('prompt_types', 'section_logic')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at')
}),
)
def get_account_display(self, obj):
"""Safely get account name"""
try:
account = getattr(obj, 'account', None)
return account.name if account else '-'
except:
return '-'
get_account_display.short_description = 'Account'
def get_sector_display(self, obj):
"""Safely get sector name"""
try:
return obj.sector.name if obj.sector else 'Global'
except:
return 'Global'
get_sector_display.short_description = 'Sector'

View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class SystemConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'igny8_core.modules.system'
verbose_name = 'System Configuration'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
# Generated by Django 5.2.7 on 2025-11-02 21:42
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('igny8_core_auth', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='SystemLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('module', models.CharField(choices=[('planner', 'Planner'), ('writer', 'Writer'), ('thinker', 'Thinker'), ('ai', 'AI Pipeline'), ('wp_bridge', 'WordPress Bridge'), ('system', 'System')], db_index=True, max_length=50)),
('level', models.CharField(choices=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error'), ('success', 'Success')], db_index=True, default='info', max_length=20)),
('action', models.CharField(max_length=255)),
('message', models.TextField()),
('metadata', models.JSONField(blank=True, default=dict)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'igny8_system_logs',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['tenant', 'module', 'level', '-created_at'], name='igny8_syste_tenant__1a6cda_idx')],
},
),
migrations.CreateModel(
name='SystemStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('component', models.CharField(db_index=True, max_length=100)),
('status', models.CharField(choices=[('healthy', 'Healthy'), ('warning', 'Warning'), ('error', 'Error'), ('maintenance', 'Maintenance')], default='healthy', max_length=20)),
('message', models.TextField(blank=True)),
('last_check', models.DateTimeField(auto_now=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_system_status',
'indexes': [models.Index(fields=['tenant', 'status'], name='igny8_syste_tenant__0e1889_idx')],
'unique_together': {('tenant', 'component')},
},
),
]

View File

@@ -0,0 +1,61 @@
# Generated migration for IntegrationSettings and AIPrompt models
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0001_initial'),
('system', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AIPrompt',
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_template', 'Image Prompt Template'), ('negative_prompt', 'Negative Prompt')], db_index=True, max_length=50)),
('prompt_value', models.TextField(help_text='The prompt template text')),
('default_prompt', models.TextField(help_text='Default prompt value (for reset)')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_ai_prompts',
'ordering': ['prompt_type'],
'unique_together': {('tenant', 'prompt_type')},
'indexes': [
models.Index(fields=['prompt_type'], name='igny8_ai_pr_prompt__idx'),
models.Index(fields=['tenant', 'prompt_type'], name='igny8_ai_pr_tenant__idx'),
],
},
),
migrations.CreateModel(
name='IntegrationSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('integration_type', models.CharField(choices=[('openai', 'OpenAI'), ('runware', 'Runware'), ('gsc', 'Google Search Console')], db_index=True, max_length=50)),
('config', models.JSONField(default=dict, help_text='Integration configuration (API keys, settings, etc.)')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'db_table': 'igny8_integration_settings',
'ordering': ['integration_type'],
'unique_together': {('tenant', 'integration_type')},
'indexes': [
models.Index(fields=['integration_type'], name='igny8_integ_integra_idx'),
models.Index(fields=['tenant', 'integration_type'], name='igny8_integ_tenant__idx'),
],
},
),
]

View File

@@ -0,0 +1,28 @@
# Generated migration to add image_generation integration type
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0002_integration_settings_ai_prompts'),
]
operations = [
migrations.AlterField(
model_name='integrationsettings',
name='integration_type',
field=models.CharField(
choices=[
('openai', 'OpenAI'),
('runware', 'Runware'),
('gsc', 'Google Search Console'),
('image_generation', 'Image Generation Service'),
],
db_index=True,
max_length=50
),
),
]

View File

@@ -0,0 +1,200 @@
# Generated by Django 5.2.8 on 2025-11-07 10:06
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0008_passwordresettoken_alter_industry_options_and_more'),
('system', '0003_add_image_generation_integration_type'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AISettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('integration_type', models.CharField(db_index=True, help_text="Integration type (e.g., 'openai', 'runware')", max_length=50)),
('config', models.JSONField(default=dict, help_text='Integration configuration (API keys, settings, etc.)')),
('model_preferences', models.JSONField(default=dict, help_text='Model preferences per operation type')),
('cost_limits', models.JSONField(default=dict, help_text='Cost limits and budgets')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'igny8_ai_settings',
'ordering': ['integration_type'],
},
),
migrations.CreateModel(
name='ModuleSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config', models.JSONField(default=dict, help_text='Settings configuration as JSON')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('module_name', models.CharField(db_index=True, help_text="Module name (e.g., 'planner', 'writer')", max_length=100)),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
],
options={
'db_table': 'igny8_module_settings',
'ordering': ['module_name', 'key'],
},
),
migrations.CreateModel(
name='SystemSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255, unique=True)),
('value', models.JSONField(default=dict, help_text='Settings value as JSON')),
('description', models.TextField(blank=True, help_text='Description of this setting')),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'igny8_system_settings',
'ordering': ['key'],
},
),
migrations.CreateModel(
name='TenantSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config', models.JSONField(default=dict, help_text='Settings configuration as JSON')),
('is_active', models.BooleanField(default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
],
options={
'db_table': 'igny8_tenant_settings',
'ordering': ['key'],
},
),
migrations.CreateModel(
name='UserSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(db_index=True, help_text='Settings key identifier', max_length=255)),
('value', models.JSONField(default=dict, help_text='Settings value as JSON')),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'igny8_user_settings',
'ordering': ['key'],
},
),
migrations.RenameIndex(
model_name='aiprompt',
new_name='igny8_ai_pr_prompt__4b2dbe_idx',
old_name='igny8_ai_pr_prompt__idx',
),
migrations.RenameIndex(
model_name='aiprompt',
new_name='igny8_ai_pr_tenant__9e7b95_idx',
old_name='igny8_ai_pr_tenant__idx',
),
migrations.RenameIndex(
model_name='integrationsettings',
new_name='igny8_integ_integra_5e382e_idx',
old_name='igny8_integ_integra_idx',
),
migrations.RenameIndex(
model_name='integrationsettings',
new_name='igny8_integ_tenant__5da472_idx',
old_name='igny8_integ_tenant__idx',
),
migrations.AlterField(
model_name='aiprompt',
name='prompt_type',
field=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')], db_index=True, max_length=50),
),
migrations.AddField(
model_name='aisettings',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant'),
),
migrations.AddField(
model_name='modulesettings',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant'),
),
migrations.AddIndex(
model_name='systemsettings',
index=models.Index(fields=['key'], name='igny8_syste_key_20500b_idx'),
),
migrations.AddField(
model_name='tenantsettings',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant'),
),
migrations.AddField(
model_name='usersettings',
name='tenant',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_settings', to='igny8_core_auth.tenant'),
),
migrations.AddField(
model_name='usersettings',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_settings', to=settings.AUTH_USER_MODEL),
),
migrations.RunSQL(
sql="DROP TABLE IF EXISTS igny8_system_logs CASCADE",
reverse_sql=migrations.RunSQL.noop,
),
migrations.RunSQL(
sql="DROP TABLE IF EXISTS igny8_system_status CASCADE",
reverse_sql=migrations.RunSQL.noop,
),
migrations.AddIndex(
model_name='aisettings',
index=models.Index(fields=['integration_type'], name='igny8_ai_se_integra_4f0b21_idx'),
),
migrations.AddIndex(
model_name='aisettings',
index=models.Index(fields=['tenant', 'integration_type'], name='igny8_ai_se_tenant__05ae98_idx'),
),
migrations.AlterUniqueTogether(
name='aisettings',
unique_together={('tenant', 'integration_type')},
),
migrations.AddIndex(
model_name='modulesettings',
index=models.Index(fields=['tenant', 'module_name', 'key'], name='igny8_modul_tenant__21ee25_idx'),
),
migrations.AddIndex(
model_name='modulesettings',
index=models.Index(fields=['module_name', 'key'], name='igny8_modul_module__95373a_idx'),
),
migrations.AlterUniqueTogether(
name='modulesettings',
unique_together={('tenant', 'module_name', 'key')},
),
migrations.AddIndex(
model_name='tenantsettings',
index=models.Index(fields=['tenant', 'key'], name='igny8_tenan_tenant__8ce0b3_idx'),
),
migrations.AlterUniqueTogether(
name='tenantsettings',
unique_together={('tenant', 'key')},
),
migrations.AddIndex(
model_name='usersettings',
index=models.Index(fields=['user', 'tenant', 'key'], name='igny8_user__user_id_ac09d9_idx'),
),
migrations.AddIndex(
model_name='usersettings',
index=models.Index(fields=['tenant', 'key'], name='igny8_user__tenant__01033d_idx'),
),
migrations.AlterUniqueTogether(
name='usersettings',
unique_together={('user', 'tenant', 'key')},
),
]

View File

@@ -0,0 +1,77 @@
# Generated by Django 5.2.8 on 2025-11-07 11:34
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0010_add_seed_keyword'),
('system', '0004_aisettings_modulesettings_systemsettings_and_more'),
]
operations = [
migrations.CreateModel(
name='AuthorProfile',
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 Informative')", max_length=255)),
('description', models.TextField(blank=True, help_text='Description of the writing style')),
('tone', models.CharField(help_text="Writing tone (e.g., 'Professional', 'Casual', 'Technical', 'Conversational')", max_length=100)),
('language', models.CharField(default='en', help_text="Language code (e.g., 'en', 'es', 'fr')", max_length=50)),
('structure_template', models.JSONField(default=dict, help_text='Structure template defining content sections and their order')),
('is_active', models.BooleanField(db_index=True, default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'verbose_name': 'Author Profile',
'verbose_name_plural': 'Author Profiles',
'db_table': 'igny8_author_profiles',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Strategy',
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)),
('description', models.TextField(blank=True, help_text='Description of the content strategy')),
('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_active', models.BooleanField(db_index=True, default=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('sector', models.ForeignKey(blank=True, help_text='Optional: Link strategy to a specific sector', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='strategies', to='igny8_core_auth.sector')),
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.tenant')),
],
options={
'verbose_name': 'Strategy',
'verbose_name_plural': 'Strategies',
'db_table': 'igny8_strategies',
'ordering': ['name'],
},
),
migrations.AddIndex(
model_name='authorprofile',
index=models.Index(fields=['tenant', 'is_active'], name='igny8_autho_tenant__97d2c2_idx'),
),
migrations.AddIndex(
model_name='authorprofile',
index=models.Index(fields=['name'], name='igny8_autho_name_8295f3_idx'),
),
migrations.AddIndex(
model_name='strategy',
index=models.Index(fields=['tenant', 'is_active'], name='igny8_strat_tenant__344de9_idx'),
),
migrations.AddIndex(
model_name='strategy',
index=models.Index(fields=['tenant', 'sector'], name='igny8_strat_tenant__279cfa_idx'),
),
migrations.AddIndex(
model_name='strategy',
index=models.Index(fields=['name'], name='igny8_strat_name_8fe823_idx'),
),
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 5.2.8 on 2025-11-07 14:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('system', '0005_add_author_profile_strategy'),
]
operations = [
migrations.AlterUniqueTogether(
name='systemstatus',
unique_together=None,
),
migrations.RemoveField(
model_name='systemstatus',
name='tenant',
),
migrations.DeleteModel(
name='SystemLog',
),
migrations.DeleteModel(
name='SystemStatus',
),
]

View File

@@ -0,0 +1,150 @@
"""
System module models - for global settings and prompts
"""
from django.db import models
from igny8_core.auth.models import AccountBaseModel
# Import settings models
from .settings_models import (
SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
)
class AIPrompt(AccountBaseModel):
"""AI Prompt templates for various AI operations"""
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'),
]
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)")
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']),
]
def __str__(self):
return f"{self.get_prompt_type_display()}"
class IntegrationSettings(AccountBaseModel):
"""Integration settings for OpenAI, Runware, GSC, etc."""
INTEGRATION_TYPE_CHOICES = [
('openai', 'OpenAI'),
('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.)")
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.
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_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']),
]
def __str__(self):
account = getattr(self, 'account', None)
return f"{self.name} ({account.name if account else 'No Account'})"
class Strategy(AccountBaseModel):
"""
Defined content strategies per sector, integrating prompt types, section logic, etc.
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_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']),
]
def __str__(self):
sector_name = self.sector.name if self.sector else 'Global'
return f"{self.name} ({sector_name})"

View File

@@ -0,0 +1,56 @@
"""
JSON Schemas for Settings Validation
"""
SETTINGS_SCHEMAS = {
'table_settings': {
"type": "object",
"properties": {
"records_per_page": {"type": "integer", "minimum": 5, "maximum": 100},
"default_sort": {"type": "string"},
"default_sort_direction": {"type": "string", "enum": ["asc", "desc"]}
},
"required": ["records_per_page"]
},
'planner_automation': {
"type": "object",
"properties": {
"auto_cluster_enabled": {"type": "boolean"},
"auto_ideas_enabled": {"type": "boolean"},
"max_keywords_per_cluster": {"type": "integer", "minimum": 1, "maximum": 50}
}
},
'writer_automation': {
"type": "object",
"properties": {
"auto_generate_content_enabled": {"type": "boolean"},
"auto_generate_images_enabled": {"type": "boolean"},
"auto_publish_enabled": {"type": "boolean"}
}
},
'ai_settings': {
"type": "object",
"properties": {
"default_model": {"type": "string"},
"temperature": {"type": "number", "minimum": 0, "maximum": 2},
"max_tokens": {"type": "integer", "minimum": 1, "maximum": 32000}
}
},
'user_preferences': {
"type": "object",
"properties": {
"theme": {"type": "string", "enum": ["light", "dark", "auto"]},
"language": {"type": "string"},
"notifications_enabled": {"type": "boolean"}
}
}
}
# Map settings keys to their schemas
SETTINGS_KEY_SCHEMA_MAP = {
'table_settings': 'table_settings',
'planner_automation': 'planner_automation',
'writer_automation': 'writer_automation',
'ai_settings': 'ai_settings',
'user_preferences': 'user_preferences',
}

View File

@@ -0,0 +1,53 @@
"""
System module serializers
"""
from rest_framework import serializers
from .models import AIPrompt, AuthorProfile, Strategy
class AIPromptSerializer(serializers.ModelSerializer):
"""Serializer for AI Prompts"""
prompt_type_display = serializers.CharField(source='get_prompt_type_display', read_only=True)
class Meta:
model = AIPrompt
fields = [
'id',
'prompt_type',
'prompt_type_display',
'prompt_value',
'default_prompt',
'is_active',
'updated_at',
'created_at',
]
read_only_fields = ['id', 'created_at', 'updated_at', 'default_prompt']
class AuthorProfileSerializer(serializers.ModelSerializer):
"""Serializer for AuthorProfile"""
class Meta:
model = AuthorProfile
fields = [
'id', 'name', 'description', 'tone', 'language',
'structure_template', 'is_active',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']
class StrategySerializer(serializers.ModelSerializer):
"""Serializer for Strategy"""
sector_name = serializers.CharField(source='sector.name', read_only=True)
class Meta:
model = Strategy
fields = [
'id', 'name', 'description', 'sector', 'sector_name',
'prompt_types', 'section_logic', 'is_active',
'created_at', 'updated_at'
]
read_only_fields = ['id', 'created_at', 'updated_at']

View File

@@ -0,0 +1,93 @@
"""
Settings Models Admin
"""
from django.contrib import admin
from igny8_core.admin.base import AccountAdminMixin
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
@admin.register(SystemSettings)
class SystemSettingsAdmin(admin.ModelAdmin):
"""SystemSettings - Global, no account filtering"""
list_display = ['key', 'description', 'updated_at']
search_fields = ['key', 'description']
readonly_fields = ['created_at', 'updated_at']
@admin.register(AccountSettings)
class AccountSettingsAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['account', 'key', 'is_active', 'updated_at']
list_filter = ['is_active', 'account']
search_fields = ['key', 'account__name']
readonly_fields = ['created_at', 'updated_at']
def get_account_display(self, obj):
"""Safely get account name"""
try:
account = getattr(obj, 'account', None)
return account.name if account else '-'
except:
return '-'
get_account_display.short_description = 'Account'
@admin.register(UserSettings)
class UserSettingsAdmin(admin.ModelAdmin):
list_display = ['user', 'account', 'key', 'updated_at']
list_filter = ['account']
search_fields = ['key', 'user__email', 'account__name']
readonly_fields = ['created_at', 'updated_at']
def get_queryset(self, request):
"""Filter by account for non-superusers"""
qs = super().get_queryset(request)
if request.user.is_superuser or (hasattr(request.user, 'is_developer') and request.user.is_developer()):
return qs
user_account = getattr(request.user, 'account', None)
if user_account:
return qs.filter(account=user_account)
return qs.none()
def get_account_display(self, obj):
"""Safely get account name"""
try:
account = getattr(obj, 'account', None)
return account.name if account else '-'
except:
return '-'
get_account_display.short_description = 'Account'
@admin.register(ModuleSettings)
class ModuleSettingsAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['account', 'module_name', 'key', 'is_active', 'updated_at']
list_filter = ['module_name', 'is_active', 'account']
search_fields = ['key', 'module_name', 'account__name']
readonly_fields = ['created_at', 'updated_at']
def get_account_display(self, obj):
"""Safely get account name"""
try:
account = getattr(obj, 'account', None)
return account.name if account else '-'
except:
return '-'
get_account_display.short_description = 'Account'
@admin.register(AISettings)
class AISettingsAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['account', 'integration_type', 'is_active', 'updated_at']
list_filter = ['integration_type', 'is_active', 'account']
search_fields = ['integration_type', 'account__name']
readonly_fields = ['created_at', 'updated_at']
def get_account_display(self, obj):
"""Safely get account name"""
try:
account = getattr(obj, 'account', None)
return account.name if account else '-'
except:
return '-'
get_account_display.short_description = 'Account'

View File

@@ -0,0 +1,133 @@
"""
Settings Models for System, Account, User, Module, and AI Settings
"""
from django.db import models
from igny8_core.auth.models import AccountBaseModel
class BaseSettings(AccountBaseModel):
"""Base class for all account-scoped settings models"""
config = models.JSONField(default=dict, help_text="Settings configuration as JSON")
is_active = models.BooleanField(default=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class SystemSettings(models.Model):
"""Global system-wide settings (no account scope)"""
key = models.CharField(max_length=255, unique=True, db_index=True, help_text="Settings key identifier")
value = models.JSONField(default=dict, help_text="Settings value as JSON")
description = models.TextField(blank=True, help_text="Description of this setting")
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'igny8_system_settings'
ordering = ['key']
indexes = [
models.Index(fields=['key']),
]
def __str__(self):
return f"SystemSetting: {self.key}"
class AccountSettings(BaseSettings):
"""Account-level settings"""
key = models.CharField(max_length=255, db_index=True, help_text="Settings key identifier")
class Meta:
db_table = 'igny8_account_settings'
unique_together = [['account', 'key']]
indexes = [
models.Index(fields=['account', 'key']),
]
ordering = ['key']
def __str__(self):
account = getattr(self, 'account', None)
return f"AccountSetting: {account.name if account else 'No Account'} - {self.key}"
class UserSettings(models.Model):
"""User-level settings"""
user = models.ForeignKey('igny8_core_auth.User', on_delete=models.CASCADE, related_name='user_settings')
account = models.ForeignKey('igny8_core_auth.Account', on_delete=models.CASCADE, related_name='user_settings', db_column='tenant_id')
key = models.CharField(max_length=255, db_index=True, help_text="Settings key identifier")
value = models.JSONField(default=dict, help_text="Settings value as JSON")
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'igny8_user_settings'
unique_together = [['user', 'account', 'key']]
indexes = [
models.Index(fields=['user', 'account', 'key']),
models.Index(fields=['account', 'key']),
]
ordering = ['key']
def __str__(self):
return f"UserSetting: {self.user.email} - {self.key}"
class ModuleSettings(BaseSettings):
"""Module-specific settings"""
module_name = models.CharField(max_length=100, db_index=True, help_text="Module name (e.g., 'planner', 'writer')")
key = models.CharField(max_length=255, db_index=True, help_text="Settings key identifier")
class Meta:
db_table = 'igny8_module_settings'
unique_together = [['account', 'module_name', 'key']]
indexes = [
models.Index(fields=['account', 'module_name', 'key']),
models.Index(fields=['module_name', 'key']),
]
ordering = ['module_name', 'key']
def __str__(self):
return f"ModuleSetting: {self.module_name} - {self.key}"
# AISettings extends IntegrationSettings (which already exists)
# We'll create it as a separate model that can reference IntegrationSettings
class AISettings(AccountBaseModel):
"""AI-specific settings extending IntegrationSettings pattern"""
integration_type = models.CharField(
max_length=50,
db_index=True,
help_text="Integration type (e.g., 'openai', 'runware')"
)
config = models.JSONField(
default=dict,
help_text="Integration configuration (API keys, settings, etc.)"
)
model_preferences = models.JSONField(
default=dict,
help_text="Model preferences per operation type"
)
cost_limits = models.JSONField(
default=dict,
help_text="Cost limits and budgets"
)
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_settings'
unique_together = [['account', 'integration_type']]
indexes = [
models.Index(fields=['integration_type']),
models.Index(fields=['account', 'integration_type']),
]
ordering = ['integration_type']
def __str__(self):
account = getattr(self, 'account', None)
account_name = account.name if account else 'No Account'
return f"AISettings: {self.integration_type} - {account_name}"

View File

@@ -0,0 +1,69 @@
"""
Serializers for Settings Models
"""
from rest_framework import serializers
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
from .validators import validate_settings_schema
class SystemSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = SystemSettings
fields = ['id', 'key', 'value', 'description', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
def validate_value(self, value):
"""Validate value against schema if schema exists"""
if self.instance:
validate_settings_schema(self.instance.key, value)
return value
class AccountSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = AccountSettings
fields = ['id', 'key', 'config', 'is_active', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at', 'account']
def validate_config(self, value):
"""Validate config against schema if schema exists"""
if self.instance:
validate_settings_schema(self.instance.key, value)
return value
class UserSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = UserSettings
fields = ['id', 'key', 'value', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at', 'user', 'account']
def validate_value(self, value):
"""Validate value against schema if schema exists"""
if self.instance:
validate_settings_schema(self.instance.key, value)
return value
class ModuleSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = ModuleSettings
fields = ['id', 'module_name', 'key', 'config', 'is_active', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at', 'account']
def validate_config(self, value):
"""Validate config against schema if schema exists"""
if self.instance:
validate_settings_schema(self.instance.key, value)
return value
class AISettingsSerializer(serializers.ModelSerializer):
class Meta:
model = AISettings
fields = [
'id', 'integration_type', 'config', 'model_preferences',
'cost_limits', 'is_active', 'created_at', 'updated_at'
]
read_only_fields = ['created_at', 'updated_at', 'account']

View File

@@ -0,0 +1,265 @@
"""
ViewSets for Settings Models
"""
from rest_framework import viewsets, status, permissions
from rest_framework.decorators import action
from rest_framework.response import Response
from django.db import transaction
from igny8_core.api.base import AccountModelViewSet
from igny8_core.api.authentication import JWTAuthentication, CSRFExemptSessionAuthentication
from .settings_models import SystemSettings, AccountSettings, UserSettings, ModuleSettings, AISettings
from .settings_serializers import (
SystemSettingsSerializer, AccountSettingsSerializer, UserSettingsSerializer,
ModuleSettingsSerializer, AISettingsSerializer
)
class SystemSettingsViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing system-wide settings (admin only for write operations)
"""
queryset = SystemSettings.objects.all()
serializer_class = SystemSettingsSerializer
permission_classes = [permissions.IsAuthenticated] # Require authentication
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def get_permissions(self):
"""Admin only for write operations, read for authenticated users"""
if self.action in ['create', 'update', 'partial_update', 'destroy']:
return [permissions.IsAdminUser()]
return [permissions.IsAuthenticated()]
def get_queryset(self):
"""Get all system settings"""
return SystemSettings.objects.all().order_by('key')
def retrieve(self, request, pk=None):
"""Get setting by key (pk can be key string)"""
try:
# Try to get by ID first
setting = self.get_object()
except:
# Try to get by key
try:
setting = SystemSettings.objects.get(key=pk)
except SystemSettings.DoesNotExist:
return Response(
{'error': 'Setting not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = self.get_serializer(setting)
return Response(serializer.data)
class AccountSettingsViewSet(AccountModelViewSet):
"""
ViewSet for managing account-level settings
"""
queryset = AccountSettings.objects.all()
serializer_class = AccountSettingsSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def get_queryset(self):
"""Get settings for current account"""
return super().get_queryset().order_by('key')
def retrieve(self, request, pk=None):
"""Get setting by key (pk can be key string)"""
queryset = self.get_queryset()
try:
# Try to get by ID first
setting = queryset.get(pk=pk)
except:
# Try to get by key
try:
setting = queryset.get(key=pk)
except AccountSettings.DoesNotExist:
return Response(
{'error': 'Setting not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = self.get_serializer(setting)
return Response(serializer.data)
def perform_create(self, serializer):
"""Set account automatically"""
account = getattr(self.request, 'account', None)
if not account:
user = getattr(self.request, 'user', None)
if user:
account = getattr(user, 'account', None)
if not account:
from rest_framework.exceptions import ValidationError
raise ValidationError("Account is required")
serializer.save(account=account)
class UserSettingsViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing user-level settings
"""
queryset = UserSettings.objects.all()
serializer_class = UserSettingsSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def get_queryset(self):
"""Get settings for current user and account"""
user = self.request.user
account = getattr(self.request, 'account', None)
if not account:
account = getattr(user, 'account', None)
if account:
return UserSettings.objects.filter(user=user, account=account).order_by('key')
return UserSettings.objects.none()
def retrieve(self, request, pk=None):
"""Get setting by key (pk can be key string)"""
queryset = self.get_queryset()
try:
# Try to get by ID first
setting = queryset.get(pk=pk)
except:
# Try to get by key
try:
setting = queryset.get(key=pk)
except UserSettings.DoesNotExist:
return Response(
{'error': 'Setting not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = self.get_serializer(setting)
return Response(serializer.data)
def perform_create(self, serializer):
"""Set user and account automatically"""
user = self.request.user
account = getattr(self.request, 'account', None)
if not account:
account = getattr(user, 'account', None)
if not account:
from rest_framework.exceptions import ValidationError
raise ValidationError("Account is required")
serializer.save(user=user, account=account)
class ModuleSettingsViewSet(AccountModelViewSet):
"""
ViewSet for managing module-specific settings
"""
queryset = ModuleSettings.objects.all()
serializer_class = ModuleSettingsSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def get_queryset(self):
"""Get settings for current account, optionally filtered by module"""
queryset = super().get_queryset()
module_name = self.request.query_params.get('module_name')
if module_name:
queryset = queryset.filter(module_name=module_name)
return queryset.order_by('module_name', 'key')
@action(detail=False, methods=['get'], url_path='module/(?P<module_name>[^/.]+)', url_name='by_module')
def by_module(self, request, module_name=None):
"""Get all settings for a specific module"""
queryset = self.get_queryset().filter(module_name=module_name)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
"""Get setting by key (pk can be key string)"""
queryset = self.get_queryset()
try:
# Try to get by ID first
setting = queryset.get(pk=pk)
except:
# Try to get by module_name and key
module_name = request.query_params.get('module_name')
if module_name:
try:
setting = queryset.get(module_name=module_name, key=pk)
except ModuleSettings.DoesNotExist:
return Response(
{'error': 'Setting not found'},
status=status.HTTP_404_NOT_FOUND
)
else:
return Response(
{'error': 'Setting not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = self.get_serializer(setting)
return Response(serializer.data)
def perform_create(self, serializer):
"""Set account automatically"""
account = getattr(self.request, 'account', None)
if not account:
user = getattr(self.request, 'user', None)
if user:
account = getattr(user, 'account', None)
if not account:
from rest_framework.exceptions import ValidationError
raise ValidationError("Account is required")
serializer.save(account=account)
class AISettingsViewSet(AccountModelViewSet):
"""
ViewSet for managing AI-specific settings
"""
queryset = AISettings.objects.all()
serializer_class = AISettingsSerializer
permission_classes = [permissions.IsAuthenticated]
authentication_classes = [JWTAuthentication, CSRFExemptSessionAuthentication]
def get_queryset(self):
"""Get AI settings for current account"""
return super().get_queryset().order_by('integration_type')
def retrieve(self, request, pk=None):
"""Get setting by integration_type (pk can be integration_type string)"""
queryset = self.get_queryset()
try:
# Try to get by ID first
setting = queryset.get(pk=pk)
except:
# Try to get by integration_type
try:
setting = queryset.get(integration_type=pk)
except AISettings.DoesNotExist:
return Response(
{'error': 'AI Setting not found'},
status=status.HTTP_404_NOT_FOUND
)
serializer = self.get_serializer(setting)
return Response(serializer.data)
def perform_create(self, serializer):
"""Set account automatically"""
account = getattr(self.request, 'account', None)
if not account:
user = getattr(self.request, 'user', None)
if user:
account = getattr(user, 'account', None)
if not account:
from rest_framework.exceptions import ValidationError
raise ValidationError("Account is required")
serializer.save(account=account)

View File

@@ -0,0 +1,27 @@
"""
Test script to verify URL patterns are correctly registered
Run this with: python manage.py shell < test_urls.py
"""
from django.urls import resolve, reverse
from django.test import RequestFactory
# Test URL resolution
try:
# Test the generate endpoint
url_path = '/api/v1/system/settings/integrations/image_generation/generate/'
resolved = resolve(url_path)
print(f"✅ URL resolved: {url_path}")
print(f" View: {resolved.func}")
print(f" Args: {resolved.args}")
print(f" Kwargs: {resolved.kwargs}")
except Exception as e:
print(f"❌ URL NOT resolved: {url_path}")
print(f" Error: {e}")
# Test reverse
try:
reversed_url = reverse('integration-settings-generate', kwargs={'pk': 'image_generation'})
print(f"✅ Reverse URL: {reversed_url}")
except Exception as e:
print(f"❌ Reverse failed: {e}")

View File

@@ -0,0 +1,67 @@
"""
URL patterns for system module.
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import AIPromptViewSet, AuthorProfileViewSet, StrategyViewSet, system_status, get_request_metrics
from .integration_views import IntegrationSettingsViewSet
from .settings_views import (
SystemSettingsViewSet, AccountSettingsViewSet, UserSettingsViewSet,
ModuleSettingsViewSet, AISettingsViewSet
)
router = DefaultRouter()
router.register(r'prompts', AIPromptViewSet, basename='prompts')
router.register(r'author-profiles', AuthorProfileViewSet, basename='author-profile')
router.register(r'strategies', StrategyViewSet, basename='strategy')
router.register(r'settings/system', SystemSettingsViewSet, basename='system-settings')
router.register(r'settings/account', AccountSettingsViewSet, basename='account-settings')
router.register(r'settings/user', UserSettingsViewSet, basename='user-settings')
router.register(r'settings/modules', ModuleSettingsViewSet, basename='module-settings')
router.register(r'settings/ai', AISettingsViewSet, basename='ai-settings')
# Custom URL patterns for integration settings - matching reference plugin structure
# Reference: WordPress uses settings_fields('igny8_api_settings') -> options.php which calls update_option()
# We use REST endpoints: /api/v1/system/settings/integrations/{type}/save/
# Use as_view with proper method mapping
integration_detail_viewset = IntegrationSettingsViewSet.as_view({
'get': 'retrieve',
})
integration_save_viewset = IntegrationSettingsViewSet.as_view({
'post': 'save_post',
'put': 'update',
})
integration_test_viewset = IntegrationSettingsViewSet.as_view({
'post': 'test_connection',
})
integration_generate_viewset = IntegrationSettingsViewSet.as_view({
'post': 'generate_image',
})
integration_task_progress_viewset = IntegrationSettingsViewSet.as_view({
'get': 'task_progress',
})
urlpatterns = [
path('', include(router.urls)),
# System status endpoint
path('status/', system_status, name='system-status'),
# Request metrics endpoint
path('request-metrics/<str:request_id>/', get_request_metrics, name='request-metrics'),
# Integration settings routes - exact match to reference plugin workflow
# IMPORTANT: More specific paths must come BEFORE less specific ones
# GET: Task progress - MUST come before other settings paths
path('settings/task_progress/<str:task_id>/', integration_task_progress_viewset, name='integration-task-progress'),
# POST: Generate image (for image_generation integration) - MUST come before base path
path('settings/integrations/<str:pk>/generate/', integration_generate_viewset, name='integration-settings-generate'),
# POST: Test connection - MUST come before base path
path('settings/integrations/<str:pk>/test/', integration_test_viewset, name='integration-settings-test'),
# POST/PUT: Save settings (matches frontend call to /save/) - MUST come before base path
path('settings/integrations/<str:pk>/save/', integration_save_viewset, name='integration-settings-save'),
# GET: Retrieve settings - Base path comes last
path('settings/integrations/<str:pk>/', integration_detail_viewset, name='integration-settings-detail'),
]

View File

@@ -0,0 +1,116 @@
"""
System utilities - default prompts and helper functions
"""
from typing import Optional
def get_default_prompt(prompt_type: str) -> str:
"""Get default prompt value by type"""
defaults = {
'clustering': """Analyze the following keywords and group them into topic clusters.
Each cluster should include:
- "name": A clear, descriptive topic name
- "description": A brief explanation of what the cluster covers
- "keywords": A list of related keywords that belong to this cluster
Format the output as a JSON object with a "clusters" array.
IMPORTANT: In the "keywords" array, you MUST use the EXACT keyword strings from the input list below. Do not modify, paraphrase, or create variations of the keywords. Only use the exact keywords as they appear in the input list.
Clustering rules:
- Group keywords based on strong semantic or topical relationships (intent, use-case, function, audience, etc.)
- Clusters should reflect how people actually search — problem ➝ solution, general ➝ specific, product ➝ benefit, etc.
- Avoid grouping keywords just because they share similar words — focus on meaning
- Include 310 keywords per cluster where appropriate
- Skip unrelated or outlier keywords that don't fit a clear theme
- CRITICAL: Only return keywords that exactly match the input keywords (case-insensitive matching is acceptable)
Keywords to process:
[IGNY8_KEYWORDS]""",
'ideas': """Generate SEO-optimized, high-quality content ideas and detailed outlines for each of the following keyword clusters.
Clusters to analyze:
[IGNY8_CLUSTERS]
Keywords in each cluster:
[IGNY8_CLUSTER_KEYWORDS]
Return your response as JSON with an "ideas" array.
For each cluster, generate 1-3 content ideas.
Each idea must include:
- "title": compelling blog/article title that naturally includes a primary keyword
- "description": detailed content outline with H2/H3 structure (as plain text or structured JSON)
- "content_type": the type of content (blog_post, article, guide, tutorial)
- "content_structure": the editorial structure (cluster_hub, landing_page, pillar_page, supporting_page)
- "estimated_word_count": estimated total word count (1500-2200 words)
- "target_keywords": comma-separated list of keywords that will be covered (or "covered_keywords")
- "cluster_name": name of the cluster this idea belongs to (REQUIRED)
- "cluster_id": ID of the cluster this idea belongs to (REQUIRED - use the exact cluster ID from the input)
IMPORTANT: You MUST include the exact "cluster_id" from the cluster data provided. Match the cluster name to find the correct cluster_id.
Return only valid JSON with an "ideas" array.""",
'content_generation': """You are an editorial content strategist. Generate a complete blog post/article based on the provided content idea.
CONTENT IDEA DETAILS:
[IGNY8_IDEA]
KEYWORD CLUSTER:
[IGNY8_CLUSTER]
ASSOCIATED KEYWORDS:
[IGNY8_KEYWORDS]
Generate well-structured, SEO-optimized content with:
- Engaging introduction
- 5-8 H2 sections with H3 subsections
- Natural keyword integration
- 1500-2000 words total
- Proper HTML formatting (h2, h3, p, ul, ol, table tags)
Return the content as plain text with HTML tags.""",
'image_prompt_extraction': """Extract image prompts from the following article content.
ARTICLE TITLE: {title}
ARTICLE CONTENT:
{content}
Extract image prompts for:
1. Featured Image: One main image that represents the article topic
2. In-Article Images: Up to {max_images} images that would be useful within the article content
Return a JSON object with this structure:
{{
"featured_prompt": "Detailed description of the featured image",
"in_article_prompts": [
"Description of first in-article image",
"Description of second in-article image",
...
]
}}
Make sure each prompt is detailed enough for image generation, describing the visual elements, style, mood, and composition.""",
'image_prompt_template': 'Create a high-quality {image_type} image to use as a featured photo for a blog post titled "{post_title}". The image should visually represent the theme, mood, and subject implied by the image prompt: {image_prompt}. Focus on a realistic, well-composed scene that naturally communicates the topic without text or logos. Use balanced lighting, pleasing composition, and photographic detail suitable for lifestyle or editorial web content. Avoid adding any visible or readable text, brand names, or illustrative effects. **And make sure image is not blurry.**',
'negative_prompt': 'text, watermark, logo, overlay, title, caption, writing on walls, writing on objects, UI, infographic elements, post title',
}
return defaults.get(prompt_type, '')
def get_prompt_value(account, prompt_type: str) -> str:
"""Get prompt value for an account, or default if not set"""
try:
from .models import AIPrompt
prompt = AIPrompt.objects.get(account=account, prompt_type=prompt_type, is_active=True)
return prompt.prompt_value
except AIPrompt.DoesNotExist:
return get_default_prompt(prompt_type)

View File

@@ -0,0 +1,50 @@
"""
Settings Validation Utilities
"""
from django.core.exceptions import ValidationError
try:
import jsonschema
JSONSCHEMA_AVAILABLE = True
except ImportError:
JSONSCHEMA_AVAILABLE = False
from .schemas import SETTINGS_SCHEMAS, SETTINGS_KEY_SCHEMA_MAP
def validate_settings_schema(key, value, schema_name=None):
"""
Validate settings value against JSON schema.
Args:
key: Settings key
value: Settings value to validate
schema_name: Optional schema name (if not provided, will look up from key)
Raises:
ValidationError: If validation fails
"""
if not JSONSCHEMA_AVAILABLE:
# If jsonschema is not available, skip validation
return
# Get schema name from key if not provided
if schema_name is None:
schema_name = SETTINGS_KEY_SCHEMA_MAP.get(key)
if not schema_name:
# No schema defined for this key - skip validation
return
schema = SETTINGS_SCHEMAS.get(schema_name)
if not schema:
# Schema not found - skip validation
return
try:
jsonschema.validate(instance=value, schema=schema)
except jsonschema.ValidationError as e:
raise ValidationError(f"Settings validation failed for key '{key}': {e.message}")
except jsonschema.SchemaError as e:
# Schema error - log but don't fail validation
pass

View File

@@ -0,0 +1,484 @@
"""
System module views - for global settings and prompts
"""
import psutil
import os
import logging
from rest_framework import viewsets, status as http_status, filters
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from django.db import transaction, connection
from django.core.cache import cache
from django.utils import timezone
from django_filters.rest_framework import DjangoFilterBackend
from igny8_core.api.base import AccountModelViewSet
from .models import AIPrompt, AuthorProfile, Strategy
from .serializers import AIPromptSerializer, AuthorProfileSerializer, StrategySerializer
logger = logging.getLogger(__name__)
class AIPromptViewSet(AccountModelViewSet):
"""
ViewSet for managing AI prompts
"""
queryset = AIPrompt.objects.all()
serializer_class = AIPromptSerializer
permission_classes = [] # Allow any for now
def get_queryset(self):
"""Get prompts for the current account"""
return super().get_queryset().order_by('prompt_type')
@action(detail=False, methods=['get'], url_path='by_type/(?P<prompt_type>[^/.]+)', url_name='by_type')
def get_by_type(self, request, prompt_type=None):
"""Get prompt by type"""
try:
prompt = self.get_queryset().get(prompt_type=prompt_type)
serializer = self.get_serializer(prompt)
return Response(serializer.data)
except AIPrompt.DoesNotExist:
# Return default if not found
from .utils import get_default_prompt
default_value = get_default_prompt(prompt_type)
return Response({
'prompt_type': prompt_type,
'prompt_value': default_value,
'default_prompt': default_value,
'is_active': True,
})
@action(detail=False, methods=['post'], url_path='save', url_name='save')
def save_prompt(self, request):
"""Save or update a prompt"""
prompt_type = request.data.get('prompt_type')
prompt_value = request.data.get('prompt_value')
if not prompt_type:
return Response({'error': 'prompt_type is required'}, status=status.HTTP_400_BAD_REQUEST)
if prompt_value is None:
return Response({'error': 'prompt_value is required'}, status=status.HTTP_400_BAD_REQUEST)
# Get account - try multiple methods
account = getattr(request, 'account', None)
# Fallback 1: Get from authenticated user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
# Fallback 2: If still no account, get default account (for development)
if not account:
from igny8_core.auth.models import Account
try:
account = Account.objects.first()
except Exception:
pass
if not account:
return Response({'error': 'Account not found. Please ensure you are logged in.'}, status=status.HTTP_400_BAD_REQUEST)
# Get default prompt value if creating new
from .utils import get_default_prompt
default_value = get_default_prompt(prompt_type)
# Get or create prompt
prompt, created = AIPrompt.objects.get_or_create(
prompt_type=prompt_type,
account=account,
defaults={
'prompt_value': prompt_value,
'default_prompt': default_value,
'is_active': True,
}
)
if not created:
prompt.prompt_value = prompt_value
prompt.save()
serializer = self.get_serializer(prompt)
return Response({
'success': True,
'data': serializer.data,
'message': f'{prompt.get_prompt_type_display()} saved successfully'
})
@action(detail=False, methods=['post'], url_path='reset', url_name='reset')
def reset_prompt(self, request):
"""Reset prompt to default"""
prompt_type = request.data.get('prompt_type')
if not prompt_type:
return Response({'error': 'prompt_type is required'}, status=status.HTTP_400_BAD_REQUEST)
# Get account - try multiple methods (same as integration_views)
account = getattr(request, 'account', None)
# Fallback 1: Get from authenticated user's account
if not account:
user = getattr(request, 'user', None)
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None)
# Fallback 2: If still no account, get default account (for development)
if not account:
from igny8_core.auth.models import Account
try:
account = Account.objects.first()
except Exception:
pass
if not account:
return Response({'error': 'Account not found. Please ensure you are logged in.'}, status=status.HTTP_400_BAD_REQUEST)
# Get default prompt
from .utils import get_default_prompt
default_value = get_default_prompt(prompt_type)
# Update or create prompt
prompt, created = AIPrompt.objects.get_or_create(
prompt_type=prompt_type,
account=account,
defaults={
'prompt_value': default_value,
'default_prompt': default_value,
'is_active': True,
}
)
if not created:
prompt.prompt_value = default_value
prompt.save()
serializer = self.get_serializer(prompt)
return Response({
'success': True,
'data': serializer.data,
'message': f'{prompt.get_prompt_type_display()} reset to default'
})
class AuthorProfileViewSet(AccountModelViewSet):
"""
ViewSet for managing Author Profiles
"""
queryset = AuthorProfile.objects.all()
serializer_class = AuthorProfileSerializer
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['name', 'description', 'tone']
ordering_fields = ['name', 'created_at', 'updated_at']
ordering = ['name']
filterset_fields = ['is_active', 'language']
class StrategyViewSet(AccountModelViewSet):
"""
ViewSet for managing Strategies
"""
queryset = Strategy.objects.all()
serializer_class = StrategySerializer
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['name', 'description']
ordering_fields = ['name', 'created_at', 'updated_at']
ordering = ['name']
filterset_fields = ['is_active', 'sector']
@api_view(['GET'])
@permission_classes([AllowAny]) # Adjust permissions as needed
def system_status(request):
"""
Comprehensive system status endpoint for monitoring
Returns CPU, memory, disk, database, Redis, Celery, and process information
"""
status_data = {
'timestamp': timezone.now().isoformat(),
'system': {},
'database': {},
'redis': {},
'celery': {},
'processes': {},
'modules': {},
}
try:
# System Resources
cpu_percent = psutil.cpu_percent(interval=1)
cpu_count = psutil.cpu_count()
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
status_data['system'] = {
'cpu': {
'usage_percent': cpu_percent,
'cores': cpu_count,
'status': 'healthy' if cpu_percent < 80 else 'warning' if cpu_percent < 95 else 'critical'
},
'memory': {
'total_gb': round(memory.total / (1024**3), 2),
'used_gb': round(memory.used / (1024**3), 2),
'available_gb': round(memory.available / (1024**3), 2),
'usage_percent': memory.percent,
'status': 'healthy' if memory.percent < 80 else 'warning' if memory.percent < 95 else 'critical'
},
'disk': {
'total_gb': round(disk.total / (1024**3), 2),
'used_gb': round(disk.used / (1024**3), 2),
'free_gb': round(disk.free / (1024**3), 2),
'usage_percent': disk.percent,
'status': 'healthy' if disk.percent < 80 else 'warning' if disk.percent < 95 else 'critical'
}
}
except Exception as e:
logger.error(f"Error getting system resources: {str(e)}")
status_data['system'] = {'error': str(e)}
try:
# Database Status
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
db_conn = True
cursor.execute("SELECT version()")
db_version = cursor.fetchone()[0] if cursor.rowcount > 0 else 'Unknown'
# Get database size (PostgreSQL)
try:
cursor.execute("""
SELECT pg_size_pretty(pg_database_size(current_database()))
""")
db_size = cursor.fetchone()[0] if cursor.rowcount > 0 else 'Unknown'
except:
db_size = 'Unknown'
# Count active connections
try:
cursor.execute("SELECT count(*) FROM pg_stat_activity WHERE state = 'active'")
active_connections = cursor.fetchone()[0] if cursor.rowcount > 0 else 0
except:
active_connections = 0
status_data['database'] = {
'connected': db_conn,
'version': db_version,
'size': db_size,
'active_connections': active_connections,
'status': 'healthy' if db_conn else 'critical'
}
except Exception as e:
logger.error(f"Error getting database status: {str(e)}")
status_data['database'] = {'connected': False, 'error': str(e), 'status': 'critical'}
try:
# Redis Status
redis_conn = False
redis_info = {}
try:
cache.set('status_check', 'ok', 10)
test_value = cache.get('status_check')
redis_conn = test_value == 'ok'
# Try to get Redis info if available
if hasattr(cache, 'client'):
try:
redis_client = cache.client.get_client()
redis_info = redis_client.info()
except:
pass
except Exception as e:
redis_conn = False
redis_info = {'error': str(e)}
status_data['redis'] = {
'connected': redis_conn,
'status': 'healthy' if redis_conn else 'critical',
'info': redis_info if redis_info else {}
}
except Exception as e:
logger.error(f"Error getting Redis status: {str(e)}")
status_data['redis'] = {'connected': False, 'error': str(e), 'status': 'critical'}
try:
# Celery Status
celery_workers = []
celery_tasks = {
'active': 0,
'scheduled': 0,
'reserved': 0,
}
try:
from celery import current_app
inspect = current_app.control.inspect()
# Get active workers
active_workers = inspect.active() or {}
scheduled = inspect.scheduled() or {}
reserved = inspect.reserved() or {}
celery_workers = list(active_workers.keys())
celery_tasks['active'] = sum(len(tasks) for tasks in active_workers.values())
celery_tasks['scheduled'] = sum(len(tasks) for tasks in scheduled.values())
celery_tasks['reserved'] = sum(len(tasks) for tasks in reserved.values())
except Exception as e:
logger.warning(f"Error getting Celery status: {str(e)}")
celery_workers = []
celery_tasks = {'error': str(e)}
status_data['celery'] = {
'workers': celery_workers,
'worker_count': len(celery_workers),
'tasks': celery_tasks,
'status': 'healthy' if len(celery_workers) > 0 else 'warning'
}
except Exception as e:
logger.error(f"Error getting Celery status: {str(e)}")
status_data['celery'] = {'error': str(e), 'status': 'warning'}
try:
# Process Monitoring by Stack/Component
processes = {
'gunicorn': [],
'celery': [],
'postgres': [],
'redis': [],
'nginx': [],
'other': []
}
process_stats = {
'gunicorn': {'count': 0, 'cpu': 0, 'memory_mb': 0},
'celery': {'count': 0, 'cpu': 0, 'memory_mb': 0},
'postgres': {'count': 0, 'cpu': 0, 'memory_mb': 0},
'redis': {'count': 0, 'cpu': 0, 'memory_mb': 0},
'nginx': {'count': 0, 'cpu': 0, 'memory_mb': 0},
}
for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'cpu_percent', 'memory_info']):
try:
proc_info = proc.info
name = proc_info['name'].lower()
cmdline = ' '.join(proc_info['cmdline']) if proc_info['cmdline'] else ''
cmdline_lower = cmdline.lower()
cpu = proc_info.get('cpu_percent', 0) or 0
memory = proc_info.get('memory_info', None)
memory_mb = (memory.rss / (1024**2)) if memory else 0
# Categorize processes
if 'gunicorn' in cmdline_lower or 'gunicorn' in name:
processes['gunicorn'].append({
'pid': proc_info['pid'],
'name': name,
'cpu_percent': round(cpu, 2),
'memory_mb': round(memory_mb, 2)
})
process_stats['gunicorn']['count'] += 1
process_stats['gunicorn']['cpu'] += cpu
process_stats['gunicorn']['memory_mb'] += memory_mb
elif 'celery' in cmdline_lower or 'celery' in name:
processes['celery'].append({
'pid': proc_info['pid'],
'name': name,
'cpu_percent': round(cpu, 2),
'memory_mb': round(memory_mb, 2)
})
process_stats['celery']['count'] += 1
process_stats['celery']['cpu'] += cpu
process_stats['celery']['memory_mb'] += memory_mb
elif 'postgres' in name or 'postgresql' in name:
processes['postgres'].append({
'pid': proc_info['pid'],
'name': name,
'cpu_percent': round(cpu, 2),
'memory_mb': round(memory_mb, 2)
})
process_stats['postgres']['count'] += 1
process_stats['postgres']['cpu'] += cpu
process_stats['postgres']['memory_mb'] += memory_mb
elif 'redis' in name or 'redis-server' in name:
processes['redis'].append({
'pid': proc_info['pid'],
'name': name,
'cpu_percent': round(cpu, 2),
'memory_mb': round(memory_mb, 2)
})
process_stats['redis']['count'] += 1
process_stats['redis']['cpu'] += cpu
process_stats['redis']['memory_mb'] += memory_mb
elif 'nginx' in name or 'caddy' in name:
processes['nginx'].append({
'pid': proc_info['pid'],
'name': name,
'cpu_percent': round(cpu, 2),
'memory_mb': round(memory_mb, 2)
})
process_stats['nginx']['count'] += 1
process_stats['nginx']['cpu'] += cpu
process_stats['nginx']['memory_mb'] += memory_mb
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
# Round stats
for key in process_stats:
process_stats[key]['cpu'] = round(process_stats[key]['cpu'], 2)
process_stats[key]['memory_mb'] = round(process_stats[key]['memory_mb'], 2)
status_data['processes'] = {
'by_stack': process_stats,
'details': {k: v[:10] for k, v in processes.items()} # Limit details to 10 per type
}
except Exception as e:
logger.error(f"Error getting process information: {str(e)}")
status_data['processes'] = {'error': str(e)}
try:
# Module-specific task counts
from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas
from igny8_core.modules.writer.models import Tasks, Images
status_data['modules'] = {
'planner': {
'keywords': Keywords.objects.count(),
'clusters': Clusters.objects.count(),
'content_ideas': ContentIdeas.objects.count(),
},
'writer': {
'tasks': Tasks.objects.count(),
'images': Images.objects.count(),
}
}
except Exception as e:
logger.error(f"Error getting module statistics: {str(e)}")
status_data['modules'] = {'error': str(e)}
return Response(status_data)
@api_view(['GET'])
@permission_classes([AllowAny]) # Will check admin in view
def get_request_metrics(request, request_id):
"""
Get resource metrics for a specific request.
Only accessible to admins/developers.
"""
# Check if user is admin/developer
if not request.user.is_authenticated:
return Response({'error': 'Authentication required'}, status=http_status.HTTP_401_UNAUTHORIZED)
if not (hasattr(request.user, 'is_admin_or_developer') and request.user.is_admin_or_developer()):
return Response({'error': 'Admin access required'}, status=http_status.HTTP_403_FORBIDDEN)
# Get metrics from cache
from django.core.cache import cache
metrics = cache.get(f"resource_tracking_{request_id}")
if not metrics:
return Response({'error': 'Metrics not found or expired'}, status=http_status.HTTP_404_NOT_FOUND)
return Response(metrics)