diff --git a/aleksis/core/schoolapps/settings.py b/aleksis/core/schoolapps/settings.py index 36fd54aa760b3c7a73a72754b972a28b659b2241..5d59cc743dcd60426eee3f471799a3bbbe5370e7 100644 --- a/aleksis/core/schoolapps/settings.py +++ b/aleksis/core/schoolapps/settings.py @@ -192,36 +192,6 @@ else: } } -# PWA -PWA_APP_NAME = 'SchoolApps' -PWA_APP_DESCRIPTION = "Eine Sammlung an nützlichen Apps für den Schulalltag am Katharineum zu Lübeck" -PWA_APP_THEME_COLOR = '#da1f3d' -PWA_APP_BACKGROUND_COLOR = '#ffffff' -PWA_APP_DISPLAY = 'standalone' -PWA_APP_SCOPE = '/' -PWA_APP_ORIENTATION = 'any' -PWA_APP_START_URL = '/' -PWA_APP_ICONS = [ - { - "src": "/static/icons/android_192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/static/icons/android_512.png", - "sizes": "512x512", - "type": "image/png" - } -] -PWA_APP_SPLASH_SCREEN = [ - { - 'src': '/static/icons/android_512.png', - 'media': '(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)' - } -] -PWA_APP_DIR = 'ltr' -PWA_SERVICE_WORKER_PATH = os.path.join(BASE_DIR, 'static/common', 'serviceworker.js') -PWA_APP_LANG = 'de-DE' LOGGING = { 'version': 1, diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 55f09f24db5d70baba3d678f59773f41209a4448..8fe5e7f5e8356e78b22127291ffc21f48c0a01d5 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -72,7 +72,8 @@ INSTALLED_APPS = [ "aleksis.core", "impersonate", "two_factor", - "material" + "material", + "pwa" ] INSTALLED_APPS += get_app_packages() @@ -84,7 +85,6 @@ STATICFILES_FINDERS = [ "sass_processor.finders.CssFinder", ] - MIDDLEWARE = [ # 'django.middleware.cache.UpdateCacheMiddleware', "django.middleware.security.SecurityMiddleware", @@ -125,15 +125,14 @@ TEMPLATES = [ ] THUMBNAIL_PROCESSORS = ( - "image_cropping.thumbnail_processors.crop_corners", -) + thumbnail_settings.THUMBNAIL_PROCESSORS + "image_cropping.thumbnail_processors.crop_corners", + ) + thumbnail_settings.THUMBNAIL_PROCESSORS # Already included by base template / Bootstrap IMAGE_CROPPING_JQUERY_URL = None WSGI_APPLICATION = "aleksis.core.wsgi.application" - # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases @@ -161,10 +160,10 @@ if _settings.get("caching.memcached.enabled", True): # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",}, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",}, - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",}, - {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Authentication backends are dynamically populated @@ -230,7 +229,8 @@ STATIC_ROOT = _settings.get("static.root", os.path.join(BASE_DIR, "static")) MEDIA_ROOT = _settings.get("media.root", os.path.join(BASE_DIR, "media")) NODE_MODULES_ROOT = _settings.get("node_modules.root", os.path.join(BASE_DIR, "node_modules")) -YARN_INSTALLED_APPS = ["datatables", "highlight.js", "jquery", "manup", "materialize-css", "moment", "popper.js", "prop-types", "react", "react-dom", "material-design-icons-iconfont", "select2"] +YARN_INSTALLED_APPS = ["datatables", "highlight.js", "jquery", "manup", "materialize-css", "moment", "popper.js", + "prop-types", "react", "react-dom", "material-design-icons-iconfont", "select2"] JS_URL = _settings.get("js_assets.url", STATIC_URL) JS_ROOT = _settings.get("js_assets.root", NODE_MODULES_ROOT + "/node_modules") @@ -304,3 +304,49 @@ if _settings.get("2fa.twilio.sid", None): TWILIO_CALLER_ID = _settings.get("2fa.twilio.callerid") _settings.populate_obj(sys.modules[__name__]) + +PWA_APP_NAME = 'AlekSIS' # dbsettings +PWA_APP_DESCRIPTION = "AlekSIS – The free school information system" # dbsettings +PWA_APP_THEME_COLOR = _settings.get("pwa.color", "#da1f3d") # dbsettings +PWA_APP_BACKGROUND_COLOR = '#ffffff' +PWA_APP_DISPLAY = 'standalone' +PWA_APP_SCOPE = '/' +PWA_APP_ORIENTATION = 'any' +PWA_APP_START_URL = '/' +PWA_APP_ICONS = [ # three icons to upload dbsettings + { + "src": "/static/icons/android_192.png", + "sizes": "192x192" + }, + { + "src": "/static/icons/android_512.png", + "sizes": "512x512" + } +] +PWA_APP_ICONS_APPLE = [ + { + "src": "/static/icons/apple_76.png", + "sizes": "76x76" + }, + { + "src": "/static/icons/apple_114.png", + "sizes": "114x114" + }, + { + "src": "/static/icons/apple_152.png", + "sizes": "152x152" + }, + { + "src": "/static/icons/apple_180.png", + "sizes": "180x180" + }, +] +PWA_APP_SPLASH_SCREEN = [ + { + 'src': '/static/icons/android_512.png', + 'media': '(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)' + } +] +PWA_APP_DIR = 'ltr' +PWA_SERVICE_WORKER_PATH = os.path.join(STATIC_ROOT, 'js', 'serviceworker.js') +# PWA_APP_LANG = 'de-DE' diff --git a/aleksis/core/static/common/favicon.ico b/aleksis/core/static/common/favicon.ico deleted file mode 100644 index 1405a12165083ca70bcdfffeacc527f5986a3785..0000000000000000000000000000000000000000 Binary files a/aleksis/core/static/common/favicon.ico and /dev/null differ diff --git a/aleksis/core/static/common/logo.png b/aleksis/core/static/common/logo.png deleted file mode 100644 index 005e0b6c64a3475a0ccaa875caaf7673c14b8fd2..0000000000000000000000000000000000000000 Binary files a/aleksis/core/static/common/logo.png and /dev/null differ diff --git a/aleksis/core/static/common/serviceworker.js b/aleksis/core/static/common/serviceworker.js deleted file mode 100644 index 76a52613b4a8a08e45d06b1fdcc54a74e47f5913..0000000000000000000000000000000000000000 --- a/aleksis/core/static/common/serviceworker.js +++ /dev/null @@ -1,115 +0,0 @@ -//This is the SchoolApps service worker - -const CACHE = "schoolapps-cache"; - -const precacheFiles = [ - '', - '/faq/', -]; - -const offlineFallbackPage = '/offline'; - -const avoidCachingPaths = [ - '/admin', - '/settings', - '/support', - '/tools', - '/faq/ask', - '/aub/apply_for', - '/aub/check1', - '/aub/check2', - '/aktuell.pdf', - '/accounts/login', - '/timetable/aktuell.pdf', - '/api', -]; - -function pathComparer(requestUrl, pathRegEx) { - return requestUrl.match(new RegExp(pathRegEx)); -} - -function comparePaths(requestUrl, pathsArray) { - if (requestUrl) { - for (let index = 0; index < pathsArray.length; index++) { - const pathRegEx = pathsArray[index]; - if (pathComparer(requestUrl, pathRegEx)) { - return true; - } - } - } - - return false; -} - -self.addEventListener("install", function (event) { - console.log("[SchoolApps PWA] Install Event processing."); - - console.log("[SchoolApps PWA] Skipping waiting on install."); - self.skipWaiting(); - - event.waitUntil( - caches.open(CACHE).then(function (cache) { - console.log("[SchoolApps PWA] Caching pages during install."); - - return cache.addAll(precacheFiles).then(function () { - return cache.add(offlineFallbackPage); - }); - }) - ); -}); - -// Allow sw to control of current page -self.addEventListener("activate", function (event) { - console.log("[SchoolApps PWA] Claiming clients for current page."); - event.waitUntil(self.clients.claim()); -}); - -// If any fetch fails, it will look for the request in the cache and serve it from there first -self.addEventListener("fetch", function (event) { - if (event.request.method !== "GET") return; - networkFirstFetch(event); -}); - -function networkFirstFetch(event) { - event.respondWith( - fetch(event.request) - .then(function (response) { - // If request was successful, add or update it in the cache - console.log("[SchoolApps PWA] Network request successful."); - event.waitUntil(updateCache(event.request, response.clone())); - return response; - }) - .catch(function (error) { - console.log("[SchoolApps PWA] Network request failed. Serving content from cache: " + error); - return fromCache(event); - }) - ); -} - -function fromCache(event) { - // Check to see if you have it in the cache - // Return response - // If not in the cache, then return offline fallback page - return caches.open(CACHE).then(function (cache) { - return cache.match(event.request) - .then(function (matching) { - if (!matching || matching.status === 404) { - console.log("[SchoolApps PWA] Cache request failed. Serving offline fallback page."); - // Use the precached offline page as fallback - return caches.match(offlineFallbackPage) - } - - return matching; - }); - }); -} - -function updateCache(request, response) { - if (!comparePaths(request.url, avoidCachingPaths)) { - return caches.open(CACHE).then(function (cache) { - return cache.put(request, response); - }); - } - - return Promise.resolve(); -} diff --git a/aleksis/core/static/js/serviceworker.js b/aleksis/core/static/js/serviceworker.js new file mode 100644 index 0000000000000000000000000000000000000000..93d9797c78db9756cba6f1f6caa46fc8c14ae57b --- /dev/null +++ b/aleksis/core/static/js/serviceworker.js @@ -0,0 +1,105 @@ +// This is the AlekSIS service worker + +const CACHE = "aleksis-cache"; + +const precacheFiles = [ + '', +]; + +const offlineFallbackPage = '/offline'; + +const avoidCachingPaths = [ + '/admin', + '/settings', + '/accounts/login' +]; // TODO: More paths are needed + +function pathComparer(requestUrl, pathRegEx) { + return requestUrl.match(new RegExp(pathRegEx)); +} + +function comparePaths(requestUrl, pathsArray) { + if (requestUrl) { + for (let index = 0; index < pathsArray.length; index++) { + const pathRegEx = pathsArray[index]; + if (pathComparer(requestUrl, pathRegEx)) { + return true; + } + } + } + + return false; +} + +self.addEventListener("install", function (event) { + console.log("[AlekSIS PWA] Install Event processing."); + + console.log("[AlekSIS PWA] Skipping waiting on install."); + self.skipWaiting(); + + event.waitUntil( + caches.open(CACHE).then(function (cache) { + console.log("[AlekSIS PWA] Caching pages during install."); + + return cache.addAll(precacheFiles).then(function () { + return cache.add(offlineFallbackPage); + }); + }) + ); +}); + +// Allow sw to control of current page +self.addEventListener("activate", function (event) { + console.log("[AlekSIS PWA] Claiming clients for current page."); + event.waitUntil(self.clients.claim()); +}); + +// If any fetch fails, it will look for the request in the cache and serve it from there first +self.addEventListener("fetch", function (event) { + if (event.request.method !== "GET") return; + networkFirstFetch(event); +}); + +function networkFirstFetch(event) { + event.respondWith( + fetch(event.request) + .then(function (response) { + // If request was successful, add or update it in the cache + console.log("[AlekSIS PWA] Network request successful."); + event.waitUntil(updateCache(event.request, response.clone())); + return response; + }) + .catch(function (error) { + console.log("[AlekSIS PWA] Network request failed. Serving content from cache: " + error); + return fromCache(event); + }) + ); +} + +function fromCache(event) { + // Check to see if you have it in the cache + // Return response + // If not in the cache, then return offline fallback page + return caches.open(CACHE).then(function (cache) { + return cache.match(event.request) + .then(function (matching) { + if (!matching || matching.status === 404) { + console.log("[AlekSIS PWA] Cache request failed. Serving offline fallback page."); + // Use the precached offline page as fallback + return caches.match(offlineFallbackPage) + } + + return matching; + }); + }); +} + +function updateCache(request, response) { + if (!comparePaths(request.url, avoidCachingPaths)) { + return caches.open(CACHE).then(function (cache) { + return cache.put(request, response); + }); + } + + return Promise.resolve(); +} diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html index 62635310eb8c158a9efb816b9d9a9d04d6bb1c91..854a16afb3c44f281c84bcd93b704b8c66faf3f4 100644 --- a/aleksis/core/templates/core/base.html +++ b/aleksis/core/templates/core/base.html @@ -1,7 +1,7 @@ {# -*- engine:django -*- #} -{% load i18n menu_generator static sass_tags any_js %} +{% load i18n menu_generator static sass_tags any_js pwa %} <!DOCTYPE html> @@ -14,7 +14,13 @@ <title>School Information System</title> - {% include 'core/icons.html' %} + {# Favicons #} + <link href="{% static "icons/favicon_16.png" %}" rel="icon" type="image/png" sizes="16x16"> + <link href="{% static "icons/favicon_32.png" %}" rel="icon" type="image/png" sizes="32x32"> + <link href="{% static "icons/favicon_48.png" %}" rel="icon" type="image/png" sizes="48x48"> + + <!-- PWA --> + {% progressive_web_app_meta %} {# CSS #} {% include_css "material-design-icons" %} diff --git a/aleksis/core/templates/core/icons.html b/aleksis/core/templates/core/icons.html deleted file mode 100644 index 53e98aadd7f53a25431dfc1fa6d72aa9b53617a1..0000000000000000000000000000000000000000 --- a/aleksis/core/templates/core/icons.html +++ /dev/null @@ -1,7 +0,0 @@ -{# -*- engine:django -*- #} - -{% load static %} - -<link rel="shortcut icon" type="image/png" href="{% static 'img/aleksis-logo.png' %}" /> -<link rel="shortcut icon" sizes="196x196" href="{% static 'img/aleksis-logo.png' %}" /> -<link rel="apple-touch-icon" href="{% static 'img/aleksis-logo.png' %}" /> diff --git a/aleksis/core/templates/partials/header.html b/aleksis/core/templates/partials/header.html index ceb4065c957413766d0716a3bffe7c4a4953d5d9..c3edc1caa05eb2e6863ffef1ea3e7672059b05bb 100644 --- a/aleksis/core/templates/partials/header.html +++ b/aleksis/core/templates/partials/header.html @@ -68,10 +68,8 @@ <!-- Favicon --> <link rel="shortcut icon" type="image/x-icon" href="{% static 'common/favicon.ico' %}"> - <!-- PWA --> - {% progressive_web_app_meta %} - <!---------> + <!---------> <!-- CSS --> <!---------> <link href="{% static 'css/materialdesignicons-webfont/material-icons.css' %}" rel="stylesheet"> diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 17fce01f776afbfbdc6bb2eb83cd5a23d7cb4ce5..2e3b0c98dbdd9831cc7d2e5a19d2511910c64e63 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -11,6 +11,7 @@ from two_factor.urls import urlpatterns as tf_urls from . import views urlpatterns = [ + path("", include("pwa.urls"), name="pwa"), path('offline/', views.offline, name='offline'), path("admin/", admin.site.urls), path("data_management/", views.data_management, name="data_management"), diff --git a/poetry.lock b/poetry.lock index 4c091fe70c03b2ae1ba6f84157c3d5b05c19906e..cd62881947c361493e1275b5b46b8a8bec59cb7b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -161,9 +161,6 @@ optional = false python-versions = "*" version = "5.0.6" -[package.dependencies] -six = "*" - [[package]] category = "dev" description = "Code coverage measurement for Python" @@ -427,6 +424,17 @@ version = ">=7.0.2" phonenumbers = ["phonenumbers (>=7.0.2)"] phonenumberslite = ["phonenumberslite (>=7.0.2)"] +[[package]] +category = "main" +description = "A Django app to include a manifest.json and Service Worker instance to enable progressive web app behavior" +name = "django-pwa" +optional = false +python-versions = "*" +version = "1.0.6" + +[package.dependencies] +django = ">=1.8" + [[package]] category = "main" description = "SASS processor to compile SCSS files into *.css, while rendering, or offline." @@ -1226,7 +1234,7 @@ category = "main" description = "YAML parser and emitter for Python" name = "pyyaml" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" version = "5.2" [[package]] @@ -1650,7 +1658,7 @@ testing = ["pathlib2", "contextlib2", "unittest2"] ldap = ["django-auth-ldap"] [metadata] -content-hash = "40917beab5394838573def3e2c8a6719f37040d826df74b3cdc9b52262ad0290" +content-hash = "7c5cc3497e09ab1ef53c542aeba9787d5045524e6f1a9c299454f18aa4d592a6" python-versions = "^3.7" [metadata.files] @@ -1828,6 +1836,10 @@ django-phonenumber-field = [ {file = "django-phonenumber-field-3.0.1.tar.gz", hash = "sha256:794ebbc3068a7af75aa72a80cb0cec67e714ff8409a965968040f1fd210b2d97"}, {file = "django_phonenumber_field-3.0.1-py3-none-any.whl", hash = "sha256:1ab19f723928582fed412bd9844221fa4ff466276d8526b8b4a9913ee1487c5e"}, ] +django-pwa = [ + {file = "django-pwa-1.0.6.tar.gz", hash = "sha256:b3f1ad0c5241fae4c7505423540de4db93077d7c88416ff6d2af545ffe209f34"}, + {file = "django_pwa-1.0.6-py3-none-any.whl", hash = "sha256:9306105fcb637ae16fea6527be4b147d45fd53db85efb1d4f61dfea6bf793e56"}, +] django-sass-processor = [ {file = "django-sass-processor-0.8.tar.gz", hash = "sha256:e039551994feaaba6fcf880412b25a772dd313162a34cbb4289814988cfae340"}, ] @@ -2402,7 +2414,6 @@ urllib3 = [ ] wcwidth = [ {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, - {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, ] yubiotp = [ {file = "YubiOTP-0.2.2.post1-py2.py3-none-any.whl", hash = "sha256:7e281801b24678f4bda855ce8ab975a7688a912f5a6cb22b6c2b16263a93cbd2"}, diff --git a/pyproject.toml b/pyproject.toml index a1b518307a593aaca9af20c67dc0027166e5a8cc..ea95aa00dd4b6873da4982ec10d5d8203693c190 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ django-two-factor-auth = { version = "^1.10.0", extras = [ "YubiKey", "phonenumb django-yarnpkg = "^6.0" django-dbsettings = "^1.0.0" django-material = "^1.6.0" +django-pwa = "^1.0.6" [tool.poetry.extras] ldap = ["django-auth-ldap"] diff --git a/requirements.txt b/requirements.txt index 8b767d1c91300b74fb4bce041688565787ae8949..08d6e1aa88a09416668bf8e0238fe0ef44cff4bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,5 @@ django-filter django_react_templatetags PyPDF2 martor -django-pwa django_widget_tweaks ics