cleanup: Удален мертвый код task_base.py

- Файл task_base.py (500 строк) использовал удаленный NotificationService API
- Нигде не импортировался и не использовался
- Создавал путаницу с новым tasks/base.py
- Экономия: -500 строк кода
This commit is contained in:
lizerk 2026-03-09 16:44:54 +03:00
parent aa614bb4c1
commit 2381288bbf

View File

@ -1,607 +0,0 @@
"""
Базовые классы и утилиты для Celery задач уведомлений.
Предоставляет общую логику для обработки уведомлений,
чтобы избежать дублирования кода в tasks.py
"""
import logging
from typing import Set, Optional, Dict, Any
from django.contrib.auth import get_user_model
from celery import Task
logger = logging.getLogger(__name__)
User = get_user_model()
class BaseNotificationProcessor:
"""
Базовый класс для процессоров уведомлений.
Содержит общую логику определения получателей и отправки уведомлений.
"""
def __init__(self, task: Task = None):
"""
Args:
task: Celery task для retry при ошибках
"""
self.task = task
self.logger = logging.getLogger(self.__class__.__name__)
def send_notifications(
self,
recipients: Set[User],
notification_type: str,
title: str,
message: str,
content_object=None,
action_url: str = '',
metadata: Optional[Dict[str, Any]] = None
) -> int:
"""
Отправляет уведомления списку получателей.
Args:
recipients: Набор получателей
notification_type: Код типа уведомления
title: Заголовок
message: Текст сообщения
content_object: Связанный объект
action_url: URL для действия
metadata: Дополнительные данные
Returns:
Количество отправленных уведомлений
"""
from notifications.services import NotificationService
count = 0
for recipient in recipients:
try:
NotificationService.create_notification(
recipient=recipient,
notification_type_code=notification_type,
title=title,
message=message,
content_object=content_object,
action_url=action_url,
metadata=metadata or {}
)
count += 1
except Exception as e:
self.logger.error(
f"Failed to send notification to user {recipient.id}: {e}"
)
return count
def get_active_employees(self, exclude_ids: Set[int] = None) -> Set[User]:
"""
Получает всех активных сотрудников.
Args:
exclude_ids: ID пользователей для исключения
Returns:
Набор активных сотрудников
"""
queryset = User.objects.filter(is_active=True)
if exclude_ids:
queryset = queryset.exclude(id__in=exclude_ids)
return set(queryset)
def get_department_employees(
self,
department_ids: list,
exclude_ids: Set[int] = None
) -> Set[User]:
"""
Получает сотрудников отделов.
Args:
department_ids: Список ID отделов
exclude_ids: ID пользователей для исключения
Returns:
Набор сотрудников отделов
"""
queryset = User.objects.filter(
departments_links__department_id__in=department_ids,
departments_links__is_active=True,
is_active=True
).distinct()
if exclude_ids:
queryset = queryset.exclude(id__in=exclude_ids)
return set(queryset)
def log_result(
self,
entity_type: str,
entity_id: int,
action: str,
notifications_sent: int
) -> Dict[str, Any]:
"""
Логирует результат обработки и возвращает словарь результата.
Args:
entity_type: Тип сущности (message, event, post и т.д.)
entity_id: ID сущности
action: Выполненное действие
notifications_sent: Количество отправленных уведомлений
Returns:
Словарь с результатом операции
"""
self.logger.info(
f"Processed {entity_type} {entity_id} ({action}): "
f"sent {notifications_sent} notifications"
)
return {
"status": "success",
f"{entity_type}_id": entity_id,
"action": action,
"notifications_sent": notifications_sent
}
def handle_not_found(
self,
entity_type: str,
entity_id: int
) -> Dict[str, Any]:
"""
Обрабатывает случай когда сущность не найдена.
Args:
entity_type: Тип сущности
entity_id: ID сущности
Returns:
Словарь с ошибкой
"""
self.logger.error(f"{entity_type.capitalize()} {entity_id} not found")
return {
"status": "error",
"reason": f"{entity_type}_not_found"
}
def handle_exception(self, exc: Exception):
"""
Обрабатывает исключение - логирует и делает retry.
Args:
exc: Исключение для обработки
"""
self.logger.exception(f"Failed to process notifications: {exc}")
if self.task:
raise self.task.retry(exc=exc)
else:
raise
class MessageNotificationProcessor(BaseNotificationProcessor):
"""Процессор уведомлений для сообщений в чатах"""
def process(self, message_id: int) -> Dict[str, Any]:
"""
Обрабатывает уведомления для сообщения.
Args:
message_id: ID сообщения
Returns:
Результат обработки
"""
try:
from communications.models import Message
from communications.notification_signals import (
get_users_with_notifications_enabled,
extract_mentions,
truncate_message,
get_chat_name
)
# Загружаем сообщение
message = Message.objects.select_related(
'chat', 'author', 'reply_to__author'
).get(id=message_id)
if message.is_system or message.is_deleted:
return {"status": "skipped", "reason": "system_or_deleted"}
chat = message.chat
author = message.author
content = message.content
# Получаем участников
if chat.type in ['announcement', 'channel', 'department', 'global']:
participants = chat.get_participants.exclude(id=author.id)
else:
participants = chat.participants.exclude(id=author.id)
participants_list = list(participants)
if not participants_list:
return {"status": "success", "notifications_sent": 0}
# Настройки уведомлений
notification_settings = get_users_with_notifications_enabled(
chat, participants_list
)
notifications_count = 0
mentioned_user_ids = []
# 1. Упоминания
mentioned_users = extract_mentions(content)
if mentioned_users:
for email in mentioned_users:
try:
user = User.objects.get(email=email)
if user in participants_list and user.id != author.id:
if notification_settings.get(user.id, True):
notifications_count += self.send_notifications(
{user},
'chat_mention',
f'Вас упомянул {author.get_full_name() or author.username}',
truncate_message(content, 100),
message,
f'/communications/chats/{chat.id}/?message={message.id}',
{
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': message.id,
'author_id': author.id,
}
)
mentioned_user_ids.append(user.id)
except User.DoesNotExist:
continue
# 2. Ответы на сообщения
if message.reply_to and message.reply_to.author_id != author.id:
original_author = message.reply_to.author
if original_author.id not in mentioned_user_ids:
if notification_settings.get(original_author.id, True):
notifications_count += self.send_notifications(
{original_author},
'chat_reply',
f'{author.get_full_name() or author.username} ответил на ваше сообщение',
truncate_message(content, 100),
message,
f'/communications/chats/{chat.id}/?message={message.id}',
{
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': message.id,
'reply_to_id': message.reply_to.id,
'author_id': author.id,
}
)
# 3. Обычные уведомления
excluded_ids = set(mentioned_user_ids)
if message.reply_to and message.reply_to.author_id != author.id:
excluded_ids.add(message.reply_to.author_id)
if chat.type == 'announcement':
notification_type = 'announcement_new_message'
title = f'Новое объявление от {author.get_full_name() or author.username}'
is_announcement = True
else:
notification_type = 'chat_new_message'
if chat.type == 'private':
title = f'Новое сообщение от {author.get_full_name() or author.username}'
else:
title = f'{author.get_full_name() or author.username} в {get_chat_name(chat)}'
is_announcement = False
for recipient in participants_list:
if recipient.id in excluded_ids:
continue
if not notification_settings.get(recipient.id, True):
continue
metadata = {
'chat_id': chat.id,
'chat_name': get_chat_name(chat),
'message_id': message.id,
'author_id': author.id,
}
if is_announcement:
metadata['is_announcement'] = True
notifications_count += self.send_notifications(
{recipient},
notification_type,
title,
truncate_message(content, 150 if is_announcement else 100),
message,
f'/communications/chats/{chat.id}/',
metadata
)
return self.log_result('message', message_id, 'created', notifications_count)
except Message.DoesNotExist:
return self.handle_not_found('message', message_id)
except Exception as exc:
self.handle_exception(exc)
def truncate_text(text: str, max_length: int = 100) -> str:
"""
Обрезает текст до указанной длины, добавляя троеточие.
Args:
text: Исходный текст
max_length: Максимальная длина
Returns:
Обрезанный текст
"""
if not text or len(text) <= max_length:
return text or ''
return text[:max_length].rstrip() + '...'
class EventNotificationProcessor(BaseNotificationProcessor):
"""Процессор уведомлений о событиях в календаре"""
def process(self, event_id: int) -> Dict[str, Any]:
"""Обработка уведомлений для события календаря"""
try:
from calendar_app.models import CalendarEvent
from django.contrib.auth import get_user_model
User = get_user_model()
event = CalendarEvent.objects.select_related('created_by', 'employee', 'department').get(id=event_id)
# Собираем всех получателей
recipients = set()
# Если событие отдела - уведомляем всех сотрудников отдела
if event.department:
recipients.update(self.get_department_employees([event.department.id]))
# Если личное событие сотрудника - уведомляем только его
elif event.employee:
# Проверяем что сотрудник активен
if event.employee.is_active and event.employee.email_verified:
recipients.add(event.employee)
# Если глобальное событие (нет department и employee) - уведомляем всех активных
else:
recipients = set(User.objects.filter(is_active=True, email_verified=True))
# Исключаем создателя
if event.created_by:
recipients.discard(event.created_by)
if not recipients:
return self.log_result('event', event_id, 'skipped', 0)
# Отправляем уведомления
notifications_sent = self.send_notifications(
recipients=recipients,
notification_type='event_created',
title=f'Новое событие: {event.title}',
message=truncate_text(event.description or '', 150),
content_object=event,
action_url='/calendar',
metadata={
'event_id': event.id,
'event_title': event.title,
'start_time': event.start_time.isoformat() if event.start_time else None,
'end_time': event.end_time.isoformat() if event.end_time else None,
}
)
return self.log_result('event', event_id, 'created', notifications_sent)
except Exception as exc:
self.handle_exception(exc)
class PostNotificationProcessor(BaseNotificationProcessor):
"""Процессор уведомлений о постах в ленте"""
def process(self, post_id: int) -> Dict[str, Any]:
"""Обработка уведомлений для поста"""
try:
from feed.models import Post
from django.contrib.auth import get_user_model
User = get_user_model()
post = Post.objects.select_related('author', 'department').get(id=post_id)
# Собираем получателей
recipients = set()
# Если пост для конкретного отдела - уведомляем сотрудников отдела
if post.department:
recipients.update(self.get_department_employees([post.department.id]))
else:
# Иначе уведомляем всех активных сотрудников
recipients = set(User.objects.filter(is_active=True, email_verified=True))
# Исключаем автора
if post.author:
recipients.discard(post.author)
if not recipients:
return self.log_result('post', post_id, 'skipped', 0)
# Отправляем уведомления
notifications_sent = self.send_notifications(
recipients=recipients,
notification_type='feed_new_post',
title=f'{post.title}',
message=truncate_text(post.body, 200),
content_object=post,
action_url='/',
metadata={
'post_id': post.id,
'author_id': post.author.id if post.author else None,
'post_type': post.type,
}
)
return self.log_result('post', post_id, 'created', notifications_sent)
except Exception as exc:
self.handle_exception(exc)
class RequestNotificationProcessor(BaseNotificationProcessor):
"""Процессор уведомлений о заявках"""
def process(self, request_id: int, notification_type: str = 'created') -> Dict[str, Any]:
"""
Обработка уведомлений для заявки
Args:
request_id: ID заявки
notification_type: Тип уведомления (created, status_changed, comment_added)
"""
try:
from requests_app.models import Request
request_obj = Request.objects.select_related('employee', 'approver').prefetch_related('recipients', 'cc_users').get(id=request_id)
# Собираем получателей в зависимости от типа уведомления
recipients = set()
if notification_type == 'created':
# Уведомляем согласующего и получателей
if request_obj.approver:
recipients.add(request_obj.approver)
for user in request_obj.recipients.all():
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
for user in request_obj.cc_users.all():
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
elif notification_type == 'status_changed':
# Уведомляем создателя заявки
if request_obj.employee:
if request_obj.employee.is_active and getattr(request_obj.employee, 'email_verified', True):
recipients.add(request_obj.employee)
elif notification_type == 'comment_added':
# Уведомляем всех участников
if request_obj.employee:
if request_obj.employee.is_active and getattr(request_obj.employee, 'email_verified', True):
recipients.add(request_obj.employee)
if request_obj.approver:
if request_obj.approver.is_active and getattr(request_obj.approver, 'email_verified', True):
recipients.add(request_obj.approver)
for user in request_obj.recipients.all():
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
for user in request_obj.cc_users.all():
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
if not recipients:
return self.log_result('request', request_id, notification_type, 0)
# Формируем заголовок
title_map = {
'created': f'Новая заявка #{request_obj.id}: {request_obj.title or request_obj.get_type_display()}',
'status_changed': f'Изменён статус заявки #{request_obj.id}',
'comment_added': f'Новый комментарий к заявке #{request_obj.id}',
}
type_code_map = {
'created': 'request_new',
'status_changed': 'request_status_changed',
'comment_added': 'request_comment',
}
# Отправляем уведомления
notifications_sent = self.send_notifications(
recipients=recipients,
notification_type=type_code_map.get(notification_type, 'request_new'),
title=title_map.get(notification_type, f'Обновление заявки #{request_obj.id}'),
message=truncate_text(request_obj.comment or '', 150),
content_object=request_obj,
action_url='/requests',
metadata={
'request_id': request_obj.id,
'request_type': request_obj.type,
'status': request_obj.status,
}
)
return self.log_result('request', request_id, notification_type, notifications_sent)
except Exception as exc:
self.handle_exception(exc)
class DocumentNotificationProcessor(BaseNotificationProcessor):
"""Процессор уведомлений о документах"""
def process(self, document_id: int) -> Dict[str, Any]:
"""Обработка уведомлений для документа"""
try:
from documents.models import Document
from django.contrib.auth import get_user_model
User = get_user_model()
document = Document.objects.select_related('uploaded_by').prefetch_related('departments', 'recipients').get(id=document_id)
# Собираем получателей
recipients = set()
# Если разослать всем
if document.sent_to_all:
recipients = set(User.objects.filter(is_active=True, email_verified=True))
else:
# Департаменты
if document.departments.exists():
dept_ids = [d.id for d in document.departments.all()]
recipients.update(self.get_department_employees(dept_ids))
# Конкретные получатели
for user in document.recipients.all():
if user.is_active and getattr(user, 'email_verified', True):
recipients.add(user)
# Исключаем автора
if document.uploaded_by:
recipients.discard(document.uploaded_by)
if not recipients:
return self.log_result('document', document_id, 'skipped', 0)
# Отправляем уведомления
notifications_sent = self.send_notifications(
recipients=recipients,
notification_type='document_ready',
title=f'Новый документ: {document.title}',
message=truncate_text(document.description or '', 150),
content_object=document,
action_url='/documents',
metadata={
'document_id': document.id,
'document_title': document.title,
}
)
return self.log_result('document', document_id, 'created', notifications_sent)
except Exception as exc:
self.handle_exception(exc)