pwa: add option to enable support for push notifications (#31388)
This commit is contained in:
parent
9784211b5a
commit
73648052ce
|
@ -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):
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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__)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
1
setup.py
1
setup.py
|
@ -165,6 +165,7 @@ setup(
|
|||
'sorl-thumbnail',
|
||||
'Pillow',
|
||||
'pyproj',
|
||||
'pywebpush',
|
||||
],
|
||||
zip_safe=False,
|
||||
cmdclass={
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue