Skip to content

Commit

Permalink
feat(subscriber): #221 - mark-as, mark-all-as and mark-message-action
Browse files Browse the repository at this point in the history
Close #221
  • Loading branch information
ryshu authored and unicodeveloper committed Feb 26, 2024
1 parent 46cf4e4 commit eda325a
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 2 deletions.
79 changes: 78 additions & 1 deletion novu/api/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@

from novu.api.base import Api, PaginationIterator
from novu.constants import SUBSCRIBERS_ENDPOINT
from novu.dto.message import MessageDto
from novu.dto.subscriber import (
BulkResultSubscriberDto,
PaginatedSubscriberDto,
SubscriberDto,
SubscriberPreferenceDto,
)
from novu.enums import Channel, ProviderIdEnum
from novu.enums import Channel, MarkAsEnum, MessageActionStatus, ProviderIdEnum


class SubscriberApi(Api):
Expand Down Expand Up @@ -245,3 +246,79 @@ def unseen_notifications(self, subscriber_id: str) -> int:
"""
res = self.handle_request("GET", f"{self._subscriber_url}/{subscriber_id}/notifications/unseen")
return res.get("data", {}).get("count", 0)

def mark_as(
self, subscriber_id: str, message_id: str, seen: Optional[bool] = None, read: Optional[bool] = None
) -> MessageDto:
"""Mark a subscriber feed message as seen or read.
Args:
subscriber_id: The subscriber identifier
message_id: The message identifier
seen: If provided, set the 'seen' state of the message
read: If provided, set the 'read' state of the message
Returns:
Return the updated message
"""
mark: Dict[str, bool] = {}
if read is not None:
mark["read"] = read
if seen is not None:
mark["seen"] = seen
payload = {"messageId": message_id, "mark": mark}

return MessageDto.from_camel_case(
self.handle_request("POST", f"{self._subscriber_url}/{subscriber_id}/messages/markAs", json=payload)[
"data"
][0]
)

def mark_all_as(self, subscriber_id: str, mark_as: MarkAsEnum, feed_identifiers: Optional[List[str]] = None) -> int:
"""Mark all the subscriber messages as read, unread, seen or unseen.
Args:
subscriber_id: The subscriber identifier
mark_as: Mark all subscriber messages as read, unread, seen or unseen via this instruction
feed_identifiers: Optionally, you can pass a list of feed identifiers to mark messages of a particular feed.
Returns:
Number of updated messages.
"""
payload: Dict[str, Union[List[str], MarkAsEnum]] = {"markAs": mark_as}
if feed_identifiers:
payload["feedIdentifier"] = feed_identifiers

return self.handle_request(
"POST", f"{self._subscriber_url}/{subscriber_id}/messages/mark-all", json=payload
).get("data", 0)

def mark_message_action(
self,
subscriber_id: str,
message_id: str,
action_type: str,
status: MessageActionStatus,
payload: Optional[dict] = None,
) -> MessageDto:
"""Mark message action as seen.
Args:
subscriber_id: The subscriber identifier
message_id: The message identifier
action_type: The message action type
status: The message action status
payload: The message action payload
Returns:
Return the updated message
"""
body: Dict[str, Union[MessageActionStatus, dict]] = {"status": status}
if payload is not None:
body["payload"] = payload

return MessageDto.from_camel_case(
self.handle_request(
"POST", f"{self._subscriber_url}/{subscriber_id}/messages/{message_id}/actions/{action_type}", json=body
).get("data", {})
)
3 changes: 3 additions & 0 deletions novu/enums/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
FieldFilterPartTimeOperator,
)
from novu.enums.member import MemberRole, MemberStatus
from novu.enums.message import MarkAsEnum, MessageActionStatus
from novu.enums.notification import (
NotificationStepMetadataType,
NotificationStepMetadataUnit,
Expand Down Expand Up @@ -41,8 +42,10 @@
"FieldFilterPartOperator",
"FieldFilterPartTimeOperator",
"InAppProviderIdEnum",
"MarkAsEnum",
"MemberRole",
"MemberStatus",
"MessageActionStatus",
"NotificationStepMetadataType",
"NotificationStepMetadataUnit",
"OrganizationBrandingDirection",
Expand Down
29 changes: 29 additions & 0 deletions novu/enums/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""This module is used to gather enumerations related to the Message resource in Novu"""

from novu.enums.polyfill import StrEnum


class MarkAsEnum(StrEnum):
"""This enumeration define possible instruction for a mark-as instruction"""

READ = "read"
"""Mark message as read"""

SEEN = "seen"
"""Mark message as seen"""

UNREAD = "unread"
"""Mark message as unread"""

UNSEEN = "unseen"
"""Mark message as unseen"""


class MessageActionStatus(StrEnum):
"""This enumeration define possible message action status"""

PENDING = "pending"
"""Message action status is pending"""

DONE = "done"
"""Message action status is done"""
162 changes: 161 additions & 1 deletion tests/api/test_subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from novu.api import SubscriberApi
from novu.api.base import PaginationIterator
from novu.config import NovuConfig
from novu.dto.message import MessageDto
from novu.dto.subscriber import (
BulkResultSubscriberDto,
PaginatedSubscriberDto,
Expand All @@ -17,13 +18,68 @@
SubscriberPreferencePreferenceDto,
SubscriberPreferenceTemplateDto,
)
from novu.enums import Channel, ChatProviderIdEnum
from novu.enums import Channel, ChatProviderIdEnum, MarkAsEnum, MessageActionStatus
from tests.factories import MockResponse

__version__ = pkg_resources.get_distribution("novu").version


class SubscriberApiTests(TestCase):
message_json = {
"_id": "63daff4cc037e013fd82dadd",
"_templateId": "63daff36c037e013fd82da05",
"_environmentId": "63dafed97779f59258e38445",
"_messageTemplateId": "63daff36c037e013fd82d9f4",
"_notificationId": "63daff487779f59258e38b24",
"_organizationId": "63dafed97779f59258e3843f",
"_subscriberId": "63dafedbc037e013fd82d37a",
"_jobId": "63daff4c7779f59258e38b3c",
"templateIdentifier": "absences",
"cta": {"action": {"buttons": []}},
"_feedId": None,
"channel": "in_app",
"content": "test",
"deviceTokens": [],
"seen": True,
"read": True,
"status": "sent",
"transactionId": "aa287682-cb30-4a5f-a03a-f28f59c9d46d",
"deleted": False,
"createdAt": "2023-02-02T00:09:48.673Z",
"updatedAt": "2023-02-02T00:10:21.544Z",
"__v": 0,
"lastReadDate": "2023-02-02T00:10:21.544Z",
"lastSeenDate": "2023-02-02T00:10:21.544Z",
}
message_expectation = MessageDto(
identifier=None,
_id="63daff4cc037e013fd82dadd",
_template_id="63daff36c037e013fd82da05",
_environment_id="63dafed97779f59258e38445",
_message_template_id="63daff36c037e013fd82d9f4",
_organization_id="63dafed97779f59258e3843f",
_subscriber_id="63dafedbc037e013fd82d37a",
_job_id="63daff4c7779f59258e38b3c",
template_identifier="absences",
email=None,
subject=None,
cta={"action": {"buttons": []}},
channel="in_app",
content="test",
provider_id=None,
device_tokens=[],
seen=True,
read=True,
status="sent",
transaction_id="aa287682-cb30-4a5f-a03a-f28f59c9d46d",
payload=None,
created_at="2023-02-02T00:09:48.673Z",
updated_at="2023-02-02T00:10:21.544Z",
deleted=False,
last_read_date="2023-02-02T00:10:21.544Z",
last_seen_date="2023-02-02T00:10:21.544Z",
)

@classmethod
def setUpClass(cls) -> None:
NovuConfig.configure("sample.novu.com", "api-key")
Expand Down Expand Up @@ -689,3 +745,107 @@ def test_delete_credentials(self, mock_request: mock.MagicMock) -> None:
params=None,
timeout=5,
)

@mock.patch("requests.request")
def test_mark_as_read(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, data={"data": [self.message_json]})

result = self.api.mark_as("subscriber-id", "63daff4cc037e013fd82dadd", read=True)

self.assertEqual(result, self.message_expectation)
mock_request.assert_called_once_with(
method="POST",
url="sample.novu.com/v1/subscribers/subscriber-id/messages/markAs",
headers={"Authorization": "ApiKey api-key", "User-Agent": f"novu/python@{__version__}"},
json={"messageId": "63daff4cc037e013fd82dadd", "mark": {"read": True}},
params=None,
timeout=5,
)

@mock.patch("requests.request")
def test_mark_as_seen(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, data={"data": [self.message_json]})

result = self.api.mark_as("subscriber-id", "63daff4cc037e013fd82dadd", seen=True)

self.assertEqual(result, self.message_expectation)
mock_request.assert_called_once_with(
method="POST",
url="sample.novu.com/v1/subscribers/subscriber-id/messages/markAs",
headers={"Authorization": "ApiKey api-key", "User-Agent": f"novu/python@{__version__}"},
json={"messageId": "63daff4cc037e013fd82dadd", "mark": {"seen": True}},
params=None,
timeout=5,
)

@mock.patch("requests.request")
def test_mark_all_as(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, data={"data": 1})

result = self.api.mark_all_as("subscriber-id", MarkAsEnum.READ)

self.assertEqual(result, 1)
mock_request.assert_called_once_with(
method="POST",
url="sample.novu.com/v1/subscribers/subscriber-id/messages/mark-all",
headers={"Authorization": "ApiKey api-key", "User-Agent": f"novu/python@{__version__}"},
json={"markAs": "read"},
params=None,
timeout=5,
)

@mock.patch("requests.request")
def test_mark_all_as_with_identifier(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, data={"data": 1})

result = self.api.mark_all_as("subscriber-id", MarkAsEnum.SEEN, ["feed-id"])

self.assertEqual(result, 1)
mock_request.assert_called_once_with(
method="POST",
url="sample.novu.com/v1/subscribers/subscriber-id/messages/mark-all",
headers={"Authorization": "ApiKey api-key", "User-Agent": f"novu/python@{__version__}"},
json={"markAs": "seen", "feedIdentifier": ["feed-id"]},
params=None,
timeout=5,
)

@mock.patch("requests.request")
def test_mark_message_action(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, data={"data": self.message_json})

result = self.api.mark_message_action(
"subscriber-id", "63daff4cc037e013fd82dadd", "action-type", MessageActionStatus.DONE
)

self.assertEqual(result, self.message_expectation)
mock_request.assert_called_once_with(
method="POST",
url="sample.novu.com/v1/subscribers/subscriber-id/messages/63daff4cc037e013fd82dadd/actions/action-type",
headers={"Authorization": "ApiKey api-key", "User-Agent": f"novu/python@{__version__}"},
json={"status": "done"},
params=None,
timeout=5,
)

@mock.patch("requests.request")
def test_mark_message_action_with_payload(self, mock_request: mock.MagicMock) -> None:
mock_request.return_value = MockResponse(200, data={"data": self.message_json})

result = self.api.mark_message_action(
"subscriber-id",
"63daff4cc037e013fd82dadd",
"action-type",
MessageActionStatus.PENDING,
{"body": "something"},
)

self.assertEqual(result, self.message_expectation)
mock_request.assert_called_once_with(
method="POST",
url="sample.novu.com/v1/subscribers/subscriber-id/messages/63daff4cc037e013fd82dadd/actions/action-type",
headers={"Authorization": "ApiKey api-key", "User-Agent": f"novu/python@{__version__}"},
json={"status": "pending", "payload": {"body": "something"}},
params=None,
timeout=5,
)

0 comments on commit eda325a

Please sign in to comment.