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:
parent
95926151b0
commit
7e410ebff8
@ -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
|
||||
|
||||
@ -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: регистрация предикатов и правил доступа
|
||||
|
||||
29
backend/feed/notifications/__init__.py
Normal file
29
backend/feed/notifications/__init__.py
Normal 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',
|
||||
]
|
||||
93
backend/feed/notifications/config.py
Normal file
93
backend/feed/notifications/config.py
Normal 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] + '...'
|
||||
162
backend/feed/notifications/handlers.py
Normal file
162
backend/feed/notifications/handlers.py
Normal 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 []
|
||||
44
backend/feed/notifications/signals.py
Normal file
44
backend/feed/notifications/signals.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user