Add source tracking and sync status fields to Content model; update services module
- Introduced new fields in the Content model for source tracking and sync status, including external references and optimization fields. - Updated the services module to include new content generation and pipeline services for better organization and clarity.
This commit is contained in:
6
backend/igny8_core/business/site_building/__init__.py
Normal file
6
backend/igny8_core/business/site_building/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Site Building Business Logic
|
||||
Phase 3: Site Builder
|
||||
"""
|
||||
|
||||
|
||||
168
backend/igny8_core/business/site_building/models.py
Normal file
168
backend/igny8_core/business/site_building/models.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Site Builder Models
|
||||
Phase 3: Site Builder
|
||||
"""
|
||||
from django.db import models
|
||||
from django.core.validators import MinValueValidator
|
||||
from igny8_core.auth.models import SiteSectorBaseModel
|
||||
|
||||
|
||||
class SiteBlueprint(SiteSectorBaseModel):
|
||||
"""
|
||||
Site Blueprint model for storing AI-generated site structures.
|
||||
"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('draft', 'Draft'),
|
||||
('generating', 'Generating'),
|
||||
('ready', 'Ready'),
|
||||
('deployed', 'Deployed'),
|
||||
]
|
||||
|
||||
HOSTING_TYPE_CHOICES = [
|
||||
('igny8_sites', 'IGNY8 Sites'),
|
||||
('wordpress', 'WordPress'),
|
||||
('shopify', 'Shopify'),
|
||||
('multi', 'Multiple Destinations'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=255, help_text="Site name")
|
||||
description = models.TextField(blank=True, null=True, help_text="Site description")
|
||||
|
||||
# Site configuration (from wizard)
|
||||
config_json = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Wizard configuration: business_type, style, objectives, etc."
|
||||
)
|
||||
|
||||
# Generated structure (from AI)
|
||||
structure_json = models.JSONField(
|
||||
default=dict,
|
||||
help_text="AI-generated structure: pages, layout, theme, etc."
|
||||
)
|
||||
|
||||
# Status tracking
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='draft',
|
||||
db_index=True,
|
||||
help_text="Blueprint status"
|
||||
)
|
||||
|
||||
# Hosting configuration
|
||||
hosting_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=HOSTING_TYPE_CHOICES,
|
||||
default='igny8_sites',
|
||||
help_text="Target hosting platform"
|
||||
)
|
||||
|
||||
# Version tracking
|
||||
version = models.IntegerField(default=1, validators=[MinValueValidator(1)], help_text="Blueprint version")
|
||||
deployed_version = models.IntegerField(null=True, blank=True, help_text="Currently deployed version")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
app_label = 'site_building'
|
||||
db_table = 'igny8_site_blueprints'
|
||||
ordering = ['-created_at']
|
||||
verbose_name = 'Site Blueprint'
|
||||
verbose_name_plural = 'Site Blueprints'
|
||||
indexes = [
|
||||
models.Index(fields=['status']),
|
||||
models.Index(fields=['hosting_type']),
|
||||
models.Index(fields=['site', 'sector']),
|
||||
models.Index(fields=['account', 'status']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.get_status_display()})"
|
||||
|
||||
|
||||
class PageBlueprint(SiteSectorBaseModel):
|
||||
"""
|
||||
Page Blueprint model for storing individual page definitions.
|
||||
"""
|
||||
|
||||
PAGE_TYPE_CHOICES = [
|
||||
('home', 'Home'),
|
||||
('about', 'About'),
|
||||
('services', 'Services'),
|
||||
('products', 'Products'),
|
||||
('blog', 'Blog'),
|
||||
('contact', 'Contact'),
|
||||
('custom', 'Custom'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('draft', 'Draft'),
|
||||
('generating', 'Generating'),
|
||||
('ready', 'Ready'),
|
||||
]
|
||||
|
||||
site_blueprint = models.ForeignKey(
|
||||
SiteBlueprint,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='pages',
|
||||
help_text="The site blueprint this page belongs to"
|
||||
)
|
||||
slug = models.SlugField(max_length=255, help_text="Page URL slug")
|
||||
title = models.CharField(max_length=255, help_text="Page title")
|
||||
|
||||
# Page type
|
||||
type = models.CharField(
|
||||
max_length=50,
|
||||
choices=PAGE_TYPE_CHOICES,
|
||||
default='custom',
|
||||
help_text="Page type"
|
||||
)
|
||||
|
||||
# Page content (blocks)
|
||||
blocks_json = models.JSONField(
|
||||
default=list,
|
||||
help_text="Page content blocks: [{'type': 'hero', 'data': {...}}, ...]"
|
||||
)
|
||||
|
||||
# Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='draft',
|
||||
db_index=True,
|
||||
help_text="Page status"
|
||||
)
|
||||
|
||||
# Order
|
||||
order = models.IntegerField(default=0, help_text="Page order in navigation")
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
app_label = 'site_building'
|
||||
db_table = 'igny8_page_blueprints'
|
||||
ordering = ['order', 'created_at']
|
||||
verbose_name = 'Page Blueprint'
|
||||
verbose_name_plural = 'Page Blueprints'
|
||||
unique_together = [['site_blueprint', 'slug']]
|
||||
indexes = [
|
||||
models.Index(fields=['site_blueprint', 'status']),
|
||||
models.Index(fields=['type']),
|
||||
models.Index(fields=['site_blueprint', 'order']),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Automatically set account, site, and sector from site_blueprint"""
|
||||
if self.site_blueprint:
|
||||
self.account = self.site_blueprint.account
|
||||
self.site = self.site_blueprint.site
|
||||
self.sector = self.site_blueprint.sector
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.site_blueprint.name})"
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Site Building Services
|
||||
"""
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
"""
|
||||
Site File Management Service
|
||||
Manages file uploads, deletions, and access control for site assets
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from igny8_core.auth.models import User, Site
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Base path for site files
|
||||
SITES_DATA_BASE = Path('/data/app/sites-data/clients')
|
||||
|
||||
|
||||
class SiteBuilderFileService:
|
||||
"""Service for managing site files and assets"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_path = SITES_DATA_BASE
|
||||
self.max_file_size = 10 * 1024 * 1024 # 10MB per file
|
||||
self.max_storage_per_site = 100 * 1024 * 1024 # 100MB per site
|
||||
|
||||
def get_user_accessible_sites(self, user: User) -> List[Site]:
|
||||
"""
|
||||
Get sites user can access for file management.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
|
||||
Returns:
|
||||
List of Site instances user can access
|
||||
"""
|
||||
# Owner/Admin: Full access to all account sites
|
||||
if user.is_owner_or_admin():
|
||||
return Site.objects.filter(account=user.account, is_active=True)
|
||||
|
||||
# Editor/Viewer: Access to granted sites (via SiteUserAccess)
|
||||
# TODO: Implement SiteUserAccess check when available
|
||||
return Site.objects.filter(account=user.account, is_active=True)
|
||||
|
||||
def check_file_access(self, user: User, site_id: int) -> bool:
|
||||
"""
|
||||
Check if user can access site's files.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
site_id: Site ID
|
||||
|
||||
Returns:
|
||||
True if user has access, False otherwise
|
||||
"""
|
||||
accessible_sites = self.get_user_accessible_sites(user)
|
||||
return any(site.id == site_id for site in accessible_sites)
|
||||
|
||||
def get_site_files_path(self, site_id: int, version: int = 1) -> Path:
|
||||
"""
|
||||
Get site's files directory path.
|
||||
|
||||
Args:
|
||||
site_id: Site ID
|
||||
version: Site version (default: 1)
|
||||
|
||||
Returns:
|
||||
Path object for site files directory
|
||||
"""
|
||||
return self.base_path / str(site_id) / f"v{version}" / "assets"
|
||||
|
||||
def check_storage_quota(self, site_id: int, file_size: int) -> bool:
|
||||
"""
|
||||
Check if site has enough storage quota.
|
||||
|
||||
Args:
|
||||
site_id: Site ID
|
||||
file_size: Size of file to upload in bytes
|
||||
|
||||
Returns:
|
||||
True if quota available, False otherwise
|
||||
"""
|
||||
site_path = self.get_site_files_path(site_id)
|
||||
|
||||
# Calculate current storage usage
|
||||
current_usage = self._calculate_storage_usage(site_path)
|
||||
|
||||
# Check if adding file would exceed quota
|
||||
return (current_usage + file_size) <= self.max_storage_per_site
|
||||
|
||||
def _calculate_storage_usage(self, site_path: Path) -> int:
|
||||
"""Calculate current storage usage for a site"""
|
||||
if not site_path.exists():
|
||||
return 0
|
||||
|
||||
total_size = 0
|
||||
for file_path in site_path.rglob('*'):
|
||||
if file_path.is_file():
|
||||
total_size += file_path.stat().st_size
|
||||
|
||||
return total_size
|
||||
|
||||
def upload_file(
|
||||
self,
|
||||
user: User,
|
||||
site_id: int,
|
||||
file,
|
||||
folder: str = 'images',
|
||||
version: int = 1
|
||||
) -> Dict:
|
||||
"""
|
||||
Upload file to site's assets folder.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
site_id: Site ID
|
||||
file: Django UploadedFile instance
|
||||
folder: Subfolder name (images, documents, media)
|
||||
version: Site version
|
||||
|
||||
Returns:
|
||||
Dict with file_path, file_url, file_size
|
||||
|
||||
Raises:
|
||||
PermissionDenied: If user doesn't have access
|
||||
ValidationError: If file size exceeds limit or quota exceeded
|
||||
"""
|
||||
# Check access
|
||||
if not self.check_file_access(user, site_id):
|
||||
raise PermissionDenied("No access to this site")
|
||||
|
||||
# Check file size
|
||||
if file.size > self.max_file_size:
|
||||
raise ValidationError(f"File size exceeds maximum of {self.max_file_size / 1024 / 1024}MB")
|
||||
|
||||
# Check storage quota
|
||||
if not self.check_storage_quota(site_id, file.size):
|
||||
raise ValidationError("Storage quota exceeded")
|
||||
|
||||
# Get target directory
|
||||
site_path = self.get_site_files_path(site_id, version)
|
||||
target_dir = site_path / folder
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save file
|
||||
file_path = target_dir / file.name
|
||||
with open(file_path, 'wb') as f:
|
||||
for chunk in file.chunks():
|
||||
f.write(chunk)
|
||||
|
||||
# Generate file URL (relative to site assets)
|
||||
file_url = f"/sites/{site_id}/v{version}/assets/{folder}/{file.name}"
|
||||
|
||||
logger.info(f"Uploaded file {file.name} to site {site_id}/{folder}")
|
||||
|
||||
return {
|
||||
'file_path': str(file_path),
|
||||
'file_url': file_url,
|
||||
'file_size': file.size,
|
||||
'folder': folder
|
||||
}
|
||||
|
||||
def delete_file(
|
||||
self,
|
||||
user: User,
|
||||
site_id: int,
|
||||
file_path: str,
|
||||
version: int = 1
|
||||
) -> bool:
|
||||
"""
|
||||
Delete file from site's assets.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
site_id: Site ID
|
||||
file_path: Relative file path (e.g., 'images/photo.jpg')
|
||||
version: Site version
|
||||
|
||||
Returns:
|
||||
True if deleted, False otherwise
|
||||
|
||||
Raises:
|
||||
PermissionDenied: If user doesn't have access
|
||||
"""
|
||||
# Check access
|
||||
if not self.check_file_access(user, site_id):
|
||||
raise PermissionDenied("No access to this site")
|
||||
|
||||
# Get full file path
|
||||
site_path = self.get_site_files_path(site_id, version)
|
||||
full_path = site_path / file_path
|
||||
|
||||
# Check if file exists and is within site directory
|
||||
if not full_path.exists() or not str(full_path).startswith(str(site_path)):
|
||||
return False
|
||||
|
||||
# Delete file
|
||||
full_path.unlink()
|
||||
|
||||
logger.info(f"Deleted file {file_path} from site {site_id}")
|
||||
|
||||
return True
|
||||
|
||||
def list_files(
|
||||
self,
|
||||
user: User,
|
||||
site_id: int,
|
||||
folder: Optional[str] = None,
|
||||
version: int = 1
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
List files in site's assets.
|
||||
|
||||
Args:
|
||||
user: User instance
|
||||
site_id: Site ID
|
||||
folder: Optional folder to list (None = all folders)
|
||||
version: Site version
|
||||
|
||||
Returns:
|
||||
List of file dicts with: name, path, size, folder, url
|
||||
|
||||
Raises:
|
||||
PermissionDenied: If user doesn't have access
|
||||
"""
|
||||
# Check access
|
||||
if not self.check_file_access(user, site_id):
|
||||
raise PermissionDenied("No access to this site")
|
||||
|
||||
site_path = self.get_site_files_path(site_id, version)
|
||||
|
||||
if not site_path.exists():
|
||||
return []
|
||||
|
||||
files = []
|
||||
|
||||
# List files in specified folder or all folders
|
||||
if folder:
|
||||
folder_path = site_path / folder
|
||||
if folder_path.exists():
|
||||
files.extend(self._list_directory(folder_path, folder, site_id, version))
|
||||
else:
|
||||
# List all folders
|
||||
for folder_dir in site_path.iterdir():
|
||||
if folder_dir.is_dir():
|
||||
files.extend(self._list_directory(folder_dir, folder_dir.name, site_id, version))
|
||||
|
||||
return files
|
||||
|
||||
def _list_directory(self, directory: Path, folder_name: str, site_id: int, version: int) -> List[Dict]:
|
||||
"""List files in a directory"""
|
||||
files = []
|
||||
for file_path in directory.iterdir():
|
||||
if file_path.is_file():
|
||||
file_url = f"/sites/{site_id}/v{version}/assets/{folder_name}/{file_path.name}"
|
||||
files.append({
|
||||
'name': file_path.name,
|
||||
'path': f"{folder_name}/{file_path.name}",
|
||||
'size': file_path.stat().st_size,
|
||||
'folder': folder_name,
|
||||
'url': file_url
|
||||
})
|
||||
return files
|
||||
|
||||
|
||||
Reference in New Issue
Block a user