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:
IGNY8 VPS (Salman)
2025-11-17 11:15:15 +00:00
parent fe95d09bbe
commit 9930728e8a
19 changed files with 2281 additions and 1 deletions

View File

@@ -0,0 +1,6 @@
"""
Site Building Business Logic
Phase 3: Site Builder
"""

View 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})"

View File

@@ -0,0 +1,5 @@
"""
Site Building Services
"""

View File

@@ -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