diff --git a/python/apps/taiga/src/taiga/__main__.py b/python/apps/taiga/src/taiga/__main__.py index 8830d3e70..56e7f2f8f 100644 --- a/python/apps/taiga/src/taiga/__main__.py +++ b/python/apps/taiga/src/taiga/__main__.py @@ -35,6 +35,7 @@ from taiga.base.sampledata.commands import cli as sampledata_cli from taiga.commons.storage.commands import cli as storage_cli from taiga.emails.commands import cli as emails_cli +from taiga.notifications.commands import cli as notifications_cli from taiga.tasksqueue.commands import cli as tasksqueue_cli from taiga.tasksqueue.commands import init as init_tasksqueue from taiga.tasksqueue.commands import run_worker, worker @@ -69,9 +70,10 @@ def main( cli.add_typer(db_cli, name="db") cli.add_typer(emails_cli, name="emails") cli.add_typer(i18n_cli, name="i18n") +cli.add_typer(notifications_cli, name="notifications") cli.add_typer(sampledata_cli, name="sampledata") -cli.add_typer(tasksqueue_cli, name="tasksqueue") cli.add_typer(storage_cli, name="storage") +cli.add_typer(tasksqueue_cli, name="tasksqueue") cli.add_typer(tokens_cli, name="tokens") cli.add_typer(users_cli, name="users") diff --git a/python/apps/taiga/src/taiga/conf/__init__.py b/python/apps/taiga/src/taiga/conf/__init__.py index 09ccdcd97..d6eab6bc4 100644 --- a/python/apps/taiga/src/taiga/conf/__init__.py +++ b/python/apps/taiga/src/taiga/conf/__init__.py @@ -17,6 +17,7 @@ from taiga.conf.events import EventsSettings from taiga.conf.images import ImageSettings from taiga.conf.logs import LOGGING_CONFIG +from taiga.conf.notifications import NotificationsSettings from taiga.conf.storage import StorageSettings from taiga.conf.tasksqueue import TaskQueueSettings from taiga.conf.tokens import TokensSettings @@ -102,6 +103,7 @@ class Settings(BaseSettings): EMAIL: EmailSettings = EmailSettings() EVENTS: EventsSettings = EventsSettings() IMAGES: ImageSettings = ImageSettings() + NOTIFICATIONS: NotificationsSettings = NotificationsSettings() STORAGE: StorageSettings = StorageSettings() TASKQUEUE: TaskQueueSettings = TaskQueueSettings() TOKENS: TokensSettings = TokensSettings() diff --git a/python/apps/taiga/src/taiga/conf/notifications.py b/python/apps/taiga/src/taiga/conf/notifications.py new file mode 100644 index 000000000..27e80c8d4 --- /dev/null +++ b/python/apps/taiga/src/taiga/conf/notifications.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2023-present Kaleidos INC + +from pydantic import BaseSettings + + +class NotificationsSettings(BaseSettings): + CLEAN_READ_NOTIFICATIONS_CRON: str = "30 * * * *" # default: every hour at minute 30. + MINUTES_TO_STORE_READ_NOTIFICATIONS: int = 2 * 60 # 120 minutes diff --git a/python/apps/taiga/src/taiga/conf/tasksqueue.py b/python/apps/taiga/src/taiga/conf/tasksqueue.py index 4c1cac184..24e555b80 100644 --- a/python/apps/taiga/src/taiga/conf/tasksqueue.py +++ b/python/apps/taiga/src/taiga/conf/tasksqueue.py @@ -13,6 +13,7 @@ class TaskQueueSettings(BaseSettings): TASKS_MODULES_PATHS: set[str] = { "taiga.commons.storage.tasks", "taiga.emails.tasks", + "taiga.notifications.tasks", "taiga.projects.projects.tasks", "taiga.tokens.tasks", "taiga.users.tasks", diff --git a/python/apps/taiga/src/taiga/notifications/commands.py b/python/apps/taiga/src/taiga/notifications/commands.py new file mode 100644 index 000000000..c32df4e10 --- /dev/null +++ b/python/apps/taiga/src/taiga/notifications/commands.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2023-present Kaleidos INC + +from datetime import timedelta + +import typer +from taiga.base.utils import pprint +from taiga.base.utils.concurrency import run_async_as_sync +from taiga.base.utils.datetime import aware_utcnow +from taiga.conf import settings +from taiga.notifications import services as notifications_services + +cli = typer.Typer( + name="Taiga Notifications commands", + help="Manage the notifications system of Taiga.", + add_completion=True, +) + + +@cli.command(help="Clean read notifications. Remove entries from DB.") +def clean_read_notifications( + minutes_to_store_read_notifications: int = typer.Option( + settings.NOTIFICATIONS.MINUTES_TO_STORE_READ_NOTIFICATIONS, + "--minutes", + "-m", + help="Number of minutes to store read notifications", + ), +) -> None: + total_deleted = run_async_as_sync( + notifications_services.clean_read_notifications( + before=aware_utcnow() - timedelta(minutes=minutes_to_store_read_notifications) + ) + ) + + color = "red" if total_deleted else "white" + pprint.print(f"Deleted [bold][{color}]{total_deleted}[/{color}][/bold] notifications.") diff --git a/python/apps/taiga/src/taiga/notifications/repositories.py b/python/apps/taiga/src/taiga/notifications/repositories.py index 9d1b1b5e7..1dd33fbd6 100644 --- a/python/apps/taiga/src/taiga/notifications/repositories.py +++ b/python/apps/taiga/src/taiga/notifications/repositories.py @@ -6,6 +6,7 @@ # Copyright (c) 2023-present Kaleidos INC from collections.abc import Iterable +from datetime import datetime from typing import Any, TypedDict from uuid import UUID @@ -25,6 +26,7 @@ class NotificationFilters(TypedDict, total=False): id: UUID owner: User is_read: bool + read_before: datetime async def _apply_filters_to_queryset( @@ -36,6 +38,9 @@ async def _apply_filters_to_queryset( if "is_read" in filter_data: is_read = filter_data.pop("is_read") filter_data["read_at__isnull"] = not is_read + if "read_before" in filter_data: + read_before = filter_data.pop("read_before") + filter_data["read_at__lt"] = read_before return qs.filter(**filter_data) @@ -111,6 +116,17 @@ async def mark_notifications_as_read( return [a async for a in qs.all()] +########################################################## +# delete notifications +########################################################## + + +async def delete_notifications(filters: NotificationFilters = {}) -> int: + qs = await _apply_filters_to_queryset(qs=DEFAULT_QUERYSET, filters=filters) + count, _ = await qs.adelete() + return count + + ########################################################## # misc ########################################################## diff --git a/python/apps/taiga/src/taiga/notifications/services.py b/python/apps/taiga/src/taiga/notifications/services.py index 2e1635964..c1d1919d3 100644 --- a/python/apps/taiga/src/taiga/notifications/services.py +++ b/python/apps/taiga/src/taiga/notifications/services.py @@ -6,6 +6,7 @@ # Copyright (c) 2023-present Kaleidos INC from collections.abc import Iterable +from datetime import datetime from uuid import UUID from taiga.base.serializers import BaseModel @@ -57,3 +58,7 @@ async def count_user_notifications(user: User) -> dict[str, int]: total = await notifications_repositories.count_notifications(filters={"owner": user}) read = await notifications_repositories.count_notifications(filters={"owner": user, "is_read": True}) return {"total": total, "read": read, "unread": total - read} + + +async def clean_read_notifications(before: datetime) -> int: + return await notifications_repositories.delete_notifications(filters={"is_read": True, "read_before": before}) diff --git a/python/apps/taiga/src/taiga/notifications/tasks.py b/python/apps/taiga/src/taiga/notifications/tasks.py new file mode 100644 index 000000000..772f8437c --- /dev/null +++ b/python/apps/taiga/src/taiga/notifications/tasks.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2023-present Kaleidos INC + +import logging +from datetime import timedelta + +from taiga.base.utils.datetime import aware_utcnow +from taiga.conf import settings +from taiga.notifications import services as notifications_services +from taiga.tasksqueue.manager import manager as tqmanager + +logger = logging.getLogger(__name__) + + +@tqmanager.periodic(cron=settings.NOTIFICATIONS.CLEAN_READ_NOTIFICATIONS_CRON) # type: ignore +@tqmanager.task +async def clean_read_notifications(timestamp: int) -> int: + total_deleted = await notifications_services.clean_read_notifications( + before=aware_utcnow() - timedelta(minutes=settings.NOTIFICATIONS.MINUTES_TO_STORE_READ_NOTIFICATIONS) + ) + + logger.info( + "deleted notifications: %s", + total_deleted, + extra={"deleted": total_deleted}, + ) + + return total_deleted