feat(blog): expand publishing and moderation APIs

This commit is contained in:
2026-06-11 21:20:44 +03:30
parent 4039be0187
commit 5045f8da47
7 changed files with 600 additions and 42 deletions

View File

@@ -5,17 +5,37 @@ from typing import List, Optional
from ninja import ModelSchema, Schema
from apps.blog.models import Category, Comment, PostAsset, Tag
from apps.blog.models import BlogBanner, Category, Comment, PostAsset, Tag
from core.media import PREVIEW_VARIANT, THUMBNAIL_VARIANT, derivative_url
class CategorySchema(ModelSchema):
created_at: Optional[datetime] = None
parent_id: Optional[int] = None
class Config:
model = Category
model_fields = ["id", "name", "slug", "description", "created_at"]
@staticmethod
def resolve_parent_id(obj):
return obj.parent_id
class CategoryPathSchema(Schema):
id: int
name: str
slug: str
class CategoryFilterSchema(Schema):
id: int
name: str
slug: str
parent_id: Optional[int] = None
post_count: int = 0
children: List["CategoryFilterSchema"] = []
class TagSchema(ModelSchema):
created_at: Optional[datetime] = None
@@ -25,11 +45,19 @@ class TagSchema(ModelSchema):
model_fields = ["id", "name", "slug", "created_at"]
class TagFilterSchema(Schema):
id: int
name: str
slug: str
post_count: int = 0
class AuthorSchema(Schema):
id: int
username: str
first_name: str
last_name: str
bio: Optional[str] = None
profile_picture: Optional[str] = None
profile_picture_thumbnail_url: Optional[str] = None
profile_picture_preview_url: Optional[str] = None
@@ -124,6 +152,8 @@ class PostListSchema(Schema):
status: str
published_at: Optional[datetime] = None
category: Optional[CategorySchema] = None
category_path: List[CategoryPathSchema] = []
writers: List[AuthorSchema] = []
tags: List[TagSchema]
is_featured: bool
created_at: datetime
@@ -159,6 +189,17 @@ class PostListSchema(Schema):
url = derivative_url(obj.featured_image, PREVIEW_VARIANT)
return request.build_absolute_uri(url) if url else None
@staticmethod
def resolve_category_path(obj):
if not obj.category_id:
return []
return obj.category.path
@staticmethod
def resolve_writers(obj):
writers = list(obj.writers.all())
return writers or [obj.author]
@staticmethod
def resolve_likes_count(obj):
return getattr(obj, "likes_count", None) or obj.likes.count()
@@ -169,7 +210,11 @@ class PostListSchema(Schema):
@staticmethod
def resolve_comments_count(obj):
return getattr(obj, "comments_count", None) or obj.comments.filter(is_approved=True).count()
return getattr(obj, "comments_count", None) or obj.comments.filter(
is_approved=True,
is_hidden=False,
is_deleted=False,
).count()
class PostDetailSchema(PostListSchema):
@@ -192,6 +237,7 @@ class PostCreateSchema(Schema):
excerpt: Optional[str] = None
category_id: Optional[int] = None
tag_ids: Optional[List[int]] = []
writer_ids: Optional[List[int]] = None
status: str = "draft"
is_featured: bool = False
seo_title: Optional[str] = ""
@@ -221,10 +267,14 @@ class CommentSchema(ModelSchema):
post_title: str
post_slug: str
parent_id: Optional[int] = None
is_hidden: bool = False
is_deleted: bool = False
deleted_at: Optional[datetime] = None
hidden_replies_count: int = 0
class Config:
model = Comment
model_fields = ["id", "content", "created_at", "is_approved", "hidden_at"]
model_fields = ["id", "content", "created_at", "updated_at", "is_approved", "hidden_at"]
@staticmethod
def resolve_post_id(obj):
@@ -242,12 +292,22 @@ class CommentSchema(ModelSchema):
def resolve_parent_id(obj):
return obj.parent_id
@staticmethod
def resolve_hidden_replies_count(obj):
if not getattr(obj, "replies", None):
return 0
return sum(len(reply.replies.all()) for reply in obj.replies.all())
class CommentCreateSchema(Schema):
content: str
parent_id: Optional[int] = None
class CommentUpdateSchema(Schema):
content: str
class CommentHideSchema(Schema):
note: Optional[str] = ""
@@ -265,3 +325,30 @@ class BlogProfileActivitySchema(Schema):
saved_posts: List[PostListSchema]
comments: List[CommentSchema]
replies: List[CommentSchema]
class BlogBannerSchema(ModelSchema):
image_url: str
class Config:
model = BlogBanner
model_fields = ["id", "title", "alt_text", "url", "sort_order"]
@staticmethod
def resolve_image_url(obj, context):
request = context["request"]
return request.build_absolute_uri(obj.image.url) if obj.image else ""
class BlogFilterAuthorSchema(Schema):
id: int
username: str
first_name: str
last_name: str
post_count: int = 0
class BlogFiltersSchema(Schema):
categories: List[CategoryFilterSchema]
tags: List[TagFilterSchema]
authors: List[BlogFilterAuthorSchema]