Skip to content

Commit

Permalink
feat(notifications): u#2899 mark notification as read
Browse files Browse the repository at this point in the history
  • Loading branch information
bameda committed Oct 18, 2023
1 parent ccd8b13 commit 7cce3e2
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 2 deletions.
42 changes: 41 additions & 1 deletion python/apps/taiga/src/taiga/notifications/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@
#
# Copyright (c) 2023-present Kaleidos INC

from uuid import UUID

from taiga.base.api import AuthRequest
from taiga.base.api.permissions import check_permissions
from taiga.exceptions.api.errors import ERROR_403
from taiga.base.validators import B64UUID
from taiga.exceptions import api as ex
from taiga.exceptions.api.errors import ERROR_403, ERROR_404, ERROR_422
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
from taiga.users.models import User

LIST_MY_NOTIFICATIONS = IsAuthenticated()
COUNT_MY_NOTIFICATIONS = IsAuthenticated()
MARK_MY_NOTIFICATIONS_AS_READ = IsAuthenticated()

##########################################################
# list notifications
Expand Down Expand Up @@ -55,3 +61,37 @@ async def count_my_notifications(request: AuthRequest) -> dict[str, int]:
"""
await check_permissions(permissions=COUNT_MY_NOTIFICATIONS, user=request.user, obj=None)
return await notifications_services.count_user_notifications(user=request.user)


##########################################################
# mark notification as read
##########################################################


@routes.notifications.post(
"/my/notifications/{id}/read",
name="my.notifications.read",
summary="Mark notification as read",
responses=ERROR_403 | ERROR_404 | ERROR_422,
response_model=NotificationSerializer,
)
async def mark_my_notification_as_read(id: B64UUID, request: AuthRequest) -> Notification:
"""
Mark a notification as read.
"""
await check_permissions(permissions=MARK_MY_NOTIFICATIONS_AS_READ, user=request.user, obj=None)
await get_notification_or_404(user=request.user, id=id)
return (await notifications_services.mark_user_notifications_as_read(user=request.user, id=id))[0]


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


async def get_notification_or_404(user: User, id: UUID) -> Notification:
notification = await notifications_services.get_user_notification(user=user, id=id)
if notification is None:
raise ex.NotFoundError("Notification does not exist")

return notification
32 changes: 32 additions & 0 deletions python/apps/taiga/src/taiga/notifications/repositories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

from collections.abc import Iterable
from typing import Any, TypedDict
from uuid import UUID

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

Expand All @@ -20,6 +22,7 @@


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

Expand Down Expand Up @@ -79,6 +82,35 @@ async def list_notifications(
return [a async for a in qs[offset:limit]]


##########################################################
# get notifications
##########################################################


async def get_notification(
filters: NotificationFilters = {},
) -> Notification | None:
qs = await _apply_filters_to_queryset(qs=DEFAULT_QUERYSET, filters=filters)

try:
return await qs.aget()
except Notification.DoesNotExist:
return None


##########################################################
# mark notificatiosn as read
##########################################################


async def mark_notifications_as_read(
filters: NotificationFilters = {},
) -> list[Notification]:
qs = await _apply_filters_to_queryset(qs=DEFAULT_QUERYSET, filters=filters)
await qs.aupdate(read_at=aware_utcnow())
return [a async for a in qs.all()]


##########################################################
# misc
##########################################################
Expand Down
15 changes: 15 additions & 0 deletions python/apps/taiga/src/taiga/notifications/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) 2023-present Kaleidos INC

from collections.abc import Iterable
from uuid import UUID

from taiga.base.serializers import BaseModel
from taiga.notifications import events as notifications_events
Expand Down Expand Up @@ -33,6 +35,19 @@ async def list_user_notifications(user: User, is_read: bool | None = None) -> li
return await notifications_repositories.list_notifications(filters=filters)


async def get_user_notification(user: User, id: UUID) -> Notification | None:
return await notifications_repositories.get_notification(filters={"owner": user, "id": id})


async def mark_user_notifications_as_read(user: User, id: UUID | None = None) -> list[Notification]:
filters: NotificationFilters = {"owner": user}

if id is not None:
filters["id"] = id

return await notifications_repositories.mark_notifications_as_read(filters=filters)


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})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from fastapi import status
from taiga.base.utils.datetime import aware_utcnow
from tests.utils import factories as f
from tests.utils.bad_params import INVALID_B64ID

pytestmark = pytest.mark.django_db(transaction=True)

Expand Down Expand Up @@ -61,6 +62,46 @@ async def test_list_my_notifications_403_forbidden_error(client):
assert response.status_code == status.HTTP_403_FORBIDDEN, response.text


##########################################################
# POST my/notifications/{id}/read
##########################################################


async def test_mark_notification_as_read_200_ok(client):
user = await f.create_user()
notification = await f.create_notification(owner=user)

client.login(user)
response = client.post(f"/my/notifications/{notification.b64id}/read")
assert response.status_code == status.HTTP_200_OK, response.text
assert response.json()["readAt"] is not None, response.json()


async def test_mark_my_notification_as_read_404_not_found(client):
user = await f.create_user()
notification = await f.create_notification()

client.login(user)
response = client.post(f"/my/notifications/{notification.b64id}/read")
assert response.status_code == status.HTTP_404_NOT_FOUND, response.text


async def test_mark_my_notification_as_read_403_forbidden_error(client):
user = await f.create_user()
notification = await f.create_notification(owner=user)

response = client.post(f"/my/notifications/{notification.b64id}/read")
assert response.status_code == status.HTTP_403_FORBIDDEN, response.text


async def test_mark_my_notification_as_read_422_unprocessable_entity(client):
user = await f.create_user()

client.login(user)
response = client.post(f"/my/notifications/{INVALID_B64ID}/read")
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.text


##########################################################
# GET my/notifications/count
##########################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,39 @@ async def test_list_notifications_filters():
assert [n22, n12] == await repositories.list_notifications(filters={"is_read": True})


##########################################################
# get_notification
##########################################################


async def test_get_notification():
user1 = await f.create_user()
user2 = await f.create_user()
notification = await f.create_notification(owner=user1)

assert await repositories.get_notification(filters={"id": notification.id}) == notification
assert await repositories.get_notification(filters={"id": notification.id, "owner": user1}) == notification
assert await repositories.get_notification(filters={"id": notification.id, "owner": user2}) is None


##########################################################
# mark notifications as read
##########################################################


async def test_mark_notifications_as_read():
user = await f.create_user()
n1 = await f.create_notification(owner=user)
n2 = await f.create_notification(owner=user)
n3 = await f.create_notification(owner=user)

assert n1.read_at == n2.read_at == n3.read_at is None

ns = await repositories.mark_notifications_as_read(filters={"owner": user})

assert ns[0].read_at == ns[1].read_at == ns[2].read_at is not None


##########################################################
# misc
##########################################################
Expand Down
3 changes: 2 additions & 1 deletion python/apps/taiga/tests/utils/factories/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@

from asgiref.sync import sync_to_async

from .base import Factory
from .base import Factory, factory


class NotificationFactory(Factory):
type = "test_notification"
owner = factory.SubFactory("tests.utils.factories.UserFactory")

class Meta:
model = "notifications.Notification"
Expand Down

0 comments on commit 7cce3e2

Please sign in to comment.