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 20, 2023
1 parent c2dd562 commit f6cd0fc
Show file tree
Hide file tree
Showing 39 changed files with 2,015 additions and 69 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
32 changes: 29 additions & 3 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 @@ -17,8 +18,10 @@

class UUIDB64(UUID):
@classmethod
def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
field_schema["example"] = "6JgsbGyoEe2VExhWgGrI2w"
def __modify_schema__(cls, field_schema: dict[str, Any] = {}) -> None:
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, 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=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]:
"""
Get user notifications counters
"""
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

1 comment on commit f6cd0fc

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/taiga
   __main__.py71710%17–155
src/taiga/attachments
   admin.py27390%25, 28, 70
   events.py8275%15, 20
   models.py19289%61, 64
   repositories.py49197%62
src/taiga/auth/services
   __init__.py47098%90–>93
src/taiga/base
   mocks.py741280%93–94, 107–118, 143–>exit
src/taiga/base/api
   permissions.py49199%22
   requests.py11380%15, 21–23
src/taiga/base/db
   __init__.py4175%15
   commands.py52520%8–269
   sequences.py45297%91–92
src/taiga/base/db/admin
   forms.py14838%17–25
   utils.py11736%17–23
src/taiga/base/db/models
   fields.py15188%20
src/taiga/base/django
   __init__.py5260%17–19
   settings.py30293%16–17
   urls.py330%8–11
src/taiga/base/i18n
   __init__.py102988%171–>170, 173–>170, 192–193, 203–205, 215, 268, 279, 283
   choices.py34291%80, 87
   commands.py74740%8–108
src/taiga/base/logging
   formatters.py241050%22–>exit, 38, 44–45, 51–60
   middlewares.py23196%20
src/taiga/base/repositories
   neighbors.py39391%43–44, 65
src/taiga/base/sampledata
   commands.py11110%8–40
   constants.py15150%14–51
   demo_data.py2732730%8–529
   factories.py1221220%9–353
   test_data.py2902900%8–634
src/taiga/base/serializers
   fields.py34393%22, 47, 61
src/taiga/base/utils
   commands.py11110%8–30
   datetime.py441860%54, 67, 126–147
   enum.py14192%43
   images.py26387%21–22, 30
   json.py331545%30–35, 37, 39–44, 46, 50
   pprint.py660%8–17
   slug.py23485%22–25
src/taiga/base/validators/fields
   i18n.py15195%19
   uuid.py14195%19
src/taiga/comments
   admin.py29392%16, 20, 24
   events.py11373%15, 20, 25
   models.py20291%43, 46
src/taiga/commons/storage
   commands.py13130%8–40
   models.py16288%38, 41
   services.py9092%17–>16
   tasks.py990%8–19
src/taiga/conf
   __init__.py94395%112, 128–129
   tokens.py17190%34
src/taiga/emails
   commands.py35350%9–68
   sender.py18571%48–50, 54–73
   tasks.py39490%44–45, 53–54
src/taiga/events
   app.py18192%21
   events.py16288%31–32
   logging.py392331%18–>exit, 32–48, 51–60
   manager.py1171190%78–79, 223–233, 239–242
   responses.py33684%33, 37–41
   subscriber.py80495%51, 55, 88, 91, 108–>120
src/taiga/events/actions
   auth.py231144%27–32, 39–45
   event_handlers.py9456%18–20, 28–30
   projects.py553038%36–40, 48–64, 72–80, 88–96
   workspaces.py553038%36–40, 48–64, 72–80, 88–96
src/taiga/events/pubsub/backends
   base.py30198%53
   redis.py66594%41–43, 85–86
src/taiga/exceptions/api
   __init__.py27097%28–>30
   handlers.py27290%27–28
   middlewares.py26287%37, 41
src/taiga/integrations/auth
   services.py39782%50–56
src/taiga/integrations/github
   services.py40098%63–>68
src/taiga/mediafiles
   admin.py20291%24, 27
   models.py26292%84, 87
src/taiga/notifications
   admin.py11285%20, 23
   repositories.py26194%77
src/taiga/permissions
   services.py103198%28, 106–>109
src/taiga/permissions/validators
   fields.py24388%18–19, 45
src/taiga/projects/invitations
   models.py25292%89, 92
   repositories.py84198%135
src/taiga/projects/invitations/api
   validators.py65789%38–40, 65–66, 84–85
src/taiga/projects/invitations/events
   __init__.py33191%37–>exit, 49–>exit, 74–>exit, 102
src/taiga/projects/invitations/services
   __init__.py183199%58–>53, 286
src/taiga/projects/memberships
   models.py16288%51, 54
   repositories.py86297%91, 124
src/taiga/projects/memberships/events
   __init__.py16288%18–24
src/taiga/projects/memberships/services
   __init__.py45195%64–>69, 111
src/taiga/projects/projects
   admin.py531275%33–34, 39–42, 50–51, 56–59, 90, 94
   models.py61886%64, 67, 78, 113, 116, 119–121
   repositories.py115496%125, 126–>129, 155, 254–255
   tasks.py6188%17
src/taiga/projects/projects/services
   __init__.py107592%78, 87, 199, 205–>208, 213, 235
src/taiga/projects/references
   mixins.py13093%25–>28
src/taiga/projects/roles
   api.py35393%46–48
   models.py26386%50, 53, 57
   repositories.py44196%92
src/taiga/projects/roles/services
   __init__.py25194%24, 57–>62
src/taiga/stories/assignments
   models.py15287%42, 45
   repositories.py52197%49
src/taiga/stories/assignments/notifications
   __init__.py18091%25–>27, 44–>46
src/taiga/stories/attachments
   api.py51197%159
   serializers.py11093%20–>28
src/taiga/stories/comments
   api.py58197%177
src/taiga/stories/stories
   admin.py25584%76–81, 85, 89
   models.py25292%73, 76
src/taiga/stories/stories/services
   __init__.py131297%210–>226, 270, 293
src/taiga/tasksqueue
   app.py20286%29–30
   commands.py65650%8–122
   logging.py331740%18–>exit, 29–39, 42–51
   manager.py561377%37, 41, 45, 49, 94, 163, 177, 186, 191, 198, 202, 209, 213
   task.py12192%23
src/taiga/tokens
   admin.py441081%67, 70, 73, 76, 104, 108, 112, 116, 120, 124
   models.py30487%59, 62, 78, 81
src/taiga/users
   admin.py51781%32, 41, 50, 87–95
   commands.py990%8–26
   models.py651086%71, 74, 90, 93, 97, 100, 103, 106, 127, 130
   repositories.py204398%209–>212, 335, 428–429
src/taiga/users/api
   __init__.py66296%216, 304
src/taiga/users/api/validators
   __init__.py42392%39–41
src/taiga/users/services
   __init__.py244795%75, 259, 262–>257, 297–>280, 306–>280, 322–>318, 345–>341, 366, 494–>499, 513–520, 521–>526, 524–525
src/taiga/workflows
   models.py40490%42, 45, 85, 88
src/taiga/workflows/services
   __init__.py113494%53, 232, 259, 330–>338, 345
src/taiga/workspaces/invitations
   models.py24292%84, 87
   permissions.py9182%19
   repositories.py77198%123
src/taiga/workspaces/invitations/api
   validators.py18288%24–25
src/taiga/workspaces/invitations/events
   __init__.py25191%36–>exit, 60–>exit, 71
src/taiga/workspaces/invitations/services
   __init__.py123199%222
src/taiga/workspaces/memberships
   api.py43196%126
   models.py15287%43, 46
   repositories.py63295%72, 105
src/taiga/workspaces/memberships/services
   __init__.py46197%117
src/taiga/workspaces/workspaces
   admin.py571962%25–26, 31–34, 49, 52–57, 61, 65, 73–74, 79–82
   models.py18290%32, 35
   repositories.py127299%232–233
src/taiga/workspaces/workspaces/services
   __init__.py54294%113, 116
TOTAL10075154783% 

Tests Skipped Failures Errors Time
1161 1 💤 0 ❌ 0 🔥 23m 25s ⏱️

Please sign in to comment.