You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

408 lines
16 KiB

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)