0001-pwa-add-option-to-enable-support-for-push-notificati.patch
combo/apps/notifications/models.py | ||
---|---|---|
27 | 27 |
from combo.data.models import CellBase |
28 | 28 |
from combo.data.library import register_cell_class |
29 | 29 | |
30 |
from combo.apps.pwa.models import PwaSettings |
|
31 | ||
30 | 32 | |
31 | 33 |
class NotificationQuerySet(QuerySet): |
32 | 34 |
def namespace(self, namespace): |
... | ... | |
181 | 183 |
qs = Notification.objects.visible(user) |
182 | 184 |
extra_context['notifications'] = qs |
183 | 185 |
extra_context['new_notifications'] = qs.new() |
186 |
pwa_settings = PwaSettings.singleton() |
|
187 |
extra_context['push_notifications_enabled'] = pwa_settings.push_notifications |
|
184 | 188 |
return extra_context |
185 | 189 | |
186 | 190 |
def get_badge(self, context): |
combo/apps/notifications/templates/combo/notificationscell.html | ||
---|---|---|
20 | 20 |
<p>{% trans 'No notifications.' %}</p> |
21 | 21 |
</div> |
22 | 22 |
{% endif %} |
23 | ||
24 |
{% if push_notifications_enabled %} |
|
25 |
<div class="notification-buttons"> |
|
26 |
<div class="notification-push-on" style="display: none"><a href="#" class="pk-button">Activer les notifications</a></div> |
|
27 |
<div class="notification-push-off" style="display: none"><a href="#" class="pk-button">Désactiver les notifications</a></div> |
|
28 |
</div> |
|
29 | ||
30 |
<script> |
|
31 |
$(function() { |
|
32 |
$('.notification-push-on a').on('click', function() { |
|
33 |
$('.notification-push-on').hide(); |
|
34 |
$('.notification-push-off').hide(); |
|
35 |
combo_pwa_subscribe_user(); |
|
36 |
return false; |
|
37 |
}); |
|
38 |
$('.notification-push-off a').on('click', function() { |
|
39 |
$('.notification-push-on').hide(); |
|
40 |
$('.notification-push-off').hide(); |
|
41 |
combo_pwa_unsubscribe_user(); |
|
42 |
return false; |
|
43 |
}); |
|
44 |
$(document).on('combo:pwa-user-info', function() { |
|
45 |
if (COMBO_PWA_USER_SUBSCRIPTION) { |
|
46 |
$('.notification-push-off').show(); |
|
47 |
} else { |
|
48 |
$('.notification-push-on').show(); |
|
49 |
} |
|
50 |
}); |
|
51 |
} |
|
52 |
); |
|
53 |
</script> |
|
54 |
{% endif %} |
|
55 | ||
23 | 56 |
{% endblock %} |
combo/apps/pwa/manager_views.py | ||
---|---|---|
24 | 24 |
from combo.data.forms import get_page_choices |
25 | 25 | |
26 | 26 |
from .models import PwaSettings, PwaNavigationEntry |
27 |
from .forms import PwaSettingsForm |
|
27 | 28 | |
28 | 29 | |
29 | 30 |
class ManagerHomeView(UpdateView): |
30 | 31 |
template_name = 'combo/pwa/manager_home.html' |
31 | 32 |
model = PwaSettings |
32 |
fields = '__all__'
|
|
33 |
form_class = PwaSettingsForm
|
|
33 | 34 |
success_url = reverse_lazy('pwa-manager-homepage') |
34 | 35 | |
35 | 36 |
def get_initial(self): |
combo/apps/pwa/models.py | ||
---|---|---|
29 | 29 |
from django.utils.six import BytesIO |
30 | 30 |
from django.utils.translation import ugettext_lazy as _ |
31 | 31 | |
32 |
from py_vapid import Vapid |
|
33 | ||
32 | 34 |
from jsonfield import JSONField |
33 | 35 |
from combo.data.fields import RichTextField |
34 | 36 |
from combo import utils |
... | ... | |
51 | 53 |
default=_('You are currently offline.'), |
52 | 54 |
config_name='small') |
53 | 55 |
offline_retry_button = models.BooleanField(_('Include Retry Button'), default=True) |
56 |
push_notifications = models.BooleanField( |
|
57 |
verbose_name=_('Enable subscription to push notifications'), |
|
58 |
default=False) |
|
59 |
push_notifications_infos = JSONField(blank=True) |
|
54 | 60 |
last_update_timestamp = models.DateTimeField(auto_now=True) |
55 | 61 | |
62 |
def save(self, **kwargs): |
|
63 |
if self.push_notifications and not self.push_notifications_infos: |
|
64 |
# generate VAPID keys |
|
65 |
vapid = Vapid() |
|
66 |
vapid.generate_keys() |
|
67 |
self.push_notifications_infos = { |
|
68 |
'private_key': vapid.private_pem(), |
|
69 |
} |
|
70 |
return super(PwaSettings, self).save(**kwargs) |
|
71 | ||
56 | 72 |
@classmethod |
57 | 73 |
def singleton(cls): |
58 | 74 |
return cls.objects.first() or cls() |
combo/apps/pwa/signals.py | ||
---|---|---|
21 | 21 |
from django.db.models.signals import post_save |
22 | 22 |
from django.dispatch import receiver |
23 | 23 | |
24 |
try: |
|
25 |
import pywebpush |
|
26 |
except ImportError: |
|
27 |
pywebpush = None |
|
24 |
from py_vapid import Vapid |
|
25 |
import pywebpush |
|
28 | 26 | |
29 | 27 |
from combo.apps.notifications.models import Notification |
30 | 28 | |
31 |
from .models import PushSubscription |
|
29 |
from .models import PushSubscription, PwaSettings
|
|
32 | 30 | |
33 | 31 | |
34 | 32 |
@receiver(post_save, sender=Notification) |
35 | 33 |
def notification(sender, instance=None, created=False, **kwargs): |
36 |
if not pywebpush: |
|
37 |
return |
|
38 | 34 |
if not created: |
39 | 35 |
return |
36 |
pwa_settings = PwaSettings.singleton() |
|
37 |
if not pwa_settings.push_notifications: |
|
38 |
return |
|
39 |
if settings.PWA_VAPID_PRIVATE_KEY: # legacy |
|
40 |
pwa_vapid_private_key = settings.PWA_VAPID_PRIVATE_KEY |
|
41 |
else: |
|
42 |
pwa_vapid_private_key = Vapid.from_pem(pwa_settings.push_notifications_infos['private_key'].encode('ascii')) |
|
43 |
if settings.PWA_VAPID_CLAIMS: # legacy |
|
44 |
claims = settings.PWA_VAPID_CLAIMS |
|
45 |
else: |
|
46 |
claims = {'sub': 'mailto:%s' % settings.DEFAULT_FROM_EMAIL} |
|
40 | 47 |
message = json.dumps({ |
41 | 48 |
'summary': instance.summary, |
42 | 49 |
'body': instance.body, |
... | ... | |
48 | 55 |
pywebpush.webpush( |
49 | 56 |
subscription_info=subscription.subscription_info, |
50 | 57 |
data=message, |
51 |
vapid_private_key=settings.PWA_VAPID_PRIVATE_KEY,
|
|
52 |
vapid_claims=settings.PWA_VAPID_CLAIMS
|
|
58 |
vapid_private_key=pwa_vapid_private_key,
|
|
59 |
vapid_claims=claims,
|
|
53 | 60 |
) |
54 | 61 |
except pywebpush.WebPushException as e: |
55 | 62 |
logger = logging.getLogger(__name__) |
combo/apps/pwa/templates/combo/service-worker-registration.js | ||
---|---|---|
1 |
var applicationServerPublicKey = {% if pwa_vapid_publik_key %}'{{ pwa_vapid_publik_key }}'{% else %}null{% endif %}; |
|
1 |
{% load combo %} |
|
2 | ||
3 |
var applicationServerPublicKey = {{ pwa_vapid_public_key|as_json|safe }}; |
|
2 | 4 |
var COMBO_PWA_USER_SUBSCRIPTION = false; |
3 | 5 | |
4 | 6 |
function urlB64ToUint8Array(base64String) { |
combo/apps/pwa/templates/combo/service-worker.js | ||
---|---|---|
3 | 3 |
/* global self, caches, fetch, URL, Response */ |
4 | 4 |
'use strict'; |
5 | 5 | |
6 |
const applicationServerPublicKey = {{ pwa_vapid_publik_key|as_json|safe }};
|
|
6 |
const applicationServerPublicKey = {{ pwa_vapid_public_key|as_json|safe }};
|
|
7 | 7 | |
8 | 8 |
function urlB64ToUint8Array(base64String) { |
9 | 9 |
const padding = '='.repeat((4 - base64String.length % 4) % 4); |
combo/apps/pwa/views.py | ||
---|---|---|
14 | 14 |
# You should have received a copy of the GNU Affero General Public License |
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 |
import base64 |
|
17 | 18 |
import json |
18 | 19 | |
19 | 20 |
from django.conf import settings |
... | ... | |
23 | 24 |
from django.views.decorators.csrf import csrf_exempt |
24 | 25 |
from django.views.generic import TemplateView |
25 | 26 | |
27 |
from cryptography.hazmat.primitives import serialization |
|
28 |
from py_vapid import Vapid |
|
29 | ||
26 | 30 |
from .models import PushSubscription, PwaSettings |
27 | 31 |
from combo import VERSION |
28 | 32 | |
... | ... | |
40 | 44 | |
41 | 45 |
def js_response(request, template_name, **kwargs): |
42 | 46 |
template = get_template(template_name) |
47 |
pwa_vapid_public_key = None |
|
48 |
pwa_settings = PwaSettings.singleton() |
|
49 |
if pwa_settings.push_notifications: |
|
50 |
if settings.PWA_VAPID_PUBLIK_KEY: # legacy |
|
51 |
pwa_vapid_public_key = settings.PWA_VAPID_PUBLIK_KEY |
|
52 |
else: |
|
53 |
pwa_vapid_public_key = base64.urlsafe_b64encode( |
|
54 |
Vapid.from_pem(pwa_settings.push_notifications_infos['private_key'].encode('ascii') |
|
55 |
).private_key.public_key().public_bytes( |
|
56 |
encoding=serialization.Encoding.X962, |
|
57 |
format=serialization.PublicFormat.UncompressedPoint)).strip('=') |
|
43 | 58 |
context = { |
44 |
'pwa_vapid_publik_key': settings.PWA_VAPID_PUBLIK_KEY,
|
|
59 |
'pwa_vapid_public_key': pwa_vapid_public_key,
|
|
45 | 60 |
'pwa_notification_badge_url': settings.PWA_NOTIFICATION_BADGE_URL, |
46 | 61 |
'pwa_notification_icon_url': settings.PWA_NOTIFICATION_ICON_URL, |
47 | 62 |
} |
debian/control | ||
---|---|---|
23 | 23 |
python-eopayment (>= 1.35), |
24 | 24 |
python-django-haystack (>= 2.4.0), |
25 | 25 |
python-sorl-thumbnail, |
26 |
python-pil |
|
27 |
Recommends: python-django-mellon, python-whoosh, python-pywebpush |
|
26 |
python-pil, |
|
27 |
python-pywebpush |
|
28 |
Recommends: python-django-mellon, python-whoosh |
|
28 | 29 |
Conflicts: python-lingo |
29 | 30 |
Description: Portal Management System (Python module) |
30 | 31 |
setup.py | ||
---|---|---|
165 | 165 |
'sorl-thumbnail', |
166 | 166 |
'Pillow', |
167 | 167 |
'pyproj', |
168 |
'pywebpush', |
|
168 | 169 |
], |
169 | 170 |
zip_safe=False, |
170 | 171 |
cmdclass={ |
tests/test_pwa.py | ||
---|---|---|
5 | 5 |
import pytest |
6 | 6 |
from webtest import Upload |
7 | 7 | |
8 |
try: |
|
9 |
import pywebpush |
|
10 |
except ImportError: |
|
11 |
pywebpush = None |
|
8 |
import pywebpush |
|
12 | 9 | |
13 | 10 |
from django.conf import settings |
14 | 11 |
from django.core.files import File |
... | ... | |
34 | 31 | |
35 | 32 |
def test_service_worker(app): |
36 | 33 |
app.get('/service-worker.js', status=200) |
37 |
app.get('/service-worker-registration.js', status=200) |
|
34 |
resp = app.get('/service-worker-registration.js', status=200) |
|
35 |
assert 'applicationServerPublicKey = null' in resp.text |
|
36 | ||
37 |
pwa_settings = PwaSettings.singleton() |
|
38 |
pwa_settings.push_notifications = True |
|
39 |
pwa_settings.save() |
|
40 | ||
41 |
resp = app.get('/service-worker-registration.js', status=200) |
|
42 |
assert 'applicationServerPublicKey = "' in resp.text |
|
43 | ||
44 |
# check legacy settings are still supported |
|
45 |
with override_settings( |
|
46 |
PWA_VAPID_PUBLIK_KEY="BFzvUdXB...", |
|
47 |
PWA_VAPID_PRIVATE_KEY="4WbCnBF...", |
|
48 |
PWA_VAPID_CLAIMS = {'sub': 'mailto:admin@entrouvert.com'}): |
|
49 |
resp = app.get('/service-worker-registration.js', status=200) |
|
50 |
assert 'applicationServerPublicKey = "BFzvUdXB..."' in resp.text |
|
38 | 51 | |
39 | 52 |
def test_webpush_subscription(app, john_doe, jane_doe): |
40 | 53 |
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=403) |
... | ... | |
53 | 66 |
app.post_json(reverse('pwa-subscribe-push'), params=None, status=200) |
54 | 67 |
assert PushSubscription.objects.count() == 1 |
55 | 68 | |
56 |
@pytest.mark.skipif('pywebpush is None') |
|
57 | 69 |
def test_webpush_notification(app, john_doe): |
58 | 70 |
PushSubscription.objects.all().delete() |
59 | 71 |
app = login(app, john_doe.username, john_doe.username) |
60 | 72 |
app.post_json(reverse('pwa-subscribe-push'), params={'sample': 'content'}, status=200) |
61 | 73 | |
74 |
pwa_settings = PwaSettings.singleton() |
|
75 |
pwa_settings.push_notifications = False |
|
76 |
pwa_settings.save() |
|
77 |
with mock.patch('pywebpush.webpush') as webpush: |
|
78 |
notification = Notification.notify(john_doe, 'test', body='hello world') |
|
79 |
assert webpush.call_count == 0 |
|
80 | ||
81 |
pwa_settings.push_notifications = True |
|
82 |
pwa_settings.save() |
|
62 | 83 |
with mock.patch('pywebpush.webpush') as webpush: |
63 | 84 |
notification = Notification.notify(john_doe, 'test', body='hello world') |
64 | 85 |
assert webpush.call_count == 1 |
65 | 86 |
assert webpush.call_args[1]['subscription_info'] == {'sample': 'content'} |
66 | 87 | |
88 |
# check legacy settings are still supported |
|
89 |
with override_settings( |
|
90 |
PWA_VAPID_PUBLIK_KEY="BFzvUdXB...", |
|
91 |
PWA_VAPID_PRIVATE_KEY="4WbCnBF...", |
|
92 |
PWA_VAPID_CLAIMS = {'sub': 'mailto:admin@entrouvert.com'}): |
|
93 |
with mock.patch('pywebpush.webpush') as webpush: |
|
94 |
notification = Notification.notify(john_doe, 'test', body='hello world') |
|
95 |
assert webpush.call_count == 1 |
|
96 |
assert webpush.call_args[1]['subscription_info'] == {'sample': 'content'} |
|
97 |
assert webpush.call_args[1]['vapid_private_key'] == settings.PWA_VAPID_PRIVATE_KEY |
|
98 |
assert webpush.call_args[1]['vapid_claims'] == settings.PWA_VAPID_CLAIMS |
|
99 | ||
67 | 100 |
def test_no_pwa_manager(app, admin_user): |
68 | 101 |
app = login(app) |
69 | 102 |
resp = app.get('/manage/', status=200) |
70 |
- |