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.
 
 

758 lines
32 KiB

import logging
from django.db.models import Q, F
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework import status, serializers
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, inline_serializer
from apps.cms.models.post import (
PostCategory,
PostTag,
Post,
PostRevision,
PostComment,
PostLike,
MediaAsset,
)
from apps.cms.permissions import (
IsAuthorOrEditorOrReadOnly,
CanPublishPost,
can_publish_or_moderate_post,
)
from apps.cms.serializers import (
PostCategorySerializer,
PostTagSerializer,
PostRevisionSerializer,
PostRevisionCreateSerializer,
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().select_related('author', 'category', 'institution')
if not user or not user.is_authenticated:
qs = qs.filter(status=Post.Status.PUBLISHED)
elif not (user.is_superuser or getattr(user, 'is_super_admin', False)):
accessible_inst_ids = list(user.institution_memberships.values_list('institution_id', flat=True))
region_ids = list(user.managed_regions.values_list('id', flat=True)) if hasattr(user, 'managed_regions') else []
qs = qs.filter(
Q(status=Post.Status.PUBLISHED) |
Q(author=user) |
Q(institution_id__in=accessible_inst_ids) |
Q(institution__region_id__in=region_ids)
).distinct()
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')
status_param = self.request.query_params.get('status')
my_posts = self.request.query_params.get('my_posts')
is_super = user.is_authenticated and (user.is_superuser or getattr(user, 'is_super_admin', False))
if is_super:
if status_param:
qs = qs.filter(status=status_param)
elif user.is_authenticated and my_posts:
accessible_inst_ids = list(user.institution_memberships.filter(
role__in=['admin', 'editor']
).values_list('institution_id', flat=True))
qs = qs.filter(
Q(author=user) |
Q(author_type=Post.AuthorType.INSTITUTION, institution_id__in=accessible_inst_ids)
).distinct()
if status_param:
qs = qs.filter(status=status_param)
elif user.is_authenticated and user.has_perm_flag('can_publish_posts_directly') and status_param:
accessible_inst_ids = list(user.institution_memberships.values_list('institution_id', flat=True))
region_ids = list(user.managed_regions.values_list('id', flat=True)) if hasattr(user, 'managed_regions') else []
qs = qs.filter(status=status_param).filter(
Q(author=user) |
Q(institution_id__in=accessible_inst_ids) |
Q(institution__region_id__in=region_ids)
).distinct()
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)
author_type = self.request.query_params.get('author_type')
if author_type:
qs = qs.filter(author_type=author_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('author_type', str, description='Filter by author type (user, institution)'),
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. If post is published and editor rights are lacking, submits a zero-downtime staged revision.",
request=PostCreateUpdateSerializer,
responses={
200: PostDetailSerializer,
202: OpenApiResponse(description="Revision staged for editorial review"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Content Management System (CMS)"],
)
def patch(self, request, pk_or_slug, *args, **kwargs):
post_obj = self.get_object()
user = request.user
has_mod_cap = can_publish_or_moderate_post(user, post_obj.institution)
target_status = request.data.get('status')
is_unpublishing_to_draft = (target_status == Post.Status.DRAFT)
# Option C: If post is PUBLISHED and editor capability is lacking,
# create or update a staged PostRevision (Zero-Downtime, live post remains online).
# EXCEPT when the author or owner is deliberately unpublishing the post back to DRAFT!
if post_obj.status == Post.Status.PUBLISHED and not has_mod_cap and not is_unpublishing_to_draft:
if not user.has_perm_flag('can_edit_own_posts'):
return Response(
{"detail": _("You do not have permission to edit posts.")},
status=status.HTTP_403_FORBIDDEN
)
rev_serializer = PostRevisionCreateSerializer(data=request.data, partial=True)
rev_serializer.is_valid(raise_exception=True)
validated_data = rev_serializer.validated_data
revision = post_obj.revisions.filter(status=PostRevision.Status.PENDING).first()
if revision:
for field, val in validated_data.items():
setattr(revision, field, val)
revision.updated_at = timezone.now()
revision.save()
else:
revision = PostRevision.objects.create(
post=post_obj,
author=user,
title=validated_data.get('title', post_obj.title),
excerpt=validated_data.get('excerpt', post_obj.excerpt or ''),
content=validated_data.get('content', post_obj.content),
tags=validated_data.get('tags', post_obj.tags),
featured_image=validated_data.get('featured_image', post_obj.featured_image),
category=validated_data.get('category', post_obj.category),
status=PostRevision.Status.PENDING,
)
return Response({
"detail": _("Post is currently published. Your changes have been submitted as a pending revision for review without taking down the live article."),
"has_pending_revision": True,
"revision": PostRevisionSerializer(revision, context={'request': request}).data,
"live_post": PostDetailSerializer(post_obj, context={'request': request}).data,
}, status=status.HTTP_202_ACCEPTED)
serializer = PostCreateUpdateSerializer(
post_obj,
data=request.data,
partial=True,
context={'request': request}
)
serializer.is_valid(raise_exception=True)
serializer.save()
# If post was successfully unpublished to DRAFT, clear any open pending revision
if post_obj.status == Post.Status.DRAFT:
post_obj.revisions.filter(status=PostRevision.Status.PENDING).delete()
response_serializer = PostDetailSerializer(post_obj, context={'request': request})
return Response(response_serializer.data, status=status.HTTP_200_OK)
def put(self, request, pk_or_slug, *args, **kwargs):
return self.patch(request, pk_or_slug, *args, **kwargs)
@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)
class PostApproveView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = PostDetailSerializer
queryset = Post.objects.all()
@extend_schema(
summary="Approve and publish post",
description="Approves a pending post and immediately publishes it. Requires direct publishing capability for the associated institution.",
responses={
200: PostDetailSerializer,
400: OpenApiResponse(description="Post cannot be approved"),
403: OpenApiResponse(description="Permission denied"),
404: OpenApiResponse(description="Post not found"),
},
tags=["Content Management System (CMS)"],
)
def post(self, request, pk_or_slug, *args, **kwargs):
if str(pk_or_slug).isdigit():
post_obj = get_object_or_404(Post, id=int(pk_or_slug))
else:
post_obj = get_object_or_404(Post, slug=pk_or_slug)
if not can_publish_or_moderate_post(request.user, post_obj.institution):
return Response(
{"detail": _("You do not have permission to publish or approve posts for this institution.")},
status=status.HTTP_403_FORBIDDEN
)
post_obj.status = Post.Status.PUBLISHED
post_obj.reviewed_by = request.user
post_obj.reviewed_at = timezone.now()
post_obj.rejection_reason = ''
post_obj.save(update_fields=['status', 'reviewed_by', 'reviewed_at', 'rejection_reason', 'updated_at'])
serializer = PostDetailSerializer(post_obj, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
class PostRejectView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = PostDetailSerializer
queryset = Post.objects.all()
@extend_schema(
summary="Reject post with reason",
description="Rejects a pending post. Requires direct publishing capability for the associated institution and a mandatory explanation reason.",
request=inline_serializer(
name='PostRejectRequest',
fields={'reason': serializers.CharField(required=True, help_text="Explanation for rejection")}
),
responses={
200: PostDetailSerializer,
400: OpenApiResponse(description="Rejection reason is missing"),
403: OpenApiResponse(description="Permission denied"),
404: OpenApiResponse(description="Post not found"),
},
tags=["Content Management System (CMS)"],
)
def post(self, request, pk_or_slug, *args, **kwargs):
if str(pk_or_slug).isdigit():
post_obj = get_object_or_404(Post, id=int(pk_or_slug))
else:
post_obj = get_object_or_404(Post, slug=pk_or_slug)
if not can_publish_or_moderate_post(request.user, post_obj.institution):
return Response(
{"detail": _("You do not have permission to moderate posts for this institution.")},
status=status.HTTP_403_FORBIDDEN
)
reason = request.data.get('reason', '').strip()
if not reason:
return Response(
{"reason": [_("A reason is required when rejecting a post.")]},
status=status.HTTP_400_BAD_REQUEST
)
post_obj.status = Post.Status.REJECTED
post_obj.rejection_reason = reason
post_obj.reviewed_by = request.user
post_obj.reviewed_at = timezone.now()
post_obj.save(update_fields=['status', 'rejection_reason', 'reviewed_by', 'reviewed_at', 'updated_at'])
serializer = PostDetailSerializer(post_obj, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
class PostRevisionListView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = PostRevisionSerializer
queryset = PostRevision.objects.all()
@extend_schema(
summary="List revisions for a post",
description="Lists all historical and pending revisions for this post. Permitted for the author or institution editors.",
responses={
200: PostRevisionSerializer(many=True),
403: OpenApiResponse(description="Permission denied"),
404: OpenApiResponse(description="Post not found"),
},
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)
is_author = (post_obj.author_id == request.user.id)
is_editor = can_publish_or_moderate_post(request.user, post_obj.institution)
if not (is_author or is_editor or request.user.is_superuser):
return Response(
{"detail": _("You do not have permission to view revisions for this post.")},
status=status.HTTP_403_FORBIDDEN
)
revisions = post_obj.revisions.all().select_related('author', 'reviewed_by', 'category').order_by('-created_at')
serializer = PostRevisionSerializer(revisions, many=True, context={'request': request})
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
summary="Discard pending revision",
description="Allows the post author to withdraw or delete their pending revision before editorial review.",
responses={
204: OpenApiResponse(description="Pending revision discarded"),
400: OpenApiResponse(description="No pending revision to discard"),
403: OpenApiResponse(description="Permission denied"),
},
tags=["Content Management System (CMS)"],
)
def delete(self, request, pk_or_slug, *args, **kwargs):
post_obj = get_post_by_id_or_slug(pk_or_slug, request.user)
is_author = (post_obj.author_id == request.user.id)
is_editor = can_publish_or_moderate_post(request.user, post_obj.institution)
if not (is_author or is_editor or request.user.is_superuser):
return Response(
{"detail": _("You do not have permission to discard revisions for this post.")},
status=status.HTTP_403_FORBIDDEN
)
pending = post_obj.revisions.filter(status=PostRevision.Status.PENDING).first()
if not pending:
return Response(
{"detail": _("No pending revision found to discard.")},
status=status.HTTP_400_BAD_REQUEST
)
pending.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class PostRevisionApproveView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = PostDetailSerializer
queryset = PostRevision.objects.all()
@extend_schema(
summary="Approve and merge staged post revision",
description="Approves a pending post revision and merges it live into the published post with zero downtime. Requires direct publishing capability for the institution.",
responses={
200: inline_serializer(
name='PostRevisionApproveResponse',
fields={
'detail': serializers.CharField(),
'post': PostDetailSerializer(),
'revision': PostRevisionSerializer(),
}
),
400: OpenApiResponse(description="No pending revision found"),
403: OpenApiResponse(description="Permission denied"),
404: OpenApiResponse(description="Post not found"),
},
tags=["Content Management System (CMS)"],
)
def post(self, request, pk_or_slug, *args, **kwargs):
if str(pk_or_slug).isdigit():
post_obj = get_object_or_404(Post, id=int(pk_or_slug))
else:
post_obj = get_object_or_404(Post, slug=pk_or_slug)
if not can_publish_or_moderate_post(request.user, post_obj.institution):
return Response(
{"detail": _("You do not have permission to approve revisions for this institution.")},
status=status.HTTP_403_FORBIDDEN
)
revision = post_obj.revisions.filter(status=PostRevision.Status.PENDING).first()
if not revision:
return Response(
{"detail": _("No pending revision found for this post.")},
status=status.HTTP_400_BAD_REQUEST
)
revision.apply_to_post(editor=request.user)
post_obj.refresh_from_db()
return Response({
"detail": _("Revision approved and merged successfully. Live post updated with zero downtime."),
"post": PostDetailSerializer(post_obj, context={'request': request}).data,
"revision": PostRevisionSerializer(revision, context={'request': request}).data,
}, status=status.HTTP_200_OK)
class PostRevisionRejectView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = PostRevisionSerializer
queryset = PostRevision.objects.all()
@extend_schema(
summary="Reject staged post revision with explanation",
description="Rejects a pending post revision without affecting the live published article. Requires direct publishing capability and an explanation reason.",
request=inline_serializer(
name='PostRevisionRejectRequest',
fields={'reason': serializers.CharField(required=True, help_text="Explanation for rejection")}
),
responses={
200: inline_serializer(
name='PostRevisionRejectResponse',
fields={
'detail': serializers.CharField(),
'post': PostDetailSerializer(),
'revision': PostRevisionSerializer(),
}
),
400: OpenApiResponse(description="Rejection reason missing or no pending revision"),
403: OpenApiResponse(description="Permission denied"),
404: OpenApiResponse(description="Post not found"),
},
tags=["Content Management System (CMS)"],
)
def post(self, request, pk_or_slug, *args, **kwargs):
if str(pk_or_slug).isdigit():
post_obj = get_object_or_404(Post, id=int(pk_or_slug))
else:
post_obj = get_object_or_404(Post, slug=pk_or_slug)
if not can_publish_or_moderate_post(request.user, post_obj.institution):
return Response(
{"detail": _("You do not have permission to moderate revisions for this institution.")},
status=status.HTTP_403_FORBIDDEN
)
revision = post_obj.revisions.filter(status=PostRevision.Status.PENDING).first()
if not revision:
return Response(
{"detail": _("No pending revision found for this post.")},
status=status.HTTP_400_BAD_REQUEST
)
reason = request.data.get('reason', '').strip()
if not reason:
return Response(
{"reason": [_("A reason is required when rejecting a revision.")]},
status=status.HTTP_400_BAD_REQUEST
)
revision.status = PostRevision.Status.REJECTED
revision.rejection_reason = reason
revision.reviewed_by = request.user
revision.reviewed_at = timezone.now()
revision.save(update_fields=['status', 'rejection_reason', 'reviewed_by', 'reviewed_at', 'updated_at'])
return Response({
"detail": _("Revision rejected. Live post remains published and unchanged."),
"post": PostDetailSerializer(post_obj, context={'request': request}).data,
"revision": PostRevisionSerializer(revision, context={'request': request}).data,
}, status=status.HTTP_200_OK)