Projet

Général

Profil

0001-pwa-add-settings-page-with-offline-parameters-25496.patch

Frédéric Péters, 27 décembre 2018 10:33

Télécharger (22 ko)

Voir les différences:

Subject: [PATCH] pwa: add settings page with offline parameters (#25496)

 combo/apps/pwa/__init__.py                    |  10 ++
 combo/apps/pwa/manager_views.py               |  30 +++++
 combo/apps/pwa/migrations/0002_pwasettings.py |  24 ++++
 combo/apps/pwa/models.py                      |  15 +++
 .../pwa/static/css/combo.manager.pwa.scss     |  84 +++++++++++++
 combo/apps/pwa/static/img/mobile-case.svg     | 112 ++++++++++++++++++
 .../pwa/templates/combo/pwa/manager_base.html |  16 +++
 .../pwa/templates/combo/pwa/manager_home.html |  54 +++++++++
 .../apps/pwa/templates/combo/pwa/offline.html |  53 +++++++++
 .../pwa/templates/combo/service-worker.js     |   4 +-
 combo/apps/pwa/urls.py                        |  16 ++-
 combo/apps/pwa/views.py                       |  14 ++-
 combo/settings.py                             |   4 +
 tests/test_pwa.py                             |  34 +++++-
 14 files changed, 465 insertions(+), 5 deletions(-)
 create mode 100644 combo/apps/pwa/manager_views.py
 create mode 100644 combo/apps/pwa/migrations/0002_pwasettings.py
 create mode 100644 combo/apps/pwa/static/css/combo.manager.pwa.scss
 create mode 100644 combo/apps/pwa/static/img/mobile-case.svg
 create mode 100644 combo/apps/pwa/templates/combo/pwa/manager_base.html
 create mode 100644 combo/apps/pwa/templates/combo/pwa/manager_home.html
 create mode 100644 combo/apps/pwa/templates/combo/pwa/offline.html
combo/apps/pwa/__init__.py
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import django.apps
18
from django.core.urlresolvers import reverse
19
from django.utils.translation import ugettext_lazy as _
20

  
18 21

  
19 22
class AppConfig(django.apps.AppConfig):
20 23
    name = 'combo.apps.pwa'
......
26 29
        from . import urls
27 30
        return urls.urlpatterns
28 31

  
32
    def get_extra_manager_actions(self):
33
        from django.conf import settings
34
        if settings.TEMPLATE_VARS.get('pwa_display') in ('standalone', 'fullscreen'):
35
            return [{'href': reverse('pwa-manager-homepage'),
36
                     'text': _('Mobile Application (PWA)')}]
37
        return []
38

  
29 39
default_app_config = 'combo.apps.pwa.AppConfig'
combo/apps/pwa/manager_views.py
1
# combo - content management system
2
# Copyright (C) 2018  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.core.urlresolvers import reverse_lazy
18
from django.views.generic import UpdateView
19

  
20
from .models import PwaSettings
21

  
22

  
23
class ManagerHomeView(UpdateView):
24
    template_name = 'combo/pwa/manager_home.html'
25
    model = PwaSettings
26
    fields = '__all__'
27
    success_url = reverse_lazy('pwa-manager-homepage')
28

  
29
    def get_object(self):
30
        return PwaSettings.singleton()
combo/apps/pwa/migrations/0002_pwasettings.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.12 on 2018-12-27 08:44
3
from __future__ import unicode_literals
4

  
5
import combo.data.fields
6
from django.db import migrations, models
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('pwa', '0001_initial'),
13
    ]
14

  
15
    operations = [
16
        migrations.CreateModel(
17
            name='PwaSettings',
18
            fields=[
19
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20
                ('offline_text', combo.data.fields.RichTextField(default='You are currently offline.', verbose_name='Offline Information Text')),
21
                ('offline_retry_button', models.BooleanField(default=True, verbose_name='Include Retry Button')),
22
            ],
23
        ),
24
    ]
combo/apps/pwa/models.py
16 16

  
17 17
from django.conf import settings
18 18
from django.db import models
19
from django.utils.translation import ugettext_lazy as _
19 20

  
20 21
from jsonfield import JSONField
22
from combo.data.fields import RichTextField
23

  
24

  
25
class PwaSettings(models.Model):
26
    offline_text = RichTextField(
27
            verbose_name=_('Offline Information Text'),
28
            default=_('You are currently offline.'),
29
            config_name='small')
30
    offline_retry_button = models.BooleanField(_('Include Retry Button'), default=True)
31

  
32
    @classmethod
33
    def singleton(cls):
34
        return cls.objects.all().first() or cls()
35

  
21 36

  
22 37

  
23 38
class PushSubscription(models.Model):
combo/apps/pwa/static/css/combo.manager.pwa.scss
1
.manager-mobile-home-layout {
2
	display: flex;
3
	div.sections {
4
		flex: 1;
5
	}
6
}
7

  
8
div#mobile-case {
9
	background: url(../img/mobile-case.svg) top left no-repeat;
10
	width: 400px;
11
	height: 720px;
12
	position: relative;
13
	overflow: hidden;
14
	div.screen {
15
		position: absolute;
16
		overflow: hidden;
17
		left: 12px;
18
		top: 52px;
19
		bottom: 67px;
20
		right: 28px;
21
		div.mobile-top-bar {
22
			position: absolute;
23
			top: 0;
24
			left: 0;
25
			background: rgba(0, 0, 0, 0.7);
26
			width: 100%;
27
			text-align: right;
28
			color: white;
29
			box-sizing: border-box;
30
			padding-right: 5px;
31
			height: 20px;
32
		}
33
		div.mobile-app-content {
34
			position: absolute;
35
			top: 20px;
36
			left: 0;
37
			width: 100%;
38
			bottom: 0;
39
			div.splash,
40
			iframe {
41
				position: absolute;
42
				top: 0;
43
				left: 0;
44
				width: 100%;
45
				height: 100%;
46
				z-index: 0;
47
				opacity: 0;
48
				transition: all ease-out 0.4s;
49
			}
50
			div.splash {
51
				z-index: 100;
52
				opacity: 1;
53
				transform: scale(1);
54
			}
55
			&.splash-off {
56
				div.splash {
57
					pointer-events: none;
58
					opacity: 0;
59
					transform: scale(10);
60
				}
61
				iframe {
62
					opacity: 1;
63
				}
64
			}
65
		}
66
		div.appicon {
67
			position: absolute;
68
			top: 40%;
69
			text-align: center;
70
			img {
71
				width: 50%;
72
			}
73
		}
74
		div.applabel {
75
			position: absolute;
76
			bottom: 50px;
77
			left: 0;
78
			width: 100%;
79
			text-align: center;
80
			font-size: 30px;
81
			color: white;
82
		}
83
	}
84
}
combo/apps/pwa/static/img/mobile-case.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<!-- Created with Inkscape (http://www.inkscape.org/) -->
3

  
4
<svg
5
   xmlns:dc="http://purl.org/dc/elements/1.1/"
6
   xmlns:cc="http://creativecommons.org/ns#"
7
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
   xmlns:svg="http://www.w3.org/2000/svg"
9
   xmlns="http://www.w3.org/2000/svg"
10
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
   version="1.1"
13
   id="svg2"
14
   xml:space="preserve"
15
   width="386.93579"
16
   height="707.42499"
17
   viewBox="0 0 386.93579 707.42499"
18
   sodipodi:docname="mobile-case.svg"
19
   inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
20
     id="metadata8"><rdf:RDF><cc:Work
21
         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
22
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
23
     id="defs6"><clipPath
24
       clipPathUnits="userSpaceOnUse"
25
       id="clipPath18"><path
26
         d="M 0,410 H 1028 V 0 H 0 Z"
27
         id="path16"
28
         inkscape:connector-curvature="0" /></clipPath></defs><sodipodi:namedview
29
     pagecolor="#ffffff"
30
     bordercolor="#666666"
31
     borderopacity="1"
32
     objecttolerance="10"
33
     gridtolerance="10"
34
     guidetolerance="10"
35
     inkscape:pageopacity="0"
36
     inkscape:pageshadow="2"
37
     inkscape:window-width="1920"
38
     inkscape:window-height="1043"
39
     id="namedview4"
40
     showgrid="false"
41
     inkscape:zoom="0.5"
42
     inkscape:cx="-114.43322"
43
     inkscape:cy="238.84637"
44
     inkscape:window-x="0"
45
     inkscape:window-y="0"
46
     inkscape:window-maximized="1"
47
     inkscape:current-layer="g10"
48
     inkscape:measure-start="27,428"
49
     inkscape:measure-end="387,428"
50
     fit-margin-top="0"
51
     fit-margin-left="0"
52
     fit-margin-right="0"
53
     fit-margin-bottom="0" /><g
54
     id="g10"
55
     inkscape:groupmode="layer"
56
     inkscape:label="x"
57
     transform="matrix(1.3333333,0,0,-1.3333333,-3.6475299,539.99784)"><g
58
       id="g20"
59
       transform="matrix(1.8601685,0,0,1.8601685,309.66345,-151.14733)"
60
       style="stroke-width:0.5"><path
61
         d="m -10.548622,33.67008 c 0,-16.568 -13.431,-19.920268 -29.999,-19.920268 H -134.999 c -16.569,0 -30.001,3.352268 -30.001,19.920268 V 280.665 c 0,9.72565 13.432,18.31099 30.001,18.31099 h 94.451378 c 16.568,0 29.999,-9.44063 29.999,-18.31099 z"
62
         style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
63
         id="path22"
64
         inkscape:connector-curvature="0"
65
         sodipodi:nodetypes="sssssssss" /></g><g
66
       id="g24"
67
       transform="matrix(1.8601685,0,0,1.8601685,300.67326,-142.46964)"
68
       style="stroke-width:0.5"><path
69
         d="m -10.120975,32.690552 c 0,-16.568 -13.432,-20.726646 -30,-20.726646 H -125.333 c -16.569,0 -30,4.158646 -30,20.726646 V 271.334 c 0,16.569 13.431,20.71282 30,20.71282 h 85.212025 c 16.568,0 30,-4.14382 30,-20.71282 z"
70
         style="fill:#232323;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
71
         id="path26"
72
         inkscape:connector-curvature="0"
73
         sodipodi:nodetypes="sssssssss" /></g><g
74
       id="g30"
75
       transform="matrix(1.8601685,0,0,1.8601685,189.06408,381.94033)"
76
       style="stroke-width:0.5"><path
77
         d="m 0,0 c 0,-1 -1,-2 -2,-2 h -30 c -1,0 -2,1 -2,2 0,1 1,2 2,2 H -2 C -1,2 0,1 0,0"
78
         style="fill:#0e0e0e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
79
         id="path32"
80
         inkscape:connector-curvature="0" /></g><g
81
       id="g38"
82
       transform="matrix(1.8601685,0,0,1.8601685,111.24393,381.94125)"
83
       style="stroke-width:0.5"><path
84
         d="m 0,0 c 0,-2 -1,-4 -4,-4 -2,0 -4,1 -4,4 0,2 1,4 4,4 2,0 4,-1 4,-4"
85
         style="fill:#0e0e0e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
86
         id="path40"
87
         inkscape:connector-curvature="0" /></g><g
88
       id="g42"
89
       transform="matrix(1.8601685,0,0,1.8601685,108.45368,381.94125)"
90
       style="stroke-width:0.5"><path
91
         d="m 0,0 c 0,-1 -1,-2 -2,-2 -1,0 -3,1 -3,2 0,1 2,2 3,2 1,0 2,-1 2,-2"
92
         style="fill:#2c2c2c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
93
         id="path44"
94
         inkscape:connector-curvature="0" /></g><g
95
       id="g46"
96
       transform="matrix(1.8601685,0,0,1.8601685,106.43911,381.94125)"
97
       style="stroke-width:0.5"><path
98
         d="m 0,0 c 0,0 0,-1 -1,-1 0,0 -1,0 -1,1 0,0 0,1 1,1 1,0 1,-1 1,-1"
99
         style="fill:#373737;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
100
         id="path48"
101
         inkscape:connector-curvature="0" /></g><path
102
       d="m 292.9375,309.4375 h -3 v 41 h 3 z"
103
       style="fill:#1a1a1a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1"
104
       id="path50"
105
       inkscape:connector-curvature="0" /><rect
106
       style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.8;fill:#00ffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.28346458;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
107
       id="rect831"
108
       width="270"
109
       height="450"
110
       x="11.735635"
111
       y="-365.1778"
112
       transform="scale(1,-1)" /></g></svg>
combo/apps/pwa/templates/combo/pwa/manager_base.html
1
{% extends "combo/manager_base.html" %}
2
{% load i18n %}
3

  
4
{% block css %}
5
{{ block.super }}
6
<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/combo.manager.pwa.css"/>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans 'Mobile Application' %}</h2>
11
{% endblock %}
12

  
13
{% block breadcrumb %}
14
{{ block.super }}
15
<a href="{% url 'pwa-manager-homepage' %}">{% trans 'Mobile Application' %}</a>
16
{% endblock %}
combo/apps/pwa/templates/combo/pwa/manager_home.html
1
{% extends "combo/pwa/manager_base.html" %}
2
{% load i18n static %}
3

  
4
{% block content %}
5
<div class="manager-mobile-home-layout">
6
<div id="mobile-case">
7
  <div class="screen" style="background: {{ theme_color }};">
8
    <div class="mobile-top-bar"><span class="clock">--:--</span></div>
9
    <div class="mobile-app-content">
10
      <div class="splash">
11
      <div class="appicon">
12
        <img src="{% static "" %}{{ css_variant }}/{{ icon_prefix }}{{icon_sizes|last}}px.png" alt="">
13
      </div>
14
      <div class="applabel">{% firstof global_title "Compte Citoyen" %}</div>
15
      </div>
16
      <iframe scrolling="no"></iframe>
17
    </div>
18
  </div>
19
</div>
20

  
21
<div class="sections">
22

  
23
<div class="section settings">
24
<h3>{% trans "Settings" %}</h3>
25
<div>
26
  <form method="post" enctype="multipart/form-data">
27
    {% csrf_token %}
28
    {{ form.as_p }}
29
    <div class="buttons">
30
      <button class="submit-button">{% trans "Save" %}</button>
31
    </div>
32
  </form>
33
</div>
34
</div>
35

  
36
</div> {# .sections #}
37
</div> {# .manager-mobile-home-layout #}
38

  
39
<script>
40
setInterval(function() {
41
  var $clock = $('#mobile-case .clock');
42
  var date = new Date();
43
  $clock.text(('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2));
44
}, 500);
45

  
46
$(function() {
47
  $('.mobile-app-content .splash').on('click', function() {
48
    $('.mobile-app-content iframe').attr('src', '/');
49
    $('.mobile-app-content').addClass('splash-off');
50
  });
51
});
52
</script>
53

  
54
{% endblock %}
combo/apps/pwa/templates/combo/pwa/offline.html
1
{% load i18n static %}<!DOCTYPE html>
2
<html>
3
<head>
4
  <meta charset="utf-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <style>
7
html, body {
8
  margin: 0; padding: 1rem;
9
  font-family: sans-serif;
10
  background: {{theme_color}};
11
}
12

  
13
div.info-text {
14
  background: white;
15
  padding: 1rem;
16
  border-radius: 3px;
17
}
18

  
19
img {
20
  max-width: 100%;
21
  margin: 0 auto;
22
  display: block;
23
}
24

  
25
p.retry {
26
  margin-top: 2rem;
27
  text-align: center;
28
}
29

  
30
p.retry a {
31
  border: 1px solid {{theme_color}};
32
  text-decoration: none;
33
  background: white;
34
  padding: 0.5rem 1rem;
35
  border-radius: 3px;
36
  color: inherit;
37
}
38

  
39
  </style>
40
</head>
41
<body>
42
<div class="info-text">
43
  <img src="{% static "" %}{{ css_variant }}/{{ icon_prefix }}{{icon_sizes|last}}px.png" alt="">
44
  {{ pwa_settings.offline_text|safe }}
45

  
46
  {% if pwa_settings.offline_retry_button %}
47
  <p class="retry">
48
  <a href=".">{% trans "Retry" %}</a>
49
  </p>
50
  {% endif %}
51
</div>
52
</body>
53
</html>
combo/apps/pwa/templates/combo/service-worker.js
25 25
var config = {
26 26
  version: 'v{% start_timestamp %}',
27 27
  staticCacheItems: [
28
    '/offline/'
28
    '/__pwa__/offline/'
29 29
  ],
30 30
  cachePathPattern: /^\/static\/.*/,
31 31
  handleFetchPathPattern: /.*/,
32
  offlinePage: '/offline/'
32
  offlinePage: '/__pwa__/offline/'
33 33
};
34 34

  
35 35
function cacheName (key, opts) {
combo/apps/pwa/urls.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
from django.conf.urls import url
17
from django.conf.urls import url, include
18 18

  
19
from combo.urls_utils import decorated_includes, manager_required
20

  
21
from .manager_views import (
22
        ManagerHomeView,
23
        )
19 24
from .views import (
20 25
        manifest_json,
21 26
        service_worker_js,
22 27
        service_worker_registration_js,
23 28
        subscribe_push,
29
        offline_page,
24 30
        )
25 31

  
32

  
33
pwa_manager_urls = [
34
    url('^$', ManagerHomeView.as_view(), name='pwa-manager-homepage'),
35
]
36

  
26 37
urlpatterns = [
27 38
    url('^manifest.json$', manifest_json),
28 39
    url('^service-worker.js$', service_worker_js),
29 40
    url('^service-worker-registration.js$', service_worker_registration_js),
30 41
    url('^api/pwa/push/subscribe$', subscribe_push, name='pwa-subscribe-push'),
42
    url('^__pwa__/offline/$', offline_page),
43
    url(r'^manage/pwa/', decorated_includes(manager_required,
44
        include(pwa_manager_urls))),
31 45
]
combo/apps/pwa/views.py
21 21
from django.http import HttpResponse, HttpResponseForbidden, Http404, JsonResponse
22 22
from django.template.loader import get_template, TemplateDoesNotExist
23 23
from django.views.decorators.csrf import csrf_exempt
24
from django.views.generic import TemplateView
24 25

  
25
from .models import PushSubscription
26
from .models import PushSubscription, PwaSettings
26 27

  
27 28

  
28 29
def manifest_json(request, *args, **kwargs):
......
67 68
                subscription_info=subscription_data)
68 69
        subscription.save()
69 70
    return JsonResponse({'err': 0})
71

  
72

  
73
class OfflinePage(TemplateView):
74
    template_name = 'combo/pwa/offline.html'
75

  
76
    def get_context_data(self, **kwargs):
77
        context = super(OfflinePage, self).get_context_data(**kwargs)
78
        context['pwa_settings'] = PwaSettings.singleton()
79
        return context
80

  
81
offline_page = OfflinePage.as_view()
combo/settings.py
23 23
and to disable DEBUG mode in production.
24 24
"""
25 25

  
26
import copy
26 27
import os
27 28
from django.conf import global_settings
28 29
from django.utils.translation import ugettext_lazy as _
......
177 178
    },
178 179
}
179 180

  
181
CKEDITOR_CONFIGS['small'] = copy.copy(CKEDITOR_CONFIGS['default'])
182
CKEDITOR_CONFIGS['small']['height'] = 150
183

  
180 184
HAYSTACK_CONNECTIONS = {
181 185
    'default': {
182 186
        'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
tests/test_pwa.py
13 13
from django.test import override_settings
14 14

  
15 15
from combo.apps.notifications.models import Notification
16
from combo.apps.pwa.models import PushSubscription
16
from combo.apps.pwa.models import PushSubscription, PwaSettings
17 17

  
18 18
from .test_manager import login
19 19

  
......
59 59
        notification = Notification.notify(john_doe, 'test', body='hello world')
60 60
        assert webpush.call_count == 1
61 61
        assert webpush.call_args[1]['subscription_info'] == {'sample': 'content'}
62

  
63
def test_no_pwa_manager(app, admin_user):
64
    app = login(app)
65
    resp = app.get('/manage/', status=200)
66
    assert not '/manage/pwa/' in resp.text
67

  
68
def test_pwa_manager(app, admin_user):
69
    app = login(app)
70
    with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
71
        resp = app.get('/manage/', status=200)
72
        assert '/manage/pwa/' in resp.text
73
        resp = app.get('/manage/pwa/')
74
        resp.form['offline_text'] = 'You are offline.'
75
        assert resp.form['offline_retry_button'].checked
76
        resp.form['offline_retry_button'].checked = False
77
        resp = resp.form.submit().follow()
78
        assert resp.form['offline_text'].value == 'You are offline.'
79
        assert resp.form['offline_retry_button'].checked is False
80

  
81
def test_pwa_offline_page(app):
82
    PwaSettings.objects.all().delete()
83
    resp = app.get('/__pwa__/offline/')
84
    assert 'You are currently offline.' in resp.text
85
    assert 'Retry' in resp.text
86
    pwa_settings = PwaSettings.singleton()
87
    pwa_settings.offline_text = 'You are offline.'
88
    pwa_settings.offline_retry_button = False
89
    pwa_settings.save()
90

  
91
    resp = app.get('/__pwa__/offline/')
92
    assert 'You are offline.' in resp.text
93
    assert 'Retry' not in resp.text
62
-