Enhance Content Management with New Taxonomy and Attribute Models

- Introduced `ContentTaxonomy` and `ContentAttribute` models for improved content categorization and attribute management.
- Updated `Content` model to support new fields for content format, cluster role, and external type.
- Refactored serializers and views to accommodate new models, including `ContentTaxonomySerializer` and `ContentAttributeSerializer`.
- Added new API endpoints for managing taxonomies and attributes, enhancing the content management capabilities.
- Updated admin interfaces for `Content`, `ContentTaxonomy`, and `ContentAttribute` to reflect new structures and improve usability.
- Implemented backward compatibility for existing attribute mappings.
- Enhanced filtering and search capabilities in the API for better content retrieval.
This commit is contained in:
IGNY8 VPS (Salman)
2025-11-22 00:21:00 +00:00
parent a82be89d21
commit 55dfd5ad19
17 changed files with 2934 additions and 40 deletions

View File

@@ -11,7 +11,14 @@ from igny8_core.api.response import success_response, error_response
from igny8_core.api.throttles import DebugScopedRateThrottle
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsViewerOrAbove, IsEditorOrAbove
from .models import Tasks, Images, Content
from .serializers import TasksSerializer, ImagesSerializer, ContentSerializer
from .serializers import (
TasksSerializer,
ImagesSerializer,
ContentSerializer,
ContentTaxonomySerializer,
ContentAttributeSerializer,
)
from igny8_core.business.content.models import ContentTaxonomy, ContentAttribute
from igny8_core.business.content.services.content_generation_service import ContentGenerationService
from igny8_core.business.content.services.validation_service import ContentValidationService
from igny8_core.business.content.services.metadata_mapping_service import MetadataMappingService
@@ -48,8 +55,8 @@ class TasksViewSet(SiteSectorModelViewSet):
ordering_fields = ['title', 'created_at', 'word_count', 'status']
ordering = ['-created_at'] # Default ordering (newest first)
# Filter configuration
filterset_fields = ['status', 'cluster_id', 'content_type', 'content_structure']
# Filter configuration (removed deprecated fields)
filterset_fields = ['status', 'cluster_id']
def perform_create(self, serializer):
"""Require explicit site_id and sector_id - no defaults."""
@@ -748,10 +755,10 @@ class ImagesViewSet(SiteSectorModelViewSet):
)
class ContentViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing task content
ViewSet for managing content with new unified structure
Unified API Standard v1.0 compliant
"""
queryset = Content.objects.all()
queryset = Content.objects.select_related('task', 'site', 'sector', 'cluster').prefetch_related('taxonomies', 'attributes')
serializer_class = ContentSerializer
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
pagination_class = CustomPageNumberPagination
@@ -759,10 +766,20 @@ class ContentViewSet(SiteSectorModelViewSet):
throttle_classes = [DebugScopedRateThrottle]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['title', 'meta_title', 'primary_keyword']
ordering_fields = ['generated_at', 'updated_at', 'word_count', 'status']
search_fields = ['title', 'meta_title', 'primary_keyword', 'external_url']
ordering_fields = ['generated_at', 'updated_at', 'word_count', 'status', 'entity_type', 'content_format']
ordering = ['-generated_at']
filterset_fields = ['task_id', 'status']
filterset_fields = [
'task_id',
'status',
'entity_type',
'content_format',
'cluster_role',
'source',
'sync_status',
'cluster',
'external_type',
]
def perform_create(self, serializer):
"""Override to automatically set account"""
@@ -1345,6 +1362,210 @@ class ContentViewSet(SiteSectorModelViewSet):
def _has_taxonomy_mapping(self, content):
"""Helper to check if content has taxonomy mapping"""
from igny8_core.business.content.models import ContentTaxonomyMap
return ContentTaxonomyMap.objects.filter(content=content).exists()
# Check new M2M relationship
return content.taxonomies.exists()
@extend_schema_view(
list=extend_schema(tags=['Writer - Taxonomies']),
create=extend_schema(tags=['Writer - Taxonomies']),
retrieve=extend_schema(tags=['Writer - Taxonomies']),
update=extend_schema(tags=['Writer - Taxonomies']),
partial_update=extend_schema(tags=['Writer - Taxonomies']),
destroy=extend_schema(tags=['Writer - Taxonomies']),
)
class ContentTaxonomyViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing content taxonomies (categories, tags, product attributes)
Unified API Standard v1.0 compliant
"""
queryset = ContentTaxonomy.objects.select_related('parent', 'site', 'sector').prefetch_related('clusters', 'contents')
serializer_class = ContentTaxonomySerializer
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
pagination_class = CustomPageNumberPagination
throttle_scope = 'writer'
throttle_classes = [DebugScopedRateThrottle]
# DRF filtering configuration
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
# Search configuration
search_fields = ['name', 'slug', 'description', 'external_taxonomy']
# Ordering configuration
ordering_fields = ['name', 'taxonomy_type', 'count', 'created_at']
ordering = ['taxonomy_type', 'name']
# Filter configuration
filterset_fields = ['taxonomy_type', 'sync_status', 'parent', 'external_id', 'external_taxonomy']
def perform_create(self, serializer):
"""Create taxonomy with site/sector context"""
user = getattr(self.request, 'user', None)
try:
query_params = getattr(self.request, 'query_params', None)
if query_params is None:
query_params = getattr(self.request, 'GET', {})
except AttributeError:
query_params = {}
site_id = serializer.validated_data.get('site_id') or query_params.get('site_id')
sector_id = serializer.validated_data.get('sector_id') or query_params.get('sector_id')
from igny8_core.auth.models import Site, Sector
from rest_framework.exceptions import ValidationError
if not site_id:
raise ValidationError("site_id is required")
try:
site = Site.objects.get(id=site_id)
except Site.DoesNotExist:
raise ValidationError(f"Site with id {site_id} does not exist")
if not sector_id:
raise ValidationError("sector_id is required")
try:
sector = Sector.objects.get(id=sector_id)
if sector.site_id != site_id:
raise ValidationError(f"Sector does not belong to the selected site")
except Sector.DoesNotExist:
raise ValidationError(f"Sector with id {sector_id} does not exist")
serializer.validated_data.pop('site_id', None)
serializer.validated_data.pop('sector_id', None)
account = getattr(self.request, 'account', None)
if not account and user and user.is_authenticated and user.account:
account = user.account
if not account:
account = site.account
serializer.save(account=account, site=site, sector=sector)
@action(detail=True, methods=['post'], permission_classes=[IsAuthenticatedAndActive, IsEditorOrAbove])
def map_to_cluster(self, request, pk=None):
"""Map taxonomy to semantic cluster"""
taxonomy = self.get_object()
cluster_id = request.data.get('cluster_id')
if not cluster_id:
return error_response(
error="cluster_id is required",
status_code=status.HTTP_400_BAD_REQUEST,
request=request
)
from igny8_core.business.planning.models import Clusters
try:
cluster = Clusters.objects.get(id=cluster_id, site=taxonomy.site)
taxonomy.clusters.add(cluster)
return success_response(
data={'message': f'Taxonomy "{taxonomy.name}" mapped to cluster "{cluster.name}"'},
message="Taxonomy mapped to cluster successfully",
request=request
)
except Clusters.DoesNotExist:
return error_response(
error=f"Cluster with id {cluster_id} not found",
status_code=status.HTTP_404_NOT_FOUND,
request=request
)
@action(detail=True, methods=['get'])
def contents(self, request, pk=None):
"""Get all content associated with this taxonomy"""
taxonomy = self.get_object()
contents = taxonomy.contents.all()
serializer = ContentSerializer(contents, many=True, context={'request': request})
return success_response(
data=serializer.data,
message=f"Found {contents.count()} content items for taxonomy '{taxonomy.name}'",
request=request
)
@extend_schema_view(
list=extend_schema(tags=['Writer - Attributes']),
create=extend_schema(tags=['Writer - Attributes']),
retrieve=extend_schema(tags=['Writer - Attributes']),
update=extend_schema(tags=['Writer - Attributes']),
partial_update=extend_schema(tags=['Writer - Attributes']),
destroy=extend_schema(tags=['Writer - Attributes']),
)
class ContentAttributeViewSet(SiteSectorModelViewSet):
"""
ViewSet for managing content attributes (product specs, service modifiers, semantic facets)
Unified API Standard v1.0 compliant
"""
queryset = ContentAttribute.objects.select_related('content', 'cluster', 'site', 'sector')
serializer_class = ContentAttributeSerializer
permission_classes = [IsAuthenticatedAndActive, IsViewerOrAbove]
pagination_class = CustomPageNumberPagination
throttle_scope = 'writer'
throttle_classes = [DebugScopedRateThrottle]
# DRF filtering configuration
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
# Search configuration
search_fields = ['name', 'value', 'external_attribute_name', 'content__title']
# Ordering configuration
ordering_fields = ['name', 'attribute_type', 'created_at']
ordering = ['attribute_type', 'name']
# Filter configuration
filterset_fields = ['attribute_type', 'source', 'content', 'cluster', 'external_id']
def perform_create(self, serializer):
"""Create attribute with site/sector context"""
user = getattr(self.request, 'user', None)
try:
query_params = getattr(self.request, 'query_params', None)
if query_params is None:
query_params = getattr(self.request, 'GET', {})
except AttributeError:
query_params = {}
site_id = serializer.validated_data.get('site_id') or query_params.get('site_id')
sector_id = serializer.validated_data.get('sector_id') or query_params.get('sector_id')
from igny8_core.auth.models import Site, Sector
from rest_framework.exceptions import ValidationError
if not site_id:
raise ValidationError("site_id is required")
try:
site = Site.objects.get(id=site_id)
except Site.DoesNotExist:
raise ValidationError(f"Site with id {site_id} does not exist")
if not sector_id:
raise ValidationError("sector_id is required")
try:
sector = Sector.objects.get(id=sector_id)
if sector.site_id != site_id:
raise ValidationError(f"Sector does not belong to the selected site")
except Sector.DoesNotExist:
raise ValidationError(f"Sector with id {sector_id} does not exist")
serializer.validated_data.pop('site_id', None)
serializer.validated_data.pop('sector_id', None)
account = getattr(self.request, 'account', None)
if not account and user and user.is_authenticated and user.account:
account = user.account
if not account:
account = site.account
serializer.save(account=account, site=site, sector=sector)