From f5e095c8a9966e381fafcdb70915c02342608d8e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Thu, 27 Dec 2018 09:44:21 +0100
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

diff --git a/combo/apps/pwa/__init__.py b/combo/apps/pwa/__init__.py
index ade658df..19952e6c 100644
--- a/combo/apps/pwa/__init__.py
+++ b/combo/apps/pwa/__init__.py
@@ -15,6 +15,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import django.apps
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
 
 class AppConfig(django.apps.AppConfig):
     name = 'combo.apps.pwa'
@@ -26,4 +29,11 @@ class AppConfig(django.apps.AppConfig):
         from . import urls
         return urls.urlpatterns
 
+    def get_extra_manager_actions(self):
+        from django.conf import settings
+        if settings.TEMPLATE_VARS.get('pwa_display') in ('standalone', 'fullscreen'):
+            return [{'href': reverse('pwa-manager-homepage'),
+                     'text': _('Mobile Application (PWA)')}]
+        return []
+
 default_app_config = 'combo.apps.pwa.AppConfig'
diff --git a/combo/apps/pwa/manager_views.py b/combo/apps/pwa/manager_views.py
new file mode 100644
index 00000000..4ff9328b
--- /dev/null
+++ b/combo/apps/pwa/manager_views.py
@@ -0,0 +1,30 @@
+# combo - content management system
+# Copyright (C) 2018  Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# 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/>.
+
+from django.core.urlresolvers import reverse_lazy
+from django.views.generic import UpdateView
+
+from .models import PwaSettings
+
+
+class ManagerHomeView(UpdateView):
+    template_name = 'combo/pwa/manager_home.html'
+    model = PwaSettings
+    fields = '__all__'
+    success_url = reverse_lazy('pwa-manager-homepage')
+
+    def get_object(self):
+        return PwaSettings.singleton()
diff --git a/combo/apps/pwa/migrations/0002_pwasettings.py b/combo/apps/pwa/migrations/0002_pwasettings.py
new file mode 100644
index 00000000..183e8aff
--- /dev/null
+++ b/combo/apps/pwa/migrations/0002_pwasettings.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.12 on 2018-12-27 08:44
+from __future__ import unicode_literals
+
+import combo.data.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('pwa', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='PwaSettings',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('offline_text', combo.data.fields.RichTextField(default='You are currently offline.', verbose_name='Offline Information Text')),
+                ('offline_retry_button', models.BooleanField(default=True, verbose_name='Include Retry Button')),
+            ],
+        ),
+    ]
diff --git a/combo/apps/pwa/models.py b/combo/apps/pwa/models.py
index 2ac31cb6..23c14092 100644
--- a/combo/apps/pwa/models.py
+++ b/combo/apps/pwa/models.py
@@ -16,8 +16,23 @@
 
 from django.conf import settings
 from django.db import models
+from django.utils.translation import ugettext_lazy as _
 
 from jsonfield import JSONField
+from combo.data.fields import RichTextField
+
+
+class PwaSettings(models.Model):
+    offline_text = RichTextField(
+            verbose_name=_('Offline Information Text'),
+            default=_('You are currently offline.'),
+            config_name='small')
+    offline_retry_button = models.BooleanField(_('Include Retry Button'), default=True)
+
+    @classmethod
+    def singleton(cls):
+        return cls.objects.all().first() or cls()
+
 
 
 class PushSubscription(models.Model):
diff --git a/combo/apps/pwa/static/css/combo.manager.pwa.scss b/combo/apps/pwa/static/css/combo.manager.pwa.scss
new file mode 100644
index 00000000..ae2c51aa
--- /dev/null
+++ b/combo/apps/pwa/static/css/combo.manager.pwa.scss
@@ -0,0 +1,84 @@
+.manager-mobile-home-layout {
+	display: flex;
+	div.sections {
+		flex: 1;
+	}
+}
+
+div#mobile-case {
+	background: url(../img/mobile-case.svg) top left no-repeat;
+	width: 400px;
+	height: 720px;
+	position: relative;
+	overflow: hidden;
+	div.screen {
+		position: absolute;
+		overflow: hidden;
+		left: 12px;
+		top: 52px;
+		bottom: 67px;
+		right: 28px;
+		div.mobile-top-bar {
+			position: absolute;
+			top: 0;
+			left: 0;
+			background: rgba(0, 0, 0, 0.7);
+			width: 100%;
+			text-align: right;
+			color: white;
+			box-sizing: border-box;
+			padding-right: 5px;
+			height: 20px;
+		}
+		div.mobile-app-content {
+			position: absolute;
+			top: 20px;
+			left: 0;
+			width: 100%;
+			bottom: 0;
+			div.splash,
+			iframe {
+				position: absolute;
+				top: 0;
+				left: 0;
+				width: 100%;
+				height: 100%;
+				z-index: 0;
+				opacity: 0;
+				transition: all ease-out 0.4s;
+			}
+			div.splash {
+				z-index: 100;
+				opacity: 1;
+				transform: scale(1);
+			}
+			&.splash-off {
+				div.splash {
+					pointer-events: none;
+					opacity: 0;
+					transform: scale(10);
+				}
+				iframe {
+					opacity: 1;
+				}
+			}
+		}
+		div.appicon {
+			position: absolute;
+			top: 40%;
+			text-align: center;
+			img {
+				width: 50%;
+			}
+		}
+		div.applabel {
+			position: absolute;
+			bottom: 50px;
+			left: 0;
+			width: 100%;
+			text-align: center;
+			font-size: 30px;
+			color: white;
+		}
+	}
+}
diff --git a/combo/apps/pwa/static/img/mobile-case.svg b/combo/apps/pwa/static/img/mobile-case.svg
new file mode 100644
index 00000000..70039167
--- /dev/null
+++ b/combo/apps/pwa/static/img/mobile-case.svg
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="svg2"
+   xml:space="preserve"
+   width="386.93579"
+   height="707.42499"
+   viewBox="0 0 386.93579 707.42499"
+   sodipodi:docname="mobile-case.svg"
+   inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
+     id="metadata8"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+     id="defs6"><clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath18"><path
+         d="M 0,410 H 1028 V 0 H 0 Z"
+         id="path16"
+         inkscape:connector-curvature="0" /></clipPath></defs><sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1043"
+     id="namedview4"
+     showgrid="false"
+     inkscape:zoom="0.5"
+     inkscape:cx="-114.43322"
+     inkscape:cy="238.84637"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g10"
+     inkscape:measure-start="27,428"
+     inkscape:measure-end="387,428"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" /><g
+     id="g10"
+     inkscape:groupmode="layer"
+     inkscape:label="x"
+     transform="matrix(1.3333333,0,0,-1.3333333,-3.6475299,539.99784)"><g
+       id="g20"
+       transform="matrix(1.8601685,0,0,1.8601685,309.66345,-151.14733)"
+       style="stroke-width:0.5"><path
+         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"
+         style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
+         id="path22"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sssssssss" /></g><g
+       id="g24"
+       transform="matrix(1.8601685,0,0,1.8601685,300.67326,-142.46964)"
+       style="stroke-width:0.5"><path
+         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"
+         style="fill:#232323;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
+         id="path26"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sssssssss" /></g><g
+       id="g30"
+       transform="matrix(1.8601685,0,0,1.8601685,189.06408,381.94033)"
+       style="stroke-width:0.5"><path
+         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"
+         style="fill:#0e0e0e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
+         id="path32"
+         inkscape:connector-curvature="0" /></g><g
+       id="g38"
+       transform="matrix(1.8601685,0,0,1.8601685,111.24393,381.94125)"
+       style="stroke-width:0.5"><path
+         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"
+         style="fill:#0e0e0e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
+         id="path40"
+         inkscape:connector-curvature="0" /></g><g
+       id="g42"
+       transform="matrix(1.8601685,0,0,1.8601685,108.45368,381.94125)"
+       style="stroke-width:0.5"><path
+         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"
+         style="fill:#2c2c2c;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
+         id="path44"
+         inkscape:connector-curvature="0" /></g><g
+       id="g46"
+       transform="matrix(1.8601685,0,0,1.8601685,106.43911,381.94125)"
+       style="stroke-width:0.5"><path
+         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"
+         style="fill:#373737;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5"
+         id="path48"
+         inkscape:connector-curvature="0" /></g><path
+       d="m 292.9375,309.4375 h -3 v 41 h 3 z"
+       style="fill:#1a1a1a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1"
+       id="path50"
+       inkscape:connector-curvature="0" /><rect
+       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"
+       id="rect831"
+       width="270"
+       height="450"
+       x="11.735635"
+       y="-365.1778"
+       transform="scale(1,-1)" /></g></svg>
diff --git a/combo/apps/pwa/templates/combo/pwa/manager_base.html b/combo/apps/pwa/templates/combo/pwa/manager_base.html
new file mode 100644
index 00000000..422d54d7
--- /dev/null
+++ b/combo/apps/pwa/templates/combo/pwa/manager_base.html
@@ -0,0 +1,16 @@
+{% extends "combo/manager_base.html" %}
+{% load i18n %}
+
+{% block css %}
+{{ block.super }}
+<link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/combo.manager.pwa.css"/>
+{% endblock %}
+
+{% block appbar %}
+<h2>{% trans 'Mobile Application' %}</h2>
+{% endblock %}
+
+{% block breadcrumb %}
+{{ block.super }}
+<a href="{% url 'pwa-manager-homepage' %}">{% trans 'Mobile Application' %}</a>
+{% endblock %}
diff --git a/combo/apps/pwa/templates/combo/pwa/manager_home.html b/combo/apps/pwa/templates/combo/pwa/manager_home.html
new file mode 100644
index 00000000..eb6d7d44
--- /dev/null
+++ b/combo/apps/pwa/templates/combo/pwa/manager_home.html
@@ -0,0 +1,54 @@
+{% extends "combo/pwa/manager_base.html" %}
+{% load i18n static %}
+
+{% block content %}
+<div class="manager-mobile-home-layout">
+<div id="mobile-case">
+  <div class="screen" style="background: {{ theme_color }};">
+    <div class="mobile-top-bar"><span class="clock">--:--</span></div>
+    <div class="mobile-app-content">
+      <div class="splash">
+      <div class="appicon">
+        <img src="{% static "" %}{{ css_variant }}/{{ icon_prefix }}{{icon_sizes|last}}px.png" alt="">
+      </div>
+      <div class="applabel">{% firstof global_title "Compte Citoyen" %}</div>
+      </div>
+      <iframe scrolling="no"></iframe>
+    </div>
+  </div>
+</div>
+
+<div class="sections">
+
+<div class="section settings">
+<h3>{% trans "Settings" %}</h3>
+<div>
+  <form method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {{ form.as_p }}
+    <div class="buttons">
+      <button class="submit-button">{% trans "Save" %}</button>
+    </div>
+  </form>
+</div>
+</div>
+
+</div> {# .sections #}
+</div> {# .manager-mobile-home-layout #}
+
+<script>
+setInterval(function() {
+  var $clock = $('#mobile-case .clock');
+  var date = new Date();
+  $clock.text(('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2));
+}, 500);
+
+$(function() {
+  $('.mobile-app-content .splash').on('click', function() {
+    $('.mobile-app-content iframe').attr('src', '/');
+    $('.mobile-app-content').addClass('splash-off');
+  });
+});
+</script>
+
+{% endblock %}
diff --git a/combo/apps/pwa/templates/combo/pwa/offline.html b/combo/apps/pwa/templates/combo/pwa/offline.html
new file mode 100644
index 00000000..5e13b5bd
--- /dev/null
+++ b/combo/apps/pwa/templates/combo/pwa/offline.html
@@ -0,0 +1,53 @@
+{% load i18n static %}<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="utf-8"/>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <style>
+html, body {
+  margin: 0; padding: 1rem;
+  font-family: sans-serif;
+  background: {{theme_color}};
+}
+
+div.info-text {
+  background: white;
+  padding: 1rem;
+  border-radius: 3px;
+}
+
+img {
+  max-width: 100%;
+  margin: 0 auto;
+  display: block;
+}
+
+p.retry {
+  margin-top: 2rem;
+  text-align: center;
+}
+
+p.retry a {
+  border: 1px solid {{theme_color}};
+  text-decoration: none;
+  background: white;
+  padding: 0.5rem 1rem;
+  border-radius: 3px;
+  color: inherit;
+}
+
+  </style>
+</head>
+<body>
+<div class="info-text">
+  <img src="{% static "" %}{{ css_variant }}/{{ icon_prefix }}{{icon_sizes|last}}px.png" alt="">
+  {{ pwa_settings.offline_text|safe }}
+
+  {% if pwa_settings.offline_retry_button %}
+  <p class="retry">
+  <a href=".">{% trans "Retry" %}</a>
+  </p>
+  {% endif %}
+</div>
+</body>
+</html>
diff --git a/combo/apps/pwa/templates/combo/service-worker.js b/combo/apps/pwa/templates/combo/service-worker.js
index d813302f..c2c64db2 100644
--- a/combo/apps/pwa/templates/combo/service-worker.js
+++ b/combo/apps/pwa/templates/combo/service-worker.js
@@ -25,11 +25,11 @@ function urlB64ToUint8Array(base64String) {
 var config = {
   version: 'v{% start_timestamp %}',
   staticCacheItems: [
-    '/offline/'
+    '/__pwa__/offline/'
   ],
   cachePathPattern: /^\/static\/.*/,
   handleFetchPathPattern: /.*/,
-  offlinePage: '/offline/'
+  offlinePage: '/__pwa__/offline/'
 };
 
 function cacheName (key, opts) {
diff --git a/combo/apps/pwa/urls.py b/combo/apps/pwa/urls.py
index d8357820..766c8f2e 100644
--- a/combo/apps/pwa/urls.py
+++ b/combo/apps/pwa/urls.py
@@ -14,18 +14,32 @@
 # 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/>.
 
-from django.conf.urls import url
+from django.conf.urls import url, include
 
+from combo.urls_utils import decorated_includes, manager_required
+
+from .manager_views import (
+        ManagerHomeView,
+        )
 from .views import (
         manifest_json,
         service_worker_js,
         service_worker_registration_js,
         subscribe_push,
+        offline_page,
         )
 
+
+pwa_manager_urls = [
+    url('^$', ManagerHomeView.as_view(), name='pwa-manager-homepage'),
+]
+
 urlpatterns = [
     url('^manifest.json$', manifest_json),
     url('^service-worker.js$', service_worker_js),
     url('^service-worker-registration.js$', service_worker_registration_js),
     url('^api/pwa/push/subscribe$', subscribe_push, name='pwa-subscribe-push'),
+    url('^__pwa__/offline/$', offline_page),
+    url(r'^manage/pwa/', decorated_includes(manager_required,
+        include(pwa_manager_urls))),
 ]
diff --git a/combo/apps/pwa/views.py b/combo/apps/pwa/views.py
index fbd775b9..80e14269 100644
--- a/combo/apps/pwa/views.py
+++ b/combo/apps/pwa/views.py
@@ -21,8 +21,9 @@ from django.conf import settings
 from django.http import HttpResponse, HttpResponseForbidden, Http404, JsonResponse
 from django.template.loader import get_template, TemplateDoesNotExist
 from django.views.decorators.csrf import csrf_exempt
+from django.views.generic import TemplateView
 
-from .models import PushSubscription
+from .models import PushSubscription, PwaSettings
 
 
 def manifest_json(request, *args, **kwargs):
@@ -67,3 +68,14 @@ def subscribe_push(request, *args, **kwargs):
                 subscription_info=subscription_data)
         subscription.save()
     return JsonResponse({'err': 0})
+
+
+class OfflinePage(TemplateView):
+    template_name = 'combo/pwa/offline.html'
+
+    def get_context_data(self, **kwargs):
+        context = super(OfflinePage, self).get_context_data(**kwargs)
+        context['pwa_settings'] = PwaSettings.singleton()
+        return context
+
+offline_page = OfflinePage.as_view()
diff --git a/combo/settings.py b/combo/settings.py
index 1262fee8..20a6e53b 100644
--- a/combo/settings.py
+++ b/combo/settings.py
@@ -23,6 +23,7 @@ The local settings file should exist, at least to set a suitable SECRET_KEY,
 and to disable DEBUG mode in production.
 """
 
+import copy
 import os
 from django.conf import global_settings
 from django.utils.translation import ugettext_lazy as _
@@ -177,6 +178,9 @@ CKEDITOR_CONFIGS = {
     },
 }
 
+CKEDITOR_CONFIGS['small'] = copy.copy(CKEDITOR_CONFIGS['default'])
+CKEDITOR_CONFIGS['small']['height'] = 150
+
 HAYSTACK_CONNECTIONS = {
     'default': {
         'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
diff --git a/tests/test_pwa.py b/tests/test_pwa.py
index ea24da22..f4fbae36 100644
--- a/tests/test_pwa.py
+++ b/tests/test_pwa.py
@@ -13,7 +13,7 @@ from django.core.urlresolvers import reverse
 from django.test import override_settings
 
 from combo.apps.notifications.models import Notification
-from combo.apps.pwa.models import PushSubscription
+from combo.apps.pwa.models import PushSubscription, PwaSettings
 
 from .test_manager import login
 
@@ -59,3 +59,35 @@ def test_webpush_notification(app, john_doe):
         notification = Notification.notify(john_doe, 'test', body='hello world')
         assert webpush.call_count == 1
         assert webpush.call_args[1]['subscription_info'] == {'sample': 'content'}
+
+def test_no_pwa_manager(app, admin_user):
+    app = login(app)
+    resp = app.get('/manage/', status=200)
+    assert not '/manage/pwa/' in resp.text
+
+def test_pwa_manager(app, admin_user):
+    app = login(app)
+    with override_settings(TEMPLATE_VARS={'pwa_display': 'standalone'}):
+        resp = app.get('/manage/', status=200)
+        assert '/manage/pwa/' in resp.text
+        resp = app.get('/manage/pwa/')
+        resp.form['offline_text'] = 'You are offline.'
+        assert resp.form['offline_retry_button'].checked
+        resp.form['offline_retry_button'].checked = False
+        resp = resp.form.submit().follow()
+        assert resp.form['offline_text'].value == 'You are offline.'
+        assert resp.form['offline_retry_button'].checked is False
+
+def test_pwa_offline_page(app):
+    PwaSettings.objects.all().delete()
+    resp = app.get('/__pwa__/offline/')
+    assert 'You are currently offline.' in resp.text
+    assert 'Retry' in resp.text
+    pwa_settings = PwaSettings.singleton()
+    pwa_settings.offline_text = 'You are offline.'
+    pwa_settings.offline_retry_button = False
+    pwa_settings.save()
+
+    resp = app.get('/__pwa__/offline/')
+    assert 'You are offline.' in resp.text
+    assert 'Retry' not in resp.text
-- 
2.20.1

