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)