Skip to content

Commit

Permalink
feat(notifications): u#2899 notifications scaffolding + notify when a…
Browse files Browse the repository at this point in the history
… story is [un]assigned
  • Loading branch information
bameda committed Oct 16, 2023
1 parent 0b78fbb commit ff3a02c
Show file tree
Hide file tree
Showing 28 changed files with 599 additions and 47 deletions.
Binary file modified .github/sql-fixtures/fixtures.sql
Binary file not shown.
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
2 changes: 1 addition & 1 deletion python/apps/taiga/src/taiga/base/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from humps.main import camelize
from pydantic import BaseModel as _BaseModel
from taiga.base.serializers.fields import UUIDB64 # noqa
from taiga.base.serializers.fields import UUIDB64, CamelizeDict, FileField # noqa


class BaseModel(_BaseModel):
Expand Down
30 changes: 28 additions & 2 deletions python/apps/taiga/src/taiga/base/serializers/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
#
# Copyright (c) 2023-present Kaleidos INC

from typing import Any, Callable, Generator
from typing import Any, Callable, Generator, TypeVar
from uuid import UUID

from humps.main import camelize
from pydantic import AnyHttpUrl, AnyUrl, BaseConfig
from pydantic.fields import ModelField
from taiga.base.utils.uuid import encode_uuid_to_b64str
Expand All @@ -18,7 +19,9 @@
class UUIDB64(UUID):
@classmethod
def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
field_schema["example"] = "6JgsbGyoEe2VExhWgGrI2w"
field_schema.update(
example="6JgsbGyoEe2VExhWgGrI2w",
)

@classmethod
def __get_validators__(cls) -> CallableGenerator:
Expand All @@ -33,3 +36,26 @@ class FileField(AnyHttpUrl):
@classmethod
def validate(cls, value: Any, field: ModelField, config: BaseConfig) -> AnyUrl:
return value.url


_Key = TypeVar("_Key")
_Val = TypeVar("_Val")


class CamelizeDict(str):
def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
field_schema.update(
type="object",
example={},
)

@classmethod
def __get_validators__(cls) -> CallableGenerator:
yield cls.validate

@classmethod
def validate(cls, value: dict[_Key, _Val]) -> dict[_Key, _Val]:
return camelize(value)

def __repr__(self) -> str:
return "CamelizeDict"
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: 4 additions & 2 deletions python/apps/taiga/src/taiga/base/validators/fields/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
class LanguageCode(str):
@classmethod
def __modify_schema__(cls: Type["LanguageCode"], field_schema: dict[str, Any]) -> None:
field_schema["example"] = settings.LANG
field_schema["enum"] = i18n.available_languages
field_schema.update(
example=settings.LANG,
enum=i18n.available_languages,
)

@classmethod
def __get_validators__(cls: Type["LanguageCode"]) -> CallableGenerator:
Expand Down
6 changes: 4 additions & 2 deletions python/apps/taiga/src/taiga/base/validators/fields/uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
class B64UUID(UUID):
@classmethod
def __modify_schema__(cls: Type["B64UUID"], field_schema: dict[str, Any]) -> None:
field_schema["example"] = "6JgsbGyoEe2VExhWgGrI2w"
field_schema["format"] = None
field_schema.update(
example="6JgsbGyoEe2VExhWgGrI2w",
format=None,
)

@classmethod
def __get_validators__(cls: Type["B64UUID"]) -> CallableGenerator:
Expand Down
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# -*- 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

# Generated by Django 4.2.3 on 2023-10-04 18:15

import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
import taiga.base.db.models
import taiga.base.utils.datetime
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Notification",
fields=[
(
"id",
models.UUIDField(
blank=True,
default=taiga.base.db.models.uuid_generator,
editable=False,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_at",
models.DateTimeField(
default=taiga.base.utils.datetime.aware_utcnow,
verbose_name="created at",
),
),
("type", models.CharField(max_length=500, verbose_name="type")),
(
"read_at",
models.DateTimeField(blank=True, null=True, verbose_name="read at"),
),
(
"content",
django.contrib.postgres.fields.jsonb.JSONField(default=dict, verbose_name="content"),
),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
verbose_name="created by",
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to=settings.AUTH_USER_MODEL,
verbose_name="owner",
),
),
],
options={
"verbose_name": "notification",
"verbose_name_plural": "notifications",
"ordering": ["-created_at"],
"indexes": [
models.Index(fields=["owner"], name="notificatio_owner_i_2bc47d_idx"),
models.Index(
fields=["owner", "read_at"],
name="notificatio_owner_i_37308f_idx",
),
],
},
),
]
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
Loading

0 comments on commit ff3a02c

Please sign in to comment.