web push notifications : added new templatetag, javascript static, model, a mgmt command and tests (#22727)
This commit is contained in:
parent
266ede325f
commit
3efc1a387d
|
@ -20,7 +20,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
class Plugin(object):
|
class Plugin(object):
|
||||||
def get_apps(self):
|
def get_apps(self):
|
||||||
return [__name__]
|
return [__name__, 'webpush']
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(django.apps.AppConfig):
|
class AppConfig(django.apps.AppConfig):
|
||||||
|
|
|
@ -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))
|
|
@ -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')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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()
|
|
@ -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
|
|
@ -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'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é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éessayer dans quelques minutes (Debug = '+ error +')');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}
|
|
@ -15,10 +15,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^gnm/plusone/$', views.plusone, name='gnm-plus-one'),
|
url(r'^gnm/plusone/$', views.plusone, name='gnm-plus-one'),
|
||||||
|
url(r'^gnm/webpush/backend/', include('webpush.urls')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -22,6 +22,7 @@ from django.template import RequestContext
|
||||||
|
|
||||||
from combo.utils import requests, get_templated_url
|
from combo.utils import requests, get_templated_url
|
||||||
|
|
||||||
|
|
||||||
def plusone(request, *args, **kwargs):
|
def plusone(request, *args, **kwargs):
|
||||||
# add reference to a jsondatastore with slug "plus1"
|
# add reference to a jsondatastore with slug "plus1"
|
||||||
reference = request.GET.get('ref')
|
reference = request.GET.get('ref')
|
||||||
|
|
|
@ -279,3 +279,6 @@ JSON_CELL_TYPES = {
|
||||||
|
|
||||||
import memcache
|
import memcache
|
||||||
memcache.SERVER_MAX_VALUE_LENGTH = 10 * 1024 * 1024
|
memcache.SERVER_MAX_VALUE_LENGTH = 10 * 1024 * 1024
|
||||||
|
|
||||||
|
# Exponential backoff maximum milliseconds spent retrying
|
||||||
|
WEBPUSH_BACKOFF_MAXMILLISECS = 10000
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -97,6 +97,11 @@ setup(
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'django>=1.8, <1.12',
|
'django>=1.8, <1.12',
|
||||||
'python-dateutil',
|
'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,
|
zip_safe=False,
|
||||||
entry_points={
|
entry_points={
|
||||||
|
|
|
@ -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()
|
|
@ -0,0 +1,2 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
USE_TZ = True
|
|
@ -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())
|
Loading…
Reference in New Issue