diff --git a/dockerfile-dev-with-volumes/README.adoc b/dockerfile-dev-with-volumes/README.adoc index 978bd698ed..8dc7408708 100755 --- a/dockerfile-dev-with-volumes/README.adoc +++ b/dockerfile-dev-with-volumes/README.adoc @@ -108,6 +108,13 @@ MIGRATION_MODULES = {'flatpages': 'pod.db_migrations'} # pour avoir le maximum de log sur la console LOGGING = {} +# PUSH NOTIFICATIONS +# Les clés VAPID peuvent être générées avec https://web-push-codelab.glitch.me/ +WEBPUSH_SETTINGS = { + "VAPID_PUBLIC_KEY": "", + "VAPID_PRIVATE_KEY": "", + "VAPID_ADMIN_EMAIL": "contact@esup-portail.org" +} ---- == Commandes diff --git a/pod/authentication/forms.py b/pod/authentication/forms.py index 1a917c0a1b..2e5d4348a1 100644 --- a/pod/authentication/forms.py +++ b/pod/authentication/forms.py @@ -49,6 +49,15 @@ class Meta(object): fields = [] +class SetNotificationForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super(SetNotificationForm, self).__init__(*args, **kwargs) + + class Meta(object): + model = Owner + fields = ["accepts_notifications"] + + User = get_user_model() diff --git a/pod/authentication/models.py b/pod/authentication/models.py index b788045410..97928ea925 100644 --- a/pod/authentication/models.py +++ b/pod/authentication/models.py @@ -105,6 +105,12 @@ class Owner(models.Model): ) accessgroups = models.ManyToManyField("authentication.AccessGroup", blank=True) sites = models.ManyToManyField(Site) + accepts_notifications = models.BooleanField( + verbose_name=_("Accept notifications"), + default=None, + null=True, + help_text=_("Get push notifications from your devices."), + ) class Meta: verbose_name = _("Owner") diff --git a/pod/locale/fr/LC_MESSAGES/django.mo b/pod/locale/fr/LC_MESSAGES/django.mo index 9cbc9898ea..34cf0d3dbd 100644 Binary files a/pod/locale/fr/LC_MESSAGES/django.mo and b/pod/locale/fr/LC_MESSAGES/django.mo differ diff --git a/pod/locale/fr/LC_MESSAGES/django.po b/pod/locale/fr/LC_MESSAGES/django.po index e886257365..2cc56bca81 100644 --- a/pod/locale/fr/LC_MESSAGES/django.po +++ b/pod/locale/fr/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: Pod\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-01 09:54+0000\n" +"POT-Creation-Date: 2023-09-04 12:50+0000\n" "PO-Revision-Date: \n" "Last-Translator: obado \n" "Language-Team: Pod Team pod@esup-portail.org\n" @@ -100,6 +100,14 @@ msgstr "Commentaire" msgid "Picture" msgstr "Image" +#: pod/authentication/models.py +msgid "Accept notifications" +msgstr "Accepter les notifications" + +#: pod/authentication/models.py +msgid "Get push notifications from your devices." +msgstr "Recevez des notifications push sur vos appareils" + #: pod/authentication/models.py pod/live/models.py pod/meeting/models.py #: pod/podfile/models.py pod/video/admin.py pod/video/models.py #: pod/video_search/templates/search/search.html @@ -4727,6 +4735,10 @@ msgstr "Mes fichiers" msgid "Claim a record" msgstr "Revendiquer un enregistrement" +#: pod/main/templates/navbar.html pod/progressive_web_app/templates/debug.html +msgid "Notifications settings" +msgstr "Paramètres de notification" + #: pod/main/templates/navbar.html msgid "Log out" msgstr "Déconnexion" @@ -6199,6 +6211,26 @@ msgstr "Vous ne pouvez pas voir ce dossier." msgid "You cannot edit this file." msgstr "Vous ne pouvez pas éditer ce fichier." +#: pod/progressive_web_app/templates/notification_toast.html +msgid "Get application notifications" +msgstr "Recevez les notifications de l'application" + +#: pod/progressive_web_app/templates/notification_toast.html +msgid "" +"Get notified for specific events (when one of your video " +"encoding is completed)." +msgstr "" +"Recevez des notifications pour des événements spécifiques (lorsque " +"l'encodage d'une de vos vidéos est terminé)" + +#: pod/progressive_web_app/templates/notification_toast.html +msgid "Allow" +msgstr "Autoriser" + +#: pod/progressive_web_app/templates/notification_toast.html +msgid "Deny on all devices" +msgstr "Refuser sur tous les appareils" + #: pod/recorder/admin.py msgid "Delete selected Recording file treatments + source files" msgstr "" diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.mo b/pod/locale/fr/LC_MESSAGES/djangojs.mo index 3e858ed5d1..00648e7462 100644 Binary files a/pod/locale/fr/LC_MESSAGES/djangojs.mo and b/pod/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.po b/pod/locale/fr/LC_MESSAGES/djangojs.po index ca09391b09..b87850c00f 100644 --- a/pod/locale/fr/LC_MESSAGES/djangojs.po +++ b/pod/locale/fr/LC_MESSAGES/djangojs.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: Esup-Pod\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-01 09:54+0000\n" +"POT-Creation-Date: 2023-09-04 13:15+0000\n" "PO-Revision-Date: \n" "Last-Translator: obado \n" "Language-Team: \n" @@ -550,6 +550,18 @@ msgstr "Ce dossier est vide" msgid "Channel" msgstr "Chaîne" +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "" +"Don't forget to allow notifications from this website in your browser's " +"settings!" +msgstr "" +"N'oubliez pas d'autoriser les notifications provenant de ce site web dans " +"votre navigateur !" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Error during unsubscription..." +msgstr "Erreur durant la désinscription..." + #: pod/video/static/js/ajax-display-channels.js msgid "No channels found" msgstr "Aucun chaîne trouvée" @@ -757,6 +769,10 @@ msgstr "Créer catégorie" msgid "Select the general type of the video." msgstr "Sélectionnez le type général de vidéo." +#: pod/video/static/js/video_edit.js +msgid "Get notified when the video encoding is finished." +msgstr "Recevez une notification lorsque l'encodage de la vidéo est terminé." + #: pod/video/static/js/video_stats_view.js msgid "Title" msgstr "Titre" diff --git a/pod/locale/nl/LC_MESSAGES/django.po b/pod/locale/nl/LC_MESSAGES/django.po index f1ecd0e284..03fcf2a4fe 100644 --- a/pod/locale/nl/LC_MESSAGES/django.po +++ b/pod/locale/nl/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: Pod\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-01 09:54+0000\n" +"POT-Creation-Date: 2023-09-04 12:50+0000\n" "PO-Revision-Date: 2023-06-08 14:37+0200\n" "Last-Translator: obado \n" "Language-Team: \n" @@ -100,6 +100,14 @@ msgstr "" msgid "Picture" msgstr "" +#: pod/authentication/models.py +msgid "Accept notifications" +msgstr "" + +#: pod/authentication/models.py +msgid "Get push notifications from your devices." +msgstr "" + #: pod/authentication/models.py pod/live/models.py pod/meeting/models.py #: pod/podfile/models.py pod/video/admin.py pod/video/models.py #: pod/video_search/templates/search/search.html @@ -4481,6 +4489,10 @@ msgstr "" msgid "Claim a record" msgstr "" +#: pod/main/templates/navbar.html pod/progressive_web_app/templates/debug.html +msgid "Notifications settings" +msgstr "" + #: pod/main/templates/navbar.html msgid "Log out" msgstr "Uitloggen" @@ -5858,6 +5870,24 @@ msgstr "" msgid "You cannot edit this file." msgstr "" +#: pod/progressive_web_app/templates/notification_toast.html +msgid "Get application notifications" +msgstr "" + +#: pod/progressive_web_app/templates/notification_toast.html +msgid "" +"Get notified for specific events (when one of your video " +"encoding is completed)." +msgstr "" + +#: pod/progressive_web_app/templates/notification_toast.html +msgid "Allow" +msgstr "" + +#: pod/progressive_web_app/templates/notification_toast.html +msgid "Deny on all devices" +msgstr "" + #: pod/recorder/admin.py msgid "Delete selected Recording file treatments + source files" msgstr "" diff --git a/pod/locale/nl/LC_MESSAGES/djangojs.po b/pod/locale/nl/LC_MESSAGES/djangojs.po index 1d6fc51ca9..a9df6c9601 100644 --- a/pod/locale/nl/LC_MESSAGES/djangojs.po +++ b/pod/locale/nl/LC_MESSAGES/djangojs.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: Esup-Pod\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-09-01 09:54+0000\n" +"POT-Creation-Date: 2023-09-04 13:15+0000\n" "PO-Revision-Date: 2023-02-08 15:22+0100\n" "Last-Translator: obado \n" "Language-Team: \n" @@ -521,6 +521,16 @@ msgstr "" msgid "This folder is empty" msgstr "" +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "" +"Don't forget to allow notifications from this website in your browser's " +"settings!" +msgstr "" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Error during unsubscription..." +msgstr "" + #: pod/video/static/js/ajax-display-channels.js msgid "Channel" msgstr "" @@ -731,6 +741,10 @@ msgstr "" msgid "Select the general type of the video." msgstr "" +#: pod/video/static/js/video_edit.js +msgid "Get notified when the video encoding is finished." +msgstr "" + #: pod/video/static/js/video_stats_view.js msgid "Title" msgstr "Titel" diff --git a/pod/main/configuration.json b/pod/main/configuration.json index 4cfeef4e92..94f3a85f2b 100644 --- a/pod/main/configuration.json +++ b/pod/main/configuration.json @@ -1714,6 +1714,28 @@ "fr": "Configuration application podfile" } }, + "progressive_web_app": { + "description": {}, + "settings": { + "WEBPUSH_SETTINGS": { + "default_value": "```python\n{\n 'VAPID_PUBLIC_KEY': '',\n 'VAPID_PRIVATE_KEY': '',\n 'VAPID_ADMIN_EMAIL': 'contact@esup-portail.org'\n}\n```", + "description": { + "en": [ + "" + ], + "fr": [ + "Les clés VAPID sont nécessaires à la lib [django-webpush](https://github.com/safwanrahman/django-webpush). Elles peuvent être générées avec [https://web-push-codelab.glitch.me/]()" + ] + }, + "pod_version_end": "", + "pod_version_init": "" + } + }, + "title": { + "en": "", + "fr": "Configuration application progressive_web_app" + } + }, "recorder": { "description": {}, "settings": { @@ -4965,4 +4987,4 @@ } } } -] \ No newline at end of file +] diff --git a/pod/main/settings.py b/pod/main/settings.py index 52ce0ca9ac..3b18ccf5fd 100644 --- a/pod/main/settings.py +++ b/pod/main/settings.py @@ -305,3 +305,9 @@ VIDEO_RECENT_VIEWCOUNT = 180 HONEYPOT_FIELD_NAME = "firstname" + +WEBPUSH_SETTINGS = { + "VAPID_PUBLIC_KEY": "", + "VAPID_PRIVATE_KEY": "", + "VAPID_ADMIN_EMAIL": "contact@esup-portail.org" +} diff --git a/pod/main/templates/base.html b/pod/main/templates/base.html index 7a8cf660b1..d6dac009e4 100644 --- a/pod/main/templates/base.html +++ b/pod/main/templates/base.html @@ -1,4 +1,6 @@ {% load static i18n custom_tags %} +{% load pwa %} +{% load webpush_notifications %} {% get_current_language as LANGUAGE_CODE %} @@ -46,8 +48,9 @@ {% if request.GET.is_iframe %} {% endif %} + {% progressive_web_app_meta %} {% endspaceless %} - + {% webpush_header %} @@ -116,6 +119,7 @@

{{page_title|capfirst}}

{% endif %} + {% include "notification_toast.html" %} {% endblock content %} {% if not request.GET.is_iframe %} @@ -270,4 +274,3 @@

{{page_title|capfirst}}

- diff --git a/pod/main/templates/navbar.html b/pod/main/templates/navbar.html index e6c8b0b6ed..a3915b11fe 100644 --- a/pod/main/templates/navbar.html +++ b/pod/main/templates/navbar.html @@ -243,6 +243,11 @@
{% if user.get_full_name != '' %}{{ user.get_ {% trans 'Perform a BigBlueButton live' %} {% endif %} {% endif %} + {% comment %} Gestion de mon compte {% endcomment %} diff --git a/pod/main/views.py b/pod/main/views.py index bb482bffbe..3c61ba5073 100644 --- a/pod/main/views.py +++ b/pod/main/views.py @@ -34,7 +34,7 @@ from wsgiref.util import FileWrapper from django.db.models import Q, Count from pod.video.models import Video, remove_accents -from pod.authentication.forms import FrontOwnerForm +from pod.authentication.forms import FrontOwnerForm, SetNotificationForm from django.db.models import Sum import os import mimetypes @@ -395,3 +395,24 @@ def userpicture(request): "userpicture/userpicture.html", {"frontOwnerForm": frontOwnerForm}, ) + + +@csrf_protect +@login_required(redirect_field_name="referrer") +def set_notifications(request): + """Sets 'accepts_notifications' attribute on owner instance.""" + setNotificationForm = SetNotificationForm(instance=request.user.owner) + + if request.method == "POST": + setNotificationForm = SetNotificationForm(request.POST, instance=request.user.owner) + if setNotificationForm.is_valid(): + setNotificationForm.save() + return JsonResponse({"success": True, "user_accepts_notifications": request.user.owner.accepts_notifications}) + else: + messages.add_message( + request, + messages.ERROR, + _("One or more errors have been found in the form."), + ) + + return JsonResponse({"success": False}) diff --git a/pod/progressive_web_app/__init__.py b/pod/progressive_web_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pod/progressive_web_app/apps.py b/pod/progressive_web_app/apps.py new file mode 100644 index 0000000000..b01102dde4 --- /dev/null +++ b/pod/progressive_web_app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProgressiveWebAppConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "pod.progressive_web_app" diff --git a/pod/progressive_web_app/static/img/icon_x1024.png b/pod/progressive_web_app/static/img/icon_x1024.png new file mode 100644 index 0000000000..eee5b7eb10 Binary files /dev/null and b/pod/progressive_web_app/static/img/icon_x1024.png differ diff --git a/pod/progressive_web_app/static/img/icon_x128.png b/pod/progressive_web_app/static/img/icon_x128.png new file mode 100644 index 0000000000..526d8d1cbc Binary files /dev/null and b/pod/progressive_web_app/static/img/icon_x128.png differ diff --git a/pod/progressive_web_app/static/img/icon_x192.png b/pod/progressive_web_app/static/img/icon_x192.png new file mode 100644 index 0000000000..216c676a36 Binary files /dev/null and b/pod/progressive_web_app/static/img/icon_x192.png differ diff --git a/pod/progressive_web_app/static/img/icon_x384.png b/pod/progressive_web_app/static/img/icon_x384.png new file mode 100644 index 0000000000..1a47212800 Binary files /dev/null and b/pod/progressive_web_app/static/img/icon_x384.png differ diff --git a/pod/progressive_web_app/static/img/icon_x48.png b/pod/progressive_web_app/static/img/icon_x48.png new file mode 100644 index 0000000000..021dbce6d7 Binary files /dev/null and b/pod/progressive_web_app/static/img/icon_x48.png differ diff --git a/pod/progressive_web_app/static/img/icon_x512.png b/pod/progressive_web_app/static/img/icon_x512.png new file mode 100644 index 0000000000..ffde4e2e40 Binary files /dev/null and b/pod/progressive_web_app/static/img/icon_x512.png differ diff --git a/pod/progressive_web_app/static/img/icon_x72.png b/pod/progressive_web_app/static/img/icon_x72.png new file mode 100644 index 0000000000..15ac15ef9a Binary files /dev/null and b/pod/progressive_web_app/static/img/icon_x72.png differ diff --git a/pod/progressive_web_app/static/img/icon_x96.png b/pod/progressive_web_app/static/img/icon_x96.png new file mode 100644 index 0000000000..976ce886db Binary files /dev/null and b/pod/progressive_web_app/static/img/icon_x96.png differ diff --git a/pod/progressive_web_app/static/img/splash-512.png b/pod/progressive_web_app/static/img/splash-512.png new file mode 100644 index 0000000000..15d9e2c079 Binary files /dev/null and b/pod/progressive_web_app/static/img/splash-512.png differ diff --git a/pod/progressive_web_app/static/js/notification-toast.js b/pod/progressive_web_app/static/js/notification-toast.js new file mode 100644 index 0000000000..8b7e41bfda --- /dev/null +++ b/pod/progressive_web_app/static/js/notification-toast.js @@ -0,0 +1,58 @@ +async function postNotificationPreference(acceptsNotifications, notificationSettingUrl) { + // Post notification setting to form. + let post_url = notificationSettingUrl; + let formData = new FormData(); + + formData.append("accepts_notifications", acceptsNotifications); + formData.append("csrfmiddlewaretoken", Cookies.get("csrftoken")); + const response = await fetch(post_url, { + method: "POST", + body: formData, + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + }); + return response; +} + +async function setPushPreference(webPushAction, notificationSettingUrl) { + // Subscribe or unsubscribe to browser notification service, then ask for browser permission to notify, then set owner attribute. + let wait = ms => new Promise(resolve => setTimeout(resolve, ms)); + const notificationsPreferenceTips = document.querySelector("#notifications-preference-tips"); + const notificationsSpinner = document.querySelector("#notifications-spinner"); + + notificationsSpinner.classList.toggle("d-none"); + webPushAction(registration); + await wait(1000); + let subscription = await registration.pushManager.getSubscription(); + let subscriptionFailed = true; + let acceptsNotifications = false; + let errorMessage = gettext("Error"); + if (webPushAction == subscribe) { + subscriptionFailed = subscription === null || Notification.permission !== "granted"; + acceptsNotifications = true + errorMessage = gettext("Don't forget to allow notifications from this website in your browser's settings!"); + } else if (webPushAction == unsubscribe) { + subscriptionFailed = subscription !== null; + acceptsNotifications = false + errorMessage = gettext("Error during unsubscription..."); + } + + if (subscriptionFailed) { + notificationsPreferenceTips.classList.remove("d-none"); + notificationsPreferenceTips.textContent = errorMessage; + notificationsSpinner.classList.toggle("d-none"); + } else { + const preferenceResponse = await postNotificationPreference(acceptsNotifications, notificationSettingUrl); + if (!preferenceResponse.ok) { + notificationsPreferenceTips.classList.remove("d-none"); + notificationsPreferenceTips.textContent = errorMessage; + notificationsSpinner.classList.toggle("d-none"); + } else { + if (!("d-none" in notificationsPreferenceTips.classList)) notificationsPreferenceTips.classList.add("d-none"); + await wait(500); + notificationsSpinner.classList.toggle("d-none"); + new bootstrap.Toast(document.querySelector('#notification-toast')).hide(); + } + } +} diff --git a/pod/progressive_web_app/static/js/serviceworker.js b/pod/progressive_web_app/static/js/serviceworker.js new file mode 100644 index 0000000000..e7056f1dbc --- /dev/null +++ b/pod/progressive_web_app/static/js/serviceworker.js @@ -0,0 +1,47 @@ +var staticCacheName = "django-pwa-v" + new Date().getTime(); + +var filesToCache = [ +// '/offline', + '/static/img/logoPod.svg', + '/static/img/esup-pod.svg', + '/static/img/pwa-splash-512.png', + '/static/img/icon_x1024.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'); + }) + ) +}); diff --git a/pod/progressive_web_app/templates/debug.html b/pod/progressive_web_app/templates/debug.html new file mode 100644 index 0000000000..c9d15d7b85 --- /dev/null +++ b/pod/progressive_web_app/templates/debug.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% load webpush_notifications %} + +{% block page_title %} + Notifications debug +{% endblock %} + +{% block page_content %} + + +{% endblock page_content %} + +{% block more_script %} + +{% endblock more_script %} diff --git a/pod/progressive_web_app/templates/notification_toast.html b/pod/progressive_web_app/templates/notification_toast.html new file mode 100644 index 0000000000..c9f7edbd2b --- /dev/null +++ b/pod/progressive_web_app/templates/notification_toast.html @@ -0,0 +1,32 @@ +{% load webpush_notifications %} +{% load static %} +{% load i18n %} + + +
+ +
+ + + diff --git a/pod/progressive_web_app/urls.py b/pod/progressive_web_app/urls.py new file mode 100644 index 0000000000..ec1137c017 --- /dev/null +++ b/pod/progressive_web_app/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + + +urlpatterns = [ + path("", views.debug, name="debug"), + path("send", views.send, name="send"), +] diff --git a/pod/progressive_web_app/utils.py b/pod/progressive_web_app/utils.py new file mode 100644 index 0000000000..4de4eb9703 --- /dev/null +++ b/pod/progressive_web_app/utils.py @@ -0,0 +1,15 @@ +from webpush import send_user_notification +from django.templatetags.static import static + + +DEFAULT_ICON = static('img/icon_x1024.png') + + +def notify_user(user, title, message, url=None, icon=None): + payload = { + "head": title, + "body": message, + "url": url, + "icon": icon or DEFAULT_ICON, + } + send_user_notification(user=user, payload=payload, ttl=1000) diff --git a/pod/progressive_web_app/views.py b/pod/progressive_web_app/views.py new file mode 100644 index 0000000000..ea4ab92c6f --- /dev/null +++ b/pod/progressive_web_app/views.py @@ -0,0 +1,23 @@ +from django.shortcuts import render +from .utils import notify_user +from django.http import JsonResponse +from django.core.exceptions import PermissionDenied + + + +def debug(request): + if not request.user.is_superuser: + raise PermissionDenied() + + return render( + request, + "debug.html", + ) + + +def send(request): + if not request.user.is_superuser: + raise PermissionDenied() + + notify_user(request.user, "Hello", "World!") + return JsonResponse({"success": True}) diff --git a/pod/settings.py b/pod/settings.py index 7a6f56d13b..8a59ba9cf2 100644 --- a/pod/settings.py +++ b/pod/settings.py @@ -3,6 +3,7 @@ Django version: 3.2. """ +import json import os import importlib.util @@ -65,6 +66,9 @@ "pod.video_encode_transcript", "pod.import_video", "pod.custom", + "pwa", + "pod.progressive_web_app", + "webpush", ] ## @@ -381,6 +385,61 @@ # 'flatpages': 'pod.db_migrations' # } + +# PWA + +PWA_APP_NAME = 'Podeduc' +PWA_APP_DESCRIPTION = ( + "Podeduc a pour but de faciliter la mise à disposition de vidéo et de ce fait, " + "d’encourager l’utilisation de celles-ci dans le cadre de l’enseignement et " + "la recherche." +) +PWA_APP_THEME_COLOR = '#0A0302' +PWA_APP_BACKGROUND_COLOR = '#ffffff' +PWA_APP_DISPLAY = 'standalone' +PWA_APP_SCOPE = '/' +PWA_APP_ORIENTATION = 'any' +PWA_APP_START_URL = '/' +PWA_APP_STATUS_BAR_COLOR = 'default' +PWA_APP_ICONS = [ + { + 'src': f'/static/img/icon_x{size}.png', + 'sizes': f"{size}x{size}", + 'purpose': "any maskable", + } + for size in (1024, 512, 384, 192, 128, 96, 72, 48) +] +PWA_APP_ICONS_APPLE = [ + { + 'src': f'/static/img/icon_x{size}.png', + 'sizes': f"{size}x{size}", + } + for size in (1024, 512, 384, 192, 128, 96, 72, 48) +] +PWA_APP_SPLASH_SCREEN = [ + { + 'src': '/static/img/splash-512.png', + 'media': ( + '(device-width: 320px) ' + 'and (device-height: 568px) ' + 'and (-webkit-device-pixel-ratio: 2)' + ) + } +] +PWA_APP_DIR = 'ltr' +PWA_APP_LANG = 'fr-FR' +PWA_APP_SCREENSHOTS = [ + { + 'src': '/static/img/esup-pod.svg', + 'sizes': '750x1334', + "type": "image/png" + } +] +PWA_SERVICE_WORKER_PATH = os.path.join( + BASE_DIR, "pod", "progressive_web_app", "static", "js", "serviceworker.js", +) +PWA_APP_DEBUG_MODE = locals().get("DEBUG", False) + ## # Applications settings (and settings locale if any) # diff --git a/pod/urls.py b/pod/urls.py index d4bafe30ac..35b8ada3b5 100644 --- a/pod/urls.py +++ b/pod/urls.py @@ -20,6 +20,7 @@ robots_txt, info_pod, userpicture, + set_notifications, ) from pod.main.rest_router import urlpatterns as rest_urlpatterns @@ -68,6 +69,7 @@ url(r"^accounts/change-password/$", auth_views.PasswordChangeView.as_view()), url(r"^accounts/reset-password/$", auth_views.PasswordResetView.as_view()), url(r"^accounts/userpicture/$", userpicture, name="userpicture"), + url(r"^accounts/set-notifications/$", set_notifications, name="set_notifications"), # rest framework url(r"^api-auth/", include("rest_framework.urls")), url(r"^rest/", include(rest_urlpatterns)), @@ -79,6 +81,10 @@ url(r"^custom/", include("pod.custom.urls")), # cut url(r"^cut/", include("pod.cut.urls")), + # pwa + url('', include('pwa.urls')), + # webpush + url(r'^webpush/', include('webpush.urls')) ] # CAS @@ -95,6 +101,11 @@ url(r"^oidc/", include("mozilla_django_oidc.urls")), ] +# PWA +urlpatterns += [ + url(r"^pwa/", include("pod.progressive_web_app.urls")), +] + # BBB: TODO REPLACE BBB BY MEETING if getattr(settings, "USE_MEETING", False): urlpatterns += [ diff --git a/pod/video/static/js/video_edit.js b/pod/video/static/js/video_edit.js index 71c322292a..895e4807c5 100644 --- a/pod/video/static/js/video_edit.js +++ b/pod/video/static/js/video_edit.js @@ -137,3 +137,7 @@ if (document.getElementById("video_form")) { } } /** end channel **/ + +// Change notification setting text to stick with a video upload context +const notificationMessage = document.querySelector("#notification-toast>.toast-body>p") +notificationMessage.textContent = gettext("Get notified when the video encoding is finished.") diff --git a/pod/video/templates/videos/video_edit.html b/pod/video/templates/videos/video_edit.html index 297c8bf186..6c57c4b2d3 100644 --- a/pod/video/templates/videos/video_edit.html +++ b/pod/video/templates/videos/video_edit.html @@ -265,5 +265,12 @@

{% trans "Help for fo const max_duration_date_delete = {{ form.max_duration_date_delete }}; - + {% if form.instance.encoding_in_progress and request.user.owner.accepts_notifications is not False %} + + {% endif %} {% endblock more_script %} diff --git a/pod/video/views.py b/pod/video/views.py index d6ec9cb389..2cf49a789f 100644 --- a/pod/video/views.py +++ b/pod/video/views.py @@ -1079,7 +1079,7 @@ def video_edit(request, slug=None): return render( request, "videos/video_edit.html", - {"form": form, "listTheme": json.dumps(get_list_theme_in_form(form))}, + {"form": form, "listTheme": json.dumps(get_list_theme_in_form(form))} ) diff --git a/pod/video_encode_transcript/encode.py b/pod/video_encode_transcript/encode.py index 53370595c8..bd53201e87 100644 --- a/pod/video_encode_transcript/encode.py +++ b/pod/video_encode_transcript/encode.py @@ -1,6 +1,8 @@ """This module handles video encoding with CPU.""" from django.conf import settings +from webpush.models import PushInformation + from pod.video.models import Video from .Encoding_video_model import Encoding_video_model from .encoding_studio import encode_video_studio @@ -13,6 +15,7 @@ add_encoding_log, send_email, send_email_encoding, + send_notification_encoding, time_to_seconds, ) import logging @@ -161,7 +164,9 @@ def get_encoding_video(video_to_encode): def end_of_encoding(video): """Send mail at the end of encoding, call transcription.""" - if EMAIL_ON_ENCODING_COMPLETION: + if video.owner.owner.accepts_notifications and PushInformation.objects.filter(user=video.owner).exists(): + send_notification_encoding(video) + elif EMAIL_ON_ENCODING_COMPLETION: send_email_encoding(video) transcript_video(video.id) diff --git a/pod/video_encode_transcript/utils.py b/pod/video_encode_transcript/utils.py index ce0d188f83..cdf3c83860 100644 --- a/pod/video_encode_transcript/utils.py +++ b/pod/video_encode_transcript/utils.py @@ -2,6 +2,7 @@ import bleach import time +from django.urls import reverse from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.core.mail import mail_admins @@ -9,6 +10,7 @@ from django.core.mail import mail_managers from django.core.mail import EmailMultiAlternatives from pod.video.models import Video +from pod.progressive_web_app.utils import notify_user from .models import EncodingStep from .models import EncodingLog @@ -126,6 +128,10 @@ def send_email_encoding(video_to_encode): send_notification_email(video_to_encode, subject_prefix) +def send_notification_encoding(video_to_encode): + subject_prefix = _("Encoding") + send_notification(video_to_encode, subject_prefix) + def send_email(msg, video_id): """Send email notification when video encoding failed.""" send_email_item(msg, "Video", video_id) @@ -226,6 +232,31 @@ def send_notification_email(video_to_encode, subject_prefix): ) +def send_notification(video_to_encode, subject_prefix): + subject = "[%s] %s" % ( + __TITLE_SITE__, + _("%(subject)s #%(content_id)s completed") + % {"subject": subject_prefix, "content_id": video_to_encode.id}, + ) + message = _( + "The %(content_type)s “%(content_title)s” has been %(action)s" + + ", and is now available on %(site_title)s." + ) % { + "content_type": ( + _("content") if subject_prefix == _("Transcripting") else _("video") + ), + "content_title": video_to_encode.title, + "action": ( + _("automatically transcript") + if (subject_prefix == _("Transcripting")) + else _("encoded to Web formats") + ), + "site_title": __TITLE_SITE__, + } + + notify_user(video_to_encode.owner, subject, message, url=reverse("video:video", args=(video_to_encode.slug,))) + + def time_to_seconds(a_time): """Convert a time to seconds.""" seconds = time.strptime(str(a_time), "%H:%M:%S") diff --git a/requirements.txt b/requirements.txt index c1ef5078d3..8718a726c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,3 +35,5 @@ bleach==6.0.0 pytube==15.0.0 django-redis-sessions paramiko~=3.1.0 +django-pwa==1.1.0 +django-webpush==0.3.5