refactor(feed): реструктуризация notifications в модульную архитектуру

Проблема:
- Монолитный notification_signals.py (~200 строк) смешивал разные concerns
- Сложно поддерживать и понимать логику уведомлений
- Отсутствовала интеграция с API для уведомлений о лайках

Решение:
Разделен на модульную структуру feed/notifications/:
- config.py - NotificationVerbs, MessageTemplates, ActionURLs, truncate_text
- handlers.py - notify_new_post(), notify_comment(), notify_post_reaction(), get_post_recipients()
- signals.py - Django signal receivers (@receiver)
- __init__.py - публичный API модуля

Изменения:
- Создан: feed/notifications/ модуль с 4 файлами
- Переименован: notification_signals.py → notification_signals.py.old
- Изменен: apps.py - обновлен импорт на feed.notifications
- Изменен: api/v1/feed/views.py - добавлен вызов notify_post_reaction() в метод like()

Преимущества:
- ✅ Единообразие с requests_app и procurement
- ✅ Четкое разделение ответственности (SRP)
- ✅ Легче тестировать отдельные компоненты
- ✅ Публичный API через __init__.py
- ✅ Уведомления о лайках теперь работают
- ✅ Централизованные шаблоны сообщений

Верификация:
- python manage.py check - успешно
This commit is contained in:
lizerk 2026-03-10 13:05:58 +03:00
parent 95926151b0
commit 7e410ebff8
7 changed files with 333 additions and 2 deletions

View File

@ -7,6 +7,7 @@ from django.db.models import BooleanField, Count, Exists, F, OuterRef, Subquery,
from employees.constants import DeptPerm
from feed.constants import TYPE_COMPANY, TYPE_DEPARTMENT, TYPE_EMPLOYEE
from feed.models import Comment, Post, PostLike
from feed.notifications import notify_post_reaction
from rest_framework import filters, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
@ -227,6 +228,8 @@ class PostViewSet(viewsets.ModelViewSet):
_, created = PostLike.objects.get_or_create(post=post, user=request.user)
if created:
Post.objects.filter(pk=post.pk).update(likes_count=F("likes_count") + 1)
# Отправляем уведомление автору публикации
notify_post_reaction(post, request.user)
post.refresh_from_db(fields=["likes_count"])
return Response(
{"liked": True, "likes_count": post.likes_count}, status=status.HTTP_200_OK

View File

@ -6,6 +6,6 @@ class FeedConfig(AppConfig):
name = 'feed'
def ready(self):
# Подключаем signals для уведомлений
import feed.notification_signals # noqa
# Подключаем модуль уведомлений (signals регистрируются автоматически)
import feed.notifications # noqa
import feed.rules # django-rules: регистрация предикатов и правил доступа

View File

@ -0,0 +1,29 @@
"""
Модуль уведомлений для Feed приложения.
Публичный API:
- NotificationVerbs - константы глаголов уведомлений
- MessageTemplates - шаблоны сообщений
- ActionURLs - генераторы URL-адресов
- notify_post_reaction - функция для уведомления о реакциях (вызывается из views)
Использование:
from feed.notifications import notify_post_reaction
# В view при добавлении лайка:
notify_post_reaction(post, request.user)
"""
from .config import NotificationVerbs, MessageTemplates, ActionURLs
from .handlers import notify_post_reaction
# Импортируем handlers и signals для регистрации
from . import handlers # noqa: F401
from . import signals # noqa: F401
__all__ = [
'NotificationVerbs',
'MessageTemplates',
'ActionURLs',
'notify_post_reaction',
]

View File

@ -0,0 +1,93 @@
"""
Конфигурация уведомлений для модуля Feed.
Содержит:
- NotificationVerbs - глаголы для уведомлений
- MessageTemplates - шаблоны сообщений
- ActionURLs - URL-адреса действий
"""
class NotificationVerbs:
"""Глаголы (verbs) для уведомлений feed."""
NEW_POST = 'feed_new_post'
POST_COMMENT = 'feed_post_comment'
POST_REACTION = 'feed_post_reaction'
class MessageTemplates:
"""Шаблоны сообщений для уведомлений feed."""
@staticmethod
def new_post_company() -> str:
"""Шаблон для новости компании."""
return 'Новая публикация компании'
@staticmethod
def new_post_department(department_name: str) -> str:
"""Шаблон для новости отдела."""
return f'Новая публикация отдела {department_name}'
@staticmethod
def new_post_employee(author_name: str) -> str:
"""Шаблон для личной публикации."""
return f'Новая публикация от {author_name}'
@staticmethod
def comment(author_name: str, comment_text: str) -> str:
"""Шаблон для комментария к публикации."""
return f'{author_name} прокомментировал: {comment_text}'
@staticmethod
def comment_title() -> str:
"""Заголовок для уведомления о комментарии."""
return 'Новый комментарий к вашей публикации'
@staticmethod
def reaction(user_name: str, post_title: str) -> str:
"""Шаблон для реакции на публикацию."""
return f'{user_name} понравилась публикация "{post_title}"'
@staticmethod
def reaction_title() -> str:
"""Заголовок для уведомления о реакции."""
return 'Ваша публикация понравилась'
class ActionURLs:
"""URL-адреса для действий с публикациями."""
# Пока все действия ведут на главную страницу
# TODO: реализовать прямые ссылки на публикации
FEED_HOME = '/'
@staticmethod
def post_detail(post_id: int) -> str:
"""
Возвращает URL-адрес публикации.
TODO: реализовать отдельную страницу публикации
"""
return f'/?post={post_id}' # Временное решение
def truncate_text(text: str, max_length: int = 100) -> str:
"""
Обрезает текст до указанной длины.
Args:
text: Исходный текст
max_length: Максимальная длина (по умолчанию 100)
Returns:
Обрезанный текст с многоточием если нужно
"""
if not text:
return ''
text = text.strip()
if len(text) <= max_length:
return text
return text[:max_length - 3] + '...'

View File

@ -0,0 +1,162 @@
"""
Бизнес-логика отправки уведомлений для модуля Feed.
Функции:
- notify_new_post - уведомление о новой публикации
- notify_comment - уведомление о комментарии
- notify_post_reaction - уведомление о реакции (лайке)
- get_post_recipients - определение получателей уведомлений
"""
from django.contrib.auth import get_user_model
from notifications.signals import notify
from ..constants import TYPE_COMPANY, TYPE_DEPARTMENT, TYPE_EMPLOYEE
from .config import (
NotificationVerbs,
MessageTemplates,
ActionURLs,
truncate_text,
)
Employee = get_user_model()
def notify_new_post(post):
"""
Отправляет уведомления о новой публикации.
Args:
post: Экземпляр модели Post
"""
recipients = get_post_recipients(post)
# Исключаем автора
recipients = [r for r in recipients if r.id != post.author.id]
# Определяем заголовок в зависимости от типа публикации
if post.type == TYPE_COMPANY:
title = MessageTemplates.new_post_company()
elif post.type == TYPE_DEPARTMENT and post.department:
title = MessageTemplates.new_post_department(post.department.name)
else:
author_name = post.author.get_full_name() or post.author.username
title = MessageTemplates.new_post_employee(author_name)
# Отправляем уведомление каждому получателю
for recipient in recipients:
notify.send(
sender=post.author,
recipient=recipient,
verb=NotificationVerbs.NEW_POST,
action_object=post,
description=post.title,
action_url=ActionURLs.FEED_HOME,
data={
'title': title,
'post_id': post.id,
'post_type': post.type,
'post_title': post.title,
'author_id': post.author.id,
'department_id': (
post.department.id if post.department else None
),
}
)
def notify_comment(comment):
"""
Отправляет уведомление автору публикации о новом комментарии.
Args:
comment: Экземпляр модели Comment
"""
post = comment.post
comment_author = comment.author
# Уведомляем автора публикации (если комментарий не от него)
if post.author.id == comment_author.id:
return
author_name = comment_author.get_full_name() or comment_author.username
comment_text = truncate_text(comment.text, 80)
notify.send(
sender=comment_author,
recipient=post.author,
verb=NotificationVerbs.POST_COMMENT,
action_object=post,
description=MessageTemplates.comment(author_name, comment_text),
action_url=ActionURLs.FEED_HOME,
data={
'title': MessageTemplates.comment_title(),
'post_id': post.id,
'post_title': post.title,
'comment_id': comment.id,
'author_id': comment_author.id,
}
)
def notify_post_reaction(post, user):
"""
Создает уведомление о реакции (лайке) на публикацию.
Вызывается вручную из view, так как лайки не имеют отдельной модели.
Args:
post: Публикация, которую лайкнули
user: Пользователь, который поставил лайк
"""
# Уведомляем автора (если лайк не от него)
if post.author.id == user.id:
return
user_name = user.get_full_name() or user.username
notify.send(
sender=user,
recipient=post.author,
verb=NotificationVerbs.POST_REACTION,
action_object=post,
description=MessageTemplates.reaction(user_name, post.title),
action_url=ActionURLs.FEED_HOME,
data={
'title': MessageTemplates.reaction_title(),
'post_id': post.id,
'post_title': post.title,
'user_id': user.id,
'reaction_type': 'like',
}
)
def get_post_recipients(post):
"""
Возвращает список получателей уведомлений для публикации.
Args:
post: Экземпляр модели Post
Returns:
Список пользователей (Employee), которые должны получить уведомление:
- TYPE_COMPANY - все активные сотрудники
- TYPE_DEPARTMENT - сотрудники отдела
- TYPE_EMPLOYEE - подписчики (пока не реализовано)
"""
if post.type == TYPE_COMPANY:
# Новость компании - всем активным сотрудникам
return list(Employee.objects.filter(is_active=True))
elif post.type == TYPE_DEPARTMENT and post.department:
# Новость отдела - сотрудникам отдела
return list(post.department.employees.filter(is_active=True))
elif post.type == TYPE_EMPLOYEE:
# Личная публикация - подписчикам
# TODO: реализовать систему подписок
# Пока возвращаем пустой список
return []
return []

View File

@ -0,0 +1,44 @@
"""
Django signals для автоматической генерации уведомлений в модуле Feed.
Обрабатывает события:
- post_save для Post - создание новой публикации
- post_save для Comment - добавление комментария
"""
from django.db.models.signals import post_save
from django.dispatch import receiver
from ..models import Post, Comment
from .handlers import notify_new_post, notify_comment
@receiver(post_save, sender=Post)
def create_post_notification(sender, instance, created, **kwargs):
"""
Создает уведомления при создании новой публикации.
Уведомления отправляются в зависимости от типа:
- TYPE_COMPANY - всем сотрудникам
- TYPE_DEPARTMENT - сотрудникам отдела
- TYPE_EMPLOYEE - подписчикам автора (если реализовано)
"""
if not created:
return
# Отправляем уведомления через универсальную систему
# channels.py автоматически обработает через Celery
notify_new_post(instance)
@receiver(post_save, sender=Comment)
def create_comment_notification(sender, instance, created, **kwargs):
"""
Создает уведомления при добавлении комментария к публикации.
Уведомляет автора публикации о новом комментарии.
"""
if not created:
return
notify_comment(instance)