more fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-03 10:29:13 +00:00
parent 291d8cc968
commit aa8b8a9756
14 changed files with 61 additions and 276 deletions

View File

@@ -45,8 +45,6 @@ class Igny8AdminSite(admin.AdminSite):
('igny8_core_auth', 'User'),
('igny8_core_auth', 'SiteUserAccess'),
('igny8_core_auth', 'PasswordResetToken'),
('site_building', 'SiteBlueprint'),
('site_building', 'PageBlueprint'),
],
},
'Global Reference Data': {

View File

@@ -6,7 +6,6 @@ from igny8_core.ai.functions.generate_ideas import GenerateIdeasFunction
from igny8_core.ai.functions.generate_content import GenerateContentFunction
from igny8_core.ai.functions.generate_images import GenerateImagesFunction, generate_images_core
from igny8_core.ai.functions.generate_image_prompts import GenerateImagePromptsFunction
from igny8_core.ai.functions.generate_site_structure import GenerateSiteStructureFunction
from igny8_core.ai.functions.generate_page_content import GeneratePageContentFunction
__all__ = [
@@ -16,6 +15,5 @@ __all__ = [
'GenerateImagesFunction',
'generate_images_core',
'GenerateImagePromptsFunction',
'GenerateSiteStructureFunction',
'GeneratePageContentFunction',
]

View File

@@ -1,214 +0,0 @@
"""
Generate Site Structure AI Function
Phase 3 Site Builder
"""
import json
import logging
from typing import Any, Dict, List, Tuple
from django.utils.text import slugify
from igny8_core.ai.base import BaseAIFunction
from igny8_core.ai.prompts import PromptRegistry
from igny8_core.business.site_building.models import SiteBlueprint, PageBlueprint
logger = logging.getLogger(__name__)
class GenerateSiteStructureFunction(BaseAIFunction):
"""AI function that turns a business brief into a full site blueprint."""
def get_name(self) -> str:
return 'generate_site_structure'
def get_metadata(self) -> Dict:
metadata = super().get_metadata()
metadata.update({
'display_name': 'Generate Site Structure',
'description': 'Create site/page architecture from business brief, objectives, and style guides.',
'phases': {
'INIT': 'Validating blueprint data…',
'PREP': 'Preparing site context…',
'AI_CALL': 'Generating site structure with AI…',
'PARSE': 'Parsing generated blueprint…',
'SAVE': 'Saving pages and blocks…',
'DONE': 'Site structure ready!'
}
})
return metadata
def validate(self, payload: dict, account=None) -> Dict[str, Any]:
if not payload.get('ids'):
return {'valid': False, 'error': 'Site blueprint ID is required'}
return {'valid': True}
def prepare(self, payload: dict, account=None) -> Dict[str, Any]:
blueprint_ids = payload.get('ids', [])
queryset = SiteBlueprint.objects.filter(id__in=blueprint_ids)
if account:
queryset = queryset.filter(account=account)
blueprint = queryset.select_related('account', 'site').prefetch_related('pages').first()
if not blueprint:
raise ValueError("Site blueprint not found")
config = blueprint.config_json or {}
business_brief = payload.get('business_brief') or config.get('business_brief') or ''
objectives = payload.get('objectives') or config.get('objectives') or []
style = payload.get('style') or config.get('style') or {}
return {
'blueprint': blueprint,
'business_brief': business_brief,
'objectives': objectives,
'style': style,
}
def build_prompt(self, data: Dict[str, Any], account=None) -> str:
blueprint: SiteBlueprint = data['blueprint']
objectives = data.get('objectives') or []
objectives_text = '\n'.join(f"- {obj}" for obj in objectives) if isinstance(objectives, list) else objectives
style = data.get('style') or {}
style_text = json.dumps(style, indent=2) if isinstance(style, dict) and style else str(style)
existing_pages = [
{
'title': page.title,
'slug': page.slug,
'type': page.type,
'status': page.status,
}
for page in blueprint.pages.all()
]
context = {
'BUSINESS_BRIEF': data.get('business_brief', ''),
'OBJECTIVES': objectives_text or 'Create a full marketing site with clear navigation.',
'STYLE': style_text or 'Modern, responsive, accessible web design.',
'SITE_INFO': json.dumps({
'site_name': blueprint.name,
'site_description': blueprint.description,
'hosting_type': blueprint.hosting_type,
'existing_pages': existing_pages,
'existing_structure': blueprint.structure_json or {},
}, indent=2)
}
return PromptRegistry.get_prompt(
'generate_site_structure',
account=account or blueprint.account,
context=context
)
def parse_response(self, response: str, step_tracker=None) -> Dict[str, Any]:
if not response:
raise ValueError("AI response is empty")
response = response.strip()
try:
return self._ensure_dict(json.loads(response))
except json.JSONDecodeError:
logger.warning("Response not valid JSON, attempting to extract JSON object")
cleaned = self._extract_json_object(response)
if cleaned:
return self._ensure_dict(json.loads(cleaned))
raise ValueError("Unable to parse AI response into JSON")
def save_output(
self,
parsed: Dict[str, Any],
original_data: Dict[str, Any],
account=None,
progress_tracker=None,
step_tracker=None
) -> Dict[str, Any]:
blueprint: SiteBlueprint = original_data['blueprint']
structure = self._ensure_dict(parsed)
pages = structure.get('pages', [])
blueprint.structure_json = structure
blueprint.status = 'ready'
blueprint.save(update_fields=['structure_json', 'status', 'updated_at'])
created, updated, deleted = self._sync_page_blueprints(blueprint, pages)
message = f"Pages synced (created: {created}, updated: {updated}, deleted: {deleted})"
logger.info("[GenerateSiteStructure] %s for blueprint %s", message, blueprint.id)
return {
'success': True,
'count': created + updated,
'site_blueprint_id': blueprint.id,
'pages_created': created,
'pages_updated': updated,
'pages_deleted': deleted,
}
# Helpers -----------------------------------------------------------------
def _ensure_dict(self, data: Any) -> Dict[str, Any]:
if isinstance(data, dict):
return data
raise ValueError("AI response must be a JSON object with site metadata")
def _extract_json_object(self, text: str) -> str:
start = text.find('{')
end = text.rfind('}')
if start != -1 and end != -1 and end > start:
return text[start:end + 1]
return ''
def _sync_page_blueprints(self, blueprint: SiteBlueprint, pages: List[Dict[str, Any]]) -> Tuple[int, int, int]:
existing = {page.slug: page for page in blueprint.pages.all()}
seen_slugs = set()
created = updated = 0
for order, page_data in enumerate(pages or []):
slug = page_data.get('slug') or page_data.get('id') or page_data.get('title') or f"page-{order + 1}"
slug = slugify(slug) or f"page-{order + 1}"
seen_slugs.add(slug)
defaults = {
'title': page_data.get('title') or page_data.get('name') or slug.replace('-', ' ').title(),
'type': self._map_page_type(page_data.get('type')),
'blocks_json': page_data.get('blocks') or page_data.get('sections') or [],
'status': page_data.get('status') or 'draft',
'order': order,
}
page_obj, created_flag = PageBlueprint.objects.update_or_create(
site_blueprint=blueprint,
slug=slug,
defaults=defaults
)
if created_flag:
created += 1
else:
updated += 1
# Delete pages not present in new structure
deleted = 0
for slug, page in existing.items():
if slug not in seen_slugs:
page.delete()
deleted += 1
return created, updated, deleted
def _map_page_type(self, page_type: Any) -> str:
allowed = {choice[0] for choice in PageBlueprint._meta.get_field('type').choices}
if isinstance(page_type, str):
normalized = page_type.lower()
if normalized in allowed:
return normalized
# Map friendly names
mapping = {
'homepage': 'home',
'landing': 'home',
'service': 'services',
'product': 'products',
}
mapped = mapping.get(normalized)
if mapped in allowed:
return mapped
return 'custom'

View File

@@ -94,11 +94,6 @@ def _load_generate_image_prompts():
from igny8_core.ai.functions.generate_image_prompts import GenerateImagePromptsFunction
return GenerateImagePromptsFunction
def _load_generate_site_structure():
"""Lazy loader for generate_site_structure function"""
from igny8_core.ai.functions.generate_site_structure import GenerateSiteStructureFunction
return GenerateSiteStructureFunction
def _load_optimize_content():
"""Lazy loader for optimize_content function"""
from igny8_core.ai.functions.optimize_content import OptimizeContentFunction
@@ -109,6 +104,5 @@ register_lazy_function('generate_ideas', _load_generate_ideas)
register_lazy_function('generate_content', _load_generate_content)
register_lazy_function('generate_images', _load_generate_images)
register_lazy_function('generate_image_prompts', _load_generate_image_prompts)
register_lazy_function('generate_site_structure', _load_generate_site_structure)
register_lazy_function('optimize_content', _load_optimize_content)

View File

@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('system', '__latest__'),
('igny8_core_auth', '__latest__'),
]
operations = [
@@ -35,11 +35,11 @@ class Migration(migrations.Migration):
('next_run_at', models.DateTimeField(blank=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.account')),
('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='automation_config', to='system.site')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.account')),
('site', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='automation_config', to='igny8_core_auth.site')),
],
options={
'db_table': 'automation_config',
'db_table': 'igny8_automation_configs',
},
),
migrations.CreateModel(
@@ -74,11 +74,11 @@ class Migration(migrations.Migration):
('error_message', models.TextField(blank=True, null=True)),
('started_at', models.DateTimeField(auto_now_add=True)),
('completed_at', models.DateTimeField(blank=True, null=True)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.account')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='automation_runs', to='system.site')),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='igny8_core_auth.account')),
('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='automation_runs', to='igny8_core_auth.site')),
],
options={
'db_table': 'automation_run',
'db_table': 'igny8_automation_runs',
'ordering': ['-started_at'],
'indexes': [
models.Index(fields=['site', 'status'], name='automation_site_status_idx'),

View File

@@ -760,14 +760,14 @@ class AutomationService:
def estimate_credits(self) -> int:
"""Estimate total credits needed for automation"""
# Count items
keywords_count = Keywords.objects.filter(site=self.site, status='new', cluster__isnull=True).count()
keywords_count = Keywords.objects.filter(site=self.site, status='new', cluster__isnull=True, disabled=False).count()
clusters_count = Clusters.objects.filter(site=self.site, status='new').exclude(ideas__isnull=False).count()
ideas_count = ContentIdeas.objects.filter(site=self.site, status='new').count()
tasks_count = Tasks.objects.filter(site=self.site, status='queued', content__isnull=True).count()
tasks_count = Tasks.objects.filter(site=self.site, status='queued').count()
content_count = Content.objects.filter(site=self.site, status='draft').annotate(images_count=Count('images')).filter(images_count=0).count()
# Estimate credits
clustering_credits = (keywords_count // 5) + 1 # 1 credit per 5 keywords
clustering_credits = (keywords_count // 5) + 1 if keywords_count > 0 else 0 # 1 credit per 5 keywords
ideas_credits = clusters_count * 2 # 2 credits per cluster
content_credits = tasks_count * 5 # Assume 2500 words avg = 5 credits
prompts_credits = content_count * 2 # Assume 4 prompts per content = 2 credits

View File

@@ -27,7 +27,7 @@ class AutomationViewSet(viewsets.ViewSet):
status=status.HTTP_400_BAD_REQUEST
)
site = get_object_or_404(Site, id=site_id, account__user=request.user)
site = get_object_or_404(Site, id=site_id, account=request.user.account)
return site, None
@action(detail=False, methods=['get'])
@@ -308,6 +308,6 @@ class AutomationViewSet(viewsets.ViewSet):
return Response({
'estimated_credits': estimated_credits,
'current_balance': site.account.credits_balance,
'sufficient': site.account.credits_balance >= (estimated_credits * 1.2)
'current_balance': site.account.credits,
'sufficient': site.account.credits >= (estimated_credits * 1.2)
})

View File

@@ -1,18 +1,17 @@
"""
Tests for DeploymentService
DEPRECATED: Tests for DeploymentService - SiteBlueprint models removed
Phase 5: Sites Renderer & Publishing
"""
from django.test import TestCase
from django.utils import timezone
from igny8_core.auth.models import Account, Site, Sector, User, Plan, Industry, IndustrySector
from igny8_core.business.site_building.models import SiteBlueprint
from igny8_core.business.publishing.models import DeploymentRecord
from igny8_core.business.publishing.services.deployment_service import DeploymentService
class DeploymentServiceTestCase(TestCase):
"""Test cases for DeploymentService"""
"""DEPRECATED: Test cases for DeploymentService"""
def setUp(self):
"""Set up test data"""
@@ -69,14 +68,8 @@ class DeploymentServiceTestCase(TestCase):
name="Test Sector",
slug="test-sector"
)
self.blueprint = SiteBlueprint.objects.create(
account=self.account,
site=self.site,
sector=self.sector,
name="Test Blueprint",
status='ready',
version=1
)
# DEPRECATED: SiteBlueprint model removed
self.blueprint = None
self.service = DeploymentService()
def test_get_status_returns_deployed_record(self):

View File

@@ -1,9 +1,10 @@
"""
Site Building Models
Legacy SiteBuilder module has been removed.
This file is kept for backwards compatibility with migrations.
This file is kept for backwards compatibility with migrations and legacy code.
"""
from django.db import models
from igny8_core.auth.models import AccountBaseModel
# All SiteBuilder models have been removed:
# - SiteBlueprint
@@ -13,3 +14,32 @@ from django.db import models
# - BusinessType, AudienceProfile, BrandPersonality, HeroImageryDirection
#
# Taxonomy functionality moved to ContentTaxonomy model in business/content/models.py
# Stub classes for backwards compatibility with legacy imports
class SiteBlueprint(AccountBaseModel):
"""Legacy stub - SiteBuilder has been removed"""
class Meta:
app_label = 'site_building'
db_table = 'legacy_site_blueprint_stub'
managed = False # Don't create table
class PageBlueprint(AccountBaseModel):
"""Legacy stub - SiteBuilder has been removed"""
class Meta:
app_label = 'site_building'
db_table = 'legacy_page_blueprint_stub'
managed = False # Don't create table
class SiteBlueprintCluster(AccountBaseModel):
"""Legacy stub - SiteBuilder has been removed"""
class Meta:
app_label = 'site_building'
db_table = 'legacy_site_blueprint_cluster_stub'
managed = False # Don't create table
class SiteBlueprintTaxonomy(AccountBaseModel):
"""Legacy stub - SiteBuilder has been removed"""
class Meta:
app_label = 'site_building'
db_table = 'legacy_site_blueprint_taxonomy_stub'
managed = False # Don't create table

View File

@@ -13,13 +13,12 @@ from igny8_core.auth.models import (
Site,
User,
)
from igny8_core.business.site_building.models import PageBlueprint, SiteBlueprint
class SiteBuilderTestBase(TestCase):
"""
Provides a lightweight set of fixtures (account/site/sector/blueprint)
so Site Builder tests can focus on service logic instead of boilerplate.
DEPRECATED: Provides a lightweight set of fixtures (account/site/sector/blueprint)
SiteBlueprint models have been removed.
"""
def setUp(self):
@@ -65,20 +64,9 @@ class SiteBuilderTestBase(TestCase):
account=self.account,
)
self.blueprint = SiteBlueprint.objects.create(
site=self.site,
sector=self.sector,
name='Core Blueprint',
description='Initial blueprint used for tests',
hosting_type='igny8_sites',
config_json={
'business_brief': 'Default brief',
'objectives': ['Drive demos'],
'style': {'palette': 'bold'},
},
)
self.page_blueprint = PageBlueprint.objects.create(
site_blueprint=self.blueprint,
# DEPRECATED: SiteBlueprint and PageBlueprint models removed
self.blueprint = None
self.page_blueprint = None
slug='home',
title='Home',
type='home',

View File

@@ -1,20 +1,18 @@
"""
Tests for Bulk Page Generation
DEPRECATED: Tests for Bulk Page Generation - SiteBlueprint models removed
Phase 5: Sites Renderer & Bulk Generation
"""
from django.test import TestCase
from unittest.mock import patch, Mock
from igny8_core.auth.models import Account, Site, Sector
from igny8_core.business.site_building.models import SiteBlueprint, PageBlueprint
from igny8_core.business.site_building.services.page_generation_service import PageGenerationService
from igny8_core.business.content.models import Tasks
from .base import SiteBuilderTestBase
class BulkGenerationTestCase(SiteBuilderTestBase):
"""Test cases for bulk page generation"""
"""DEPRECATED: Test cases for bulk page generation"""
def setUp(self):
"""Set up test data"""

View File

@@ -52,3 +52,9 @@ urlpatterns = [
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
]
# Error handlers
handler400 = 'django.views.defaults.bad_request'
handler403 = 'django.views.defaults.permission_denied'
handler404 = 'django.views.defaults.page_not_found'
handler500 = 'django.views.defaults.server_error'