pwa: add option to enable support for push notifications (#31388)

This commit is contained in:
Frédéric Péters 2019-06-30 09:43:16 +02:00
parent 9784211b5a
commit 73648052ce
11 changed files with 136 additions and 21 deletions

View File

@ -27,6 +27,8 @@ from django.db.models.query import QuerySet
from combo.data.models import CellBase
from combo.data.library import register_cell_class
from combo.apps.pwa.models import PwaSettings
class NotificationQuerySet(QuerySet):
def namespace(self, namespace):
@ -181,6 +183,8 @@ class NotificationsCell(CellBase):
qs = Notification.objects.visible(user)
extra_context['notifications'] = qs
extra_context['new_notifications'] = qs.new()
pwa_settings = PwaSettings.singleton()
extra_context['push_notifications_enabled'] = pwa_settings.push_notifications
return extra_context
def get_badge(self, context):

View File

@ -20,4 +20,37 @@
<p>{% trans 'No notifications.' %}</p>
</div>
{% endif %}
{% if push_notifications_enabled %}
<div class="notification-buttons">
<div class="notification-push-on" style="display: none"><a href="#" class="pk-button">Activer les notifications</a></div>
<div class="notification-push-off" style="display: none"><a href="#" class="pk-button">Désactiver les notifications</a></div>
</div>
<script>
$(function() {
$('.notification-push-on a').on('click', function() {
$('.notification-push-on').hide();
$('.notification-push-off').hide();
combo_pwa_subscribe_user();
return false;
});
$('.notification-push-off a').on('click', function() {
$('.notification-push-on').hide();
$('.notification-push-off').hide();
combo_pwa_unsubscribe_user();
return false;
});
$(document).on('combo:pwa-user-info', function() {
if (COMBO_PWA_USER_SUBSCRIPTION) {
$('.notification-push-off').show();
} else {
$('.notification-push-on').show();
}
});
}
);
</script>
{% endif %}
{% endblock %}

View File

@ -24,12 +24,13 @@ from django.views.generic import CreateView, UpdateView, DeleteView
from combo.data.forms import get_page_choices
from .models import PwaSettings, PwaNavigationEntry
from .forms import PwaSettingsForm
class ManagerHomeView(UpdateView):
template_name = 'combo/pwa/manager_home.html'
model = PwaSettings
fields = '__all__'
form_class = PwaSettingsForm
success_url = reverse_lazy('pwa-manager-homepage')
def get_initial(self):

View File

@ -29,6 +29,8 @@ from django.utils.encoding import force_text, force_bytes
from django.utils.six import BytesIO
from django.utils.translation import ugettext_lazy as _
from py_vapid import Vapid
from jsonfield import JSONField
from combo.data.fields import RichTextField
from combo import utils
@ -51,8 +53,24 @@ class PwaSettings(models.Model):
default=_('You are currently offline.'),
config_name='small')
offline_retry_button = models.BooleanField(_('Include Retry Button'), default=True)
push_notifications = models.BooleanField(
verbose_name=_('Enable subscription to push notifications'),
default=False)
push_notifications_infos = JSONField(blank=True)
last_update_timestamp = models.DateTimeField(auto_now=True)
def save(self, **kwargs):
if self.push_notifications and not self.push_notifications_infos:
# generate VAPID keys
vapid = Vapid()
vapid.generate_keys()
self.push_notifications_infos = {
'private_key': vapid.private_pem(),
}
elif not self.push_notifications:
self.push_notifications_infos = {}
return super(PwaSettings, self).save(**kwargs)
@classmethod
def singleton(cls):
return cls.objects.first() or cls()

View File

@ -21,22 +21,29 @@ 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 py_vapid import Vapid
import pywebpush
from combo.apps.notifications.models import Notification
from .models import PushSubscription
from .models import PushSubscription, PwaSettings
@receiver(post_save, sender=Notification)
def notification(sender, instance=None, created=False, **kwargs):
if not pywebpush:
return
if not created:
return
pwa_settings = PwaSettings.singleton()
if not pwa_settings.push_notifications:
return
if settings.PWA_VAPID_PRIVATE_KEY: # legacy
pwa_vapid_private_key = settings.PWA_VAPID_PRIVATE_KEY
else:
pwa_vapid_private_key = Vapid.from_pem(pwa_settings.push_notifications_infos['private_key'].encode('ascii'))
if settings.PWA_VAPID_CLAIMS: # legacy
claims = settings.PWA_VAPID_CLAIMS
else:
claims = {'sub': 'mailto:%s' % settings.DEFAULT_FROM_EMAIL}
message = json.dumps({
'summary': instance.summary,
'body': instance.body,
@ -48,8 +55,8 @@ def notification(sender, instance=None, created=False, **kwargs):
pywebpush.webpush(
subscription_info=subscription.subscription_info,
data=message,
vapid_private_key=settings.PWA_VAPID_PRIVATE_KEY,
vapid_claims=settings.PWA_VAPID_CLAIMS
vapid_private_key=pwa_vapid_private_key,
vapid_claims=claims,
)
except pywebpush.WebPushException as e:
logger = logging.getLogger(__name__)

View File

@ -1,4 +1,6 @@
var applicationServerPublicKey = {% if pwa_vapid_publik_key %}'{{ pwa_vapid_publik_key }}'{% else %}null{% endif %};
{% load combo %}
var applicationServerPublicKey = {{ pwa_vapid_public_key|as_json|safe }};
var COMBO_PWA_USER_SUBSCRIPTION = false;
function urlB64ToUint8Array(base64String) {

View File

@ -3,7 +3,7 @@
/* global self, caches, fetch, URL, Response */
'use strict';
const applicationServerPublicKey = {{ pwa_vapid_publik_key|as_json|safe }};
const applicationServerPublicKey = {{ pwa_vapid_public_key|as_json|safe }};
function urlB64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);

View File

@ -14,6 +14,7 @@
# 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 base64
import json
from django.conf import settings
@ -23,6 +24,9 @@ from django.template.loader import get_template, TemplateDoesNotExist
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from cryptography.hazmat.primitives import serialization
from py_vapid import Vapid
from .models import PushSubscription, PwaSettings
from combo import VERSION
@ -40,8 +44,19 @@ def manifest_json(request, *args, **kwargs):
def js_response(request, template_name, **kwargs):
template = get_template(template_name)
pwa_vapid_public_key = None
pwa_settings = PwaSettings.singleton()
if pwa_settings.push_notifications:
if settings.PWA_VAPID_PUBLIK_KEY: # legacy
pwa_vapid_public_key = settings.PWA_VAPID_PUBLIK_KEY
else:
pwa_vapid_public_key = base64.urlsafe_b64encode(
Vapid.from_pem(pwa_settings.push_notifications_infos['private_key'].encode('ascii')
).private_key.public_key().public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint)).strip('=')
context = {
'pwa_vapid_publik_key': settings.PWA_VAPID_PUBLIK_KEY,
'pwa_vapid_public_key': pwa_vapid_public_key,
'pwa_notification_badge_url': settings.PWA_NOTIFICATION_BADGE_URL,
'pwa_notification_icon_url': settings.PWA_NOTIFICATION_ICON_URL,
}

5
debian/control vendored
View File

@ -23,8 +23,9 @@ Depends: ${misc:Depends}, ${python:Depends},
python-eopayment (>= 1.35),
python-django-haystack (>= 2.4.0),
python-sorl-thumbnail,
python-pil
Recommends: python-django-mellon, python-whoosh, python-pywebpush
python-pil,
python-pywebpush
Recommends: python-django-mellon, python-whoosh
Conflicts: python-lingo
Description: Portal Management System (Python module)

View File

@ -165,6 +165,7 @@ setup(
'sorl-thumbnail',
'Pillow',
'pyproj',
'pywebpush',
],
zip_safe=False,
cmdclass={

View File

@ -5,10 +5,7 @@ import mock
import pytest
from webtest import Upload
try:
import pywebpush
except ImportError:
pywebpush = None
import pywebpush
from django.conf import settings
from django.core.files import File
@ -34,7 +31,23 @@ def test_manifest_json(app):
def test_service_worker(app):
app.get('/service-worker.js', status=200)
app.get('/service-worker-registration.js', status=200)
resp = app.get('/service-worker-registration.js', status=200)
assert 'applicationServerPublicKey = null' in resp.text
pwa_settings = PwaSettings.singleton()
pwa_settings.push_notifications = True
pwa_settings.save()
resp = app.get('/service-worker-registration.js', status=200)
assert 'applicationServerPublicKey = "' in resp.text
# check legacy settings are still supported
with override_settings(
PWA_VAPID_PUBLIK_KEY="BFzvUdXB...",
PWA_VAPID_PRIVATE_KEY="4WbCnBF...",
PWA_VAPID_CLAIMS = {'sub': 'mailto:admin@entrouvert.com'}):
resp = app.get('/service-worker-registration.js', status=200)
assert 'applicationServerPublicKey = "BFzvUdXB..."' in resp.text
def test_webpush_subscription(app, john_doe, jane_doe):
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=403)
@ -53,17 +66,37 @@ def test_webpush_subscription(app, john_doe, jane_doe):
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)
pwa_settings = PwaSettings.singleton()
pwa_settings.push_notifications = False
pwa_settings.save()
with mock.patch('pywebpush.webpush') as webpush:
notification = Notification.notify(john_doe, 'test', body='hello world')
assert webpush.call_count == 0
pwa_settings.push_notifications = True
pwa_settings.save()
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'}
# check legacy settings are still supported
with override_settings(
PWA_VAPID_PUBLIK_KEY="BFzvUdXB...",
PWA_VAPID_PRIVATE_KEY="4WbCnBF...",
PWA_VAPID_CLAIMS = {'sub': 'mailto:admin@entrouvert.com'}):
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'}
assert webpush.call_args[1]['vapid_private_key'] == settings.PWA_VAPID_PRIVATE_KEY
assert webpush.call_args[1]['vapid_claims'] == settings.PWA_VAPID_CLAIMS
def test_no_pwa_manager(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)