diff --git a/backend/fcm.py b/backend/fcm.py
new file mode 100644
index 000000000..3666717f1
--- /dev/null
+++ b/backend/fcm.py
@@ -0,0 +1,179 @@
+"""Pyfcm is a library to enhance and make it easier the communication
+with firebase cloud messaging - fcm - from python."""
+
+from pyfcm import FCMNotification
+
+from firebase_config import SERVER_KEY, FIREBASE_URL
+
+from firebase import _get_http
+
+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"
+
+# Instantiate a fcm service to the application server key
+push_service = FCMNotification(api_key=SERVER_KEY)
+
+
+def notify_single_user(data, user_key):
+ """Notify a single user.
+ It sends a notification to each user's device.
+
+ Args:
+ 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(data, tokens)
+
+
+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:
+ 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(data, tokens)
+
+
+def send_push_notifications(data, tokens):
+ """It wraps the call to pyfcm notify function.
+
+ Args:
+ 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,
+ click_action=click_action
+ )
+ return result
+
+
+def get_single_user_tokens(user_key):
+ """Calls get_tokens_from_firebase()
+ to get all the tokens and then filter them.
+
+ Args:
+ user_key: The user ndb key.urlsafe().
+
+ Returns:
+ The user's tokens or an empty list when the user hasn't
+ enabled notifications yet.
+ """
+ data = get_tokens_from_firebase(user_key)
+ tokens = filter_single_user_tokens(data)
+ return tokens
+
+
+def get_multiple_user_tokens(users_keys):
+ """This function calls get_all_tokens_from_firebase()
+ to get all the tokens and then filter them using
+ filter_multiple_user_tokens function.
+
+ Args:
+ users_keys: The users ndb key.urlsafe().
+
+ Returns:
+ The users' token or an empty list when None
+ of the users haven't enabled notifications yet.
+ """
+ data = get_all_tokens_from_firebase()
+ tokens = filter_multiple_user_tokens(data, users_keys)
+ return tokens
+
+
+def get_all_tokens_from_firebase():
+ """This function only wraps the logic of
+ make a request to firebase to retrieve all
+ the tokens from the database.
+
+ Returns:
+ The request's content parsed to json.
+ """
+ response, content = _get_http().request(FIREBASE_TOKENS_ENDPOINT, method='GET')
+ return json.loads(content)
+
+
+def get_tokens_from_firebase(user_key):
+ """It gets all tokens from the firebase
+ of the user whose key is user_key received as parameter.
+
+ Args:
+ user_key: The user's urlsafe key.
+
+ Returns:
+ The request's content parsed to json.
+ """
+ firebase_endpoint = "%s/pushNotifications/%s.json" %(FIREBASE_URL, user_key)
+ response, content = _get_http().request(firebase_endpoint, method='GET')
+ return json.loads(content)
+
+
+def filter_single_user_tokens(content):
+ """It loops through the content keys
+ and for each object it appends the
+ token property to the token's list.
+
+ Args:
+ content: A json returned from firebase.
+
+ Returns:
+ The user's tokens.
+ """
+ tokens = []
+ for key in content:
+ tokens.append(content[key]['token'])
+ return tokens
+
+
+def filter_multiple_user_tokens(content, users_keys):
+ """For each user, represented by user_key
+ It get the user's firebase objects and loops through
+ each object getting the token.
+
+ Args:
+ content: The json returned from firebase
+ users_keys: The users' keys who will receive the
+ notification.
+
+ Returns:
+ The users' tokens.
+ """
+ tokens = []
+ for user_key in users_keys:
+ if user_key in content:
+ current_firebase_objects = content[user_key]
+ current_tokens = filter_single_user_tokens(current_firebase_objects)
+ map(lambda token: tokens.append(token), current_tokens)
+ return tokens
diff --git a/backend/firebase.py b/backend/firebase.py
index d831b069d..031a0f93e 100644
--- a/backend/firebase.py
+++ b/backend/firebase.py
@@ -13,8 +13,8 @@
_FIREBASE_SCOPES = [
'https://www.googleapis.com/auth/firebase.database',
- 'https://www.googleapis.com/auth/userinfo.email']
-
+ 'https://www.googleapis.com/auth/userinfo.email'
+]
@lru_cache()
def _get_http():
@@ -44,4 +44,4 @@ def send_notification(user, message, entity_type, entity):
message['timestamp'] = datetime.datetime.now().isoformat()
message['entity_type'] = entity_type
message['entity'] = entity
- firebase_post(url, value=json.dumps(message))
+ firebase_post(url, value=json.dumps(message))
\ No newline at end of file
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 f1a4e4ef4..61c6aa82d 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -14,7 +14,7 @@ pyasn1==0.4.2
pyasn1-modules==0.2.1
PyJWT==1.5.2
PyYAML==3.12
-requests==2.10.0
+requests==2.3
requests-toolbelt==0.8.0
rsa==3.4.2
six==1.10.0
@@ -22,4 +22,6 @@ waitress==1.1.0
webapp2==2.5.2
WebOb==1.7.3
WebTest==2.0.27
-jinja2==2.10
\ No newline at end of file
+jinja2==2.10
+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
new file mode 100644
index 000000000..7fc0da8d4
--- /dev/null
+++ b/backend/test/util_modules_tests/fcm_test.py
@@ -0,0 +1,111 @@
+"""Fcm Module Test."""
+
+from ..test_base import TestBase
+from mock import patch
+from .. import mocks
+import fcm
+
+
+class FcmTest(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.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):
+ fcm.notify_single_user(self.notification_props, self.f_user_key)
+
+ get_token.assert_called()
+ send_notification.assert_called()
+
+ @patch('fcm.send_push_notifications')
+ @patch('fcm.get_multiple_user_tokens')
+ def test_notify_multiple_users(self, get_tokens, send_notification):
+ fcm.notify_multiple_users(self.notification_props, [self.f_user_key, self.s_user_key])
+
+ get_tokens.assert_called()
+ send_notification.assert_called()
+
+ @patch('fcm.filter_single_user_tokens')
+ @patch('fcm.get_tokens_from_firebase')
+ def test_get_single_user_tokens(self, get_tokens, filter):
+ fcm.get_single_user_tokens(self.f_user_key)
+
+ get_tokens.assert_called()
+ filter.assert_called()
+
+ @patch('fcm.filter_multiple_user_tokens')
+ @patch('fcm.get_all_tokens_from_firebase')
+ def test_get_multiple_user_tokens(self, get_tokens, filter):
+ fcm.get_multiple_user_tokens([self.f_user_key, self.s_user_key])
+
+ get_tokens.assert_called()
+ filter.assert_called()
+
+ def test_filter_single_user_tokens(self):
+ content = {
+ 'sapok-DOP': {
+ 'token': 'token-1'
+ },
+ 'oakdopak-qopekqp': {
+ 'token': 'token-2'
+ },
+ 'opkdsfa-OFDO': {
+ 'token': 'token-3'
+ }
+ }
+
+ tokens = fcm.filter_single_user_tokens(content)
+ self.assertTrue(len(tokens) == 3, "The size of the list tokens is not the expected one")
+ self.assertTrue(
+ 'token-1' in tokens and
+ 'token-2' in tokens and
+ 'token-3' in tokens,
+ "The list tokens is not the expected one"
+ )
+
+ def test_filter_multiple_user_tokens(self):
+ content = {
+ self.f_user_key: {
+ 'sapok-DOP': {
+ 'token': 'token-1'
+ }
+ },
+ self.s_user_key: {
+ 'oakdopak-qopekqp': {
+ 'token': 'token-2'
+ }
+ }
+ }
+
+ users_keys = [self.f_user_key, self.s_user_key, 'aop-OPAKSD']
+
+ tokens = fcm.filter_multiple_user_tokens(content, users_keys)
+ self.assertTrue(len(tokens) == 2,
+ "The size of the list tokens is not the expected one")
+ self.assertTrue(
+ 'token-1' in tokens and
+ 'token-2' in tokens,
+ "The list tokens is not the expected one"
+ )
+
+ def tearDown(self):
+ """Deactivate the test."""
+ self.test.deactivate()
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 1e0a3e7d4..c1f51e25c 100644
--- a/backend/worker.py
+++ b/backend/worker.py
@@ -19,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):
@@ -286,6 +287,7 @@ def post(self):
message=notification_message
)
+
class EmailMembersHandler(BaseHandler):
"""Handle requests to send emails to institution members."""
@@ -509,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)
diff --git a/ecis b/ecis
index de5e2bae3..59d4b86d3 100755
--- a/ecis
+++ b/ecis
@@ -256,6 +256,7 @@ case "$1" in
cd backend
echo 'FIREBASE_URL = "FIREBASE_URL"' > firebase_config.py
+ echo 'SERVER_KEY = "SERVER_KEY"' >> firebase_config.py
TEST_NAME="*test.py"
if [ "$3" == "--name" ] && [ ! -z "$4" ]; then
diff --git a/frontend/auth/authService.js b/frontend/auth/authService.js
index 6ca7568a6..70a748532 100644
--- a/frontend/auth/authService.js
+++ b/frontend/auth/authService.js
@@ -4,7 +4,7 @@
var app = angular.module("app");
app.service("AuthService", function AuthService($q, $state, $window, UserService,
- MessageService) {
+ MessageService, PushNotificationService) {
var service = this;
var authObj = firebase.auth();
@@ -121,6 +121,7 @@
if (user.emailVerified) {
return user.getIdToken(true).then(function(idToken) {
return service.setupUser(idToken, user.emailVerified).then(function success(userInfo) {
+ PushNotificationService.requestNotificationPermission(service.getCurrentUser());
return userInfo;
});
});
diff --git a/frontend/index.html b/frontend/index.html
index 3947152d3..fcfc07fbf 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -209,6 +209,7 @@
+
diff --git a/frontend/notification/pushNotificationService.js b/frontend/notification/pushNotificationService.js
new file mode 100644
index 000000000..d1f5c5d83
--- /dev/null
+++ b/frontend/notification/pushNotificationService.js
@@ -0,0 +1,134 @@
+(function() {
+ 'use strict';
+
+ const app = angular.module('app');
+
+ app.service('PushNotificationService', function PushNotificationService($firebaseArray,
+ $firebaseObject, MessageService) {
+ /**
+ * Service responsible for send request permission
+ * to enable notifications to the user and for deal
+ * with the token resulted from this operation by saving
+ * or updating it in firebase database.
+ * Just in case the user is on a mobile device.
+ */
+ const service = this;
+
+ /**
+ * Retrieves the application instance of
+ * firebase messaging.
+ */
+ const messaging = firebase.messaging();
+
+ const ref = firebase.database().ref();
+
+ const PUSH_NOTIFICATIONS_URL = "pushNotifications/";
+
+ /**
+ * @private
+ */
+ service._isMobile = {
+ Android: () => {
+ return navigator.userAgent.match(/Android/i);
+ },
+ BlackBerry: () => {
+ return navigator.userAgent.match(/BlackBerry/i);
+ },
+ iOS: () => {
+ return navigator.userAgent.match(/iPhone|iPad|iPod/i);
+ },
+ Opera: () => {
+ return navigator.userAgent.match(/Opera Mini/i);
+ },
+ Windows: () => {
+ return navigator.userAgent.match(/IEMobile/i);
+ },
+ any: () => {
+ return (
+ service._isMobile.Android() ||
+ service._isMobile.BlackBerry() ||
+ service._isMobile.iOS() ||
+ service._isMobile.Opera() ||
+ service._isMobile.Windows()
+ );
+ }
+ };
+
+ service.firebaseArrayNotifications;
+
+ /**
+ * Ask permission to the user to send push notifications
+ * and if the permission is conceded the user's new token
+ * is retrieved and saveToken is called passing the token
+ * as parameter.
+ */
+ service.requestNotificationPermission = function requestNotificationPermission(user) {
+ service.currentUser = user;
+ const isOnMobile = service._isMobile.any();
+ if (!service._hasNotificationPermission() && isOnMobile) {
+ messaging.requestPermission().then(() => {
+ messaging.getToken().then(token => {
+ service._saveToken(token);
+ }, () => {
+ MessageService.showToast('Não foi possível ativar as notificações.');
+ });
+ });
+ }
+ };
+
+ /**
+ * It receives a token, starts a reference to
+ * firebase's database and save the token.
+ * @param {String} token
+ * @private
+ */
+ service._saveToken = function saveToken(token) {
+ const notificationsRef = service._initFirebaseArray();
+ service._setToken(token, notificationsRef);
+ };
+
+ /**
+ * Instantiate a reference to the database
+ * based on the userKey, starts a firebaseArray
+ * in case of it hasn't been started before, and
+ * return the reference.
+ * @private
+ */
+ service._initFirebaseArray = function initFirebaseArray() {
+ const endPoint = `${PUSH_NOTIFICATIONS_URL}${service.currentUser.key}`;
+ const notificationsRef = ref.child(endPoint);
+
+ if (!service.firebaseArrayNotifications) {
+ service.firebaseArrayNotifications = $firebaseArray(notificationsRef);
+ }
+
+ return notificationsRef;
+ };
+
+ /**
+ * It is responsible for check if the user already have a token.
+ * If it does, the token is replaced by the new one received as parameter.
+ * Otherwise the token is saved.
+ * @param {String} token
+ * @param {object} notificationsRef
+ * @private
+ */
+ service._setToken = function setToken(token, notificationsRef) {
+ service.firebaseArrayNotifications.$loaded().then(() => {
+ const tokenObject = $firebaseObject(notificationsRef);
+ tokenObject.token = token;
+ service.firebaseArrayNotifications.$add(tokenObject);
+ });
+ };
+
+ /**
+ * Check if the user has already conceded the permission
+ * using Notification object.
+ * @private
+ */
+ service._hasNotificationPermission = function hasNotificationPermission() {
+ const { permission } = Notification;
+ return permission === "granted";
+ };
+ });
+})();
\ No newline at end of file
diff --git a/frontend/test/init.js b/frontend/test/init.js
index 2b9f5ba5a..f271fb525 100644
--- a/frontend/test/init.js
+++ b/frontend/test/init.js
@@ -9,7 +9,8 @@
apiKey: "MOCK-API_KEY",
authDomain: "eciis-splab.firebaseapp.com", // Your Firebase Auth domain ("*.firebaseapp.com")
databaseURL: "https://eciis-splab.firebaseio.com", // Your Firebase Database URL ("https://*.firebaseio.com")
- storageBucket: "eciis-splab.appspot.com"
+ storageBucket: "eciis-splab.appspot.com",
+ messagingSenderId: "jdsfkbcbmnweuiyeuiwyhdjskalhdjkhjk"
});
var user = {
diff --git a/frontend/test/specs/notification/pushNotificationServiceSpec.js b/frontend/test/specs/notification/pushNotificationServiceSpec.js
new file mode 100644
index 000000000..6196fb0b7
--- /dev/null
+++ b/frontend/test/specs/notification/pushNotificationServiceSpec.js
@@ -0,0 +1,143 @@
+'use strict';
+
+(describe('Test PushNotificationService', function () {
+ var service, messaging, defaultToken, notificationsRef, messageService;
+
+ const fakeCallback = function fakeCallback(data) {
+ return {
+ then: function (callback) {
+ return callback(data);
+ }
+ };
+ };
+
+ beforeEach(module('app'));
+
+ beforeEach(inject(function (PushNotificationService, MessageService) {
+ service = PushNotificationService;
+ messaging = firebase.messaging();
+ messageService = MessageService;
+ var ref = firebase.database().ref();
+ notificationsRef = ref.child("notifications/key");
+ defaultToken = 'oaspkd-OPASKDAPO';
+ }));
+
+ describe('requestNotificationPermission', () => {
+ beforeEach(() => {
+ spyOn(service, '_hasNotificationPermission').and.returnValue(false);
+ spyOn(service._isMobile, 'any').and.returnValue(true);
+ });
+
+ it('should call saveToken', () => {
+ spyOn(messaging, 'requestPermission').and.callFake(fakeCallback);
+ spyOn(messaging, 'getToken').and.callFake(fakeCallback);
+ spyOn(service, '_saveToken').and.callFake(fakeCallback);
+
+ service.requestNotificationPermission(new User({}));
+
+ expect(messaging.requestPermission).toHaveBeenCalled();
+ expect(messaging.getToken).toHaveBeenCalled();
+ expect(service._saveToken).toHaveBeenCalled();
+ });
+
+ it('should not call saveToken when the user does not enable notification', () => {
+ spyOn(messaging, 'requestPermission').and.callFake(function() {
+ return {
+ then: function () {
+ return;
+ }
+ }
+ });
+ spyOn(messaging, 'getToken');
+ spyOn(service, '_saveToken');
+
+ service.requestNotificationPermission();
+
+ expect(messaging.requestPermission).toHaveBeenCalled();
+ expect(messaging.getToken).not.toHaveBeenCalled();
+ expect(service._saveToken).not.toHaveBeenCalled();
+ });
+
+ it('should call showToast', () => {
+ spyOn(messaging, 'requestPermission').and.callFake(fakeCallback);
+ spyOn(messaging, 'getToken').and.callFake(() => {
+ return {
+ then: (success, error) => {
+ return error();
+ }
+ }
+ });
+ spyOn(service, '_saveToken');
+ spyOn(messageService, 'showToast');
+
+ service.requestNotificationPermission();
+
+ expect(messaging.requestPermission).toHaveBeenCalled();
+ expect(messaging.getToken).toHaveBeenCalled();
+ expect(service._saveToken).not.toHaveBeenCalled();
+ expect(messageService.showToast).toHaveBeenCalledWith('Não foi possível ativar as notificações.');
+ });
+ });
+
+ describe('saveToken', () => {
+ it('should initFirebaseArray and setToken', () => {
+ spyOn(service, '_initFirebaseArray').and.returnValue(notificationsRef);
+ spyOn(service, '_setToken');
+
+ service._saveToken(defaultToken);
+
+ expect(service._initFirebaseArray).toHaveBeenCalled();
+ expect(service._setToken).toHaveBeenCalledWith(defaultToken, notificationsRef);
+ });
+ });
+
+ describe('initFirebaseArray', () => {
+ it('should starts a firebaseArray', () => {
+ expect(service.firebaseArrayNotifications).toBe(undefined);
+ service.currentUser = new User({
+ key: 'aopskdpoaAPOSDKAPOKDPK'
+ });
+
+ service._initFirebaseArray();
+
+ expect(service.firebaseArrayNotifications).not.toBe(undefined);
+ });
+ });
+
+ describe('setToken', () => {
+ beforeEach(() => {
+ service.currentUser = new User({
+ key: 'aopskdpoaAPOSDKAPOKDPK'
+ });
+ service._initFirebaseArray();
+ });
+
+ it('should call $add', () => {
+ spyOn(service.firebaseArrayNotifications, '$loaded').and.callFake(fakeCallback);
+ spyOn(service.firebaseArrayNotifications, '$add').and.callFake(fakeCallback);
+
+ service._setToken(defaultToken, notificationsRef);
+
+ expect(service.firebaseArrayNotifications.$loaded).toHaveBeenCalled();
+ expect(service.firebaseArrayNotifications.$add).toHaveBeenCalled();
+ });
+ });
+
+ describe('hasNotificationPermission', () => {
+ it('should return true when the user has enabled notifications', () => {
+ Notification = {permission: 'granted'};
+
+ const result = service._hasNotificationPermission();
+
+ expect(result).toEqual(true);
+ });
+
+ it('should return false when the user has not enabled notifications', () => {
+ Notification = {permission: 'ask'};
+
+ const result = service._hasNotificationPermission();
+
+ expect(result).toEqual(false);
+ });
+ });
+}));
\ No newline at end of file