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:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user