from django.shortcuts import get_object_or_404 from django.db.models import Q, Prefetch from ninja import Router, Query from typing import List, Optional from apps.users.models import User from apps.blog.api.schemas import ( CategorySchema, CommentCreateSchema, CommentSchema, PostCreateSchema, PostDetailSchema, PostListSchema, TagSchema, ) from apps.blog.models import Post, Category, Tag, Comment, Like from core.api.schemas import ErrorSchema, MessageSchema from core.authentication import jwt_auth blog_router = Router() # Post endpoints @blog_router.get("/posts", response=List[PostListSchema]) def list_posts( request, page: int = Query(1, ge=1), limit: int = Query(10, ge=1, le=50), category: Optional[str] = None, tag: Optional[str] = None, search: Optional[str] = None, featured: Optional[bool] = None, author: Optional[str] = None ): """List published posts with filtering and pagination""" queryset = Post.objects.filter(status=Post.StatusChoices.PUBLISHED).select_related( 'author', 'category' ).prefetch_related('tags') # Apply filters if category: queryset = queryset.filter(category__slug=category) if tag: queryset = queryset.filter(tags__slug=tag) if search: queryset = queryset.filter( Q(title__icontains=search) | Q(content__icontains=search) | Q(excerpt__icontains=search) ) if featured is not None: queryset = queryset.filter(is_featured=featured) if author: queryset = queryset.filter(author__username=author) # Pagination offset = (page - 1) * limit posts = queryset[offset:offset + limit] return posts @blog_router.get("/posts/{slug}", response=PostDetailSchema) def get_post(request, slug: str): """Get single post by slug""" post = get_object_or_404( Post.objects.select_related('author', 'category').prefetch_related('tags'), slug=slug, status=Post.StatusChoices.PUBLISHED ) return post @blog_router.post("/posts", response={201: PostDetailSchema, 400: ErrorSchema}, auth=jwt_auth) def create_post(request, data: PostCreateSchema): """Create a new post (committee members only)""" user = request.auth if not (user.is_superuser or user.is_staff): return 400, {"error": "Only committee members can create posts"} try: post = Post.objects.create( title=data.title, content=data.content, excerpt=data.excerpt, author=user, category_id=data.category_id, status=data.status, is_featured=data.is_featured ) if data.tag_ids: post.tags.set(data.tag_ids) return 201, post except Exception as e: return 400, {"error": "Failed to create post", "details": str(e)} @blog_router.put("/posts/{slug}", response={200: PostDetailSchema, 400: ErrorSchema}, auth=jwt_auth) def update_post(request, slug: str, data: PostCreateSchema): """Update a post (author or committee only)""" user = request.auth post = get_object_or_404(Post, slug=slug) if not (post.author == user or user.is_superuser or user.is_staff): return 400, {"error": "You can only edit your own posts"} try: for field, value in data.dict(exclude_unset=True).items(): if field == 'tag_ids': if value: post.tags.set(value) elif field == 'category_id': post.category_id = value else: setattr(post, field, value) post.save() return 200, post except Exception as e: return 400, {"error": "Failed to update post", "details": str(e)} @blog_router.delete("/posts/{slug}", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth) def delete_post(request, slug: str): """Soft delete a post owned by the requester or committee.""" user = request.auth post = get_object_or_404(Post, slug=slug) if not (post.author == user or user.is_superuser or user.is_staff): return 400, {"error": "You can only delete your own posts"} post.delete() return 200, {"message": "Post deleted successfully"} @blog_router.get("/deleted/posts", response=List[PostListSchema], auth=jwt_auth) def list_deleted_posts(request): """List all soft-deleted posts (Admin/Committee only)""" if not (request.auth.is_staff or request.auth.is_superuser): return 403, {"error": "Permission denied"} return Post.deleted_objects.all().select_related('author', 'category').prefetch_related('tags') @blog_router.post("deleted/posts/{post_id}/restore", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth) def restore_post(request, post_id: int): """Restore a soft-deleted post (Admin/Committee only)""" if not (request.auth.is_staff or request.auth.is_superuser): return 403, {"error": "Permission denied"} try: post = Post.deleted_objects.get(id=post_id) post.restore() return 200, {"message": f"Post '{post.title}' restored successfully."} except Post.DoesNotExist: return 400, {"error": "Post not found or not soft-deleted."} # Comment endpoints @blog_router.get("/posts/{slug}/comments", response=List[CommentSchema]) def list_comments(request, slug: str): """List approved comments for a post""" post = get_object_or_404(Post, slug=slug, status=Post.StatusChoices.PUBLISHED) comments = Comment.objects.filter( post=post, is_approved=True, parent=None ).select_related('author').prefetch_related( Prefetch( 'replies', queryset=Comment.objects.filter(is_approved=True).select_related('author') ) ) return comments @blog_router.post("/posts/{slug}/comments", response={201: CommentSchema, 400: ErrorSchema}, auth=jwt_auth) def create_comment(request, slug: str, data: CommentCreateSchema): """Create a comment on a post""" post = get_object_or_404(Post, slug=slug, status=Post.StatusChoices.PUBLISHED) user = request.auth try: comment = Comment.objects.create( post=post, author=user, content=data.content, parent_id=data.parent_id ) return 201, comment except Exception as e: return 400, {"error": "Failed to create comment", "details": str(e)} @blog_router.get("/deleted/comments", response=List[CommentSchema], auth=jwt_auth) def list_deleted_comments(request): """List all soft-deleted comments (Admin/Committee only)""" if not (request.auth.is_staff or request.auth.is_superuser): return 403, {"error": "Permission denied"} return Comment.deleted_objects.all().select_related('author', 'post') @blog_router.post("/deleted/comments/{comment_id}/restore", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth) def restore_comment(request, comment_id: int): """Restore a soft-deleted comment (Admin/Committee only)""" if not (request.auth.is_staff or request.auth.is_superuser): return 403, {"error": "Permission denied"} try: comment = Comment.deleted_objects.get(id=comment_id) comment.restore() return 200, {"message": f"Comment by {comment.author.username} restored successfully."} except Comment.DoesNotExist: return 400, {"error": "Comment not found or not soft-deleted."} # Like endpoints @blog_router.post("/posts/{slug}/like", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth) def toggle_like(request, slug: str): """Toggle like on a post""" post = get_object_or_404(Post, slug=slug, status=Post.StatusChoices.PUBLISHED) user = request.auth like, created = Like.objects.get_or_create(post=post, user=user) if not created: like.delete() return 200, {"message": "Post unliked"} return 200, {"message": "Post liked"} @blog_router.get("/posts/{slug}/likes", response={200: MessageSchema}) def get_likes_count(request, slug: str): """Get likes count for a post""" post = get_object_or_404(Post, slug=slug, status=Post.StatusChoices.PUBLISHED) count = post.likes.count() return {"message": f"{count}"} # Category endpoints @blog_router.get("/categories", response=List[CategorySchema]) def list_categories(request): """List all categories""" return Category.objects.all() @blog_router.get("/categories/{slug}", response=CategorySchema) def get_category(request, slug: str): """Get single category by slug""" return get_object_or_404(Category, slug=slug) @blog_router.get("/deleted/categories", response=List[CategorySchema], auth=jwt_auth) def list_deleted_categories(request): """List all soft-deleted categories (Admin/Committee only)""" if not (request.auth.is_staff or request.auth.is_superuser): return 403, {"error": "Permission denied"} return Category.deleted_objects.all() @blog_router.post("/deleted/categories/{category_id}/restore", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth) def restore_category(request, category_id: int): """Restore a soft-deleted category (Admin/Committee only)""" if not (request.auth.is_staff or request.auth.is_superuser): return 403, {"error": "Permission denied"} try: category = Category.deleted_objects.get(id=category_id) category.restore() return 200, {"message": f"Category '{category.name}' restored successfully."} except Category.DoesNotExist: return 400, {"error": "Category not found or not soft-deleted."} # Tag endpoints @blog_router.get("/tags", response=List[TagSchema]) def list_tags(request): """List all tags""" return Tag.objects.all() @blog_router.get("/tags/{slug}", response=TagSchema) def get_tag(request, slug: str): """Get single tag by slug""" return get_object_or_404(Tag, slug=slug) @blog_router.get("/deleted/tags", response=List[TagSchema], auth=jwt_auth) def list_deleted_tags(request): """List all soft-deleted tags (Admin/Committee only)""" if not (request.auth.is_staff or request.auth.is_superuser): return 403, {"error": "Permission denied"} return Tag.all_objects.all() @blog_router.post("/deleted/tags/{tag_id}/restore", response={200: MessageSchema, 400: ErrorSchema}, auth=jwt_auth) def restore_tag(request, tag_id: int): """Restore a soft-deleted tag (Admin/Committee only)""" if not (request.auth.is_staff or request.auth.is_superuser): return 403, {"error": "Permission denied"} try: tag = Tag.deleted_objects.get(id=tag_id) tag.restore() return 200, {"message": f"Tag '{tag.name}' restored successfully."} except Tag.DoesNotExist: return 400, {"error": "Tag not found or not soft-deleted."}