Files
CE-Association-Website/backend/api/views/blog.py
Amirhossein Khalili 7a8ddeabed
Some checks failed
CI/CD / Backend & Frontend Checks (push) Has been cancelled
CI/CD / Deploy to Production (push) Has been cancelled
init
2026-05-18 11:34:07 +03:30

300 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 users.models import User
from blog.models import Post, Category, Tag, Comment, Like
from api.authentication import jwt_auth
from api.schemas import (
PostListSchema, PostDetailSchema, PostCreateSchema,
CategorySchema, TagSchema, CommentSchema, CommentCreateSchema,
MessageSchema, ErrorSchema
)
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."}