import logging from django.db.models import Q, F from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.permissions import AllowAny, IsAuthenticated, IsAdminUser from rest_framework.response import Response from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiResponse from apps.cms.models.post import ( PostCategory, PostTag, Post, PostComment, PostLike, MediaAsset, ) from apps.cms.permissions import IsAuthorOrEditorOrReadOnly, CanPublishPost from apps.cms.serializers import ( PostCategorySerializer, PostTagSerializer, PostCommentSerializer, PostCommentCreateSerializer, PostListSerializer, PostDetailSerializer, PostCreateUpdateSerializer, PostLikeResponseSerializer, MediaAssetSerializer, ) from utils.pagination import StandardResultsSetPagination logger = logging.getLogger(__name__) def get_post_by_id_or_slug(lookup_val, user=None): qs = Post.objects.all() # Filter visible status if anonymous if not user or not user.is_authenticated or not (user.is_staff or getattr(user, 'is_super_admin', False) or getattr(user, 'is_regional_admin', False)): qs = qs.filter(status=Post.Status.PUBLISHED) if str(lookup_val).isdigit(): return get_object_or_404(qs, id=int(lookup_val)) return get_object_or_404(qs, slug=lookup_val) class PostListCreateView(GenericAPIView): serializer_class = PostListSerializer pagination_class = StandardResultsSetPagination def get_permissions(self): if self.request.method == 'POST': return [IsAuthenticated(), CanPublishPost()] return [AllowAny()] def get_queryset(self): if getattr(self, 'swagger_fake_view', False): return Post.objects.none() user = self.request.user qs = Post.objects.all().select_related('author', 'category', 'institution') # Visibility filter: non-staff only see published posts unless filtering by my_posts is_admin = user.is_authenticated and (user.is_staff or getattr(user, 'is_super_admin', False) or getattr(user, 'is_regional_admin', False)) status_param = self.request.query_params.get('status') if status_param and is_admin: qs = qs.filter(status=status_param) elif not is_admin: # If requesting own posts, can see drafts my_posts = self.request.query_params.get('my_posts') if my_posts and user.is_authenticated: qs = qs.filter(author=user) if status_param: qs = qs.filter(status=status_param) else: qs = qs.filter(status=Post.Status.PUBLISHED) # Filters post_type = self.request.query_params.get('post_type') if post_type: qs = qs.filter(post_type=post_type) language = self.request.query_params.get('language') if language: qs = qs.filter(language=language) category = self.request.query_params.get('category') if category: if str(category).isdigit(): qs = qs.filter(category_id=int(category)) else: qs = qs.filter(category__slug=category) tag = self.request.query_params.get('tag') if tag: qs = qs.filter(tags__icontains=tag) institution_id = self.request.query_params.get('institution_id') if institution_id: qs = qs.filter(institution_id=institution_id) author_id = self.request.query_params.get('author_id') if author_id: qs = qs.filter(author_id=author_id) is_featured = self.request.query_params.get('is_featured') if is_featured is not None: qs = qs.filter(is_featured=is_featured.lower() in ['true', '1']) search = self.request.query_params.get('search') if search: qs = qs.filter( Q(title__icontains=search) | Q(excerpt__icontains=search) | Q(content__icontains=search) ) return qs.order_by('-is_featured', '-publish_date') @extend_schema( summary="List multilingual posts & articles", description="Returns a paginated list of published news, articles, and interviews with multilingual filtering.", parameters=[ OpenApiParameter('post_type', str, description='Filter by type (news, article, report, interview)'), OpenApiParameter('language', str, description='Filter by language code (fa, ar, en, ur, fr)'), OpenApiParameter('category', str, description='Filter by category ID or slug'), OpenApiParameter('tag', str, description='Filter by tag string'), OpenApiParameter('institution_id', int, description='Filter by associated institution ID'), OpenApiParameter('author_id', int, description='Filter by author user ID'), OpenApiParameter('is_featured', bool, description='Filter by featured status'), OpenApiParameter('search', str, description='Search text in title, excerpt, and content'), OpenApiParameter('status', str, description='Filter by status (staff/admin only)'), OpenApiParameter('my_posts', bool, description='Filter by current authenticated user posts'), ], responses={200: PostListSerializer(many=True)}, tags=["Content Management System (CMS)"], ) def get(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True, context={'request': request}) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Publish or draft new post", description="Creates a new article, news announcement, or report.", request=PostCreateUpdateSerializer, responses={ 201: PostDetailSerializer, 400: OpenApiResponse(description="Validation error"), 401: OpenApiResponse(description="Authentication required"), }, tags=["Content Management System (CMS)"], ) def post(self, request, *args, **kwargs): serializer = PostCreateUpdateSerializer(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) post_obj = serializer.save() response_serializer = PostDetailSerializer(post_obj, context={'request': request}) return Response(response_serializer.data, status=status.HTTP_201_CREATED) class PostDetailView(GenericAPIView): serializer_class = PostDetailSerializer queryset = Post.objects.all() def get_permissions(self): if self.request.method in ['PATCH', 'PUT', 'DELETE']: return [IsAuthenticated(), IsAuthorOrEditorOrReadOnly()] return [AllowAny()] def get_object(self): lookup = self.kwargs.get('pk_or_slug') post_obj = get_post_by_id_or_slug(lookup, self.request.user) self.check_object_permissions(self.request, post_obj) return post_obj @extend_schema( summary="Get full post details", description="Retrieves full article content and increments the view counter.", responses={ 200: PostDetailSerializer, 404: OpenApiResponse(description="Post not found"), }, tags=["Content Management System (CMS)"], ) def get(self, request, pk_or_slug, *args, **kwargs): post_obj = self.get_object() # Increment views counter Post.objects.filter(id=post_obj.id).update(views_count=F('views_count') + 1) post_obj.refresh_from_db() serializer = PostDetailSerializer(post_obj, context={'request': request}) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Update post", description="Updates an existing post. Permitted for original author, institution editors, or platform admins.", request=PostCreateUpdateSerializer, responses={ 200: PostDetailSerializer, 403: OpenApiResponse(description="Permission denied"), }, tags=["Content Management System (CMS)"], ) def patch(self, request, pk_or_slug, *args, **kwargs): post_obj = self.get_object() serializer = PostCreateUpdateSerializer( post_obj, data=request.data, partial=True, context={'request': request} ) serializer.is_valid(raise_exception=True) serializer.save() response_serializer = PostDetailSerializer(post_obj, context={'request': request}) return Response(response_serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Delete post", description="Removes a post from the platform.", responses={ 204: OpenApiResponse(description="Post deleted successfully"), 403: OpenApiResponse(description="Permission denied"), }, tags=["Content Management System (CMS)"], ) def delete(self, request, pk_or_slug, *args, **kwargs): post_obj = self.get_object() post_obj.delete() return Response(status=status.HTTP_204_NO_CONTENT) class PostLikeToggleView(GenericAPIView): permission_classes = [IsAuthenticated] serializer_class = PostLikeResponseSerializer queryset = PostLike.objects.all() @extend_schema( summary="Toggle like on post", description="Likes or unlikes an article for the authenticated user and updates the like counter.", responses={200: PostLikeResponseSerializer}, tags=["Content Management System (CMS)"], ) def post(self, request, pk_or_slug, *args, **kwargs): post_obj = get_post_by_id_or_slug(pk_or_slug, request.user) like_obj, created = PostLike.objects.get_or_create( post=post_obj, user=request.user ) if created: Post.objects.filter(id=post_obj.id).update(likes_count=F('likes_count') + 1) is_liked = True msg = _("Post liked.") else: like_obj.delete() Post.objects.filter(id=post_obj.id, likes_count__gt=0).update(likes_count=F('likes_count') - 1) is_liked = False msg = _("Post unliked.") post_obj.refresh_from_db() return Response({ 'is_liked': is_liked, 'likes_count': post_obj.likes_count, 'message': msg }, status=status.HTTP_200_OK) class PostCommentsView(GenericAPIView): serializer_class = PostCommentSerializer queryset = PostComment.objects.all() def get_permissions(self): return [AllowAny()] @extend_schema( summary="List post comments", description="Retrieves approved reader comments for a post.", responses={200: PostCommentSerializer(many=True)}, tags=["Content Management System (CMS)"], ) def get(self, request, pk_or_slug, *args, **kwargs): post_obj = get_post_by_id_or_slug(pk_or_slug, request.user) comments = post_obj.comments.filter(status=PostComment.Status.APPROVED).order_by('-created_at') serializer = PostCommentSerializer(comments, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Submit post comment", description="Submits a comment on an article.", request=PostCommentCreateSerializer, responses={ 201: PostCommentSerializer, 400: OpenApiResponse(description="Validation error"), }, tags=["Content Management System (CMS)"], ) def post(self, request, pk_or_slug, *args, **kwargs): post_obj = get_post_by_id_or_slug(pk_or_slug, request.user) serializer = PostCommentCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) user = request.user if request.user.is_authenticated else None author_name = serializer.validated_data.get('author_name') or (user.fullname if user else "Visitor") author_email = serializer.validated_data.get('author_email') or (user.email if user else "") comment = PostComment.objects.create( post=post_obj, user=user, author_name=author_name, author_email=author_email, content=serializer.validated_data['content'], status=PostComment.Status.APPROVED ) return Response(PostCommentSerializer(comment).data, status=status.HTTP_201_CREATED) class PostCategoryListView(GenericAPIView): permission_classes = [AllowAny] serializer_class = PostCategorySerializer queryset = PostCategory.objects.all() @extend_schema( summary="List post categories", description="Returns content categories with optional language filtering.", parameters=[ OpenApiParameter('language', str, description='Filter categories by language code (fa, ar, en, ur, fr)') ], responses={200: PostCategorySerializer(many=True)}, tags=["Content Management System (CMS)"], ) def get(self, request, *args, **kwargs): qs = PostCategory.objects.all() lang = request.query_params.get('language') if lang: qs = qs.filter(language=lang) serializer = PostCategorySerializer(qs, many=True) return Response(serializer.data, status=status.HTTP_200_OK) class PostTagListView(GenericAPIView): permission_classes = [AllowAny] serializer_class = PostTagSerializer queryset = PostTag.objects.all() @extend_schema( summary="List CMS tags", description="Returns distinct content tags used across posts.", responses={200: PostTagSerializer(many=True)}, tags=["Content Management System (CMS)"], ) def get(self, request, *args, **kwargs): tags = PostTag.objects.all() serializer = PostTagSerializer(tags, many=True) return Response(serializer.data, status=status.HTTP_200_OK) class MediaAssetListCreateView(GenericAPIView): serializer_class = MediaAssetSerializer parser_classes = [MultiPartParser, FormParser, JSONParser] queryset = MediaAsset.objects.all() def get_permissions(self): if self.request.method == 'POST': return [IsAuthenticated()] return [AllowAny()] @extend_schema( summary="List media library assets", description="Retrieves uploaded photos, videos, and documents.", parameters=[ OpenApiParameter('media_type', str, description='Filter by media type (image, video, audio, document)') ], responses={200: MediaAssetSerializer(many=True)}, tags=["Content Management System (CMS)"], ) def get(self, request, *args, **kwargs): qs = MediaAsset.objects.all().order_by('-created_at') m_type = request.query_params.get('media_type') if m_type: qs = qs.filter(media_type=m_type) serializer = MediaAssetSerializer(qs, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @extend_schema( summary="Upload media asset", description="Uploads a new photo, document, or audio file to the media library.", request=MediaAssetSerializer, responses={201: MediaAssetSerializer}, tags=["Content Management System (CMS)"], ) def post(self, request, *args, **kwargs): serializer = MediaAssetSerializer(data=request.data, context={'request': request}) serializer.is_valid(raise_exception=True) asset = serializer.save() return Response(MediaAssetSerializer(asset).data, status=status.HTTP_201_CREATED)