303 lines
12 KiB
Python
303 lines
12 KiB
Python
"""
|
|
Publisher ViewSet
|
|
Phase 5: Sites Renderer & Publishing
|
|
"""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from rest_framework import status, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.utils.decorators import method_decorator
|
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
|
|
|
from igny8_core.api.base import SiteSectorModelViewSet
|
|
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove, IsViewerOrAbove
|
|
from igny8_core.api.response import success_response, error_response
|
|
from igny8_core.api.throttles import DebugScopedRateThrottle
|
|
from igny8_core.business.publishing.models import PublishingRecord, DeploymentRecord
|
|
from igny8_core.business.publishing.services.publisher_service import PublisherService
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=['Publisher']),
|
|
create=extend_schema(tags=['Publisher']),
|
|
retrieve=extend_schema(tags=['Publisher']),
|
|
update=extend_schema(tags=['Publisher']),
|
|
partial_update=extend_schema(tags=['Publisher']),
|
|
destroy=extend_schema(tags=['Publisher']),
|
|
)
|
|
class PublishingRecordViewSet(SiteSectorModelViewSet):
|
|
"""
|
|
ViewSet for PublishingRecord model.
|
|
"""
|
|
queryset = PublishingRecord.objects.select_related('content')
|
|
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
|
|
throttle_scope = 'publisher'
|
|
throttle_classes = [DebugScopedRateThrottle]
|
|
|
|
def get_permissions(self):
|
|
if self.action in ['list', 'retrieve']:
|
|
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
|
|
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
|
|
|
|
def get_serializer_class(self):
|
|
# Dynamically create serializer
|
|
from rest_framework import serializers
|
|
|
|
class PublishingRecordSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = PublishingRecord
|
|
exclude = [] # Legacy: site_blueprint field removed from model
|
|
|
|
return PublishingRecordSerializer
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=['Publisher']),
|
|
create=extend_schema(tags=['Publisher']),
|
|
retrieve=extend_schema(tags=['Publisher']),
|
|
update=extend_schema(tags=['Publisher']),
|
|
partial_update=extend_schema(tags=['Publisher']),
|
|
destroy=extend_schema(tags=['Publisher']),
|
|
)
|
|
class DeploymentRecordViewSet(SiteSectorModelViewSet):
|
|
"""
|
|
ViewSet for DeploymentRecord model.
|
|
Legacy: SiteBlueprint functionality removed.
|
|
"""
|
|
queryset = DeploymentRecord.objects.all()
|
|
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
|
|
throttle_scope = 'publisher'
|
|
throttle_classes = [DebugScopedRateThrottle]
|
|
|
|
def get_permissions(self):
|
|
if self.action in ['list', 'retrieve']:
|
|
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
|
|
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
|
|
|
|
def get_serializer_class(self):
|
|
# Dynamically create serializer
|
|
from rest_framework import serializers
|
|
|
|
class DeploymentRecordSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = DeploymentRecord
|
|
exclude = [] # Legacy: site_blueprint field removed from model
|
|
|
|
return DeploymentRecordSerializer
|
|
|
|
|
|
@extend_schema_view()
|
|
class PublisherViewSet(viewsets.ViewSet):
|
|
"""
|
|
Publisher actions for publishing content.
|
|
Legacy SiteBlueprint publishing removed - only content publishing supported.
|
|
"""
|
|
permission_classes = [IsAuthenticatedAndActive, IsEditorOrAbove]
|
|
throttle_scope = 'publisher'
|
|
throttle_classes = [DebugScopedRateThrottle]
|
|
|
|
def get_permissions(self):
|
|
if self.action == 'get_status':
|
|
return [IsAuthenticatedAndActive(), IsViewerOrAbove()]
|
|
return [IsAuthenticatedAndActive(), IsEditorOrAbove()]
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.publisher_service = PublisherService()
|
|
|
|
@extend_schema(tags=['Publisher'])
|
|
@action(detail=False, methods=['post'], url_path='publish')
|
|
def publish(self, request):
|
|
"""
|
|
Publish content to destinations.
|
|
|
|
Request body:
|
|
{
|
|
"content_id": 123, # Required: content to publish
|
|
"destinations": ["wordpress"] # Required: list of destinations
|
|
}
|
|
"""
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
content_id = request.data.get('content_id')
|
|
destinations = request.data.get('destinations', [])
|
|
|
|
logger.info(f"[PublisherViewSet.publish] 🚀 Publish request received: content_id={content_id}, destinations={destinations}")
|
|
|
|
if not content_id:
|
|
return error_response(
|
|
'content_id is required',
|
|
status.HTTP_400_BAD_REQUEST,
|
|
request
|
|
)
|
|
|
|
if not destinations:
|
|
return error_response(
|
|
'destinations is required',
|
|
status.HTTP_400_BAD_REQUEST,
|
|
request
|
|
)
|
|
|
|
account = request.account
|
|
|
|
# Publish content
|
|
logger.info(f"[PublisherViewSet.publish] 📝 Publishing content {content_id} to {destinations}")
|
|
result = self.publisher_service.publish_content(
|
|
content_id,
|
|
destinations,
|
|
account
|
|
)
|
|
logger.info(f"[PublisherViewSet.publish] {'✅' if result.get('success') else '❌'} Publish result: {result}")
|
|
return success_response(result, request=request)
|
|
|
|
@action(detail=False, methods=['get'], url_path='status/(?P<id>[^/.]+)')
|
|
def get_status(self, request, id):
|
|
"""
|
|
Get publishing status.
|
|
|
|
GET /api/v1/publisher/status/{id}/
|
|
"""
|
|
account = request.account
|
|
|
|
# Get publishing record
|
|
try:
|
|
publishing = PublishingRecord.objects.get(id=id, account=account)
|
|
return success_response({
|
|
'type': 'publishing',
|
|
'status': publishing.status,
|
|
'destination': publishing.destination,
|
|
'destination_url': publishing.destination_url,
|
|
'published_at': publishing.published_at,
|
|
'error_message': publishing.error_message,
|
|
}, request=request)
|
|
except PublishingRecord.DoesNotExist:
|
|
return error_response(
|
|
f'Record {id} not found',
|
|
status.HTTP_404_NOT_FOUND,
|
|
request
|
|
)
|
|
|
|
|
|
@method_decorator(csrf_exempt, name='dispatch')
|
|
class SiteDefinitionView(APIView):
|
|
"""
|
|
Public endpoint to serve deployed site definitions.
|
|
Used by Sites Renderer to load site definitions.
|
|
No authentication required for public sites.
|
|
"""
|
|
permission_classes = [] # Public endpoint
|
|
authentication_classes = [] # No authentication required
|
|
|
|
def get(self, request, site_id):
|
|
"""
|
|
Get deployed site definition.
|
|
|
|
GET /api/v1/publisher/sites/{site_id}/definition/
|
|
"""
|
|
try:
|
|
sites_data_path = os.getenv('SITES_DATA_PATH', '/data/app/sites-data')
|
|
|
|
# Try to find latest deployed version
|
|
site_dir = Path(sites_data_path) / 'clients' / str(site_id)
|
|
|
|
if not site_dir.exists():
|
|
return error_response(
|
|
error=f'Site {site_id} not found at {site_dir}. Site may not be deployed yet.',
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
request=request
|
|
)
|
|
|
|
# Look for latest version (check for 'latest' symlink or highest version number)
|
|
latest_path = site_dir / 'latest' / 'site.json'
|
|
if latest_path.exists():
|
|
try:
|
|
with open(latest_path, 'r', encoding='utf-8') as f:
|
|
definition = json.load(f)
|
|
return Response(definition, status=status.HTTP_200_OK)
|
|
except json.JSONDecodeError as e:
|
|
return error_response(
|
|
error=f'Invalid JSON in site definition file: {str(e)}',
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
request=request
|
|
)
|
|
except Exception as e:
|
|
return error_response(
|
|
error=f'Failed to load site definition from {latest_path}: {str(e)}',
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
request=request
|
|
)
|
|
|
|
# Fallback: find highest version number
|
|
try:
|
|
version_dirs = [d for d in site_dir.iterdir() if d.is_dir() and d.name.startswith('v')]
|
|
except PermissionError as e:
|
|
return error_response(
|
|
error=f'Permission denied accessing site directory: {str(e)}',
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
request=request
|
|
)
|
|
except Exception as e:
|
|
return error_response(
|
|
error=f'Error reading site directory: {str(e)}',
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
request=request
|
|
)
|
|
|
|
if not version_dirs:
|
|
return error_response(
|
|
error=f'No deployed versions found for site {site_id}. Site may not be deployed yet.',
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
request=request
|
|
)
|
|
|
|
# Sort by version number (extract number from 'v1', 'v2', etc.)
|
|
try:
|
|
version_dirs.sort(key=lambda d: int(d.name[1:]) if d.name[1:].isdigit() else 0, reverse=True)
|
|
latest_version_dir = version_dirs[0]
|
|
site_json_path = latest_version_dir / 'site.json'
|
|
|
|
if site_json_path.exists():
|
|
with open(site_json_path, 'r', encoding='utf-8') as f:
|
|
definition = json.load(f)
|
|
return Response(definition, status=status.HTTP_200_OK)
|
|
else:
|
|
return error_response(
|
|
error=f'Site definition file not found at {site_json_path}',
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
request=request
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
return error_response(
|
|
error=f'Invalid JSON in site definition file: {str(e)}',
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
request=request
|
|
)
|
|
except Exception as e:
|
|
return error_response(
|
|
error=f'Failed to load site definition: {str(e)}',
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
request=request
|
|
)
|
|
|
|
return error_response(
|
|
error=f'Site definition not found for site {site_id}',
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
request=request
|
|
)
|
|
except Exception as e:
|
|
# Catch any unhandled exceptions
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f'Unhandled exception in SiteDefinitionView: {str(e)}', exc_info=True)
|
|
return error_response(
|
|
error=f'Internal server error: {str(e)}',
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
request=request
|
|
)
|
|
|