Files
guilan-ace-backend/apps/blog/api/views.py
Amirhossein Khalili 88b793ed9f
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled
initial commit
2026-05-19 20:53:08 +03:30

305 lines
11 KiB
Python

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."}