pwa: add basic support for push notifications (#25462)
This commit is contained in:
parent
88baf45c71
commit
e039b655e1
|
@ -19,6 +19,9 @@ import django.apps
|
||||||
class AppConfig(django.apps.AppConfig):
|
class AppConfig(django.apps.AppConfig):
|
||||||
name = 'combo.apps.pwa'
|
name = 'combo.apps.pwa'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from . import signals
|
||||||
|
|
||||||
def get_before_urls(self):
|
def get_before_urls(self):
|
||||||
from . import urls
|
from . import urls
|
||||||
return urls.urlpatterns
|
return urls.urlpatterns
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.12 on 2018-11-29 09:45
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import jsonfield.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PushSubscription',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('subscription_info', jsonfield.fields.JSONField(default=dict)),
|
||||||
|
('creation_timestamp', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,26 @@
|
||||||
|
# combo - content management system
|
||||||
|
# Copyright (C) 2015-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 models
|
||||||
|
|
||||||
|
from jsonfield import JSONField
|
||||||
|
|
||||||
|
|
||||||
|
class PushSubscription(models.Model):
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
subscription_info = JSONField()
|
||||||
|
creation_timestamp = models.DateTimeField(auto_now_add=True)
|
|
@ -0,0 +1,56 @@
|
||||||
|
# combo - content management system
|
||||||
|
# Copyright (C) 2015-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 json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pywebpush
|
||||||
|
except ImportError:
|
||||||
|
pywebpush = None
|
||||||
|
|
||||||
|
from combo.apps.notifications.models import Notification
|
||||||
|
|
||||||
|
from .models import PushSubscription
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Notification)
|
||||||
|
def notification(sender, instance=None, created=False, **kwargs):
|
||||||
|
if not pywebpush:
|
||||||
|
return
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
message = json.dumps({
|
||||||
|
'summary': instance.summary,
|
||||||
|
'body': instance.body,
|
||||||
|
'url': instance.url,
|
||||||
|
})
|
||||||
|
|
||||||
|
for subscription in PushSubscription.objects.filter(user_id=instance.user_id):
|
||||||
|
try:
|
||||||
|
pywebpush.webpush(
|
||||||
|
subscription_info=subscription.subscription_info,
|
||||||
|
data=message,
|
||||||
|
vapid_private_key=settings.PWA_VAPID_PRIVATE_KEY,
|
||||||
|
vapid_claims=settings.PWA_VAPID_CLAIMS
|
||||||
|
)
|
||||||
|
except pywebpush.WebPushException as e:
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.exception('webpush error (%r)', e)
|
|
@ -1,10 +1,90 @@
|
||||||
|
var applicationServerPublicKey = {% if pwa_vapid_publik_key %}'{{ pwa_vapid_publik_key }}'{% else %}null{% endif %};
|
||||||
|
var COMBO_PWA_USER_SUBSCRIPTION = false;
|
||||||
|
|
||||||
|
function urlB64ToUint8Array(base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function(registration) {
|
navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function(registration) {
|
||||||
// Registration was successful
|
// Registration was successful
|
||||||
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
||||||
swRegistration = registration;
|
swRegistration = registration;
|
||||||
|
combo_pwa_initialize();
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
// registration failed :(
|
// registration failed :(
|
||||||
console.log('ServiceWorker registration failed: ', err);
|
console.log('ServiceWorker registration failed: ', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function combo_pwa_initialize() {
|
||||||
|
if (applicationServerPublicKey !== null) {
|
||||||
|
swRegistration.pushManager.getSubscription()
|
||||||
|
.then(function(subscription) {
|
||||||
|
if (subscription !== null) {
|
||||||
|
COMBO_PWA_USER_SUBSCRIPTION = true;
|
||||||
|
} else {
|
||||||
|
COMBO_PWA_USER_SUBSCRIPTION = false;
|
||||||
|
}
|
||||||
|
$(document).trigger('combo:pwa-user-info');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function combo_pwa_subscribe_user() {
|
||||||
|
const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
|
||||||
|
swRegistration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: applicationServerKey
|
||||||
|
})
|
||||||
|
.then(function(subscription) {
|
||||||
|
console.log('User is subscribed.');
|
||||||
|
COMBO_PWA_USER_SUBSCRIPTION = true;
|
||||||
|
$(document).trigger('combo:pwa-user-info');
|
||||||
|
combo_pwa_update_subscription_on_server(subscription);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.log('Failed to subscribe the user: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function combo_pwa_unsubscribe_user() {
|
||||||
|
swRegistration.pushManager.getSubscription()
|
||||||
|
.then(function(subscription) {
|
||||||
|
if (subscription) {
|
||||||
|
return subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.log('Error unsubscribing', error);
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
combo_pwa_update_subscription_on_server(null);
|
||||||
|
console.log('User is unsubscribed.');
|
||||||
|
COMBO_PWA_USER_SUBSCRIPTION = false;
|
||||||
|
$(document).trigger('combo:pwa-user-info');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function combo_pwa_update_subscription_on_server(subscription) {
|
||||||
|
$.ajax({
|
||||||
|
url: '{% url "pwa-subscribe-push" %}',
|
||||||
|
data: JSON.stringify(subscription),
|
||||||
|
contentType: 'application/json; charset=utf-8',
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,27 @@
|
||||||
{% load gadjo %}
|
{% load combo gadjo %}
|
||||||
|
|
||||||
/* global self, caches, fetch, URL, Response */
|
/* global self, caches, fetch, URL, Response */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const applicationServerPublicKey = {{ pwa_vapid_publik_key|as_json|safe }};
|
||||||
|
|
||||||
|
function urlB64ToUint8Array(base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/\-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
|
||||||
|
const rawData = window.atob(base64);
|
||||||
|
const outputArray = new Uint8Array(rawData.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
|
outputArray[i] = rawData.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return outputArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var config = {
|
var config = {
|
||||||
version: 'v{% start_timestamp %}',
|
version: 'v{% start_timestamp %}',
|
||||||
staticCacheItems: [
|
staticCacheItems: [
|
||||||
|
@ -115,3 +135,44 @@ self.addEventListener('fetch', event => {
|
||||||
onFetch(event, config);
|
onFetch(event, config);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.addEventListener('push', event => {
|
||||||
|
console.log('[Service Worker] Push Received.');
|
||||||
|
console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);
|
||||||
|
var message = JSON.parse(event.data.text());
|
||||||
|
|
||||||
|
const title = message.summary;
|
||||||
|
const options = {
|
||||||
|
body: message.body,
|
||||||
|
data: {"url": message.url},
|
||||||
|
badge: {{ pwa_notification_badge_url|as_json|safe }},
|
||||||
|
icon: {{ pwa_notification_icon_url|as_json|safe }}
|
||||||
|
};
|
||||||
|
|
||||||
|
event.waitUntil(self.registration.showNotification(title, options));
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', function(event) {
|
||||||
|
var url = event.notification.data.url;
|
||||||
|
event.notification.close();
|
||||||
|
if (url) {
|
||||||
|
event.waitUntil(
|
||||||
|
clients.openWindow(url)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('pushsubscriptionchange', event => {
|
||||||
|
console.log('[Service Worker]: \'pushsubscriptionchange\' event fired.');
|
||||||
|
const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
|
||||||
|
event.waitUntil(
|
||||||
|
self.registration.pushManager.subscribe({
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: applicationServerKey
|
||||||
|
})
|
||||||
|
.then(function(newSubscription) {
|
||||||
|
console.log('[Service Worker] New subscription: ', newSubscription);
|
||||||
|
combo_pwa_update_subscription_on_server(newSubscription);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -20,10 +20,12 @@ from .views import (
|
||||||
manifest_json,
|
manifest_json,
|
||||||
service_worker_js,
|
service_worker_js,
|
||||||
service_worker_registration_js,
|
service_worker_registration_js,
|
||||||
|
subscribe_push,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url('^manifest.json$', manifest_json),
|
url('^manifest.json$', manifest_json),
|
||||||
url('^service-worker.js$', service_worker_js),
|
url('^service-worker.js$', service_worker_js),
|
||||||
url('^service-worker-registration.js$', service_worker_registration_js),
|
url('^service-worker-registration.js$', service_worker_registration_js),
|
||||||
|
url('^api/pwa/push/subscribe$', subscribe_push, name='pwa-subscribe-push'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -14,8 +14,15 @@
|
||||||
# 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.http import HttpResponse, Http404
|
import json
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from django.http import HttpResponse, HttpResponseForbidden, Http404, JsonResponse
|
||||||
from django.template.loader import get_template, TemplateDoesNotExist
|
from django.template.loader import get_template, TemplateDoesNotExist
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
from .models import PushSubscription
|
||||||
|
|
||||||
|
|
||||||
def manifest_json(request, *args, **kwargs):
|
def manifest_json(request, *args, **kwargs):
|
||||||
|
@ -26,13 +33,37 @@ def manifest_json(request, *args, **kwargs):
|
||||||
return HttpResponse(template.render({}, request), content_type='application/json')
|
return HttpResponse(template.render({}, request), content_type='application/json')
|
||||||
|
|
||||||
|
|
||||||
def service_worker_js(request, *args, **kwargs):
|
def js_response(request, template_name):
|
||||||
template = get_template('combo/service-worker.js')
|
template = get_template(template_name)
|
||||||
return HttpResponse(template.render({}, request),
|
context = {
|
||||||
|
'pwa_vapid_publik_key': settings.PWA_VAPID_PUBLIK_KEY,
|
||||||
|
'pwa_notification_badge_url': settings.PWA_NOTIFICATION_BADGE_URL,
|
||||||
|
'pwa_notification_icon_url': settings.PWA_NOTIFICATION_ICON_URL,
|
||||||
|
}
|
||||||
|
return HttpResponse(template.render(context, request),
|
||||||
content_type='application/javascript; charset=utf-8')
|
content_type='application/javascript; charset=utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def service_worker_js(request, *args, **kwargs):
|
||||||
|
return js_response(request, 'combo/service-worker.js')
|
||||||
|
|
||||||
|
|
||||||
def service_worker_registration_js(request, *args, **kwargs):
|
def service_worker_registration_js(request, *args, **kwargs):
|
||||||
template = get_template('combo/service-worker-registration.js')
|
return js_response(request, 'combo/service-worker-registration.js')
|
||||||
return HttpResponse(template.render({}, request),
|
|
||||||
content_type='application/javascript; charset=utf-8')
|
|
||||||
|
@csrf_exempt
|
||||||
|
def subscribe_push(request, *args, **kwargs):
|
||||||
|
if not (request.user and request.user.is_authenticated()):
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
if request.method != 'POST':
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
subscription_data = json.loads(request.body)
|
||||||
|
if subscription_data is None:
|
||||||
|
PushSubscription.objects.filter(user=request.user).delete()
|
||||||
|
else:
|
||||||
|
subscription, created = PushSubscription.objects.get_or_create(
|
||||||
|
user=request.user,
|
||||||
|
subscription_info=subscription_data)
|
||||||
|
subscription.save()
|
||||||
|
return JsonResponse({'err': 0})
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
@ -236,6 +237,10 @@ def is_empty_placeholder(page, placeholder_name):
|
||||||
def as_list(obj):
|
def as_list(obj):
|
||||||
return list(obj)
|
return list(obj)
|
||||||
|
|
||||||
|
@register.filter(name='as_json')
|
||||||
|
def as_json(obj):
|
||||||
|
return json.dumps(obj)
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def signed(obj):
|
def signed(obj):
|
||||||
return signing.dumps(obj)
|
return signing.dumps(obj)
|
||||||
|
|
|
@ -315,6 +315,14 @@ WCS_CATEGORY_ASSET_SLOTS = {
|
||||||
|
|
||||||
WCS_FORM_ASSET_SLOTS = {}
|
WCS_FORM_ASSET_SLOTS = {}
|
||||||
|
|
||||||
|
# PWA Settings
|
||||||
|
PWA_VAPID_PUBLIK_KEY = None
|
||||||
|
PWA_VAPID_PRIVATE_KEY = None
|
||||||
|
PWA_VAPID_CLAIMS = None
|
||||||
|
PWA_NOTIFICATION_BADGE_URL = None
|
||||||
|
PWA_NOTIFICATION_ICON_URL = None
|
||||||
|
|
||||||
|
|
||||||
# hide work-in-progress/experimental/broken/legacy/whatever cells for now
|
# hide work-in-progress/experimental/broken/legacy/whatever cells for now
|
||||||
BOOKING_CALENDAR_CELL_ENABLED = False
|
BOOKING_CALENDAR_CELL_ENABLED = False
|
||||||
NEWSLETTERS_CELL_ENABLED = False
|
NEWSLETTERS_CELL_ENABLED = False
|
||||||
|
|
|
@ -24,7 +24,7 @@ Depends: ${misc:Depends}, ${python:Depends},
|
||||||
python-django-haystack (>= 2.4.0),
|
python-django-haystack (>= 2.4.0),
|
||||||
python-sorl-thumbnail,
|
python-sorl-thumbnail,
|
||||||
python-pil
|
python-pil
|
||||||
Recommends: python-django-mellon, python-whoosh
|
Recommends: python-django-mellon, python-whoosh, python-pywebpush
|
||||||
Conflicts: python-lingo
|
Conflicts: python-lingo
|
||||||
Description: Portal Management System (Python module)
|
Description: Portal Management System (Python module)
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,25 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pywebpush
|
||||||
|
except ImportError:
|
||||||
|
pywebpush = None
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from combo.apps.notifications.models import Notification
|
||||||
|
from combo.apps.pwa.models import PushSubscription
|
||||||
|
|
||||||
|
from .test_manager import login
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_manifest_json(app):
|
def test_manifest_json(app):
|
||||||
app.get('/manifest.json', status=404)
|
app.get('/manifest.json', status=404)
|
||||||
|
|
||||||
|
@ -16,3 +30,32 @@ def test_manifest_json(app):
|
||||||
|
|
||||||
def test_service_worker(app):
|
def test_service_worker(app):
|
||||||
app.get('/service-worker.js', status=200)
|
app.get('/service-worker.js', status=200)
|
||||||
|
app.get('/service-worker-registration.js', status=200)
|
||||||
|
|
||||||
|
def test_webpush_subscription(app, john_doe, jane_doe):
|
||||||
|
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=403)
|
||||||
|
app.get(reverse('pwa-subscribe-push'), status=403)
|
||||||
|
app = login(app, john_doe.username, john_doe.username)
|
||||||
|
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=200)
|
||||||
|
assert PushSubscription.objects.count() == 1
|
||||||
|
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content2'}, status=200)
|
||||||
|
assert PushSubscription.objects.count() == 2
|
||||||
|
|
||||||
|
app = login(app, jane_doe.username, jane_doe.username)
|
||||||
|
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=200)
|
||||||
|
assert PushSubscription.objects.count() == 3
|
||||||
|
|
||||||
|
app = login(app, john_doe.username, john_doe.username)
|
||||||
|
app.post_json(reverse('pwa-subscribe-push'), params=None, status=200)
|
||||||
|
assert PushSubscription.objects.count() == 1
|
||||||
|
|
||||||
|
@pytest.mark.skipif('pywebpush is None')
|
||||||
|
def test_webpush_notification(app, john_doe):
|
||||||
|
PushSubscription.objects.all().delete()
|
||||||
|
app = login(app, john_doe.username, john_doe.username)
|
||||||
|
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=200)
|
||||||
|
|
||||||
|
with mock.patch('pywebpush.webpush') as webpush:
|
||||||
|
notification = Notification.notify(john_doe, 'test', body='hello world')
|
||||||
|
assert webpush.call_count == 1
|
||||||
|
assert webpush.call_args[1]['subscription_info'] == {'sample': 'content'}
|
||||||
|
|
Loading…
Reference in New Issue