2026-09-07 19:43:48 +03:00

333 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import logging
from django.core.cache import cache
from django.db.models import Count, Q
UNREAD_SUMMARY_TIMEOUT = 60
logger = logging.getLogger(__name__)
def unread_summary_cache_key(user_id):
return f"notifications:unread_summary:v2:user:{user_id}"
def _coerce_positive_int(value):
try:
integer_value = int(value)
except (TypeError, ValueError):
return None
return integer_value if integer_value > 0 else None
def _build_procurement_request_counts(user_id):
from django.contrib.contenttypes.models import ContentType
from procurement.models import ProcurementRequest
from .models import Notification
request_content_type = ContentType.objects.get_for_model(
ProcurementRequest,
)
request_content_type_id = request_content_type.id
counts = {}
notifications = (
Notification.objects.filter(
recipient_id=user_id,
unread=True,
deleted=False,
)
.filter(Q(verb__startswith="procurement_") | Q(verb__startswith="equipment_"))
.only(
"data",
"target_content_type_id",
"target_object_id",
"action_object_content_type_id",
"action_object_object_id",
)
)
for notification in notifications:
data = notification.data if isinstance(notification.data, dict) else {}
request_id = _coerce_positive_int(data.get("request_id"))
if request_id is None and (
notification.target_content_type_id == request_content_type_id
):
request_id = _coerce_positive_int(notification.target_object_id)
if request_id is None and (
notification.action_object_content_type_id == request_content_type_id
):
request_id = _coerce_positive_int(
notification.action_object_object_id,
)
if request_id is not None:
counts[request_id] = counts.get(request_id, 0) + 1
return [
{"request_id": request_id, "unread": unread}
for request_id, unread in sorted(counts.items())
]
def _build_employee_request_counts(user_id):
from django.contrib.contenttypes.models import ContentType
from requests_app.models import Request as EmployeeRequest
from .models import Notification
request_content_type_id = ContentType.objects.get_for_model(EmployeeRequest).id
counts = {}
notifications = Notification.objects.filter(
recipient_id=user_id,
unread=True,
deleted=False,
verb__startswith="request_",
).only(
"data",
"target_content_type_id",
"target_object_id",
"action_object_content_type_id",
"action_object_object_id",
)
for notification in notifications:
data = notification.data if isinstance(notification.data, dict) else {}
request_id = _coerce_positive_int(data.get("request_id"))
if request_id is None and (
notification.target_content_type_id == request_content_type_id
):
request_id = _coerce_positive_int(notification.target_object_id)
if request_id is None and (
notification.action_object_content_type_id == request_content_type_id
):
request_id = _coerce_positive_int(
notification.action_object_object_id,
)
if request_id is not None:
counts[request_id] = counts.get(request_id, 0) + 1
return [
{"request_id": request_id, "unread": unread}
for request_id, unread in sorted(counts.items())
]
def _build_procurement_item_comment_counts(user_id):
from .models import Notification
counts = {}
notifications = Notification.objects.filter(
recipient_id=user_id,
unread=True,
deleted=False,
verb="procurement_item_commented",
).only("data")
for notification in notifications:
data = notification.data if isinstance(notification.data, dict) else {}
item_id = _coerce_positive_int(data.get("item_id"))
if item_id is not None:
counts[item_id] = counts.get(item_id, 0) + 1
return [
{"item_id": item_id, "unread": unread}
for item_id, unread in sorted(counts.items())
]
def _build_task_board_counts(user_id):
from django.contrib.contenttypes.models import ContentType
from tasks.models import Task, TaskBoard
from .models import Notification
task_content_type_id = ContentType.objects.get_for_model(Task).id
board_content_type_id = ContentType.objects.get_for_model(TaskBoard).id
counts = {}
unresolved_task_ids = set()
unresolved_notifications = []
notifications = Notification.objects.filter(
recipient_id=user_id,
unread=True,
deleted=False,
verb__startswith="task_",
).only(
"data",
"target_content_type_id",
"target_object_id",
"action_object_content_type_id",
"action_object_object_id",
)
for notification in notifications:
data = notification.data if isinstance(notification.data, dict) else {}
board_id = _coerce_positive_int(data.get("board_id"))
if board_id is None and (
notification.action_object_content_type_id == board_content_type_id
):
board_id = _coerce_positive_int(
notification.action_object_object_id,
)
if board_id is not None:
counts[board_id] = counts.get(board_id, 0) + 1
continue
task_id = None
if notification.target_content_type_id == task_content_type_id:
task_id = _coerce_positive_int(notification.target_object_id)
elif notification.action_object_content_type_id == task_content_type_id:
task_id = _coerce_positive_int(
notification.action_object_object_id,
)
if task_id is not None:
unresolved_task_ids.add(task_id)
unresolved_notifications.append(task_id)
if unresolved_task_ids:
task_boards = dict(
Task.objects.filter(id__in=unresolved_task_ids).values_list(
"id",
"board_id",
)
)
for task_id in unresolved_notifications:
board_id = task_boards.get(task_id)
if board_id is not None:
counts[board_id] = counts.get(board_id, 0) + 1
return [
{"board_id": board_id, "unread": unread}
for board_id, unread in sorted(counts.items())
]
def _build_regulation_counts(user_id):
"""Группирует непрочитанные уведомления о регламентах по отделам."""
from .models import Notification
department_counts = {}
company_unread = 0
personal_unread = 0
notifications = Notification.objects.filter(
recipient_id=user_id,
unread=True,
deleted=False,
verb__startswith="regulation_",
).only("data")
for notification in notifications:
data = notification.data if isinstance(notification.data, dict) else {}
raw_department_ids = data.get("regulation_department_ids")
department_ids = set()
if isinstance(raw_department_ids, list):
for value in raw_department_ids:
department_id = _coerce_positive_int(value)
if department_id is not None:
department_ids.add(department_id)
for department_id in department_ids:
department_counts[department_id] = (
department_counts.get(department_id, 0) + 1
)
if not department_ids:
if data.get("regulation_scope") == "company":
company_unread += 1
else:
personal_unread += 1
return {
"departments": [
{"department_id": department_id, "unread": unread}
for department_id, unread in sorted(department_counts.items())
],
"company_unread": company_unread,
"personal_unread": personal_unread,
}
def build_unread_summary(user_id):
from .models import Notification
rows = (
Notification.objects.filter(
recipient_id=user_id,
unread=True,
deleted=False,
)
.values("verb")
.annotate(unread=Count("id"))
.order_by()
)
verbs = [{"verb": row["verb"], "unread": row["unread"]} for row in rows]
regulation_counts = _build_regulation_counts(user_id)
return {
"total": sum(item["unread"] for item in verbs),
"verbs": verbs,
"employee_requests": _build_employee_request_counts(user_id),
"procurement_requests": _build_procurement_request_counts(user_id),
"procurement_item_comments": _build_procurement_item_comment_counts(user_id),
"task_boards": _build_task_board_counts(user_id),
"regulation_departments": regulation_counts["departments"],
"regulation_company_unread": regulation_counts["company_unread"],
"regulation_personal_unread": regulation_counts["personal_unread"],
}
def get_unread_summary(user_id):
key = unread_summary_cache_key(user_id)
try:
cached = cache.get(key)
except Exception:
cached = None
if cached is not None:
return cached
summary = build_unread_summary(user_id)
try:
cache.set(key, summary, UNREAD_SUMMARY_TIMEOUT)
except Exception as exc:
logger.warning(
"Unread notification summary could not be cached",
extra={
"event": "notifications.unread_summary.cache_set_failed",
"user_id": user_id,
"error_type": type(exc).__name__,
},
)
return summary
def get_unread_count(user_id):
return get_unread_summary(user_id)["total"]
def invalidate_unread_summary(user_id):
if not user_id:
return
try:
cache.delete(unread_summary_cache_key(user_id))
except Exception as exc:
logger.warning(
"Unread notification summary cache could not be invalidated",
extra={
"event": "notifications.unread_summary.cache_delete_failed",
"user_id": user_id,
"error_type": type(exc).__name__,
},
)