Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4d0db5bea | ||
|
|
79c6963219 | ||
|
|
73151d2585 | ||
|
|
93133a3292 | ||
|
|
662cf155d7 | ||
|
|
0d31371002 | ||
|
|
43cb44b5c0 | ||
|
|
c7e9813ac5 | ||
|
|
ed2caecbd7 | ||
|
|
cd144e2ef7 | ||
|
|
625eee756c | ||
|
|
eba9fe5951 | ||
|
|
7c0df3201e | ||
|
|
fc56890d83 | ||
|
|
45d344503f | ||
|
|
86ed4b6504 | ||
|
|
e45b83d54c |
2
.coveragerc
Normal file
@@ -0,0 +1,2 @@
|
||||
[run]
|
||||
source=pwa
|
||||
1
.gitignore
vendored
@@ -6,3 +6,4 @@ build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.tox/
|
||||
.coverage
|
||||
@@ -7,4 +7,9 @@ install:
|
||||
- pip install Django==$DJANGO_VERSION
|
||||
- pip install tox-travis
|
||||
- pip install -q -r requirements-dev.txt
|
||||
script: tox
|
||||
- pip install coverage
|
||||
- pip install codecov
|
||||
script:
|
||||
- coverage run runtests.py
|
||||
after_success:
|
||||
- codecov
|
||||
14
CHANGELOG.md
@@ -9,4 +9,18 @@
|
||||
- Unit tests
|
||||
- Add Oritentation on manifest.json
|
||||
- Add tox
|
||||
- Add Coverage
|
||||
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Add django 2 requirement
|
||||
- Use templateviews instead of own implementations
|
||||
- Add content_types
|
||||
- Update `README.md`
|
||||
- Add `PWA_APP_FETCH_URL`
|
||||
- Add default_config in `__init__.py`
|
||||
- Add basic serviceworker
|
||||
- Add default offline page and default icons
|
||||
- Updated the unit tests
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
include *.md
|
||||
recursive-include pwa/templates *
|
||||
recursive-include pwa/templates *
|
||||
recursive-include pwa/static *
|
||||
81
README.md
@@ -1,5 +1,11 @@
|
||||
django-pwa
|
||||
=====
|
||||
[](https://travis-ci.org/silviolleite/django-pwa)
|
||||
[](https://codeclimate.com/github/silviolleite/django-pwa/maintainability)
|
||||
[](https://codecov.io/gh/silviolleite/django-pwa)
|
||||
[](https://pypi.org/project/django-pwa/)
|
||||
[](https://pypi.org/project/django-pwa)
|
||||
|
||||
This Django app turns your project into a [progressive web app](https://developers.google.com/web/progressive-web-apps/). Navigating to your site on an Android phone will prompt you to add the app to your home screen.
|
||||
|
||||

|
||||
@@ -54,11 +60,11 @@ All settings are optional, and the app will work fine with its internal defaults
|
||||
|
||||
Add the progressive web app URLs to urls.py:
|
||||
```python
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
...
|
||||
url('', include('pwa.urls')), # You MUST use an empty string as the URL prefix
|
||||
path('', include('pwa.urls')), # You MUST use an empty string as the URL prefix
|
||||
...
|
||||
]
|
||||
```
|
||||
@@ -80,18 +86,85 @@ While running the Django test server:
|
||||
|
||||
1. Verify that `/manifest.json` is being served
|
||||
1. Verify that `/serviceworker.js` is being served
|
||||
1. Verify that `/offline` is being served
|
||||
1. Use the Application tab in the Chrome Developer Tools to verify the progressive web app is configured correctly.
|
||||
1. Use the "Add to homescreen" link on the Application Tab to verify you can add the app successfully.
|
||||
|
||||
|
||||
The Service Worker
|
||||
=====
|
||||
By default, the service worker implemented by this app is:
|
||||
```js
|
||||
// Base Service Worker implementation. To use your own Service Worker, set the PWA_SERVICE_WORKER_PATH variable in settings.py
|
||||
|
||||
var staticCacheName = "django-pwa-v" + new Date().getTime();
|
||||
var filesToCache = [
|
||||
'/offline',
|
||||
'/css/django-pwa-app.css',
|
||||
'/images/icons/icon-72x72.png',
|
||||
'/images/icons/icon-96x96.png',
|
||||
'/images/icons/icon-128x128.png',
|
||||
'/images/icons/icon-144x144.png',
|
||||
'/images/icons/icon-152x152.png',
|
||||
'/images/icons/icon-192x192.png',
|
||||
'/images/icons/icon-384x384.png',
|
||||
'/images/icons/icon-512x512.png',
|
||||
];
|
||||
|
||||
// Cache on install
|
||||
self.addEventListener("install", event => {
|
||||
this.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(staticCacheName)
|
||||
.then(cache => {
|
||||
return cache.addAll(filesToCache);
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
// Clear cache on activate
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(cacheName => (cacheName.startsWith("django-pwa-")))
|
||||
.filter(cacheName => (cacheName !== staticCacheName))
|
||||
.map(cacheName => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Serve from Cache
|
||||
self.addEventListener("fetch", event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
return response || fetch(event.request);
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match('offline');
|
||||
})
|
||||
)
|
||||
});
|
||||
```
|
||||
|
||||
Adding Your Own Service Worker
|
||||
=====
|
||||
By default, the service worker implemented by this app is empty. To add service worker functionality, you'll want to create a `serviceworker.js` or similarly named file, and then point at it using the PWA_SERVICE_WORKER_PATH variable.
|
||||
By default, the service worker implemented by this app is empty. To add service worker functionality, you'll want to create a `serviceworker.js` or similarly named template in a template directory, and then point at it using the PWA_SERVICE_WORKER_PATH variable (PWA_APP_FETCH_URL is passed through).
|
||||
|
||||
```python
|
||||
PWA_SERVICE_WORKER_PATH = os.path.join(BASE_DIR, 'my_app', 'serviceworker.js')
|
||||
PWA_SERVICE_WORKER_PATH = 'my_app/serviceworker.js'
|
||||
|
||||
```
|
||||
|
||||
The offline view
|
||||
=====
|
||||
By default, the offline view is implemented in `templates/offline.html`
|
||||
You can overwrite it in a template directory if you continue using the default `serviceworker.js`.
|
||||
|
||||
|
||||
Feedback
|
||||
=====
|
||||
I welcome your feedback and pull requests. Enjoy!
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
default_app_config = 'pwa.apps.PwaConfig'
|
||||
@@ -1,10 +1,9 @@
|
||||
""" Settings required by django-progressive-web-app. """
|
||||
""" Settings required by django-app. """
|
||||
from django.conf import settings
|
||||
import os
|
||||
|
||||
# Path to the service worker implementation. Default implementation is empty.
|
||||
PWA_SERVICE_WORKER_PATH = getattr(settings, 'PWA_SERVICE_WORKER_PATH',
|
||||
os.path.join(os.path.abspath(os.path.dirname(__file__)), 'templates', 'serviceworker.js'))
|
||||
PWA_SERVICE_WORKER_PATH = getattr(settings, 'PWA_SERVICE_WORKER_PATH', 'serviceworker.js')
|
||||
|
||||
# App parameters to include in manifest.json and appropriate meta tags
|
||||
PWA_APP_NAME = getattr(settings, 'PWA_APP_NAME', 'MyApp')
|
||||
@@ -15,10 +14,39 @@ PWA_APP_BACKGROUND_COLOR = getattr(settings, 'PWA_APP_BACKGROUND_COLOR', '#fff')
|
||||
PWA_APP_DISPLAY = getattr(settings, 'PWA_APP_DISPLAY', 'standalone')
|
||||
PWA_APP_ORIENTATION = getattr(settings, 'PWA_APP_ORIENTATION', 'any')
|
||||
PWA_APP_START_URL = getattr(settings, 'PWA_APP_START_URL', '/')
|
||||
PWA_APP_FETCH_URL = getattr(settings, 'PWA_APP_FETCH_URL', '/')
|
||||
PWA_APP_ICONS = getattr(settings, 'PWA_APP_ICONS', [
|
||||
{
|
||||
'src': '/',
|
||||
'sizes': '160x160'
|
||||
'src': '/static/images/icons/icon-72x72.png',
|
||||
'sizes': '72x72'
|
||||
},
|
||||
{
|
||||
'src': '/static/images/icons/icon-96x96.png',
|
||||
'sizes': '96x96'
|
||||
},
|
||||
{
|
||||
'src': '/static/images/icons/icon-128x128.png',
|
||||
'sizes': '128x128'
|
||||
},
|
||||
{
|
||||
'src': '/static/images/icons/icon-144x144.png',
|
||||
'sizes': '144x144'
|
||||
},
|
||||
{
|
||||
'src': '/static/images/icons/icon-152x152.png',
|
||||
'sizes': '152x152'
|
||||
},
|
||||
{
|
||||
'src': '/static/images/icons/icon-192x192.png',
|
||||
'sizes': '192x192'
|
||||
},
|
||||
{
|
||||
'src': '/static/images/icons/icon-384x384.png',
|
||||
'sizes': '384x384'
|
||||
},
|
||||
{
|
||||
'src': '/static/images/icons/icon-512x512.png',
|
||||
'sizes': '512x512'
|
||||
}
|
||||
])
|
||||
PWA_APP_DIR = getattr(settings, 'PWA_APP_DIR', 'auto')
|
||||
|
||||
5
pwa/static/css/django-pwa-app.css
Normal file
BIN
pwa/static/images/icons/Thumbs.db
Normal file
BIN
pwa/static/images/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
pwa/static/images/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
pwa/static/images/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
pwa/static/images/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
pwa/static/images/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
pwa/static/images/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
pwa/static/images/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
pwa/static/images/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
11
pwa/templates/offline.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% load static %}<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Default offline template</title>
|
||||
<link href="{% static '/css/django-pwa-app.css' %}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<h1>You are currently not connected to any networks.</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,2 +1,53 @@
|
||||
// Empty Service Worker implementation. To use your own Service Worker, set the PWA_SERVICE_WORKER_PATH variable in
|
||||
// settings.py
|
||||
// Base Service Worker implementation. To use your own Service Worker, set the PWA_SERVICE_WORKER_PATH variable in settings.py
|
||||
|
||||
var staticCacheName = "django-pwa-v" + new Date().getTime();
|
||||
var filesToCache = [
|
||||
'/offline',
|
||||
'/static/css/django-pwa-app.css',
|
||||
'/static/images/icons/icon-72x72.png',
|
||||
'/static/images/icons/icon-96x96.png',
|
||||
'/static/images/icons/icon-128x128.png',
|
||||
'/static/images/icons/icon-144x144.png',
|
||||
'/static/images/icons/icon-152x152.png',
|
||||
'/static/images/icons/icon-192x192.png',
|
||||
'/static/images/icons/icon-384x384.png',
|
||||
'/static/images/icons/icon-512x512.png',
|
||||
];
|
||||
|
||||
// Cache on install
|
||||
self.addEventListener("install", event => {
|
||||
this.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(staticCacheName)
|
||||
.then(cache => {
|
||||
return cache.addAll(filesToCache);
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
// Clear cache on activate
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(cacheName => (cacheName.startsWith("django-pwa-")))
|
||||
.filter(cacheName => (cacheName !== staticCacheName))
|
||||
.map(cacheName => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Serve from Cache
|
||||
self.addEventListener("fetch", event => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
return response || fetch(event.request);
|
||||
})
|
||||
.catch(() => {
|
||||
return caches.match('offline');
|
||||
})
|
||||
)
|
||||
});
|
||||
10
pwa/urls.py
@@ -1,8 +1,10 @@
|
||||
from django.conf.urls import url
|
||||
from .views import manifest, service_worker
|
||||
from django.urls import path
|
||||
|
||||
from .views import Manifest, ServiceWorker, OfflineView
|
||||
|
||||
# Serve up serviceworker.js and manifest.json at the root
|
||||
urlpatterns = [
|
||||
url('^serviceworker.js$', service_worker, name="serviceworker"),
|
||||
url('^manifest.json$', manifest, name="manifest")
|
||||
path('serviceworker.js', ServiceWorker.as_view(), name='serviceworker'),
|
||||
path('manifest.json', Manifest.as_view(), name='manifest'),
|
||||
path('offline', OfflineView.as_view(), name='offline')
|
||||
]
|
||||
|
||||
32
pwa/views.py
@@ -1,17 +1,27 @@
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from . import app_settings
|
||||
|
||||
|
||||
def service_worker(request):
|
||||
response = HttpResponse(open(app_settings.PWA_SERVICE_WORKER_PATH).read(), content_type='application/javascript')
|
||||
return response
|
||||
class ServiceWorker(TemplateView):
|
||||
content_type = 'application/javascript'
|
||||
template_name = app_settings.PWA_SERVICE_WORKER_PATH
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['PWA_APP_FETCH_URL'] = app_settings.PWA_APP_FETCH_URL
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
def manifest(request):
|
||||
return render(request, 'manifest.json', {
|
||||
setting_name: getattr(app_settings, setting_name)
|
||||
for setting_name in dir(app_settings)
|
||||
if setting_name.startswith('PWA_')
|
||||
})
|
||||
class Manifest(TemplateView):
|
||||
content_type = 'application/json'
|
||||
template_name = 'manifest.json'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
for setting_name in dir(app_settings):
|
||||
if setting_name.startswith('PWA_'):
|
||||
kwargs[setting_name] = getattr(app_settings, setting_name)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class OfflineView(TemplateView):
|
||||
template_name = "offline.html"
|
||||
12
setup.py
@@ -10,16 +10,21 @@ try:
|
||||
import pypandoc
|
||||
|
||||
long_description = pypandoc.convert('README.md', 'rst')
|
||||
except:
|
||||
except RuntimeError:
|
||||
long_description = short_description
|
||||
|
||||
# allow setup.py to be run from any path
|
||||
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
||||
|
||||
install_requirements = [
|
||||
"django>=2",
|
||||
]
|
||||
|
||||
setup(
|
||||
name='django-pwa',
|
||||
version='1.0.0',
|
||||
version='1.0.1',
|
||||
packages=find_packages(),
|
||||
install_requires=install_requirements,
|
||||
include_package_data=True,
|
||||
license='MIT License',
|
||||
description=short_description,
|
||||
@@ -30,7 +35,8 @@ setup(
|
||||
classifiers=[
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 2.0',
|
||||
'Framework :: Django :: 2.1',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
|
||||
@@ -33,4 +33,6 @@ DATABASES = {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'mydatabase',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
@@ -15,6 +15,7 @@ class AppSettingsTest(TestCase):
|
||||
'PWA_APP_DISPLAY',
|
||||
'PWA_APP_ORIENTATION',
|
||||
'PWA_APP_START_URL',
|
||||
'PWA_APP_FETCH_URL',
|
||||
'PWA_APP_ICONS',
|
||||
'PWA_APP_DIR',
|
||||
'PWA_APP_LANG'
|
||||
|
||||
@@ -14,7 +14,14 @@ class CreateMetaTemplateTagTest(TestCase):
|
||||
def test_has_tags(self):
|
||||
"""Must contains the tags in HTML"""
|
||||
tags = [
|
||||
'<link rel="apple-touch-icon" href="/" sizes="160x160">',
|
||||
'<link rel="apple-touch-icon" href="/static/images/icons/icon-72x72.png" sizes="72x72">',
|
||||
'<link rel="apple-touch-icon" href="/static/images/icons/icon-96x96.png" sizes="96x96">',
|
||||
'<link rel="apple-touch-icon" href="/static/images/icons/icon-128x128.png" sizes="128x128">',
|
||||
'<link rel="apple-touch-icon" href="/static/images/icons/icon-144x144.png" sizes="144x144">',
|
||||
'<link rel="apple-touch-icon" href="/static/images/icons/icon-152x152.png" sizes="152x152">',
|
||||
'<link rel="apple-touch-icon" href="/static/images/icons/icon-192x192.png" sizes="192x192">',
|
||||
'<link rel="apple-touch-icon" href="/static/images/icons/icon-384x384.png" sizes="384x384">',
|
||||
'<link rel="apple-touch-icon" href="/static/images/icons/icon-512x512.png" sizes="512x512">',
|
||||
'<link rel="manifest" href="/manifest.json">',
|
||||
'<meta name="theme-color" content="#000">',
|
||||
'<meta name="apple-mobile-web-app-capable" content="yes">',
|
||||
|
||||
@@ -41,3 +41,13 @@ class ManifestTest(TestCase):
|
||||
for expected in contents:
|
||||
with self.subTest():
|
||||
self.assertContains(self.response, expected)
|
||||
|
||||
|
||||
class OfflineTest(TestCase):
|
||||
def setUp(self):
|
||||
self.response = self.client.get(r('offline'))
|
||||
|
||||
def test_get(self):
|
||||
"""GET /offline Should return status code 200"""
|
||||
self.assertEqual(200, self.response.status_code)
|
||||
|
||||
|
||||
8
tox.ini
@@ -1,5 +1,7 @@
|
||||
[tox]
|
||||
envlist = py36-django{20}
|
||||
envlist =
|
||||
py36-django{20}
|
||||
py36-django{21}
|
||||
|
||||
[testenv]
|
||||
commands = python runtests.py
|
||||
@@ -7,4 +9,6 @@ setenv =
|
||||
DJANGO_SETTINGS_MODULE=tests.settings
|
||||
PYTHONPATH={toxinidir}
|
||||
basepython = py36: python3.6
|
||||
deps = django20: Django==2.0
|
||||
deps =
|
||||
django20: Django==2.0
|
||||
django21: Django==2.1
|
||||