Skip to content

Commit 1e61f2e

Browse files
authored
Additional fields in the messaging.WebpushNotification type (#186)
* Additional webpush notificationp fields * More test coverage * Updated documentation * Updated documentation * Updated changelog
1 parent 3f1190d commit 1e61f2e

File tree

3 files changed

+264
-5
lines changed

3 files changed

+264
-5
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
- [added] The `db.reference()` method now optionally takes a `url`
44
parameter. This can be used to access multiple Firebase Databases
55
in the same project more easily.
6+
- [added] The `messaging.WebpushNotification` type now supports
7+
additional parameters.
68

79
# v2.12.0
810

firebase_admin/messaging.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,21 +283,76 @@ def __init__(self, headers=None, data=None, notification=None):
283283
self.notification = notification
284284

285285

286+
class WebpushNotificationAction(object):
287+
"""An action available to the users when the notification is presented.
288+
289+
Args:
290+
action: Action string.
291+
title: Title string.
292+
icon: Icon URL for the action (optional).
293+
"""
294+
295+
def __init__(self, action, title, icon=None):
296+
self.action = action
297+
self.title = title
298+
self.icon = icon
299+
300+
286301
class WebpushNotification(object):
287302
"""Webpush-specific notification parameters.
288303
304+
Refer to the `Notification Reference`_ for more information.
305+
289306
Args:
290307
title: Title of the notification (optional). If specified, overrides the title set via
291308
``messaging.Notification``.
292309
body: Body of the notification (optional). If specified, overrides the body set via
293310
``messaging.Notification``.
294311
icon: Icon URL of the notification (optional).
312+
actions: A list of ``messaging.WebpushNotificationAction`` instances (optional).
313+
badge: URL of the image used to represent the notification when there is
314+
not enough space to display the notification itself (optional).
315+
data: Any arbitrary JSON data that should be associated with the notification (optional).
316+
direction: The direction in which to display the notification (optional). Must be either
317+
'auto', 'ltr' or 'rtl'.
318+
image: The URL of an image to be displayed in the notification (optional).
319+
language: Notification language (optional).
320+
renotify: A boolean indicating whether the user should be notified after a new
321+
notification replaces an old one (optional).
322+
require_interaction: A boolean indicating whether a notification should remain active
323+
until the user clicks or dismisses it, rather than closing automatically (optional).
324+
silent: True to indicate that the notification should be silent (optional).
325+
tag: An identifying tag on the notification (optional).
326+
timestamp_millis: A timestamp value in milliseconds on the notification (optional).
327+
vibrate: A vibration pattern for the device's vibration hardware to emit when the
328+
notification fires (optional). THe pattern is specified as an integer array.
329+
custom_data: A dict of custom key-value pairs to be included in the notification
330+
(optional)
331+
332+
.. _Notification Reference: https://developer.mozilla.org/en-US/docs/Web/API\
333+
/notification/Notification
295334
"""
296335

297-
def __init__(self, title=None, body=None, icon=None):
336+
def __init__(self, title=None, body=None, icon=None, actions=None, badge=None, data=None,
337+
direction=None, image=None, language=None, renotify=None,
338+
require_interaction=None, silent=None, tag=None, timestamp_millis=None,
339+
vibrate=None, custom_data=None):
298340
self.title = title
299341
self.body = body
300342
self.icon = icon
343+
self.actions = actions
344+
self.badge = badge
345+
self.data = data
346+
self.direction = direction
347+
self.image = image
348+
self.language = language
349+
self.renotify = renotify
350+
self.require_interaction = require_interaction
351+
self.silent = silent
352+
self.tag = tag
353+
self.timestamp_millis = timestamp_millis
354+
self.vibrate = vibrate
355+
self.custom_data = custom_data
301356

302357

303358
class APNSConfig(object):
@@ -579,15 +634,68 @@ def encode_webpush_notification(cls, notification):
579634
raise ValueError('WebpushConfig.notification must be an instance of '
580635
'WebpushNotification class.')
581636
result = {
637+
'actions': cls.encode_webpush_notification_actions(notification.actions),
638+
'badge': _Validators.check_string(
639+
'WebpushNotification.badge', notification.badge),
582640
'body': _Validators.check_string(
583641
'WebpushNotification.body', notification.body),
642+
'data': notification.data,
643+
'dir': _Validators.check_string(
644+
'WebpushNotification.direction', notification.direction),
584645
'icon': _Validators.check_string(
585646
'WebpushNotification.icon', notification.icon),
647+
'image': _Validators.check_string(
648+
'WebpushNotification.image', notification.image),
649+
'lang': _Validators.check_string(
650+
'WebpushNotification.language', notification.language),
651+
'renotify': notification.renotify,
652+
'requireInteraction': notification.require_interaction,
653+
'silent': notification.silent,
654+
'tag': _Validators.check_string(
655+
'WebpushNotification.tag', notification.tag),
656+
'timestamp': _Validators.check_number(
657+
'WebpushNotification.timestamp_millis', notification.timestamp_millis),
586658
'title': _Validators.check_string(
587659
'WebpushNotification.title', notification.title),
660+
'vibrate': notification.vibrate,
588661
}
662+
direction = result.get('dir')
663+
if direction and direction not in ('auto', 'ltr', 'rtl'):
664+
raise ValueError('WebpushNotification.direction must be "auto", "ltr" or "rtl".')
665+
if notification.custom_data is not None:
666+
if not isinstance(notification.custom_data, dict):
667+
raise ValueError('WebpushNotification.custom_data must be a dict.')
668+
for key, value in notification.custom_data.items():
669+
if key in result:
670+
raise ValueError(
671+
'Multiple specifications for {0} in WebpushNotification.'.format(key))
672+
result[key] = value
589673
return cls.remove_null_values(result)
590674

675+
@classmethod
676+
def encode_webpush_notification_actions(cls, actions):
677+
"""Encodes a list of WebpushNotificationActions into JSON."""
678+
if actions is None:
679+
return None
680+
if not isinstance(actions, list):
681+
raise ValueError('WebpushConfig.notification.actions must be a list of '
682+
'WebpushNotificationAction instances.')
683+
results = []
684+
for action in actions:
685+
if not isinstance(action, WebpushNotificationAction):
686+
raise ValueError('WebpushConfig.notification.actions must be a list of '
687+
'WebpushNotificationAction instances.')
688+
result = {
689+
'action': _Validators.check_string(
690+
'WebpushNotificationAction.action', action.action),
691+
'title': _Validators.check_string(
692+
'WebpushNotificationAction.title', action.title),
693+
'icon': _Validators.check_string(
694+
'WebpushNotificationAction.icon', action.icon),
695+
}
696+
results.append(cls.remove_null_values(result))
697+
return results
698+
591699
@classmethod
592700
def encode_apns(cls, apns):
593701
"""Encodes an APNSConfig instance into JSON."""

tests/test_messaging.py

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -447,25 +447,174 @@ def test_invalid_icon(self, data):
447447
excinfo = self._check_notification(notification)
448448
assert str(excinfo.value) == 'WebpushNotification.icon must be a string.'
449449

450+
@pytest.mark.parametrize('data', NON_STRING_ARGS)
451+
def test_invalid_badge(self, data):
452+
notification = messaging.WebpushNotification(badge=data)
453+
excinfo = self._check_notification(notification)
454+
assert str(excinfo.value) == 'WebpushNotification.badge must be a string.'
455+
456+
@pytest.mark.parametrize('data', NON_STRING_ARGS + ['foo'])
457+
def test_invalid_direction(self, data):
458+
notification = messaging.WebpushNotification(direction=data)
459+
excinfo = self._check_notification(notification)
460+
if isinstance(data, six.string_types):
461+
assert str(excinfo.value) == ('WebpushNotification.direction must be "auto", '
462+
'"ltr" or "rtl".')
463+
else:
464+
assert str(excinfo.value) == 'WebpushNotification.direction must be a string.'
465+
466+
@pytest.mark.parametrize('data', NON_STRING_ARGS)
467+
def test_invalid_image(self, data):
468+
notification = messaging.WebpushNotification(image=data)
469+
excinfo = self._check_notification(notification)
470+
assert str(excinfo.value) == 'WebpushNotification.image must be a string.'
471+
472+
@pytest.mark.parametrize('data', NON_STRING_ARGS)
473+
def test_invalid_language(self, data):
474+
notification = messaging.WebpushNotification(language=data)
475+
excinfo = self._check_notification(notification)
476+
assert str(excinfo.value) == 'WebpushNotification.language must be a string.'
477+
478+
@pytest.mark.parametrize('data', NON_STRING_ARGS)
479+
def test_invalid_tag(self, data):
480+
notification = messaging.WebpushNotification(tag=data)
481+
excinfo = self._check_notification(notification)
482+
assert str(excinfo.value) == 'WebpushNotification.tag must be a string.'
483+
484+
@pytest.mark.parametrize('data', ['', 'foo', list(), tuple(), dict()])
485+
def test_invalid_timestamp(self, data):
486+
notification = messaging.WebpushNotification(timestamp_millis=data)
487+
excinfo = self._check_notification(notification)
488+
assert str(excinfo.value) == 'WebpushNotification.timestamp_millis must be a number.'
489+
490+
@pytest.mark.parametrize('data', ['', list(), tuple(), True, False, 1, 0])
491+
def test_invalid_custom_data(self, data):
492+
notification = messaging.WebpushNotification(custom_data=data)
493+
excinfo = self._check_notification(notification)
494+
assert str(excinfo.value) == 'WebpushNotification.custom_data must be a dict.'
495+
496+
@pytest.mark.parametrize('data', ['', dict(), tuple(), True, False, 1, 0, [1, 2]])
497+
def test_invalid_actions(self, data):
498+
notification = messaging.WebpushNotification(actions=data)
499+
excinfo = self._check_notification(notification)
500+
assert str(excinfo.value) == ('WebpushConfig.notification.actions must be a list of '
501+
'WebpushNotificationAction instances.')
502+
450503
def test_webpush_notification(self):
451504
msg = messaging.Message(
452505
topic='topic',
453506
webpush=messaging.WebpushConfig(
454-
notification=messaging.WebpushNotification(title='t', body='b', icon='i')
507+
notification=messaging.WebpushNotification(
508+
badge='badge',
509+
body='body',
510+
data={'foo': 'bar'},
511+
icon='icon',
512+
image='image',
513+
language='language',
514+
renotify=True,
515+
require_interaction=True,
516+
silent=True,
517+
tag='tag',
518+
timestamp_millis=100,
519+
title='title',
520+
vibrate=[100, 200, 100],
521+
custom_data={'k1': 'v1', 'k2': 'v2'},
522+
),
455523
)
456524
)
457525
expected = {
458526
'topic': 'topic',
459527
'webpush': {
460528
'notification': {
461-
'title': 't',
462-
'body': 'b',
463-
'icon': 'i',
529+
'badge': 'badge',
530+
'body': 'body',
531+
'data': {'foo': 'bar'},
532+
'icon': 'icon',
533+
'image': 'image',
534+
'lang': 'language',
535+
'renotify': True,
536+
'requireInteraction': True,
537+
'silent': True,
538+
'tag': 'tag',
539+
'timestamp': 100,
540+
'vibrate': [100, 200, 100],
541+
'title': 'title',
542+
'k1': 'v1',
543+
'k2': 'v2',
464544
},
465545
},
466546
}
467547
check_encoding(msg, expected)
468548

549+
def test_multiple_field_specifications(self):
550+
notification = messaging.WebpushNotification(
551+
badge='badge',
552+
custom_data={'badge': 'other badge'},
553+
)
554+
excinfo = self._check_notification(notification)
555+
expected = 'Multiple specifications for badge in WebpushNotification.'
556+
assert str(excinfo.value) == expected
557+
558+
def test_webpush_notification_action(self):
559+
msg = messaging.Message(
560+
topic='topic',
561+
webpush=messaging.WebpushConfig(
562+
notification=messaging.WebpushNotification(
563+
actions=[
564+
messaging.WebpushNotificationAction(
565+
action='a1',
566+
title='t1',
567+
),
568+
messaging.WebpushNotificationAction(
569+
action='a2',
570+
title='t2',
571+
icon='i2',
572+
),
573+
],
574+
),
575+
)
576+
)
577+
expected = {
578+
'topic': 'topic',
579+
'webpush': {
580+
'notification': {
581+
'actions': [
582+
{
583+
'action': 'a1',
584+
'title': 't1',
585+
},
586+
{
587+
'action': 'a2',
588+
'title': 't2',
589+
'icon': 'i2',
590+
},
591+
],
592+
},
593+
},
594+
}
595+
check_encoding(msg, expected)
596+
597+
@pytest.mark.parametrize('data', NON_STRING_ARGS)
598+
def test_invalid_action_name(self, data):
599+
action = messaging.WebpushNotificationAction(action=data, title='title')
600+
notification = messaging.WebpushNotification(actions=[action])
601+
excinfo = self._check_notification(notification)
602+
assert str(excinfo.value) == 'WebpushNotificationAction.action must be a string.'
603+
604+
@pytest.mark.parametrize('data', NON_STRING_ARGS)
605+
def test_invalid_action_title(self, data):
606+
action = messaging.WebpushNotificationAction(action='action', title=data)
607+
notification = messaging.WebpushNotification(actions=[action])
608+
excinfo = self._check_notification(notification)
609+
assert str(excinfo.value) == 'WebpushNotificationAction.title must be a string.'
610+
611+
@pytest.mark.parametrize('data', NON_STRING_ARGS)
612+
def test_invalid_action_icon(self, data):
613+
action = messaging.WebpushNotificationAction(action='action', title='title', icon=data)
614+
notification = messaging.WebpushNotification(actions=[action])
615+
excinfo = self._check_notification(notification)
616+
assert str(excinfo.value) == 'WebpushNotificationAction.icon must be a string.'
617+
469618

470619
class TestAPNSConfigEncoder(object):
471620

0 commit comments

Comments
 (0)