feat(backend): add blog publishing platform
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-06-08 21:31:06 +03:30
parent b7b21a6cc6
commit 954e78d0cb
14 changed files with 1334 additions and 278 deletions

View File

@@ -1,22 +1,29 @@
"""Blog API schemas."""
from ninja import Schema, ModelSchema
from typing import Optional, List
from datetime import datetime
from typing import List, Optional
from apps.blog.models import Category, Tag, Comment
from ninja import ModelSchema, Schema
from apps.blog.models import Category, Comment, PostAsset, Tag
from core.media import PREVIEW_VARIANT, THUMBNAIL_VARIANT, derivative_url
class CategorySchema(ModelSchema):
created_at: Optional[datetime] = None
class Config:
model = Category
model_fields = ['id', 'name', 'slug', 'description']
model_fields = ["id", "name", "slug", "description", "created_at"]
class TagSchema(ModelSchema):
created_at: Optional[datetime] = None
class Config:
model = Tag
model_fields = ['id', 'name', 'slug']
model_fields = ["id", "name", "slug", "created_at"]
class AuthorSchema(Schema):
id: int
@@ -29,8 +36,8 @@ class AuthorSchema(Schema):
@staticmethod
def resolve_profile_picture(obj, context):
request = context['request']
if obj.profile_picture and hasattr(obj.profile_picture, 'url'):
request = context["request"]
if obj.profile_picture and hasattr(obj.profile_picture, "url"):
return request.build_absolute_uri(obj.profile_picture.url)
return None
@@ -46,6 +53,64 @@ class AuthorSchema(Schema):
url = derivative_url(obj.profile_picture, PREVIEW_VARIANT)
return request.build_absolute_uri(url) if url else None
class PostAssetSchema(ModelSchema):
absolute_file_url: Optional[str] = None
absolute_thumbnail_url: Optional[str] = None
absolute_preview_url: Optional[str] = None
absolute_blur_url: Optional[str] = None
markdown_image: Optional[str] = None
markdown_link: Optional[str] = None
uploaded_by: AuthorSchema
class Config:
model = PostAsset
model_fields = [
"id",
"file_type",
"title",
"alt_text",
"caption",
"size",
"mime_type",
"created_at",
]
@staticmethod
def resolve_absolute_file_url(obj, context):
request = context["request"]
return request.build_absolute_uri(obj.file.url) if obj.file else None
@staticmethod
def resolve_absolute_thumbnail_url(obj, context):
request = context["request"]
return request.build_absolute_uri(obj.thumbnail_url) if obj.thumbnail_url else None
@staticmethod
def resolve_absolute_preview_url(obj, context):
request = context["request"]
return request.build_absolute_uri(obj.preview_url) if obj.preview_url else None
@staticmethod
def resolve_absolute_blur_url(obj, context):
request = context["request"]
return request.build_absolute_uri(obj.blur_url) if obj.blur_url else None
@staticmethod
def resolve_markdown_image(obj, context):
request = context["request"]
if obj.file_type != PostAsset.FileType.IMAGE or not obj.file:
return None
return f"![{obj.alt_text or obj.title}]({request.build_absolute_uri(obj.file.url)})"
@staticmethod
def resolve_markdown_link(obj, context):
request = context["request"]
if not obj.file:
return None
return f"[{obj.title}]({request.build_absolute_uri(obj.file.url)})"
class PostListSchema(Schema):
id: int
title: str
@@ -62,7 +127,18 @@ class PostListSchema(Schema):
tags: List[TagSchema]
is_featured: bool
created_at: datetime
updated_at: datetime
reading_time: int
seo_title: str
seo_description: str
canonical_url: str
og_title: str
og_description: str
noindex: bool
focus_keyword: str
likes_count: int
saves_count: int
comments_count: int
@staticmethod
def resolve_absolute_featured_image_url(obj, context):
@@ -83,9 +159,32 @@ class PostListSchema(Schema):
url = derivative_url(obj.featured_image, PREVIEW_VARIANT)
return request.build_absolute_uri(url) if url else None
@staticmethod
def resolve_likes_count(obj):
return getattr(obj, "likes_count", None) or obj.likes.count()
@staticmethod
def resolve_saves_count(obj):
return getattr(obj, "saves_count", None) or obj.saves.count()
@staticmethod
def resolve_comments_count(obj):
return getattr(obj, "comments_count", None) or obj.comments.filter(is_approved=True).count()
class PostDetailSchema(PostListSchema):
content: str
content_html: str
og_image_url: Optional[str] = None
assets: List[PostAssetSchema] = []
@staticmethod
def resolve_og_image_url(obj, context):
request = context["request"]
if obj.og_image and hasattr(obj.og_image, "url"):
return request.build_absolute_uri(obj.og_image.url)
return None
class PostCreateSchema(Schema):
title: str
@@ -95,17 +194,37 @@ class PostCreateSchema(Schema):
tag_ids: Optional[List[int]] = []
status: str = "draft"
is_featured: bool = False
seo_title: Optional[str] = ""
seo_description: Optional[str] = ""
canonical_url: Optional[str] = ""
og_title: Optional[str] = ""
og_description: Optional[str] = ""
noindex: Optional[bool] = False
focus_keyword: Optional[str] = ""
class PostReviewSchema(Schema):
action: str
note: Optional[str] = ""
class PostAssetCreateSchema(Schema):
title: Optional[str] = ""
alt_text: Optional[str] = ""
caption: Optional[str] = ""
class CommentSchema(ModelSchema):
author: AuthorSchema
replies: List['CommentSchema'] = []
replies: List["CommentSchema"] = []
post_id: int
post_title: str
post_slug: str
parent_id: Optional[int] = None
class Config:
model = Comment
model_fields = ['id', 'content', 'created_at', 'is_approved']
model_fields = ["id", "content", "created_at", "is_approved", "hidden_at"]
@staticmethod
def resolve_post_id(obj):
@@ -119,6 +238,30 @@ class CommentSchema(ModelSchema):
def resolve_post_slug(obj):
return obj.post.slug
@staticmethod
def resolve_parent_id(obj):
return obj.parent_id
class CommentCreateSchema(Schema):
content: str
parent_id: Optional[int] = None
class CommentHideSchema(Schema):
note: Optional[str] = ""
class BlogInteractionSchema(Schema):
liked: bool
saved: bool
likes_count: int
saves_count: int
comments_count: int
class BlogProfileActivitySchema(Schema):
liked_posts: List[PostListSchema]
saved_posts: List[PostListSchema]
comments: List[CommentSchema]
replies: List[CommentSchema]