"""Blog API schemas.""" from datetime import datetime from typing import List, Optional from ninja import ModelSchema, Schema 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 AdminCategorySchema(CategorySchema): post_count: int = 0 @staticmethod def resolve_post_count(obj): return getattr(obj, "post_count", None) or obj.posts.filter(is_deleted=False).count() class CategoryWriteSchema(Schema): name: str slug: Optional[str] = None description: Optional[str] = "" parent_id: Optional[int] = None 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 class Config: model = Tag model_fields = ["id", "name", "slug", "created_at"] class AdminTagSchema(TagSchema): post_count: int = 0 @staticmethod def resolve_post_count(obj): return getattr(obj, "post_count", None) or obj.posts.filter(is_deleted=False).count() class TagWriteSchema(Schema): name: str slug: Optional[str] = None 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 @staticmethod def resolve_profile_picture(obj, context): request = context["request"] if obj.profile_picture and hasattr(obj.profile_picture, "url"): return request.build_absolute_uri(obj.profile_picture.url) return None @staticmethod def resolve_profile_picture_thumbnail_url(obj, context): request = context["request"] url = derivative_url(obj.profile_picture, THUMBNAIL_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_profile_picture_preview_url(obj, context): request = context["request"] 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 slug: str excerpt: str author: AuthorSchema featured_image: Optional[str] = None absolute_featured_image_url: Optional[str] = None absolute_featured_image_thumbnail_url: Optional[str] = None absolute_featured_image_preview_url: Optional[str] = None 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 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): request = context["request"] if obj.featured_image and hasattr(obj.featured_image, "url"): return request.build_absolute_uri(obj.featured_image.url) return None @staticmethod def resolve_absolute_featured_image_thumbnail_url(obj, context): request = context["request"] url = derivative_url(obj.featured_image, THUMBNAIL_VARIANT) return request.build_absolute_uri(url) if url else None @staticmethod def resolve_absolute_featured_image_preview_url(obj, context): request = context["request"] 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() @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, is_hidden=False, is_deleted=False, ).count() class PostDetailSchema(PostListSchema): content: str content_html: str review_note: Optional[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 content: str 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] = "" 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"] = [] post_id: int 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", "updated_at", "is_approved", "hidden_at"] @staticmethod def resolve_post_id(obj): return obj.post_id @staticmethod def resolve_post_title(obj): return obj.post.title @staticmethod def resolve_post_slug(obj): return obj.post.slug @staticmethod 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] = "" 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] 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]