summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorElias Showk <eshowk@entrouvert.com>2018-04-04 18:53:14 (GMT)
committerElias Showk <eshowk@entrouvert.com>2018-04-04 18:53:14 (GMT)
commit3efc1a387d2cf6e88652584a7d2f7f22d84ca6c5 (patch)
treecde4f8bcdf91de489940f0d5472e7798656d8977
parent266ede325f59c3a59b56ba20b7ece2e51dfdda3b (diff)
downloadcombo-plugin-gnm-wip/webpush.zip
combo-plugin-gnm-wip/webpush.tar.gz
combo-plugin-gnm-wip/webpush.tar.bz2
web push notifications : added new templatetag, javascript static, model, a mgmt command and tests (#22727)wip/webpush
-rw-r--r--combo_plugin_gnm/__init__.py2
-rw-r--r--combo_plugin_gnm/management/commands/send_webpush.py66
-rw-r--r--combo_plugin_gnm/migrations/0001_initial.py29
-rw-r--r--combo_plugin_gnm/migrations/__init__.py0
-rw-r--r--combo_plugin_gnm/models.py77
-rw-r--r--combo_plugin_gnm/push.py67
-rw-r--r--combo_plugin_gnm/static/combo_plugin_gnm/webpush.js238
-rw-r--r--combo_plugin_gnm/templates/webpush_checkbox.html16
-rw-r--r--combo_plugin_gnm/templates/webpush_scripts.html4
-rw-r--r--combo_plugin_gnm/templatetags/gnm_notifications.py51
-rw-r--r--combo_plugin_gnm/urls.py4
-rw-r--r--combo_plugin_gnm/views.py1
-rw-r--r--debian/50gnm.py3
-rw-r--r--setup.py5
-rw-r--r--tests/conftest.py10
-rw-r--r--tests/settings.py2
-rw-r--r--tests/test_webpush.py94
17 files changed, 667 insertions, 2 deletions
diff --git a/combo_plugin_gnm/__init__.py b/combo_plugin_gnm/__init__.py
index 4524f52..5221e16 100644
--- a/combo_plugin_gnm/__init__.py
+++ b/combo_plugin_gnm/__init__.py
@@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _
class Plugin(object):
def get_apps(self):
- return [__name__]
+ return [__name__, 'webpush']
class AppConfig(django.apps.AppConfig):
diff --git a/combo_plugin_gnm/management/commands/send_webpush.py b/combo_plugin_gnm/management/commands/send_webpush.py
new file mode 100644
index 0000000..763ddd9
--- /dev/null
+++ b/combo_plugin_gnm/management/commands/send_webpush.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+# combo-plugin-gnm - Combo WebPush App
+# Copyright (C) 2018 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+from dateutil.parser import parse
+from datetime import timedelta, datetime
+import argparse
+
+from django.core.management.base import BaseCommand
+from django.utils.timezone import make_aware, now
+from combo.apps.notifications.models import Notification
+from combo_plugin_gnm.push import send_web_push
+
+
+
+
+def to_datetime_argument(string):
+ try:
+ return make_aware(parse(string))
+ except ValueError as verr:
+ raise argparse.ArgumentTypeError(verr)
+
+
+class Command(BaseCommand):
+ help = u'''Send push notification for the last notifications, for example :
+ > python manage.py send_webpush --since=2018-03-03T00:00:00
+ will all waiting web push notifications from march 3rd 2018 until now
+ '''
+
+ def add_arguments(self, parser):
+ # Named (optional) arguments
+ parser.add_argument(
+ '--since',
+ dest='since',
+ default=None,
+ type=to_datetime_argument,
+ help='Send push notification created since this datetime',
+ )
+
+ def handle(self, *args, **options):
+ ''' Send visible and new web-push notifications
+ '''
+ since = options.get('since')
+ if since is None:
+ since = now() - timedelta(days=1)
+
+ verbosity = options.get('verbosity')
+ notif_query = Notification.objects.new().visible().filter(start_timestamp__gte=since)
+ for notif in notif_query:
+ if verbosity > 0:
+ print('Pushing notification %s' % str(notif).decode('utf-8'))
+ answer = [str(response_obj) for response_obj in send_web_push(notif, since=since)]
+ if verbosity > 0:
+ print('Web Push Responses : %s' % " | ".join(answer))
diff --git a/combo_plugin_gnm/migrations/0001_initial.py b/combo_plugin_gnm/migrations/0001_initial.py
new file mode 100644
index 0000000..dfed5b4
--- /dev/null
+++ b/combo_plugin_gnm/migrations/0001_initial.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('webpush', '0001_initial'),
+ ('notifications', '0004_auto_20180316_1026'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='WebPushRecord',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+ ('creation_date', models.DateTimeField(auto_now_add=True)),
+ ('status', models.CharField(default=b'NEW', max_length=4, blank=True, choices=[(b'NEW', b'Not-sent notification'), (b'SENT', b'Sent notification'), (b'ERR', b'Invalid notification')])),
+ ('notification', models.ForeignKey(to='notifications.Notification')),
+ ('subscription', models.ForeignKey(to='webpush.PushInformation')),
+ ],
+ ),
+ migrations.AlterUniqueTogether(
+ name='webpushrecord',
+ unique_together=set([('subscription', 'notification')]),
+ ),
+ ]
diff --git a/combo_plugin_gnm/migrations/__init__.py b/combo_plugin_gnm/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/combo_plugin_gnm/migrations/__init__.py
diff --git a/combo_plugin_gnm/models.py b/combo_plugin_gnm/models.py
new file mode 100644
index 0000000..4959b91
--- /dev/null
+++ b/combo_plugin_gnm/models.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# combo-plugin-gnm - Combo WebPush App
+# Copyright (C) 2018 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from django.db import models
+
+# TODO use signals to hook Notification model create or update ?
+from combo.apps.notifications.models import Notification
+from webpush.models import PushInformation
+
+
+class WebPushRecord(models.Model):
+
+ DEFAULT_STATUS = 'NEW'
+ ERR_STATUS = 'ERR'
+ OK_STATUS = 'SENT'
+ STATUS = (
+ (DEFAULT_STATUS, 'Not-sent notification'),
+ (OK_STATUS, 'Sent notification'),
+ (ERR_STATUS, 'Invalid notification'),
+ )
+
+ subscription = models.ForeignKey(PushInformation, null=False)
+ notification = models.ForeignKey(Notification, null=False)
+ creation_date = models.DateTimeField(blank=True, auto_now_add=True)
+ status = models.CharField(blank=True, max_length=4,
+ choices=STATUS, default=DEFAULT_STATUS)
+
+ class Meta:
+ unique_together = ('subscription', 'notification')
+
+ @property
+ def ttl(self):
+ delta = self.notification.end_timestamp - self.notification.start_timestamp
+ return int(delta.total_seconds())
+
+ @property
+ def payload(self):
+ '''
+ For example, we could extend later this data to such JSON supported by Chrome for Android
+ {
+ "body": "Did you make a $1,000,000 purchase at Dr. Evil...",
+ "icon": "images/ccard.png",
+ "vibrate": [200, 100, 200, 100, 200, 100, 400],
+ "tag": "request",
+ "actions": [
+ { "action": "yes", "title": "Yes", "icon": "images/yes.png" },
+ { "action": "no", "title": "No", "icon": "images/no.png" }
+ ]
+ }
+ '''
+ return {
+ 'head': self.notification.summary,
+ 'body': self.notification.body,
+ 'url': self.notification.url,
+ }
+
+ def set_status_ok(self):
+ self.status = self.OK_STATUS
+ self.save()
+
+ def set_status_err(self):
+ self.status = self.ERR_STATUS
+ self.save()
diff --git a/combo_plugin_gnm/push.py b/combo_plugin_gnm/push.py
new file mode 100644
index 0000000..32e6660
--- /dev/null
+++ b/combo_plugin_gnm/push.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# combo-plugin-gnm - Combo WebPush App
+# Copyright (C) 2018 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from django.conf import settings
+from django.db import transaction
+from django.utils.timezone import is_naive, make_aware
+
+from pywebpush import WebPushException
+from retrying import retry
+from webpush.utils import _send_notification
+
+from .models import WebPushRecord
+
+
+def retry_if_webpush_exception(exception):
+ '''Return True if we should retry'''
+ return isinstance(exception, WebPushException)
+
+
+@retry(wait_exponential_multiplier=1000,
+ wait_exponential_max=getattr(settings, 'WEBPUSH_BACKOFF_MAXMILLISECS', 10000),
+ retry_on_exception=retry_if_webpush_exception)
+def send_web_push(notification, since=None):
+ '''Get all the user subscriptions (every browsers with authoorized notifications)
+ and send a push mesg to each one
+ '''
+ if is_naive(since):
+ since = make_aware(since)
+ user_subscription_list = notification.user.webpush_info.select_related("subscription")
+ responses = []
+ for sub_info in user_subscription_list:
+ with transaction.atomic():
+ web_push_record, created = WebPushRecord.objects.select_for_update().get_or_create(
+ notification=notification,
+ subscription=sub_info)
+
+ if web_push_record.status == WebPushRecord.DEFAULT_STATUS and \
+ web_push_record.creation_date >= since:
+ # check the user's subscription and send the request to the endpoint
+ req = _send_notification(sub_info, web_push_record.payload, web_push_record.ttl)
+ # requests.Response object
+ if req.status_code <= 201:
+ web_push_record.status = web_push_record.OK_STATUS
+ web_push_record.save()
+ responses += [req]
+ else:
+ web_push_record.status = web_push_record.ERR_STATUS
+ web_push_record.save()
+
+ else:
+ web_push_record.set_status_err()
+
+ return responses
diff --git a/combo_plugin_gnm/static/combo_plugin_gnm/webpush.js b/combo_plugin_gnm/static/combo_plugin_gnm/webpush.js
new file mode 100644
index 0000000..1fcc56e
--- /dev/null
+++ b/combo_plugin_gnm/static/combo_plugin_gnm/webpush.js
@@ -0,0 +1,238 @@
+// Based On https://raw.githubusercontent.com/safwanrahman/django-webpush/aed8d5213d76857bfb3a020c1c6b8af18cad0f12/webpush/static/webpush/webpush.js
+
+// combo-plugin-gnm - Combo GNM plugin
+// Copyright (C) 2018 Entr'ouvert
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published
+// by the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+var subBtn,
+ messageBox,
+ registration;
+
+var activateTextMessage = 'Activez les notifications';
+var stopTextMessage = 'Stoppez les notifications';
+var incompatibleMessage = 'Ce navigateur n&#39;est pas compatible avec les notifications push.';
+var browserShortName = navigator.userAgent.match(/(firefox|msie|chrome|safari|trident)/ig)[0].toLowerCase();
+
+
+$(window).load(function () {
+ subBtn = $('#webpush-subscribe-checkbox');
+ messageBox = $('#webpush-subscribe-message');
+
+ function set_btn_activate() {
+ subBtn.attr('disabled', false);
+ subBtn.attr('checked', false);
+ subBtn.removeClass("checked");
+ messageBox.text(activateTextMessage);
+ }
+
+ function set_btn_cancel() {
+ subBtn.attr('disabled', false);
+ subBtn.attr('checked', true);
+ subBtn.addClass("checked");
+ messageBox.text(stopTextMessage);
+ }
+
+ var user_subscription_browser_list = subBtn.data('userSubscriptionBrowserList').split("=#=#");
+ if (user_subscription_browser_list.indexOf(browserShortName) >= 0) {
+ set_btn_cancel();
+ } else {
+ set_btn_activate();
+ }
+
+ // show warning to the message box and disabled the input
+ if (!('serviceWorker' in navigator)) {
+ messageBox.text(incompatibleMessage);
+ subBtn.attr('checked', false);
+ subBtn.attr('disabled', true);
+ return;
+ }
+
+ subBtn.click(
+ function (evt) {
+ subBtn.attr('disabled', true);
+ // if the Browser Supports Service Worker
+ // registered the worker and the click event hanlder
+ if ('serviceWorker' in navigator) {
+ var serviceWorker = document.getElementById('service-worker-js').src;
+ navigator.serviceWorker.register(serviceWorker)
+ .then(
+ function (reg) {
+ messageBox.text('Connexion au serveur en cours...');
+ registration = reg;
+ initialiseState(reg);
+ }
+ );
+ }
+ }
+ );
+
+
+ // Once the service worker is registered set the initial state
+ function initialiseState(reg) {
+ // Check if PushManager, Notification is supported in the browser
+ if (!('PushManager' in window) || !(reg.showNotification)) {
+ messageBox.text(incompatibleMessage);
+ subBtn.attr('checked', false);
+ subBtn.attr('disabled', true);
+ return;
+ }
+ // Check the current Notification permission.
+ // If its denied, it's a permanent block until the
+ // user changes the permission
+ if (Notification.permission === 'denied') {
+ // Show a message and activate the button
+ set_btn_activate();
+ return;
+ }
+ if (subBtn.filter(':checked')
+ .length > 0) {
+ return subscribe(reg);
+ } else {
+ return unsubscribe(reg);
+ }
+ }
+
+
+ function subscribe(reg) {
+ // Get the Subscription or register one
+ getSubscription(reg)
+ .then(
+ function (subscription) {
+ postSubscribeObj('subscribe', subscription);
+ }
+ )
+ .catch(
+ function (error) {
+ messageBox.text('Impossible de communiquer avec le serveur, veuillez r&eacute;essayer dans quelques minutes (Debug = '+ error +')');
+ }
+ );
+ }
+
+ function urlB64ToUint8Array(base64String) {
+
+ var b64padding = '='.repeat((4 - base64String.length % 4) % 4);
+ var base64 = (base64String + b64padding)
+ .replace(/\-/g, '+')
+ .replace(/_/g, '/');
+
+ var rawData = window.atob(base64);
+ var outputArray = new Uint8Array(rawData.length);
+
+ for (var i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+ return outputArray;
+ }
+
+ function getSubscription(reg) {
+ return reg.pushManager.getSubscription()
+ .then(
+ function (subscription) {
+ var metaObj, applicationServerKey, options;
+ // Check if Subscription is available
+ if (subscription) {
+ return subscription;
+ }
+
+ metaObj = document.querySelector('meta[name="django-webpush-vapid-key"]');
+ applicationServerKey = metaObj.content;
+ options = {
+ userVisibleOnly: true
+ };
+ if (applicationServerKey) {
+ options.applicationServerKey = urlB64ToUint8Array(applicationServerKey)
+ }
+ // If not, register one
+ return registration.pushManager.subscribe(options)
+ }
+ )
+ }
+
+ function unsubscribe() {
+ // Get the Subscription to unregister
+ registration.pushManager.getSubscription()
+ .then(
+ function (subscription) {
+
+ // Check we have a subscription to unsubscribe
+ if (!subscription) {
+ // No subscription object, so set the state
+ // to allow the user to subscribe to push
+ set_btn_activate();
+ return;
+ }
+ postSubscribeObj('unsubscribe', subscription);
+ }
+ )
+ }
+
+ function postSubscribeObj(statusType, subscription) {
+ // Send the information to the server with fetch API.
+ // the type of the request, the name of the user subscribing,
+ // and the push subscription endpoint + key the server needs
+ // to send push messages
+
+ // Each subscription is different for each of these navigator userAgent short name
+ var data = {
+ status_type: statusType,
+ subscription: subscription.toJSON(),
+ browser: browserShortName
+ // group: subBtn.dataset.group
+ };
+
+ fetch(subBtn.data('url'), {
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(data),
+ credentials: 'include'
+ })
+ .then(
+ function (response) {
+ // Check the information is saved successfully into server
+ if ((response.status == 201) && (statusType == 'subscribe')) {
+ // Show unsubscribe button instead
+ set_btn_cancel();
+ }
+
+ // Check if the information is deleted from server
+ if ((response.status == 202) && (statusType == 'unsubscribe')) {
+ // Get the Subscription
+ getSubscription(registration)
+ .then(
+ function (subscription) {
+ // Remove the subscription
+ subscription.unsubscribe()
+ .then(
+ function () {
+ set_btn_activate();
+ }
+ )
+ }
+ )
+ .catch(
+ function (error) {
+ subBtn.attr('disabled', false);
+ subBtn.attr('checked', false);
+ subBtn.removeClass("checked");
+ messageBox.text('Erreur lors de la requête, veuillez r&eacute;essayer dans quelques minutes (Debug = '+ error +')');
+ }
+ );
+ }
+ }
+ )
+ }
+});
diff --git a/combo_plugin_gnm/templates/webpush_checkbox.html b/combo_plugin_gnm/templates/webpush_checkbox.html
new file mode 100644
index 0000000..0bcc6e5
--- /dev/null
+++ b/combo_plugin_gnm/templates/webpush_checkbox.html
@@ -0,0 +1,16 @@
+{% load i18n static gnm gnm_notifications %}
+
+
+<div class="notification-dialog">
+{% if request.user.is_authenticated or group %}
+ {% webpush_scripts %}
+ <label class="checkbox">
+ <input id="webpush-subscribe-checkbox" class="checkbox_input" type="checkbox"
+ {% if group %}data-group="{{ group }}"{% endif %} data-url="{{ url }}"
+ data-user-subscription-browser-list="{{ user_subscription_browser_list|join:'=#=#'}}">
+ <p id="webpush-subscribe-message" class="checkbox_text"></p>
+ </label>
+{% else %}
+ <label class="checkbox">{% trans "Login to receive push notifications" %}</label>
+{% endif %}
+</div>
diff --git a/combo_plugin_gnm/templates/webpush_scripts.html b/combo_plugin_gnm/templates/webpush_scripts.html
new file mode 100644
index 0000000..ddc39ff
--- /dev/null
+++ b/combo_plugin_gnm/templates/webpush_scripts.html
@@ -0,0 +1,4 @@
+{% load static %}
+
+<script id="service-worker-js" type="text/javascript" src="{{site_base}}{% static 'webpush/webpush_serviceworker.js' %}?{{statics_hash}}"></script>
+<script type="text/javascript" src="{{site_base}}{% static 'combo_plugin_gnm/webpush.js' %}?{{statics_hash}}"></script>
diff --git a/combo_plugin_gnm/templatetags/gnm_notifications.py b/combo_plugin_gnm/templatetags/gnm_notifications.py
new file mode 100644
index 0000000..b7512b5
--- /dev/null
+++ b/combo_plugin_gnm/templatetags/gnm_notifications.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# combo-plugin-gnm - Combo GNM plugin
+# Copyright (C) 2018 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from django import template
+from django.conf import settings
+from django.core.urlresolvers import reverse
+
+from webpush.models import PushInformation
+
+
+register = template.Library()
+
+
+@register.inclusion_tag('webpush_checkbox.html', takes_context=True)
+def webpush_checkbox(context):
+ group = context.get('webpush', {}).get('group')
+ url = reverse('save_webpush_info')
+ request = context['request']
+ user_subscription_browser_list = [info.subscription.browser for info in PushInformation.objects.filter(user=request.user)]
+ print user_subscription_browser_list
+ return {
+ 'group': group,
+ 'url': url,
+ 'request': request,
+ 'user_subscription_browser_list': user_subscription_browser_list
+ }
+
+
+@register.inclusion_tag('webpush_scripts.html', takes_context=True)
+def webpush_scripts(context):
+ return
+
+
+@register.assignment_tag(takes_context=True)
+def get_webpush_vars(context):
+ vapid_public_key = getattr(settings, 'WEBPUSH_SETTINGS', {}).get('VAPID_PUBLIC_KEY', '')
+ return {'vapid_public_key': vapid_public_key}
diff --git a/combo_plugin_gnm/urls.py b/combo_plugin_gnm/urls.py
index 2041cda..14b3432 100644
--- a/combo_plugin_gnm/urls.py
+++ b/combo_plugin_gnm/urls.py
@@ -15,10 +15,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from django.conf.urls import url
+from django.conf.urls import url, include
from . import views
+
urlpatterns = [
url(r'^gnm/plusone/$', views.plusone, name='gnm-plus-one'),
+ url(r'^gnm/webpush/backend/', include('webpush.urls')),
]
diff --git a/combo_plugin_gnm/views.py b/combo_plugin_gnm/views.py
index 959c9b4..a6763e2 100644
--- a/combo_plugin_gnm/views.py
+++ b/combo_plugin_gnm/views.py
@@ -22,6 +22,7 @@ from django.template import RequestContext
from combo.utils import requests, get_templated_url
+
def plusone(request, *args, **kwargs):
# add reference to a jsondatastore with slug "plus1"
reference = request.GET.get('ref')
diff --git a/debian/50gnm.py b/debian/50gnm.py
index 63681eb..6193724 100644
--- a/debian/50gnm.py
+++ b/debian/50gnm.py
@@ -279,3 +279,6 @@ JSON_CELL_TYPES = {
import memcache
memcache.SERVER_MAX_VALUE_LENGTH = 10 * 1024 * 1024
+
+# Exponential backoff maximum milliseconds spent retrying
+WEBPUSH_BACKOFF_MAXMILLISECS = 10000
diff --git a/setup.py b/setup.py
index 803110c..c9e0653 100644
--- a/setup.py
+++ b/setup.py
@@ -97,6 +97,11 @@ setup(
install_requires=[
'django>=1.8, <1.12',
'python-dateutil',
+ 'retrying',
+ 'django-webpush>=0.2.2.3'
+ ],
+ dependency_links=[
+ 'https://github.com/elishowk/django-webpush/tarball/master#egg=django-webpush-0.2.2.3',
],
zip_safe=False,
entry_points={
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..6a58cb4
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,10 @@
+import pytest
+
+import django_webtest
+
+@pytest.fixture
+def app(request):
+ wtm = django_webtest.WebTestMixin()
+ wtm._patch_settings()
+ request.addfinalizer(wtm._unpatch_settings)
+ return django_webtest.DjangoTestApp()
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644
index 0000000..7f2c953
--- /dev/null
+++ b/tests/settings.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+USE_TZ = True
diff --git a/tests/test_webpush.py b/tests/test_webpush.py
new file mode 100644
index 0000000..82fa888
--- /dev/null
+++ b/tests/test_webpush.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+# combo-plugin-gnm - Combo WebPush App
+# Copyright (C) 2018 Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import base64
+from datetime import datetime
+
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.backends import default_backend
+
+import pytest
+from mock import patch
+
+from django.contrib.auth.models import User
+from django.core.management import call_command
+from django.utils.timezone import make_aware
+
+from webpush.models import SubscriptionInfo, PushInformation
+
+from combo.apps.notifications.models import Notification
+import combo_plugin_gnm
+from combo_plugin_gnm.models import WebPushRecord
+
+
+pytestmark = pytest.mark.django_db
+
+
+@pytest.fixture
+def user1():
+ user1 = User.objects.create_user('user1', email='user1@example.net', password='user1')
+ user1.save()
+ return user1
+
+
+def _get_pubkey_str(priv_key):
+ '''From pywebpush tests'''
+ return base64.urlsafe_b64encode(
+ priv_key.public_key().public_numbers().encode_point()
+ ).strip(b'=')
+
+
+def _gen_subscription_info(recv_key=None,
+ endpoint="https://example.com/"):
+ '''From pywebpush tests'''
+ if not recv_key:
+ recv_key = ec.generate_private_key(ec.SECP256R1, default_backend())
+ return {
+ "endpoint": endpoint,
+ "keys": {
+ 'auth': base64.urlsafe_b64encode(os.urandom(16)).strip(b'='),
+ 'p256dh': _get_pubkey_str(recv_key),
+ }
+ }
+
+
+@patch('pywebpush.WebPusher.send')
+def test_command_send_webpush(mock_send, user1):
+ mock_send.return_value.status_code = 201
+ recv_key = ec.generate_private_key(ec.SECP256R1, default_backend())
+ subscription_keys_dict = _gen_subscription_info(recv_key)
+ subscription_record = SubscriptionInfo.objects.create(
+ browser='test browser',
+ endpoint=subscription_keys_dict['endpoint'],
+ auth=subscription_keys_dict['keys']['auth'],
+ p256dh=subscription_keys_dict['keys']['p256dh'],
+ )
+ push_info = PushInformation.objects.create(
+ user=user1,
+ subscription=subscription_record
+ )
+ notification = Notification.notify(user1, u'tèst headér', body=u'test uniçode bodéï')
+ call_command('send_webpush', '--verbosity=1')
+ assert WebPushRecord.objects.filter(status=WebPushRecord.ERR_STATUS).count() == 0
+ assert WebPushRecord.objects.filter(status=WebPushRecord.DEFAULT_STATUS).count() == 0
+ ok_push = WebPushRecord.objects.filter(status=WebPushRecord.OK_STATUS)
+ assert ok_push.count() == 1
+ ok_push = ok_push.first()
+ assert ok_push.subscription == push_info
+ assert ok_push.notification == notification
+ assert ok_push.creation_date < make_aware(datetime.now())