diff --git a/backend/fcm.py b/backend/fcm.py index 9e4e29e0e..3666717f1 100644 --- a/backend/fcm.py +++ b/backend/fcm.py @@ -9,6 +9,8 @@ import json +from utils import validate_object + FIREBASE_TOKENS_ENDPOINT = "%s/pushNotifications.json" % FIREBASE_URL ICON_URL = "https://firebasestorage.googleapis.com/v0/b/eciis-splab.appspot.com/o/images%2FLOGO-E-CIS-1510941864112?alt=media&token=ca197614-ad60-408e-b21e-0ebe258c4a80" @@ -17,50 +19,63 @@ push_service = FCMNotification(api_key=SERVER_KEY) -def notify_single_user(title, body, user_key): +def notify_single_user(data, user_key): """Notify a single user. It sends a notification to each user's device. Args: - title: A string that represents the notification's title. - body: The body message of the notification, the information - you want pass to the users. + data: An object that has the title, body and click_action + as properties. title is a string that represents the + notification's title. body is the body message of the notification, + the information you want pass to the users. click_action is the url + that the user is gonna be redirected when he click on the notification. user_key: user's urlsafe key. """ tokens = get_single_user_tokens(user_key) - send_push_notifications(title, body, tokens) + send_push_notifications(data, tokens) -def notify_multiple_users(title, body, user_keys): +def notify_multiple_users(data, user_keys): """Notify multiple users. This function receives a list of user_keys and use it to retrieve the tokens. Args: - title: A string that represents the notification's title. - body: The body message of the notification, the information - you want pass to the users. + data: An object that has the title, body and click_action + as properties. title is a string that represents the + notification's title. body is the body message of the notification, + the information you want pass to the users. click_action is the url + that the user is gonna be redirected when he click on the notification. user_keys: A list with all users' urlsafe keys that will receive the notification. """ tokens = get_multiple_user_tokens(user_keys) - send_push_notifications(title, body, tokens) + send_push_notifications(data, tokens) -def send_push_notifications(title, body, tokens): +def send_push_notifications(data, tokens): """It wraps the call to pyfcm notify function. Args: - title: A string that represents the notification's title. - body: The body message of the notification, the information - you want pass to the users. + data: An object that has the title, body and click_action + as properties. title is a string that represents the + notification's title. body is the body message of the notification, + the information you want pass to the users. click_action is the url + that the user is gonna be redirected when he click on the notification. tokens: The devices' tokens that will receive the notification. """ + validate_object(data, ['title', 'body', 'click_action']) + + title = data['title'] + body = data['body'] + click_action = data['click_action'] + if tokens: result = push_service.notify_multiple_devices( registration_ids=tokens, message_title=title, - message_body=body, message_icon=ICON_URL + message_body=body, message_icon=ICON_URL, + click_action=click_action ) return result diff --git a/backend/handlers/institution_children_request_collection_handler.py b/backend/handlers/institution_children_request_collection_handler.py index 154e62b11..c2b5faf28 100644 --- a/backend/handlers/institution_children_request_collection_handler.py +++ b/backend/handlers/institution_children_request_collection_handler.py @@ -12,9 +12,25 @@ from models import Institution from models import InviteFactory from models import RequestInstitutionChildren +from service_entities import enqueue_task +from push_notification import NotificationType __all__ = ['InstitutionChildrenRequestCollectionHandler'] + +def enqueue_push_notification(requested_inst_key): + """Get the necessary parameters and insert + a new push notification in the queue. + """ + requested_inst = requested_inst_key.get() + receiver = requested_inst.admin.urlsafe() + + enqueue_task('send-push-notification', { + 'type': NotificationType.link.value, + 'receivers': [receiver], + 'entity': requested_inst_key.urlsafe() + }) + class InstitutionChildrenRequestCollectionHandler(BaseHandler): """Institution Children Request Collection Handler.""" @@ -72,4 +88,6 @@ def post(self, user, institution_urlsafe): request.send_invite(host, user.current_institution) + enqueue_push_notification(requested_inst_key) + self.response.write(json.dumps(request.make())) diff --git a/backend/handlers/institution_parent_request_collection_handler.py b/backend/handlers/institution_parent_request_collection_handler.py index cb736f96e..0a3e851d5 100644 --- a/backend/handlers/institution_parent_request_collection_handler.py +++ b/backend/handlers/institution_parent_request_collection_handler.py @@ -15,7 +15,8 @@ from util import Notification, NotificationsQueueManager from service_entities import enqueue_task from service_messages import create_message - +from service_entities import enqueue_task +from push_notification import NotificationType __all__ = ['InstitutionParentRequestCollectionHandler'] @@ -40,6 +41,20 @@ def remake_link(request, requested_inst_key, child_institution, user): request.change_status('accepted') + +def enqueue_push_notification(requested_inst_key): + """Get the necessary parameters and insert + a new push notification in the queue. + """ + requested_inst = requested_inst_key.get() + receiver = requested_inst.admin.urlsafe() + + enqueue_task('send-push-notification', { + 'type': NotificationType.link.value, + 'receivers': [receiver], + 'entity': requested_inst_key.urlsafe() + }) + class InstitutionParentRequestCollectionHandler(BaseHandler): """Institution Parent Collectcion Request Handler.""" @@ -125,4 +140,6 @@ def main_operations(request, requested_inst_key, child_institution, user, host): request = main_operations(request, requested_inst_key, child_institution, user, host) + enqueue_push_notification(requested_inst_key) + self.response.write(json.dumps(request.make())) diff --git a/backend/handlers/invite_user_collection_handler.py b/backend/handlers/invite_user_collection_handler.py index 150b68284..c7c7d6250 100644 --- a/backend/handlers/invite_user_collection_handler.py +++ b/backend/handlers/invite_user_collection_handler.py @@ -85,8 +85,16 @@ def process_invites(emails, invite, current_institution_key): else: invite = createInvite(invite) invites.append({'email': invite.invitee, 'key': invite.key.urlsafe()}) - enqueue_task('send-invite', {'invites_keys': json.dumps([invite.key.urlsafe()]), 'host': host, - 'current_institution': user.current_institution.urlsafe()}) + enqueue_task('send-invite', { + 'invites_keys': json.dumps([invite.key.urlsafe()]), + 'host': host, + 'current_institution': user.current_institution.urlsafe() + }) + + enqueue_task('send-push-notification', { + 'type': type_of_invite, + 'invites': json.dumps(map(lambda invite: invite['key'], invites)) + }) self.response.write(json.dumps( {'msg': 'The invites are being processed.', 'invites' : invites})) @@ -97,4 +105,4 @@ def createInvite(data): invite = InviteFactory.create(data, data['type_of_invite']) invite.put() - return invite \ No newline at end of file + return invite diff --git a/backend/handlers/like_handler.py b/backend/handlers/like_handler.py index 0fb2cab22..ddafd8738 100644 --- a/backend/handlers/like_handler.py +++ b/backend/handlers/like_handler.py @@ -85,6 +85,14 @@ def post(self, user, post_key, comment_id=None, reply_id=None): enqueue_task('post-notification', params) + is_first_like = post.get_number_of_likes() == 1 + if is_first_like: + enqueue_task('send-push-notification', { + 'type': entity_type, + 'receivers': [subscriber.urlsafe() for subscriber in post.subscribers], + 'entity': post.key.urlsafe() + }) + @json_response @login_required def delete(self, user, post_key, comment_id=None, reply_id=None): diff --git a/backend/handlers/post_comment_handler.py b/backend/handlers/post_comment_handler.py index 771b707d6..d310b1945 100644 --- a/backend/handlers/post_comment_handler.py +++ b/backend/handlers/post_comment_handler.py @@ -67,6 +67,14 @@ def post(self, user, post_key): } enqueue_task('post-notification', params) + is_first_comment = post.get_number_of_comment() == 1 + if is_first_comment: + enqueue_task('send-push-notification', { + 'type': entity_type, + 'receivers': [subscriber.urlsafe() for subscriber in post.subscribers], + 'entity': post.key.urlsafe() + }) + self.response.write(json.dumps(Utils.toJson(comment))) @json_response diff --git a/backend/models/post.py b/backend/models/post.py index 44dc5ce49..f082a8378 100644 --- a/backend/models/post.py +++ b/backend/models/post.py @@ -259,9 +259,8 @@ def get_number_of_comment(self): @ndb.transactional(retries=10) def add_comment(self, comment): """Add a comment to the post.""" - post = self.key.get() - post.comments[comment.id] = Utils.toJson(comment) - post.put() + self.comments[comment.id] = Utils.toJson(comment) + self.put() def remove_comment(self, comment): """Remove a commet from post.""" diff --git a/backend/push_notification/__init__.py b/backend/push_notification/__init__.py new file mode 100644 index 000000000..779236342 --- /dev/null +++ b/backend/push_notification/__init__.py @@ -0,0 +1,10 @@ +"""Initialize push_notification module.""" + +from .push_notification_service import * +from .send_push_notification_worker_handler import * + +notifications = [ + push_notification_service, send_push_notification_worker_handler +] + +__all__ = [prop for notification in notifications for prop in notification.__all__] diff --git a/backend/push_notification/push_notification_service.py b/backend/push_notification/push_notification_service.py new file mode 100644 index 000000000..bf035b1f8 --- /dev/null +++ b/backend/push_notification/push_notification_service.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +"""Push Notification Service.""" +from custom_exceptions import EntityException +from enum import Enum + +__all__ = ['get_notification_props', 'NotificationType'] + +def get_notification_props(_type, entity=None): + """This function represents the interface + the service's provides to the application + to get the notification properties. + + Args: + _type -- the notification's type. type: NotificationType + entity -- an optional parameter that can be + used to determine the click_action property + """ + notification = NotificationProperties(_type, entity) + return notification.get_props() + +class NotificationType(Enum): + """This Enum wraps the + possible notification's type + to make them more maintable + """ + like = 'LIKE_POST' + comment = 'COMMENT' + invite_user = 'USER' + invite_user_adm = 'USER_ADM' + link = 'LINK' + +class NotificationProperties(object): + """This class has several private + methods, each one for an especific + notification's type. These methods + return an object with the notification + properties. + To access them, the instance is initialized + with a notification_method which is set based on + the _type property received by the constructor, + this method is called in get_props, the unique + public method. + """ + + def __init__(self, _type, entity): + """Set the notification_method based on the _type. + types object helps this operation by maping a notification's + type to its especific method. + The entity, also, is set here. + """ + types = { + NotificationType.like: self.__get_like_props, + NotificationType.comment: self.__get_comment_props, + NotificationType.invite_user: self.__get_invite_user_props, + NotificationType.invite_user_adm: self.__get_invite_user_adm_props, + NotificationType.link: self.__get_link_props + } + self.entity = entity + self.notification_method = types[_type] + + def get_props(self): + """Just returns the result of + notification_method(). + """ + return self.notification_method() + + def __get_like_props(self): + """Responsible for return the right + properties for the like notification. + self.entity can't be None once it is + used to set the url of the click_action property. + """ + if not self.entity: + raise EntityException( + 'A LIKE_POST notification requires the entity.') + + url = '/posts/%s' % self.entity.key.urlsafe() + + return { + 'title': 'Publicação curtida', + 'body': 'Uma publicação de seu interesse foi curtida', + 'click_action': url + } + + def __get_comment_props(self): + """Responsible for return the right + properties for the comment notification. + self.entity can't be None once it is + used to set the url of the click_action property. + """ + if not self.entity: + raise EntityException( + 'A COMMENT notification requires the entity.') + url = "/posts/%s" % self.entity.key.urlsafe() + + return { + 'title': 'Publicação comentada', + 'body': 'Uma publicação do seu interesse foi comentada', + 'click_action': url + } + + def __get_invite_user_props(self): + """Responsible for return the right + properties for the invite_user notification. + """ + url = "/notifications" + + return { + 'title': 'Novo convite', + 'body': 'Você recebeu um novo convite para ser membro de uma instituição', + 'click_action': url + } + + def __get_invite_user_adm_props(self): + """Responsible for return the right + properties for the invite_user_adm notification. + """ + url = "/notifications" + + return { + 'title': 'Novo convite', + 'body': 'Você recebeu um novo convite para ser administrador de uma instituição', + 'click_action': url + } + + def __get_link_props(self): + """Responsible for return the right + properties for the link notification. + """ + url = "/institution/%s/inviteInstitution" % self.entity.key.urlsafe() + + return { + 'title': 'Solicitação de vínculo', + 'body': 'Uma instituição que você administra recebeu uma nova solicitação de vínculo', + 'click_action': url + } diff --git a/backend/push_notification/send_push_notification_worker_handler.py b/backend/push_notification/send_push_notification_worker_handler.py new file mode 100644 index 000000000..1718dc6d6 --- /dev/null +++ b/backend/push_notification/send_push_notification_worker_handler.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""Send Push Notification Worker Handler.""" +from handlers import BaseHandler +from google.appengine.ext import ndb +import json +from . import get_notification_props, NotificationType +from fcm import notify_multiple_users +from models import User + +__all__ = ['SendPushNotificationHandler'] + + +class SendPushNotificationHandler(BaseHandler): + """Handles send push notification operation + in the queue.""" + + def post(self): + """Sends a push notification to each user in the receivers list. + If only one user has to receive the notification, the length of the list + is going to be equal to 1.""" + entity = ndb.Key(urlsafe=self.request.get( + 'entity')).get() if self.request.get('entity') else None + receivers = self.get_receivers() + notification_type = NotificationType(self.request.get('type')) + notification_props = get_notification_props(notification_type, entity) + notify_multiple_users(notification_props, receivers) + + def get_users_from_invite(self, invite_keys): + """This function is called when the notification's + type is invite. + It iterates through the invite_keys retrieving the user + who will receive the invite and adding its key + in the user_keys method. + + Args: + invite_keys: An array with all the invites' keys that are + going to be send. + + Returns: + A list with all the users' keys who will receive invite. + """ + user_keys = [] + for invite_key in invite_keys: + invite = ndb.Key(urlsafe=invite_key).get() + user = User.get_active_user(invite.invitee) + user_key = user.key.urlsafe() if user else None + user_keys.append(user_key) + return user_keys + + def get_receivers(self): + """Responsible for check from where the receivers + have to be got. There are two possibilites, the receivers + can be retrieved from the invites, when the notification's type + is invite, or, they can be retrieved by the receivers property from + the request, what happens for the other types. + + Returns: + A list with all the users' keys who will receive the notification. + """ + return (self.get_users_from_invite(json.loads(self.request.get('invites'))) + if self.request.get('invites') else self.request.get_all('receivers')) diff --git a/backend/requirements.txt b/backend/requirements.txt index 614ced87e..61c6aa82d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -23,4 +23,5 @@ webapp2==2.5.2 WebOb==1.7.3 WebTest==2.0.27 jinja2==2.10 -pyfcm==1.4.5 \ No newline at end of file +pyfcm==1.4.5 +enum34~=1.1.6 \ No newline at end of file diff --git a/backend/test/invite_handlers_tests/invite_user_collection_handler_test.py b/backend/test/invite_handlers_tests/invite_user_collection_handler_test.py index bcb28def1..38dde6725 100644 --- a/backend/test/invite_handlers_tests/invite_user_collection_handler_test.py +++ b/backend/test/invite_handlers_tests/invite_user_collection_handler_test.py @@ -8,7 +8,7 @@ from google.appengine.ext import ndb from handlers import InviteUserCollectionHandler from models import Invite, InviteUser -from mock import patch +from mock import patch, call @@ -86,15 +86,24 @@ def test_post_invite_user(self, enqueue_task, verify_token, create_sent_invites_ create_sent_invites_notification.assert_called_with(institution.key) - enqueue_task.assert_called_with( - 'send-invite', - { - 'invites_keys': json.dumps([invite.key.urlsafe()]), - 'host': response.request.host, - 'current_institution': institution.key.urlsafe(), - 'notifications_ids': ['some_notification_id'] - } - ) + send_invite_params = { + 'invites_keys': json.dumps([invite.key.urlsafe()]), + 'host': response.request.host, + 'current_institution': institution.key.urlsafe(), + 'notifications_ids': ['some_notification_id'] + } + + send_push_notification_params = { + 'type': 'USER', + 'invites': json.dumps([invite.key.urlsafe()]) + } + + calls = [ + call('send-invite', send_invite_params), + call('send-push-notification', send_push_notification_params) + ] + + enqueue_task.assert_has_calls(calls) @patch('util.login_service.verify_token') @@ -144,14 +153,23 @@ def test_post_invite_user_adm(self, enqueue_task, verify_token): self.assertEqual(expected_make, invite.make()) - enqueue_task.assert_called_with( - 'send-invite', - { - 'invites_keys': json.dumps([invite.key.urlsafe()]), - 'host': response.request.host, - 'current_institution': institution.key.urlsafe() - } - ) + send_invite_params = { + 'invites_keys': json.dumps([invite.key.urlsafe()]), + 'host': response.request.host, + 'current_institution': institution.key.urlsafe() + } + + send_push_notification_params = { + 'type': 'USER_ADM', + 'invites': json.dumps([invite.key.urlsafe()]) + } + + calls = [ + call('send-invite', send_invite_params), + call('send-push-notification', send_push_notification_params) + ] + + enqueue_task.assert_has_calls(calls) @patch('util.login_service.verify_token') def test_post_invalid_invite_type(self, verify_token): diff --git a/backend/test/like_handlers_tests/like_post_handler_test.py b/backend/test/like_handlers_tests/like_post_handler_test.py index 5a6a271cd..52835f71c 100644 --- a/backend/test/like_handlers_tests/like_post_handler_test.py +++ b/backend/test/like_handlers_tests/like_post_handler_test.py @@ -8,7 +8,7 @@ from handlers.like_handler import LikeHandler from .. import mocks -from mock import patch +from mock import patch, call class LikePostHandlerTest(TestBaseHandler): @@ -88,7 +88,7 @@ def test_post(self, verify_token, enqueue_task): "The number of likes expected was 1, but was %d" % self.post.get_number_of_likes()) # assert the notification was sent - params = { + post_not_params = { 'receiver_key': self.post.author.urlsafe(), 'sender_key': self.other_user.key.urlsafe(), 'entity_key': self.post.key.urlsafe(), @@ -97,7 +97,18 @@ def test_post(self, verify_token, enqueue_task): 'sender_institution_key': self.post.institution.urlsafe() } - enqueue_task.assert_called_with('post-notification', params) + push_not_params = { + 'entity': self.post.key.urlsafe(), + 'type': 'LIKE_POST', + 'receivers': [subscriber.urlsafe() for subscriber in self.post.subscribers] + } + + calls = [ + call('post-notification', post_not_params), + call('send-push-notification', push_not_params) + ] + + enqueue_task.assert_has_calls(calls) # Call the post method again with self.assertRaises(Exception) as exc: diff --git a/backend/test/post_handlers_tests/post_comment_handler_test.py b/backend/test/post_handlers_tests/post_comment_handler_test.py index df5a8d34f..54458ab7b 100644 --- a/backend/test/post_handlers_tests/post_comment_handler_test.py +++ b/backend/test/post_handlers_tests/post_comment_handler_test.py @@ -12,7 +12,7 @@ from models import Institution from models import Post -from mock import patch +from mock import patch, call USER_EMAIL = 'user@email.com' OTHER_USER_EMAIL = 'other_usero@email.com' @@ -80,7 +80,7 @@ def test_post(self, verify_token, enqueue_task): "Expected size of comment's list should be one") # assert the notification was sent - params = { + post_not_params = { 'receiver_key': self.user_post.author.urlsafe(), 'sender_key': self.other_user.key.urlsafe(), 'entity_key': self.user_post.key.urlsafe(), @@ -88,7 +88,19 @@ def test_post(self, verify_token, enqueue_task): 'current_institution': self.institution.key.urlsafe(), 'sender_institution_key': self.user_post.institution.urlsafe() } - enqueue_task.assert_called_with('post-notification', params) + + push_not_params = { + 'entity': self.user_post.key.urlsafe(), + 'type': 'COMMENT', + 'receivers': [subscriber.urlsafe() for subscriber in self.user_post.subscribers] + } + + calls = [ + call('post-notification', post_not_params), + call('send-push-notification', push_not_params) + ] + + enqueue_task.assert_has_calls(calls) # Verify that the post is published self.assertEquals(self.user_post.state, "published") diff --git a/backend/test/request_handlers_test/institution_children_request_collection_handler_test.py b/backend/test/request_handlers_test/institution_children_request_collection_handler_test.py index 4ffed98a3..3b01a9774 100644 --- a/backend/test/request_handlers_test/institution_children_request_collection_handler_test.py +++ b/backend/test/request_handlers_test/institution_children_request_collection_handler_test.py @@ -29,8 +29,9 @@ def setUp(cls): ], debug=True) cls.testapp = cls.webtest.TestApp(app) + @patch('handlers.institution_children_request_collection_handler.enqueue_task') @patch('util.login_service.verify_token', return_value=ADMIN) - def test_post(self, verify_token): + def test_post(self, verify_token, enqueue_task): """Test method post of InstitutionParentRequestCollectionHandler.""" admin = mocks.create_user(ADMIN['email']) institution = mocks.create_institution() @@ -78,6 +79,12 @@ def test_post(self, verify_token): request['type_of_invite'], 'REQUEST_INSTITUTION_CHILDREN', 'Expected sender type_of_invite is REQUEST_INSTITUTION_CHILDREN') + + enqueue_task.assert_called_with('send-push-notification', { + 'type': 'LINK', + 'receivers': [inst_requested.admin.urlsafe()], + 'entity': inst_requested.key.urlsafe() + }) @patch('util.login_service.verify_token', return_value=ADMIN) def test_post_with_wrong_institution(self, verify_token): @@ -121,6 +128,7 @@ def test_post_with_wrong_institution(self, verify_token): "Error! User is not allowed to send request", exception_message, "Expected error message is Error! User is not allowed to send request") + @patch('util.login_service.verify_token', return_value=USER) def test_post_user_not_admin(self, verify_token): @@ -235,4 +243,4 @@ def test_post_circular_hierarchy(self, verify_token): self.assertEqual( expected_message, exception_message, "The expected error message is not equal to the exception one") - \ No newline at end of file + diff --git a/backend/test/request_handlers_test/institution_parent_request_collection_handler_test.py b/backend/test/request_handlers_test/institution_parent_request_collection_handler_test.py index ded0da4b8..8c548bd18 100644 --- a/backend/test/request_handlers_test/institution_parent_request_collection_handler_test.py +++ b/backend/test/request_handlers_test/institution_parent_request_collection_handler_test.py @@ -9,7 +9,7 @@ from handlers.institution_parent_request_collection_handler import InstitutionParentRequestCollectionHandler from .. import mocks -from mock import patch +from mock import patch, call ADMIN = {'email': 'user1@gmail.com'} USER = {'email': 'otheruser@ccc.ufcg.edu.br'} @@ -28,8 +28,9 @@ def setUp(cls): ], debug=True) cls.testapp = cls.webtest.TestApp(app) + @patch('handlers.institution_parent_request_collection_handler.enqueue_task', return_value={}) @patch('util.login_service.verify_token', return_value=ADMIN) - def test_post(self, verify_token): + def test_post(self, verify_token, enqueue_task): """Test method post of InstitutionParentRequestCollectionHandler.""" admin = mocks.create_user(ADMIN['email']) institution = mocks.create_institution() @@ -81,6 +82,14 @@ def test_post(self, verify_token): self.assertEqual( institution.parent_institution, inst_requested.key, "The parent institution of inst test must be update to inst_requested") + + enqueue_task.assert_called_with( + 'send-push-notification', { + 'type': 'LINK', + 'receivers': [inst_requested.admin.urlsafe()], + 'entity': inst_requested.key.urlsafe() + } + ) @patch('util.login_service.verify_token', return_value=ADMIN) def test_post_with_wrong_institution(self, verify_token): diff --git a/backend/test/test_base_handler.py b/backend/test/test_base_handler.py index edc74cc5f..3d17d5fc7 100644 --- a/backend/test/test_base_handler.py +++ b/backend/test/test_base_handler.py @@ -17,7 +17,7 @@ def setUp(cls): cls.test.init_memcache_stub() cls.ndb.get_context().set_cache_policy(False) cls.test.init_search_stub() - + def get_message_exception(self, exception): """Return only message of string exception for tests.""" self.list_args = exception.split("\n") diff --git a/backend/test/util_modules_tests/fcm_test.py b/backend/test/util_modules_tests/fcm_test.py index 71c173936..7fc0da8d4 100644 --- a/backend/test/util_modules_tests/fcm_test.py +++ b/backend/test/util_modules_tests/fcm_test.py @@ -21,13 +21,16 @@ def setUp(cls): cls.s_user = mocks.create_user() cls.f_user_key = cls.f_user.key.urlsafe() cls.s_user_key = cls.s_user.key.urlsafe() + cls.notification_props = notification_props = { + 'title': 'test', + 'body': 'test', + 'click_action': '/' + } @patch('fcm.send_push_notifications') @patch('fcm.get_single_user_tokens') def test_notify_single_user(self, get_token, send_notification): - title = 'test' - body = 'test' - fcm.notify_single_user(title, body, self.f_user_key) + fcm.notify_single_user(self.notification_props, self.f_user_key) get_token.assert_called() send_notification.assert_called() @@ -35,9 +38,7 @@ def test_notify_single_user(self, get_token, send_notification): @patch('fcm.send_push_notifications') @patch('fcm.get_multiple_user_tokens') def test_notify_multiple_users(self, get_tokens, send_notification): - title = 'test' - body = 'test' - fcm.notify_multiple_users(title, body, [self.f_user_key, self.s_user_key]) + fcm.notify_multiple_users(self.notification_props, [self.f_user_key, self.s_user_key]) get_tokens.assert_called() send_notification.assert_called() diff --git a/backend/test/util_modules_tests/push_notification_service_test.py b/backend/test/util_modules_tests/push_notification_service_test.py new file mode 100644 index 000000000..88cfeea04 --- /dev/null +++ b/backend/test/util_modules_tests/push_notification_service_test.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +"""Push Notification Service Test.""" + +from ..test_base import TestBase +from .. import mocks +from push_notification import get_notification_props, NotificationType +from custom_exceptions import EntityException + + +class PushNotificationServiceTest(TestBase): + + @classmethod + def setUp(cls): + """Provide the base for the tests.""" + cls.test = cls.testbed.Testbed() + cls.test.activate() + cls.policy = cls.datastore.PseudoRandomHRConsistencyPolicy( + probability=1) + cls.test.init_datastore_v3_stub(consistency_policy=cls.policy) + cls.test.init_memcache_stub() + cls.f_user = mocks.create_user() + cls.inst = mocks.create_institution() + cls.post = mocks.create_post(cls.f_user.key, cls.inst.key) + + + def test_like_post_notification(self): + """Test if the properties for LIKE_POST + notification are the expected.""" + url = "/posts/%s" %self.post.key.urlsafe() + + notification_props = get_notification_props(NotificationType('LIKE_POST'), self.post) + + self.assertEqual(notification_props['title'], 'Publicação curtida', + "The notification's title wasn't the expected one") + self.assertEqual( + notification_props['body'], 'Uma publicação de seu interesse foi curtida', + "The notification's body wasn't the expected one") + self.assertEqual(notification_props['click_action'], url, + "The click_action's url wasn't the expected one") + + def test_like_post_notification_without_entity(self): + """Test if a exception is raised when try to get + the notification props without an entity for LIKE_POST + notification.""" + with self.assertRaises(EntityException) as ex: + notification_props = get_notification_props(NotificationType('LIKE_POST')) + + error_message = ex.exception.message + + self.assertEqual( + error_message, 'A LIKE_POST notification requires the entity.', + "The error_message wasn't the expected one") + + def test_comment_notification(self): + """Test if the properties for COMMENT + notification are the expected.""" + url = "/posts/%s" % self.post.key.urlsafe() + + notification_props = get_notification_props(NotificationType('COMMENT'), self.post) + + self.assertEqual(notification_props['title'], 'Publicação comentada', + "The notification's title wasn't the expected one") + self.assertEqual( + notification_props['body'], 'Uma publicação do seu interesse foi comentada', + "The notification's body wasn't the expected one") + self.assertEqual(notification_props['click_action'], url, + "The click_action wasn't the expected one") + + def test_comment_notification_without_entity(self): + """Test if a exception is raised when try to get + the notification props without an entity for LIKE_POST + notification.""" + with self.assertRaises(EntityException) as ex: + notification_props = get_notification_props(NotificationType('COMMENT')) + + error_message = ex.exception.message + + self.assertEqual( + error_message, 'A COMMENT notification requires the entity.', + "The error_message wasn't the expected one") + + def test_invite_user_notification(self): + """Test if the properties for USER + notification are the expected.""" + url = "/notifications" + + notification_props = get_notification_props(NotificationType('USER')) + + self.assertEqual(notification_props['title'], 'Novo convite', + "The notification's title wasn't the expected one") + self.assertEqual(notification_props['body'], + 'Você recebeu um novo convite para ser membro de uma instituição', + "The notification's body wasn't the expected one") + self.assertEqual(notification_props['click_action'], url, + "The click_action wasn't the expected one") + + def test_invite_user_adm_notification(self): + """Test if the properties for USER_ADM + notification are the expected.""" + url = "/notifications" + + notification_props = get_notification_props(NotificationType('USER_ADM')) + + self.assertEqual(notification_props['title'], 'Novo convite', + "The notification's title wasn't the expected one") + self.assertEqual(notification_props['body'], + 'Você recebeu um novo convite para ser administrador de uma instituição', + "The notification's body wasn't the expected one") + self.assertEqual(notification_props['click_action'], url, + "The click_action wasn't the expected one") diff --git a/backend/test/worker_handlers_tests/send_invite_handler_test.py b/backend/test/worker_handlers_tests/send_invite_handler_test.py index 77abe7341..1378eaeea 100644 --- a/backend/test/worker_handlers_tests/send_invite_handler_test.py +++ b/backend/test/worker_handlers_tests/send_invite_handler_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Send Invite Handler Tests.""" from ..test_base_handler import TestBaseHandler @@ -49,8 +50,8 @@ def test_post(self, send_invite, resolve_notification_task): second_invite.key.urlsafe() ] - request_url = '/api/queue/send-invite?invites_keys=%s&host=%s¤t_institution=%s¬ifications_ids=%s' % ( - json.dumps(invites_keys), host, institution.key.urlsafe(), notification_id) + request_url = '/api/queue/send-invite?invites_keys=%s&host=%s¤t_institution=%s¬ifications_ids=%s&type_of_invite=%s' % ( + json.dumps(invites_keys), host, institution.key.urlsafe(), notification_id, 'USER') self.testapp.post(request_url) @@ -58,4 +59,3 @@ def test_post(self, send_invite, resolve_notification_task): send_invite.assert_called_with(host, institution.key) resolve_notification_task.assert_called_with(notification_id) - \ No newline at end of file diff --git a/backend/test/worker_handlers_tests/send_push_notification_handler_test.py b/backend/test/worker_handlers_tests/send_push_notification_handler_test.py new file mode 100644 index 000000000..df26a149c --- /dev/null +++ b/backend/test/worker_handlers_tests/send_push_notification_handler_test.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""Send push notification handler test.""" + +from ..test_base_handler import TestBaseHandler +from push_notification import SendPushNotificationHandler, get_notification_props, NotificationType +import json +from .. import mocks +from mock import patch + +MAIN_URI = '/api/queue/send-push-notification' + + +class SendPushNotificationHandlerTest(TestBaseHandler): + """Test Send Push Notification Handler.""" + + @classmethod + def setUp(cls): + """Provide the base for the tests.""" + super(SendPushNotificationHandlerTest, cls).setUp() + app = cls.webapp2.WSGIApplication( + [ + (MAIN_URI, + SendPushNotificationHandler) + ], debug=True) + cls.testapp = cls.webtest.TestApp(app) + + @patch('push_notification.send_push_notification_worker_handler.User.get_active_user') + @patch('push_notification.send_push_notification_worker_handler.notify_multiple_users') + def test_send_notification_with_regular_receivers(self, notify, get_user): + """Test if the notify's method is called + properly with the regular receivers.""" + user = mocks.create_user() + inst = mocks.create_institution() + post = mocks.create_post(user.key, inst.key) + post.add_subscriber(user) + notification_type = 'LIKE_POST' + + body = { + 'type': notification_type, + 'receivers': [user.key.urlsafe()], + 'entity': post.key.urlsafe() + } + + self.testapp.post(MAIN_URI, body) + + props = get_notification_props(NotificationType(notification_type), post) + + notify.assert_called_with(props, [user.key.urlsafe()]) + get_user.assert_not_called() + + @patch('push_notification.send_push_notification_worker_handler.User.get_active_user') + @patch('push_notification.send_push_notification_worker_handler.notify_multiple_users') + def test_send_notification_with_invites(self, notify, get_user): + """Test if the notify's method is called + properly without the receivers as parameter.""" + user = mocks.create_user() + inst = mocks.create_institution() + post = mocks.create_post(user.key, inst.key) + post.add_subscriber(user) + notification_type = 'USER' + f_invite = mocks.create_invite(user, inst.key, 'USER') + s_user = mocks.create_user() + s_invite = mocks.create_invite(s_user, inst.key, 'USER') + user.state = 'active' + s_user.state = 'active' + user.put() + s_user.put() + + invites = [f_invite.make(), s_invite.make()] + body = { + 'type': notification_type, + 'invites': json.dumps(map(lambda invite: invite['key'], invites)) + } + + self.testapp.post(MAIN_URI, body) + + notify.assert_called() + get_user.assert_called() + diff --git a/backend/utils.py b/backend/utils.py index cbd8ed24e..66f3b5bcc 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -14,8 +14,7 @@ from oauth2client.crypt import AppIdentityError from custom_exceptions import NotAuthorizedException -from custom_exceptions import QueryException - +from custom_exceptions import QueryException, FieldException class Utils(): @@ -173,3 +172,13 @@ def text_normalize(text): normal_form_text = normalize('NFKD', unicode(text)).encode('ascii', 'ignore') text_ignoring_escape_chars = normal_form_text.encode('unicode-escape') return text_ignoring_escape_chars + +def validate_object(obj, props): + """It iterates in props list and + check if each one is in the obj's keys.""" + for prop in props: + Utils._assert( + prop not in obj, + "The %s property is not in the object" %prop, + FieldException + ) diff --git a/backend/worker.py b/backend/worker.py index 2cac22eed..c1f51e25c 100644 --- a/backend/worker.py +++ b/backend/worker.py @@ -3,7 +3,6 @@ import webapp2 import json from firebase import send_notification -from fcm import notify_single_user from google.appengine.api import mail import logging from google.appengine.ext import ndb @@ -20,6 +19,7 @@ from permissions import DEFAULT_ADMIN_PERMISSIONS from send_email_hierarchy import RemoveInstitutionEmailSender from util import NotificationsQueueManager +from push_notification import SendPushNotificationHandler def should_remove(user, inst_key_urlsafe, transfer_inst_key_urlsafe): @@ -268,7 +268,6 @@ def post(self): current_institution_key = ndb.Key(urlsafe=self.request.get('current_institution')) sender_inst_key = self.request.get('sender_institution_key') and ndb.Key(urlsafe=self.request.get('sender_institution_key')) post = ndb.Key(urlsafe=post_key).get() - is_first_like = post.get_number_of_likes() == 1 notification_message = post.create_notification_message( ndb.Key(urlsafe=sender_url_key), @@ -287,11 +286,7 @@ def post(self): entity_key=post_key, message=notification_message ) - - if is_first_like: - title = "Primeira curtida" - body = "O post pelo qual você deseja receber atualizações recebeu a primeira curtida." - notify_single_user(title, body, subscriber) + class EmailMembersHandler(BaseHandler): """Handle requests to send emails to institution members.""" @@ -516,5 +511,6 @@ def save_changes(admin, new_admin, institution): ('/api/queue/add-admin-permissions', AddAdminPermissionsInInstitutionHierarchy), ('/api/queue/remove-admin-permissions', RemoveAdminPermissionsInInstitutionHierarchy), ('/api/queue/send-invite', SendInviteHandler), - ('/api/queue/transfer-admin-permissions', TransferAdminPermissionsHandler) + ('/api/queue/transfer-admin-permissions', TransferAdminPermissionsHandler), + ('/api/queue/send-push-notification', SendPushNotificationHandler) ], debug=True)