Projet

Général

Profil

0001-pwa-add-option-to-enable-support-for-push-notificati.patch

Frédéric Péters, 30 juin 2019 09:56

Télécharger (14,3 ko)

Voir les différences:

Subject: [PATCH] pwa: add option to enable support for push notifications
 (#31388)

 combo/apps/notifications/models.py            |  4 ++
 .../templates/combo/notificationscell.html    | 33 ++++++++++++++
 combo/apps/pwa/manager_views.py               |  3 +-
 combo/apps/pwa/models.py                      | 16 +++++++
 combo/apps/pwa/signals.py                     | 25 +++++++----
 .../combo/service-worker-registration.js      |  4 +-
 .../pwa/templates/combo/service-worker.js     |  2 +-
 combo/apps/pwa/views.py                       | 17 ++++++-
 debian/control                                |  5 ++-
 setup.py                                      |  1 +
 tests/test_pwa.py                             | 45 ++++++++++++++++---
 11 files changed, 134 insertions(+), 21 deletions(-)
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
-