diff --git a/dockerfile-dev-with-volumes/README.adoc b/dockerfile-dev-with-volumes/README.adoc index 978bd698ed..a902038a23 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@example.org" +} ---- == Commandes diff --git a/pod/authentication/forms.py b/pod/authentication/forms.py index 1a917c0a1b..e920bae91d 100644 --- a/pod/authentication/forms.py +++ b/pod/authentication/forms.py @@ -49,6 +49,17 @@ class Meta(object): fields = [] +class SetNotificationForm(forms.ModelForm): + """Push notification preferences form.""" + + 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..1cd8c126fd 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=_("Receive push notifications on 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 190518175f..ee898c9eed 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 30a923534e..b0db4d7358 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-22 16:41+0200\n" +"POT-Creation-Date: 2023-09-27 12:16+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 "Receive push notifications on 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 @@ -820,6 +828,7 @@ msgstr "Le fichier doit être au format VTT." #: pod/chapter/models.py pod/completion/models.py pod/enrichment/models.py #: pod/playlist/templates/playlist/playlist_card.html pod/video/models.py +#: pod/video_encode_transcript/utils.py msgid "video" msgstr "vidéo" @@ -2118,8 +2127,8 @@ msgid "" msgstr "" "Les champs \"Début\" et \"Fin\" doivent contenir des valeurs en secondes. " "Lancez la lecture de la vidéo, mettez sur pause et cliquez sur \"Récupérer " -"le temps depuis le lecteur\" pour renseigner automatiquement le champ \"Début" -"\". Vous pouvez le faire également pour remplir le champ \"Fin\"." +"le temps depuis le lecteur\" pour renseigner automatiquement le champ " +"\"Début\". Vous pouvez le faire également pour remplir le champ \"Fin\"." #: pod/enrichment/templates/enrichment/edit_enrichment.html msgid "You cannot overlap enrichments." @@ -2624,8 +2633,8 @@ msgid "" "This video was uploaded to Pod; its origin is %(type)s: %(url)s" msgstr "" -"Cette vidéo a été téléversée sur Pod ; son origine est %(type)s : %(url)s" +"Cette vidéo a été téléversée sur Pod ; son origine est %(type)s : %(url)s" #: pod/import_video/views.py pod/meeting/views.py msgid "Try changing the record type or address for this recording." @@ -2636,8 +2645,8 @@ msgstr "" #: pod/import_video/views.py #, python-format msgid "" -"This video '%(name)s' was uploaded to Pod; its origin is Youtube: %(url)s" +"This video '%(name)s' was uploaded to Pod; its origin is Youtube: %(url)s" msgstr "" "Cette vidéo « %(name)s » a été téléversée sur Pod ; son origine est " "Youtube : %(url)s" @@ -4731,6 +4740,14 @@ 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 notifications" + +#: pod/main/templates/navbar.html +msgid "Install" +msgstr "Installer" + #: pod/main/templates/navbar.html msgid "Log out" msgstr "Déconnexion" @@ -5526,16 +5543,16 @@ msgid "" msgstr "" "\n" "

Bonjour,\n" -"

%(owner)s vous invite à une réunion récurrente " -"%(meeting_title)s.

\n" +"

%(owner)s vous invite à une réunion récurrente " +"%(meeting_title)s.

\n" "

Date de début : %(start_date_time)s

\n" "

Récurrent jusqu’à la date : %(end_date)s

\n" "

La réunion se tiendra tou(te)s les %(frequency)s %(recurrence)s \n" "

Voici le lien pour rejoindre la réunion :\n" " %(join_link)s

\n" -"

Vous avez besoin de ce mot de passe pour entrer : " -"%(password)s

\n" +"

Vous avez besoin de ce mot de passe pour entrer : " +"%(password)s

\n" "

Cordialement

\n" " " @@ -5561,8 +5578,8 @@ msgstr "" "

Date de fin : %(end_date)s

\n" "

Voici le lien pour rejoindre la réunion :\n" " %(join_link)s

\n" -"

Vous avez besoin de ce mot de passe pour entrer : " -"%(password)s

\n" +"

Vous avez besoin de ce mot de passe pour entrer : " +"%(password)s

\n" "

Cordialement

\n" " " @@ -6254,6 +6271,22 @@ 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/recorder/admin.py msgid "Delete selected Recording file treatments + source files" msgstr "" @@ -6538,8 +6571,8 @@ msgstr "Prévisualisation d’enregistrement" #: pod/video/templates/videos/video-element.html msgid "" "To view this video please enable JavaScript, and consider upgrading to a web " -"browser that supports HTML5 video" +"browser that supports HTML5 video" msgstr "" "Pour visionner cette vidéo, veuillez activer JavaScript et envisager de " "passer à un navigateur Web qui un nouvel enregistrement a été ajouté sur la plateforme " "%(title_site)s à partir de l’enregistreur \"%(recorder)s\".
Pour " -"l’ajouter, cliquez sur le lien ci dessous.

" -"%(link_url)s
Si le lien n’est pas actif, il faut le copier-coller " -"dans la barre d’adresse de votre navigateur.

Cordialement

" +"l’ajouter, cliquez sur le lien ci dessous.

%(link_url)s
Si le lien n’est pas actif, il " +"faut le copier-coller dans la barre d’adresse de votre navigateur.

Cordialement

" #: pod/recorder/views.py msgid "New recording added." @@ -6634,6 +6668,17 @@ msgstr "L’enregistrement a été supprimé." msgid "Recorder for Studio not found." msgstr "Enregistreur studio non trouvé." +#: pod/settings.py +msgid "" +"Pod is aimed at users of our institutions, by allowing the publication of " +"videos in the fields of research (promotion of platforms, etc.), training " +"(tutorials, distance training, student reports, etc.), institutional life " +"(video of events), offering several days of content." +msgstr "" +"Pod 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." + #: pod/urls.py msgid "Pod Administration" msgstr "Administration de Pod" @@ -6968,8 +7013,8 @@ msgid "" "%(url)s

\n" msgstr "" "vous pouvez changer la date de suppression en éditant votre vidéo :

\n" -"

" -"%(scheme)s:%(url)s

\n" +"

%(scheme)s:%(url)s

\n" "\n" #: pod/video/management/commands/check_obsolete_videos.py @@ -7193,10 +7238,6 @@ msgstr "" msgid "Date of event" msgstr "Date de l’évènement" -#: pod/video/models.py -msgid "The password is / will be encrypted." -msgstr "Le mot de passe est / sera encrypté." - #: pod/video/models.py msgid "Overview" msgstr "Vue d’ensemble" @@ -7731,8 +7772,8 @@ msgid "" "This video is chaptered. Click the chapter button on the video player to view them." msgstr "" -"Cette vidéo est chapitrée. Cliquez sur le bouton de chapitre sur le lecteur vidéo pour les voir." +"Cette vidéo est chapitrée. Cliquez sur le bouton de chapitre sur le lecteur vidéo pour les voir." #: pod/video/templates/videos/video-all-info.html msgid "Other versions" @@ -8537,6 +8578,24 @@ msgstr "encodé aux formats Web" msgid "the:" msgstr "le :" +#: pod/video_encode_transcript/utils.py +#, fuzzy +#| msgid "Content" +msgid "content" +msgstr "Contenu" + +#: pod/video_encode_transcript/utils.py +#, fuzzy +#| msgid "Transcript" +msgid "Transcripting" +msgstr "Transcrire" + +#: pod/video_encode_transcript/utils.py +#, fuzzy +#| msgid "automatically transcripted" +msgid "automatically transcript" +msgstr "automatiquement transcrit" + #: pod/video_search/apps.py msgid "Video search" msgstr "Recherche de vidéo" @@ -8564,3 +8623,9 @@ msgstr "Résultats de la recherche" #: pod/xapi/apps.py msgid "Esup-Pod xAPI" msgstr "xAPI Esup-Pod" + +#~ msgid "Deny on all devices" +#~ msgstr "Refuser sur tous les appareils" + +#~ msgid "The password is / will be encrypted." +#~ msgstr "Le mot de passe est / sera encrypté." diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.mo b/pod/locale/fr/LC_MESSAGES/djangojs.mo index 63d979d030..9a7e0028c1 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 f5860ed5aa..f4c85f5b11 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-22 16:41+0200\n" +"POT-Creation-Date: 2023-09-27 12:16+0000\n" "PO-Revision-Date: \n" "Last-Translator: obado \n" "Language-Team: \n" @@ -557,6 +557,55 @@ msgstr "Voir plus" msgid "This folder is empty" msgstr "Ce dossier est vide" +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "An error happened during notification subscription" +msgstr "Erreur durant la désinscription..." + +#: 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 "" +"You have denied notifications in your browser, to enable them back you must " +"do it in your browser configuration menu." +msgstr "" +"Vous avez décliné les notifications via votre navigateur, pour les activer à " +"nouveau vous devez autoriser les notifications depuis les menus de " +"configuration de votre navigateur." + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Enable notifications" +msgstr "Activer les notifications" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Notifications are currently disabled." +msgstr "Les notifications sont actuellement désactivées." + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Notifications are currently enabled." +msgstr "Les notifications sont actuellement activées" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Disable notifications" +msgstr "Désactiver les notifications" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Unsubscribe from Push Messaging" +msgstr "Se désinscrire des notifications push" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Successfully subscribed to push notifications." +msgstr "Souscription aux notifications réussie." + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Error while subscribing to push notifications." +msgstr "Une erreur est survenue lors de la souscription aux notifications." + #: pod/video/static/js/ajax-display-channels.js msgid "%(count)s channel" msgid_plural "%(count)s channels" @@ -801,6 +850,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" @@ -857,6 +910,9 @@ msgstr "Ajouts en favoris total depuis la création" msgid "Slug" msgstr "Titre court" +#~ msgid "Error" +#~ msgstr "Erreur" + #~ msgid "video" #~ msgid_plural "videos" #~ msgstr[0] "vidéo" diff --git a/pod/locale/nl/LC_MESSAGES/django.po b/pod/locale/nl/LC_MESSAGES/django.po index 2d75489c6a..7433078ab4 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-22 16:41+0200\n" +"POT-Creation-Date: 2023-09-27 12:16+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 "Receive push notifications on 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 @@ -770,6 +778,7 @@ msgstr "" #: pod/chapter/models.py pod/completion/models.py pod/enrichment/models.py #: pod/playlist/templates/playlist/playlist_card.html pod/video/models.py +#: pod/video_encode_transcript/utils.py msgid "video" msgstr "" @@ -2472,8 +2481,8 @@ msgstr "" #: pod/import_video/views.py #, python-format msgid "" -"This video '%(name)s' was uploaded to Pod; its origin is Youtube: %(url)s" +"This video '%(name)s' was uploaded to Pod; its origin is Youtube: %(url)s" msgstr "" #: pod/import_video/views.py @@ -4488,6 +4497,14 @@ 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 "Install" +msgstr "" + #: pod/main/templates/navbar.html msgid "Log out" msgstr "Uitloggen" @@ -5911,6 +5928,20 @@ 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/recorder/admin.py msgid "Delete selected Recording file treatments + source files" msgstr "" @@ -6172,8 +6203,8 @@ msgstr "" #: pod/video/templates/videos/video-element.html msgid "" "To view this video please enable JavaScript, and consider upgrading to a web " -"browser that supports HTML5 video" +"browser that supports HTML5 video" msgstr "" #: pod/recorder/templates/recorder/link_record.html @@ -6258,6 +6289,14 @@ msgstr "" msgid "Recorder for Studio not found." msgstr "" +#: pod/settings.py +msgid "" +"Pod is aimed at users of our institutions, by allowing the publication of " +"videos in the fields of research (promotion of platforms, etc.), training " +"(tutorials, distance training, student reports, etc.), institutional life " +"(video of events), offering several days of content." +msgstr "" + #: pod/urls.py msgid "Pod Administration" msgstr "" @@ -6736,10 +6775,6 @@ msgstr "" msgid "Date of event" msgstr "" -#: pod/video/models.py -msgid "The password is / will be encrypted." -msgstr "" - #: pod/video/models.py msgid "Overview" msgstr "" @@ -8022,6 +8057,18 @@ msgstr "" msgid "the:" msgstr "" +#: pod/video_encode_transcript/utils.py +msgid "content" +msgstr "" + +#: pod/video_encode_transcript/utils.py +msgid "Transcripting" +msgstr "" + +#: pod/video_encode_transcript/utils.py +msgid "automatically transcript" +msgstr "" + #: pod/video_search/apps.py msgid "Video search" msgstr "" diff --git a/pod/locale/nl/LC_MESSAGES/djangojs.po b/pod/locale/nl/LC_MESSAGES/djangojs.po index 30e9fcfe27..7a125682d8 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-22 16:41+0200\n" +"POT-Creation-Date: 2023-09-27 12:16+0000\n" "PO-Revision-Date: 2023-02-08 15:22+0100\n" "Last-Translator: obado \n" "Language-Team: \n" @@ -532,6 +532,50 @@ msgstr "" msgid "This folder is empty" msgstr "" +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "An error happened during notification subscription" +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 "" +"You have denied notifications in your browser, to enable them back you must " +"do it in your browser configuration menu." +msgstr "" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Enable notifications" +msgstr "" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Notifications are currently disabled." +msgstr "" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Notifications are currently enabled." +msgstr "" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Disable notifications" +msgstr "" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Unsubscribe from Push Messaging" +msgstr "" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Successfully subscribed to push notifications." +msgstr "" + +#: pod/progressive_web_app/static/js/notification-toast.js +msgid "Error while subscribing to push notifications." +msgstr "" + #: pod/video/static/js/ajax-display-channels.js msgid "%(count)s channel" msgid_plural "%(count)s channels" @@ -775,6 +819,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 85453986ef..bcb2fe0f0b 100644 --- a/pod/main/configuration.json +++ b/pod/main/configuration.json @@ -1727,6 +1727,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": { @@ -4978,4 +5000,4 @@ } } } -] \ No newline at end of file +] diff --git a/pod/main/settings.py b/pod/main/settings.py index e46b3853d2..c99af8917b 100644 --- a/pod/main/settings.py +++ b/pod/main/settings.py @@ -4,6 +4,7 @@ Django version: 3.2. """ import os +from django.utils.translation import gettext_lazy as _ ## # flatpages @@ -306,3 +307,58 @@ VIDEO_RECENT_VIEWCOUNT = 180 HONEYPOT_FIELD_NAME = "firstname" + +# PWA + +PWA_APP_NAME = 'Pod' +PWA_APP_DESCRIPTION = _( + "Pod is aimed at users of our institutions, by allowing the publication of " + "videos in the fields of research (promotion of platforms, etc.), training " + "(tutorials, distance training, student reports, etc.), institutional life (video " + "of events), offering several days of content." +) +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, "progressive_web_app", "static", "js", "serviceworker.js", +) +PWA_APP_DEBUG_MODE = locals().get("DEBUG", False) diff --git a/pod/main/templates/base.html b/pod/main/templates/base.html index 0f59dda399..6c234daa66 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 %} @@ -124,6 +127,7 @@

{{page_title|capfirst}}

{% endif %} + {% include "notification_toast.html" %} {% endblock content %} {% if not request.GET.is_iframe %} @@ -275,6 +279,7 @@

{{page_title|capfirst}}

{% endif %} {% if POST_FOOTER_TEMPLATE %}{% include POST_FOOTER_TEMPLATE %}{% endif %} {% if TRACKING_TEMPLATE %}{% include TRACKING_TEMPLATE %}{% endif %} + diff --git a/pod/main/templates/navbar.html b/pod/main/templates/navbar.html index 7452d118b7..dc547c9f96 100644 --- a/pod/main/templates/navbar.html +++ b/pod/main/templates/navbar.html @@ -247,6 +247,16 @@
{% 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..b8a90d624f --- /dev/null +++ b/pod/progressive_web_app/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class ProgressiveWebAppConfig(AppConfig): + """Push notification preferences.""" + + 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..f072ee2d6d --- /dev/null +++ b/pod/progressive_web_app/static/js/notification-toast.js @@ -0,0 +1,163 @@ +var notificationToast = document.querySelector('#notification-toast') +notificationToast.addEventListener('shown.bs.toast', updateToast) + +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(notificationSettingUrl) { + const notificationsSpinner = document.querySelector("#notifications-spinner"); + const notificationButton = document.querySelector("#notification-action-button"); + const notificationsPreferenceTips = document.querySelector("#notifications-preference-tips"); + + notificationsSpinner.classList.remove("d-none"); + notificationButton.disabled = true + + let afterBrowserNotificationPermissionChanged = async function (subscription) { + acceptsNotifications = subscription != null; + let response = await postNotificationPreference(acceptsNotifications, notificationSettingUrl); + + await updateToast(); + + if (!response.ok) { + notificationsPreferenceTips.textContent = gettext("An error happened during notification subscription"); + notificationsPreferenceTips.classList.remove("alert-primary") + notificationsPreferenceTips.classList.remove("alert-warning") + notificationsPreferenceTips.classList.add("alert-error") + } + } + + let permissionState = await registration.pushManager.permissionState({userVisibleOnly: true}); + let subscription = await registration.pushManager.getSubscription() + + if (permissionState == "prompt") { + notificationsPreferenceTips.classList.remove("d-none") + notificationsPreferenceTips.classList.remove("alert-primary") + notificationsPreferenceTips.classList.remove("alert-error") + notificationsPreferenceTips.classList.add("alert-warning") + notificationsPreferenceTips.textContent = gettext("Don't forget to allow notifications from this website in your browser's settings!"); + + subscribe(registration, afterBrowserNotificationPermissionChanged); + } + + else if (permissionState == "granted" && !subscription) { + subscribe(registration, afterBrowserNotificationPermissionChanged); + } + + else if (permissionState == "granted" && subscription) { + subscription.unsubscribe().then(() => { + afterBrowserNotificationPermissionChanged(subscription) + }) + } +} + +async function updateToast() { + const notificationButton = document.querySelector("#notification-action-button"); + const notificationsPreferenceTips = document.querySelector("#notifications-preference-tips"); + const notificationsSpinner = document.querySelector("#notifications-spinner"); + + notificationsSpinner.classList.add("d-none"); + + let permissionState = await registration.pushManager.permissionState({userVisibleOnly: true}); + let subscription = await registration.pushManager.getSubscription() + + if (permissionState == "denied") { + notificationsPreferenceTips.classList.remove("d-none") + notificationsPreferenceTips.classList.remove("alert-primary") + notificationsPreferenceTips.classList.remove("alert-warning") + notificationsPreferenceTips.classList.add("alert-error") + notificationsPreferenceTips.textContent = gettext("You have denied notifications in your browser, to enable them back you must do it in your browser configuration menu."); + notificationButton.textContent = gettext("Enable notifications") + notificationButton.disabled = true + } + + else if (permissionState == "prompt" || (permissionState == "granted" && !subscription)) { + notificationsPreferenceTips.classList.remove("d-none") + notificationsPreferenceTips.classList.remove("alert-primary") + notificationsPreferenceTips.classList.remove("alert-warning") + notificationsPreferenceTips.classList.add("alert-warning") + notificationsPreferenceTips.textContent = gettext("Notifications are currently disabled."); + notificationButton.textContent = gettext("Enable notifications") + notificationButton.disabled = false + } + + else if (permissionState == "granted") { + notificationsPreferenceTips.classList.remove("d-none") + notificationsPreferenceTips.classList.remove("alert-error") + notificationsPreferenceTips.classList.remove("alert-warning") + notificationsPreferenceTips.classList.add("alert-primary") + notificationsPreferenceTips.textContent = gettext("Notifications are currently enabled."); + notificationButton.textContent = gettext("Disable notifications") + notificationButton.disabled = false + } +} + +// This function is an adaptation of +// https://github.com/safwanrahman/django-webpush/blob/0561306f133619297d7d66dd683690edf55cf325/webpush/static/webpush/webpush.js#L92-L132 +// The only change is the addition of the callback parameter and its call on success +// It can be removed when the following feature is implemented: +// https://github.com/safwanrahman/django-webpush/issues/133 +function subscribe(reg, callback=null) { + // Get the Subscription or register one + reg.pushManager.getSubscription().then( + function(subscription) { + var metaObj, applicationServerKey, options; + // Check if Subscription is available + if (subscription) { + return subscription; + } + + metaObj = document.querySelector('meta[name="django-webpush-vapid-key"]'); + applicationServerKey = metaObj.content; + options = { + userVisibleOnly: true + }; + if (applicationServerKey){ + options.applicationServerKey = urlB64ToUint8Array(applicationServerKey) + } + // If not, register one + reg.pushManager.subscribe(options) + .then( + function(subscription) { + postSubscribeObj('subscribe', subscription, + function(response) { + // Check the information is saved successfully into server + if (response.status === 201) { + // Show unsubscribe button instead + subBtn.textContent = gettext('Unsubscribe from Push Messaging'); + subBtn.disabled = false; + isPushEnabled = true; + showMessage(gettext('Successfully subscribed to push notifications.')); + } + } + ); + if (callback) { + callback(subscription); + } + } + ) + .catch( + function() { + console.log(gettext('Error while subscribing to push notifications.'), arguments) + if (callback) { + callback(null); + } + } + ) + } + ); +} diff --git a/pod/progressive_web_app/static/js/pwa-installation.js b/pod/progressive_web_app/static/js/pwa-installation.js new file mode 100644 index 0000000000..ba07005c46 --- /dev/null +++ b/pod/progressive_web_app/static/js/pwa-installation.js @@ -0,0 +1,29 @@ +let installPrompt = null; +let installButton = null; +let isInstalled = window.matchMedia("(display-mode:standalone)").matches; + +document.addEventListener("DOMContentLoaded", function() { + installButton = document.querySelector("#pwa-install-container"); + + if (!installButton) { + return; + } + + window.addEventListener("beforeinstallprompt", (event) => { + event.preventDefault(); + installPrompt = event; + if(!isInstalled) { + installButton.classList.remove("d-none"); + } + }); + + installButton.addEventListener("click", async () => { + if (!installPrompt) { + return; + } + const result = await installPrompt.prompt(); + if (result.outcome == "accepted") { + installButton.classList.add("d-none"); + } + }); +}) 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..3a9861976c --- /dev/null +++ b/pod/progressive_web_app/static/js/serviceworker.js @@ -0,0 +1,45 @@ +var staticCacheName = "django-pwa-v" + new Date().getTime(); + +var filesToCache = [ + '/static/img/logoPod.svg', + '/static/img/esup-pod.svg', + '/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..0260fc7ae2 --- /dev/null +++ b/pod/progressive_web_app/templates/notification_toast.html @@ -0,0 +1,29 @@ +{% 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..bfe76f6236 --- /dev/null +++ b/pod/progressive_web_app/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = "progressive_web_app" + + +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..470586cc47 --- /dev/null +++ b/pod/progressive_web_app/utils.py @@ -0,0 +1,16 @@ +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): + """Fills the payload to send a webpush notification to users devices.""" + 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..d742cb7002 --- /dev/null +++ b/pod/progressive_web_app/views.py @@ -0,0 +1,24 @@ +from django.shortcuts import render +from .utils import notify_user +from django.http import JsonResponse +from django.core.exceptions import PermissionDenied + + +def debug(request): + """Simple push notification debug page.""" + if not request.user.is_superuser: + raise PermissionDenied() + + return render( + request, + "debug.html", + ) + + +def send(request): + """Send a 'hello world' push notification for debug purpose.""" + 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..14a6113b90 100644 --- a/pod/settings.py +++ b/pod/settings.py @@ -65,6 +65,9 @@ "pod.video_encode_transcript", "pod.import_video", "pod.custom", + "pwa", + "pod.progressive_web_app", + "webpush", ] ## @@ -381,6 +384,7 @@ # 'flatpages': 'pod.db_migrations' # } + ## # Applications settings (and settings locale if any) # diff --git a/pod/urls.py b/pod/urls.py index d4bafe30ac..d27cab0700 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 += [ + path("pwa/", include("pod.progressive_web_app.urls", namespace="progressive_web_app")), +] + # 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 4feb648572..583f2b2a18 100644 --- a/pod/video/templates/videos/video_edit.html +++ b/pod/video/templates/videos/video_edit.html @@ -301,4 +301,12 @@

{% trans "Help for fo }); + {% if form.instance.encoding_in_progress and request.user.owner.accepts_notifications is not False %} + + {% endif %} {% endblock more_script %} 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 2b206748e0..b6a46dace1 100644 --- a/pod/video_encode_transcript/utils.py +++ b/pod/video_encode_transcript/utils.py @@ -3,6 +3,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 @@ -10,6 +11,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 @@ -127,6 +129,12 @@ def send_email_encoding(video_to_encode): send_notification_email(video_to_encode, subject_prefix) +def send_notification_encoding(video_to_encode): + """Send push notification on encoding completion.""" + 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) @@ -229,6 +237,32 @@ def send_notification_email(video_to_encode, subject_prefix): ) +def send_notification(video_to_encode, subject_prefix): + """Send push notification on video encoding or transcripting completion.""" + subject = "[%s] %s" % ( + __TITLE_SITE__, + _("%(subject)s #%(content_id)s completed") + % {"subject": subject_prefix, "content_id": video_to_encode.id}, + ) + message = _( + "%(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 0eab49e34c..09891ff415 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,3 +36,5 @@ pytube==15.0.0 django-redis-sessions paramiko~=3.1.0 djangorestframework-simplejwt==5.3.0 +django-pwa==1.1.0 +django-webpush==0.3.5