From 029f0c7b8d633b40c0afc63e6f1fe9d12de9ef35 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Fri, 12 Jun 2026 11:20:47 +0330 Subject: [PATCH] feat(blog): notify users about comment activity --- apps/blog/api/views.py | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/apps/blog/api/views.py b/apps/blog/api/views.py index 34a1e8a..f0c615c 100644 --- a/apps/blog/api/views.py +++ b/apps/blog/api/views.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import mimetypes from pathlib import Path from typing import List, Optional @@ -39,12 +40,14 @@ from apps.blog.permissions import ( can_review_blog_posts, can_write_blog_posts, ) +from apps.notifications.services import notify_user from core.api.schemas import ErrorSchema, MessageSchema from core.authentication import jwt_auth blog_router = Router() User = get_user_model() +logger = logging.getLogger(__name__) IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff", ".svg"} VIDEO_EXTENSIONS = {".mp4", ".webm", ".ogg", ".mov", ".mkv", ".avi"} @@ -240,6 +243,73 @@ def _interaction_payload(post: Post, user) -> BlogInteractionSchema: ) +def _frontend_blog_url(post: Post) -> str: + root = getattr(settings, "FRONTEND_ROOT", "/") or "/" + if not root.endswith("/"): + root = f"{root}/" + return f"{root}blog/{post.slug}" + + +def _blog_moderator_ids() -> set[int]: + return set( + User.objects.filter(is_active=True) + .filter( + Q(is_staff=True) + | Q(is_superuser=True) + | Q(groups__permissions__content_type__app_label="blog", groups__permissions__codename="moderate_blog_comment") + | Q(user_permissions__content_type__app_label="blog", user_permissions__codename="moderate_blog_comment") + ) + .distinct() + .values_list("id", flat=True) + ) + + +def _post_author_ids(post: Post) -> set[int]: + author_ids = set(post.writers.values_list("id", flat=True)) + if post.author_id: + author_ids.add(post.author_id) + return author_ids + + +def _notify_blog_comment(comment: Comment) -> None: + post = comment.post + actor_id = comment.author_id + action_url = _frontend_blog_url(post) + excluded_ids = {actor_id} + + if comment.parent_id and comment.parent.author_id != actor_id: + notify_user( + comment.parent.author_id, + { + "type": "blog_reply", + "title": "پاسخ جدید", + "message": f"به کامنت شما در «{post.title}» پاسخ داده شد.", + "level": "info", + "action_url": action_url, + "entity_type": "blog_comment", + "entity_id": comment.id, + "meta": {"post_id": post.id, "post_slug": post.slug, "parent_id": comment.parent_id}, + }, + ) + excluded_ids.add(comment.parent.author_id) + + recipient_ids = (_blog_moderator_ids() | _post_author_ids(post)) - excluded_ids + for user_id in recipient_ids: + notify_user( + user_id, + { + "type": "blog_comment", + "title": "کامنت جدید", + "message": f"برای «{post.title}» کامنت جدید ثبت شد.", + "level": "info", + "action_url": action_url, + "entity_type": "blog_post", + "entity_id": post.id, + "meta": {"post_id": post.id, "post_slug": post.slug, "comment_id": comment.id}, + }, + ) + + @blog_router.get("/admin/writers", response={200: List[AuthorSchema], 403: ErrorSchema}, auth=jwt_auth) def list_blog_writers(request): if not (request.auth.is_superuser or request.auth.is_staff or can_review_blog_posts(request.auth)): @@ -619,6 +689,10 @@ def create_comment(request, slug: str, data: CommentCreateSchema): if data.parent_id: parent = get_object_or_404(Comment, id=data.parent_id, post=post, is_approved=True, is_hidden=False, is_deleted=False) comment = Comment.objects.create(post=post, author=request.auth, content=data.content, parent=parent) + try: + _notify_blog_comment(comment) + except Exception: + logger.exception("Failed to send blog comment notifications for comment=%s", comment.id) return 201, comment