diff --git a/pyopenproject/api_connection/request.py b/pyopenproject/api_connection/request.py index 613a75d7..45630a42 100644 --- a/pyopenproject/api_connection/request.py +++ b/pyopenproject/api_connection/request.py @@ -40,6 +40,9 @@ def execute(self): return response.content elif 'text' in response.headers['Content-Type']: return response.content.decode("utf-8") + # for responses only received with control code + elif response.status_code == 204 and not response.content: + return response except requests.exceptions.Timeout as err: # Maybe set up for a retry, or continue in a retry loop raise RequestError(f"Timeout running request with the URL (Timeout):" diff --git a/pyopenproject/business/notification_service.py b/pyopenproject/business/notification_service.py new file mode 100644 index 00000000..023d7261 --- /dev/null +++ b/pyopenproject/business/notification_service.py @@ -0,0 +1,40 @@ +from abc import ABCMeta, abstractmethod + +from pyopenproject.business.abstract_service import AbstractService + + +class NotificationService(AbstractService): + """ + Class Notification service + implements all notification services + """ + __metaclass__ = ABCMeta + + def __init__(self, connection): + super().__init__(connection) + + @abstractmethod + def find(self, notification_id): raise NotImplementedError + + @abstractmethod + def find_all(self, offset=None, page_size=None, filters=None, + sort_by=None, group_by=None): raise NotImplementedError + + @abstractmethod + def read_all(filter=None): raise NotImplementedError + + @abstractmethod + def unread_all(filter=None): raise NotImplementedError + + @abstractmethod + def read_notification(notification_id=None): raise NotImplementedError + + @abstractmethod + def unread_notification(notification_id=None): raise NotImplementedError + + @abstractmethod + def unread_notification(notification_id=None): raise NotImplementedError + + @abstractmethod + def find_notification_detail( + notification_id=None, detail_id=None): raise NotImplementedError diff --git a/pyopenproject/business/services/command/notification/__init__.py b/pyopenproject/business/services/command/notification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyopenproject/business/services/command/notification/detail.py b/pyopenproject/business/services/command/notification/detail.py new file mode 100644 index 00000000..8d79b37a --- /dev/null +++ b/pyopenproject/business/services/command/notification/detail.py @@ -0,0 +1,24 @@ +from pyopenproject.api_connection.exceptions.request_exception import RequestError +from pyopenproject.api_connection.requests.get_request import GetRequest +from pyopenproject.business.exception.business_error import BusinessError +from pyopenproject.business.services.command.find_list_command import FindListCommand +from pyopenproject.business.services.command.notification.notification_command import NotificationCommand +from pyopenproject.model.notification import Notification + + +class Detail(NotificationCommand): + + def __init__(self, connection, notification_id, detail_id): + super().__init__(connection) + self.notification_id = notification_id + self.detail_id = detail_id + + def execute(self): + try: + json_obj = GetRequest( + self.connection, f"{self.CONTEXT}/{self.notification_id}/details/{self.detail_id}",).execute() + return Notification(json_obj) + + except RequestError as re: + raise BusinessError( + f"Error finding detail notifications:{re.args}") diff --git a/pyopenproject/business/services/command/notification/find.py b/pyopenproject/business/services/command/notification/find.py new file mode 100644 index 00000000..a015d612 --- /dev/null +++ b/pyopenproject/business/services/command/notification/find.py @@ -0,0 +1,23 @@ +from pyopenproject.api_connection.exceptions.request_exception import RequestError +from pyopenproject.api_connection.requests.get_request import GetRequest +from pyopenproject.business.exception.business_error import BusinessError +from pyopenproject.business.services.command.find_list_command import FindListCommand +from pyopenproject.business.services.command.notification.notification_command import NotificationCommand +from pyopenproject.model.notification import Notification + + +class Find(NotificationCommand): + + def __init__(self, connection, notification_id): + super().__init__(connection) + self.notification_id = notification_id + + def execute(self): + try: + json_obj = GetRequest( + self.connection, f"{self.CONTEXT}/{self.notification_id}",).execute() + return Notification(json_obj) + + except RequestError as re: + raise BusinessError( + f"Error finding notifications:{re.args}") diff --git a/pyopenproject/business/services/command/notification/find_all.py b/pyopenproject/business/services/command/notification/find_all.py new file mode 100644 index 00000000..14c4eae8 --- /dev/null +++ b/pyopenproject/business/services/command/notification/find_all.py @@ -0,0 +1,44 @@ +from pyopenproject.api_connection.exceptions.request_exception import RequestError +from pyopenproject.api_connection.requests.get_request import GetRequest +from pyopenproject.business.exception.business_error import BusinessError +from pyopenproject.business.services.command.find_list_command import FindListCommand +from pyopenproject.business.services.command.notification.notification_command import NotificationCommand +from pyopenproject.business.util.filters import Filters +from pyopenproject.business.util.url import URL +from pyopenproject.business.util.url_parameter import URLParameter +from pyopenproject.model.notification import Notification + + +class FindAll(NotificationCommand): + + def __init__(self, connection, offset, page_size, filters, + sort_by, group_by): + super().__init__(connection) + self.offset = offset + self.page_size = page_size + self.filters = filters + self.group_by = group_by + self.sort_by = sort_by + + def execute(self): + try: + request = GetRequest(self.connection, + str(URL(f"{self.CONTEXT}", + [ + Filters( + self.filters), + URLParameter( + "groupBy", self.group_by), + URLParameter( + "sortBy", self.sort_by), + URLParameter( + "offset", self.offset), + URLParameter( + "pageSize", self.page_size), + ]) + )) + return FindListCommand(self.connection, request, Notification).execute() + + except RequestError as re: + raise BusinessError( + f"Error finding notifications:{re.args}") diff --git a/pyopenproject/business/services/command/notification/notification_command.py b/pyopenproject/business/services/command/notification/notification_command.py new file mode 100644 index 00000000..97c18e8c --- /dev/null +++ b/pyopenproject/business/services/command/notification/notification_command.py @@ -0,0 +1,15 @@ +from abc import abstractmethod, ABCMeta + +from pyopenproject.business.services.command.command import Command + + +class NotificationCommand(Command): + __metaclass__ = ABCMeta + + CONTEXT = "/api/v3/notifications" + + def __init__(self, connection): + self.connection = connection + + @abstractmethod + def execute(self): raise NotImplementedError diff --git a/pyopenproject/business/services/command/notification/read.py b/pyopenproject/business/services/command/notification/read.py new file mode 100644 index 00000000..4e113f7d --- /dev/null +++ b/pyopenproject/business/services/command/notification/read.py @@ -0,0 +1,24 @@ +from pyopenproject.api_connection.exceptions.request_exception import RequestError +from pyopenproject.api_connection.requests.post_request import PostRequest +from pyopenproject.business.exception.business_error import BusinessError +from pyopenproject.business.services.command.notification.notification_command import NotificationCommand +from pyopenproject.model.notification import Notification +import json + + +class Read(NotificationCommand): + + def __init__(self, connection, notification_id): + super().__init__(connection) + self.notification_id = notification_id + + def execute(self): + try: + response = PostRequest(connection=self.connection, + headers={ + "Content-Type": "application/json"}, + context=f"{self.CONTEXT}/{self.notification_id}/read_ian").execute() + return response + except RequestError as re: + raise BusinessError( + f"Error reading notification: {re.args}") diff --git a/pyopenproject/business/services/command/notification/read_all.py b/pyopenproject/business/services/command/notification/read_all.py new file mode 100644 index 00000000..d3e21ff6 --- /dev/null +++ b/pyopenproject/business/services/command/notification/read_all.py @@ -0,0 +1,26 @@ +from pyopenproject.api_connection.exceptions.request_exception import RequestError +from pyopenproject.api_connection.requests.post_request import PostRequest +from pyopenproject.business.exception.business_error import BusinessError +from pyopenproject.business.services.command.find_list_command import FindListCommand +from pyopenproject.business.services.command.notification.notification_command import NotificationCommand +from pyopenproject.model.notification import Notification +import json + + +class ReadAll(NotificationCommand): + + def __init__(self, connection, filters): + super().__init__(connection) + self.filters = json.dumps([{'filters':filters}]) + + def execute(self): + try: + response = PostRequest(connection=self.connection, + headers={ + "Content-Type": "application/json"}, + context=f"{self.CONTEXT}/read_ian", + json=self.filters).execute() + return response + except RequestError as re: + raise BusinessError( + f"Error reading notifications: {re.args}") diff --git a/pyopenproject/business/services/command/notification/unread.py b/pyopenproject/business/services/command/notification/unread.py new file mode 100644 index 00000000..cdeeec5b --- /dev/null +++ b/pyopenproject/business/services/command/notification/unread.py @@ -0,0 +1,23 @@ +from pyopenproject.api_connection.exceptions.request_exception import RequestError +from pyopenproject.api_connection.requests.post_request import PostRequest +from pyopenproject.business.exception.business_error import BusinessError +from pyopenproject.business.services.command.notification.notification_command import NotificationCommand + + +class UnRead(NotificationCommand): + + def __init__(self, connection, notification_id): + super().__init__(connection) + + self.notification_id = notification_id + + def execute(self): + try: + response = PostRequest(connection=self.connection, + headers={ + "Content-Type": "application/json"}, + context=f"{self.CONTEXT}/{self.notification_id}/unread_ian").execute() + return response + except RequestError as re: + raise BusinessError( + f"Error unreading notifications: {re.args}") from re diff --git a/pyopenproject/business/services/command/notification/unread_all.py b/pyopenproject/business/services/command/notification/unread_all.py new file mode 100644 index 00000000..677a8202 --- /dev/null +++ b/pyopenproject/business/services/command/notification/unread_all.py @@ -0,0 +1,25 @@ +from pyopenproject.api_connection.exceptions.request_exception import RequestError +from pyopenproject.api_connection.requests.post_request import PostRequest +from pyopenproject.business.exception.business_error import BusinessError +from pyopenproject.business.services.command.notification.notification_command import NotificationCommand +from pyopenproject.model.notification import Notification +import json + + +class UnReadAll(NotificationCommand): + + def __init__(self, connection, filters): + super().__init__(connection) + self.filters = json.dumps({'filters':filters}) + + def execute(self): + try: + response = PostRequest(connection=self.connection, + headers={ + "Content-Type": "application/json"}, + context=f"{self.CONTEXT}/unread_ian", + json=self.filters).execute() + return response + except RequestError as re: + raise BusinessError( + f"Error unreading all notifications: {re.args}") diff --git a/pyopenproject/business/services/notification_service_impl.py b/pyopenproject/business/services/notification_service_impl.py new file mode 100644 index 00000000..2524d346 --- /dev/null +++ b/pyopenproject/business/services/notification_service_impl.py @@ -0,0 +1,91 @@ +from pyopenproject.business.notification_service import NotificationService +from pyopenproject.business.services.command.notification.find_all import FindAll +from pyopenproject.business.services.command.notification.find import Find +from pyopenproject.business.services.command.notification.read_all import ReadAll +from pyopenproject.business.services.command.notification.unread_all import UnReadAll +from pyopenproject.business.services.command.notification.read import Read +from pyopenproject.business.services.command.notification.unread import UnRead +from pyopenproject.business.services.command.notification.detail import Detail + + +class NotificationServiceImpl(NotificationService): + + def __init__(self, connection): + """Constructor for class NotificationServiceImpl, from NotificationService + + :param connection: The connection data + """ + super().__init__(connection) + + def find(self, notification_id): + """ + Finds a notificacion by nofitication ID + Args: + notification_id (int): The notification that is searched by id + """ + return Find(self.connection, notification_id).execute() + + def find_all(self, offset=None, page_size=None, filters=None, + sort_by=None, group_by=None): + """ + Returns a collection of notifications based on parameters + + Args: + offset (int, optional): Page number inside the requested. Defaults to None. + page_size (int, optional): Number of elements to display. Defaults to None. + filters (str(JSON), optional): id | project | readIAN | reason | resourceId | resourceType. Defaults to None. + sort_by (str(JSON), optional): id | reason | readIAn. Defaults to None. + group_by (str, optional): _description_. Defaults to None. + + Returns: + Notification: notificacion object with the results + """ + return FindAll(self.connection, offset, page_size, filters, sort_by, group_by).execute() + + def read_all(self, filters=None): + """ + Marks the whole notification collection as read. The collection contains only elements the authenticated user can see, and can be further reduced with filters. + Args: + filters (dict(JSON), optional): Filters. Defaults to None. + + Returns: + str: If the resource was created returns the response (204), else throw an error + """ + return ReadAll(self.connection, filters).execute() + + def unread_all(self, filters=None): + """ + Marks the whole notification collection as unread. The collection contains only elements the authenticated user can see, and can be further reduced with filters. + Args: + filters (list, optional): A list of dictionaries of filtres. Defaults to None. + + Returns: + _type_: _description_ + """ + return UnReadAll(self.connection, filters).execute() + + def read_notification(self, notification_id=None): + """ + Marks the given notification as read. + Args: + notification_id (integer, optional): _description_. Defaults to None. + + Returns: + _type_: _description_ + """ + return Read(self.connection, notification_id).execute() + + def unread_notification(self, notification_id=None): + """ + + Args: + notification_id (_type_, optional): _description_. Defaults to None. + + Returns: + _type_: _description_ + """ + return UnRead(self.connection, notification_id).execute() + + def find_notification_detail( + notification_id=None, detail_id=None): + return Detail(self.connection, notification_id, detail_id) diff --git a/pyopenproject/business/util/filters.py b/pyopenproject/business/util/filters.py index 0611fcab..9c31226d 100644 --- a/pyopenproject/business/util/filters.py +++ b/pyopenproject/business/util/filters.py @@ -1,4 +1,6 @@ from pyopenproject.business.util.url_parameter import URLParameter +import urllib +import json class Filters(URLParameter): @@ -15,17 +17,19 @@ def __str__(self) -> str: :return: The filters as a string """ - output = f"{self.name}=[" - for i in range(len(self.value)): - output += "{" - output += f"\"{self.value[i].name}\":" - output += "{" - output += f"\"operator\":\"{self.value[i].operator}\",\"values\":[" - for j in range(len(self.value[i].values)): - if j != 0: - output += "," - output += f"\"{self.value[i].values[j]}\"" - output += "]}}" - output += "," if len(self.value) != 1 and i != len(self.value)-1 else "" - output += "]" - return output + # output = f"{self.name}=[" + # for i in range(len(self.value)): + # output += "{" + # output += f"\"{self.value[i].name}\":" + # output += "{" + # output += f"\"operator\":\"{self.value[i].operator}\",\"values\":[" + # for j in range(len(self.value[i].values)): + # if j != 0: + # output += "," + # output += f"\"{self.value[i].values[j]}\"" + # output += "]}}" + # output += "," if len(self.value) != 1 and i != len(self.value)-1 else "" + # output += "]" + params = urllib.parse.quote_plus( + json.dumps(self.value, separators=(',', ':'))) + return f"{self.name}={params}" diff --git a/pyopenproject/model/notification.py b/pyopenproject/model/notification.py new file mode 100644 index 00000000..06f6f79b --- /dev/null +++ b/pyopenproject/model/notification.py @@ -0,0 +1,22 @@ +import json + + +class Notification: + """ + Class Notification, + represents a Notification in the app + """ + def __init__(self, json_obj): + """Constructor for class Notification + + :param json_obj: The dict with the object data + """ + self.__dict__ = json_obj + + def __str__(self): + """ + Returns the object as a string JSON + + :return: JSON as a string + """ + return json.dumps(self.__dict__) diff --git a/pyopenproject/openproject.py b/pyopenproject/openproject.py index 6a9f5a0b..49f395d4 100644 --- a/pyopenproject/openproject.py +++ b/pyopenproject/openproject.py @@ -30,6 +30,8 @@ from pyopenproject.business.services.version_service_impl import VersionServiceImpl from pyopenproject.business.services.wiki_page_service_impl import WikiPageServiceImpl from pyopenproject.business.services.work_package_service_impl import WorkPackageServiceImpl +from pyopenproject.business.services.notification_service_impl import NotificationServiceImpl + from pyopenproject.model.connection import Connection @@ -135,3 +137,6 @@ def get_wiki_page_service(self): def get_work_package_service(self): return WorkPackageServiceImpl(self.conn) + + def get_notification_service(self): + return NotificationServiceImpl(self.conn) diff --git a/tests/infra/docker-compose.yml b/tests/infra/docker-compose.yml index bd37b3c9..c874c3ce 100644 --- a/tests/infra/docker-compose.yml +++ b/tests/infra/docker-compose.yml @@ -13,8 +13,7 @@ x-op-restart-policy: &restart_policy x-op-image: &image image: openproject/community:${TAG:-11} x-op-app: &app - <<: *image - <<: *restart_policy + <<: [*image,*restart_policy] environment: RAILS_CACHE_STORE: "memcache" OPENPROJECT_CACHE__MEMCACHE__SERVER: "cache:11211" @@ -47,8 +46,8 @@ services: - backend proxy: - <<: *image - <<: *restart_policy + <<: [*image,*restart_policy] + command: "./docker/prod/proxy" ports: - "${PORT:-8080}:80" diff --git a/tests/test_cases/notification_test.py b/tests/test_cases/notification_test.py new file mode 100644 index 00000000..44d52c89 --- /dev/null +++ b/tests/test_cases/notification_test.py @@ -0,0 +1,10 @@ +import json +import os + +from pyopenproject.model.notification import Notification +from tests.test_cases.openproject_test_case import OpenProjectTestCase +from pyopenproject.model.notification import Notification + + +class NotificationTestCase(OpenProjectTestCase): + pass diff --git a/tests/test_cases/project_service_test.py b/tests/test_cases/project_service_test.py index 1c27b335..18f0ed83 100644 --- a/tests/test_cases/project_service_test.py +++ b/tests/test_cases/project_service_test.py @@ -30,6 +30,7 @@ def setUp(self): def test_find(self): current = self.proSer.find(self.project) + print(current) self.assertEqual(self.project.identifier, current.identifier) def test_find_all(self):