Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
bameda committed Oct 4, 2023
1 parent 92b909c commit 2e34434
Show file tree
Hide file tree
Showing 21 changed files with 466 additions and 39 deletions.
2 changes: 1 addition & 1 deletion python/apps/taiga/src/taiga/attachments/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async def _apply_prefetch_related_to_queryset(


##########################################################
# create comment
# create attachment
##########################################################


Expand Down
2 changes: 0 additions & 2 deletions python/apps/taiga/src/taiga/base/db/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from django.conf import settings
from taiga.base.django.commands import call_django_command
from taiga.base.utils import pprint
from taiga.base.utils.commands import unwrap_typer_param

cli = typer.Typer(
name="The Taiga DB Manager",
Expand Down Expand Up @@ -75,7 +74,6 @@ def init_migrations(


@cli.command(help="Creates new migration(s) for apps.")
@unwrap_typer_param
def make_migrations(
name: str = typer.Option("", "--name", "-n", help="Use this name for migration file(s)."),
dry_run: bool = typer.Option(
Expand Down
1 change: 1 addition & 0 deletions python/apps/taiga/src/taiga/base/django/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"taiga.commons.storage",
"taiga.emails",
"taiga.mediafiles",
"taiga.notifications",
"taiga.projects.invitations",
"taiga.projects.memberships",
"taiga.projects.projects",
Expand Down
32 changes: 1 addition & 31 deletions python/apps/taiga/src/taiga/base/utils/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@
#
# Copyright (c) 2023-present Kaleidos INC

import functools
import os
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Callable, Generator

import typer
from typing import Generator


@contextmanager
Expand All @@ -31,30 +28,3 @@ def set_working_directory(path: Path) -> Generator[None, None, None]:
yield
finally:
os.chdir(origin)


def unwrap_typer_param(f: Callable[..., Any]) -> Callable[..., Any]:
"""
Unwraps the default values from typer.Argument or typer.Option to allow function to be called normally.
See: https://github.com/tiangolo/typer/issues/279 and https://gitlab.com/boratko/typer-utils
"""
if f.__defaults__ is None:
return f
else:
patched_defaults = []
actual_default_observed = False
for i, value in enumerate(f.__defaults__):
default_value = value.default if isinstance(value, typer.models.ParameterInfo) else value
if default_value != ...:
actual_default_observed = True
patched_defaults.append(default_value)
elif actual_default_observed:
raise SyntaxError("non-default argument follows default argument")
f.__defaults__ = tuple(patched_defaults)

@functools.wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> Callable[..., Any]:
f.__defaults__ = tuple(patched_defaults)
return f(*args, **kwargs)

return wrapper
6 changes: 6 additions & 0 deletions python/apps/taiga/src/taiga/notifications/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# -*- 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
23 changes: 23 additions & 0 deletions python/apps/taiga/src/taiga/notifications/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# -*- 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 typing import Any

from taiga.base.db import admin
from taiga.base.db.admin.http import HttpRequest
from taiga.notifications.models import Notification


@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin[Notification]):
list_display = ("id", "type", "owner", "created_at", "read_at")

def has_change_permission(self, request: HttpRequest, obj: Any = None) -> bool:
return False

def has_add_permission(self, request: HttpRequest, obj: Any = None) -> bool:
return False
57 changes: 57 additions & 0 deletions python/apps/taiga/src/taiga/notifications/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# -*- 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 taiga.base.api import AuthRequest
from taiga.base.api.permissions import check_permissions
from taiga.exceptions.api.errors import ERROR_403
from taiga.notifications import services as notifications_services
from taiga.notifications.models import Notification
from taiga.notifications.serializers import NotificationCountersSerializer, NotificationSerializer
from taiga.permissions import IsAuthenticated
from taiga.routers import routes

LIST_MY_NOTIFICATIONS = IsAuthenticated()
COUNT_MY_NOTIFICATIONS = IsAuthenticated()

##########################################################
# list notifications
##########################################################


@routes.notifications.get(
"/my/notifications",
name="my.notifications.list",
summary="List all the user notifications",
responses=ERROR_403,
response_model=list[NotificationSerializer],
)
async def list_my_notifications(request: AuthRequest, is_read: bool | None = None) -> list[Notification]:
"""
List the notifications of the logged user.
"""
await check_permissions(permissions=LIST_MY_NOTIFICATIONS, user=request.user, obj=None)
return await notifications_services.list_user_notifications(user=request.user, is_read=is_read)


##########################################################
# count notifications
##########################################################


@routes.notifications.get(
"/my/notifications/count",
name="my.notifications.count",
summary="Get user notifications counters",
responses=ERROR_403,
response_model=NotificationCountersSerializer,
)
async def count_my_notifications(request: AuthRequest) -> dict[str, int]:
"""
List the notifications of the logged user.
"""
await check_permissions(permissions=COUNT_MY_NOTIFICATIONS, user=request.user, obj=None)
return await notifications_services.count_user_notifications(user=request.user)
25 changes: 25 additions & 0 deletions python/apps/taiga/src/taiga/notifications/events/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -*- 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 taiga.events import events_manager
from taiga.notifications.events.content import CreateNotificationContent
from taiga.notifications.models import Notification

CREATE_NOTIFICATION = "notifications.create"


async def emit_event_when_notifications_are_created(
notifications: list[Notification],
) -> None:
for notification in notifications:
await events_manager.publish_on_user_channel(
user=notification.owner,
type=CREATE_NOTIFICATION,
content=CreateNotificationContent(
notification=notification,
),
)
13 changes: 13 additions & 0 deletions python/apps/taiga/src/taiga/notifications/events/content.py
Original file line number Diff line number Diff line change
@@ -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 taiga.base.serializers import BaseModel
from taiga.notifications.serializers import NotificationSerializer


class CreateNotificationContent(BaseModel):
notification: NotificationSerializer
54 changes: 54 additions & 0 deletions python/apps/taiga/src/taiga/notifications/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- 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 taiga.base.db import models
from taiga.base.db.mixins import CreatedMetaInfoMixin

#######################################################################
# Base Notification
######################################################################


class Notification(models.BaseModel, CreatedMetaInfoMixin):
type = models.CharField(
max_length=500,
null=False,
blank=False,
verbose_name="type",
)
owner = models.ForeignKey(
"users.User",
null=False,
blank=False,
on_delete=models.CASCADE,
related_name="notifications",
verbose_name="owner",
)
read_at = models.DateTimeField(
null=True,
blank=True,
verbose_name="read at",
)
content = models.JSONField(
null=False,
blank=False,
default=dict,
verbose_name="content",
)

class Meta:
verbose_name = "notification"
verbose_name_plural = "notifications"
ordering = ["-created_at"]
indexes = [
models.Index(
fields=[
"owner",
]
),
models.Index(fields=["owner", "read_at"]),
]
92 changes: 92 additions & 0 deletions python/apps/taiga/src/taiga/notifications/repositories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# -*- 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 collections.abc import Iterable
from typing import Any, TypedDict

from taiga.base.db.models import QuerySet
from taiga.notifications.models import Notification
from taiga.users.models import User

##########################################################
# filters and querysets
##########################################################

DEFAULT_QUERYSET = Notification.objects.select_related("created_by").all()


class NotificationFilters(TypedDict, total=False):
owner: User
is_read: bool


async def _apply_filters_to_queryset(
qs: QuerySet[Notification],
filters: NotificationFilters = {},
) -> QuerySet[Notification]:
filter_data = dict(filters.copy())

if "is_read" in filter_data:
is_read = filter_data.pop("is_read")
filter_data["read_at__isnull"] = not is_read

return qs.filter(**filter_data)


##########################################################
# create notifications
##########################################################


async def create_notifications(
owners: Iterable[User],
created_by: User,
notification_type: str,
content: dict[str, Any],
) -> list[Notification]:
notifications = [
Notification(
owner=owner,
created_by=created_by,
type=notification_type,
content=content,
)
for owner in owners
]

return await Notification.objects.abulk_create(notifications)


##########################################################
# list notifications
##########################################################


async def list_notifications(
filters: NotificationFilters = {},
offset: int | None = None,
limit: int | None = None,
) -> list[Notification]:
qs = await _apply_filters_to_queryset(qs=DEFAULT_QUERYSET, filters=filters)

if limit is not None and offset is not None:
limit += offset

return [a async for a in qs[offset:limit]]


##########################################################
# misc
##########################################################


async def count_notifications(
filters: NotificationFilters = {},
) -> int:
qs = await _apply_filters_to_queryset(qs=DEFAULT_QUERYSET, filters=filters)

return await qs.acount()
28 changes: 28 additions & 0 deletions python/apps/taiga/src/taiga/notifications/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# -*- 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 datetime
from typing import Any

from taiga.base.serializers import BaseModel
from taiga.users.serializers.nested import UserNestedSerializer


class NotificationSerializer(BaseModel):
created_by: UserNestedSerializer | None
created_at: datetime
read_at: datetime | None
content: dict[str, Any]

class Config:
orm_mode = True


class NotificationCountersSerializer(BaseModel):
read: int
unread: int
total: int
Loading

0 comments on commit 2e34434

Please sign in to comment.