from django.core.cache import cache from django.db.models import Count from django.db.models import Q UNREAD_SUMMARY_TIMEOUT = 60 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: pass 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: pass