Remove obsolete workflow components from site building; delete WorkflowState model, related services, and frontend steps. Update serializers and routes to reflect the removal of the site builder wizard functionality.

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-20 23:25:00 +00:00
parent c31567ec9f
commit b38553cfc3
25 changed files with 0 additions and 5268 deletions

Binary file not shown.

View File

@@ -292,57 +292,6 @@ class SiteBlueprintTaxonomy(SiteSectorBaseModel):
return f"{self.name} ({self.get_taxonomy_type_display()})"
class WorkflowState(SiteSectorBaseModel):
"""
Persists wizard progress + gating data for each site blueprint.
"""
DEFAULT_STEP = 'business_details'
site_blueprint = models.OneToOneField(
SiteBlueprint,
on_delete=models.CASCADE,
related_name='workflow_state',
help_text="Blueprint whose progress is being tracked",
)
current_step = models.CharField(max_length=50, default=DEFAULT_STEP)
step_status = models.JSONField(
default=dict,
blank=True,
help_text="Dictionary of step → status/progress metadata",
)
blocking_reason = models.TextField(blank=True, null=True, help_text="Human-readable explanation when blocked")
completed = models.BooleanField(default=False, help_text="Marks wizard completion")
metadata = models.JSONField(default=dict, blank=True)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'site_building'
db_table = 'igny8_site_blueprint_workflow_states'
verbose_name = 'Workflow State'
verbose_name_plural = 'Workflow States'
indexes = [
models.Index(fields=['site_blueprint']),
models.Index(fields=['current_step']),
models.Index(fields=['completed']),
]
def save(self, *args, **kwargs):
if self.site_blueprint:
# Only set fields if blueprint has them (avoid errors if blueprint is missing fields)
if self.site_blueprint.account_id:
self.account_id = self.site_blueprint.account_id
if self.site_blueprint.site_id:
self.site_id = self.site_blueprint.site_id
if self.site_blueprint.sector_id:
self.sector_id = self.site_blueprint.sector_id
super().save(*args, **kwargs)
def __str__(self):
return f"Workflow for {self.site_blueprint.name} ({self.current_step})"
class SiteBuilderOption(models.Model):
"""
Base model for Site Builder dropdown metadata.

View File

@@ -5,15 +5,11 @@ Site Building Services
from igny8_core.business.site_building.services.file_management_service import SiteBuilderFileService
from igny8_core.business.site_building.services.structure_generation_service import StructureGenerationService
from igny8_core.business.site_building.services.page_generation_service import PageGenerationService
from igny8_core.business.site_building.services.workflow_state_service import WorkflowStateService
from igny8_core.business.site_building.services.taxonomy_service import TaxonomyService
from igny8_core.business.site_building.services.wizard_context_service import WizardContextService
__all__ = [
'SiteBuilderFileService',
'StructureGenerationService',
'PageGenerationService',
'WorkflowStateService',
'TaxonomyService',
'WizardContextService',
]

View File

@@ -1,41 +0,0 @@
"""
Reusable validation helpers for the site builder workflow.
"""
from __future__ import annotations
from django.core.exceptions import ValidationError
from igny8_core.business.site_building.models import SiteBlueprint
def ensure_clusters_attached(site_blueprint: SiteBlueprint) -> bool:
if not site_blueprint.cluster_links.exists():
raise ValidationError("Attach at least one planner cluster before proceeding.")
return True
def ensure_taxonomies_defined(site_blueprint: SiteBlueprint) -> bool:
if not site_blueprint.taxonomies.exists():
raise ValidationError("Define or import at least one taxonomy to continue.")
return True
def ensure_sitemap_ready(site_blueprint: SiteBlueprint) -> bool:
if not site_blueprint.pages.exists():
raise ValidationError("Generate the AI sitemap before reviewing this step.")
return True
def ensure_coverage_ready(site_blueprint: SiteBlueprint) -> bool:
incomplete = site_blueprint.cluster_links.exclude(coverage_status='complete').exists()
if incomplete:
raise ValidationError("Complete coverage for all attached clusters.")
return True
def ensure_ideas_ready(site_blueprint: SiteBlueprint) -> bool:
if not site_blueprint.cluster_links.exists() or not site_blueprint.pages.exists():
raise ValidationError("Attach clusters and generate pages before sending ideas.")
return True

View File

@@ -1,138 +0,0 @@
"""
Wizard Context Service
Provides aggregated data for the site builder wizard UI.
"""
from __future__ import annotations
from collections import Counter
from typing import Dict, List, Optional
from django.db.models import Prefetch
from igny8_core.business.planning.models import Clusters
from igny8_core.business.site_building.models import (
PageBlueprint,
SiteBlueprint,
SiteBlueprintCluster,
SiteBlueprintTaxonomy,
)
from igny8_core.business.site_building.services.workflow_state_service import WorkflowStateService
class WizardContextService:
"""Builds blueprint-centric context for the guided wizard experience."""
def __init__(self):
self.workflow_service = WorkflowStateService()
def build_context(self, site_blueprint: SiteBlueprint) -> Dict[str, object]:
if not site_blueprint:
return {}
workflow_state = None
if self.workflow_service.enabled:
workflow_state = self.workflow_service.refresh_state(site_blueprint)
workflow_payload = self.workflow_service.serialize_state(workflow_state) if workflow_state else None
coverage_data = self._coverage_summary(site_blueprint)
context = {
'workflow': workflow_payload,
'cluster_summary': self._cluster_summary(site_blueprint),
'taxonomy_summary': self._taxonomy_summary(site_blueprint),
'sitemap_summary': coverage_data, # Frontend expects 'sitemap_summary' not 'coverage'
'coverage': coverage_data, # Keep for backward compatibility
}
context['next_actions'] = self._next_actions(workflow_payload)
return context
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
def _cluster_summary(self, site_blueprint: SiteBlueprint) -> Dict[str, object]:
cluster_links: List[SiteBlueprintCluster] = list(
site_blueprint.cluster_links.select_related('cluster')
)
coverage_counts = Counter(link.coverage_status for link in cluster_links)
clusters_payload = []
for link in cluster_links:
cluster = link.cluster
dimension_meta = cluster.dimension_meta or {}
clusters_payload.append({
'id': cluster.id,
'name': cluster.name,
'context_type': cluster.context_type,
'dimension_meta': dimension_meta,
'keyword_count': cluster.keywords_count,
'volume': cluster.volume,
'coverage_status': link.coverage_status,
'role': link.role,
'metadata': link.metadata or {},
'suggested_taxonomies': dimension_meta.get('suggested_taxonomies', []),
'attribute_hints': dimension_meta.get('attributes', []),
})
return {
'attached_count': len(cluster_links),
'coverage_counts': dict(coverage_counts),
'clusters': clusters_payload,
}
def _taxonomy_summary(self, site_blueprint: SiteBlueprint) -> Dict[str, object]:
taxonomies: List[SiteBlueprintTaxonomy] = list(
site_blueprint.taxonomies.prefetch_related(
Prefetch('clusters', queryset=Clusters.objects.only('id'))
)
)
counts_by_type = Counter(taxonomy.taxonomy_type for taxonomy in taxonomies)
taxonomy_payload = []
for taxonomy in taxonomies:
taxonomy_payload.append({
'id': taxonomy.id,
'name': taxonomy.name,
'slug': taxonomy.slug,
'taxonomy_type': taxonomy.taxonomy_type,
'description': taxonomy.description,
'cluster_ids': [cluster.id for cluster in taxonomy.clusters.all()],
'metadata': taxonomy.metadata or {},
'external_reference': taxonomy.external_reference,
})
return {
'total_taxonomies': len(taxonomies),
'counts_by_type': dict(counts_by_type),
'taxonomies': taxonomy_payload,
}
def _coverage_summary(self, site_blueprint: SiteBlueprint) -> Dict[str, object]:
pages: List[PageBlueprint] = list(site_blueprint.pages.all())
per_status = Counter(page.status for page in pages)
per_type = Counter(page.type for page in pages)
return {
'pages_total': len(pages),
'pages_by_status': dict(per_status),
'pages_by_type': dict(per_type),
}
def _next_actions(self, workflow_payload: Optional[Dict[str, object]]) -> Optional[Dict[str, object]]:
if not workflow_payload:
return None
for step in workflow_payload.get('steps', []):
if step.get('status') != 'ready':
return {
'step': step.get('step'),
'status': step.get('status'),
'message': step.get('message'),
'code': step.get('code'),
}
return {
'step': None,
'status': 'ready',
'message': None,
'code': None,
}

View File

@@ -1,266 +0,0 @@
"""
Workflow State Service
Manages wizard progress + gating checks for site blueprints.
"""
from __future__ import annotations
import logging
from typing import Dict, List, Optional
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils import timezone
from igny8_core.business.site_building.models import SiteBlueprint, WorkflowState
from igny8_core.business.site_building.services import validators
logger = logging.getLogger(__name__)
DEFAULT_STEPS: List[str] = [
'business_details',
'clusters',
'taxonomies',
'sitemap',
'coverage',
'ideas',
]
STEP_VALIDATORS = {
'clusters': validators.ensure_clusters_attached,
'taxonomies': validators.ensure_taxonomies_defined,
'sitemap': validators.ensure_sitemap_ready,
'coverage': validators.ensure_coverage_ready,
'ideas': validators.ensure_ideas_ready,
}
STEP_CODES = {
'business_details': 'missing_business_details',
'clusters': 'missing_clusters',
'taxonomies': 'missing_taxonomies',
'sitemap': 'sitemap_not_generated',
'coverage': 'coverage_incomplete',
'ideas': 'ideas_not_ready',
}
class WorkflowStateService:
"""Centralizes workflow persistence + validation logic."""
def __init__(self):
self.enabled = getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False)
def initialize(self, site_blueprint: SiteBlueprint) -> Optional[WorkflowState]:
if not self.enabled or not site_blueprint:
return None
state, _ = WorkflowState.objects.get_or_create(
site_blueprint=site_blueprint,
defaults={
'current_step': WorkflowState.DEFAULT_STEP,
'step_status': {},
},
)
return state
def refresh_state(self, site_blueprint: SiteBlueprint) -> Optional[WorkflowState]:
"""Re-run validators to keep the state snapshot fresh."""
state = self.initialize(site_blueprint)
if not state:
return None
timestamp = timezone.now().isoformat()
step_status: Dict[str, Dict[str, str]] = state.step_status or {}
blocking_reason = None
for step in DEFAULT_STEPS:
validator = STEP_VALIDATORS.get(step)
try:
if validator:
validator(site_blueprint)
step_status[step] = self._build_step_entry(
step=step,
status='ready',
message=None,
timestamp=timestamp,
)
except ValidationError as exc:
message = str(exc)
step_status[step] = self._build_step_entry(
step=step,
status='blocked',
message=message,
timestamp=timestamp,
)
if not blocking_reason:
blocking_reason = message
self._emit_event(site_blueprint, 'wizard_blocking_issue', {
'step': step,
'message': message,
})
state.step_status = step_status
state.blocking_reason = blocking_reason
state.completed = all(value.get('status') == 'ready' for value in step_status.values())
# Ensure account/site/sector are set from blueprint before saving
update_fields = ['step_status', 'blocking_reason', 'completed', 'updated_at']
if state.site_blueprint:
if state.site_blueprint.account_id:
state.account_id = state.site_blueprint.account_id
update_fields.append('account')
if state.site_blueprint.site_id:
state.site_id = state.site_blueprint.site_id
update_fields.append('site')
if state.site_blueprint.sector_id:
state.sector_id = state.site_blueprint.sector_id
update_fields.append('sector')
try:
state.save(update_fields=update_fields)
except Exception as e:
logger.error(
f"Failed to save workflow state for blueprint {site_blueprint.id}: {str(e)}. "
f"Blueprint fields: account_id={site_blueprint.account_id}, site_id={site_blueprint.site_id}, sector_id={site_blueprint.sector_id}",
exc_info=True
)
raise
return state
def update_step(
self,
site_blueprint: SiteBlueprint,
step: str,
status: str,
metadata: Optional[Dict[str, str]] = None,
) -> Optional[WorkflowState]:
"""Persist explicit step updates coming from the wizard."""
if not self.enabled:
return None
state = self.initialize(site_blueprint)
if not state:
return None
metadata = metadata or {}
timestamp = timezone.now().isoformat()
# Ensure step_status is a dict (handle None case)
if state.step_status is None:
state.step_status = {}
step_status = dict(state.step_status) # Create a copy to avoid mutation issues
entry = self._build_step_entry(
step=step,
status=status,
message=metadata.get('message'),
timestamp=timestamp,
)
entry.update({k: v for k, v in metadata.items() if k not in entry})
step_status[step] = entry
if step in DEFAULT_STEPS:
state.current_step = step
state.step_status = step_status
state.blocking_reason = metadata.get('message')
# Calculate completed status - only true if all steps are ready and we have at least one step
if step_status:
state.completed = all(
value.get('status') == 'ready' or value.get('status') == 'complete'
for value in step_status.values()
)
else:
state.completed = False
# Ensure account/site/sector are set from blueprint before saving
update_fields = ['current_step', 'step_status', 'blocking_reason', 'completed', 'updated_at']
if state.site_blueprint:
if state.site_blueprint.account_id:
state.account_id = state.site_blueprint.account_id
update_fields.append('account')
if state.site_blueprint.site_id:
state.site_id = state.site_blueprint.site_id
update_fields.append('site')
if state.site_blueprint.sector_id:
state.sector_id = state.site_blueprint.sector_id
update_fields.append('sector')
try:
state.save(update_fields=update_fields)
except Exception as e:
logger.error(
f"Failed to save workflow state for blueprint {site_blueprint.id}: {str(e)}. "
f"Blueprint fields: account_id={site_blueprint.account_id}, site_id={site_blueprint.site_id}, sector_id={site_blueprint.sector_id}",
exc_info=True
)
raise
self._emit_event(site_blueprint, 'wizard_step_updated', {
'step': step,
'status': status,
})
return state
def validate_step(self, site_blueprint: SiteBlueprint, step: str) -> None:
"""Run validator for a single step (raises ValidationError when blocked)."""
if not self.enabled:
return
validator = STEP_VALIDATORS.get(step)
if not validator:
return
validator(site_blueprint)
def serialize_state(self, state: Optional[WorkflowState]) -> Optional[Dict[str, object]]:
"""Return a stable payload for API consumers."""
if not self.enabled or not state:
return None
step_status = state.step_status or {}
steps_payload = []
for step in DEFAULT_STEPS:
meta = step_status.get(step, {})
steps_payload.append({
'step': step,
'status': meta.get('status', 'pending'),
'code': meta.get('code') or STEP_CODES.get(step),
'message': meta.get('message'),
'updated_at': meta.get('updated_at') or state.updated_at.isoformat(),
})
return {
'current_step': state.current_step,
'completed': state.completed,
'blocking_reason': state.blocking_reason,
'steps': steps_payload,
'updated_at': state.updated_at.isoformat() if hasattr(state.updated_at, 'isoformat') else str(state.updated_at),
}
def _build_step_entry(
self,
step: str,
status: str,
message: Optional[str],
timestamp: str,
) -> Dict[str, Optional[str]]:
return {
'status': status,
'code': STEP_CODES.get(step),
'message': message,
'updated_at': timestamp,
}
def _emit_event(self, site_blueprint: SiteBlueprint, event: str, payload: Optional[Dict[str, object]] = None) -> None:
if not self.enabled:
return
payload = payload or {}
logger.info(
"Wizard event: %s blueprint=%s site=%s account=%s payload=%s",
event,
site_blueprint.id,
site_blueprint.site_id,
site_blueprint.account_id,
payload,
)

View File

@@ -8,9 +8,7 @@ from igny8_core.business.site_building.models import (
HeroImageryDirection,
PageBlueprint,
SiteBlueprint,
WorkflowState,
)
from igny8_core.business.site_building.services.workflow_state_service import WorkflowStateService
class PageBlueprintSerializer(serializers.ModelSerializer):
@@ -48,8 +46,6 @@ class SiteBlueprintSerializer(serializers.ModelSerializer):
site_id = serializers.IntegerField(required=False, read_only=True)
sector_id = serializers.IntegerField(required=False, read_only=True)
account_id = serializers.IntegerField(read_only=True)
workflow_state = serializers.SerializerMethodField()
gating_messages = serializers.SerializerMethodField()
class Meta:
model = SiteBlueprint
@@ -69,8 +65,6 @@ class SiteBlueprintSerializer(serializers.ModelSerializer):
'created_at',
'updated_at',
'pages',
'workflow_state',
'gating_messages',
]
read_only_fields = [
'structure_json',
@@ -92,33 +86,6 @@ class SiteBlueprintSerializer(serializers.ModelSerializer):
attrs['sector_id'] = sector_id
return attrs
def get_workflow_state(self, obj):
return self._get_workflow_payload(obj)
def get_gating_messages(self, obj):
workflow_payload = self._get_workflow_payload(obj)
if not workflow_payload:
return None
blocked = [step for step in workflow_payload.get('steps', []) if step.get('status') == 'blocked']
return blocked or None
def _get_workflow_payload(self, obj):
if not getattr(settings, 'USE_SITE_BUILDER_REFACTOR', False):
return None
cache = self.context.setdefault('_workflow_state_cache', {})
if obj.id in cache:
return cache[obj.id]
try:
state: WorkflowState = obj.workflow_state
except WorkflowState.DoesNotExist:
state = None
service = getattr(self, '_workflow_service', None)
if service is None:
service = WorkflowStateService()
self._workflow_service = service
payload = service.serialize_state(state)
cache[obj.id] = payload
return payload
class MetadataOptionSerializer(serializers.Serializer):

View File

@@ -28,7 +28,6 @@ from igny8_core.business.site_building.services import (
SiteBuilderFileService,
StructureGenerationService,
TaxonomyService,
WizardContextService,
)
from igny8_core.modules.site_builder.serializers import (
PageBlueprintSerializer,
@@ -51,7 +50,6 @@ class SiteBlueprintViewSet(SiteSectorModelViewSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.taxonomy_service = TaxonomyService()
self.wizard_context_service = WizardContextService()
def get_permissions(self):
"""