diff --git a/.env.dev-exemple b/.env.dev-exemple index ec66ca9ad5..47de0ea142 100644 --- a/.env.dev-exemple +++ b/.env.dev-exemple @@ -2,7 +2,7 @@ DJANGO_SUPERUSER_USERNAME= DJANGO_SUPERUSER_PASSWORD= DJANGO_SUPERUSER_EMAIL= ### You can use internal registry -ELASTICSEARCH_TAG=elasticsearch:8.13.0 +ELASTICSEARCH_TAG=elasticsearch:8.16.1 NODE_TAG=node:23 PYTHON_TAG=python:3.9-bullseye REDIS_TAG=redis:alpine3.16 diff --git a/.github/workflows/code_formatting.yml b/.github/workflows/code_formatting.yml index 2a24b508e3..f790304edc 100644 --- a/.github/workflows/code_formatting.yml +++ b/.github/workflows/code_formatting.yml @@ -3,7 +3,7 @@ name: Code Formatting on: push: - branches: [master, develop] + branches: [master, develop, main, pod_V4] workflow_dispatch: jobs: diff --git a/.github/workflows/pod_dev.yml b/.github/workflows/pod_dev.yml index 673d71af5b..a0b21c0536 100644 --- a/.github/workflows/pod_dev.yml +++ b/.github/workflows/pod_dev.yml @@ -6,11 +6,13 @@ on: push: branches: - develop + - dev_v4 - features/** - dependabot/** pull_request: branches: - develop + - dev_v4 workflow_dispatch: env: @@ -18,7 +20,7 @@ env: DJANGO_SUPERUSER_USERNAME: "admin" DJANGO_SUPERUSER_PASSWORD: "passwd" DJANGO_SUPERUSER_EMAIL: "noreply@uni.fr" - ELASTICSEARCH_TAG: "elasticsearch:7.17.18" + ELASTICSEARCH_TAG: "elasticsearch:8.16.1" NODE_VERSION: "20" NODE_TAG: "node:20" PYTHON_TAG: "python:3.9-bullseye" @@ -86,8 +88,10 @@ jobs: echo PYTHON_TAG=$PYTHON_TAG >> .env.dev echo REDIS_TAG=$REDIS_TAG >> .env.dev echo DOCKER_ENV=$DOCKER_ENV >> .env.dev + - name: cat env run: cat .env.dev + - name: make Build container run: | sudo rm -rf ./pod/log @@ -95,16 +99,19 @@ jobs: sudo rm -rf ./pod/node_modules docker compose -f ./docker-compose-full-dev-with-volumes-test.yml -p esup-pod build --build-arg ELASTICSEARCH_VERSION=$ELASTICSEARCH_TAG --build-arg NODE_VERSION=$NODE_TAG --build-arg PYTHON_VERSION=$PYTHON_TAG --no-cache docker compose -f ./docker-compose-full-dev-with-volumes-test.yml up --detach --force-recreate --always-recreate-deps + - name: Sleep for 60 seconds to wait run server on pod back uses: jakejarvis/wait-action@master with: time: '60s' + - name: show running container run: | docker ps echo "🍏 Docker is UP ${{ job.status }}." docker exec pod-back-with-volumes ps auxf docker exec pod-back-with-volumes python manage.py loaddata pod/video/fixtures/sample_videos.json + - name: run test in docker run: docker exec pod-back-with-volumes coverage run --append manage.py test -v 3 --keepdb pod.video_encode_transcript.tests.test_remote_encode_transcode @@ -126,18 +133,19 @@ jobs: sudo chown -R runner:runner . ## Start unit test test ## - - name: Runs Elasticsearch - uses: elastic/elastic-github-actions/elasticsearch@refactor_with_plugins + - name: Run Elasticsearch + uses: elastic/elastic-github-actions/elasticsearch@master with: - # stack-version: 7.6.0 - stack-version: 6.8.23 + stack-version: 8.16.1 plugins: analysis-icu + security-enabled: false - name: Setup Pod run: | - coverage run --append manage.py create_pod_index --settings=pod.main.test_settings python manage.py makemigrations --settings=pod.main.test_settings python manage.py migrate --settings=pod.main.test_settings + coverage run --append manage.py create_pod_index --settings=pod.main.test_settings + curl -XGET "http://elasticsearch.localhost:9200/pod/_search?pretty=true&q=fr" - name: Run Tests with coverage env: diff --git a/.github/workflows/pod_main.yml b/.github/workflows/pod_main.yml index b1ab94202c..d07b8952ca 100644 --- a/.github/workflows/pod_main.yml +++ b/.github/workflows/pod_main.yml @@ -18,7 +18,7 @@ jobs: strategy: max-parallel: 2 matrix: - python-version: ['3.8', '3.10'] + python-version: ['3.10', '3.12'] steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" @@ -53,17 +53,18 @@ jobs: pip install -r requirements-dev.txt sudo npm install -g yarn - ## Start unit test test ## - - name: Runs Elasticsearch - uses: elastic/elastic-github-actions/elasticsearch@refactor_with_plugins + ## Start unit tests ## + - name: Run Elasticsearch + uses: elastic/elastic-github-actions/elasticsearch@master with: - # stack-version: 7.6.0 - stack-version: 6.8.23 + stack-version: 8.16.1 plugins: analysis-icu + security-enabled: false - name: Setup Pod run: | python manage.py create_pod_index --settings=pod.main.test_settings + curl -XGET "http://elasticsearch.localhost:9200/pod/_search?pretty=true&q=fr" python manage.py makemigrations --settings=pod.main.test_settings python manage.py migrate --settings=pod.main.test_settings cd pod diff --git a/.gitignore b/.gitignore index 9f02003adc..5de083a2db 100644 --- a/.gitignore +++ b/.gitignore @@ -62,11 +62,13 @@ pod/static/ .cache_ggshield htmlcov compile-model +pod/activitypub/*.pub # IDE Files # ############# .idea .vscode +*.code-workspace # Certificates # ################ diff --git a/CONFIGURATION_FR.md b/CONFIGURATION_FR.md index 7a57e1f6f8..43d94f715a 100644 --- a/CONFIGURATION_FR.md +++ b/CONFIGURATION_FR.md @@ -1,28 +1,28 @@ # Configuration de la plateforme Esup-Pod -## Information gĂ©nĂ©rale +## Informations gĂ©nĂ©rales La plateforme Esup-Pod se base sur le framework Django Ă©crit en Python.
-Elle est compatible avec les versions 3.8, 3.9 et 3.10 de Python.
+Elle est compatible avec les versions 3.9, 3.10 et 3.12 de Python.
-**Django Version : 3.2 LTS**
+**Django Version : 4.2 LTS**
-> La documentation complÚte du framework : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/)

-> L’ensemble des variables de configuration du framework est accessible à cette adresse : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/)
+> La documentation complÚte du framework : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/)

+> L’ensemble des variables de configuration du framework est accessible à cette adresse : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/)
Voici les configurations des applications tierces utilisées par Esup-Pod.
* `CAS` - > valeur par dĂ©faut : `1.5.2` + > valeur par dĂ©faut : `1.5.3` >> SystĂšme d’authentification SSO_CAS
>> [kstateome/django-cas](https://github.com/kstateome/django-cas)
* `ModelTranslation` - > valeur par dĂ©faut : `0.18.7` + > valeur par dĂ©faut : `0.19.11` >> L’application modeltranslation est utilisĂ©e pour traduire le contenu dynamique
>> des modĂšles Django existants
>> [django-modeltranslation.readthedocs.io](https://django-modeltranslation.readthedocs.io/en/latest/installation.html#configuration)
* `captcha` - > valeur par défaut : `0.5.17` + > valeur par défaut : `0.6.0` >> Gestion du captcha du formulaire de contact
>> [django-simple-captcha.readthedocs.io](https://django-simple-captcha.readthedocs.io/en/latest/usage.html)
* `chunked_upload` @@ -31,23 +31,24 @@ Voici les configurations des applications tierces utilisées par Esup-Pod.
>> [juliomalegria/django-chunked-upload](https://github.com/juliomalegria/django-chunked-upload)
* `ckeditor` > valeur par dĂ©faut : `6.3.0` - >> Application permettant d’ajouter un Ă©diteur CKEditor dans certains champs
- >> [django-ckeditor.readthedocs.io](https://django-ckeditor.readthedocs.io/en/latest/#installation)
+ >> ATTENTION. django-ckeditor integre la version gratuite de CKEditor 4.22.1,
+ >> qui n'est plus prise en charge et qui présente des problÚmes de sécurité non résolus,
+ >> voir par exemple https://ckeditor.com/cke4/release/CKEditor-4.24.0-LTS.
* `django_select2` > valeur par défaut : `latest` >> Recherche et completion dans les formulaires
>> [django-select2.readthedocs.io](https://django-select2.readthedocs.io/en/latest/)
* `honeypot` - > valeur par défaut : `1.0.3` + > valeur par défaut : `1.2.1` >> Utilisé pour le formulaire de contact de Pod -
>> ajoute un champ caché pour diminuer le spam
>> [jamesturk/django-honeypot](https://github.com/jamesturk/django-honeypot/)
* `mozilla_django_oidc` - > valeur par dĂ©faut : `3.0.0` + > valeur par dĂ©faut : `4.0.1` >> SystĂšme d’authentification OpenID Connect
>> [mozilla-django-oidc.readthedocs.io](https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html)
* `pwa` - > valeur par dĂ©faut : `1.1.0` + > valeur par dĂ©faut : `2.0.1` >> Mise en place du mode PWA grĂące Ă  l’application Django-pwa
>> Voici la configuration par défaut pour Pod,
>> vous pouvez surcharger chaque variable dans votre fichier de configuration.
@@ -73,21 +74,25 @@ Voici les configurations des applications tierces utilisées par Esup-Pod.
>> >> Pour en savoir plus : [silviolleite/django-pwa](https://github.com/silviolleite/django-pwa)
* `rest_framework` - > valeur par dĂ©faut : `3.14.0` - >> version 3.14.0 : mise en place de l’API rest pour l’application
+ > valeur par dĂ©faut : `3.15.2` + >> mise en place de l’API rest pour l’application
>> [django-rest-framework.org](https://www.django-rest-framework.org/)
* `shibboleth` > valeur par dĂ©faut : `latest` >> SystĂšme d’authentification Shibboleth
>> [Brown-University-Library/django-shibboleth-remoteuser](https://github.com/Brown-University-Library/django-shibboleth-remoteuser)
* `sorl.thumbnail` - > valeur par défaut : `12.9.0` + > valeur par défaut : `12.11.0` >> Utilisée pour la génération de miniature des images
>> [sorl-thumbnail.readthedocs.io](https://sorl-thumbnail.readthedocs.io/en/latest/reference/settings.html)
* `tagging` > valeur par défaut : `0.5.0` >> Gestion des mots-clés associés à une vidéo // voir pour référencer une nouvelle application
>> [django-tagging.readthedocs.io](https://django-tagging.readthedocs.io/en/develop/#settings)
+* `tagulous` + > valeur par défaut : `2.1.0` + >> Gestion des mots-clés associés à un objet Django.
+ >> [django-tagulous.readthedocs.io](https://django-tagulous.readthedocs.io)
## Configuration générale de la plateforme Esup_Pod @@ -110,7 +115,7 @@ Voici les configurations des applications tierces utilisées par Esup-Pod.
>> C’est un dictionnaire imbriquĂ© dont les contenus font correspondre
>> l’alias de base de donnĂ©es avec un dictionnaire contenant
>> les options de chacune des bases de données.
- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#databases)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#databases)_
>> valeur par défaut : une base de données au format sqlite
>> Voici un exemple de configuration pour utiliser une base MySQL :
>> @@ -148,7 +153,7 @@ Voici les configurations des applications tierces utilisées par Esup-Pod.
* `EMAIL_HOST` > valeur par défaut : `smtp.univ.fr` >> nom du serveur smtp
- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#email-host)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#email-host)_
* `EMAIL_PORT` > valeur par dĂ©faut : `25` >> Port d’écoute du serveur SMTP.
@@ -283,13 +288,13 @@ Voici les configurations des applications tierces utilisées par Esup-Pod.
>> Le répertoire dans lequel stocker temporairement les données
>> (typiquement pour les fichiers plus grands que `FILE_UPLOAD_MAX_MEMORY_SIZE`)
>> lors des téléversements de fichiers.
- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#file-upload-temp-dir)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#file-upload-temp-dir)_
* `MEDIA_ROOT` > valeur par défaut : `/pod/media` >> Chemin absolu du systÚme de fichiers pointant vers le répertoire qui contiendra
>> les fichiers téléversés par les utilisateurs.

>> Attention, ce répertoire doit exister.

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#std:setting-MEDIA_ROOT)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#std:setting-MEDIA_ROOT)_
* `MEDIA_URL` > valeur par défaut : `/media/` >> prefix url utilisé pour accéder aux fichiers du répertoire media
@@ -304,7 +309,7 @@ Voici les configurations des applications tierces utilisées par Esup-Pod.
>> Le chemin absolu vers le répertoire dans lequel collectstatic rassemble
>> les fichiers statiques en vue du déploiement.
>> Ce chemin sera précisé dans le fichier de configurtation du vhost nginx.

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#std:setting-STATIC_ROOT)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#std:setting-STATIC_ROOT)_
* `STATIC_URL` > valeur par défaut : `/static/` >> prefix url utilisé pour accÚder aux fichiers static
@@ -341,13 +346,13 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
>> Exemple : `[('John', 'john@example.com'), ('Mary', 'mary@example.com')]`

>> Dans Pod, les "admins" sont Ă©galement destinataires des courriels de contact,
>> d’encodage ou de flux RSS si la variable `CONTACT_US_EMAIL` n’est pas renseignĂ©e.

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#admins)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#admins)_
* `ALLOWED_HOSTS` > valeur par dĂ©faut : `['pod.localhost']` >> Une liste de chaĂźnes reprĂ©sentant des noms de domaine/d’hĂŽte que ce site Django peut servir.

>> C’est une mesure de sĂ©curitĂ© pour empĂȘcher les attaques d’en-tĂȘte Host HTTP,
>> qui sont possibles mĂȘme avec bien des configurations de serveur Web apparemment sĂ©curisĂ©es.

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#allowed-hosts)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#allowed-hosts)_
* `BASE_DIR` > valeur par défaut : `os.path.dirname(os.path.dirname(os.path.abspath(__file__)))` >> répertoire de base
@@ -384,12 +389,12 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
>> l’ensemble des requetes en https.
>> Idem pour les cookies de session et de cross-sites qui seront également sécurisés

>> Il faut les passer Ă  False en cas d’usage du runserver (phase de dĂ©veloppement / debugage)

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#secure-ssl-redirect)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#secure-ssl-redirect)_
* `DEBUG` > valeur par défaut : `True` >> Une valeur booléenne qui active ou désactive le mode de débogage.

>> Ne déployez jamais de site en production avec le réglage DEBUG activé.

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#debug)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#debug)_
* `USE_DEBUG_TOOLBAR` > valeur par dĂ©faut : `True` >> Une valeur boolĂ©enne qui active ou dĂ©sactive l’outil de dĂ©bogage.

@@ -398,7 +403,7 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
* `LOGIN_URL` > valeur par dĂ©faut : `/authentication_login/` >> url de redirection pour l’authentification de l’utilisateur
- >> voir : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#login-url)
+ >> voir : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#login-url)
* `MANAGERS` > valeur par dĂ©faut : `[]` >> Dans Pod, les "managers" sont destinataires des courriels de fin d’encodage
@@ -406,7 +411,7 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
>> Le premier manager renseigné est également contact des flus RSS.

>> Ils sont aussi destinataires des courriels de contact
>> si la variable `CONTACT_US_EMAIL` n’est pas renseignĂ©e.

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#managers)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#managers)_
* `PROXY_HOST` > valeur par défaut : `` >> Utilisation du proxy - host
@@ -419,7 +424,7 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
>> Elle est utilisée dans le contexte de la signature cryptographique,
>> et doit ĂȘtre dĂ©finie Ă  une valeur unique et non prĂ©dictible.

>> Vous pouvez utiliser ce site pour en générer une : [djecrety.ir](https://djecrety.ir/)

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#secret-key)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#secret-key)_
* `SECURE_SSL_REDIRECT` > valeur par dĂ©faut : `False` >> À moins que votre site ne doive ĂȘtre disponible sur des connexions SSL et non SSL,
@@ -433,7 +438,7 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
>> Cette option empĂȘche le cookie d’ĂȘtre envoyĂ© dans les requĂȘtes inter-sites,
>> ce qui prévient les attaques CSRF et rend impossible
>> certaines méthodes de vol du cookie de session.
- >> Voir [docs.djangoproject.com](https://docs.djangoproject.com/en/3.2/ref/settings/#std-setting-SESSION_COOKIE_SAMESITE)
+ >> Voir [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#std-setting-SESSION_COOKIE_SAMESITE)
* `SESSION_COOKIE_SECURE` > valeur par défaut : `not DEBUG` >> @@ -445,7 +450,7 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
>> L’identifiant (nombre entier) du site actuel.
>> Peut ĂȘtre utilisĂ© pour mettre en place une instance multi-tenant
>> et ainsi gĂ©rer dans une mĂȘme base de donnĂ©es du contenu pour plusieurs sites.

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#site-id)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#site-id)_
* `TEST_SETTINGS` > valeur par défaut : `False` >> Permet de vérifier si la configuration de la plateforme est en mode test.
@@ -460,7 +465,7 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
* `TIME_ZONE` > valeur par défaut : `UTC` >> Une chaßne représentant le fuseau horaire pour cette installation.

- >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#std:setting-TIME_ZONE)_
+ >> _ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#std:setting-TIME_ZONE)_
>> Liste des adresses destinataires des courriels de contact
### Obsolescence @@ -520,11 +525,12 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
>> sur la boite de dialogue d’information sur l’usage des cookies dans Pod.
>> On peut préciser un lien vers les mentions légales ou page DPO.
* `DARKMODE_ENABLED` - > valeur par défaut : `False` - >> Activation du mode sombre
+ > valeur par dĂ©faut : `True` + >> Permet aux utilisateurs d’activer un mode sombre.
* `DYSLEXIAMODE_ENABLED` - > valeur par défaut : `False` - >> Activation du mode dyslexie
+ > valeur par dĂ©faut : `True` + >> Permet d’utiliser une police de caractĂšres plus adaptĂ©e
+ >> aux personnes atteintes de dyslexie.
* `HIDE_CHANNEL_TAB` > valeur par dĂ©faut : `False` >> Si True, permet de cacher l’onglet chaine dans la barre de menu du haut.
@@ -1022,7 +1028,7 @@ Application Cut permettant de découper des vidéos.
Mettre `USE_CUT` Ă  True pour activer cette application.
* `USE_CUT` - > valeur par dĂ©faut : `True` + > valeur par dĂ©faut : `False` >> Activation de l’application Cut
### Configuration de l’application dressing @@ -1031,7 +1037,7 @@ Application Dressing pour customiser une vidĂ©o avec un filigrane et des crĂ©dit Mettre `USE_DRESSING` Ă  True pour activer cette application.
* `USE_DRESSING` - > valeur par défaut : `True` + > valeur par défaut : `False` >> Activation des habillages.
>> Permet aux utilisateurs de customiser une vidéo avec un filigrane et des crédits.
@@ -1073,7 +1079,7 @@ Mettre `USE_IMPORT_VIDEO` Ă  True pour activer cette application.
> valeur par défaut : `True` >> Seuls les utilisateurs "staff" pourront importer des vidéos
* `USE_IMPORT_VIDEO` - > valeur par dĂ©faut : `True` + > valeur par dĂ©faut : `False` >> Activation de l’application d’import des vidĂ©os
* `USE_IMPORT_VIDEO_BBB_RECORDER` > valeur par défaut : `False` @@ -1085,7 +1091,7 @@ Mettre `USE_IMPORT_VIDEO` à True pour activer cette application.
>> bbb-recorder doit ĂȘtre installĂ© dans ce rĂ©pertoire, sur tous les serveurs d’encodage.
>> bbb-recorder crĂ©e un rĂ©pertoire Downloads, au mĂȘme niveau, qui nĂ©cessite de l’espace disque.
* `IMPORT_VIDEO_BBB_RECORDER_PATH` - > valeur par défaut : `True` + > valeur par défaut : `/data/bbb-recorder/media/` >> Répertoire qui contiendra les fichiers vidéo générés par bbb-recorder.
### Configuration de l’application live @@ -1206,13 +1212,13 @@ Mettre `USE_IMPORT_VIDEO` à True pour activer cette application.
* `USE_BBB` > valeur par défaut : `True` >> Utilisation de BigBlueButton
- >> [TODO] À retirer dans les futures versions de Pod
+ >> Module obsolĂšte.
* `USE_BBB_LIVE` > valeur par défaut : `False` >> Utilisation du systÚme de diffusion de Webinaires en lien avec BigBlueButton
>> [TODO] À retirer dans les futures versions de Pod
* `USE_IMPORT_VIDEO` - > valeur par dĂ©faut : `True` + > valeur par dĂ©faut : `False` >> Activation de l’application d’import des vidĂ©os
* `USE_MEETING` > valeur par défaut : `False` @@ -1398,14 +1404,14 @@ Mettre `USE_PLAYLIST` à True pour activer cette application.
>> Restreindre l’accĂšs Ă  la crĂ©ation de listes de lecture promues
>> au staff uniquement.
* `USE_FAVORITES` - > valeur par défaut : `True` + > valeur par défaut : `False` >> Activation des vidéos favorites.
>> Permet aux utilisateurs d’ajouter des vidĂ©os dans leurs favoris.
* `USE_PLAYLIST` - > valeur par dĂ©faut : `True` + > valeur par dĂ©faut : `False` >> Activation des playlist. Permet aux utilisateurs d’ajouter des vidĂ©os dans une playlist.
* `USE_PROMOTED_PLAYLIST` - > valeur par défaut : `True` + > valeur par défaut : `False` >> Activation des playlist promues.
>> Permet aux utilisateurs d'utiliser les listes de lecture promues.
@@ -1432,7 +1438,7 @@ Mettre `USE_PLAYLIST` Ă  True pour activer cette application.
### Configuration de l’application progressive_web_app * `USE_NOTIFICATIONS` - > valeur par dĂ©faut : `True` + > valeur par dĂ©faut : `False` >> Activation des notifications, attention, elles sont actives par dĂ©faut.
* `WEBPUSH_SETTINGS` > valeur par défaut : @@ -1454,7 +1460,7 @@ Application Quiz pour ajouter des questions sur les vidéos.
Mettre `USE_QUIZ` Ă  True pour activer cette application.
* `USE_QUIZ` - > valeur par défaut : `True` + > valeur par défaut : `False` >> Activation des quiz. Permet aux utilisateurs de créer, répondre et utiliser des quiz dans les vidéos.
### Configuration de l’application recorder @@ -2144,12 +2150,11 @@ Attention, il faut configurer Celery pour l’envoi des instructions pour l’en >> Adresse du ou des instances d’Elasticsearch utilisĂ©es pour
>> l’indexation et la recherche de vidĂ©o.
* `ES_VERSION` - > valeur par dĂ©faut : `6` + > valeur par dĂ©faut : `8` >> Version d’ElasticSearch.
- >> valeurs possibles 6, 7 ou 8 correspondant à la version du Elasticsearch utilisé.
- >> Pour utiliser la version 7 ou 8, faire une mise Ă  jour du paquet elasticsearch-py
- >> Pour la 7, `pip3 install elasticsearch==7.17.7`,
- >> et pour la 8, `pip3 install elasticsearch==8.8.1`.
+ >> valeurs possibles : `8`, correspondant à la version du serveur Elasticsearch utilisé.
+ >> Attention, le paquet elasticsearch-py doit correspondre Ă  la version du serveur.
+ >> pour la 8, `pip3 install elasticsearch==8.16.0`.
>> Voir [elasticsearch-py.readthedocs.io](https://elasticsearch-py.readthedocs.io/)
>> pour plus d’information.
* `ES_OPTIONS` diff --git a/Makefile b/Makefile index e29ae0170c..da4c93f4ef 100755 --- a/Makefile +++ b/Makefile @@ -13,15 +13,16 @@ help: # Démarre le serveur de test start: (sleep 15 ; open http://pod.localhost:8000) & - python3 manage.py runserver pod.localhost:8000 --insecure + python3 -Wd manage.py runserver pod.localhost:8000 --insecure # --insecure let serve static files even when DEBUG=False + # -Wd enable default warning mode (Warn once per call location) # Démarre le serveur de test en https auto-signé starts: # nécessite les django-extensions # cf https://timonweb.com/django/https-django-development-server-ssl-certificate/ (sleep 15 ; open https://pod.localhost:8000) & - python3 manage.py runserver_plus pod.localhost:8000 --cert-file cert.pem --key-file key.pem + python3 -Wd manage.py runserver_plus pod.localhost:8000 --cert-file cert.pem --key-file key.pem # PremiÚre installation de pod (BDD SQLite intégrée) install: @@ -44,7 +45,7 @@ createDB: find . -path "*/migrations/*.pyc" -delete make updatedb make migrate - python3 manage.py loaddata initial_data + python3 -Wd manage.py loaddata initial_data # Mise à jour des fichiers de langue lang: @@ -82,8 +83,8 @@ statics: # Generate configuration docs in .MD format createconfigs: - python3 manage.py createconfiguration fr - python3 manage.py createconfiguration en + python3 -Wd manage.py createconfiguration fr + python3 -Wd manage.py createconfiguration en # -- Docker # Use for docker run and docker exec commands diff --git a/dockerfile-dev-with-volumes/README.adoc b/dockerfile-dev-with-volumes/README.adoc index 0c9fb2b8a7..d3e3f88829 100755 --- a/dockerfile-dev-with-volumes/README.adoc +++ b/dockerfile-dev-with-volumes/README.adoc @@ -10,7 +10,7 @@ v1.2, 2023-08-30 === Conteneur ElasticSearch http://elasticsearch.localhost:9200 -==== elasticsearch:8.8.1 +==== elasticsearch:8.16.1 ===== OS/ARCH ---- OS/ARCH @@ -50,7 +50,7 @@ linux/arm64/v8 DJANGO_SUPERUSER_USERNAME= DJANGO_SUPERUSER_PASSWORD= DJANGO_SUPERUSER_EMAIL= -ELASTICSEARCH_TAG=elasticsearch:8.13.0 +ELASTICSEARCH_TAG=elasticsearch:8.16.1 NODE_TAG=node:23 PYTHON_TAG=python:3.9-bullseye REDIS_TAG=redis:alpine3.16 diff --git a/dockerfile-dev-with-volumes/pod-back/Dockerfile b/dockerfile-dev-with-volumes/pod-back/Dockerfile index d092993fc2..bd7dfad3ba 100755 --- a/dockerfile-dev-with-volumes/pod-back/Dockerfile +++ b/dockerfile-dev-with-volumes/pod-back/Dockerfile @@ -35,7 +35,7 @@ RUN mkdir /tmp/node_modules/ COPY --from=source-build-js /tmp/pod/node_modules/ /tmp/node_modules/ # TODO remove ES version - move it into env var RUN pip3 install --no-cache-dir -r requirements-conteneur.txt \ - && pip3 install elasticsearch==7.17.7 + && pip3 install elasticsearch==8.16.0 # ENTRYPOINT: COPY ./dockerfile-dev-with-volumes/pod-back/my-entrypoint-back.sh /tmp/my-entrypoint-back.sh diff --git a/dockerfile-dev-with-volumes/pod/Dockerfile b/dockerfile-dev-with-volumes/pod/Dockerfile index e686bf3857..2d379731d2 100755 --- a/dockerfile-dev-with-volumes/pod/Dockerfile +++ b/dockerfile-dev-with-volumes/pod/Dockerfile @@ -38,7 +38,7 @@ RUN mkdir /tmp/node_modules/ COPY --from=source-build-js /tmp/pod/node_modules/ /tmp/node_modules/ RUN pip3 install --no-cache-dir -r requirements-conteneur.txt \ - && pip3 install elasticsearch==8.9.0 + && pip3 install elasticsearch==8.16.0 # ENTRYPOINT: COPY ./dockerfile-dev-with-volumes/pod/my-entrypoint.sh /tmp/my-entrypoint.sh diff --git a/pod/ai_enhancement/forms.py b/pod/ai_enhancement/forms.py index f0bc053370..b90981ec3a 100644 --- a/pod/ai_enhancement/forms.py +++ b/pod/ai_enhancement/forms.py @@ -2,8 +2,8 @@ from django import forms from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from tagging.fields import TagField +from django.utils.translation import gettext_lazy as _ +from tagulous.models import TagField from pod.main.forms_utils import add_placeholder_and_asterisk from pod.video.models import Video, Discipline diff --git a/pod/ai_enhancement/models.py b/pod/ai_enhancement/models.py index 01f15bce75..c1067a736b 100644 --- a/pod/ai_enhancement/models.py +++ b/pod/ai_enhancement/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.dispatch import receiver from django.db.models.signals import post_save from pod.video.models import Video diff --git a/pod/ai_enhancement/utils.py b/pod/ai_enhancement/utils.py index ecab02890e..cf4e56847c 100644 --- a/pod/ai_enhancement/utils.py +++ b/pod/ai_enhancement/utils.py @@ -7,7 +7,7 @@ from django.conf import settings from requests import Response from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.ai_enhancement.models import AIEnhancement from pod.main.utils import extract_json_from_str from pod.video.models import Discipline, Video @@ -27,7 +27,7 @@ AI_ENHANCEMENT_API_URL = getattr(settings, "AI_ENHANCEMENT_API_URL", "") AI_ENHANCEMENT_API_VERSION = getattr(settings, "AI_ENHANCEMENT_API_VERSION", "") -USE_NOTIFICATIONS = getattr(settings, "USE_NOTIFICATIONS", True) +USE_NOTIFICATIONS = getattr(settings, "USE_NOTIFICATIONS", False) EMAIL_ON_ENHANCEMENT_COMPLETION = getattr( settings, "EMAIL_ON_ENHANCEMENT_COMPLETION", True ) diff --git a/pod/ai_enhancement/views.py b/pod/ai_enhancement/views.py index 0db42c7e07..7ef1a2c1c5 100644 --- a/pod/ai_enhancement/views.py +++ b/pod/ai_enhancement/views.py @@ -9,7 +9,7 @@ from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_protect, csrf_exempt from django.views.decorators.csrf import ensure_csrf_cookie from django.core.exceptions import PermissionDenied diff --git a/pod/authentication/__init__.py b/pod/authentication/__init__.py index 4f77c34d41..e69de29bb2 100644 --- a/pod/authentication/__init__.py +++ b/pod/authentication/__init__.py @@ -1 +0,0 @@ -default_app_config = "pod.authentication.apps.AuthConfig" diff --git a/pod/authentication/admin.py b/pod/authentication/admin.py index 80967ad8c3..c2db7d6dc3 100644 --- a/pod/authentication/admin.py +++ b/pod/authentication/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.utils.html import format_html from django.contrib.sites.shortcuts import get_current_site from django.contrib.auth.models import Group @@ -84,12 +84,11 @@ class Media: class UserAdmin(BaseUserAdmin): + @admin.display(description=_("Email")) def clickable_email(self, obj): email = obj.email return format_html('{}', email, email) - clickable_email.allow_tags = True - clickable_email.short_description = _("Email") list_display = ( "username", "last_name", @@ -130,11 +129,10 @@ def formfield_for_manytomany(self, db_field, request, **kwargs): kwargs["widget"] = widgets.FilteredSelectMultiple(db_field.verbose_name, False) return super().formfield_for_foreignkey(db_field, request, **kwargs) + @admin.display(description=_("Establishment")) def owner_establishment(self, obj) -> str: return "%s" % Owner.objects.get(user=obj).establishment - owner_establishment.short_description = _("Establishment") - ordering = ( "-is_superuser", "username", @@ -188,6 +186,7 @@ def get_inline_instances(self, request, obj=None): return _inlines +@admin.register(AccessGroup) class AccessGroupAdmin(admin.ModelAdmin): # form = AccessGroupAdminForm # search_fields = ["user__username__icontains", "user__email__icontains"] @@ -200,6 +199,7 @@ class AccessGroupAdmin(admin.ModelAdmin): ) +@admin.register(Owner) class OwnerAdmin(admin.ModelAdmin): # form = AdminOwnerForm autocomplete_fields = ["user", "accessgroups"] @@ -225,5 +225,3 @@ class Meta: # Register the new Group ModelAdmin instead of the original one. admin.site.unregister(Group) admin.site.register(Group, GroupAdmin) -admin.site.register(Owner, OwnerAdmin) -admin.site.register(AccessGroup, AccessGroupAdmin) diff --git a/pod/authentication/forms.py b/pod/authentication/forms.py index 11159d5a5b..490481bca3 100644 --- a/pod/authentication/forms.py +++ b/pod/authentication/forms.py @@ -4,7 +4,7 @@ from django.contrib.auth import get_user_model from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Group -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.sites.models import Site __FILEPICKER__ = False diff --git a/pod/authentication/models.py b/pod/authentication/models.py index a09a6541ef..6474dbb93a 100644 --- a/pod/authentication/models.py +++ b/pod/authentication/models.py @@ -1,7 +1,7 @@ """Esup-Pod authentication models.""" from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User, Permission, Group from django.conf import settings from django.dispatch import receiver diff --git a/pod/authentication/shibmiddleware.py b/pod/authentication/shibmiddleware.py index ab7285ffea..c0702b457e 100644 --- a/pod/authentication/shibmiddleware.py +++ b/pod/authentication/shibmiddleware.py @@ -63,7 +63,7 @@ def check_user_meta(self, user, shib_meta): and user.owner.affiliation != shib_meta["affiliation"] ) - def is_staffable(self, user): + def is_staffable(self, user) -> bool: """Check that given user, his domain is in authorized domains of shibboleth staff. Args: @@ -81,7 +81,7 @@ def is_staffable(self, user): return True return False - def make_profile(self, user, shib_meta): + def make_profile(self, user, shib_meta) -> None: if ("affiliation" in shib_meta) and self.check_user_meta(user, shib_meta): user.owner.affiliation = shib_meta["affiliation"] user.owner.save() diff --git a/pod/authentication/tests/test_populated.py b/pod/authentication/tests/test_populated.py index 6fc9606f3a..70924148a7 100644 --- a/pod/authentication/tests/test_populated.py +++ b/pod/authentication/tests/test_populated.py @@ -1,8 +1,17 @@ -"""Esup-Pod test populated authentication.""" +"""Unit tests for Esup-Pod populated authentication. + +* run with 'python manage.py test pod.authentication.tests.test_populated' +""" import random from django.conf import settings -from django.test import TestCase, override_settings +from django.contrib.auth.models import User +from django.test import TestCase, override_settings, RequestFactory +from importlib import reload +from ldap3 import Server, Connection, MOCK_SYNC +from unittest import skip +from xml.etree import ElementTree as ET + from pod.authentication.models import Owner, AccessGroup, AFFILIATION_STAFF from pod.authentication import populatedCASbackend from pod.authentication import shibmiddleware @@ -13,12 +22,7 @@ OIDC_CLAIM_FAMILY_NAME, OIDC_CLAIM_GIVEN_NAME, ) -from django.test import RequestFactory -from django.contrib.auth.models import User -from importlib import reload -from xml.etree import ElementTree as ET -from ldap3 import Server, Connection, MOCK_SYNC """ USER_CAS_MAPPING_ATTRIBUTES = getattr( @@ -431,6 +435,7 @@ def test_populate_user_from_entry_unpopulate_group(self) -> None: ) +@skip("# Esup-Pod 4.0 is no more compatible with Shibb (until someone correct this)") class PopulatedShibTestCase(TestCase): def setUp(self) -> None: """Set up PopulatedShibTestCase create user Pod.""" diff --git a/pod/authentication/tests/test_views.py b/pod/authentication/tests/test_views.py index 7ead289664..77fb0baf7d 100644 --- a/pod/authentication/tests/test_views.py +++ b/pod/authentication/tests/test_views.py @@ -7,7 +7,7 @@ from django.test import Client from django.contrib.auth.models import User from django.conf import settings -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class authenticationViewsTestCase(TestCase): diff --git a/pod/authentication/urls.py b/pod/authentication/urls.py index b0c2206fd0..9617f7465e 100644 --- a/pod/authentication/urls.py +++ b/pod/authentication/urls.py @@ -2,26 +2,26 @@ from .views import authentication_logout from .views import authentication_login_gateway -from django.conf.urls import url +from django.urls import path app_name = "authentication" urlpatterns = [ # auth cas - url( - r"^login/$", + path( + "login/", authentication_login, name="authentication_login", ), - url( - r"^logout/$", + path( + "logout/", authentication_logout, name="authentication_logout", ), - # url(r"^login/$", authentication_login, name="login"), - # url(r"^logout/$", authentication_logout, name="logout"), - url( - r"^login_gateway/$", + # re_path(r"^login/$", authentication_login, name="login"), + # re_path(r"^logout/$", authentication_logout, name="logout"), + path( + "login_gateway/", authentication_login_gateway, name="authentication_login_gateway", ), diff --git a/pod/authentication/views.py b/pod/authentication/views.py index 2376b5c751..fefb77f550 100644 --- a/pod/authentication/views.py +++ b/pod/authentication/views.py @@ -6,7 +6,7 @@ from django.urls import reverse from django.conf import settings from django.core.exceptions import SuspiciousOperation -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from cas.decorators import gateway from django.contrib import auth diff --git a/pod/chapter/admin.py b/pod/chapter/admin.py index 9bd7d65928..cae88009e4 100644 --- a/pod/chapter/admin.py +++ b/pod/chapter/admin.py @@ -7,6 +7,7 @@ from pod.video.models import Video +@admin.register(Chapter) class ChapterAdmin(admin.ModelAdmin): """Chapter administration.""" @@ -33,9 +34,6 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, **kwargs) -admin.site.register(Chapter, ChapterAdmin) - - class ChapterInline(admin.TabularInline): model = Chapter extra = 0 diff --git a/pod/chapter/forms.py b/pod/chapter/forms.py index 7e15b50d86..7e819038bd 100644 --- a/pod/chapter/forms.py +++ b/pod/chapter/forms.py @@ -3,7 +3,7 @@ from django import forms from django.conf import settings from django.core.exceptions import ValidationError -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from pod.chapter.models import Chapter from pod.chapter.utils import vtt_to_chapter diff --git a/pod/chapter/models.py b/pod/chapter/models.py index 6ec098e7a6..dcbe6555f2 100644 --- a/pod/chapter/models.py +++ b/pod/chapter/models.py @@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError from django.db import models from django.template.defaultfilters import slugify -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from pod.video.models import Video from pod.video.utils import verify_field_length diff --git a/pod/chapter/urls.py b/pod/chapter/urls.py index ab3486d690..cfb60bce40 100644 --- a/pod/chapter/urls.py +++ b/pod/chapter/urls.py @@ -1,10 +1,10 @@ -from django.conf.urls import url +from django.urls import re_path from pod.chapter.views import video_chapter app_name = "chapter" urlpatterns = [ - url( + re_path( r"^(?P[\-\d\w]+)/$", video_chapter, name="video_chapter", diff --git a/pod/chapter/views.py b/pod/chapter/views.py index bf17ba9cec..1b66fec915 100644 --- a/pod/chapter/views.py +++ b/pod/chapter/views.py @@ -7,7 +7,7 @@ from django.template.loader import render_to_string from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_protect from pod.video.models import Video from pod.chapter.models import Chapter diff --git a/pod/completion/admin.py b/pod/completion/admin.py index 4c404f292d..408224c01e 100644 --- a/pod/completion/admin.py +++ b/pod/completion/admin.py @@ -2,7 +2,7 @@ from django.conf import settings from django.contrib import admin -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.completion.models import Contributor from pod.completion.models import Document from pod.completion.models import Overlay @@ -45,6 +45,7 @@ def has_add_permission(self, request, obj=None): return False +@admin.register(Contributor) class ContributorAdmin(admin.ModelAdmin): list_display = ( "name", @@ -71,9 +72,6 @@ def get_queryset(self, request): return qs -admin.site.register(Contributor, ContributorAdmin) - - class DocumentInline(admin.TabularInline): model = Document readonly_fields = ( @@ -86,6 +84,7 @@ def has_add_permission(self, request, obj=None): return False +@admin.register(Document) class DocumentAdmin(admin.ModelAdmin): if __FILEPICKER__: form = DocumentAdminForm @@ -124,9 +123,6 @@ class Media: ) -admin.site.register(Document, DocumentAdmin) - - class TrackInline(admin.TabularInline): model = Track readonly_fields = ( @@ -142,6 +138,7 @@ def has_add_permission(self, request, obj=None): return False +@admin.register(EnrichModelQueue) class EnrichModelQueueAdmin(admin.ModelAdmin): list_display = ( "title", @@ -150,9 +147,7 @@ class EnrichModelQueueAdmin(admin.ModelAdmin): list_filter = ("in_treatment",) -admin.site.register(EnrichModelQueue, EnrichModelQueueAdmin) - - +@admin.register(Track) class TrackAdmin(admin.ModelAdmin): def debug(text) -> None: if DEBUG: @@ -311,9 +306,6 @@ class Media: ) -admin.site.register(Track, TrackAdmin) - - class OverlayInline(admin.TabularInline): model = Overlay readonly_fields = ( @@ -332,6 +324,7 @@ def has_add_permission(self, request, obj=None): return False +@admin.register(Overlay) class OverlayAdmin(admin.ModelAdmin): list_display = ( "title", @@ -354,6 +347,3 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): # class Media: # css = {"all": ("css/pod.css",)} - - -admin.site.register(Overlay, OverlayAdmin) diff --git a/pod/completion/forms.py b/pod/completion/forms.py index 69c387e22f..433ec4c5cc 100644 --- a/pod/completion/forms.py +++ b/pod/completion/forms.py @@ -4,8 +4,6 @@ from django.conf import settings from django.forms.widgets import HiddenInput -# from django.utils.translation import ugettext_lazy as _ - from pod.completion.models import Contributor from pod.completion.models import Document from pod.completion.models import Track diff --git a/pod/completion/models.py b/pod/completion/models.py index 0217471364..e4867ad097 100644 --- a/pod/completion/models.py +++ b/pod/completion/models.py @@ -5,7 +5,7 @@ from django.db import models from django.conf import settings from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.template.defaultfilters import slugify from ckeditor.fields import RichTextField from pod.video.models import Video diff --git a/pod/completion/templates/video_completion.html b/pod/completion/templates/video_completion.html index 3c920fcff4..a07a4badba 100644 --- a/pod/completion/templates/video_completion.html +++ b/pod/completion/templates/video_completion.html @@ -278,7 +278,7 @@

{% trans "Help"%}

{% endblock page_aside %} {% block more_script %} - + {% endblock more_script %} diff --git a/pod/completion/tests/test_models.py b/pod/completion/tests/test_models.py index 245c89792c..7831e1138d 100644 --- a/pod/completion/tests/test_models.py +++ b/pod/completion/tests/test_models.py @@ -12,7 +12,7 @@ from django.contrib.auth.models import User from django.core.files.uploadedfile import SimpleUploadedFile from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.video.models import Video from pod.video.models import Type from pod.completion.models import Contributor diff --git a/pod/completion/tests/test_views.py b/pod/completion/tests/test_views.py index 1310eb157f..5d3213b612 100644 --- a/pod/completion/tests/test_views.py +++ b/pod/completion/tests/test_views.py @@ -8,7 +8,7 @@ from django.contrib.auth.models import User from django.core.files.uploadedfile import SimpleUploadedFile from django.contrib.auth import authenticate -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.video.models import Video, Type from ..models import Contributor from ..models import Document diff --git a/pod/completion/urls.py b/pod/completion/urls.py index bd3151e722..87a18cb3e5 100644 --- a/pod/completion/urls.py +++ b/pod/completion/urls.py @@ -1,6 +1,6 @@ """Esup-Pod Video completion urls.""" -from django.conf.urls import url +from django.urls import re_path from .views import video_completion from .views import video_caption_maker from .views import video_completion_contributor @@ -12,37 +12,37 @@ app_name = "completion" urlpatterns = [ - url( + re_path( r"^caption_maker/(?P[\-\d\w]+)/$", video_caption_maker, name="video_caption_maker", ), - url( + re_path( r"^contributor/(?P[\-\d\w]+)/$", video_completion_contributor, name="video_completion_contributor", ), - url( + re_path( r"^speaker/(?P[\-\d\w]+)/$", video_completion_speaker, name="video_completion_speaker", ), - url( + re_path( r"^document/(?P[\-\d\w]+)/$", video_completion_document, name="video_completion_document", ), - url( + re_path( r"^track/(?P[\-\d\w]+)/$", video_completion_track, name="video_completion_track", ), - url( + re_path( r"^overlay/(?P[\-\d\w]+)/$", video_completion_overlay, name="video_completion_overlay", ), - url( + re_path( r"^(?P[\-\d\w]+)/$", video_completion, name="video_completion", diff --git a/pod/completion/utils.py b/pod/completion/utils.py index 53a9cd95f5..223b08ed0c 100644 --- a/pod/completion/utils.py +++ b/pod/completion/utils.py @@ -1,6 +1,6 @@ """Esup-Pod completion app utilities.""" -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from pod.speaker.utils import get_video_speakers diff --git a/pod/completion/views.py b/pod/completion/views.py index 2d2db01bf5..5d17053f7d 100644 --- a/pod/completion/views.py +++ b/pod/completion/views.py @@ -6,13 +6,14 @@ from django.template.loader import render_to_string from django.shortcuts import render from django.shortcuts import get_object_or_404 -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_protect from django.contrib.auth.decorators import login_required from django.contrib.admin.views.decorators import staff_member_required from django.core.exceptions import PermissionDenied from django.core.handlers.wsgi import WSGIRequest from pod.video.models import Video +from pod.main.utils import is_ajax from .models import Contributor from .forms import ContributorForm from .models import Document @@ -293,7 +294,7 @@ def video_completion_contributor_new(request: WSGIRequest, video: Video): form_contributor = ContributorForm(initial={"video": video}) context = get_video_completion_context(video, form_contributor=form_contributor) context["page_title"] = _("Add a new contributor to the video “%s”") % video.title - if request.is_ajax(): + if is_ajax(request): return render( request, "contributor/form_contributor.html", @@ -322,7 +323,7 @@ def video_completion_contributor_save(request: WSGIRequest, video: Video): if form_contributor.is_valid(): form_contributor.save() list_contributor = video.contributor_set.all() - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "contributor/list_contributor.html", @@ -351,7 +352,7 @@ def video_completion_contributor_save(request: WSGIRequest, video: Video): context, ) else: - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "errors": "{0}".format(_("Please correct errors")), "form": render_to_string( @@ -381,7 +382,7 @@ def video_completion_contributor_modify(request: WSGIRequest, video: Video): contributor = get_object_or_404(Contributor, id=request.POST["id"]) form_contributor = ContributorForm(instance=contributor) page_title = _("Edit the contributor “%s”") % contributor.name - if request.is_ajax(): + if is_ajax(request): return render( request, "contributor/form_contributor.html", @@ -406,7 +407,7 @@ def video_completion_contributor_delete(request: WSGIRequest, video: Video): contributor.delete() page_title = get_completion_home_page_title(video) list_contributor = video.contributor_set.all() - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "contributor/list_contributor.html", @@ -491,7 +492,7 @@ def video_completion_speaker_new(request: WSGIRequest, video: Video): form_speaker = JobVideoForm(initial={"video": video}) context = get_video_completion_context(video, form_speaker=form_speaker) context["page_title"] = _("Add a new contributor to the video “%s”") % video.title - if request.is_ajax(): + if is_ajax(request): return render( request, "speaker/form_speaker.html", @@ -516,7 +517,7 @@ def video_completion_speaker_save(request: WSGIRequest, video: Video): if form_speaker.is_valid(): form_speaker.save() list_speaker = get_video_speakers(video) - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "speaker/list_speaker.html", @@ -543,7 +544,7 @@ def video_completion_speaker_save(request: WSGIRequest, video: Video): context, ) else: - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "errors": "{0}".format(_("Please correct errors")), "form": render_to_string( @@ -574,7 +575,7 @@ def video_completion_speaker_delete(request: WSGIRequest, video: Video): speaker.delete() page_title = get_completion_home_page_title(video) list_speaker = get_video_speakers(video) - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "speaker/list_speaker.html", @@ -632,7 +633,7 @@ def video_completion_document_new(request, video): """View to add new document to a video.""" form_document = DocumentForm(initial={"video": video}) context = get_video_completion_context(video, form_document=form_document) - if request.is_ajax(): + if is_ajax(request): return render( request, "document/form_document.html", @@ -658,7 +659,7 @@ def video_completion_document_save(request, video): if form_document.is_valid(): form_document.save() list_document = video.document_set.all() - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "document/list_document.html", @@ -675,7 +676,7 @@ def video_completion_document_save(request, video): context, ) else: - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "errors": "{0}".format(_("Please correct errors")), "form": render_to_string( @@ -698,7 +699,7 @@ def video_completion_document_modify(request, video): """View to modify a document associated to a video.""" document = get_object_or_404(Document, id=request.POST["id"]) form_document = DocumentForm(instance=document) - if request.is_ajax(): + if is_ajax(request): return render( request, "document/form_document.html", @@ -718,7 +719,7 @@ def video_completion_document_delete(request, video): document = get_object_or_404(Document, id=request.POST["id"]) document.delete() list_document = video.document_set.all() - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "document/list_document.html", @@ -772,7 +773,7 @@ def video_completion_track_new(request, video): """View to add new track to a video.""" form_track = TrackForm(initial={"video": video}) context = get_video_completion_context(video, form_track=form_track) - if request.is_ajax(): + if is_ajax(request): return render( request, "track/form_track.html", @@ -799,7 +800,7 @@ def video_completion_get_form_track(request): def toggle_form_track_is_valid__video_completion_track(request, video, list_track): """Toggle form_track is valid.""" - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "track/list_track.html", @@ -829,7 +830,7 @@ def video_completion_track_save(request, video): request, video, list_track ) else: - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "errors": "{0}".format("Please correct errors"), "form": render_to_string( @@ -855,7 +856,7 @@ def video_completion_track_modify(request, video): """View to modify a track associated to a video.""" track = get_object_or_404(Track, id=request.POST["id"]) form_track = TrackForm(instance=track) - if request.is_ajax(): + if is_ajax(request): return render( request, "track/form_track.html", @@ -956,7 +957,7 @@ def video_completion_overlay_new(request, video): """Form to create a new completion overlay.""" form_overlay = OverlayForm(initial={"video": video}) context = get_video_completion_context(video, form_overlay=form_overlay) - if request.is_ajax(): + if is_ajax(request): return render( request, "overlay/form_overlay.html", @@ -985,7 +986,7 @@ def video_completion_overlay_save(request, video): if form_overlay.is_valid(): form_overlay.save() list_overlay = video.overlay_set.all() - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "overlay/list_overlay.html", @@ -1003,7 +1004,7 @@ def video_completion_overlay_save(request, video): context, ) else: - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "errors": "{0}".format(_("Please correct errors")), "form": render_to_string( @@ -1032,7 +1033,7 @@ def video_completion_overlay_modify(request, video): overlay = get_simple_url(overlay) form_overlay = OverlayForm(instance=overlay) - if request.is_ajax(): + if is_ajax(request): return render( request, "overlay/form_overlay.html", @@ -1051,7 +1052,7 @@ def video_completion_overlay_delete(request, video): overlay = get_object_or_404(Overlay, id=request.POST["id"]) overlay.delete() list_overlay = video.overlay_set.all() - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_data": render_to_string( "overlay/list_overlay.html", diff --git a/pod/custom/settings_local_docker_full_test.py b/pod/custom/settings_local_docker_full_test.py index 884d291c9e..f342f972bd 100644 --- a/pod/custom/settings_local_docker_full_test.py +++ b/pod/custom/settings_local_docker_full_test.py @@ -24,8 +24,8 @@ EMAIL_ON_ENCODING_COMPLETION = False SECRET_KEY = 'A_CHANGER' -# We specify here that we're using ES version 7\n -ES_VERSION = 7 +# ElasticSearch version +ES_VERSION = 8 ES_URL = ['http://elasticsearch.localhost:9200/'] CACHES = { 'default': { diff --git a/pod/cut/admin.py b/pod/cut/admin.py index f499a8e7ef..94ff1b65b3 100644 --- a/pod/cut/admin.py +++ b/pod/cut/admin.py @@ -2,6 +2,7 @@ from .models import CutVideo +@admin.register(CutVideo) class CutVideoAdmin(admin.ModelAdmin): def start_seconds(self, obj): return obj.start.strftime("%H:%M:%S") @@ -13,6 +14,3 @@ def end_seconds(self, obj): def has_add_permission(self, request, obj=None): return False - - -admin.site.register(CutVideo, CutVideoAdmin) diff --git a/pod/cut/context_processors.py b/pod/cut/context_processors.py index 2fe8f4bde6..8e82955f69 100644 --- a/pod/cut/context_processors.py +++ b/pod/cut/context_processors.py @@ -2,7 +2,7 @@ from django.conf import settings as django_settings -USE_CUT = getattr(django_settings, "USE_CUT", True) +USE_CUT = getattr(django_settings, "USE_CUT", False) def context_settings(request): diff --git a/pod/cut/models.py b/pod/cut/models.py index 074dd64c56..ab829c6728 100644 --- a/pod/cut/models.py +++ b/pod/cut/models.py @@ -1,7 +1,7 @@ from django.db import models from django.forms import ValidationError -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.video.models import Video from pod.video_encode_transcript.utils import time_to_seconds diff --git a/pod/cut/templates/video_cut.html b/pod/cut/templates/video_cut.html index 6dbb8ceb64..71c8998247 100644 --- a/pod/cut/templates/video_cut.html +++ b/pod/cut/templates/video_cut.html @@ -1,7 +1,6 @@ {% extends 'base.html' %} {% load i18n %} {% load static %} -{% load tagging_tags %} {% load thumbnail %} {% load video_filters %} {% load video_tags %} diff --git a/pod/cut/tests/test_models.py b/pod/cut/tests/test_models.py index 6866a8a867..a781f08aaf 100644 --- a/pod/cut/tests/test_models.py +++ b/pod/cut/tests/test_models.py @@ -1,13 +1,19 @@ """Unit tests for cut models.""" +import unittest + from django.test import TestCase from django.core.exceptions import ValidationError +from django.conf import settings from django.contrib.auth.models import User from pod.video.models import Video from pod.video.models import Type from ..models import CutVideo +USE_CUT = getattr(settings, "USE_CUT", False) + +@unittest.skipUnless(USE_CUT, "Set USE_CUT to True before testing video cut stuffs.") class CutVideoModelTestCase(TestCase): """Test case for the CutVideo model.""" @@ -15,7 +21,7 @@ class CutVideoModelTestCase(TestCase): "initial_data.json", ] - def setUp(self): + def setUp(self) -> None: """Set up the test with a video and a cut.""" owner = User.objects.create(username="test") video_type = Type.objects.create(title="others") @@ -28,7 +34,7 @@ def setUp(self): ) CutVideo.objects.create(video=video, start="00:00:00", end="00:00:20") - def test_bad_time(self): + def test_bad_time(self) -> None: """Test the creation with bad time values.""" video = Video.objects.get(id=1) cut = CutVideo() @@ -42,7 +48,7 @@ def test_bad_time(self): self.assertRaises(ValidationError, cut.clean) print(" ---> test_bad_time: OK! --- CutVideoModel") - def test_verify_time__value_error(self): + def test_verify_time__value_error(self) -> None: """Test verify_time method with bad values.""" video = Video.objects.get(id=1) cut = CutVideo() @@ -53,7 +59,7 @@ def test_verify_time__value_error(self): self.assertFalse(cut.verify_time()) print(" ---> test_verify_time_value_error: OK! --- CutVideoModel") - def test_verify_time__good_values(self): + def test_verify_time__good_values(self) -> None: """Test verify_time method with good values.""" video = Video.objects.get(id=1) cut = CutVideo() @@ -64,13 +70,13 @@ def test_verify_time__good_values(self): self.assertTrue(cut.verify_time()) print(" ---> test_verify_time_good_values: OK! --- CutVideoModel") - def test_start_in_int(self): + def test_start_in_int(self) -> None: """Test the start_in_int property.""" cut = CutVideo.objects.get(id=1) self.assertEqual(cut.start_in_int, 0) print(" ---> test_start_in_int: OK! --- CutVideoModel") - def test_end_in_int(self): + def test_end_in_int(self) -> None: """Test the end_in_int property.""" cut = CutVideo.objects.get(id=1) self.assertEqual(cut.end_in_int, 20) diff --git a/pod/cut/tests/test_utils.py b/pod/cut/tests/test_utils.py index 621a795743..705df618ca 100644 --- a/pod/cut/tests/test_utils.py +++ b/pod/cut/tests/test_utils.py @@ -1,15 +1,24 @@ +"""Unit tests for video cut utils.""" + +import unittest + +from django.conf import settings +from django.contrib.auth.models import User from django.test import TestCase + from pod.cut.utils import clean_database from pod.video.models import Notes, AdvancedNotes, Type, Video from pod.chapter.models import Chapter from pod.completion.models import Overlay, Track -from django.contrib.auth.models import User + +USE_CUT = getattr(settings, "USE_CUT", False) +@unittest.skipUnless(USE_CUT, "Set USE_CUT to True before testing video cut stuffs.") class CleanDatabaseTest(TestCase): fixtures = ["initial_data.json"] - def setUp(self): + def setUp(self) -> None: self.user = User.objects.create(username="pod", password="pod1234pod") self.video = Video.objects.create( title="Video1", @@ -28,7 +37,7 @@ def setUp(self): self.overlay = Overlay.objects.create(video=self.video, title="Overlay 1") self.track = Track.objects.create(video=self.video) - def test_clean_database(self): + def test_clean_database(self) -> None: """Test if clean_database works correctly.""" # Check if the models exist before cleaning self.assertTrue(Chapter.objects.filter(video=self.video).exists()) diff --git a/pod/cut/tests/test_views.py b/pod/cut/tests/test_views.py index 189d68a389..de28435fa8 100644 --- a/pod/cut/tests/test_views.py +++ b/pod/cut/tests/test_views.py @@ -8,7 +8,7 @@ from pod.main.models import Configuration from datetime import time from django.contrib.messages import get_messages -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .. import views from importlib import reload diff --git a/pod/cut/views.py b/pod/cut/views.py index 6bff49b9ac..2e74a2af37 100644 --- a/pod/cut/views.py +++ b/pod/cut/views.py @@ -7,7 +7,7 @@ from django.shortcuts import get_object_or_404 from django.contrib.sites.shortcuts import get_current_site from django.contrib.auth.decorators import login_required -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.contrib import messages from django.core.exceptions import PermissionDenied from django.http import QueryDict diff --git a/pod/dressing/admin.py b/pod/dressing/admin.py index de2f9962ca..a443ce440e 100644 --- a/pod/dressing/admin.py +++ b/pod/dressing/admin.py @@ -5,6 +5,7 @@ from .forms import DressingAdminForm +@admin.register(Dressing) class DressingAdmin(admin.ModelAdmin): """Dressing admin page.""" @@ -50,6 +51,3 @@ class Media: "podfile/js/filewidget.js", "bootstrap/dist/js/bootstrap.min.js", ) - - -admin.site.register(Dressing, DressingAdmin) diff --git a/pod/dressing/context_processors.py b/pod/dressing/context_processors.py index fa36733113..d5a2f1cd1d 100644 --- a/pod/dressing/context_processors.py +++ b/pod/dressing/context_processors.py @@ -2,7 +2,7 @@ from django.conf import settings as django_settings -USE_DRESSING = getattr(django_settings, "USE_DRESSING", True) +USE_DRESSING = getattr(django_settings, "USE_DRESSING", False) def context_settings(request): diff --git a/pod/dressing/forms.py b/pod/dressing/forms.py index 8d620e9d6d..a48dc586a4 100644 --- a/pod/dressing/forms.py +++ b/pod/dressing/forms.py @@ -6,7 +6,7 @@ from django.contrib.sites.models import Site from django_select2 import forms as s2forms from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib import admin from django.contrib.admin import widgets @@ -79,7 +79,7 @@ def __init__(self, *args, **kwargs): self.fields["opening_credits"].queryset = query_videos.all() self.fields["ending_credits"].queryset = query_videos.all() - # change CKEditor config for no staff user + # Remove watermark config for no staff user if not hasattr(self, "admin_form") and ( self.is_staff is False and self.is_superuser is False ): diff --git a/pod/dressing/models.py b/pod/dressing/models.py index 140cfa9647..c69a0418ee 100644 --- a/pod/dressing/models.py +++ b/pod/dressing/models.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import User from pod.authentication.models import AccessGroup from pod.podfile.models import CustomImageModel -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.video.models import Video diff --git a/pod/dressing/templates/dressing_edit.html b/pod/dressing/templates/dressing_edit.html index 0c38c1cccd..72719d6021 100644 --- a/pod/dressing/templates/dressing_edit.html +++ b/pod/dressing/templates/dressing_edit.html @@ -1,7 +1,6 @@ {% extends 'base.html' %} {% load i18n %} {% load static %} -{% load tagging_tags %} {% load thumbnail %} {% block page_extra_head %} diff --git a/pod/dressing/templates/video_dressing.html b/pod/dressing/templates/video_dressing.html index 5cf342b5bf..f8bb83d91d 100644 --- a/pod/dressing/templates/video_dressing.html +++ b/pod/dressing/templates/video_dressing.html @@ -1,7 +1,6 @@ {% extends 'base.html' %} {% load i18n %} {% load static %} -{% load tagging_tags %} {% load thumbnail %} {% load video_filters %} {% load video_tags %} diff --git a/pod/dressing/tests/test_models.py b/pod/dressing/tests/test_models.py index 7d7a36300c..947ee9db84 100644 --- a/pod/dressing/tests/test_models.py +++ b/pod/dressing/tests/test_models.py @@ -1,10 +1,12 @@ """Unit tests for dressing models.""" import os +import unittest from django.test import TestCase from django.contrib.auth.models import User from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile + from pod.video.models import Type, Video from pod.dressing.models import Dressing @@ -17,11 +19,16 @@ FILEPICKER = False from pod.main.models import CustomImageModel +USE_DRESSING = getattr(settings, "USE_DRESSING", False) + +@unittest.skipUnless( + USE_DRESSING, "Set USE_DRESSING to True before testing Dressing stuffs." +) class DressingModelTest(TestCase): """Test case for Pod dressing models.""" - def setUp(self): + def setUp(self) -> None: """Set up DressingModel Tests.""" owner = User.objects.create(username="pod") currentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -69,7 +76,7 @@ def setUp(self): dressing.owners.set([owner]) dressing.users.set([owner]) - def test_attributs_full(self): + def test_attributs_full(self) -> None: dressing = Dressing.objects.get(id=1) owner = User.objects.get(username="pod") video = Video.objects.get(id=1) @@ -85,7 +92,7 @@ def test_attributs_full(self): self.assertEqual(dressing.ending_credits, video2) print(" ---> test_attributs_full: OK! --- DressingModelTest") - def test_dressing_to_json(self): + def test_dressing_to_json(self) -> None: """Test the to_json function.""" dressing = Dressing.objects.get(id=1) dressing_json = dressing.to_json() diff --git a/pod/dressing/tests/test_utils.py b/pod/dressing/tests/test_utils.py index 7218d575f7..f1b2eeb301 100644 --- a/pod/dressing/tests/test_utils.py +++ b/pod/dressing/tests/test_utils.py @@ -3,12 +3,18 @@ import os import unittest from unittest.mock import patch +from django.conf import settings from django.contrib.auth.models import User from pod.authentication.models import AccessGroup from pod.dressing.utils import get_dressings, get_dressing_input from pod.dressing.models import Dressing +USE_DRESSING = getattr(settings, "USE_DRESSING", False) + +@unittest.skipUnless( + USE_DRESSING, "Set USE_DRESSING to True before testing Dressing stuffs." +) class DressingUtilitiesTests(unittest.TestCase): """TestCase for Esup-Pod dressing utilities.""" diff --git a/pod/dressing/tests/test_views.py b/pod/dressing/tests/test_views.py index 18f7cf65e1..a8a5ec9a15 100644 --- a/pod/dressing/tests/test_views.py +++ b/pod/dressing/tests/test_views.py @@ -1,7 +1,13 @@ -"""Unit tests for Esup-Pod dressing views.""" +"""Unit tests for Esup-Pod dressing views. + +* run with 'python manage.py test pod.dressing.tests.test_views' +""" + +import unittest from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ +from django.conf import settings from django.contrib.messages import get_messages from django.test import TestCase from django.urls import reverse @@ -11,7 +17,12 @@ from pod.video.models import Type, Video from pod.main.models import Configuration +USE_DRESSING = getattr(settings, "USE_DRESSING", False) + +@unittest.skipUnless( + USE_DRESSING, "Set USE_DRESSING to True before testing Dressing stuffs." +) class VideoDressingViewTest(TestCase): """Dressing page test case.""" @@ -19,6 +30,7 @@ class VideoDressingViewTest(TestCase): def setUp(self) -> None: """Set up VideoDressingViewTest.""" + self.user = User.objects.create_user( username="user", password="password", is_staff=1 ) @@ -35,7 +47,7 @@ def setUp(self) -> None: self.dressing.users.set([self.user]) self.dressing.videos.set([self.first_video]) - def test_maintenance(self): + def test_maintenance(self) -> None: """Test Pod maintenance mode in VideoDressingViewTest.""" self.client.force_login(self.user) url = reverse("dressing:video_dressing", args=[self.first_video.slug]) @@ -51,7 +63,7 @@ def test_maintenance(self): self.assertRedirects(response, "/maintenance/") print(" ---> test_maintenance ok") - def test_video_dressing_page(self): + def test_video_dressing_page(self) -> None: """Test test_video_dressing_page in MyDressingViewTest.""" self.client.force_login(self.user) response = self.client.get( @@ -61,7 +73,7 @@ def test_video_dressing_page(self): self.assertTemplateUsed(response, "video_dressing.html") print(" ---> test_video_dressing_page ok") - def test_video_encoding_in_progress(self): + def test_video_encoding_in_progress(self) -> None: """Test video encoding in progress in VideoDressingViewTest.""" self.first_video.encoding_in_progress = True self.first_video.save() @@ -77,7 +89,7 @@ def test_video_encoding_in_progress(self): self.assertEqual(messages[0].message, _("The video is currently being encoded.")) print(" ---> test_video_encoding_in_progress ok") - def test_video_dressing_permission_denied(self): + def test_video_dressing_permission_denied(self) -> None: """Test test_video_dressing_permission_denied in VideoDressingViewTest.""" user_without_permission = User.objects.create_user( username="useless_user", password="testpass" @@ -95,6 +107,9 @@ def test_video_dressing_permission_denied(self): print(" ---> test_video_dressing_permission_denied ok") +@unittest.skipUnless( + USE_DRESSING, "Set USE_DRESSING to True before testing Dressing stuffs." +) class MyDressingViewTest(TestCase): """My dressing page tests case.""" @@ -118,7 +133,7 @@ def setUp(self) -> None: self.dressing.users.set([self.user]) self.dressing.videos.set([self.first_video]) - def test_maintenance(self): + def test_maintenance(self) -> None: """Test Pod maintenance mode in MyDressingViewTest.""" self.client.force_login(self.user) url = reverse("dressing:my_dressings") @@ -158,6 +173,9 @@ def test_my_dressing_permission_denied(self): print(" ---> test_my_dressing_permission_denied ok") +@unittest.skipUnless( + USE_DRESSING, "Set USE_DRESSING to True before testing Dressing stuffs." +) class DressingEditViewTest(TestCase): """Dressing edit page tests case.""" @@ -205,6 +223,9 @@ def test_dressing_edit_view_permission_denied(self): print(" ---> test_dressing_create_view_permission_denied ok") +@unittest.skipUnless( + USE_DRESSING, "Set USE_DRESSING to True before testing Dressing stuffs." +) class DressingDeleteViewTest(TestCase): """Dressing delete page test case.""" @@ -327,6 +348,9 @@ def test_dressing_delete_view_not_authenticated(self): print(" ---> test_dressing_delete_view_not_authenticated ok") +@unittest.skipUnless( + USE_DRESSING, "Set USE_DRESSING to True before testing Dressing stuffs." +) class DressingCreateViewTest(TestCase): """Dressing create page test case.""" diff --git a/pod/dressing/views.py b/pod/dressing/views.py index 3892c74dea..cbf91f6c16 100644 --- a/pod/dressing/views.py +++ b/pod/dressing/views.py @@ -5,7 +5,7 @@ from django.urls import reverse from django.contrib.sites.shortcuts import get_current_site from django.contrib.auth.decorators import login_required -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_protect from django.contrib import messages from django.core.exceptions import PermissionDenied diff --git a/pod/enrichment/admin.py b/pod/enrichment/admin.py index 323a5c9b23..28d351245c 100644 --- a/pod/enrichment/admin.py +++ b/pod/enrichment/admin.py @@ -59,6 +59,7 @@ class Media: admin.site.register(Enrichment) +@admin.register(EnrichmentGroup) class EnrichmentGroupAdmin(admin.ModelAdmin): list_display = ("video", "get_groups") autocomplete_fields = ["video"] @@ -88,6 +89,7 @@ def get_groups(self, obj): return "\n".join([g.name for g in obj.groups.all()]) +@admin.register(EnrichmentVtt) class EnrichmentVttAdmin(admin.ModelAdmin): form = EnrichmentVttAdminForm list_display = ("video", "src", "get_file_name") @@ -115,7 +117,3 @@ class Media: "podfile/js/filewidget.js", "bootstrap/dist/js/bootstrap.min.js", ) - - -admin.site.register(EnrichmentGroup, EnrichmentGroupAdmin) -admin.site.register(EnrichmentVtt, EnrichmentVttAdmin) diff --git a/pod/enrichment/apps.py b/pod/enrichment/apps.py index 7739973b68..9b58c08bf6 100644 --- a/pod/enrichment/apps.py +++ b/pod/enrichment/apps.py @@ -1,5 +1,5 @@ from django.apps import AppConfig -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class EnrichmentConfig(AppConfig): diff --git a/pod/enrichment/forms.py b/pod/enrichment/forms.py index fc79c3905c..f650c23d10 100644 --- a/pod/enrichment/forms.py +++ b/pod/enrichment/forms.py @@ -2,7 +2,7 @@ from django.conf import settings from django.forms.widgets import HiddenInput from django.contrib.admin import widgets -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.utils.safestring import mark_safe from .models import Enrichment, EnrichmentGroup, EnrichmentVtt from django.contrib.auth.models import Group diff --git a/pod/enrichment/models.py b/pod/enrichment/models.py index 268cbb5395..a74dbc044d 100755 --- a/pod/enrichment/models.py +++ b/pod/enrichment/models.py @@ -7,8 +7,7 @@ from django.db import models from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -from django.utils.translation import ugettext as _ -from django.utils import timezone +from django.utils.translation import gettext as _ from django.template.defaultfilters import slugify from ckeditor.fields import RichTextField @@ -38,12 +37,12 @@ def enrichment_to_vtt(list_enrichment, video) -> str: webvtt = WebVTT() for enrich in list_enrichment: - start = datetime.datetime.fromtimestamp(enrich.start, tz=timezone.utc).strftime( - "%H:%M:%S.%f" - )[:-3] - end = datetime.datetime.fromtimestamp(enrich.end, tz=timezone.utc).strftime( - "%H:%M:%S.%f" - )[:-3] + start = datetime.datetime.fromtimestamp( + enrich.start, tz=datetime.timezone.utc + ).strftime("%H:%M:%S.%f")[:-3] + end = datetime.datetime.fromtimestamp( + enrich.end, tz=datetime.timezone.utc + ).strftime("%H:%M:%S.%f")[:-3] url = enrichment_to_vtt_type(enrich) caption = Caption( "{0}".format(start), @@ -87,6 +86,7 @@ def enrichment_to_vtt(list_enrichment, video) -> str: def enrichment_to_vtt_type(enrich): + """Return enrichment content.""" if enrich.type == "image": return enrich.image.file.url elif enrich.type == "document": diff --git a/pod/enrichment/static/js/enrichment.js b/pod/enrichment/static/js/enrichment.js index d6bc96ac01..b04e0b1edb 100644 --- a/pod/enrichment/static/js/enrichment.js +++ b/pod/enrichment/static/js/enrichment.js @@ -248,7 +248,7 @@ var sendform = async function (elt, action) { }); } }; -/*** Function show the item selected by type field ***/ +/*** Show the item's form by selected type field ***/ document.addEventListener("change", (e) => { if (e.target.id != "id_type") return; enrich_type(); @@ -279,65 +279,24 @@ Number.prototype.toHHMMSS = function () { }; /** - * UNUSED function ?? - * TODO : remove in 3.8.0 - * @param {*} data + * Display the proper enrichment form with the selected type. */ -/* -function get_form(data) { - var form = document.getElementById("form_enrich"); - form.style.display = "none"; - //form.innerHTML = data; - let htmlData = new DOMParser().parseFromString(data, "text/html").body - .firstChild; - form.innerHTML(htmlData); - htmlData.querySelectorAll("script").forEach((item) => { - if (item.src) { - let script = document.createElement("script"); - script.src = item.src; - document.body.appendChild(script); - } else { - // inline script - (0, eval)(item.innerHTML); - } - }); - - fadeIn(form); - var inputStart = document.getElementById("id_start"); - inputStart.insertAdjacentHTML( - "beforebegin", - " ", - ); - var inputEnd = document.getElementById("id_end"); - inputEnd.insertAdjacentHTML( - "beforebegin", - " ", - ); - enrich_type(); - manageResize(); -}*/ - function enrich_type() { + /** First, hide all element types */ document.getElementById("id_image").closest("div.form-group").style.display = "none"; document .querySelector("textarea#id_richtext") .closest("div.form-group").style.display = "none"; - document .getElementById("id_weblink") .closest("div.form-group").style.display = "none"; - document .getElementById("id_document") .closest("div.form-group").style.display = "none"; - document.getElementById("id_embed").closest("div.form-group").style.display = "none"; + var val = document.getElementById("id_type").value; if (val != "") { var form = document.getElementById("form_enrich"); @@ -427,24 +386,24 @@ function verify_fields() { error = true; } + let file_selector, attached; switch (document.getElementById("id_type").value) { case "image": - let img = document.getElementById("id_image_thumbnail_img"); - if (img) { - if (img.src == "/static/filer/icons/nofile_48x48.png") { - //check with id_image value - img.insertAdjacentElement( - "beforebegin", - "   " + - gettext("Please enter a correct image.") + - "", - ); - img.closest("div.form-group").classList.add("has-error"); - error = true; - } + file_selector = document.getElementById("fileinput_id_image"); + attached = file_selector.querySelector("img"); + if (!attached || attached.src == "") { + //check with id_image value + file_selector.insertAdjacentHTML( + "beforebegin", + "   " + + gettext("Please attach an image.") + + "", + ); + file_selector.closest("div.form-group").classList.add("has-error"); + error = true; } - break; + case "richtext": let richtext = document.getElementById("id_richtext"); if (richtext.value == "") { @@ -455,10 +414,10 @@ function verify_fields() { "", ); richtext.closest("div.form-group").classList.add("has-error"); - error = true; } break; + case "weblink": let weblink = document.getElementById("id_weblink"); if (weblink.value == "") { @@ -469,7 +428,6 @@ function verify_fields() { "", ); weblink.closest("div.form-group").classList.add("has-error"); - error = true; } else { if (weblink.value > 200) { @@ -480,26 +438,26 @@ function verify_fields() { "", ); weblink.closest("div.form-group").classList.add("has-error"); - error = true; } } break; + case "document": - let documentD = document.getElementById("id_document"); - if (documentD.src == "/static/filer/icons/nofile_48x48.png") { - //check with id_document value - documentD.insertAdjacentHTML( + file_selector = document.getElementById("fileinput_id_document"); + attached = file_selector.querySelector("a"); + if (!attached || attached.href == "") { + file_selector.insertAdjacentHTML( "beforebegin", "   " + - gettext("Please select a document.") + + gettext("Please attach a document.") + "", ); - documentD.closest("div.form-group").classList.add("has-error"); - + file_selector.closest("div.form-group").classList.add("has-error"); error = true; } break; + case "embed": let embed = document.getElementById("id_embed"); if (embed.value == "") { @@ -532,7 +490,7 @@ function verify_fields() { inputType.insertAdjacentHTML( "beforebegin", "   " + - gettext("Please enter a type in index field.") + + gettext("Please choose a type for this enrichment.") + "", ); inputType.closest("div.form-group").classList.add("has-error"); diff --git a/pod/enrichment/templates/enrichment/form_enrichment.html b/pod/enrichment/templates/enrichment/form_enrichment.html index eb687b32b3..22205f4723 100644 --- a/pod/enrichment/templates/enrichment/form_enrichment.html +++ b/pod/enrichment/templates/enrichment/form_enrichment.html @@ -5,7 +5,8 @@

{% trans 'Create / Edit enrichment' %}

-
+ {% csrf_token %}
{% if form_enrichment.errors or form_enrichment.non_field_errors %} diff --git a/pod/enrichment/urls.py b/pod/enrichment/urls.py index d4864b9290..0a9a317010 100644 --- a/pod/enrichment/urls.py +++ b/pod/enrichment/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import re_path from .views import edit_enrichment from .views import video_enrichment from .views import group_enrichment @@ -6,18 +6,18 @@ app_name = "enrichment" urlpatterns = [ - url(r"^edit/(?P[\-\d\w]+)/$", edit_enrichment, name="edit_enrichment"), - url( + re_path(r"^edit/(?P[\-\d\w]+)/$", edit_enrichment, name="edit_enrichment"), + re_path( r"^group/(?P[\-\d\w]+)/$", group_enrichment, name="group_enrichment", ), - url( + re_path( r"^video/(?P[\-\d\w]+)/$", video_enrichment, name="video_enrichment", ), - url( + re_path( r"^video/(?P[\-\d\w]+)/(?P[\-\d\w]+)/$", video_enrichment, name="video_enrichment_private", diff --git a/pod/enrichment/views.py b/pod/enrichment/views.py index 0c91e45a96..b6beaa89dd 100644 --- a/pod/enrichment/views.py +++ b/pod/enrichment/views.py @@ -7,7 +7,7 @@ from django.template.loader import render_to_string from django.shortcuts import render from django.shortcuts import get_object_or_404 -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import ensure_csrf_cookie @@ -16,6 +16,7 @@ from pod.video.models import Video from pod.video.utils import sort_videos_list from pod.video.views import render_video +from pod.main.utils import is_ajax from .models import Enrichment, EnrichmentGroup from .forms import EnrichmentForm, EnrichmentGroupForm @@ -108,7 +109,7 @@ def edit_enrichment_new(request, video): list_enrichment = video.enrichment_set.all() form_enrichment = EnrichmentForm(initial={"video": video, "start": 0, "end": 1}) - if request.is_ajax(): + if is_ajax(request): return render( request, "enrichment/form_enrichment.html", @@ -143,7 +144,7 @@ def edit_enrichment_save(request, video): form_enrichment.save() # list_enrichment = video.enrichment_set.all() # enrichment_to_vtt(list_enrichment, video) - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_enrichment": render_to_string( "enrichment/list_enrichment.html", @@ -160,7 +161,7 @@ def edit_enrichment_save(request, video): {"video": video, "list_enrichment": list_enrichment}, ) else: - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "errors": "{0}".format(_("Please correct errors.")), "form": render_to_string( @@ -188,7 +189,7 @@ def edit_enrichment_modify(request, video): enrich = get_object_or_404(Enrichment, id=request.POST["id"]) form_enrichment = EnrichmentForm(instance=enrich) - if request.is_ajax(): + if is_ajax(request): return render( request, "enrichment/form_enrichment.html", @@ -212,7 +213,7 @@ def edit_enrichment_delete(request, video): list_enrichment = video.enrichment_set.all() # if list_enrichment: # enrichment_to_vtt(list_enrichment, video) - if request.is_ajax(): + if is_ajax(request): some_data_to_dump = { "list_enrichment": render_to_string( "enrichment/list_enrichment.html", diff --git a/pod/import_video/context_processors.py b/pod/import_video/context_processors.py index d72e775b31..9e2dbc4b25 100644 --- a/pod/import_video/context_processors.py +++ b/pod/import_video/context_processors.py @@ -2,7 +2,7 @@ from django.conf import settings as django_settings -USE_IMPORT_VIDEO = getattr(django_settings, "USE_IMPORT_VIDEO", True) +USE_IMPORT_VIDEO = getattr(django_settings, "USE_IMPORT_VIDEO", False) RESTRICT_EDIT_IMPORT_VIDEO_ACCESS_TO_STAFF_ONLY = getattr( django_settings, "RESTRICT_EDIT_IMPORT_VIDEO_ACCESS_TO_STAFF_ONLY", True diff --git a/pod/import_video/forms.py b/pod/import_video/forms.py index 7ea44d8e17..4b2ef756e1 100644 --- a/pod/import_video/forms.py +++ b/pod/import_video/forms.py @@ -5,7 +5,7 @@ from django.contrib.sites.models import Site from django.core.validators import URLValidator from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.import_video.models import ExternalRecording from pod.main.forms_utils import add_placeholder_and_asterisk from pod.main.forms_utils import OwnerWidget, AddOwnerWidget diff --git a/pod/import_video/models.py b/pod/import_video/models.py index b28a3615f7..68e6b77334 100644 --- a/pod/import_video/models.py +++ b/pod/import_video/models.py @@ -12,7 +12,7 @@ from django.db.models.signals import pre_save from django.dispatch import receiver from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.meeting.utils import ( api_call, diff --git a/pod/import_video/static/css/import_video.css b/pod/import_video/static/css/import_video.css index 15e642c0cc..37021a81b1 100644 --- a/pod/import_video/static/css/import_video.css +++ b/pod/import_video/static/css/import_video.css @@ -47,12 +47,15 @@ i.bi.bi-exclamation-circle.me-2 { color: #c82630; } + i.bi.bi-exclamation-triangle.me-2 { color: #f9af2c; } + i.bi.bi-check-circle.me-2 { color: #00986a; } + i.bi.bi-info-circle.me-2 { color: #00b3c8; } diff --git a/pod/import_video/tests/test_views.py b/pod/import_video/tests/test_views.py index 28baa1cc14..e70858a13e 100644 --- a/pod/import_video/tests/test_views.py +++ b/pod/import_video/tests/test_views.py @@ -14,7 +14,7 @@ from django.test import TestCase from django.test import Client from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from http import HTTPStatus # Directory that will contain the video files generated by bbb-recorder diff --git a/pod/import_video/utils.py b/pod/import_video/utils.py index fd1394cb0f..bdcfda341d 100644 --- a/pod/import_video/utils.py +++ b/pod/import_video/utils.py @@ -55,7 +55,7 @@ BBB_API_URL = getattr(settings, "BBB_API_URL", "") -def secure_request_for_upload(request): +def secure_request_for_upload(request) -> None: """Check that the request is correct for uploading a recording. Args: @@ -219,7 +219,7 @@ def manage_download( raise ValueError(mark_safe(str(exc))) -def download_video_file(session: Session, source_video_url: str, dest_file: str): +def download_video_file(session: Session, source_video_url: str, dest_file: str) -> None: """Download video file. Args: @@ -257,7 +257,7 @@ def download_video_file(session: Session, source_video_url: str, dest_file: str) def save_video( user: User, dest_path: str, recording_name: str, description: str, date_evt=None -): +) -> None: """Save and encode the Pod video file. Args: @@ -309,7 +309,7 @@ def check_url_exists(source_url: str) -> bool: return False -def verify_video_exists_and_size(video_url: str): +def verify_video_exists_and_size(video_url: str) -> None: """Check that the video file exists and its size does not exceed the limit. Args: @@ -336,7 +336,7 @@ def verify_video_exists_and_size(video_url: str): raise ValueError(msg) -def check_video_size(video_size: int): +def check_video_size(video_size: int) -> None: """Check that the video file size does not exceed the limit. Args: @@ -514,7 +514,7 @@ def check_file_exists(source: str) -> bool: return False -def move_file(source: str, destination: str): +def move_file(source: str, destination: str) -> None: """Move a file from a source to another destination.""" try: # Ensure that the source file exists @@ -616,7 +616,7 @@ class TypeSourceURL: # API URL if supplied api_url = "" - def __init__(self, type, url, extension, api_url): + def __init__(self, type, url, extension, api_url) -> None: """Initialize.""" self.type = type self.url = url @@ -632,7 +632,7 @@ class video_parser(HTMLParser): HTMLParser (_type_): _description_ """ - def __init__(self): + def __init__(self) -> None: """Initialize video parser.""" super().__init__() self.reset() @@ -644,7 +644,7 @@ def __init__(self): self.video_file = "" self.video_type = "" - def handle_starttag(self, tag, attrs): + def handle_starttag(self, tag, attrs) -> None: """Parse BBB Web page and search video file.""" attrs = dict(attrs) # Search for source tag @@ -660,7 +660,7 @@ def handle_starttag(self, tag, attrs): # Found the title line self.title_check = True - def handle_data(self, data): + def handle_data(self, data) -> None: """Search for title tag.""" if self.title_check: # Get the title that corresponds to recording's name @@ -696,7 +696,7 @@ class StatelessRecording: # Recording id (BBB format), when created on the same BBB infra as meeting module recordingId = "" - def __init__(self, id, name, state): + def __init__(self, id, name, state) -> None: """Initiliaze.""" self.id = id self.name = name @@ -710,11 +710,11 @@ def get_end_time(self): """Return BBB epoch in milliseconds.""" return dt.fromtimestamp(float(self.endTime) / 1000) - def get_duration(self): + def get_duration(self) -> str: """Return duration.""" return str(self.get_end_time() - self.get_start_time()).split(".")[0] - def to_json(self): + def to_json(self) -> str: """Return recording data (without uploadedToPodBy) in JSON format.""" exclusion_list = ["uploadedToPodBy"] return json.dumps( diff --git a/pod/import_video/views.py b/pod/import_video/views.py index eb56acbfb6..3dd14f45dc 100644 --- a/pod/import_video/views.py +++ b/pod/import_video/views.py @@ -40,7 +40,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie from pod.import_video.utils import manage_download from pod.main.views import in_maintenance -from pod.main.utils import secure_post_request, display_message_with_icon +from pod.main.utils import secure_post_request, display_message_with_icon, is_ajax # For Youtube download, use PyTubeFix in replacement of PyTube from pytubefix import YouTube @@ -1219,7 +1219,7 @@ def recording_with_token(request, id): # JSON format data = '{"presentationUrl": "%s", "videoUrl": "%s"}' % (presentation_url, video_url) - if request.is_ajax(): + if is_ajax(request): return HttpResponse(data, content_type="application/json") else: return HttpResponseBadRequest() diff --git a/pod/live/__init__.py b/pod/live/__init__.py index 2653454af3..e69de29bb2 100644 --- a/pod/live/__init__.py +++ b/pod/live/__init__.py @@ -1 +0,0 @@ -default_app_config = "pod.live.apps.LiveConfig" diff --git a/pod/live/admin.py b/pod/live/admin.py index 6f6898bb1e..431ee4f090 100644 --- a/pod/live/admin.py +++ b/pod/live/admin.py @@ -4,9 +4,9 @@ from django.contrib.sites.shortcuts import get_current_site from django.urls import reverse from django.utils.html import format_html -from django.utils.translation import ugettext_lazy as _ -from js_asset import static +from django.utils.translation import gettext_lazy as _ from sorl.thumbnail import get_thumbnail +from django.templatetags.static import static from pod.live.forms import BuildingAdminForm, EventAdminForm, BroadcasterAdminForm from pod.live.models import ( @@ -26,10 +26,12 @@ USE_PODFILE = getattr(settings, "USE_PODFILE", False) +@admin.register(HeartBeat) class HeartBeatAdmin(admin.ModelAdmin): list_display = ("viewkey", "user", "event", "last_heartbeat") +@admin.register(Building) class BuildingAdmin(admin.ModelAdmin): form = BuildingAdminForm list_display = ("name", "gmapurl") @@ -70,6 +72,7 @@ class Media: ) +@admin.register(Broadcaster) class BroadcasterAdmin(admin.ModelAdmin): form = BroadcasterAdminForm list_display = ( @@ -136,12 +139,10 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): kwargs["queryset"] = Building.objects.filter(sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) + @admin.display(description=_("QR code")) def qrcode(self, obj): return obj.qrcode - qrcode.short_description = _("QR code") - qrcode.allow_tags = True - class Media: css = { "all": ( @@ -181,6 +182,7 @@ def queryset(self, request, queryset): return queryset +@admin.register(Event) class EventAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): ModelForm = super(EventAdmin, self).get_form(request, obj, **kwargs) @@ -192,22 +194,27 @@ def __new__(cls, *args, **kwargs): return ModelFormMetaClass + @admin.display( + description=_("Broadcaster"), + ordering="broadcaster", + ) def get_broadcaster_admin(self, instance): return instance.broadcaster.name - get_broadcaster_admin.short_description = _("Broadcaster") - + @admin.display( + description=_("Auto start admin"), + boolean=True, + ordering="is_auto_start", + ) def is_auto_start_admin(self, instance): return instance.is_auto_start - is_auto_start_admin.short_description = _("Auto start admin") - is_auto_start_admin.boolean = True - def formfield_for_foreignkey(self, db_field, request, **kwargs): if db_field.name == "video_on_hold": kwargs["queryset"] = Video.objects.filter(sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) + @admin.display(description=_("Thumbnails")) def get_thumbnail_admin(self, instance): if instance.thumbnail and instance.thumbnail.file_exist(): im = get_thumbnail( @@ -225,7 +232,6 @@ def get_thumbnail_admin(self, instance): ) ) - get_thumbnail_admin.short_description = _("Thumbnails") get_thumbnail_admin.list_filter = True form = EventAdminForm @@ -278,9 +284,6 @@ def get_thumbnail_admin(self, instance): ] autocomplete_fields = ["video_on_hold"] - get_broadcaster_admin.admin_order_field = "broadcaster" - is_auto_start_admin.admin_order_field = "is_auto_start" - if USE_PODFILE: fields.append("thumbnail") @@ -300,8 +303,4 @@ class Media: ) -admin.site.register(Building, BuildingAdmin) -admin.site.register(Broadcaster, BroadcasterAdmin) -admin.site.register(HeartBeat, HeartBeatAdmin) -admin.site.register(Event, EventAdmin) admin.site.register(LiveTranscriptRunningTask) diff --git a/pod/live/forms.py b/pod/live/forms.py index b580737a51..eba64d14cc 100644 --- a/pod/live/forms.py +++ b/pod/live/forms.py @@ -1,7 +1,7 @@ from django import forms from django.conf import settings from django.db.models import Q -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.admin import widgets from pod.live.models import ( Broadcaster, diff --git a/pod/live/models.py b/pod/live/models.py index 9af42db514..dd58d22339 100644 --- a/pod/live/models.py +++ b/pod/live/models.py @@ -19,7 +19,7 @@ from django.urls import reverse from django.utils import timezone from django.utils.html import format_html -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.main.lang_settings import ALL_LANG_CHOICES as __ALL_LANG_CHOICES__ from pod.main.lang_settings import PREF_LANG_CHOICES as __PREF_LANG_CHOICES__ diff --git a/pod/live/pilotingInterface.py b/pod/live/pilotingInterface.py index a37676402d..0664058c43 100644 --- a/pod/live/pilotingInterface.py +++ b/pod/live/pilotingInterface.py @@ -7,7 +7,6 @@ import re from abc import ABC as __ABC__, abstractmethod from datetime import timedelta -from typing import Optional import paramiko import requests @@ -16,6 +15,7 @@ from .models import Broadcaster, Event from .utils import date_string_to_second +from pod.main.utils import is_ajax DEFAULT_EVENT_PATH = getattr(settings, "DEFAULT_EVENT_PATH", "") @@ -140,7 +140,7 @@ def get_stream_rtmp_infos(self) -> dict: def ajax_get_mandatory_parameters(request): """Return the mandatory parameters as a json response.""" - if request.method == "GET" and request.is_ajax(): + if request.method == "GET" and is_ajax(request): impl_name = request.GET.get("impl_name", None) params = get_mandatory_parameters(impl_name) params_json = {} @@ -223,7 +223,7 @@ def validate_json_implementation(broadcaster: Broadcaster) -> bool: return True -def get_piloting_implementation(broadcaster) -> Optional[PilotingInterface]: +def get_piloting_implementation(broadcaster): """Return the class inheriting from PilotingInterface according to the broadcaster configuration (or None).""" if broadcaster is None: return None diff --git a/pod/live/templates/live/event-all-info.html b/pod/live/templates/live/event-all-info.html index c93302bce0..840d5006db 100644 --- a/pod/live/templates/live/event-all-info.html +++ b/pod/live/templates/live/event-all-info.html @@ -1,5 +1,4 @@ {% load i18n %} -{% load tagging_tags %}
diff --git a/pod/live/templates/live/event-info.html b/pod/live/templates/live/event-info.html index af7b646649..0beccb9a04 100644 --- a/pod/live/templates/live/event-info.html +++ b/pod/live/templates/live/event-info.html @@ -1,6 +1,5 @@ {% load i18n %} {% load static %} -{% load tagging_tags %} {% load thumbnail %} {% load event_tags %} diff --git a/pod/live/templates/live/filter_aside.html b/pod/live/templates/live/filter_aside.html index 9986f68f14..520e21d3bd 100644 --- a/pod/live/templates/live/filter_aside.html +++ b/pod/live/templates/live/filter_aside.html @@ -1,6 +1,5 @@ {% load i18n %} {% load static %} -{% load tagging_tags %} {% load thumbnail %} {% spaceless %} diff --git a/pod/live/templatetags/event_tags.py b/pod/live/templatetags/event_tags.py index d817052102..0885d8e56a 100644 --- a/pod/live/templatetags/event_tags.py +++ b/pod/live/templatetags/event_tags.py @@ -1,7 +1,7 @@ from django.template.defaultfilters import register from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.live.models import Event from pod.live.views import can_manage_event from pod.main.utils import generate_qrcode diff --git a/pod/live/tests/test_views.py b/pod/live/tests/test_views.py index d79d19713a..0b75f6679b 100644 --- a/pod/live/tests/test_views.py +++ b/pod/live/tests/test_views.py @@ -1145,7 +1145,7 @@ def checkAjaxPostImplementationError(self, url): url, content_type="application/json", data=json.dumps({"idbroadcaster": 1, "idevent": 1}), - HTTP_X_REQUESTED_WITH="XMLHttpRequest", + headers={"x-requested-with": "XMLHttpRequest"}, ) self.assertEqual(response.status_code, 200) self.assertTrue("error" in response.json()) @@ -1170,7 +1170,7 @@ def test_isstreamavailabletorecord(self): # implementation error response = self.client.get( - url, {"idbroadcaster": 1}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idbroadcaster": 1}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 200) self.assertTrue("message" in response.json()) @@ -1294,7 +1294,7 @@ def response_is_recording_ok(url, request): data=json.dumps( {"idbroadcaster": 2, "idevent": 1}, ), - HTTP_X_REQUESTED_WITH="XMLHttpRequest", + headers={"x-requested-with": "XMLHttpRequest"}, ) self.assertEqual( response.json(), @@ -1307,7 +1307,7 @@ def response_is_recording_ok(url, request): url, content_type="application/json", data=json.dumps({"idbroadcaster": 2, "idevent": 1}), - HTTP_X_REQUESTED_WITH="XMLHttpRequest", + headers={"x-requested-with": "XMLHttpRequest"}, ) self.assertEqual(response.json(), {"success": True, "duration": 3}) print(" ---> test event_info_record recording: OK!") @@ -1538,7 +1538,7 @@ def test_event_video_cards(self): print(" ---> test event_video_cards not ajax: OK!") response = self.client.get( - url, {"idevent": 1}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idevent": 1}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.json(), {"content": ""}) print(" ---> test event_video_cards empty: OK!") @@ -1549,7 +1549,7 @@ def test_event_video_cards(self): event.save() response = self.client.get( - url, {"idevent": 1}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idevent": 1}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 200) self.assertNotEqual(response.json(), {"content": ""}) @@ -1741,7 +1741,7 @@ def test_ajax_event_get_rtmp_config(self): # implementation error response = self.client.get( - url, {"idbroadcaster": 1}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idbroadcaster": 1}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 200) self.assertTrue("error" in response.json()) @@ -1750,7 +1750,7 @@ def test_ajax_event_get_rtmp_config(self): # wowza response = self.client.get( - url, {"idbroadcaster": 2}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idbroadcaster": 2}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"success": True, "data": {}}) @@ -1763,7 +1763,7 @@ def rtmp_response_error(url, request): with HTTMock(rtmp_response_error): response = self.client.get( - url, {"idbroadcaster": 3}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idbroadcaster": 3}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 200) self.assertEqual( @@ -1777,7 +1777,7 @@ def rtmp_response_not_list(url, request): with HTTMock(rtmp_response_not_list): response = self.client.get( - url, {"idbroadcaster": 3}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idbroadcaster": 3}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 200) self.assertEqual( @@ -1791,7 +1791,7 @@ def rtmp_response_no_valid_record(url, request): with HTTMock(rtmp_response_no_valid_record): response = self.client.get( - url, {"idbroadcaster": 3}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idbroadcaster": 3}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"success": True, "data": {}}) @@ -1819,7 +1819,7 @@ def rtmp_response_data(url, request): with HTTMock(rtmp_response_data): response = self.client.get( - url, {"idbroadcaster": 3}, HTTP_X_REQUESTED_WITH="XMLHttpRequest" + url, {"idbroadcaster": 3}, headers={"x-requested-with": "XMLHttpRequest"} ) self.assertEqual(response.status_code, 200) expected = { @@ -1846,7 +1846,7 @@ def test_ajax_event_start_streaming(self): self.client.force_login(self.user) # http method unauthorized - response = self.client.get(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + response = self.client.get(url, headers={"x-requested-with": "XMLHttpRequest"}) self.assertEqual(response.status_code, 405) print(" ---> test ajax_event_start_streaming HttpResponseNotAllowed: OK!") @@ -1859,7 +1859,7 @@ def test_ajax_event_start_streaming(self): url, data={}, content_type="application/json", - HTTP_X_REQUESTED_WITH="XMLHttpRequest", + headers={"x-requested-with": "XMLHttpRequest"}, ) print(" ---> test ajax_event_start_streaming: OK!") @@ -1876,7 +1876,7 @@ def test_ajax_event_stop_streaming(self): self.client.force_login(self.user) # http method unauthorized - response = self.client.get(url, HTTP_X_REQUESTED_WITH="XMLHttpRequest") + response = self.client.get(url, headers={"x-requested-with": "XMLHttpRequest"}) self.assertEqual(response.status_code, 405) print(" ---> test ajax_event_stop_streaming HttpResponseNotAllowed: OK!") @@ -1889,7 +1889,7 @@ def test_ajax_event_stop_streaming(self): url, data={}, content_type="application/json", - HTTP_X_REQUESTED_WITH="XMLHttpRequest", + headers={"x-requested-with": "XMLHttpRequest"}, ) print(" ---> test ajax_event_stop_streaming: OK!") diff --git a/pod/live/urls.py b/pod/live/urls.py index c1f8377c2b..6b2bd75f7a 100644 --- a/pod/live/urls.py +++ b/pod/live/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path, re_path from .pilotingInterface import ajax_get_mandatory_parameters from .views import ( @@ -30,84 +30,84 @@ urlpatterns = [] urlpatterns += [ - # url(r"^$", lives, name="lives"), - url( - r"^ajax_calls/event_startrecord/$", + # re_path(r"^$", lives, name="lives"), + path( + "ajax_calls/event_startrecord/", ajax_event_startrecord, name="ajax_event_startrecord", ), - url( - r"^ajax_calls/event_stoprecord/$", + path( + "ajax_calls/event_stoprecord/", ajax_event_stoprecord, name="ajax_event_stoprecord", ), - url( - r"^ajax_calls/event_splitrecord/$", + path( + "ajax_calls/event_splitrecord/", ajax_event_splitrecord, name="ajax_event_splitrecord", ), - url( - r"^ajax_calls/event_start_streaming/$", + path( + "ajax_calls/event_start_streaming/", ajax_event_start_streaming, name="ajax_event_start_streaming", ), - url( - r"^ajax_calls/event_stop_streaming/$", + path( + "ajax_calls/event_stop_streaming/", ajax_event_stop_streaming, name="ajax_event_stop_streaming", ), - url( - r"^ajax_calls/event_get_rtmp_config/$", + path( + "ajax_calls/event_get_rtmp_config/", ajax_event_get_rtmp_config, name="ajax_event_get_rtmp_config", ), - url( - r"^ajax_calls/getbroadcastersfrombuiding/$", + path( + "ajax_calls/getbroadcastersfrombuiding/", broadcasters_from_building, name="broadcasters_from_building", ), - url( - r"^ajax_calls/getbroadcasterrestriction/$", + path( + "ajax_calls/getbroadcasterrestriction/", broadcaster_restriction, name="ajax_broadcaster_restriction", ), - url( - r"^ajax_calls/geteventinforcurrentecord/$", + path( + "ajax_calls/geteventinforcurrentecord/", ajax_event_info_record, name="ajax_event_info_record", ), - url( - r"^ajax_calls/geteventvideocards/$", + path( + "ajax_calls/geteventvideocards/", event_get_video_cards, name="event_get_video_cards", ), - url( - r"^ajax_calls/isstreamavailabletorecord/$", + path( + "ajax_calls/isstreamavailabletorecord/", ajax_is_stream_available_to_record, name="ajax_is_stream_available_to_record", ), - url( - r"^ajax_calls/getmandatoryparameters/$", + path( + "ajax_calls/getmandatoryparameters/", ajax_get_mandatory_parameters, name="ajax_get_mandatory_parameters", ), - url(r"^ajax_calls/heartbeat/", heartbeat, name="heartbeat"), - url(r"^direct/(?P[\-\w]+)/$", direct, name="direct"), - url(r"^directs/$", directs_all, name="directs_all"), - url(r"^directs/(?P\d+)/$", directs, name="directs"), - url(r"^event/(?P[\-\w]+)/$", event, name="event"), - url( + re_path(r"^ajax_calls/heartbeat/", heartbeat, name="heartbeat"), + re_path(r"^direct/(?P[\-\w]+)/$", direct, name="direct"), + path("directs/", directs_all, name="directs_all"), + path("directs//", directs, name="directs"), + re_path(r"^event/(?P[\-\w]+)/$", event, name="event"), + re_path( r"^event/(?P[\-\w]+)/(?P[\-\w]+)/$", event, name="event_private", ), - url(r"^event_edit/$", event_edit, name="event_edit"), - url(r"^event_edit/(?P[\-\w]+)/$", event_edit, name="event_edit"), - url(r"^event_delete/(?P[\-\w]+)/$", event_delete, name="event_delete"), - url(r"^events/$", events, name="events"), - url(r"^my_events/$", my_events, name="my_events"), - url( - r"^event_immediate_edit/(?P\d+)/$", + path("event_edit/", event_edit, name="event_edit"), + re_path(r"^event_edit/(?P[\-\w]+)/$", event_edit, name="event_edit"), + re_path(r"^event_delete/(?P[\-\w]+)/$", event_delete, name="event_delete"), + path("events/", events, name="events"), + path("my_events/", my_events, name="my_events"), + path( + "event_immediate_edit//", event_immediate_edit, name="event_immediate_edit", ), diff --git a/pod/live/utils.py b/pod/live/utils.py index c9c39094b1..1cb5bb1579 100644 --- a/pod/live/utils.py +++ b/pod/live/utils.py @@ -13,7 +13,7 @@ from django.contrib import messages from django.core.exceptions import PermissionDenied from django.core.mail import EmailMultiAlternatives, mail_managers -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.main.views import TEMPLATE_VISIBLE_SETTINGS diff --git a/pod/live/views.py b/pod/live/views.py index 862ab0cffd..4608b34092 100644 --- a/pod/live/views.py +++ b/pod/live/views.py @@ -28,7 +28,7 @@ from django.template.loader import render_to_string from django.urls import reverse from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect from pod.meeting.models import Livestream from rest_framework import status diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.po b/pod/locale/fr/LC_MESSAGES/djangojs.po index fe0daf6ccb..1adb6112dd 100644 --- a/pod/locale/fr/LC_MESSAGES/djangojs.po +++ b/pod/locale/fr/LC_MESSAGES/djangojs.po @@ -318,7 +318,7 @@ msgstr "Veuillez entrer un correct temps de fin compris entre 1 et " #: pod/enrichment/static/js/enrichment.js msgid "Please enter a correct image." -msgstr "Veuillez joindre une image correct." +msgstr "Veuillez joindre une image." #: pod/enrichment/static/js/enrichment.js msgid "Please enter a correct richtext." @@ -338,7 +338,7 @@ msgstr "Veuillez joindre un document." #: pod/enrichment/static/js/enrichment.js msgid "Please enter a correct embed." -msgstr "Veuillez entrer une intĂ©gration correct." +msgstr "Veuillez entrer un code d’intĂ©gration correct." #: pod/enrichment/static/js/enrichment.js msgid "Embed field must be less than 200 characters." diff --git a/pod/lti/views.py b/pod/lti/views.py index 49454038cb..1be0d294b1 100644 --- a/pod/lti/views.py +++ b/pod/lti/views.py @@ -8,7 +8,7 @@ from django.urls import reverse from django.contrib import messages -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.video.models import Video diff --git a/pod/main/__init__.py b/pod/main/__init__.py index 66846ce4cc..6567fafaa8 100644 --- a/pod/main/__init__.py +++ b/pod/main/__init__.py @@ -5,4 +5,3 @@ from .celery import app as celery_app __all__ = ("celery_app",) -default_app_config = "pod.main.apps.MainConfig" diff --git a/pod/main/admin.py b/pod/main/admin.py index c23483d38f..d6234d83f5 100644 --- a/pod/main/admin.py +++ b/pod/main/admin.py @@ -6,7 +6,7 @@ from django.contrib.flatpages.admin import FlatpageForm from django.contrib.flatpages.models import FlatPage from django.contrib.sites.models import Site -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.conf import settings from django.contrib.sites.shortcuts import get_current_site from modeltranslation.admin import TranslationAdmin @@ -30,12 +30,14 @@ class Meta: widgets = content_widget +@admin.register(AdditionalChannelTab) class AdditionalChannelTabAdmin(TranslationAdmin): """Create translation for additional Channel Tab Field.""" list_display = ("name",) +@admin.register(Configuration) class ConfigurationAdmin(admin.ModelAdmin): list_display = ("key", "value", "description") @@ -83,6 +85,7 @@ def save_model(self, request, obj, form, change): obj.save() +@admin.register(LinkFooter) class LinkFooterAdmin(TranslationAdmin): list_display = ( "title", @@ -120,6 +123,7 @@ class Meta: fields = "__all__" +@admin.register(Block) class BlockAdmin(TranslationAdmin): """The admin configuration for the Block model in the Django admin panel.""" @@ -150,7 +154,3 @@ def get_form(self, request, obj=None, **kwargs): # Unregister the default FlatPage admin and register CustomFlatPageAdmin. admin.site.unregister(FlatPage) admin.site.register(FlatPage, CustomFlatPageAdmin) -admin.site.register(LinkFooter, LinkFooterAdmin) -admin.site.register(Configuration, ConfigurationAdmin) -admin.site.register(AdditionalChannelTab, AdditionalChannelTabAdmin) -admin.site.register(Block, BlockAdmin) diff --git a/pod/main/configuration.json b/pod/main/configuration.json index 8930f5445d..9d74698907 100644 --- a/pod/main/configuration.json +++ b/pod/main/configuration.json @@ -1018,7 +1018,7 @@ }, "settings": { "USE_CUT": { - "default_value": true, + "default_value": false, "description": { "en": [ "Activation of the Cut application" @@ -1049,7 +1049,7 @@ }, "settings": { "USE_DRESSING": { - "default_value": true, + "default_value": false, "description": { "en": [ "Activation of dressings. Allows users to customize a video with watermark & credits." @@ -1194,7 +1194,7 @@ "pod_version_init": "3.3.0" }, "USE_IMPORT_VIDEO": { - "default_value": true, + "default_value": false, "description": { "en": [ "Activation of the video import application" @@ -1239,7 +1239,7 @@ "pod_version_init": "3.5.1" }, "IMPORT_VIDEO_BBB_RECORDER_PATH": { - "default_value": true, + "default_value": "/data/bbb-recorder/media/", "description": { "en": [ "Directory that will contain the video files generated by bbb-recorder." @@ -1611,10 +1611,10 @@ ], "fr": [ "Utilisation de BigBlueButton", - "[TODO] À retirer dans les futures versions de Pod" + "Module obsolĂšte." ] }, - "pod_version_end": "", + "pod_version_end": "3.8.2", "pod_version_init": "3.1" }, "USE_BBB_LIVE": { @@ -1632,7 +1632,7 @@ "pod_version_init": "3.1" }, "USE_IMPORT_VIDEO": { - "default_value": true, + "default_value": false, "description": { "en": [ "Activation of the video import application" @@ -2121,7 +2121,7 @@ "pod_version_init": "3.6" }, "USE_FAVORITES": { - "default_value": true, + "default_value": false, "description": { "en": [ "Activation of favorite videos.", @@ -2136,7 +2136,7 @@ "pod_version_init": "3.4" }, "USE_PLAYLIST": { - "default_value": true, + "default_value": false, "description": { "en": [ "Activation of playlist. Allows users to add videos in a playlist." @@ -2149,7 +2149,7 @@ "pod_version_init": "3.4" }, "USE_PROMOTED_PLAYLIST": { - "default_value": true, + "default_value": false, "description": { "en": [ "Activation of promoted playlists. Allows users to use the promoted playlists." @@ -2238,7 +2238,7 @@ "description": {}, "settings": { "USE_NOTIFICATIONS": { - "default_value": true, + "default_value": false, "description": { "en": [ "" @@ -2283,7 +2283,7 @@ }, "settings": { "USE_QUIZ": { - "default_value": true, + "default_value": false, "description": { "en": [ "Activation of quizzes. Allows users to create, respond and use quizzes in videos." @@ -3714,17 +3714,16 @@ "pod_version_init": "3.1.0" }, "ES_VERSION": { - "default_value": 6, + "default_value": 8, "description": { "en": [ "" ], "fr": [ "Version d’ElasticSearch.", - "valeurs possibles 6, 7 ou 8 correspondant Ă  la version du Elasticsearch utilisĂ©.", - "Pour utiliser la version 7 ou 8, faire une mise Ă  jour du paquet elasticsearch-py ", - "Pour la 7, `pip3 install elasticsearch==7.17.7`,", - "et pour la 8, `pip3 install elasticsearch==8.8.1`.", + "valeurs possibles : `8`, correspondant Ă  la version du serveur Elasticsearch utilisĂ©.", + "Attention, le paquet elasticsearch-py doit correspondre Ă  la version du serveur. ", + "pour la 8, `pip3 install elasticsearch==8.16.0`.", "Voir [elasticsearch-py.readthedocs.io](https://elasticsearch-py.readthedocs.io/)", "pour plus d’information." ] @@ -3868,7 +3867,7 @@ "C’est un dictionnaire imbriquĂ© dont les contenus font correspondre", "l’alias de base de donnĂ©es avec un dictionnaire contenant", "les options de chacune des bases de donnĂ©es.", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#databases)_", + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#databases)_", "valeur par dĂ©faut : une base de donnĂ©es au format sqlite", "Voici un exemple de configuration pour utiliser une base MySQL :", "```python", @@ -3950,7 +3949,7 @@ ], "fr": [ "nom du serveur smtp ", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#email-host)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#email-host)_" ] }, "pod_version_end": "", @@ -4418,7 +4417,7 @@ "Le rĂ©pertoire dans lequel stocker temporairement les donnĂ©es", "(typiquement pour les fichiers plus grands que `FILE_UPLOAD_MAX_MEMORY_SIZE`)", "lors des tĂ©lĂ©versements de fichiers.", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#file-upload-temp-dir)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#file-upload-temp-dir)_" ] }, "pod_version_end": "", @@ -4434,7 +4433,7 @@ "Chemin absolu du systĂšme de fichiers pointant vers le rĂ©pertoire qui contiendra", "les fichiers tĂ©lĂ©versĂ©s par les utilisateurs.
", "Attention, ce répertoire doit exister.
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#std:setting-MEDIA_ROOT)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#std:setting-MEDIA_ROOT)_" ] }, "pod_version_end": "", @@ -4479,7 +4478,7 @@ "Le chemin absolu vers le répertoire dans lequel collectstatic rassemble", "les fichiers statiques en vue du déploiement.", "Ce chemin sera précisé dans le fichier de configurtation du vhost nginx.
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#std:setting-STATIC_ROOT)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#std:setting-STATIC_ROOT)_" ] }, "pod_version_end": "", @@ -4593,7 +4592,7 @@ "Exemple : `[('John', 'john@example.com'), ('Mary', 'mary@example.com')]`
", "Dans Pod, les \"admins\" sont Ă©galement destinataires des courriels de contact,", "d’encodage ou de flux RSS si la variable `CONTACT_US_EMAIL` n’est pas renseignĂ©e.
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#admins)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#admins)_" ] }, "pod_version_end": "", @@ -4609,7 +4608,7 @@ "Une liste de chaĂźnes reprĂ©sentant des noms de domaine/d’hĂŽte que ce site Django peut servir.
", "C’est une mesure de sĂ©curitĂ© pour empĂȘcher les attaques d’en-tĂȘte Host HTTP,", "qui sont possibles mĂȘme avec bien des configurations de serveur Web apparemment sĂ©curisĂ©es.
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#allowed-hosts)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#allowed-hosts)_" ] }, "pod_version_end": "", @@ -4674,7 +4673,7 @@ "l’ensemble des requetes en https.", "Idem pour les cookies de session et de cross-sites qui seront Ă©galement sĂ©curisĂ©s
", "Il faut les passer Ă  False en cas d’usage du runserver (phase de dĂ©veloppement / debugage)
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#secure-ssl-redirect)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#secure-ssl-redirect)_" ] }, "pod_version_end": "", @@ -4689,7 +4688,7 @@ "fr": [ "Une valeur booléenne qui active ou désactive le mode de débogage.
", "Ne déployez jamais de site en production avec le réglage DEBUG activé.
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#debug)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#debug)_" ] }, "pod_version_end": "", @@ -4720,7 +4719,7 @@ ], "fr": [ "url de redirection pour l’authentification de l’utilisateur", - "voir : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#login-url)" + "voir : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#login-url)" ] }, "pod_version_end": "", @@ -4738,7 +4737,7 @@ "Le premier manager renseignĂ© est Ă©galement contact des flus RSS.
", "Ils sont aussi destinataires des courriels de contact", "si la variable `CONTACT_US_EMAIL` n’est pas renseignĂ©e.
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#managers)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#managers)_" ] }, "pod_version_end": "", @@ -4781,7 +4780,7 @@ "Elle est utilisĂ©e dans le contexte de la signature cryptographique,", "et doit ĂȘtre dĂ©finie Ă  une valeur unique et non prĂ©dictible.
", "Vous pouvez utiliser ce site pour en générer une : [djecrety.ir](https://djecrety.ir/)
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#secret-key)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#secret-key)_" ] }, "pod_version_end": "", @@ -4828,7 +4827,7 @@ "Cette option empĂȘche le cookie d’ĂȘtre envoyĂ© dans les requĂȘtes inter-sites,", "ce qui prĂ©vient les attaques CSRF et rend impossible", "certaines mĂ©thodes de vol du cookie de session.", - "Voir [docs.djangoproject.com](https://docs.djangoproject.com/en/3.2/ref/settings/#std-setting-SESSION_COOKIE_SAMESITE)" + "Voir [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#std-setting-SESSION_COOKIE_SAMESITE)" ] }, "pod_version_end": "", @@ -4870,7 +4869,7 @@ "L’identifiant (nombre entier) du site actuel.", "Peut ĂȘtre utilisĂ© pour mettre en place une instance multi-tenant", "et ainsi gĂ©rer dans une mĂȘme base de donnĂ©es du contenu pour plusieurs sites.
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#site-id)_" + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#site-id)_" ] }, "pod_version_end": "", @@ -4913,7 +4912,7 @@ ], "fr": [ "Une chaßne représentant le fuseau horaire pour cette installation.
", - "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/#std:setting-TIME_ZONE)_", + "_ref : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/#std:setting-TIME_ZONE)_", "Liste des adresses destinataires des courriels de contact" ] }, @@ -5046,26 +5045,27 @@ "pod_version_init": "3.1.0" }, "DARKMODE_ENABLED": { - "default_value": false, + "default_value": true, "description": { "en": [ - "" + "Allows users to enable a dark mode." ], "fr": [ - "Activation du mode sombre" + "Permet aux utilisateurs d’activer un mode sombre." ] }, "pod_version_end": "", "pod_version_init": "3.1.0" }, "DYSLEXIAMODE_ENABLED": { - "default_value": false, + "default_value": true, "description": { "en": [ - "" + "Allows to use a font that is more suitable for people with dyslexia." ], "fr": [ - "Activation du mode dyslexie" + "Permet d’utiliser une police de caractĂšres plus adaptĂ©e", + " aux personnes atteintes de dyslexie." ] }, "pod_version_end": "", @@ -5575,19 +5575,19 @@ "fr": [ "", "La plateforme Esup-Pod se base sur le framework Django Ă©crit en Python.", - "Elle est compatible avec les versions 3.8, 3.9 et 3.10 de Python.", + "Elle est compatible avec les versions 3.9, 3.10 et 3.12 de Python.", "", - "**Django Version : 3.2 LTS**", + "**Django Version : 4.2 LTS**", "", - "> La documentation complĂšte du framework : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/)
", - "> L’ensemble des variables de configuration du framework est accessible à cette adresse : [docs.djangoproject.com](https://docs.djangoproject.com/fr/3.2/ref/settings/)", + "> La documentation complùte du framework : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/)
", + "> L’ensemble des variables de configuration du framework est accessible Ă  cette adresse : [docs.djangoproject.com](https://docs.djangoproject.com/fr/4.2/ref/settings/)", "", "Voici les configurations des applications tierces utilisĂ©es par Esup-Pod." ] }, "settings": { "CAS": { - "default_value": "1.5.2", + "default_value": "1.5.3", "description": { "en": "", "fr": [ @@ -5599,7 +5599,7 @@ "pod_version_init": "3.1" }, "ModelTranslation": { - "default_value": "0.18.7", + "default_value": "0.19.11", "description": { "en": "", "fr": [ @@ -5612,7 +5612,7 @@ "pod_version_init": "3.1" }, "captcha": { - "default_value": "0.5.17", + "default_value": "0.6.0", "description": { "en": "", "fr": [ @@ -5638,13 +5638,19 @@ "ckeditor": { "default_value": "6.3.0", "description": { - "en": "", + "en": [ + "WARNING (ckeditor.W001) django-ckeditor bundles CKEditor 4.22.1 free version", + "which isn’t supported anmyore and which does have unfixed security issues,", + "see for example https://ckeditor.com/cke4/release/CKEditor-4.24.0-LTS .", + "You should consider strongly switching to a different editor." + ], "fr": [ - "Application permettant d’ajouter un Ă©diteur CKEditor dans certains champs", - "[django-ckeditor.readthedocs.io](https://django-ckeditor.readthedocs.io/en/latest/#installation)" + "ATTENTION. django-ckeditor integre la version gratuite de CKEditor 4.22.1,", + "qui n'est plus prise en charge et qui prĂ©sente des problĂšmes de sĂ©curitĂ© non rĂ©solus,", + "voir par exemple https://ckeditor.com/cke4/release/CKEditor-4.24.0-LTS." ] }, - "pod_version_end": "", + "pod_version_end": "4.0.0", "pod_version_init": "3.1" }, "django_select2": { @@ -5660,7 +5666,7 @@ "pod_version_init": "3.1" }, "honeypot": { - "default_value": "1.0.3", + "default_value": "1.2.1", "description": { "en": "", "fr": [ @@ -5673,7 +5679,7 @@ "pod_version_init": "3.1" }, "mozilla_django_oidc": { - "default_value": "3.0.0", + "default_value": "4.0.1", "description": { "en": "", "fr": [ @@ -5685,7 +5691,7 @@ "pod_version_init": "3.1" }, "pwa": { - "default_value": "1.1.0", + "default_value": "2.0.1", "description": { "en": "", "fr": [ @@ -5717,11 +5723,11 @@ "pod_version_init": "3.4" }, "rest_framework": { - "default_value": "3.14.0", + "default_value": "3.15.2", "description": { "en": "", "fr": [ - "version 3.14.0 : mise en place de l’API rest pour l’application", + "mise en place de l’API rest pour l’application", "[django-rest-framework.org](https://www.django-rest-framework.org/)" ] }, @@ -5741,7 +5747,7 @@ "pod_version_init": "3.1" }, "sorl.thumbnail": { - "default_value": "12.9.0", + "default_value": "12.11.0", "description": { "en": "", "fr": [ @@ -5761,13 +5767,28 @@ "[django-tagging.readthedocs.io](https://django-tagging.readthedocs.io/en/develop/#settings)" ] }, - "pod_version_end": "", + "pod_version_end": "4.0.0", "pod_version_init": "3.1" + }, + "tagulous": { + "default_value": "2.1.0", + "description": { + "en": [ + "Managing keywords associated with a Django object. ", + "[django-tagulous.readthedocs.io](https://django-tagulous.readthedocs.io)" + ], + "fr": [ + "Gestion des mots-clĂ©s associĂ©s Ă  un objet Django. ", + "[django-tagulous.readthedocs.io](https://django-tagulous.readthedocs.io)" + ] + }, + "pod_version_end": "", + "pod_version_init": "4.0.0" } }, "title": { - "en": "", - "fr": "Information gĂ©nĂ©rale" + "en": "General information", + "fr": "Informations gĂ©nĂ©rales" } } } diff --git a/pod/main/context_processors.py b/pod/main/context_processors.py index eea1502933..782756c757 100644 --- a/pod/main/context_processors.py +++ b/pod/main/context_processors.py @@ -6,7 +6,7 @@ from pod.main.models import Configuration from django.contrib.sites.shortcuts import get_current_site -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ MENUBAR_HIDE_INACTIVE_OWNERS = getattr( django_settings, "MENUBAR_HIDE_INACTIVE_OWNERS", False @@ -17,10 +17,10 @@ USE_PODFILE = getattr(django_settings, "USE_PODFILE", False) -DARKMODE_ENABLED = getattr(django_settings, "DARKMODE_ENABLED", False) -DYSLEXIAMODE_ENABLED = getattr(django_settings, "DYSLEXIAMODE_ENABLED", False) +DARKMODE_ENABLED = getattr(django_settings, "DARKMODE_ENABLED", True) +DYSLEXIAMODE_ENABLED = getattr(django_settings, "DYSLEXIAMODE_ENABLED", True) -VERSION = getattr(django_settings, "VERSION", "3.X") +VERSION = getattr(django_settings, "VERSION", "4.X") ## # Settings exposed in templates # @@ -79,7 +79,7 @@ RESTRICT_EDIT_MEETING_ACCESS_TO_STAFF_ONLY = getattr( django_settings, "RESTRICT_EDIT_MEETING_ACCESS_TO_STAFF_ONLY", False ) -USE_NOTIFICATIONS = getattr(django_settings, "USE_NOTIFICATIONS", True) +USE_NOTIFICATIONS = getattr(django_settings, "USE_NOTIFICATIONS", False) WEBTV_MODE = getattr(django_settings, "WEBTV_MODE", False) diff --git a/pod/main/forms.py b/pod/main/forms.py index 721dd949a0..dddd91e4e7 100644 --- a/pod/main/forms.py +++ b/pod/main/forms.py @@ -1,7 +1,7 @@ """Esup-Pod forms handling.""" from django import forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.conf import settings from captcha.fields import CaptchaField from .forms_utils import add_placeholder_and_asterisk diff --git a/pod/main/forms_utils.py b/pod/main/forms_utils.py index 7528135408..622c8e7817 100644 --- a/pod/main/forms_utils.py +++ b/pod/main/forms_utils.py @@ -5,7 +5,7 @@ from django.contrib.admin import widgets from django.forms.utils import to_current_timezone from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ class MyAdminSplitDateTime(forms.MultiWidget): diff --git a/pod/main/lang_settings.py b/pod/main/lang_settings.py index 5adf466588..71f941a87b 100644 --- a/pod/main/lang_settings.py +++ b/pod/main/lang_settings.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ # http://www.w3schools.com/tags/ref_language_codes.asp # http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes diff --git a/pod/main/models.py b/pod/main/models.py index 8b89b88501..2e4cf4bc6a 100644 --- a/pod/main/models.py +++ b/pod/main/models.py @@ -2,7 +2,7 @@ from django.db import models from django.conf import settings -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.flatpages.models import FlatPage from django.contrib.sites.models import Site from django.core.exceptions import ValidationError diff --git a/pod/main/rest_router.py b/pod/main/rest_router.py index 5b99fa0864..2369477155 100644 --- a/pod/main/rest_router.py +++ b/pod/main/rest_router.py @@ -1,11 +1,11 @@ """Esup-Pod Main REST api url router.""" from rest_framework import routers -from django.conf.urls import url -from django.conf.urls import include +from django.urls import include, path from pod.authentication import rest_views as authentication_views from pod.video import rest_views as video_views -from pod.main import rest_views as main_views + +# from pod.main import rest_views as main_views from pod.authentication import rest_views as auth_views from pod.video_encode_transcript import rest_views as encode_views @@ -26,8 +26,8 @@ router = routers.DefaultRouter() -router.register(r"mainfiles", main_views.CustomFileModelViewSet) -router.register(r"mainimages", main_views.CustomImageModelViewSet) +# router.register(r"mainfiles", main_views.CustomFileModelViewSet) +# router.register(r"mainimages", main_views.CustomImageModelViewSet) router.register(r"users", authentication_views.UserViewSet) router.register(r"groups", authentication_views.GroupViewSet) @@ -76,44 +76,44 @@ router.register(r"meeting_live_gateway", meeting_views.LiveGatewayModelViewSet) urlpatterns = [ - url(r"dublincore/$", video_views.DublinCoreView.as_view(), name="dublincore"), - url( - r"^launch_encode_view/$", + path("dublincore/", video_views.DublinCoreView.as_view(), name="dublincore"), + path( + "launch_encode_view/", encode_views.launch_encode_view, name="launch_encode_view", ), - url( - r"store_remote_encoded_video/$", + path( + "store_remote_encoded_video/", encode_views.store_remote_encoded_video, name="store_remote_encoded_video", ), - url( - r"store_remote_encoded_video_studio/$", + path( + "store_remote_encoded_video_studio/", encode_views.store_remote_encoded_video_studio, name="store_remote_encoded_video_studio", ), - url( - r"store_remote_transcripted_video/$", + path( + "store_remote_transcripted_video/", encode_views.store_remote_transcripted_video, name="store_remote_transcripted_video", ), - url( - r"accessgroups_set_users_by_name/$", + path( + "accessgroups_set_users_by_name/", auth_views.accessgroups_set_users_by_name, name="accessgroups_set_users_by_name", ), - url( - r"accessgroups_remove_users_by_name/$", + path( + "accessgroups_remove_users_by_name/", auth_views.accessgroups_remove_users_by_name, name="accessgroups_set_users_by_name", ), - url( - r"accessgroups_set_user_accessgroup /$", + path( + "accessgroups_set_user_accessgroup/", auth_views.accessgroups_set_user_accessgroup, name="accessgroups_set_user_accessgroup ", ), - url( - r"accessgroups_remove_user_accessgroup /$", + path( + "accessgroups_remove_user_accessgroup/", auth_views.accessgroups_remove_user_accessgroup, name="accessgroups_remove_user_accessgroup ", ), @@ -121,8 +121,8 @@ USE_TRANSCRIPTION = getattr(settings, "USE_TRANSCRIPTION", False) if USE_TRANSCRIPTION: urlpatterns += [ - url( - r"launch_transcript_view/$", + path( + "launch_transcript_view/", encode_views.launch_transcript_view, name="launch_transcript_view", ), @@ -133,5 +133,5 @@ mod.add_register(router) urlpatterns += [ - url(r"^", include(router.urls)), + path("", include(router.urls)), ] diff --git a/pod/main/rest_views.py b/pod/main/rest_views.py index 3e232219ec..5fcda2ffea 100644 --- a/pod/main/rest_views.py +++ b/pod/main/rest_views.py @@ -1,5 +1,7 @@ from rest_framework import serializers, viewsets -from .models import CustomImageModel, CustomFileModel +from .models import CustomImageModel + +# from .models import CustomFileModel class CustomImageModelSerializer(serializers.HyperlinkedModelSerializer): @@ -8,10 +10,12 @@ class Meta: fields = ("id", "url", "file") +""" class CustomFileModelSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = CustomFileModel fields = ("id", "url", "file") +""" class CustomImageModelViewSet(viewsets.ModelViewSet): @@ -19,6 +23,8 @@ class CustomImageModelViewSet(viewsets.ModelViewSet): serializer_class = CustomImageModelSerializer +""" class CustomFileModelViewSet(viewsets.ModelViewSet): queryset = CustomFileModel.objects.all() serializer_class = CustomFileModelSerializer +""" diff --git a/pod/main/settings.py b/pod/main/settings.py index a48467d8a4..a00641b644 100644 --- a/pod/main/settings.py +++ b/pod/main/settings.py @@ -1,7 +1,7 @@ """ Django local settings for pod_project. -Django version: 3.2. +Django version: 4.2. """ import os @@ -18,7 +18,7 @@ # and should be set to a unique, unpredictable value. # # Django will not start if this is not set. -# https://docs.djangoproject.com/en/3.2/ref/settings/#secret-key +# https://docs.djangoproject.com/en/4.2/ref/settings/#secret-key # # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "A_CHANGER" @@ -31,7 +31,7 @@ ## # DEBUG mode activation # -# https://docs.djangoproject.com/en/3.2/ref/settings/#debug +# https://docs.djangoproject.com/en/4.2/ref/settings/#debug # # SECURITY WARNING: MUST be set to False when deploying into production. DEBUG = True @@ -47,14 +47,14 @@ # A list of strings representing the host/domain names # that this Django site is allowed to serve. # -# https://docs.djangoproject.com/en/3.2/ref/settings/#allowed-hosts +# https://docs.djangoproject.com/en/4.2/ref/settings/#allowed-hosts ALLOWED_HOSTS = ["pod.localhost"] ## # Session settings # -# https://docs.djangoproject.com/en/3.2/ref/settings/#session-cookie-age -# https://docs.djangoproject.com/en/3.2/ref/settings/#session-expire-at-browser-close +# https://docs.djangoproject.com/en/4.2/ref/settings/#session-cookie-age +# https://docs.djangoproject.com/en/4.2/ref/settings/#session-expire-at-browser-close # SESSION_COOKIE_AGE = 14400 SESSION_EXPIRE_AT_BROWSER_CLOSE = True @@ -66,20 +66,20 @@ # A tuple that lists people who get code error notifications # when DEBUG=False and a view raises an exception. # -# https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-ADMINS +# https://docs.djangoproject.com/en/4.2/ref/settings/#std:setting-ADMINS # ADMINS = [("Name", "adminmail@univ.fr")] ## # A tuple that lists people who get other notifications # email from contact_us / end of encoding / report video # -# https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-MANAGERS +# https://docs.djangoproject.com/en/4.2/ref/settings/#std:setting-MANAGERS MANAGERS = [] ## # A dictionary containing the settings for all databases # to be used with Django. # -# https://docs.djangoproject.com/en/3.2/ref/settings/#databases +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", @@ -90,7 +90,7 @@ ## # Internationalization and localization. # -# https://docs.djangoproject.com/en/3.2/topics/i18n/ +# https://docs.djangoproject.com/en/4.2/topics/i18n/ # https://github.com/django/django/blob/master/django/conf/global_settings.py LANGUAGE_CODE = "fr" LANGUAGES = (("fr", "Français"), ("en", "English")) @@ -107,7 +107,7 @@ # If None, the standard temporary directory for the operating system # will be used. # -# https://docs.djangoproject.com/en/3.2/ref/settings/#file-upload-temp-dir +# https://docs.djangoproject.com/en/4.2/ref/settings/#file-upload-temp-dir # FILE_UPLOAD_TEMP_DIR = os.path.join(os.path.sep, "var", "tmp") # https://github.com/ouhouhsami/django-progressbarupload @@ -120,15 +120,15 @@ ## # Static files (assets, CSS, JavaScript, fonts...) # -# https://docs.djangoproject.com/en/3.2/howto/static-files/ +# https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = "/static/" STATIC_ROOT = os.path.join(BASE_DIR, "static") ## # Dynamic files (user managed content: videos, subtitles, documents, etc...) # -# https://docs.djangoproject.com/en/3.2/ref/settings/#media-url -# https://docs.djangoproject.com/en/3.2/ref/settings/#media-root +# https://docs.djangoproject.com/en/4.2/ref/settings/#media-url +# https://docs.djangoproject.com/en/4.2/ref/settings/#media-root # # WARNING: this folder must have previously been created. MEDIA_URL = "/media/" @@ -203,9 +203,9 @@ ## # eMail settings # -# https://docs.djangoproject.com/en/3.2/ref/settings/#email-host -# https://docs.djangoproject.com/en/3.2/ref/settings/#email-port -# https://docs.djangoproject.com/en/3.2/ref/settings/#default-from-email +# https://docs.djangoproject.com/en/4.2/ref/settings/#email-host +# https://docs.djangoproject.com/en/4.2/ref/settings/#email-port +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-from-email # # username: EMAIL_HOST_USER # password: EMAIL_HOST_PASSWORD @@ -214,7 +214,7 @@ EMAIL_PORT = 25 DEFAULT_FROM_EMAIL = "noreply@univ.fr" -# https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-SERVER_EMAIL +# https://docs.djangoproject.com/en/4.2/ref/settings/#std:setting-SERVER_EMAIL SERVER_EMAIL = "noreply@univ.fr" ## @@ -236,7 +236,7 @@ THIRD_PARTY_APPS = [] ## -# https://docs.djangoproject.com/en/3.2/ref/clickjacking/ +# https://docs.djangoproject.com/en/4.2/ref/clickjacking/ # Add @xframe_options_exempt on a view you want to authorize in frame # X_FRAME_OPTIONS = "EXEMPT" # SAMEORIGIN OR DENY diff --git a/pod/main/static/css/pod.css b/pod/main/static/css/pod.css index c125a28371..87bbbac328 100755 --- a/pod/main/static/css/pod.css +++ b/pod/main/static/css/pod.css @@ -65,6 +65,11 @@ --pod-alert: #fc8670; --pod-alert-dark: #b11030; + /* Tag cloud colors */ + --pod-tag-color1: 16, 131, 22; + --pod-tag-color2: 51, 51, 170; + --pod-tag-color3: 197, 53, 143; + /**** font family ****/ /* For better accessibility, avoid fonts where [1, i, L] or [O, 0] are the same. */ @@ -162,6 +167,47 @@ tr, margin-right: 0.5em; } +/** TAGs Cloud */ +.tag-cloud { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-evenly; +} + +.tag-cloud > a { + line-height: 1.5; +} + +.tag-cloud > *:nth-child(2n + 1) { + --bs-link-color-rgb: var(--pod-tag-color1); +} +.tag-cloud > *:nth-child(3n + 1) { + --bs-link-color-rgb: var(--pod-tag-color2); +} +.tag-cloud > *:nth-child(4n + 1) { + --bs-link-color-rgb: var(--pod-tag-color3); +} + +.tag-1 { + font-size: 1em; +} +.tag-2 { + font-size: 1.1em; +} +.tag-3 { + font-size: 1.2em; +} +.tag-4 { + font-size: 1.3em; +} +.tag-5 { + font-size: 1.4em; +} +.tag-6 { + font-size: 1.5em; +} + .pod-card--video a:not(.btn) { color: inherit; } diff --git a/pod/main/static/js/main.js b/pod/main/static/js/main.js index bf8408ecf6..47ad0f2740 100644 --- a/pod/main/static/js/main.js +++ b/pod/main/static/js/main.js @@ -1420,9 +1420,12 @@ function remove_quotes(text) { } let mainCollapseButton = document.getElementById("collapse-button"); -mainCollapseButton.addEventListener("click", () => { - window.scrollTo(0, 0); -}); +// No collapse-button on admin pages +if (mainCollapseButton) { + mainCollapseButton.addEventListener("click", () => { + window.scrollTo(0, 0); + }); +} /** * Remove accents and convert to lowercase. diff --git a/pod/main/templates/aside.html b/pod/main/templates/aside.html index 59b8ed96f1..0831ef99ad 100644 --- a/pod/main/templates/aside.html +++ b/pod/main/templates/aside.html @@ -1,5 +1,4 @@ -{% load i18n %} -{% load video_tags %} +{% load i18n video_tags %} {% spaceless %} {% if HIDE_SHARE == False %} @@ -24,7 +23,7 @@

@@ -45,7 +44,7 @@

@@ -63,20 +62,17 @@

{% endif %} {% if HIDE_TAGS == False %} - {% tag_cloud_for_model video.Video as tagscloud with distribution=log steps=16 min_count=4 %} - {% if tagscloud|length > 0 %} + {% if TAGS|length > 0 %}

 {% trans 'Tags' %}

-

- {% with tagslist=tagscloud|dictsortreversed:"count"|slice:":20" %} - {% for tag in tagslist %} - - {{tag.name}} {{tag.count}} +

+ {% for tag in TAGS %} + + {{ tag.name }} {% endfor %} - {% endwith %}

{% endif %} diff --git a/pod/main/templates/block/card_list.html b/pod/main/templates/block/card_list.html index 1ddfa82932..37be2bcc2b 100644 --- a/pod/main/templates/block/card_list.html +++ b/pod/main/templates/block/card_list.html @@ -3,7 +3,7 @@

{{ title |capfirst|truncatechars:43 }}

- {% if type == "video" %} + {% if type == "video" %} {% include "videos/video_list.html" with videos=elements %} {% elif type == "event" %} {% include "live/events_list.html" with events=elements hide_counter=True %} diff --git a/pod/main/templates/block/multi_carousel.html b/pod/main/templates/block/multi_carousel.html index d4e1677333..288926774a 100644 --- a/pod/main/templates/block/multi_carousel.html +++ b/pod/main/templates/block/multi_carousel.html @@ -44,7 +44,9 @@

{{ element.description|metaformat|safe|striptags|truncatechars:250 }}

{% if type == 'video' %} - {{ element.duration_in_time }} + + {{ element.duration_in_time }} + {% endif %}

diff --git a/pod/main/templates/maintenance.html b/pod/main/templates/maintenance.html index 494de8917e..26d87d5a5d 100644 --- a/pod/main/templates/maintenance.html +++ b/pod/main/templates/maintenance.html @@ -1,10 +1,5 @@ {% extends 'base.html' %} -{% load i18n %} -{% load static %} -{% load tagging_tags %} -{% load thumbnail %} -{% load video_filters %} -{% load video_tags %} +{% load i18n static thumbnail video_filters video_tags %} {% block page_content %} {% if MAINTENANCE_MODE %} diff --git a/pod/main/templatetags/flat_page_edito_filter.py b/pod/main/templatetags/flat_page_edito_filter.py index 54377fb9ee..57d32bb2c6 100644 --- a/pod/main/templatetags/flat_page_edito_filter.py +++ b/pod/main/templatetags/flat_page_edito_filter.py @@ -14,7 +14,7 @@ from django.db.models import Q, Sum from django.template import loader from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.live.models import Event from pod.video.models import Video diff --git a/pod/main/test_settings.py b/pod/main/test_settings.py index 6911b38ad6..4b02437752 100644 --- a/pod/main/test_settings.py +++ b/pod/main/test_settings.py @@ -5,9 +5,11 @@ from ..settings import BASE_DIR as settings_base_dir from ..settings import USE_TZ, REST_FRAMEWORK, LOG_DIRECTORY, LOGGING from ..settings import LOCALE_PATHS, STATICFILES_DIRS, DEFAULT_AUTO_FIELD -from ..settings import AUTH_PASSWORD_VALIDATORS, USE_I18N, USE_L10N +from ..settings import AUTH_PASSWORD_VALIDATORS, USE_I18N from ..settings import ROOT_URLCONF, WSGI_APPLICATION, TEMPLATES from ..settings import INSTALLED_APPS, MIDDLEWARE, AUTHENTICATION_BACKENDS +from ..settings import SERIALIZATION_MODULES, TAGULOUS_NAME_MAX_LENGTH + import os from bs4 import BeautifulSoup import requests @@ -20,16 +22,15 @@ os.path.join(settings_base_dir, "custom", "static", "opencast") ) USE_DOCKER = True -path = "pod/custom/settings_local.py" ES_URL = ["http://elasticsearch.localhost:9200/"] -ES_VERSION = 6 +ES_VERSION = 8 +ES_INDEX = "pod" +path = "pod/custom/settings_local.py" if os.path.exists(path): _temp = __import__("pod.custom", globals(), locals(), ["settings_local"]) - USE_DOCKER = getattr(_temp.settings_local, "USE_DOCKER", True) - ES_URL = getattr( - _temp.settings_local, "ES_URL", ["http://elasticsearch.localhost:9200/"] - ) - ES_VERSION = getattr(_temp.settings_local, "ES_VERSION", 6) + USE_DOCKER = getattr(_temp.settings_local, "USE_DOCKER", USE_DOCKER) + ES_URL = getattr(_temp.settings_local, "ES_URL", ES_URL) + ES_VERSION = getattr(_temp.settings_local, "ES_VERSION", ES_VERSION) for application in INSTALLED_APPS: if application.startswith("pod"): @@ -45,7 +46,8 @@ "ENGINE": "django.db.backends.sqlite3", "NAME": "db-test.sqlite", "OPTIONS": { - "timeout": 30, + "timeout": 30.0, # in seconds + # see also https://docs.python.org/3.10/library/sqlite3.html#sqlite3.connect }, } } @@ -53,8 +55,10 @@ LANGUAGES = (("fr", "Français"), ("en", "English")) LANGUAGE_CODE = "en" THIRD_PARTY_APPS = ["enrichment", "live"] -USE_PODFILE = True +USE_CUT = True +USE_DRESSING = True USE_FAVORITES = True +USE_PODFILE = True USE_PLAYLIST = True USE_PROMOTED_PLAYLIST = True RESTRICT_PROMOTED_PLAYLIST_ACCESS_TO_STAFF_ONLY = False diff --git a/pod/main/tests/test_models.py b/pod/main/tests/test_models.py index f1964745c9..da430e02f1 100644 --- a/pod/main/tests/test_models.py +++ b/pod/main/tests/test_models.py @@ -39,7 +39,7 @@ def test_Flatpage_null_attribut(self): when a Flatpage has been saved with the minimum of attributes. """ flatPage = FlatPage.objects.get(url="/") - self.assertQuerysetEqual( + self.assertQuerySetEqual( flatPage.sites.all(), Site.objects.filter(id=SITE_ID), transform=lambda x: x, @@ -53,7 +53,7 @@ def test_Flatpage_null_attribut(self): def test_Flatpage_with_attributs(self): """Test attributs when a Flatpage have many attributs.""" flatPage = FlatPage.objects.get(url="/home/") - self.assertQuerysetEqual( + self.assertQuerySetEqual( flatPage.sites.all(), Site.objects.filter(id=SITE_ID), transform=lambda x: x, diff --git a/pod/main/tests/test_views.py b/pod/main/tests/test_views.py index af408b92ca..2d5cb02055 100644 --- a/pod/main/tests/test_views.py +++ b/pod/main/tests/test_views.py @@ -7,7 +7,7 @@ from django.test import TestCase from django.test import Client from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.flatpages.models import FlatPage from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied diff --git a/pod/main/views.py b/pod/main/views.py index 2d20faa11f..17c06c34ee 100644 --- a/pod/main/views.py +++ b/pod/main/views.py @@ -26,7 +26,7 @@ from django.views.decorators.http import require_GET from django.template import loader from django.core.mail import EmailMultiAlternatives -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.shortcuts import redirect from django.contrib.auth.models import User from django.core.exceptions import PermissionDenied, SuspiciousOperation @@ -42,6 +42,7 @@ import json from django.contrib.auth.decorators import login_required from .models import Configuration +from .utils import is_ajax from honeypot.decorators import check_honeypot @@ -230,7 +231,7 @@ def contact_us(request): prefix = "https://" if request.is_secure() else "http://" home_page = "".join([prefix, get_current_site(request).domain]) url_referrer = ( - request.META["HTTP_REFERER"] if request.META.get("HTTP_REFERER") else home_page + request.headers["referer"] if request.headers.get("referer") else home_page ) form = ContactUsForm( @@ -324,7 +325,7 @@ def contact_us(request): @login_required(redirect_field_name="referrer") def user_autocomplete(request): """Search for users with partial names, for autocompletion.""" - if request.is_ajax(): + if is_ajax(request): additional_filters = { "video__is_draft": False, "owner__sites": get_current_site(request), diff --git a/pod/meeting/admin.py b/pod/meeting/admin.py index 9462b55f69..c569d913d5 100644 --- a/pod/meeting/admin.py +++ b/pod/meeting/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.contrib.sites.shortcuts import get_current_site from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.utils.html import mark_safe from django.contrib.admin import widgets from django.utils.safestring import SafeText @@ -212,6 +212,7 @@ class MeetingSessionLogAdmin(admin.ModelAdmin): "creator", ] + @admin.display(description=_("Moderators")) def decrypt_mods_as_json(self, obj): """Decrypt moderators value to json and show it pretty.""" if not obj: @@ -219,9 +220,7 @@ def decrypt_mods_as_json(self, obj): moderators = "
{}
".format(obj.moderators.replace(" ", " ")) return SafeText(moderators) - decrypt_mods_as_json.short_description = _("Moderators") - decrypt_mods_as_json.allow_tags = True - + @admin.display(description=_("Viewers")) def decrypt_viewers_as_json(self, obj): """Decrypt viewers value to json and show it pretty.""" if not obj: @@ -229,9 +228,6 @@ def decrypt_viewers_as_json(self, obj): viewers = "
{}
".format(obj.viewers.replace(" ", " ")) return SafeText(viewers) - decrypt_viewers_as_json.short_description = _("Viewers") - decrypt_viewers_as_json.allow_tags = True - list_filter = ["creation_date"] readonly_fields = ( "meeting", diff --git a/pod/meeting/forms.py b/pod/meeting/forms.py index 70eac033d6..e95a7072b0 100644 --- a/pod/meeting/forms.py +++ b/pod/meeting/forms.py @@ -15,7 +15,7 @@ from django.db.models.query import QuerySet from django.forms import CharField, Textarea from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.main.forms_utils import add_placeholder_and_asterisk from pod.main.forms_utils import OwnerWidget, AddOwnerWidget from pod.meeting.webinar import start_webinar, stop_webinar, toggle_rtmp_gateway diff --git a/pod/meeting/models.py b/pod/meeting/models.py index 4fd683916c..6470911aea 100644 --- a/pod/meeting/models.py +++ b/pod/meeting/models.py @@ -15,7 +15,7 @@ from django.db import models from django.conf import settings from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.sites.models import Site from django.contrib.auth.models import User from django.db.models.signals import pre_save diff --git a/pod/meeting/static/css/meeting.css b/pod/meeting/static/css/meeting.css index 5b8e09fbfb..7862e70cca 100644 --- a/pod/meeting/static/css/meeting.css +++ b/pod/meeting/static/css/meeting.css @@ -67,12 +67,15 @@ i.bi.bi-exclamation-circle.me-2 { color: #c82630; } + i.bi.bi-exclamation-triangle.me-2 { color: #f9af2c; } + i.bi.bi-check-circle.me-2 { color: #00986a; } + i.bi.bi-info-circle.me-2 { color: #00b3c8; } diff --git a/pod/meeting/urls.py b/pod/meeting/urls.py index 10d588f687..0a275e1d1c 100644 --- a/pod/meeting/urls.py +++ b/pod/meeting/urls.py @@ -1,7 +1,6 @@ """URLs for Meeting module.""" -from django.conf.urls import url -from django.urls import path +from django.urls import path, re_path from . import views @@ -55,7 +54,7 @@ path( "//", views.join, name="join" ), - url( + re_path( r"^live_publish_chat/(?P[\d]+)/$", views.live_publish_chat, name="live_publish_chat", diff --git a/pod/meeting/utils.py b/pod/meeting/utils.py index 066ed6d027..38a794bc64 100644 --- a/pod/meeting/utils.py +++ b/pod/meeting/utils.py @@ -4,7 +4,7 @@ from datetime import date, timedelta from django.conf import settings from django.core.mail import EmailMultiAlternatives -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from hashlib import sha1 from pod.main.views import TEMPLATE_VISIBLE_SETTINGS @@ -19,7 +19,7 @@ SECURE_SSL_REDIRECT = getattr(settings, "SECURE_SSL_REDIRECT", False) -def api_call(query, call): +def api_call(query, call) -> str: """Generate checksum for BBB API call.""" checksum_val = sha1(str(call + query + BBB_SECRET_KEY).encode("utf-8")).hexdigest() result = "%s&checksum=%s" % (query, checksum_val) @@ -84,7 +84,7 @@ def get_nth_week_number(original_date): return nb_weeks -def send_email_recording_ready(meeting): +def send_email_recording_ready(meeting) -> None: """Send an email when a recording was saved and available on Pod.""" if DEBUG: print("SEND EMAIL WHEN RECORDING READY") diff --git a/pod/meeting/views.py b/pod/meeting/views.py index ba4523ec24..5f6c0c5780 100644 --- a/pod/meeting/views.py +++ b/pod/meeting/views.py @@ -42,7 +42,7 @@ from pod.import_video.utils import manage_download, parse_remote_file from pod.import_video.utils import save_video, secure_request_for_upload from pod.main.views import in_maintenance, TEMPLATE_VISIBLE_SETTINGS -from pod.main.utils import secure_post_request, display_message_with_icon +from pod.main.utils import secure_post_request, display_message_with_icon, is_ajax from pod.meeting.webinar import chat_rtmp_gateway, start_webinar, stop_webinar from pod.meeting.webinar_utils import search_for_available_livegateway, manage_webinar from pod.live.models import Event @@ -648,7 +648,7 @@ def end(request: WSGIRequest, meeting_id: str) -> HttpResponse: for key in args: msg += "%s: %s
" % (key, args[key]) msg = mark_safe(msg) - if request.is_ajax(): + if is_ajax(request): return JsonResponse({"end": meeting.end(), "msg": msg}, safe=False) else: if msg != "": @@ -687,7 +687,7 @@ def get_meeting_info(request: WSGIRequest, meeting_id: str) -> JsonResponse: for key in args: msg += "%s: %s
" % (key, args[key]) msg = mark_safe(msg) - if request.is_ajax(): + if is_ajax(request): return JsonResponse({"info": info, "msg": msg}, safe=False) else: if msg != "": @@ -823,7 +823,7 @@ def internal_recording( recordings = get_internal_recordings(request, meeting_id, recording_id) # JSON format data = recordings[0].to_json() - if request.is_ajax(): + if is_ajax(request): return HttpResponse(data, content_type="application/json") else: return HttpResponseBadRequest() diff --git a/pod/meeting/webinar.py b/pod/meeting/webinar.py index af3245ce45..92d7261cdf 100644 --- a/pod/meeting/webinar.py +++ b/pod/meeting/webinar.py @@ -9,7 +9,7 @@ from django.contrib import messages from django.core.handlers.wsgi import WSGIRequest from django.utils.html import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.main.utils import display_message_with_icon from pod.meeting.models import Meeting, Livestream from pod.meeting.utils import slash_join diff --git a/pod/meeting/webinar_utils.py b/pod/meeting/webinar_utils.py index fe2835f801..a29d664994 100644 --- a/pod/meeting/webinar_utils.py +++ b/pod/meeting/webinar_utils.py @@ -5,7 +5,7 @@ from django.core.handlers.wsgi import WSGIRequest from django.core.mail import mail_admins from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .models import Meeting, LiveGateway, Livestream from pod.live.models import Event from pod.main.views import TEMPLATE_VISIBLE_SETTINGS diff --git a/pod/playlist/context_processors.py b/pod/playlist/context_processors.py index c171ddd72a..60d1fe01e5 100644 --- a/pod/playlist/context_processors.py +++ b/pod/playlist/context_processors.py @@ -2,9 +2,9 @@ from django.conf import settings as django_settings -USE_PLAYLIST = getattr(django_settings, "USE_PLAYLIST", True) -USE_PROMOTED_PLAYLIST = getattr(django_settings, "USE_PROMOTED_PLAYLIST", True) -USE_FAVORITES = getattr(django_settings, "USE_FAVORITES", True) +USE_PLAYLIST = getattr(django_settings, "USE_PLAYLIST", False) +USE_PROMOTED_PLAYLIST = getattr(django_settings, "USE_PROMOTED_PLAYLIST", False) +USE_FAVORITES = getattr(django_settings, "USE_FAVORITES", False) DEFAULT_PLAYLIST_THUMBNAIL = getattr( django_settings, "DEFAULT_PLAYLIST_THUMBNAIL", diff --git a/pod/playlist/forms.py b/pod/playlist/forms.py index fdfd3690a4..81c4c8593f 100644 --- a/pod/playlist/forms.py +++ b/pod/playlist/forms.py @@ -4,7 +4,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.db.models.query import QuerySet -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.main.forms_utils import add_placeholder_and_asterisk from pod.meeting.forms import AddOwnerWidget @@ -12,7 +12,7 @@ from .apps import FAVORITE_PLAYLIST_NAME from .models import Playlist -USE_PROMOTED_PLAYLIST = getattr(settings, "USE_PROMOTED_PLAYLIST", True) +USE_PROMOTED_PLAYLIST = getattr(settings, "USE_PROMOTED_PLAYLIST", False) RESTRICT_PROMOTED_PLAYLIST_ACCESS_TO_STAFF_ONLY = getattr( settings, "RESTRICT_PROMOTED_PLAYLIST_ACCESS_TO_STAFF_ONLY", diff --git a/pod/playlist/models.py b/pod/playlist/models.py index c86fa6863a..9c5c5256d1 100644 --- a/pod/playlist/models.py +++ b/pod/playlist/models.py @@ -7,7 +7,7 @@ from django.db import models from django.db.models import Max from django.utils import timezone -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.template.defaultfilters import slugify from pod.main.models import get_nextautoincrement diff --git a/pod/playlist/signals.py b/pod/playlist/signals.py index 45074dba51..8d0b562dc2 100644 --- a/pod/playlist/signals.py +++ b/pod/playlist/signals.py @@ -3,7 +3,7 @@ from django.contrib.sites.models import Site from django.db.models.signals import m2m_changed from django.dispatch import receiver -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.authentication.models import Owner diff --git a/pod/playlist/templatetags/favorites_playlist.py b/pod/playlist/templatetags/favorites_playlist.py index 1be76a79b4..76878017ce 100644 --- a/pod/playlist/templatetags/favorites_playlist.py +++ b/pod/playlist/templatetags/favorites_playlist.py @@ -2,7 +2,7 @@ from django.template import Library from django.contrib.auth.models import User -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.video.models import Video diff --git a/pod/playlist/tests/test_forms.py b/pod/playlist/tests/test_forms.py index bd3d1834f6..3dbe235e05 100644 --- a/pod/playlist/tests/test_forms.py +++ b/pod/playlist/tests/test_forms.py @@ -1,6 +1,6 @@ """Tests the forms for playlist module.""" -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.test import override_settings, TestCase from ...playlist.forms import PlaylistForm, PlaylistRemoveForm, PlaylistPasswordForm diff --git a/pod/playlist/tests/test_models.py b/pod/playlist/tests/test_models.py index dc2af4d010..86b160574d 100644 --- a/pod/playlist/tests/test_models.py +++ b/pod/playlist/tests/test_models.py @@ -1,7 +1,7 @@ """Tests the models for playlist module.""" from django.contrib.auth.models import User -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.test import TestCase from pod.video.models import Type, Video diff --git a/pod/playlist/tests/test_views.py b/pod/playlist/tests/test_views.py index 698c6bfc45..31a297d191 100644 --- a/pod/playlist/tests/test_views.py +++ b/pod/playlist/tests/test_views.py @@ -7,7 +7,7 @@ from django.contrib.messages import get_messages from django.http import JsonResponse from django.urls import reverse -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.test import override_settings, TestCase from pod.main.models import Configuration @@ -759,7 +759,7 @@ def test_remove_video_in_playlist(self): "video_slug": self.video.slug, }, ) - response = self.client.get(url, HTTP_REFERER=url_content) + response = self.client.get(url, headers={"referer": url_content}) self.assertEqual(response.status_code, 302) redirected_url = response.url diff --git a/pod/playlist/views.py b/pod/playlist/views.py index a5e79cce6a..2c04357b87 100644 --- a/pod/playlist/views.py +++ b/pod/playlist/views.py @@ -7,13 +7,18 @@ from django.core.exceptions import PermissionDenied from django.core.handlers.wsgi import WSGIRequest from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.http import HttpResponseRedirect from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie -from django.http import Http404, HttpResponseBadRequest, JsonResponse +from django.http import ( + Http404, + HttpResponseBadRequest, + JsonResponse, + HttpResponseRedirect, +) +from django.utils.http import url_has_allowed_host_and_scheme from django.db import transaction from pod.main.utils import is_ajax @@ -44,7 +49,6 @@ import json import hashlib -from typing import List TEMPLATE_VISIBLE_SETTINGS = getattr( @@ -75,7 +79,7 @@ else "Pod" ) -USE_PROMOTED_PLAYLIST = getattr(settings, "USE_PROMOTED_PLAYLIST", True) +USE_PROMOTED_PLAYLIST = getattr(settings, "USE_PROMOTED_PLAYLIST", False) def playlist_list(request: WSGIRequest): @@ -147,7 +151,7 @@ def playlist_content(request: WSGIRequest, slug: str): def render_playlist_page( request: WSGIRequest, playlist: Playlist, - videos: List[Video], + videos: list[Video], in_favorites_playlist: bool, count_videos: int, sort_field: str, @@ -197,7 +201,7 @@ def render_playlist_page( def toggle_render_playlist_user_has_right( request: WSGIRequest, playlist: Playlist, - videos: List[Video], + videos: list[Video], in_favorites_playlist: bool, count_videos: int, sort_field: str, @@ -224,7 +228,11 @@ def toggle_render_playlist_user_has_right( messages.ERROR, _("The password is incorrect."), ) - return redirect(request.META["HTTP_REFERER"]) + referer = request.headers.get("referer", "/") + if url_has_allowed_host_and_scheme(referer, allowed_hosts=None): + return redirect(referer) + else: + return redirect("/") else: form = PlaylistPasswordForm() return render( @@ -346,7 +354,11 @@ def remove_video_in_playlist(request: WSGIRequest, slug: str, video_slug: str): "state": "out-playlist", } ) - return redirect(request.META["HTTP_REFERER"]) + referer = request.headers.get("referer", "/") + if url_has_allowed_host_and_scheme(referer, allowed_hosts=None): + return redirect(referer) + else: + return redirect("/") @login_required(redirect_field_name="referrer") @@ -361,7 +373,11 @@ def add_video_in_playlist(request: WSGIRequest, slug: str, video_slug: str): "state": "in-playlist", } ) - return redirect(request.META["HTTP_REFERER"]) + referer = request.headers.get("referer", "/") + if url_has_allowed_host_and_scheme(referer, allowed_hosts=None): + return redirect(referer) + else: + return redirect("/") @login_required(redirect_field_name="referrer") @@ -532,7 +548,11 @@ def favorites_save_reorganisation(request: WSGIRequest, slug: str): playlist_video_1.update(rank=video_2_rank) playlist_video_2.update(rank=video_1_rank) - return redirect(request.META["HTTP_REFERER"]) + referer = request.headers.get("referer", "/") + if url_has_allowed_host_and_scheme(referer, allowed_hosts=None): + return redirect(referer) + else: + return redirect("/") else: raise Http404() @@ -556,7 +576,11 @@ def start_playlist(request: WSGIRequest, slug: str, video: Video = None): messages.add_message( request, messages.ERROR, _("The password is incorrect.") ) - return redirect(request.META["HTTP_REFERER"]) + referer = request.headers.get("referer", "/") + if url_has_allowed_host_and_scheme(referer, allowed_hosts=None): + return redirect(referer) + else: + return redirect(reverse("playlist:list")) else: form = PlaylistPasswordForm() return render( diff --git a/pod/podfile/admin.py b/pod/podfile/admin.py index 361c880d17..f8cb8fe464 100644 --- a/pod/podfile/admin.py +++ b/pod/podfile/admin.py @@ -10,6 +10,7 @@ # Register your models here. +@admin.register(UserFolder) class UserFolderAdmin(admin.ModelAdmin): list_display = ( "name", @@ -45,9 +46,7 @@ def get_queryset(self, request): return qs -admin.site.register(UserFolder, UserFolderAdmin) - - +@admin.register(CustomImageModel) class CustomImageModelAdmin(admin.ModelAdmin): list_display = ( "name", @@ -85,9 +84,7 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, **kwargs) -admin.site.register(CustomImageModel, CustomImageModelAdmin) - - +@admin.register(CustomFileModel) class CustomFileModelAdmin(admin.ModelAdmin): list_display = ( "name", @@ -123,6 +120,3 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): owner__sites=Site.objects.get_current() ) return super().formfield_for_foreignkey(db_field, request, **kwargs) - - -admin.site.register(CustomFileModel, CustomFileModelAdmin) diff --git a/pod/podfile/forms.py b/pod/podfile/forms.py index d14f0db972..c130e8e194 100644 --- a/pod/podfile/forms.py +++ b/pod/podfile/forms.py @@ -4,7 +4,7 @@ from django.conf import settings from django.core.validators import FileExtensionValidator from django.utils.deconstruct import deconstructible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.template.defaultfilters import filesizeformat from django.core.exceptions import ValidationError diff --git a/pod/podfile/models.py b/pod/podfile/models.py index f850d6926c..9ab29948b3 100644 --- a/pod/podfile/models.py +++ b/pod/podfile/models.py @@ -1,7 +1,7 @@ """Esup-Pod Podfile models.""" from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.conf import settings diff --git a/pod/podfile/tests/test_views.py b/pod/podfile/tests/test_views.py index cc16b984a1..8ca82ea3e9 100644 --- a/pod/podfile/tests/test_views.py +++ b/pod/podfile/tests/test_views.py @@ -3,7 +3,7 @@ from django.test import TestCase from django.contrib.auth.models import User from django.core.files.uploadedfile import SimpleUploadedFile -from django.utils.encoding import force_text +from django.utils.encoding import force_str from django.urls import reverse from django.test import Client @@ -267,12 +267,12 @@ def test_edit_files(self) -> None: "folderid": folder.id, }, follow=True, - HTTP_X_REQUESTED_WITH="XMLHttpRequest", + headers={"x-requested-with": "XMLHttpRequest"}, ) self.assertEqual(response.status_code, 200) # ajax with post data - result = json.loads(force_text(response.content)) + result = json.loads(force_str(response.content)) self.assertTrue(result["list_element"]) self.assertEqual(folder.customfilemodel_set.all().count(), nbfile + 1) @@ -295,6 +295,6 @@ def test_edit_files(self) -> None: "folderid": 999, }, follow=True, - HTTP_X_REQUESTED_WITH="XMLHttpRequest", + headers={"x-requested-with": "XMLHttpRequest"}, ) self.assertEqual(response.status_code, 404) # folder not exist diff --git a/pod/podfile/urls.py b/pod/podfile/urls.py index 2053af91a4..2f306b375a 100644 --- a/pod/podfile/urls.py +++ b/pod/podfile/urls.py @@ -1,6 +1,6 @@ """Esup-Pod podfile URL Configuration.""" -from django.conf.urls import url +from django.urls import path, re_path from .views import home, get_folder_files, get_file from .views import editfolder, deletefolder @@ -16,28 +16,28 @@ app_name = "podfile" urlpatterns = [ - url(r"^$", home, name="home"), - url(r"^(?P[\-\d\w]+)$", home, name="home"), - url( + path("", home, name="home"), + re_path(r"^(?P[\-\d\w]+)$", home, name="home"), + re_path( r"^get_folder_files/(?P[\d]+)/$", get_folder_files, name="get_folder_files", ), - url( + re_path( r"^get_folder_files/(?P[\d]+)/(?P[\-\d\w]+)/$", get_folder_files, name="get_folder_files", ), - url(r"^get_file/(?P[\-\d\w]+)/$", get_file, name="get_file"), - url(r"^editfolder/$", editfolder, name="editfolder"), - url(r"^deletefolder/$", deletefolder, name="deletefolder"), - url(r"^deletefile/$", deletefile, name="deletefile"), - url(r"^changefile/$", changefile, name="changefile"), - url(r"^uploadfiles/$", uploadfiles, name="uploadfiles"), - url(r"^ajax_calls/search_share_user/", user_share_autocomplete), - url(r"^ajax_calls/folder_shared_with/", folder_shared_with), - url(r"^ajax_calls/remove_shared_user/", remove_shared_user), - url(r"^ajax_calls/add_shared_user/", add_shared_user), - url(r"^ajax_calls/user_folders/", user_folders), - url(r"^ajax_calls/current_session_folder/", get_current_session_folder_ajax), + re_path(r"^get_file/(?P[\-\d\w]+)/$", get_file, name="get_file"), + path("editfolder/", editfolder, name="editfolder"), + path("deletefolder/", deletefolder, name="deletefolder"), + path("deletefile/", deletefile, name="deletefile"), + path("changefile/", changefile, name="changefile"), + path("uploadfiles/", uploadfiles, name="uploadfiles"), + re_path(r"^ajax_calls/search_share_user/", user_share_autocomplete), + re_path(r"^ajax_calls/folder_shared_with/", folder_shared_with), + re_path(r"^ajax_calls/remove_shared_user/", remove_shared_user), + re_path(r"^ajax_calls/add_shared_user/", add_shared_user), + re_path(r"^ajax_calls/user_folders/", user_folders), + re_path(r"^ajax_calls/current_session_folder/", get_current_session_folder_ajax), ] diff --git a/pod/podfile/views.py b/pod/podfile/views.py index 9d3e688ed0..4d4fb4c51f 100644 --- a/pod/podfile/views.py +++ b/pod/podfile/views.py @@ -6,7 +6,7 @@ from django.views.decorators.csrf import csrf_protect from django.contrib import messages from django.shortcuts import get_object_or_404 -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.core.exceptions import PermissionDenied from django.template.loader import render_to_string from django.contrib.admin.views.decorators import staff_member_required diff --git a/pod/progressive_web_app/utils.py b/pod/progressive_web_app/utils.py index d8b9d14404..e0ff6cb516 100644 --- a/pod/progressive_web_app/utils.py +++ b/pod/progressive_web_app/utils.py @@ -7,7 +7,7 @@ DEFAULT_ICON = static("img/icon_x1024.png") -def notify_user(user, title, message, url=None, icon=None): +def notify_user(user, title, message, url=None, icon=None) -> None: """Fill the payload to send a webpush notification to users devices.""" payload = { "head": title, diff --git a/pod/quiz/context_processors.py b/pod/quiz/context_processors.py index 303f5fed8a..a268af14b5 100644 --- a/pod/quiz/context_processors.py +++ b/pod/quiz/context_processors.py @@ -2,7 +2,7 @@ from django.conf import settings as django_settings -USE_QUIZ = getattr(django_settings, "USE_QUIZ", True) +USE_QUIZ = getattr(django_settings, "USE_QUIZ", False) def context_settings(request): diff --git a/pod/quiz/models.py b/pod/quiz/models.py index 02b276ce65..b634cc83d4 100644 --- a/pod/quiz/models.py +++ b/pod/quiz/models.py @@ -4,7 +4,7 @@ from json import JSONDecodeError, loads from django.core.exceptions import ValidationError from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from pod.video.models import Video diff --git a/pod/quiz/tests/test_models.py b/pod/quiz/tests/test_models.py index c488069c17..8f6c529f27 100644 --- a/pod/quiz/tests/test_models.py +++ b/pod/quiz/tests/test_models.py @@ -6,7 +6,7 @@ from unittest.mock import patch from django.contrib.auth.models import User from django.forms import ValidationError -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from django.test import TestCase from pod.quiz.forms import ( MultipleChoiceQuestionForm, diff --git a/pod/quiz/tests/test_views.py b/pod/quiz/tests/test_views.py index 7d7ed5ce88..4d72b1caf0 100644 --- a/pod/quiz/tests/test_views.py +++ b/pod/quiz/tests/test_views.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import User from django.urls import reverse from django.contrib.messages import get_messages -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from pod.main.models import Configuration from pod.quiz.forms import QuizDeleteForm, QuizForm diff --git a/pod/quiz/utils.py b/pod/quiz/utils.py index 529b6936e2..bedee8a819 100644 --- a/pod/quiz/utils.py +++ b/pod/quiz/utils.py @@ -1,7 +1,6 @@ """Esup-Pod quiz utilities.""" import ast -from typing import Optional from pod.quiz.models import ( MultipleChoiceQuestion, Question, @@ -42,7 +41,7 @@ def get_quiz_questions( ) -def get_video_quiz(video: Video) -> Optional[Quiz]: +def get_video_quiz(video: Video): """ Retrieve the quiz associated with a given video. @@ -50,7 +49,7 @@ def get_video_quiz(video: Video) -> Optional[Quiz]: video (Video): The video for which to retrieve the associated quiz. Returns: - Optional[Quiz]: The quiz associated with the video, or None if no quiz is found. + [Quiz | None]: The quiz associated with the video, or None if no quiz is found. """ return Quiz.objects.filter(video=video).first() diff --git a/pod/quiz/views.py b/pod/quiz/views.py index c4d6a2b65b..ef9476004b 100644 --- a/pod/quiz/views.py +++ b/pod/quiz/views.py @@ -2,7 +2,6 @@ import ast import json -from typing import Optional from django.forms import formset_factory from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render @@ -228,9 +227,7 @@ def get_question(question_type: str, question_id: int, quiz: Quiz): return question -def create_or_edit_quiz_instance( - video: Video, quiz_form: QuizForm, action: str -) -> Optional[Quiz]: +def create_or_edit_quiz_instance(video: Video, quiz_form: QuizForm, action: str): """ Create a new quiz instance or update an existing one based on the provided action. @@ -240,7 +237,7 @@ def create_or_edit_quiz_instance( action (str): The action to perform - "create" or "edit". Returns: - Optional[Quiz]: The created or updated quiz instance, or None if the action is invalid. + [Quiz | None]: The created or updated quiz instance, or None if the action is invalid. """ if action == "create": return Quiz.objects.create( diff --git a/pod/recorder/__init__.py b/pod/recorder/__init__.py index 7fb929cf74..e69de29bb2 100644 --- a/pod/recorder/__init__.py +++ b/pod/recorder/__init__.py @@ -1 +0,0 @@ -default_app_config = "pod.recorder.apps.RecorderConfig" diff --git a/pod/recorder/admin.py b/pod/recorder/admin.py index c83848d345..33572e34df 100644 --- a/pod/recorder/admin.py +++ b/pod/recorder/admin.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib import admin from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from .models import Recording, Recorder, RecordingFile from .models import RecordingFileTreatment from django.contrib.sites.shortcuts import get_current_site @@ -17,6 +17,7 @@ RECORDER_ADDITIONAL_FIELDS = getattr(settings, "RECORDER_ADDITIONAL_FIELDS", ()) +@admin.register(Recording) class RecordingAdmin(admin.ModelAdmin): list_display = ("title", "user", "source_file", "date_added") list_display_links = ("title",) @@ -39,21 +40,21 @@ def get_queryset(self, request): return qs +@admin.register(RecordingFileTreatment) class RecordingFileTreatmentAdmin(admin.ModelAdmin): list_display = ("id", "file") actions = ["delete_source"] autocomplete_fields = ["recorder"] + @admin.action( + description=_("Delete selected Recording file treatments + source files") + ) def delete_source(self, request, queryset) -> None: for item in queryset: if os.path.exists(item.file): os.remove(item.file) item.delete() - delete_source.short_description = _( - "Delete selected Recording file treatments + source files" - ) - def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "recorder": kwargs["queryset"] = Recorder.objects.filter(sites=Site.objects.get_current()) @@ -66,6 +67,7 @@ def get_queryset(self, request): return qs +@admin.register(Recorder) class RecorderAdmin(admin.ModelAdmin): search_fields = ["name"] autocomplete_fields = [ @@ -150,6 +152,7 @@ def save_model(self, request, obj, form, change) -> None: readonly_fields = [] +@admin.register(RecordingFile) class RecordingFileAdmin(admin.ModelAdmin): list_display = ("id", "file", "recorder") autocomplete_fields = ["recorder"] @@ -164,9 +167,3 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "recorder": kwargs["queryset"] = Recorder.objects.filter(sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) - - -admin.site.register(Recording, RecordingAdmin) -admin.site.register(RecordingFile, RecordingFileAdmin) -admin.site.register(RecordingFileTreatment, RecordingFileTreatmentAdmin) -admin.site.register(Recorder, RecorderAdmin) diff --git a/pod/recorder/forms.py b/pod/recorder/forms.py index e4dc8f91e7..b9e538315f 100644 --- a/pod/recorder/forms.py +++ b/pod/recorder/forms.py @@ -1,7 +1,7 @@ from django import forms from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.sites.models import Site from .models import Recording, Recorder from pod.main.forms_utils import add_placeholder_and_asterisk diff --git a/pod/recorder/models.py b/pod/recorder/models.py index a4dd9b8f99..979510f812 100644 --- a/pod/recorder/models.py +++ b/pod/recorder/models.py @@ -6,7 +6,7 @@ from ckeditor.fields import RichTextField from django.db import models from django.conf import settings -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User from django.contrib.auth.models import Group from django.dispatch import receiver @@ -23,7 +23,7 @@ from pod.video.models import CURSUS_CODES as __CURSUS_CODES__ from pod.video.models import LICENCE_CHOICES as __LICENCE_CHOICES__ from pod.video.models import RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY as __REVATSO__ -from tagging.fields import TagField +from tagulous.models import TagField from django.utils.translation import get_language LANG_CHOICES = getattr( @@ -295,6 +295,10 @@ def clean(self) -> None: cred_error["salt"] = cred_msg raise ValidationError(cred_error) + def get_tag_list(self) -> str: + """Return a list of comma separated tag names.""" + return ", ".join(tag.name for tag in self.tags.all()) + class Meta: verbose_name = _("Recorder") verbose_name_plural = _("Recorders") diff --git a/pod/recorder/plugins/type_audiovideocast.py b/pod/recorder/plugins/type_audiovideocast.py index 3d45a4fd23..5c7b4eea60 100644 --- a/pod/recorder/plugins/type_audiovideocast.py +++ b/pod/recorder/plugins/type_audiovideocast.py @@ -74,7 +74,7 @@ def save_video(recording, video_data, video_src) -> Video: # Choix des cursus video.cursus = recorder.cursus # mot clefs - video.tags = recorder.tags + video.tags = recorder.tags.get_tag_list() # transcript if getattr(settings, "USE_TRANSCRIPTION", False): video.transcript = recorder.transcript diff --git a/pod/recorder/plugins/type_studio.py b/pod/recorder/plugins/type_studio.py index 9d4bfb7bd3..93c9906848 100644 --- a/pod/recorder/plugins/type_studio.py +++ b/pod/recorder/plugins/type_studio.py @@ -76,7 +76,7 @@ def save_basic_video(recording, video_src) -> Video: # Cursus video.cursus = recorder.cursus # Tags - video.tags = recorder.tags + video.tags = recorder.tags.get_tag_list() # Transcription if getattr(settings, "USE_TRANSCRIPTION", False): video.transcript = recorder.transcript diff --git a/pod/recorder/plugins/type_video.py b/pod/recorder/plugins/type_video.py index 3fdd941d46..6bacccca7d 100644 --- a/pod/recorder/plugins/type_video.py +++ b/pod/recorder/plugins/type_video.py @@ -21,7 +21,7 @@ def process(recording): t.start() -def encode_recording(recording): +def encode_recording(recording) -> None: recorder = recording.recorder video = Video() video.title = recording.title @@ -58,7 +58,7 @@ def encode_recording(recording): # Choix des cursus video.cursus = recorder.cursus # mot clefs - video.tags = recorder.tags + video.tags = recorder.tags.get_tag_list() # transcript if getattr(settings, "USE_TRANSCRIPTION", False): video.transcript = recorder.transcript diff --git a/pod/recorder/studio_urls.py b/pod/recorder/studio_urls.py index f0447a44b6..500ec9262b 100644 --- a/pod/recorder/studio_urls.py +++ b/pod/recorder/studio_urls.py @@ -1,6 +1,6 @@ """Opencast Studio urls for Esup-Pod Integration.""" -from django.conf.urls import url +from django.urls import path, re_path from .views import studio_pod, studio_static, studio_root_file from .views import ingest_createMediaPackage, ingest_addDCCatalog from .views import ingest_addAttachment, ingest_addTrack @@ -9,63 +9,63 @@ app_name = "recorder" urlpatterns = [ - url( - r"^$", + path( + "", studio_pod, name="studio_pod", ), - url( - r"^presenter_post$", + path( + "presenter_post", presenter_post, name="presenter_post", ), - url( + re_path( r"^settings.toml$", settings_toml, name="settings_toml", ), - url( + re_path( r"^info/me.json$", info_me_json, name="info_me_json", ), - url( + re_path( r"^static/(?P.*)$", studio_static, name="studio_static", ), - url( + re_path( r"^(?P[a-zA-Z0-9\.]*)$", studio_root_file, name="studio_root_file", ), - url( - r"^ingest/createMediaPackage$", + path( + "ingest/createMediaPackage", ingest_createMediaPackage, name="ingest_createMediaPackage", ), - url( - r"^ingest/addDCCatalog$", + path( + "ingest/addDCCatalog", ingest_addDCCatalog, name="ingest_addDCCatalog", ), - url( - r"^ingest/addAttachment$", + path( + "ingest/addAttachment", ingest_addAttachment, name="ingest_addAttachment", ), - url( - r"^ingest/addTrack$", + path( + "ingest/addTrack", ingest_addTrack, name="ingest_addTrack", ), - url( - r"^ingest/addCatalog$", + path( + "ingest/addCatalog", ingest_addCatalog, name="ingest_addCatalog", ), - url( - r"^ingest/ingest$", + path( + "ingest/ingest", ingest_ingest, name="ingest_ingest", ), diff --git a/pod/recorder/studio_urls_digest.py b/pod/recorder/studio_urls_digest.py index 04b10c993a..5fa094493b 100644 --- a/pod/recorder/studio_urls_digest.py +++ b/pod/recorder/studio_urls_digest.py @@ -1,4 +1,6 @@ -from django.conf.urls import url +"""Esup-Pod Studio Recorder urls digest.""" + +from django.urls import path, re_path from pod.recorder.views import ( digest_admin_ng_series, @@ -20,78 +22,78 @@ app_name = "recorder_digest" urlpatterns = [ - url( + re_path( r"^services/hosts.json$", digest_hosts_json, name="hosts_json", ), - url( - r"^capture-admin/agents/(?P.+)/configuration$", + path( + "capture-admin/agents//configuration", digest_capture_admin_configuration, name="capture_admin_config", ), - url( + re_path( r"^capture-admin/agents/(?P.*)$", digest_capture_admin, name="capture_admin_agent", ), - url( + re_path( r"^admin-ng/series/series.json$", digest_admin_ng_series, name="admin_ng_series", ), - url( + re_path( r"^services/available.json$", digest_available, name="services_available", ), - url( - r"^presenter_post$", + path( + "presenter_post", digest_presenter_post, name="presenter_post", ), - url( + re_path( r"^settings.toml$", digest_settings_toml, name="settings_toml", ), - url( + re_path( r"^info/me.json$", digest_info_me_json, name="info_me_json", ), - url( + re_path( r"^static/(?P.*)$", digest_studio_static, name="studio_static", ), - url( - r"^ingest/createMediaPackage$", + path( + "ingest/createMediaPackage", digest_ingest_createMediaPackage, name="ingest_createMediaPackage", ), - url( - r"^ingest/addDCCatalog$", + path( + "ingest/addDCCatalog", digest_ingest_addDCCatalog, name="ingest_addDCCatalog", ), - url( - r"^ingest/addAttachment$", + path( + "ingest/addAttachment", digest_ingest_addAttachment, name="ingest_addAttachment", ), - url( - r"^ingest/addTrack$", + path( + "ingest/addTrack", digest_ingest_addTrack, name="ingest_addTrack", ), - url( - r"^ingest/addCatalog$", + path( + "ingest/addCatalog", digest_ingest_addCatalog, name="ingest_addCatalog", ), - url( - r"^ingest/ingest$", + path( + "ingest/ingest", digest_ingest_ingest, name="ingest_ingest", ), diff --git a/pod/recorder/tests/test_plugins.py b/pod/recorder/tests/test_plugins.py index 892cc88401..fe437575a4 100644 --- a/pod/recorder/tests/test_plugins.py +++ b/pod/recorder/tests/test_plugins.py @@ -1,4 +1,7 @@ -"""Unit test for Pod recorder plugins.""" +"""Unit test for Pod recorder plugins. + +* run with 'python manage.py test pod.recorder.tests.test_plugins' +""" import os import shutil @@ -27,7 +30,7 @@ class PluginVideoTestCase(TestCase): "initial_data.json", ] - def setUp(self): + def setUp(self) -> None: mediatype = Type.objects.create(title="others") Type.objects.create(title="second") user = User.objects.create(username="pod", is_staff=True) @@ -72,7 +75,7 @@ def setUp(self): print(" ---> SetUp of PluginVideoTestCase: OK!") - def test_type_video_published_attributs(self): + def test_type_video_published_attributs(self) -> None: recording = Recording.objects.get(id=1) recorder = recording.recorder shutil.copyfile(VIDEO_TEST, recording.source_file) @@ -95,7 +98,7 @@ def test_type_video_published_attributs(self): print(" ---> test_type_video_published_attributs of PluginVideoTestCase: OK!") - def test_type_audiovideocast_published_attributs(self): + def test_type_audiovideocast_published_attributs(self) -> None: recording = Recording.objects.get(id=2) recorder = recording.recorder shutil.copyfile(AUDIOVIDEOCAST_TEST, recording.source_file) @@ -123,33 +126,33 @@ def test_type_audiovideocast_published_attributs(self): "of PluginAudioVideoCastTestCase: OK!" ) - def test_change_title(self): + def test_change_title(self) -> None: """Test method change_title.""" from ..plugins.type_studio import change_title recording = Recording.objects.get(id=1) title = "A new title" - self.assertNotEquals(recording.title, title) + self.assertNotEqual(recording.title, title) change_title(recording, title) recording = Recording.objects.get(id=1) - self.assertEquals(recording.title, title) + self.assertEqual(recording.title, title) print(" ---> test_change_title of PluginVideoTestCase: OK!") - def test_change_user(self): + def test_change_user(self) -> None: """Test method change_user.""" from ..plugins.type_studio import change_user recording = Recording.objects.get(id=1) user2 = User.objects.create(username="another_user", is_staff=True) - self.assertNotEquals(recording.user, user2) + self.assertNotEqual(recording.user, user2) change_user(recording, user2.username) recording = Recording.objects.get(id=1) - self.assertEquals(recording.user, user2) + self.assertEqual(recording.user, user2) print(" ---> test_change_user of PluginVideoTestCase: OK!") - def test_link_video_to_event(self): + def test_link_video_to_event(self) -> None: """Test method link_video_to_event.""" from ..plugins.type_studio import link_video_to_event @@ -224,7 +227,7 @@ def test_link_video_to_event(self): print(" ---> test_link_video_to_event of PluginVideoTestCase: OK!") - def test_get_attribute_by_name(self): + def test_get_attribute_by_name(self) -> None: """Test method getAttributeByName.""" from ..plugins.type_studio import getAttributeByName @@ -254,7 +257,7 @@ def test_get_attribute_by_name(self): self.assertIsNone(non_existing_attribute_2) print(" ---> test_get_attribute_by_name of PluginVideoTestCase: OK!") - def test_get_elements_by_name(self): + def test_get_elements_by_name(self) -> None: """Test method getElementsByName.""" from ..plugins.type_studio import getElementsByName @@ -303,7 +306,7 @@ def test_get_elements_by_name(self): self.assertEqual(elements, expected_elements) print(" ---> test_get_elements_by_name of PluginVideoTestCase: OK!") - def test_getElementValueByName(self): + def test_getElementValueByName(self) -> None: """Test method getElementValueByName.""" from ..plugins.type_studio import getElementValueByName diff --git a/pod/recorder/views.py b/pod/recorder/views.py index 4c8d822f06..e99c917e92 100644 --- a/pod/recorder/views.py +++ b/pod/recorder/views.py @@ -35,12 +35,13 @@ from django.template.defaultfilters import truncatechars from django.template.loader import render_to_string from django.urls import reverse -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_protect from django.views.decorators.http import require_http_methods from pod.main.views import in_maintenance, TEMPLATE_VISIBLE_SETTINGS +from pod.main.utils import is_ajax from pod.recorder.models import Recorder, Recording, RecordingFileTreatment from .forms import RecordingForm, RecordingFileTreatmentDeleteForm from .models import __REVATSO__ @@ -350,7 +351,7 @@ def claim_record(request): except EmptyPage: records = paginator.page(paginator.num_pages) - if request.is_ajax(): + if is_ajax(request): return render( request, "recorder/record_list.html", @@ -894,8 +895,8 @@ def digest_hosts_json(request): if (request.is_secure()) else "http://%s" % request.get_host() ) - server_ip = request.META.get( - "HTTP_X_FORWARDED_FOR", request.META.get("REMOTE_ADDR", "") + server_ip = request.headers.get( + "x-forwarded-for", request.META.get("REMOTE_ADDR", "") ) server_ip = server_ip.split(",")[0] if server_ip else None diff --git a/pod/settings.py b/pod/settings.py index fd35a19cae..880d6d3559 100644 --- a/pod/settings.py +++ b/pod/settings.py @@ -1,19 +1,31 @@ """ Django global settings for pod_project. -Django version: 3.2. +Django version: 4.2. """ import os +import sys import importlib.util +# DEPRECATIONS HACKS +import django +from django.utils.translation import gettext +from urllib.parse import quote + +# Needed for django-cas-client==1.5.3 +django.utils.http.urlquote = quote + +# Needed for django-chunked-upload==2.0.0 +django.utils.translation.ugettext = gettext + BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # will be update in pod/main/settings.py ## # Version of the project # -VERSION = "3.9.0" +VERSION = "4.0.0--ALPHA" ## # Installed applications list @@ -33,7 +45,8 @@ # Exterior Applications "ckeditor", "sorl.thumbnail", - "tagging", + # "tagging", + "tagulous", "cas", "captcha", "rest_framework", @@ -44,7 +57,7 @@ "chunked_upload", "mozilla_django_oidc", "honeypot", - "lti_provider", + # "lti_provider", (wait until lti_provider has been upgraded > 1.0, with upgraded oauth2) "pwa", "webpush", # Pod Applications @@ -142,7 +155,7 @@ ## # Password validation -# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.{0}".format(validator)} for validator in [ @@ -155,9 +168,8 @@ ## # Internationalization -# https://docs.djangoproject.com/en/3.2/topics/i18n/ +# https://docs.djangoproject.com/en/4.2/topics/i18n/ USE_I18N = True -USE_L10N = True LOCALE_PATHS = (os.path.join(BASE_DIR, "pod", "locale"),) DEFAULT_AUTO_FIELD = "django.db.models.AutoField" @@ -185,7 +197,7 @@ ## -# Logging configuration https://docs.djangoproject.com/en/3.2/topics/logging/ +# Logging configuration https://docs.djangoproject.com/en/4.2/topics/logging/ # LOG_DIRECTORY = os.path.join(BASE_DIR, "pod", "log") if not os.path.exists(LOG_DIRECTORY): @@ -478,6 +490,7 @@ def update_settings(local_settings): locals()["DEBUG"] is True and importlib.util.find_spec("debug_toolbar") is not None and locals()["USE_DEBUG_TOOLBAR"] + and "test" not in sys.argv ): INSTALLED_APPS.append("debug_toolbar") MIDDLEWARE = [ @@ -496,3 +509,18 @@ def show_toolbar(request): and importlib.util.find_spec("django_extensions") is not None ): INSTALLED_APPS.append("django_extensions") + + +# Django Tag manager +SERIALIZATION_MODULES = { + "xml": "tagulous.serializers.xml_serializer", + "json": "tagulous.serializers.json", + "python": "tagulous.serializers.python", + "yaml": "tagulous.serializers.pyyaml", +} +# see https://django-tagulous.readthedocs.io/en/latest/installation.html#settings +TAGULOUS_NAME_MAX_LENGTH = 80 +""" +TAGULOUS_SLUG_MAX_LENGTH = 50 +TAGULOUS_LABEL_MAX_LENGTH = TAGULOUS_NAME_MAX_LENGTH +""" diff --git a/pod/speaker/forms.py b/pod/speaker/forms.py index da0f1da003..f0fa4250d8 100644 --- a/pod/speaker/forms.py +++ b/pod/speaker/forms.py @@ -1,7 +1,7 @@ """Esup-Pod forms speaker.""" from django_select2 import forms as s2forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django import forms from django.forms.widgets import HiddenInput from .models import Speaker, Job, JobVideo diff --git a/pod/speaker/models.py b/pod/speaker/models.py index 253b157227..cd346cbd05 100644 --- a/pod/speaker/models.py +++ b/pod/speaker/models.py @@ -1,7 +1,7 @@ """Esup-Pod speaker models.""" from django.db import models -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext as _ from pod.video.models import Video diff --git a/pod/speaker/utils.py b/pod/speaker/utils.py index cb62832b96..dedf730444 100644 --- a/pod/speaker/utils.py +++ b/pod/speaker/utils.py @@ -1,21 +1,20 @@ """Esup-Pod speaker utilities.""" -from typing import Optional, Dict, List from pod.speaker.models import Speaker, JobVideo from pod.video.models import Video -def get_all_speakers() -> Optional[Speaker]: +def get_all_speakers(): """ Retrieve the speakers list. Returns: - Optional[Speakers]: The speakers list, or None if no speaker is found. + [Speakers | None]: The speakers list, or None if no speaker is found. """ return Speaker.objects.prefetch_related("job_set").all() -def get_video_speakers(video: Video) -> Optional[JobVideo]: +def get_video_speakers(video: Video): """ Retrieve the speakers associated with a given video. @@ -23,12 +22,12 @@ def get_video_speakers(video: Video) -> Optional[JobVideo]: video (Video): The video for which to retrieve the speakers. Returns: - Optional[JobVideo]: The jobs associated with the video, or None if no job is found. + [JobVideo]: The jobs associated with the video, or None if no job is found. """ return JobVideo.objects.filter(video=video) -def get_video_speakers_grouped(video) -> Dict[Speaker, List[str]]: +def get_video_speakers_grouped(video) -> dict[Speaker, list[str]]: """ Group the jobs by speaker for a given video. @@ -36,7 +35,7 @@ def get_video_speakers_grouped(video) -> Dict[Speaker, List[str]]: video (Video): The video for which to group the speakers. Returns: - Dict[Speaker, List[str]]: A dictionary where the keys are the speakers (Speaker) + dict[Speaker, list[str]]: A dictionary where the keys are the speakers (Speaker) and the values are lists of job titles (str) associated with each speaker. """ speakers = video.jobvideo_set.select_related("job__speaker").all() diff --git a/pod/urls.py b/pod/urls.py index bb578c6032..e50fa6457c 100644 --- a/pod/urls.py +++ b/pod/urls.py @@ -1,14 +1,13 @@ """Esup-pod URL configuration.""" from django.conf import settings -from django.conf.urls import url -from django.conf.urls import include -from django.urls import path +from django.urls import include +from django.urls import path, re_path from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth import views as auth_views from django.views.i18n import JavaScriptCatalog -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ import importlib.util @@ -27,17 +26,17 @@ USE_CAS = getattr(settings, "USE_CAS", False) USE_SHIB = getattr(settings, "USE_SHIB", False) USE_OIDC = getattr(settings, "USE_OIDC", False) -USE_NOTIFICATIONS = getattr(settings, "USE_NOTIFICATIONS", True) -USE_CUT = getattr(settings, "USE_CUT", True) +USE_NOTIFICATIONS = getattr(settings, "USE_NOTIFICATIONS", False) +USE_CUT = getattr(settings, "USE_CUT", False) USE_MEETING = getattr(settings, "USE_MEETING", False) USE_XAPI = getattr(settings, "USE_XAPI", False) USE_OPENCAST_STUDIO = getattr(settings, "USE_OPENCAST_STUDIO", False) USE_PODFILE = getattr(settings, "USE_PODFILE", False) -USE_PLAYLIST = getattr(settings, "USE_PLAYLIST", True) -USE_DRESSING = getattr(settings, "USE_DRESSING", True) +USE_PLAYLIST = getattr(settings, "USE_PLAYLIST", False) +USE_DRESSING = getattr(settings, "USE_DRESSING", False) USE_SPEAKER = getattr(settings, "USE_SPEAKER", False) -USE_IMPORT_VIDEO = getattr(settings, "USE_IMPORT_VIDEO", True) -USE_QUIZ = getattr(settings, "USE_QUIZ", True) +USE_IMPORT_VIDEO = getattr(settings, "USE_IMPORT_VIDEO", False) +USE_QUIZ = getattr(settings, "USE_QUIZ", False) USE_AI_ENHANCEMENT = getattr(settings, "USE_AI_ENHANCEMENT", False) WEBTV_MODE = getattr(settings, "WEBTV_MODE", False) USE_DUPLICATE = getattr(settings, "USE_DUPLICATE", False) @@ -47,75 +46,75 @@ urlpatterns = [ - url("select2/", include("django_select2.urls")), - url("robots.txt", robots_txt), - url("info_pod.json", info_pod), - url(r"^admin/", admin.site.urls), + path("select2/", include("django_select2.urls")), + path("robots.txt", robots_txt), + path("info_pod.json", info_pod), + re_path(r"^admin/", admin.site.urls), # Translation - url(r"^i18n/", include("django.conf.urls.i18n")), - url(r"^jsi18n/$", JavaScriptCatalog.as_view(), name="javascript-catalog"), + path("i18n/", include("django.conf.urls.i18n")), + path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), # Maintenance mode - url(r"^maintenance/$", maintenance, name="maintenance"), + path("maintenance/", maintenance, name="maintenance"), # videos - url(r"^videos/", include("pod.video.urls-videos")), - url(r"^rss", include("pod.video.urls-rss")), - url(r"^video/", include("pod.video.urls")), - url(r"^ajax_calls/search_user/", user_autocomplete), + path("videos/", include("pod.video.urls-videos")), + path("rss", include("pod.video.urls-rss")), + path("video/", include("pod.video.urls")), + re_path(r"^ajax_calls/search_user/", user_autocomplete), # my channels - url(r"^channels/", include("pod.video.urls-channels")), + path("channels/", include("pod.video.urls-channels")), # recording - url(r"^record/", include("pod.recorder.urls")), + path("record/", include("pod.recorder.urls")), # search - url(r"^search/", include("pod.video_search.urls")), - url(r"^authentication_", include("pod.authentication.urls")), - url( - r"^accounts/login/$", + path("search/", include("pod.video_search.urls")), + path("authentication_", include("pod.authentication.urls")), + path( + "accounts/login/", auth_views.LoginView.as_view(), {"redirect_authenticated_user": True}, name="local-login", ), - url( - r"^accounts/logout/$", + path( + "accounts/logout/", auth_views.LogoutView.as_view(), {"next_page": "/"}, name="local-logout", ), - 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"), + path("accounts/change-password/", auth_views.PasswordChangeView.as_view()), + path("accounts/reset-password/", auth_views.PasswordResetView.as_view()), + path("accounts/userpicture/", userpicture, name="userpicture"), + path("accounts/set-notifications/", set_notifications, name="set_notifications"), # rest framework - url(r"^api-auth/", include("rest_framework.urls")), - url(r"^rest/", include(rest_urlpatterns)), + path("api-auth/", include("rest_framework.urls")), + path("rest/", include(rest_urlpatterns)), # contact_us - url(r"^contact_us/$", contact_us, name="contact_us"), - url(r"^captcha/", include("captcha.urls")), - url(r"^download/$", download_file, name="download_file"), + path("contact_us/", contact_us, name="contact_us"), + path("captcha/", include("captcha.urls")), + path("download/", download_file, name="download_file"), # custom - url(r"^custom/", include("pod.custom.urls")), + path("custom/", include("pod.custom.urls")), # pwa - url("", include("pwa.urls")), + path("", include("pwa.urls")), ] # WEBPUSH if USE_NOTIFICATIONS: urlpatterns += [ # webpush - url(r"^webpush/", include("webpush.urls")), + path("webpush/", include("webpush.urls")), ] # CAS if USE_CAS: - # urlpatterns += [url(r'^sso-cas/', include('cas.urls')), ] + # urlpatterns += [re_path(r'^sso-cas/', include('cas.urls')), ] urlpatterns += [ - url(r"^sso-cas/login/$", cas_views.login, name="cas-login"), - url(r"^sso-cas/logout/$", cas_views.logout, name="cas-logout"), + path("sso-cas/login/", cas_views.login, name="cas-login"), + path("sso-cas/logout/", cas_views.logout, name="cas-logout"), ] # OIDC if USE_OIDC: urlpatterns += [ - url(r"^oidc/", include("mozilla_django_oidc.urls")), + path("oidc/", include("mozilla_django_oidc.urls")), ] # PWA @@ -127,30 +126,30 @@ if USE_MEETING: urlpatterns += [ - url(r"^meeting/", include("pod.meeting.urls")), + path("meeting/", include("pod.meeting.urls")), ] if USE_XAPI: urlpatterns += [ - url(r"^xapi/", include("pod.xapi.urls")), + path("xapi/", include("pod.xapi.urls")), ] # RECORDER if USE_OPENCAST_STUDIO: urlpatterns += [ - url(r"^studio/", include("pod.recorder.studio_urls")), - url(r"^digest/studio/", include("pod.recorder.studio_urls_digest")), + path("studio/", include("pod.recorder.studio_urls")), + path("digest/studio/", include("pod.recorder.studio_urls_digest")), ] # PODFILE if USE_PODFILE: urlpatterns += [ - url(r"^podfile/", include("pod.podfile.urls")), + path("podfile/", include("pod.podfile.urls")), ] for apps in settings.THIRD_PARTY_APPS: urlpatterns += [ - url(r"^" + apps + "/", include("pod.%s.urls" % apps, namespace=apps)), + re_path(r"^" + apps + "/", include("pod.%s.urls" % apps, namespace=apps)), ] # CUT @@ -195,9 +194,7 @@ # IMPORT_VIDEO if USE_IMPORT_VIDEO: urlpatterns += [ - url( - r"^import_video/", include("pod.import_video.urls", namespace="import_video") - ), + path("import_video/", include("pod.import_video.urls", namespace="import_video")), ] if USE_DUPLICATE: @@ -214,7 +211,7 @@ # CHANNELS urlpatterns += [ - url(r"^", include("pod.video.urls-channels-video")), + path("", include("pod.video.urls-channels-video")), ] # Change admin site title diff --git a/pod/video/__init__.py b/pod/video/__init__.py index 46defcd11c..e69de29bb2 100644 --- a/pod/video/__init__.py +++ b/pod/video/__init__.py @@ -1 +0,0 @@ -default_app_config = "pod.video.apps.VideoConfig" diff --git a/pod/video/admin.py b/pod/video/admin.py index 4ab2adf5d3..aa09747e68 100644 --- a/pod/video/admin.py +++ b/pod/video/admin.py @@ -7,7 +7,7 @@ from django.urls import reverse from django.utils.html import format_html from django.utils.html import mark_safe -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from modeltranslation.admin import TranslationAdmin from .models import Video @@ -108,6 +108,7 @@ class VideoVersionInline(admin.StackedInline): can_delete = False +@admin.register(Video) class VideoAdmin(admin.ModelAdmin): list_display = ( "id", @@ -176,12 +177,11 @@ class VideoAdmin(admin.ModelAdmin): inlines += [ChapterInline] + @admin.display(description=_("Establishment")) def get_owner_establishment(self, obj): owner = obj.owner return owner.owner.establishment - get_owner_establishment.short_description = _("Establishment") - # Ajout de l'attribut 'establishment' if USE_ESTABLISHMENT_FIELD: list_filter = list_filter + ("owner__owner__establishment",) @@ -190,15 +190,13 @@ def get_owner_establishment(self, obj): "owner__owner__establishment", ) + @admin.display(description=_("Owner")) def get_owner_by_name(self, obj): owner = obj.owner url = url_to_edit_object(owner) title = "%s %s (%s)" % (owner.first_name, owner.last_name, url) return mark_safe(title) - get_owner_by_name.allow_tags = True - get_owner_by_name.short_description = _("Owner") - def get_form(self, request, obj=None, **kwargs): if request.user.is_superuser: kwargs["form"] = VideoSuperAdminForm @@ -229,28 +227,25 @@ def get_form(self, request, obj=None, **kwargs): else: actions = ["encode_video", "draft_video"] + @admin.action(description=_("Set as draft")) def draft_video(self, request, queryset): for item in queryset: item.is_draft = True item.save() - draft_video.short_description = _("Set as draft") - + @admin.action(description=_("Encode selected")) def encode_video(self, request, queryset): for item in queryset: item.launch_encode = True item.save() - encode_video.short_description = _("Encode selected") - + @admin.action(description=_("Transcript selected")) def transcript_video(self, request, queryset): for item in queryset: if item.get_video_mp3() and not item.encoding_in_progress: transcript_video = getattr(transcript, TRANSCRIPT_VIDEO) transcript_video(item.id) - transcript_video.short_description = _("Transcript selected") - def get_queryset(self, request): qs = super().get_queryset(request) if not request.user.is_superuser: @@ -288,6 +283,7 @@ class Media: ) +@admin.register(UpdateOwner) class updateOwnerAdmin(admin.ModelAdmin): """Handle an admin page to change owner of several videos.""" @@ -370,9 +366,11 @@ class Meta(object): } +@admin.register(Channel) class ChannelAdmin(admin.ModelAdmin): search_fields = ["title"] + @admin.display(description=_("Owners")) def get_owners(self, obj): owners = [] for owner in obj.owners.all(): @@ -382,9 +380,6 @@ def get_owners(self, obj): titles = ", ".join(owners) return mark_safe(titles) - get_owners.allow_tags = True - get_owners.short_description = _("Owners") - list_display = ( "title", "get_owners", @@ -440,6 +435,7 @@ def get_queryset(self, request): return qs +@admin.register(Theme) class ThemeAdmin(admin.ModelAdmin): form = ThemeForm list_display = ("title", "channel") @@ -479,6 +475,7 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, **kwargs) +@admin.register(Type) class TypeAdmin(TranslationAdmin): form = TypeForm prepopulated_fields = {"slug": ("title",)} @@ -519,6 +516,7 @@ def get_queryset(self, request): return qs +@admin.register(Discipline) class DisciplineAdmin(TranslationAdmin): form = DisciplineForm prepopulated_fields = {"slug": ("title",)} @@ -559,6 +557,7 @@ def get_queryset(self, request): return qs +@admin.register(Notes) class NotesAdmin(admin.ModelAdmin): list_display = ("video", "user") autocomplete_fields = ["video", "user"] @@ -583,6 +582,7 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, **kwargs) +@admin.register(AdvancedNotes) class AdvancedNotesAdmin(admin.ModelAdmin): list_display = ("video", "user", "timestamp", "status", "added_on", "modified_on") search_fields = ["note"] @@ -608,6 +608,7 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, **kwargs) +@admin.register(NoteComments) class NoteCommentsAdmin(admin.ModelAdmin): autocomplete_fields = ["user", "parentNote", "parentCom"] search_fields = ["comment"] @@ -638,17 +639,18 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, **kwargs) +@admin.register(VideoToDelete) class VideoToDeleteAdmin(admin.ModelAdmin): list_display = ("date_deletion", "get_videos") list_filter = ["date_deletion"] autocomplete_fields = ["video"] + @admin.display(description="video") def get_videos(self, obj): return obj.video.count() - get_videos.short_description = "video" - +@admin.register(ViewCount) class ViewCountAdmin(admin.ModelAdmin): list_display = ("video", "date", "count") readonly_fields = ("video", "date", "count") @@ -660,6 +662,7 @@ def get_queryset(self, request): return qs +@admin.register(Category) class CategoryAdmin(admin.ModelAdmin): """Admin for the Category model.""" @@ -667,30 +670,15 @@ class CategoryAdmin(admin.ModelAdmin): readonly_fields = ("slug",) # list_filter = ["owner"] + @admin.display(description="Videos") def videos_count(self, obj) -> int: return obj.video.all().count() - videos_count.short_description = "Videos" - +@admin.register(VideoAccessToken) class VideoAccessTokenAdmin(admin.ModelAdmin): """Admin for the VideoAccessToken model.""" list_display = ("video", "token", "name") readonly_fields = ("token",) autocomplete_fields = ["video"] - - -admin.site.register(Channel, ChannelAdmin) -admin.site.register(Type, TypeAdmin) -admin.site.register(Discipline, DisciplineAdmin) -admin.site.register(Theme, ThemeAdmin) -admin.site.register(Video, VideoAdmin) -admin.site.register(UpdateOwner, updateOwnerAdmin) -admin.site.register(Notes, NotesAdmin) -admin.site.register(AdvancedNotes, AdvancedNotesAdmin) -admin.site.register(NoteComments, NoteCommentsAdmin) -admin.site.register(VideoToDelete, VideoToDeleteAdmin) -admin.site.register(ViewCount, ViewCountAdmin) -admin.site.register(Category, CategoryAdmin) -admin.site.register(VideoAccessToken, VideoAccessTokenAdmin) diff --git a/pod/video/context_processors.py b/pod/video/context_processors.py index 3e37663e4c..c81069c11c 100644 --- a/pod/video/context_processors.py +++ b/pod/video/context_processors.py @@ -1,9 +1,13 @@ +"""Esup-Pod vido context processor.""" + from django.conf import settings as django_settings from pod.video.models import Type from pod.video.models import Discipline from pod.video.models import Video +from pod.video.utils import get_tag_cloud + from django.db.models import Count, Sum from django.db.models import Q from django.db.models import Exists @@ -95,6 +99,11 @@ def context_video_data(request): ) cache.set("TYPES", types, timeout=CACHE_VIDEO_DEFAULT_TIMEOUT) + tags = cache.get("TAGS") + if tags is None: + tags = get_tag_cloud() + cache.set("TAGS", tags, timeout=CACHE_VIDEO_DEFAULT_TIMEOUT) + disciplines = cache.get("DISCIPLINES") if disciplines is None: disciplines = ( @@ -130,4 +139,5 @@ def context_video_data(request): "VIDEOS_COUNT": VIDEOS_COUNT, "VIDEOS_DURATION": VIDEOS_DURATION, "CHANNELS_PER_BATCH": CHANNELS_PER_BATCH, + "TAGS": tags, } diff --git a/pod/video/feeds.py b/pod/video/feeds.py index c5377e7017..f5d81a4115 100644 --- a/pod/video/feeds.py +++ b/pod/video/feeds.py @@ -5,7 +5,7 @@ # from datetime import datetime from django.conf import settings -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.contrib.sites.shortcuts import get_current_site from django.urls import reverse from django.shortcuts import get_object_or_404 diff --git a/pod/video/fixtures/sample_videos.json b/pod/video/fixtures/sample_videos.json index 0e5fd00e5d..56cf5874d7 100644 --- a/pod/video/fixtures/sample_videos.json +++ b/pod/video/fixtures/sample_videos.json @@ -19,6 +19,7 @@ "owner": 1, "video": "pod/main/static/video_test/pod.mp4", "title": "VIDEO_TEST", + "title_en": "VIDEO_TEST", "type": 1, "is_draft": 0 } @@ -31,6 +32,7 @@ "owner": 1, "video": "pod/main/static/video_test/pod.mp3", "title": "AUDIO_TEST", + "title_en": "AUDIO_TEST", "type": 1, "is_draft": 0 } diff --git a/pod/video/forms.py b/pod/video/forms.py index 8917f23380..0aa9481ff9 100644 --- a/pod/video/forms.py +++ b/pod/video/forms.py @@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError from django.forms.widgets import ClearableFileInput from django.utils.deconstruct import deconstructible -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.template.defaultfilters import filesizeformat from .models import Video, VideoVersion, get_storage_path_video from .models import Channel @@ -742,7 +742,7 @@ def change_encoded_path(self, video, new_dir, old_dir) -> None: encoding.source_file = encoding.source_file.name.replace(old_dir, new_dir) encoding.save() - def save_visibility(self): + def save_visibility(self) -> None: """Save video access fields depends on the visibility field value.""" visibility = self.cleaned_data.get("visibility") if visibility == "public": @@ -922,7 +922,7 @@ def __init__(self, *args, **kwargs) -> None: super(VideoForm, self).__init__(*args, **kwargs) self.custom_video_form() - # change ckeditor, thumbnail and date delete config for non staff user + # change WYSIWYG, thumbnail and date delete config for non staff user self.set_nostaff_config() # hide default language self.hide_default_language() @@ -1159,7 +1159,7 @@ def __init__(self, *args, **kwargs) -> None: owner__sites=Site.objects.get_current() ) - # change ckeditor config for no staff user + # change WYSIWYG config for no staff user if not hasattr(self, "admin_form") and ( self.is_staff is False and self.is_superuser is False ): diff --git a/pod/video/management/commands/cache_video_data.py b/pod/video/management/commands/cache_video_data.py index 22c5517200..c82f241246 100644 --- a/pod/video/management/commands/cache_video_data.py +++ b/pod/video/management/commands/cache_video_data.py @@ -15,9 +15,11 @@ class Command(BaseCommand): + "types, discipline, video count and videos duration" ) - def handle(self, *args, **options): + def handle(self, *args, **options) -> None: """Store video data in cache.""" - cache.delete_many(["DISCIPLINES", "VIDEOS_COUNT", "VIDEOS_DURATION", "TYPES"]) + cache.delete_many( + ["DISCIPLINES", "VIDEOS_COUNT", "VIDEOS_DURATION", "TYPES", "TAGS"] + ) video_data = context_video_data(request=None) msg = "Successfully store video data in cache" for data in video_data: diff --git a/pod/video/management/commands/import_data.py b/pod/video/management/commands/import_data.py index 286f2deba4..1a108e6bf5 100755 --- a/pod/video/management/commands/import_data.py +++ b/pod/video/management/commands/import_data.py @@ -211,7 +211,7 @@ def get_new_data(self, type_to_import, filedata): return newdata - def add_data_to_video(self, type_to_import, obj, data): + def add_data_to_video(self, type_to_import, obj, data) -> None: """Add related data (docs, tags, track, enrich) to video.""" if type_to_import in ("docpods",): self.add_doc_to_video(obj, data) @@ -222,7 +222,7 @@ def add_data_to_video(self, type_to_import, obj, data): if type_to_import in ("enrichpods",): self.add_enrich_to_video(obj, data) - def add_tag_to_video(self, video_id, list_tag): + def add_tag_to_video(self, video_id, list_tag) -> None: """Add tags to video.""" try: video = Video.objects.get(id=video_id) @@ -231,7 +231,7 @@ def add_tag_to_video(self, video_id, list_tag): except ObjectDoesNotExist: print(video_id, " does not exist") - def add_doc_to_video(self, video_id, list_doc): + def add_doc_to_video(self, video_id, list_doc) -> None: """Add docs to video.""" print(video_id, list_doc) try: diff --git a/pod/video/management/commands/recorder.py b/pod/video/management/commands/recorder.py index 3b700da435..7e82c5f744 100644 --- a/pod/video/management/commands/recorder.py +++ b/pod/video/management/commands/recorder.py @@ -346,7 +346,7 @@ def case_studio_recording( # Cursus video.cursus = recorder.cursus # Tags - video.tags = recorder.tags + video.tags = recorder.tags.get_tag_list() # Transcription if getattr(settings, "USE_TRANSCRIPTION", False): video.transcript = recorder.transcript diff --git a/pod/video/management/commands/reindex_videos.py b/pod/video/management/commands/reindex_videos.py new file mode 100644 index 0000000000..01c3556b15 --- /dev/null +++ b/pod/video/management/commands/reindex_videos.py @@ -0,0 +1,53 @@ +"""Script reindexing all videos (useful in case of loss of the ElasticSearch database)""" + +from django.core.management.base import BaseCommand +from pod.video.models import Video +from pod.video_search.models import index_video + + +def reindex_all_videos(dry_run: bool) -> int: + """Reindex all videos.""" + print("\nReindexing all videos...") + videos = Video.objects.all() + nb_videos = 0 + for vid in videos: + print(".", end="") + if not dry_run: + index_video(vid) + nb_videos += 1 + print("") + return nb_videos + + +class Command(BaseCommand): + """Reindex all videos.""" + + help = "Reindex all videos (useful in case of loss of the ElasticSearch database)" + + def add_arguments(self, parser) -> None: + """Allow arguments to be used with the command.""" + parser.add_argument( + "--dry", + help="Simulate what would be reindexed.", + action="store_true", + default=False, + ) + + def handle(self, *args, **options) -> None: + """Handle the clean_video_files command call.""" + if options["dry"]: + print("Simulation mode ('dry'). Nothing will be deleted.") + self.nb_reindexed = reindex_all_videos(options["dry"]) + + self.print_resume(options["dry"]) + + def print_resume(self, dry_run: bool) -> None: + """Print summary of reindexed objects.""" + + if dry_run: + print( + "[DRY RUN] %i video(s) would have been reindexed." % (self.nb_reindexed) + ) + else: + print("%i video(s) reindexed." % self.nb_reindexed) + print("Have a nice day ;)") diff --git a/pod/video/models.py b/pod/video/models.py index 2c09bd96bd..283d2d9883 100644 --- a/pod/video/models.py +++ b/pod/video/models.py @@ -1,1925 +1,1926 @@ -"""Esup-Pod Video models.""" - -import os -import re -import time -import uuid -from typing import Optional -import unicodedata -import json -import logging -import hashlib - -from django.db import models -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from django.utils.translation import get_language -from django.template.defaultfilters import slugify -from django.db.models import Sum -from django.contrib.auth.models import User -from django.urls import reverse -from django.core.exceptions import ValidationError -from django.core.exceptions import ObjectDoesNotExist -from django.contrib.sites.shortcuts import get_current_site -from django.templatetags.static import static -from django.dispatch import receiver -from django.db.models.signals import pre_delete, post_delete -from tagging.models import Tag -from datetime import date -from django.utils import timezone -from django.utils.html import format_html, escape -from django.utils.text import capfirst -from ckeditor.fields import RichTextField -from tagging.fields import TagField -from django.contrib.sites.models import Site -from django.db.models.signals import post_save -from django.db.models.signals import pre_save -from pod.main.models import AdditionalChannelTab -import importlib -from django.contrib.auth.hashers import make_password -from pod.main.context_processors import WEBTV_MODE - -from sorl.thumbnail import get_thumbnail -from pod.authentication.models import AccessGroup -from pod.main.models import get_nextautoincrement -from pod.main.lang_settings import ALL_LANG_CHOICES as __ALL_LANG_CHOICES__ -from pod.main.lang_settings import PREF_LANG_CHOICES as __PREF_LANG_CHOICES__ -from django.db.models import Count, Case, When, Value, BooleanField, Q -from django.db.models.functions import Concat -from os.path import splitext - -USE_PODFILE = getattr(settings, "USE_PODFILE", False) -if USE_PODFILE: - from pod.podfile.models import CustomImageModel - from pod.podfile.models import UserFolder -else: - from pod.main.models import CustomImageModel - -logger = logging.getLogger(__name__) - -RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY = getattr( - settings, "RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY", False -) -VIDEO_RECENT_VIEWCOUNT = getattr(settings, "VIDEO_RECENT_VIEWCOUNT", 180) -VIDEOS_DIR = getattr(settings, "VIDEOS_DIR", "videos") -SITE_ID = getattr(settings, "SITE_ID", 1) - -LANG_CHOICES = getattr( - settings, - "LANG_CHOICES", - ( - (_("-- Frequently used languages --"), __PREF_LANG_CHOICES__), - (_("-- All languages --"), __ALL_LANG_CHOICES__), - ), -) - -CURSUS_CODES = getattr( - settings, - "CURSUS_CODES", - ( - ("0", _("None / All")), - ("L", _("Bachelor’s Degree")), - ("M", _("Master’s Degree")), - ("D", _("Doctorate")), - ("1", _("Other")), - ), -) - -__LANG_CHOICES_DICT__ = { - key: value for key, value in LANG_CHOICES[0][1] + LANG_CHOICES[1][1] -} -__CURSUS_CODES_DICT__ = {key: value for key, value in CURSUS_CODES} - -DEFAULT_TYPE_ID = getattr(settings, "DEFAULT_TYPE_ID", 1) -LICENCE_CHOICES = getattr( - settings, - "LICENCE_CHOICES", - ( - ("by", _("Attribution 4.0 International (CC BY 4.0)")), - ( - "by-nd", - _("Attribution-NoDerivatives 4.0 International (CC BY-ND 4.0)"), - ), - ( - "by-nc-nd", - _( - "Attribution-NonCommercial-NoDerivatives 4.0 " - "International (CC BY-NC-ND 4.0)" - ), - ), - ( - "by-nc", - _("Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)"), - ), - ( - "by-nc-sa", - _( - "Attribution-NonCommercial-ShareAlike 4.0 " - "International (CC BY-NC-SA 4.0)" - ), - ), - ("by-sa", _("Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)")), - ), -) -__LICENCE_CHOICES_DICT__ = {key: value for key, value in LICENCE_CHOICES} -FORMAT_CHOICES = getattr( - settings, - "FORMAT_CHOICES", - ( - ("video/mp4", "video/mp4"), - ("video/mp2t", "video/mp2t"), - ("video/webm", "video/webm"), - ("audio/mp3", "audio/mp3"), - ("audio/wav", "audio/wav"), - ("application/x-mpegURL", "application/x-mpegURL"), - ), -) -ENCODING_CHOICES = getattr( - settings, - "ENCODING_CHOICES", - ( - ("audio", "audio"), - ("360p", "360p"), - ("480p", "480p"), - ("720p", "720p"), - ("1080p", "1080p"), - ("playlist", "playlist"), - ), -) -DEFAULT_THUMBNAIL = getattr(settings, "DEFAULT_THUMBNAIL", "img/default.svg") -SECRET_KEY = getattr(settings, "SECRET_KEY", "") - -NOTES_STATUS = getattr( - settings, - "NOTES_STATUS", - ( - ("0", _("Private (me only)")), - ("1", _("Shared with video owner")), - ("2", _("Public")), - ), -) - -THIRD_PARTY_APPS = getattr(settings, "THIRD_PARTY_APPS", []) - -__THIRD_PARTY_APPS_CHOICES__ = THIRD_PARTY_APPS.copy() -( - __THIRD_PARTY_APPS_CHOICES__.remove("live") - if ("live" in __THIRD_PARTY_APPS_CHOICES__) - else __THIRD_PARTY_APPS_CHOICES__ -) -__THIRD_PARTY_APPS_CHOICES__.insert(0, "Original") - -__VERSION_CHOICES__ = [ - (app.capitalize()[0], _("%(app)s version" % {"app": app.capitalize()})) - for app in __THIRD_PARTY_APPS_CHOICES__ -] - -__VERSION_CHOICES_DICT__ = {key: value for key, value in __VERSION_CHOICES__} - -## -# Settings exposed in templates -# -TEMPLATE_VISIBLE_SETTINGS = getattr( - settings, - "TEMPLATE_VISIBLE_SETTINGS", - { - "TITLE_SITE": "Pod", - "TITLE_ETB": "University name", - "LOGO_SITE": "img/logoPod.svg", - "LOGO_ETB": "img/esup-pod.svg", - "LOGO_PLAYER": "img/pod_favicon.svg", - "LINK_PLAYER": "", - "LINK_PLAYER_NAME": _("Home"), - "FOOTER_TEXT": ("",), - "FAVICON": "img/pod_favicon.svg", - "CSS_OVERRIDE": "", - "PRE_HEADER_TEMPLATE": "", - "POST_FOOTER_TEMPLATE": "", - "TRACKING_TEMPLATE": "", - }, -) -__TITLE_ETB__ = ( - TEMPLATE_VISIBLE_SETTINGS["TITLE_ETB"] - if (TEMPLATE_VISIBLE_SETTINGS.get("TITLE_ETB")) - else "University name" -) -DEFAULT_DC_COVERAGE = getattr( - settings, "DEFAULT_DC_COVERAGE", __TITLE_ETB__ + " - Town - Country" -) -DEFAULT_DC_RIGHTS = getattr(settings, "DEFAULT_DC_RIGHT", "BY-NC-SA") - -DEFAULT_YEAR_DATE_DELETE = getattr(settings, "DEFAULT_YEAR_DATE_DELETE", 2) - - -USE_TRANSCRIPTION = getattr(settings, "USE_TRANSCRIPTION", False) -if USE_TRANSCRIPTION: - TRANSCRIPTION_MODEL_PARAM = getattr(settings, "TRANSCRIPTION_MODEL_PARAM", {}) - TRANSCRIPTION_TYPE = getattr(settings, "TRANSCRIPTION_TYPE", "STT") - -# FUNCTIONS - - -def get_transcription_choices() -> list: - if USE_TRANSCRIPTION: - transcript_lang = TRANSCRIPTION_MODEL_PARAM.get(TRANSCRIPTION_TYPE, {}).keys() - transcript_choices_lang = [] - for lang in transcript_lang: - transcript_choices_lang.append((lang, __LANG_CHOICES_DICT__[lang])) - return transcript_choices_lang - else: - return [] - - -def default_date_delete() -> date: - """Get the default deletion date.""" - return date.today() + timezone.timedelta(days=DEFAULT_YEAR_DATE_DELETE * 365) - - -def select_video_owner(): - if RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY: - return lambda q: ( - Q(is_staff=True) & (Q(first_name__icontains=q) | Q(last_name__icontains=q)) - ) & Q(owner__sites=Site.objects.get_current()) - else: - return lambda q: (Q(first_name__icontains=q) | Q(last_name__icontains=q)) & Q( - owner__sites=Site.objects.get_current() - ) - - -def remove_accents(input_str) -> str: - """Remove diacritics in input string.""" - nkfd_form = unicodedata.normalize("NFKD", input_str) - return "".join([c for c in nkfd_form if not unicodedata.combining(c)]) - - -def get_storage_path_video(instance, filename) -> str: - """Get the video storage path. - - Instance needs to implement owner - """ - fname, dot, extension = filename.rpartition(".") - try: - fname.index("/") - return os.path.join( - VIDEOS_DIR, - instance.owner.owner.hashkey, - os.path.dirname(fname), - "%s.%s" - % ( - slugify(os.path.basename(fname)), - extension, - ), - ) - except ValueError: - return os.path.join( - VIDEOS_DIR, - instance.owner.owner.hashkey, - "%s.%s" % (slugify(fname), extension), - ) - - -# MODELS - - -class Channel(models.Model): - """Class describing Channels objects.""" - - title = models.CharField( - _("Title"), - max_length=100, - unique=True, - help_text=_( - "Please choose a title as short and accurate as " - "possible, reflecting the main subject / context " - "of the content.(max length: 100 characters)" - ), - ) - slug = models.SlugField( - _("Slug"), - unique=True, - max_length=100, - help_text=_( - 'Used to access this instance, the "slug" is a short label ' - + "containing only letters, numbers, underscore or dash top." - ), - editable=False, - ) - description = RichTextField( - _("Description"), - config_name="complete", - blank=True, - help_text=_( - "In this field you can describe your content, " - "add all needed related information, and " - "format the result using the toolbar." - ), - ) - headband = models.ForeignKey( - CustomImageModel, - models.SET_NULL, - blank=True, - null=True, - verbose_name=_("Headband"), - ) - color = models.CharField( - _("Background color"), - max_length=10, - blank=True, - null=True, - help_text=_( - "The background color for your channel. " - "You can use the format #. i.e.: #ff0000 for red" - ), - ) - style = models.TextField( - _("Extra style"), - null=True, - blank=True, - help_text=_("The style will be added to your channel to show it"), - ) - owners = models.ManyToManyField( - User, - related_name="owners_channels", - verbose_name=_("Owners"), - blank=True, - ) - users = models.ManyToManyField( - User, - related_name="users_channels", - verbose_name=_("Users"), - blank=True, - ) - visible = models.BooleanField( - verbose_name=_("Visible"), - help_text=_( - "If checked, the channel appear in a list of available " - + "channels on the platform." - ), - default=False, - ) - allow_to_groups = models.ManyToManyField( - AccessGroup, - blank=True, - verbose_name=_("Groups"), - help_text=_("One or more groups who can upload video to this channel."), - ) - add_channels_tab = models.ManyToManyField( - AdditionalChannelTab, verbose_name=_("Additionals channels tab"), blank=True - ) - site = models.ForeignKey( - Site, verbose_name=_("Site"), on_delete=models.CASCADE, default=SITE_ID - ) - - class Meta: - """Metadata subclass for Channel object.""" - - ordering = ["title"] - verbose_name = _("Channel") - verbose_name_plural = _("Channels") - constraints = [ - models.UniqueConstraint( - fields=["slug", "site"], name="channel_unique_slug_site" - ) - ] - - def __str__(self) -> str: - """Display a channel object as string.""" - return "%s" % (self.title) - - def get_absolute_url(self) -> str: - """Return channel absolute URL.""" - return reverse("channel-video:channel", args=[str(self.slug)]) - - def get_all_theme(self) -> list: - """Return the list of all child themes in current channel.""" - list_theme = [] - themes = self.themes.filter(parentId=None).order_by("title") - for theme in themes: - list_theme.append( - { - "id": theme.id, - "title": "%s" % theme.title, - "slug": "%s" % theme.slug, - "url": "%s" % theme.get_absolute_url(), - "child": theme.get_all_children_tree(), - } - ) - return list_theme - - def get_all_theme_json(self) -> str: - """Return theme list in json format.""" - return json.dumps(self.get_all_theme()) - - def save(self, *args, **kwargs) -> None: - """Store the channel object in db.""" - self.slug = slugify(self.title) - super(Channel, self).save(*args, **kwargs) - - -@receiver(pre_save, sender=Channel) -def default_site_channel(sender, instance, **kwargs) -> None: - if not hasattr(instance, "site"): - instance.site = Site.objects.get_current() - - -class Theme(models.Model): - """Class describing a them object. - - A theme is a child of channel or another theme object. - """ - - parentId = models.ForeignKey( - "self", - null=True, - blank=True, - related_name="children", - on_delete=models.CASCADE, - verbose_name=_("Theme parent"), - ) - title = models.CharField( - _("Title"), - max_length=100, - help_text=_( - "Please choose a title as short and accurate as " - "possible, reflecting the main subject / context " - "of the content.(max length: 100 characters)" - ), - ) - slug = models.SlugField( - _("Slug"), - max_length=100, - help_text=_( - 'Used to access this instance, the "slug" is a short label ' - + "containing only letters, numbers, underscore or dash top." - ), - editable=False, - ) - description = models.TextField( - _("Description"), - null=True, - blank=True, - help_text=_( - "In this field you can describe your content, " - "add all needed related information, and " - "format the result using the toolbar." - ), - ) - - headband = models.ForeignKey( - CustomImageModel, - models.SET_NULL, - blank=True, - null=True, - verbose_name=_("Headband"), - ) - - channel = models.ForeignKey( - "Channel", - related_name="themes", - verbose_name=_("Channel"), - on_delete=models.CASCADE, - ) - - @property - def site(self): - """Return sites associated to parent channel.""" - return self.channel.site - - def __str__(self) -> str: - """Display current theme as string.""" - return "%s: %s" % (self.channel.title, self.title) - - def get_absolute_url(self) -> str: - """Get current theme absolute URL.""" - return reverse( - "channel-video:theme", args=[str(self.channel.slug), str(self.slug)] - ) - - def save(self, *args, **kwargs) -> None: - """Store current theme object in db.""" - self.slug = slugify(self.title) - super(Theme, self).save(*args, **kwargs) - - def get_all_children_tree(self) -> list: - """Get a tree of all theme children.""" - children = [] - try: - child_list = self.children.all().order_by("title") - except AttributeError: - return children - for child in child_list: - children.append( - { - "id": child.id, - "title": "%s" % child.title, - "slug": "%s" % child.slug, - "url": "%s" % child.get_absolute_url(), - "child": child.get_all_children_tree(), - } - ) - return children - - def get_all_children_flat(self): - """Get a flat list of all theme children.""" - children = [self] - try: - child_list = self.children.all() - except AttributeError: - return children - for child in child_list: - children.extend(child.get_all_children_flat()) - return children - - def get_all_children_tree_json(self) -> str: - """Get a json tree of all theme children.""" - return json.dumps(self.get_all_children_tree()) - - def get_all_parents(self): - """Get a list of all theme parents.""" - parents = [self] - if self.parentId is not None: - parent = self.parentId - parents.extend(parent.get_all_parents()) - return parents - - def clean(self) -> None: - """Validate Theme fields.""" - # Dans le cas oĂč on modifie un theme - if ( - Theme.objects.filter(channel=self.channel, slug=slugify(self.title)) - .exclude(pk=self.id) - .exists() - ): - raise ValidationError( - { - "title": ValidationError( - _("A theme with this name already exists in this channel."), - code="invalid_title", - ) - } - ) - - if self.parentId in self.get_all_children_flat(): - raise ValidationError( - { - "parentId": ValidationError( - _("A theme can’t have itself or one of it’s children as parent."), - code="invalid_parent", - ) - } - ) - if self.parentId and self.parentId not in self.channel.themes.all(): - raise ValidationError( - { - "parentId": ValidationError( - _("A theme must be in the same channel as its parent."), - code="invalid_channel", - ) - } - ) - - class Meta: - """Metadata subclass of theme object.""" - - ordering = ["channel", "title"] - verbose_name = _("Theme") - verbose_name_plural = _("Themes") - unique_together = ("channel", "slug") - - -class Type(models.Model): - """Define all video types available.""" - - title = models.CharField(_("Title"), max_length=100, unique=True) - slug = models.SlugField( - _("Slug"), - unique=True, - max_length=100, - help_text=_( - 'Used to access this instance, the "slug" is a short label ' - + "containing only letters, numbers, underscore or dash top." - ), - ) - description = models.TextField(null=True, blank=True) - icon = models.ForeignKey( - CustomImageModel, - models.SET_NULL, - blank=True, - null=True, - verbose_name=_("Icon"), - ) - sites = models.ManyToManyField(Site) - - def __str__(self) -> str: - return "%s" % (self.title) - - def save(self, *args, **kwargs) -> None: - """Store current Type in DB.""" - self.slug = slugify(self.title) - super(Type, self).save(*args, **kwargs) - - class Meta: - """Metadata subclass of Type object.""" - - ordering = ["title"] - verbose_name = _("Type") - verbose_name_plural = _("Types") - - -@receiver(post_save, sender=Type) -def default_site_type(sender, instance, created: bool, **kwargs) -> None: - if instance.sites.count() == 0: - instance.sites.add(Site.objects.get_current()) - - -class Discipline(models.Model): - title = models.CharField(_("title"), max_length=100, unique=True) - slug = models.SlugField( - _("slug"), - unique=True, - max_length=100, - help_text=_( - 'Used to access this instance, the "slug" is a short label ' - + "containing only letters, numbers, underscore or dash top." - ), - ) - description = models.TextField(null=True, blank=True) - icon = models.ForeignKey( - CustomImageModel, - models.SET_NULL, - blank=True, - null=True, - verbose_name=_("Icon"), - ) - site = models.ForeignKey( - Site, verbose_name=_("Site"), on_delete=models.CASCADE, default=SITE_ID - ) - - def __str__(self) -> str: - return "%s" % (self.title) - - def save(self, *args, **kwargs) -> None: - """Store current Discipline in DB.""" - self.slug = slugify(self.title) - super(Discipline, self).save(*args, **kwargs) - - class Meta: - """Metadata subclass of Discipline object.""" - - ordering = ["title"] - verbose_name = _("Discipline") - verbose_name_plural = _("Disciplines") - - -@receiver(pre_save, sender=Discipline) -def default_site_discipline(sender, instance, **kwargs) -> None: - if not hasattr(instance, "site"): - instance.site = Site.objects.get_current() - - -class Video(models.Model): - """Class describing video objects.""" - - video = models.FileField( - _("Video"), - upload_to=get_storage_path_video, - max_length=255, - help_text=_("You can send an audio or video file."), - null=WEBTV_MODE, - blank=WEBTV_MODE, - ) - title = models.CharField( - _("Title"), - max_length=250, - help_text=_( - "A title as short and accurate as " - "possible, reflecting the main subject / context " - "of the content. (max length: 250 characters)" - ), - ) - slug = models.SlugField( - _("Slug"), - unique=True, - max_length=255, - help_text=_( - 'Used to access this instance, the "slug" is ' - "a short label containing only letters, " - "numbers, underscore or dash top." - ), - editable=False, - ) - sites = models.ManyToManyField(Site) - type = models.ForeignKey( - Type, - verbose_name=_("Type"), - on_delete=models.CASCADE, - help_text=_("The general type of the video."), - ) - owner = models.ForeignKey(User, verbose_name=_("Owner"), on_delete=models.CASCADE) - additional_owners = models.ManyToManyField( - User, - blank=True, - verbose_name=_("Additional owners"), - related_name="owners_videos", - help_text=_( - "Additional owners will have the same rights as you, except " - + "that they can’t delete this media." - ), - ) - description = RichTextField( - _("Description"), - config_name="complete", - blank=True, - help_text=_( - "Describe your content, add all needed related information, " - + "and format the result using the toolbar." - ), - ) - date_added = models.DateTimeField(_("Date added"), default=timezone.now) - date_evt = models.DateField( - _("Date of event"), default=date.today, blank=True, null=True - ) - cursus = models.CharField( - _("University course"), - max_length=1, - choices=CURSUS_CODES, - default="0", - help_text=_("Select an university course as audience target of the content."), - ) - main_lang = models.CharField( - _("Main language"), - max_length=2, - choices=LANG_CHOICES, - default=get_language(), - help_text=_("The main language used in the content."), - ) - transcript = models.CharField( - _("Transcript"), - max_length=2, - choices=get_transcription_choices(), - blank=True, - help_text=_("Select an available language to transcribe the audio."), - ) - tags = TagField( - help_text=_( - "Separate tags with spaces, " - "enclose the tags consist of several words in quotation marks." - ), - verbose_name=_("Tags"), - ) - discipline = models.ManyToManyField( - Discipline, - blank=True, - verbose_name=_("Disciplines"), - help_text=_("The disciplines to which your content belongs."), - ) - licence = models.CharField( - _("Licence"), - max_length=8, - choices=LICENCE_CHOICES, - blank=True, - null=True, - help_text=_("Usage rights granted to your content."), - ) - channel = models.ManyToManyField( - Channel, - verbose_name=_("Channels"), - blank=True, - help_text=_("The channel where you want your content to appear."), - ) - theme = models.ManyToManyField( - Theme, - verbose_name=_("Themes"), - blank=True, - help_text=_( - 'Hold down "Control", or "Command" on a Mac, to select more than one.' - ), - ) - allow_downloading = models.BooleanField( - _("allow downloading"), - default=False, - help_text=_("Check this box if you to allow downloading of the encoded files"), - ) - is_360 = models.BooleanField( - _("video 360"), - default=False, - help_text=_("Check this box if you want to use the 360 player for the video"), - ) - - is_draft = models.BooleanField( - verbose_name=_("Draft"), - help_text=_( - "If this box is checked, " - "the video will be visible and accessible only by you " - "and the additional owners." - ), - default=True, - ) - is_restricted = models.BooleanField( - verbose_name=_("Authentication restricted access"), - help_text=_( - "If this box is checked, " - "the video will only be accessible to authenticated users." - ), - default=False, - ) - restrict_access_to_groups = models.ManyToManyField( - AccessGroup, - blank=True, - verbose_name=_("Groups"), - help_text=_("One or more groups who can access to this video"), - ) - password = models.CharField( - _("password"), - help_text=_("Viewing this video will not be possible without this password."), - max_length=50, - blank=True, - null=True, - ) - order = models.PositiveSmallIntegerField( - _("order"), - help_text=_("Order videos in channels or themes."), - default=1, - blank=True, - null=True, - ) - thumbnail = models.ForeignKey( - CustomImageModel, - on_delete=models.SET_NULL, - blank=True, - null=True, - verbose_name=_("Thumbnails"), - related_name="videos", - ) - duration = models.IntegerField(_("Duration"), default=0, editable=False, blank=True) - overview = models.ImageField( - _("Overview"), - null=True, - upload_to=get_storage_path_video, - blank=True, - max_length=255, - editable=False, - ) - encoding_in_progress = models.BooleanField( - _("Encoding in progress"), default=False, editable=False - ) - is_video = models.BooleanField(_("Is Video"), default=True, editable=False) - - date_delete = models.DateField( - _("Date to delete"), - default=default_date_delete, - help_text=_("Date when your video will be automatically removed from Pod."), - ) - - disable_comment = models.BooleanField( - _("Disable comment"), - help_text=_("Prevent users from commenting on your content."), - default=False, - ) - - class Meta: - """Metadata subclass for Video object.""" - - ordering = ["-date_added", "-id"] - get_latest_by = "date_added" - verbose_name = _("video") - verbose_name_plural = _("videos") - - def set_password(self) -> None: - """ - Encrypt the password if video is protected. - - An encrypted password cannot be re-encrypted. - """ - if self.password and not self.password.startswith(("pbkdf2_sha256$")): - self.password = make_password(self.password, hasher="pbkdf2_sha256") - - def save(self, *args, **kwargs) -> None: - """Store a video object in db.""" - newid = -1 - - # In case of creating new Video - if not self.id: - try: - newid = get_nextautoincrement(Video) - except Exception: - try: - newid = Video.objects.latest("id").id - newid += 1 - except Exception: - newid = 1 - # fix date_delete depends of owner affiliation - ACCOMMODATION_YEARS = getattr(settings, "ACCOMMODATION_YEARS", {}) - if len(ACCOMMODATION_YEARS) and len(self.owner.owner.affiliation): - add_year = self.get_date_delete_for_affiliation(ACCOMMODATION_YEARS) - self.date_delete = date.today() + timezone.timedelta(days=add_year * 365) - - # Modifying existing Video - else: - newid = self.id - newid = "%04d" % newid - self.slug = "%s-%s" % (newid, slugify(self.title)) - self.tags = remove_accents(self.tags) - # self.set_password() - super(Video, self).save(*args, **kwargs) - # Ensure video folder will accord access to additional owners - self.update_additional_owners_rights() - - def __str__(self) -> str: - """Display a video object as string.""" - if self.id: - return "%s - %s" % ("%04d" % self.id, self.title) - else: - return "None" - - @property - def establishment(self): - return self.owner.owner.establishment - - @property - def viewcount(self): - """Get the view counter of a video.""" - return self.get_viewcount() - - viewcount.fget.short_description = _("Sum of view") - - @property - def recentViewcount(self): - """Get the recent view counter of a video.""" - return self.get_viewcount(VIDEO_RECENT_VIEWCOUNT) - - recentViewcount.fget.short_description = _( - "Sum of view of last %(ndays)s days" % {"ndays": VIDEO_RECENT_VIEWCOUNT} - ) - - def is_editable(self, user) -> bool: - """Return true if video is editable by user.""" - if RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY and user.is_staff is False: - return False - - if ( - (self.owner == user) - or (user.is_superuser) - or (user.has_perm("video.change_video")) - or (user in self.additional_owners.all()) - ): - return True - else: - return False - - @property - def get_encoding_step(self): - """Get the current encoding step of a video.""" - if hasattr(self, "encodingstep"): - return "%s: %s" % (self.encodingstep.num_step, self.encodingstep.desc_step) - else: - return "" - - get_encoding_step.fget.short_description = _("Encoding step") - - def get_date_delete_for_affiliation(self, accommodation_years): - """ - Get the calculated date of deletion according to multi-values field affiliation. - - If the affiliation field is an array (the user have several affiliations) - and their deletion deadlines exist, then the largest value (the longest date) - will be applied to the video. - """ - if self.affiliation_is_array(self.owner.owner.affiliation): - years = ( - accommodation_years.get(key) - for key in self.owner.owner.affiliation - if key in accommodation_years - ) - new_year = max(years) - else: - new_year = accommodation_years.get(self.owner.owner.affiliation) - if new_year is None: - new_year = DEFAULT_YEAR_DATE_DELETE - return new_year - - def affiliation_is_array(self, affiliation) -> bool: - """Check if the user affiliation is an array of strings or a simple string.""" - return True if affiliation.find("[") != -1 else False - - def get_player_height(self) -> int: - """ - Get the default player height (half size for audio files). - - This height is mostly valid when in iframe mode, - as in main mode height is set by % of window. - """ - return 360 if self.is_video else 244 - - def get_thumbnail_url(self, size="x720") -> str: - """Get a thumbnail url for the video, with defined max size.""" - request = None - # Initialize default thumbnail URL - thumbnail_url = "".join( - [ - "//", - get_current_site(request).domain, - static(DEFAULT_THUMBNAIL), - ] - ) - if self.thumbnail and self.thumbnail.file_exist(): - # Do not serve thumbnail url directly, as it can lead to the video URL - # Handle exception to avoid sending an error email - try: - im = get_thumbnail(self.thumbnail.file, size, crop="center", quality=80) - thumbnail_url = im.url - except Exception as e: - logger.error( - "An error occured during get_thumbnail_url" - " for video %s: %s" % (self.id, e) - ) - return thumbnail_url - else: - return thumbnail_url - - @property - def get_thumbnail_admin(self): - thumbnail_url = "" - # fix title for xml description - title = re.sub(r"[\x00-\x08\x0B-\x0C\x0E-\x1F]", "", self.title) - if self.thumbnail and self.thumbnail.file_exist(): - # Handle exception to avoid sending an error email - try: - im = get_thumbnail( - self.thumbnail.file, "100x100", crop="center", quality=72 - ) - thumbnail_url = im.url - except Exception: - thumbnail_url = static(DEFAULT_THUMBNAIL) - # - else: - thumbnail_url = static(DEFAULT_THUMBNAIL) - return format_html( - '%s' - % ( - thumbnail_url, - title.replace("{", "").replace("}", "").replace('"', "'"), - ) - ) - - get_thumbnail_admin.fget.short_description = _("Thumbnails") - - def get_thumbnail_card(self) -> str: - """Return thumbnail image card of current video.""" - thumbnail_url = "" - if self.thumbnail and self.thumbnail.file_exist(): - # Handle exception to avoid sending an error email - try: - im = get_thumbnail(self.thumbnail.file, "x170", crop="center", quality=72) - thumbnail_url = im.url - except Exception: - thumbnail_url = static(DEFAULT_THUMBNAIL) - # - else: - thumbnail_url = static(DEFAULT_THUMBNAIL) - return ( - '%s' - % (thumbnail_url, self.title) - ) - - @property - def duration_in_time(self) -> str: - """Get the duration of a video.""" - return time.strftime("%H:%M:%S", time.gmtime(self.duration)) - - duration_in_time.fget.short_description = _("Duration") - - @property - def encoded(self) -> bool: - """Get the encoded status of a video.""" - return ( - self.get_playlist_master() is not None - or self.get_video_mp4().exists() - or self.get_video_m4a() is not None - ) - - encoded.fget.short_description = _("Is the video encoded?") - - @property - def get_version(self): - """Get the version of a video.""" - try: - return self.videoversion.version - except VideoVersion.DoesNotExist: - return "O" - - def get_other_version(self) -> list: - """Get other versions of a video.""" - version = [] - for app in THIRD_PARTY_APPS: - mod = importlib.import_module("pod.%s.models" % app) - if hasattr(mod, capfirst(app)): - video_app = eval( - "mod.%s.objects.filter(video__id=%s).all()" % (capfirst(app), self.id) - ) - if ( - app == "interactive" - and video_app.first() is not None - and video_app.first().is_interactive() is False - ): - video_app = False - if video_app: - url = reverse( - "%(app)s:video_%(app)s" % {"app": app}, - kwargs={"slug": self.slug}, - ) - version.append( - { - "app": app, - "url": url, - "link": __VERSION_CHOICES_DICT__[app.capitalize()[0]], - } - ) - return version - - def get_default_version_link(self, slug_private: str = None) -> Optional[str]: - """ - Get link of the version of a video. - - Args: - slug_private (str, optional): The private slug. Defaults to None. - - Returns: - str | None: The default version link. - """ - for version in self.get_other_version(): - if version["link"] == __VERSION_CHOICES_DICT__[self.get_version]: - if slug_private: - return version["url"] + slug_private + "/" - else: - return version["url"] - - def get_viewcount(self, from_nb_day=0): - """Get the view counter of a video.""" - if from_nb_day > 0: - d = date.today() - timezone.timedelta(days=from_nb_day) - set = self.viewcount_set.filter(date__gte=d) - else: - set = self.viewcount_set.all() - - count_sum = set.aggregate(Sum("count")) - - if count_sum["count__sum"] is None: - return 0 - return count_sum["count__sum"] - - def get_marker_time_for_user(self, user): - """Get the marker time of a video for the user in parameter.""" - if self.usermarkertime_set.filter(user=user).exists(): - return self.usermarkertime_set.get(user=user).markerTime - return 0 - - def get_absolute_url(self) -> str: - """Get the video absolute URL.""" - return reverse("video:video", args=[str(self.slug)]) - - def get_full_url(self, request=None) -> str: - """Get the video full URL.""" - full_url = "".join( - ["//", get_current_site(request).domain, self.get_absolute_url()] - ) - return full_url - - def get_hashkey(self) -> str: - return hashlib.sha256( - ("%s-%s" % (SECRET_KEY, self.id)).encode("utf-8") - ).hexdigest() - - def delete(self, *args, **kwargs) -> None: - """Delete the current video file and db object.""" - # use pre delete and post delete signal to remove file used by video - # see above - super(Video, self).delete(*args, **kwargs) - - def get_playlist_master(self): - playlist_video = self.playlistvideo_set.filter( - name="playlist", encoding_format="application/x-mpegURL" - ).first() - return playlist_video - - def get_video_m4a(self): - """Get the audio (m4a) version of the video.""" - encoding_audio = self.encodingaudio_set.filter( - name="audio", encoding_format="video/mp4" - ).first() - return encoding_audio - - def get_video_mp3(self): - """Get the audio (mp3) version of the video.""" - encoding_audio = self.encodingaudio_set.filter( - name="audio", encoding_format="audio/mp3" - ).first() - return encoding_audio - - def get_video_mp4(self): - """Get the mp4 version of the video.""" - return self.encodingvideo_set.filter(encoding_format="video/mp4") - - def get_video_json(self, extensions): - """Get the JSON representation of the video.""" - extension_list = extensions.split(",") if extensions else [] - list_video = self.encodingvideo_set.all() - dict_src = Video.get_media_json(extension_list, list_video) - sorted_dict_src = { - x: sorted(dict_src[x], key=lambda i: i["height"]) for x in dict_src.keys() - } - return sorted_dict_src - - def get_video_mp4_json(self) -> list: - """Get the JSON representation of the MP4 video.""" - list_mp4 = self.get_video_json(extensions="mp4") - return list_mp4["mp4"] if list_mp4.get("mp4") else [] - - def get_audio_json(self, extensions) -> dict: - """Get the JSON representation of the audio.""" - extension_list = extensions.split(",") if extensions else [] - list_audio = self.encodingaudio_set.filter(name="audio") - dict_src = Video.get_media_json(extension_list, list_audio) - return dict_src - - def get_audio_and_video_json(self, extensions) -> dict: - """Get the JSON representation of the video and audio.""" - return { - **self.get_video_json(extensions), - **self.get_audio_json(extensions), - } - - @staticmethod - def get_media_json(extension_list, list_video) -> dict: - dict_src = {} - for media in list_video: - file_extension = splitext(media.source_file.url)[-1] - if not extension_list or file_extension[1:] in extension_list: - media_height = None - if hasattr(media, "height"): - media_height = media.height - video_object = { - "id": media.id, - "type": media.encoding_format, - "src": media.source_file.url, - "height": media_height, - "extension": file_extension, - "label": media.name, - } - dict_entry = dict_src.get(file_extension[1:], None) - if dict_entry is None: - dict_src[file_extension[1:]] = [video_object] - else: - dict_entry.append(video_object) - return dict_src - - def get_json_to_index(self) -> str: - try: - current_site = Site.objects.get_current() - data_to_dump = { - "id": self.id, - "title": "%s" % self.title, - "owner": "%s" % self.owner.username, - "owner_full_name": "%s" % self.owner.get_full_name(), - "date_added": ( - "%s" % self.date_added.strftime("%Y-%m-%dT%H:%M:%S") - if self.date_added - else None - ), - "date_evt": ( - "%s" % self.date_evt.strftime("%Y-%m-%dT%H:%M:%S") - if self.date_evt - else None - ), - "description": "%s" % self.description, - "thumbnail": "%s" % self.get_thumbnail_url(), - "duration": "%s" % self.duration, - "tags": list( - [ - {"name": name[0], "slug": slugify(name)} - for name in Tag.objects.get_for_object(self).values_list("name") - ] - ), - "type": {"title": self.type.title, "slug": self.type.slug}, - "disciplines": list( - self.discipline.all() - .filter(site=current_site) - .values("title", "slug") - ), - "channels": list( - self.channel.all().filter(site=current_site).values("title", "slug") - ), - "themes": list(self.theme.all().values("title", "slug")), - "contributors": list(self.contributor_set.values("name", "role")), - "chapters": list(self.chapter_set.values("title", "slug")), - "overlays": list(self.overlay_set.values("title", "slug")), - "full_url": self.get_full_url(), - "is_restricted": self.is_restricted, - "password": True if self.password != "" else False, - "duration_in_time": self.duration_in_time, - "mediatype": "video" if self.is_video else "audio", - "cursus": "%s" % __CURSUS_CODES_DICT__[self.cursus], - "main_lang": "%s" % __LANG_CHOICES_DICT__[self.main_lang], - } - return json.dumps(data_to_dump) - except ObjectDoesNotExist as e: - logger.error( - "An error occured during get_json_to_index" - " for video %s: %s" % (self.id, e) - ) - return json.dumps({}) - - def get_json_to_video_view(video, other_data_to_dump) -> str: - try: - video_src = {} - if video.is_video: - video_src["mp4"] = video.get_video_mp4_json() - m3u8 = video.get_playlist_master() - if m3u8: - video_src["m3u8"] = { - "src": m3u8.source_file.url, - "type": m3u8.encoding_format, - } - else: - m4a = video.get_video_m4a() - video_src["m4a"] = { - "src": m4a.source_file.url, - "type": m4a.encoding_format, - } - - list_track = list( - map( - lambda t: { - "id": t.id, - "kind": t.kind, - "src": t.src.file.url, - "srclang": t.lang, - "label": t.get_label_lang(), - }, - video.track_set.all(), - ), - ) - list_overlay = list( - map( - lambda o: { - "start": o.time_start, - "end": o.time_end, - "content": o.content, - "align": o.position, - "showBackground": o.background, - }, - video.overlay_set.all(), - ), - ) - list_chapter = list( - map( - lambda c: {"time_start": c.time_start, "id": c.id, "title": c.title}, - video.chapter_set.all(), - ), - ) - overview = video.overview.url if (video.overview) else "" - data_to_dump = { - "status": "ok", - "version": video.get_version, - "slug": video.slug, - "title": video.title, - "tracks": list_track, - "is_video": video.is_video, - "src": video_src, - "is_360": video.is_360, - "thumbnail": video.get_thumbnail_url(), - "overview": overview, - "overlay": list_overlay, - "chapter": list_chapter, - } - if hasattr(video, "enrichmentvtt"): - data_to_dump["enrichtracksrc"] = video.enrichmentvtt.src.file.url - for k in other_data_to_dump: - data_to_dump[k] = other_data_to_dump[k] - return json.dumps(data_to_dump) - except ObjectDoesNotExist as e: - logger.error( - "An error occured during get_json_to_video_view" - " for video %s: %s" % (video.id, e) - ) - return json.dumps({}) - - def get_main_lang(self) -> str: - return "%s" % __LANG_CHOICES_DICT__[self.main_lang] - - def get_cursus(self) -> str: - return "%s" % __CURSUS_CODES_DICT__[self.cursus] - - def get_licence(self) -> str: - return "%s" % __LICENCE_CHOICES_DICT__[self.licence] - - def get_dublin_core(self) -> dict: - """Export Dublin Core items for current video.""" - contributors = [] - current_site = Site.objects.get_current() - for contrib in self.contributor_set.values_list("name", "role"): - contributors.append(" ".join(contrib)) - try: - data_to_dump = { - "dc.title": "%s" % escape(self.title), - "dc.creator": "%s" % self.owner.get_full_name(), - "dc.description": "%s" % escape(self.description), - "dc.subject": "%s, %s" - % ( - self.type.title, - ", ".join( - self.discipline.all() - .filter(site=current_site) - .values_list("title", flat=True) - ), - ), - "dc.publisher": __TITLE_ETB__, - "dc.contributor": ", ".join(contributors), - "dc.date": "%s" % self.date_added.strftime("%Y/%m/%d"), - "dc.type": "video" if self.is_video else "audio", - "dc.identifier": "http:%s" % self.get_full_url(), - "dc.language": "%s" % self.main_lang, - "dc.coverage": DEFAULT_DC_COVERAGE, - "dc.rights": self.licence if (self.licence) else DEFAULT_DC_RIGHTS, - "dc.format": "video/mp4" if self.is_video else "audio/mp3", - } - return data_to_dump - except ObjectDoesNotExist as e: - logger.error( - "An error occured during get_dublin_core" - " for video %s: %s" % (self.id, e) - ) - return {} - - def get_or_create_video_folder(self): - """Get or create a UserFolder associated to current video.""" - if USE_PODFILE: - videodir, created = UserFolder.objects.get_or_create( - name=self.slug, owner=self.owner - ) - return videodir - - def update_additional_owners_rights(self) -> None: - """Update folder rights for additional video owners.""" - if USE_PODFILE: - try: - # First, search for exact Userfolder match - videodir = UserFolder.objects.get(name="%s" % self.slug, owner=self.owner) - # Ensure all additional users will get access to this folder - videodir.users.set(self.additional_owners.all()) - videodir.save() - except UserFolder.DoesNotExist: - # Search for Userfolder previously associated to current vid - # (if vid changed slug or owner) - slugid = "%04d-" % self.id - videodirs = UserFolder.objects.filter(name__startswith=slugid) - if videodirs.all().count() > 0: - # We found a match - videodir = videodirs.first() - # Re-sync folder metadata with video - videodir.users.set(self.additional_owners.all()) - videodir.name = self.slug - videodir.owner = self.owner - videodir.save() - - -class UpdateOwner(models.Model): - class Meta: - verbose_name = _("Update Owner") - verbose_name_plural = _("Update Owners") - - -@receiver(post_save, sender=Video) -def default_site(sender, instance, created: bool, **kwargs) -> None: - if instance.sites.count() == 0: - instance.sites.add(Site.objects.get_current()) - - -@receiver(pre_delete, sender=Video, dispatch_uid="pre_delete-video_files_removal") -def video_files_removal(sender, instance, using, **kwargs) -> None: - """Remove files created after encoding.""" - remove_video_file(instance) - - models_to_delete = [ - instance.encodingvideo_set.model, - instance.encodingaudio_set.model, - instance.playlistvideo_set.model, - ] - for model in models_to_delete: - previous_encoding_video = model.objects.filter(video=instance) - if len(previous_encoding_video) > 0: - for encoding in previous_encoding_video: - encoding.delete() - - -def remove_video_file(video: Video) -> None: - """Remove video file linked to video.""" - if video.overview: - image_overview = os.path.join( - os.path.dirname(video.overview.path), "overview.png" - ) - if os.path.isfile(image_overview): - os.remove(image_overview) - video.overview.delete() - - log_file = os.path.join( - os.path.dirname(video.video.path), "%04d" % video.id, "info_video.json" - ) - if os.path.isfile(log_file): - os.remove(log_file) - - -@receiver(post_delete, sender=Video, dispatch_uid="post_delete-video_podfiles_removal") -def video_podfiles_removal(sender, instance, **kwargs) -> None: - """Delete UserFolder associated to current video.""" - if instance.video and os.path.isfile(instance.video.path): - os.remove(instance.video.path) - video_dir = os.path.join(os.path.dirname(instance.video.path), "%04d" % instance.id) - if os.path.isdir(video_dir) and not os.listdir(video_dir): - os.rmdir(video_dir) - if USE_PODFILE: - video_folder = UserFolder.objects.filter( - name=instance.slug, - owner=instance.owner, - ) - if video_folder: - video_folder[0].delete() - - -class ViewCount(models.Model): - video = models.ForeignKey( - Video, verbose_name=_("Video"), editable=False, on_delete=models.CASCADE - ) - date = models.DateField(_("Date"), default=date.today, editable=False) - count = models.IntegerField(_("Number of view"), default=0, editable=False) - - @property - def sites(self): - return self.video.sites - - class Meta: - unique_together = ("video", "date") - verbose_name = _("View count") - verbose_name_plural = _("View counts") - - -class UserMarkerTime(models.Model): - """Record the time of video played by a user.""" - - video = models.ForeignKey( - Video, verbose_name=_("Video"), editable=False, on_delete=models.CASCADE - ) - user = models.ForeignKey( - User, verbose_name=_("User"), editable=False, on_delete=models.CASCADE - ) - markerTime = models.IntegerField(_("Marker time"), default=0, editable=False) - - @property - def sites(self): - """Return the sites of the video.""" - return self.video.sites - - def __str__(self) -> str: - """Render the user marker time as string.""" - return "Marker time for user %s and video %s: %s" % ( - self.user, - self.video, - self.markerTime, - ) - - class Meta: - unique_together = ("video", "user") - verbose_name = _("User viewing time marker of video") - verbose_name_plural = _("Users viewing time marker of video") - - -class VideoVersion(models.Model): - video = models.OneToOneField( - Video, verbose_name=_("Video"), editable=False, on_delete=models.CASCADE - ) - version = models.CharField( - _("Video version"), - max_length=1, - blank=True, - choices=__VERSION_CHOICES__, - default="O", - help_text=_("Video default version."), - ) - - class Meta: - verbose_name = _("Video version") - verbose_name_plural = _("Video versions") - - @property - def sites(self): - return self.video.sites - - @property - def sites_all(self): - return self.video.sites_set.all() - - def __str__(self) -> str: - """Render the video version as string.""" - return "Choice for default video version: %s - %s" % ( - self.video.id, - self.version, - ) - - -class Notes(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE) - video = models.ForeignKey(Video, on_delete=models.CASCADE) - note = models.TextField(_("Note"), null=True, blank=True) - - @property - def sites(self): - return self.video.sites - - @property - def sites_all(self): - return self.video.sites_set.all() - - class Meta: - """Note Metadata.""" - - verbose_name = _("Note") - verbose_name_plural = _("Notes") - unique_together = ("video", "user") - - def __str__(self) -> str: - """Render the Note as string.""" - return "%s-%s" % (self.user.username, self.video) - - -class AdvancedNotes(models.Model): - """Advanced video notes Model.""" - - user = models.ForeignKey(User, on_delete=models.CASCADE) - video = models.ForeignKey(Video, on_delete=models.CASCADE) - status = models.CharField( - _("Availability level"), - max_length=1, - choices=NOTES_STATUS, - default="0", - help_text=_("Select an availability level for the note."), - ) - note = models.TextField(_("Note"), null=True, blank=True) - timestamp = models.IntegerField(_("Timestamp"), null=True, blank=True) - added_on = models.DateTimeField(_("Date added"), default=timezone.now) - modified_on = models.DateTimeField(_("Date modified"), default=timezone.now) - - class Meta: - """AdvancedNotes Metadata.""" - - verbose_name = _("Advanced Note") - verbose_name_plural = _("Advanced Notes") - unique_together = ("video", "user", "timestamp", "status") - - @property - def sites(self): - return self.video.sites - - @property - def sites_all(self): - return self.video.sites_set.all() - - def __str__(self) -> str: - """Render the advanced note as string.""" - return "%s-%s-%s" % (self.user.username, self.video, self.timestamp) - - def clean(self) -> None: - """Validate AdvancedNotes fields.""" - if not self.note: - raise ValidationError( - AdvancedNotes._meta.get_field("note").help_text, code="invalid_note" - ) - if not self.status or self.status not in dict(NOTES_STATUS): - raise ValidationError( - AdvancedNotes._meta.get_field("status").help_text, code="invalid_status" - ) - if ( - self.timestamp is None - or self.timestamp < 0 - or (self.video.duration and self.timestamp > self.video.duration) - ): - raise ValidationError( - AdvancedNotes._meta.get_field("timestamp").help_text, - code="invalid_timestamp", - ) - - def timestampstr(self) -> str: - if self.timestamp is None: - return "--:--:--" - seconds = int(self.timestamp) - hours = int(seconds / 3600) - seconds -= hours * 3600 - minutes = int(seconds / 60) - seconds -= minutes * 60 - hours = "0" + str(hours) if hours < 10 else str(hours) - minutes = "0" + str(minutes) if minutes < 10 else str(minutes) - seconds = "0" + str(seconds) if seconds < 10 else str(seconds) - return hours + ":" + minutes + ":" + seconds - - -class NoteComments(models.Model): - """Comments for Advanced video notes model.""" - - user = models.ForeignKey(User, on_delete=models.CASCADE) - parentNote = models.ForeignKey(AdvancedNotes, on_delete=models.CASCADE) - parentCom = models.ForeignKey( - "NoteComments", blank=True, null=True, on_delete=models.CASCADE - ) - status = models.CharField( - _("Availability level"), - max_length=1, - choices=NOTES_STATUS, - default="0", - help_text=_("Select an availability level for the comment."), - ) - comment = models.TextField(_("Comment"), null=True, blank=True) - added_on = models.DateTimeField(_("Date added"), default=timezone.now) - modified_on = models.DateTimeField(_("Date modified"), default=timezone.now) - - class Meta: - verbose_name = _("Note comment") - verbose_name_plural = _("Note comments") - - def __str__(self) -> str: - """Render the note comment as string.""" - return "%s-%s-%s" % (self.user.username, self.parentNote, self.comment) - - def clean(self) -> None: - """Validate NoteComments fields.""" - if not self.comment: - raise ValidationError( - NoteComments._meta.get_field("comment").help_text, code="invalid_comment" - ) - if not self.status or self.status not in dict(NOTES_STATUS): - raise ValidationError( - NoteComments._meta.get_field("status").help_text, code="invalid_status" - ) - - -class VideoToDelete(models.Model): - date_deletion = models.DateField( - _("Date for deletion"), default=date.today, unique=True - ) - video = models.ManyToManyField(Video, verbose_name=_("Videos")) - - class Meta: - verbose_name = _("Video to delete") - verbose_name_plural = _("Videos to delete") - - def __str__(self) -> str: - """Render the video to delete as string.""" - return "%s - nb videos: %s" % (self.date_deletion, self.video.count()) - - -class Comment(models.Model): - """Video comment model.""" - - author = models.ForeignKey(User, on_delete=models.CASCADE) - content = models.TextField() - parent = models.ForeignKey( - "self", - related_name="children", - null=True, - blank=True, - on_delete=models.CASCADE, - ) - direct_parent = models.ForeignKey( - "self", - related_name="direct_children", - null=True, - blank=True, - on_delete=models.CASCADE, - ) - video = models.ForeignKey(Video, on_delete=models.CASCADE) - added = models.DateTimeField(auto_now_add=True) - - class Meta: - """Video comment metadata.""" - - verbose_name = _("Comment") - verbose_name_plural = _("Comments") - - @property - def number_vote(self) -> None: - self.vote_set.all().count() - - @property - def get_children(self): - return Comment.objects.filter(parent_id=self.id).order_by("id") - - def get_json_children(self, user_id) -> list: - return list( - self.get_children.annotate(nbr_vote=Count("vote", distinct=True)) - .annotate( - author_name=Concat("author__first_name", Value(" "), "author__last_name") - ) - .annotate( - is_owner=Case( - When(author__id=user_id, then=Value(True)), - default=Value(False), - output_field=BooleanField(), - ) - ) - .values( - "id", - "parent__id", - "direct_parent__id", - "is_owner", - "author_name", - "added", - "content", - "nbr_vote", - ) - ) - - def __str__(self) -> str: - """Render the comment as string.""" - return self.content - - -class Vote(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE) - comment = models.ForeignKey(Comment, on_delete=models.CASCADE) - - class Meta: - verbose_name = _("Vote") - verbose_name_plural = _("Votes") - - def __str__(self) -> str: - """Render the vote as string.""" - return str(self.user) - - -class Category(models.Model): - """Video category Model.""" - - title = models.CharField( - _("Category title"), - max_length=100, - help_text=_( - "Please choose a title as short and accurate as " - "possible, reflecting the main subject / context " - "of the content. (max length: 100 characters)" - ), - ) - owner = models.ForeignKey(User, on_delete=models.CASCADE) - video = models.ManyToManyField( - Video, - verbose_name=_("Videos"), - blank=True, - help_text=_( - 'Hold down "Control", or "Command" on a Mac, to select more than one.' - ), - ) - slug = models.SlugField( - _("Slug"), - unique=True, - max_length=110, - help_text=_( - 'Used to access this instance, the "slug" is a short label ' - + "containing only letters, numbers, underscore or dash top." - ), - editable=False, - ) - - def save(self, *args, **kwargs) -> None: - """Set a slug and save the category instance.""" - self.slug = "%s-%s" % (self.owner.id, slugify(self.title)) - super(Category, self).save(*args, **kwargs) - - def __str__(self) -> str: - """Render the category as string.""" - return self.title - - class Meta: - """Category Metadata.""" - - ordering = ["title", "id"] - verbose_name = _("Category") - verbose_name_plural = _("Categories") - - -class VideoAccessToken(models.Model): - """Video access token model.""" - - video = models.ForeignKey(Video, on_delete=models.CASCADE) - token = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - name = models.CharField( - verbose_name=_("Token name"), max_length=100, blank=True, default=_("Change me!") - ) - - class Meta: - """Video access token Metadata.""" - - ordering = ["video", "token"] - verbose_name = _("Video access token") - verbose_name_plural = _("Video access tokens") - unique_together = ["video", "token"] +"""Esup-Pod Video models.""" + +import os +import re +import time +import uuid +import unicodedata +import json +import logging +import hashlib + +from django.db import models +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import get_language +from django.template.defaultfilters import slugify +from django.db.models import Sum +from django.contrib.auth.models import User +from django.urls import reverse +from django.core.exceptions import ValidationError +from django.core.exceptions import ObjectDoesNotExist +from django.contrib.sites.shortcuts import get_current_site +from django.templatetags.static import static +from django.dispatch import receiver +from django.db.models.signals import pre_delete, post_delete + +# from tagging.models import Tag +from tagulous.models import TagField +from datetime import date +from django.utils import timezone +from django.utils.html import format_html, escape +from django.utils.text import capfirst +from ckeditor.fields import RichTextField +from django.contrib.sites.models import Site +from django.db.models.signals import post_save +from django.db.models.signals import pre_save +from pod.main.models import AdditionalChannelTab +import importlib +from django.contrib.auth.hashers import make_password +from pod.main.context_processors import WEBTV_MODE + +from sorl.thumbnail import get_thumbnail +from pod.authentication.models import AccessGroup +from pod.main.models import get_nextautoincrement +from pod.main.lang_settings import ALL_LANG_CHOICES as __ALL_LANG_CHOICES__ +from pod.main.lang_settings import PREF_LANG_CHOICES as __PREF_LANG_CHOICES__ +from django.db.models import Count, Case, When, Value, BooleanField, Q +from django.db.models.functions import Concat +from os.path import splitext + +USE_PODFILE = getattr(settings, "USE_PODFILE", False) +if USE_PODFILE: + from pod.podfile.models import CustomImageModel + from pod.podfile.models import UserFolder +else: + from pod.main.models import CustomImageModel + +logger = logging.getLogger(__name__) + +RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY = getattr( + settings, "RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY", False +) +VIDEO_RECENT_VIEWCOUNT = getattr(settings, "VIDEO_RECENT_VIEWCOUNT", 180) +VIDEOS_DIR = getattr(settings, "VIDEOS_DIR", "videos") +SITE_ID = getattr(settings, "SITE_ID", 1) + +LANG_CHOICES = getattr( + settings, + "LANG_CHOICES", + ( + (_("-- Frequently used languages --"), __PREF_LANG_CHOICES__), + (_("-- All languages --"), __ALL_LANG_CHOICES__), + ), +) + +CURSUS_CODES = getattr( + settings, + "CURSUS_CODES", + ( + ("0", _("None / All")), + ("L", _("Bachelor’s Degree")), + ("M", _("Master’s Degree")), + ("D", _("Doctorate")), + ("1", _("Other")), + ), +) + +__LANG_CHOICES_DICT__ = { + key: value for key, value in LANG_CHOICES[0][1] + LANG_CHOICES[1][1] +} +__CURSUS_CODES_DICT__ = {key: value for key, value in CURSUS_CODES} + +DEFAULT_TYPE_ID = getattr(settings, "DEFAULT_TYPE_ID", 1) +LICENCE_CHOICES = getattr( + settings, + "LICENCE_CHOICES", + ( + ("by", _("Attribution 4.0 International (CC BY 4.0)")), + ( + "by-nd", + _("Attribution-NoDerivatives 4.0 International (CC BY-ND 4.0)"), + ), + ( + "by-nc-nd", + _( + "Attribution-NonCommercial-NoDerivatives 4.0 " + "International (CC BY-NC-ND 4.0)" + ), + ), + ( + "by-nc", + _("Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)"), + ), + ( + "by-nc-sa", + _( + "Attribution-NonCommercial-ShareAlike 4.0 " + "International (CC BY-NC-SA 4.0)" + ), + ), + ("by-sa", _("Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)")), + ), +) +__LICENCE_CHOICES_DICT__ = {key: value for key, value in LICENCE_CHOICES} +FORMAT_CHOICES = getattr( + settings, + "FORMAT_CHOICES", + ( + ("video/mp4", "video/mp4"), + ("video/mp2t", "video/mp2t"), + ("video/webm", "video/webm"), + ("audio/mp3", "audio/mp3"), + ("audio/wav", "audio/wav"), + ("application/x-mpegURL", "application/x-mpegURL"), + ), +) +ENCODING_CHOICES = getattr( + settings, + "ENCODING_CHOICES", + ( + ("audio", "audio"), + ("360p", "360p"), + ("480p", "480p"), + ("720p", "720p"), + ("1080p", "1080p"), + ("playlist", "playlist"), + ), +) +DEFAULT_THUMBNAIL = getattr(settings, "DEFAULT_THUMBNAIL", "img/default.svg") +SECRET_KEY = getattr(settings, "SECRET_KEY", "") + +NOTES_STATUS = getattr( + settings, + "NOTES_STATUS", + ( + ("0", _("Private (me only)")), + ("1", _("Shared with video owner")), + ("2", _("Public")), + ), +) + +THIRD_PARTY_APPS = getattr(settings, "THIRD_PARTY_APPS", []) + +__THIRD_PARTY_APPS_CHOICES__ = THIRD_PARTY_APPS.copy() +( + __THIRD_PARTY_APPS_CHOICES__.remove("live") + if ("live" in __THIRD_PARTY_APPS_CHOICES__) + else __THIRD_PARTY_APPS_CHOICES__ +) +__THIRD_PARTY_APPS_CHOICES__.insert(0, "Original") + +__VERSION_CHOICES__ = [ + (app.capitalize()[0], _("%(app)s version" % {"app": app.capitalize()})) + for app in __THIRD_PARTY_APPS_CHOICES__ +] + +__VERSION_CHOICES_DICT__ = {key: value for key, value in __VERSION_CHOICES__} + +## +# Settings exposed in templates +# +TEMPLATE_VISIBLE_SETTINGS = getattr( + settings, + "TEMPLATE_VISIBLE_SETTINGS", + { + "TITLE_SITE": "Pod", + "TITLE_ETB": "University name", + "LOGO_SITE": "img/logoPod.svg", + "LOGO_ETB": "img/esup-pod.svg", + "LOGO_PLAYER": "img/pod_favicon.svg", + "LINK_PLAYER": "", + "LINK_PLAYER_NAME": _("Home"), + "FOOTER_TEXT": ("",), + "FAVICON": "img/pod_favicon.svg", + "CSS_OVERRIDE": "", + "PRE_HEADER_TEMPLATE": "", + "POST_FOOTER_TEMPLATE": "", + "TRACKING_TEMPLATE": "", + }, +) +__TITLE_ETB__ = ( + TEMPLATE_VISIBLE_SETTINGS["TITLE_ETB"] + if (TEMPLATE_VISIBLE_SETTINGS.get("TITLE_ETB")) + else "University name" +) +DEFAULT_DC_COVERAGE = getattr( + settings, "DEFAULT_DC_COVERAGE", __TITLE_ETB__ + " - Town - Country" +) +DEFAULT_DC_RIGHTS = getattr(settings, "DEFAULT_DC_RIGHT", "BY-NC-SA") + +DEFAULT_YEAR_DATE_DELETE = getattr(settings, "DEFAULT_YEAR_DATE_DELETE", 2) + + +USE_TRANSCRIPTION = getattr(settings, "USE_TRANSCRIPTION", False) +if USE_TRANSCRIPTION: + TRANSCRIPTION_MODEL_PARAM = getattr(settings, "TRANSCRIPTION_MODEL_PARAM", {}) + TRANSCRIPTION_TYPE = getattr(settings, "TRANSCRIPTION_TYPE", "STT") + +# FUNCTIONS + + +def get_transcription_choices() -> list: + if USE_TRANSCRIPTION: + transcript_lang = TRANSCRIPTION_MODEL_PARAM.get(TRANSCRIPTION_TYPE, {}).keys() + transcript_choices_lang = [] + for lang in transcript_lang: + transcript_choices_lang.append((lang, __LANG_CHOICES_DICT__[lang])) + return transcript_choices_lang + else: + return [] + + +def default_date_delete() -> date: + """Get the default deletion date.""" + return date.today() + timezone.timedelta(days=DEFAULT_YEAR_DATE_DELETE * 365) + + +def select_video_owner(): + if RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY: + return lambda q: ( + Q(is_staff=True) & (Q(first_name__icontains=q) | Q(last_name__icontains=q)) + ) & Q(owner__sites=Site.objects.get_current()) + else: + return lambda q: (Q(first_name__icontains=q) | Q(last_name__icontains=q)) & Q( + owner__sites=Site.objects.get_current() + ) + + +def remove_accents(input_str) -> str: + """Remove diacritics in input string.""" + nkfd_form = unicodedata.normalize("NFKD", input_str) + return "".join([c for c in nkfd_form if not unicodedata.combining(c)]) + + +def get_storage_path_video(instance, filename) -> str: + """Get the video storage path. + + Instance needs to implement owner + """ + fname, dot, extension = filename.rpartition(".") + try: + fname.index("/") + return os.path.join( + VIDEOS_DIR, + instance.owner.owner.hashkey, + os.path.dirname(fname), + "%s.%s" + % ( + slugify(os.path.basename(fname)), + extension, + ), + ) + except ValueError: + return os.path.join( + VIDEOS_DIR, + instance.owner.owner.hashkey, + "%s.%s" % (slugify(fname), extension), + ) + + +# MODELS + + +class Channel(models.Model): + """Class describing Channels objects.""" + + title = models.CharField( + _("Title"), + max_length=100, + unique=True, + help_text=_( + "Please choose a title as short and accurate as " + "possible, reflecting the main subject / context " + "of the content.(max length: 100 characters)" + ), + ) + slug = models.SlugField( + _("Slug"), + unique=True, + max_length=100, + help_text=_( + 'Used to access this instance, the "slug" is a short label ' + + "containing only letters, numbers, underscore or dash top." + ), + editable=False, + ) + description = RichTextField( + _("Description"), + config_name="complete", + blank=True, + help_text=_( + "In this field you can describe your content, " + "add all needed related information, and " + "format the result using the toolbar." + ), + ) + headband = models.ForeignKey( + CustomImageModel, + models.SET_NULL, + blank=True, + null=True, + verbose_name=_("Headband"), + ) + color = models.CharField( + _("Background color"), + max_length=10, + blank=True, + null=True, + help_text=_( + "The background color for your channel. " + "You can use the format #. i.e.: #ff0000 for red" + ), + ) + style = models.TextField( + _("Extra style"), + null=True, + blank=True, + help_text=_("The style will be added to your channel to show it"), + ) + owners = models.ManyToManyField( + User, + related_name="owners_channels", + verbose_name=_("Owners"), + blank=True, + ) + users = models.ManyToManyField( + User, + related_name="users_channels", + verbose_name=_("Users"), + blank=True, + ) + visible = models.BooleanField( + verbose_name=_("Visible"), + help_text=_( + "If checked, the channel appear in a list of available " + + "channels on the platform." + ), + default=False, + ) + allow_to_groups = models.ManyToManyField( + AccessGroup, + blank=True, + verbose_name=_("Groups"), + help_text=_("One or more groups who can upload video to this channel."), + ) + add_channels_tab = models.ManyToManyField( + AdditionalChannelTab, verbose_name=_("Additionals channels tab"), blank=True + ) + site = models.ForeignKey( + Site, verbose_name=_("Site"), on_delete=models.CASCADE, default=SITE_ID + ) + + class Meta: + """Metadata subclass for Channel object.""" + + ordering = ["title"] + verbose_name = _("Channel") + verbose_name_plural = _("Channels") + constraints = [ + models.UniqueConstraint( + fields=["slug", "site"], name="channel_unique_slug_site" + ) + ] + + def __str__(self) -> str: + """Display a channel object as string.""" + return "%s" % (self.title) + + def get_absolute_url(self) -> str: + """Return channel absolute URL.""" + return reverse("channel-video:channel", args=[str(self.slug)]) + + def get_all_theme(self) -> list: + """Return the list of all child themes in current channel.""" + list_theme = [] + themes = self.themes.filter(parentId=None).order_by("title") + for theme in themes: + list_theme.append( + { + "id": theme.id, + "title": "%s" % theme.title, + "slug": "%s" % theme.slug, + "url": "%s" % theme.get_absolute_url(), + "child": theme.get_all_children_tree(), + } + ) + return list_theme + + def get_all_theme_json(self) -> str: + """Return theme list in json format.""" + return json.dumps(self.get_all_theme()) + + def save(self, *args, **kwargs) -> None: + """Store the channel object in db.""" + self.slug = slugify(self.title) + super(Channel, self).save(*args, **kwargs) + + +@receiver(pre_save, sender=Channel) +def default_site_channel(sender, instance, **kwargs) -> None: + if not hasattr(instance, "site"): + instance.site = Site.objects.get_current() + + +class Theme(models.Model): + """Class describing a them object. + + A theme is a child of channel or another theme object. + """ + + parentId = models.ForeignKey( + "self", + null=True, + blank=True, + related_name="children", + on_delete=models.CASCADE, + verbose_name=_("Theme parent"), + ) + title = models.CharField( + _("Title"), + max_length=100, + help_text=_( + "Please choose a title as short and accurate as " + "possible, reflecting the main subject / context " + "of the content.(max length: 100 characters)" + ), + ) + slug = models.SlugField( + _("Slug"), + max_length=100, + help_text=_( + 'Used to access this instance, the "slug" is a short label ' + + "containing only letters, numbers, underscore or dash top." + ), + editable=False, + ) + description = models.TextField( + _("Description"), + null=True, + blank=True, + help_text=_( + "In this field you can describe your content, " + "add all needed related information, and " + "format the result using the toolbar." + ), + ) + + headband = models.ForeignKey( + CustomImageModel, + models.SET_NULL, + blank=True, + null=True, + verbose_name=_("Headband"), + ) + + channel = models.ForeignKey( + "Channel", + related_name="themes", + verbose_name=_("Channel"), + on_delete=models.CASCADE, + ) + + @property + def site(self): + """Return sites associated to parent channel.""" + return self.channel.site + + def __str__(self) -> str: + """Display current theme as string.""" + return "%s: %s" % (self.channel.title, self.title) + + def get_absolute_url(self) -> str: + """Get current theme absolute URL.""" + return reverse( + "channel-video:theme", args=[str(self.channel.slug), str(self.slug)] + ) + + def save(self, *args, **kwargs) -> None: + """Store current theme object in db.""" + self.slug = slugify(self.title) + super(Theme, self).save(*args, **kwargs) + + def get_all_children_tree(self) -> list: + """Get a tree of all theme children.""" + children = [] + try: + child_list = self.children.all().order_by("title") + except AttributeError: + return children + for child in child_list: + children.append( + { + "id": child.id, + "title": "%s" % child.title, + "slug": "%s" % child.slug, + "url": "%s" % child.get_absolute_url(), + "child": child.get_all_children_tree(), + } + ) + return children + + def get_all_children_flat(self): + """Get a flat list of all theme children.""" + children = [self] + try: + child_list = self.children.all() + except ValueError: + # ValueError: 'Theme' instance needs to have a + # primary key value before this relationship can be used. + return children + for child in child_list: + children.extend(child.get_all_children_flat()) + return children + + def get_all_children_tree_json(self) -> str: + """Get a json tree of all theme children.""" + return json.dumps(self.get_all_children_tree()) + + def get_all_parents(self): + """Get a list of all theme parents.""" + parents = [self] + if self.parentId is not None: + parent = self.parentId + parents.extend(parent.get_all_parents()) + return parents + + def clean(self) -> None: + """Validate Theme fields.""" + # Dans le cas oĂč on modifie un theme + if ( + Theme.objects.filter(channel=self.channel, slug=slugify(self.title)) + .exclude(pk=self.id) + .exists() + ): + raise ValidationError( + { + "title": ValidationError( + _("A theme with this name already exists in this channel."), + code="invalid_title", + ) + } + ) + + if self.parentId in self.get_all_children_flat(): + raise ValidationError( + { + "parentId": ValidationError( + _("A theme can’t have itself or one of it’s children as parent."), + code="invalid_parent", + ) + } + ) + if self.parentId and self.parentId not in self.channel.themes.all(): + raise ValidationError( + { + "parentId": ValidationError( + _("A theme must be in the same channel as its parent."), + code="invalid_channel", + ) + } + ) + + class Meta: + """Metadata subclass of theme object.""" + + ordering = ["channel", "title"] + verbose_name = _("Theme") + verbose_name_plural = _("Themes") + unique_together = ("channel", "slug") + + +class Type(models.Model): + """Define all video types available.""" + + title = models.CharField(_("Title"), max_length=100, unique=True) + slug = models.SlugField( + _("Slug"), + unique=True, + max_length=100, + help_text=_( + 'Used to access this instance, the "slug" is a short label ' + + "containing only letters, numbers, underscore or dash top." + ), + ) + description = models.TextField(null=True, blank=True) + icon = models.ForeignKey( + CustomImageModel, + models.SET_NULL, + blank=True, + null=True, + verbose_name=_("Icon"), + ) + sites = models.ManyToManyField(Site) + + def __str__(self) -> str: + return "%s" % (self.title) + + def save(self, *args, **kwargs) -> None: + """Store current Type in DB.""" + self.slug = slugify(self.title) + super(Type, self).save(*args, **kwargs) + + class Meta: + """Metadata subclass of Type object.""" + + ordering = ["title"] + verbose_name = _("Type") + verbose_name_plural = _("Types") + + +@receiver(post_save, sender=Type) +def default_site_type(sender, instance, created: bool, **kwargs) -> None: + if instance.sites.count() == 0: + instance.sites.add(Site.objects.get_current()) + + +class Discipline(models.Model): + title = models.CharField(_("title"), max_length=100, unique=True) + slug = models.SlugField( + _("slug"), + unique=True, + max_length=100, + help_text=_( + 'Used to access this instance, the "slug" is a short label ' + + "containing only letters, numbers, underscore or dash top." + ), + ) + description = models.TextField(null=True, blank=True) + icon = models.ForeignKey( + CustomImageModel, + models.SET_NULL, + blank=True, + null=True, + verbose_name=_("Icon"), + ) + site = models.ForeignKey( + Site, verbose_name=_("Site"), on_delete=models.CASCADE, default=SITE_ID + ) + + def __str__(self) -> str: + return "%s" % (self.title) + + def save(self, *args, **kwargs) -> None: + """Store current Discipline in DB.""" + self.slug = slugify(self.title) + super(Discipline, self).save(*args, **kwargs) + + class Meta: + """Metadata subclass of Discipline object.""" + + ordering = ["title"] + verbose_name = _("Discipline") + verbose_name_plural = _("Disciplines") + + +@receiver(pre_save, sender=Discipline) +def default_site_discipline(sender, instance, **kwargs) -> None: + if not hasattr(instance, "site"): + instance.site = Site.objects.get_current() + + +class Video(models.Model): + """Class describing video objects.""" + + video = models.FileField( + _("Video"), + upload_to=get_storage_path_video, + max_length=255, + help_text=_("You can send an audio or video file."), + null=WEBTV_MODE, + blank=WEBTV_MODE, + ) + title = models.CharField( + _("Title"), + max_length=250, + help_text=_( + "A title as short and accurate as " + "possible, reflecting the main subject / context " + "of the content. (max length: 250 characters)" + ), + ) + slug = models.SlugField( + _("Slug"), + unique=True, + max_length=255, + help_text=_( + 'Used to access this instance, the "slug" is ' + "a short label containing only letters, " + "numbers, underscore or dash top." + ), + editable=False, + ) + sites = models.ManyToManyField(Site) + type = models.ForeignKey( + Type, + verbose_name=_("Type"), + on_delete=models.CASCADE, + help_text=_("The general type of the video."), + ) + owner = models.ForeignKey(User, verbose_name=_("Owner"), on_delete=models.CASCADE) + additional_owners = models.ManyToManyField( + User, + blank=True, + verbose_name=_("Additional owners"), + related_name="owners_videos", + help_text=_( + "Additional owners will have the same rights as you, except " + + "that they can’t delete this media." + ), + ) + description = RichTextField( + _("Description"), + config_name="complete", + blank=True, + help_text=_( + "Describe your content, add all needed related information, " + + "and format the result using the toolbar." + ), + ) + date_added = models.DateTimeField(_("Date added"), default=timezone.now) + date_evt = models.DateField( + _("Date of event"), default=date.today, blank=True, null=True + ) + cursus = models.CharField( + _("University course"), + max_length=1, + choices=CURSUS_CODES, + default="0", + help_text=_("Select an university course as audience target of the content."), + ) + main_lang = models.CharField( + _("Main language"), + max_length=2, + choices=LANG_CHOICES, + default=get_language(), + help_text=_("The main language used in the content."), + ) + transcript = models.CharField( + _("Transcript"), + max_length=2, + choices=get_transcription_choices(), + blank=True, + help_text=_("Select an available language to transcribe the audio."), + ) + tags = TagField( + help_text=_( + "Separate tags with spaces, " + "enclose the tags consist of several words in quotation marks." + ), + blank=True, + verbose_name=_("Tags"), + ) + discipline = models.ManyToManyField( + Discipline, + blank=True, + verbose_name=_("Disciplines"), + help_text=_("The disciplines to which your content belongs."), + ) + licence = models.CharField( + _("Licence"), + max_length=8, + choices=LICENCE_CHOICES, + blank=True, + null=True, + help_text=_("Usage rights granted to your content."), + ) + channel = models.ManyToManyField( + Channel, + verbose_name=_("Channels"), + blank=True, + help_text=_("The channel where you want your content to appear."), + ) + theme = models.ManyToManyField( + Theme, + verbose_name=_("Themes"), + blank=True, + help_text=_( + 'Hold down "Control", or "Command" on a Mac, to select more than one.' + ), + ) + allow_downloading = models.BooleanField( + _("allow downloading"), + default=False, + help_text=_("Check this box if you to allow downloading of the encoded files"), + ) + is_360 = models.BooleanField( + _("video 360"), + default=False, + help_text=_("Check this box if you want to use the 360 player for the video"), + ) + + is_draft = models.BooleanField( + verbose_name=_("Draft"), + help_text=_( + "If this box is checked, " + "the video will be visible and accessible only by you " + "and the additional owners." + ), + default=True, + ) + is_restricted = models.BooleanField( + verbose_name=_("Authentication restricted access"), + help_text=_( + "If this box is checked, " + "the video will only be accessible to authenticated users." + ), + default=False, + ) + restrict_access_to_groups = models.ManyToManyField( + AccessGroup, + blank=True, + verbose_name=_("Groups"), + help_text=_("One or more groups who can access to this video"), + ) + password = models.CharField( + _("password"), + help_text=_("Viewing this video will not be possible without this password."), + max_length=50, + blank=True, + null=True, + ) + order = models.PositiveSmallIntegerField( + _("order"), + help_text=_("Order videos in channels or themes."), + default=1, + blank=True, + null=True, + ) + thumbnail = models.ForeignKey( + CustomImageModel, + on_delete=models.SET_NULL, + blank=True, + null=True, + verbose_name=_("Thumbnails"), + related_name="videos", + ) + duration = models.IntegerField(_("Duration"), default=0, editable=False, blank=True) + overview = models.ImageField( + _("Overview"), + null=True, + upload_to=get_storage_path_video, + blank=True, + max_length=255, + editable=False, + ) + encoding_in_progress = models.BooleanField( + _("Encoding in progress"), default=False, editable=False + ) + is_video = models.BooleanField(_("Is Video"), default=True, editable=False) + + date_delete = models.DateField( + _("Date to delete"), + default=default_date_delete, + help_text=_("Date when your video will be automatically removed from Pod."), + ) + + disable_comment = models.BooleanField( + _("Disable comment"), + help_text=_("Prevent users from commenting on your content."), + default=False, + ) + + class Meta: + """Metadata subclass for Video object.""" + + ordering = ["-date_added", "-id"] + get_latest_by = "date_added" + verbose_name = _("video") + verbose_name_plural = _("videos") + + def set_password(self) -> None: + """ + Encrypt the password if video is protected. + + An encrypted password cannot be re-encrypted. + """ + if self.password and not self.password.startswith(("pbkdf2_sha256$")): + self.password = make_password(self.password, hasher="pbkdf2_sha256") + + def save(self, *args, **kwargs) -> None: + """Store a video object in db.""" + newid = -1 + + # In case of creating new Video + if not self.id: + try: + newid = get_nextautoincrement(Video) + except Exception: + try: + newid = Video.objects.latest("id").id + newid += 1 + except Exception: + newid = 1 + # fix date_delete depends of owner affiliation + ACCOMMODATION_YEARS = getattr(settings, "ACCOMMODATION_YEARS", {}) + if len(ACCOMMODATION_YEARS) and len(self.owner.owner.affiliation): + add_year = self.get_date_delete_for_affiliation(ACCOMMODATION_YEARS) + self.date_delete = date.today() + timezone.timedelta(days=add_year * 365) + + # Modifying existing Video + else: + newid = self.id + newid = "%04d" % newid + self.slug = "%s-%s" % (newid, slugify(self.title)) + # self.set_password() + super(Video, self).save(*args, **kwargs) + # Ensure video folder will accord access to additional owners + self.update_additional_owners_rights() + + def __str__(self) -> str: + """Display a video object as string.""" + if self.id: + return "%s - %s" % ("%04d" % self.id, self.title) + else: + return "None" + + @property + def establishment(self): + return self.owner.owner.establishment + + @property + def viewcount(self): + """Get the view counter of a video.""" + return self.get_viewcount() + + viewcount.fget.short_description = _("Sum of view") + + @property + def recentViewcount(self): + """Get the recent view counter of a video.""" + return self.get_viewcount(VIDEO_RECENT_VIEWCOUNT) + + recentViewcount.fget.short_description = _( + "Sum of view of last %(ndays)s days" % {"ndays": VIDEO_RECENT_VIEWCOUNT} + ) + + def is_editable(self, user) -> bool: + """Return true if video is editable by user.""" + if RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY and user.is_staff is False: + return False + + if ( + (self.owner == user) + or (user.is_superuser) + or (user.has_perm("video.change_video")) + or (user in self.additional_owners.all()) + ): + return True + else: + return False + + @property + def get_encoding_step(self): + """Get the current encoding step of a video.""" + if hasattr(self, "encodingstep"): + return "%s: %s" % (self.encodingstep.num_step, self.encodingstep.desc_step) + else: + return "" + + get_encoding_step.fget.short_description = _("Encoding step") + + def get_date_delete_for_affiliation(self, accommodation_years): + """ + Get the calculated date of deletion according to multi-values field affiliation. + + If the affiliation field is an array (the user have several affiliations) + and their deletion deadlines exist, then the largest value (the longest date) + will be applied to the video. + """ + if self.affiliation_is_array(self.owner.owner.affiliation): + years = ( + accommodation_years.get(key) + for key in self.owner.owner.affiliation + if key in accommodation_years + ) + new_year = max(years) + else: + new_year = accommodation_years.get(self.owner.owner.affiliation) + if new_year is None: + new_year = DEFAULT_YEAR_DATE_DELETE + return new_year + + def affiliation_is_array(self, affiliation) -> bool: + """Check if the user affiliation is an array of strings or a simple string.""" + return True if affiliation.find("[") != -1 else False + + def get_player_height(self) -> int: + """ + Get the default player height (half size for audio files). + + This height is mostly valid when in iframe mode, + as in main mode height is set by % of window. + """ + return 360 if self.is_video else 244 + + def get_thumbnail_url(self, size="x720") -> str: + """Get a thumbnail url for the video, with defined max size.""" + request = None + # Initialize default thumbnail URL + thumbnail_url = "".join( + [ + "//", + get_current_site(request).domain, + static(DEFAULT_THUMBNAIL), + ] + ) + if self.thumbnail and self.thumbnail.file_exist(): + # Do not serve thumbnail url directly, as it can lead to the video URL + # Handle exception to avoid sending an error email + try: + im = get_thumbnail(self.thumbnail.file, size, crop="center", quality=80) + thumbnail_url = im.url + except Exception as e: + logger.error( + "An error occured during get_thumbnail_url" + " for video %s: %s" % (self.id, e) + ) + return thumbnail_url + else: + return thumbnail_url + + @property + def get_thumbnail_admin(self): + thumbnail_url = "" + # fix title for xml description + title = re.sub(r"[\x00-\x08\x0B-\x0C\x0E-\x1F]", "", self.title) + if self.thumbnail and self.thumbnail.file_exist(): + # Handle exception to avoid sending an error email + try: + im = get_thumbnail( + self.thumbnail.file, "100x100", crop="center", quality=72 + ) + thumbnail_url = im.url + except Exception: + thumbnail_url = static(DEFAULT_THUMBNAIL) + # + else: + thumbnail_url = static(DEFAULT_THUMBNAIL) + return format_html( + '%s' + % ( + thumbnail_url, + title.replace("{", "").replace("}", "").replace('"', "'"), + ) + ) + + get_thumbnail_admin.fget.short_description = _("Thumbnails") + + def get_thumbnail_card(self) -> str: + """Return thumbnail image card of current video.""" + thumbnail_url = "" + if self.thumbnail and self.thumbnail.file_exist(): + # Handle exception to avoid sending an error email + try: + im = get_thumbnail(self.thumbnail.file, "x170", crop="center", quality=72) + thumbnail_url = im.url + except Exception: + thumbnail_url = static(DEFAULT_THUMBNAIL) + # + else: + thumbnail_url = static(DEFAULT_THUMBNAIL) + return ( + '%s' + % (thumbnail_url, self.title) + ) + + @property + def duration_in_time(self) -> str: + """Get the duration of a video.""" + return time.strftime("%H:%M:%S", time.gmtime(self.duration)) + + duration_in_time.fget.short_description = _("Duration") + + @property + def encoded(self) -> bool: + """Get the encoded status of a video.""" + return ( + self.get_playlist_master() is not None + or self.get_video_mp4().exists() + or self.get_video_m4a() is not None + ) + + encoded.fget.short_description = _("Is the video encoded?") + + @property + def get_version(self): + """Get the version of a video.""" + try: + return self.videoversion.version + except VideoVersion.DoesNotExist: + return "O" + + def get_other_version(self) -> list: + """Get other versions of a video.""" + version = [] + for app in THIRD_PARTY_APPS: + mod = importlib.import_module("pod.%s.models" % app) + if hasattr(mod, capfirst(app)): + video_app = eval( + "mod.%s.objects.filter(video__id=%s).all()" % (capfirst(app), self.id) + ) + if ( + app == "interactive" + and video_app.first() is not None + and video_app.first().is_interactive() is False + ): + video_app = False + if video_app: + url = reverse( + "%(app)s:video_%(app)s" % {"app": app}, + kwargs={"slug": self.slug}, + ) + version.append( + { + "app": app, + "url": url, + "link": __VERSION_CHOICES_DICT__[app.capitalize()[0]], + } + ) + return version + + def get_default_version_link(self, slug_private: str = None): + """ + Get link of the version of a video. + + Args: + slug_private (str, optional): The private slug. Defaults to None. + + Returns: + str | None: The default version link. + """ + for version in self.get_other_version(): + if version["link"] == __VERSION_CHOICES_DICT__[self.get_version]: + if slug_private: + return version["url"] + slug_private + "/" + else: + return version["url"] + + def get_viewcount(self, from_nb_day=0): + """Get the view counter of a video.""" + if from_nb_day > 0: + d = date.today() - timezone.timedelta(days=from_nb_day) + set = self.viewcount_set.filter(date__gte=d) + else: + set = self.viewcount_set.all() + + count_sum = set.aggregate(Sum("count")) + + if count_sum["count__sum"] is None: + return 0 + return count_sum["count__sum"] + + def get_marker_time_for_user(self, user): + """Get the marker time of a video for the user in parameter.""" + if self.usermarkertime_set.filter(user=user).exists(): + return self.usermarkertime_set.get(user=user).markerTime + return 0 + + def get_absolute_url(self) -> str: + """Get the video absolute URL.""" + return reverse("video:video", args=[str(self.slug)]) + + def get_full_url(self, request=None) -> str: + """Get the video full URL.""" + full_url = "".join( + ["//", get_current_site(request).domain, self.get_absolute_url()] + ) + return full_url + + def get_hashkey(self) -> str: + return hashlib.sha256( + ("%s-%s" % (SECRET_KEY, self.id)).encode("utf-8") + ).hexdigest() + + def delete(self, *args, **kwargs) -> None: + """Delete the current video file and db object.""" + # use pre delete and post delete signal to remove file used by video + # see above + super(Video, self).delete(*args, **kwargs) + + def get_playlist_master(self): + playlist_video = self.playlistvideo_set.filter( + name="playlist", encoding_format="application/x-mpegURL" + ).first() + return playlist_video + + def get_video_m4a(self): + """Get the audio (m4a) version of the video.""" + encoding_audio = self.encodingaudio_set.filter( + name="audio", encoding_format="video/mp4" + ).first() + return encoding_audio + + def get_video_mp3(self): + """Get the audio (mp3) version of the video.""" + encoding_audio = self.encodingaudio_set.filter( + name="audio", encoding_format="audio/mp3" + ).first() + return encoding_audio + + def get_video_mp4(self): + """Get the mp4 version of the video.""" + return self.encodingvideo_set.filter(encoding_format="video/mp4") + + def get_video_json(self, extensions): + """Get the JSON representation of the video.""" + extension_list = extensions.split(",") if extensions else [] + list_video = self.encodingvideo_set.all() + dict_src = Video.get_media_json(extension_list, list_video) + sorted_dict_src = { + x: sorted(dict_src[x], key=lambda i: i["height"]) for x in dict_src.keys() + } + return sorted_dict_src + + def get_video_mp4_json(self) -> list: + """Get the JSON representation of the MP4 video.""" + list_mp4 = self.get_video_json(extensions="mp4") + return list_mp4["mp4"] if list_mp4.get("mp4") else [] + + def get_audio_json(self, extensions) -> dict: + """Get the JSON representation of the audio.""" + extension_list = extensions.split(",") if extensions else [] + list_audio = self.encodingaudio_set.filter(name="audio") + dict_src = Video.get_media_json(extension_list, list_audio) + return dict_src + + def get_audio_and_video_json(self, extensions) -> dict: + """Get the JSON representation of the video and audio.""" + return { + **self.get_video_json(extensions), + **self.get_audio_json(extensions), + } + + @staticmethod + def get_media_json(extension_list, list_video) -> dict: + dict_src = {} + for media in list_video: + file_extension = splitext(media.source_file.url)[-1] + if not extension_list or file_extension[1:] in extension_list: + media_height = None + if hasattr(media, "height"): + media_height = media.height + video_object = { + "id": media.id, + "type": media.encoding_format, + "src": media.source_file.url, + "height": media_height, + "extension": file_extension, + "label": media.name, + } + dict_entry = dict_src.get(file_extension[1:], None) + if dict_entry is None: + dict_src[file_extension[1:]] = [video_object] + else: + dict_entry.append(video_object) + return dict_src + + def get_json_to_index(self) -> str: + try: + current_site = Site.objects.get_current() + data_to_dump = { + "id": self.id, + "title": "%s" % self.title, + "owner": "%s" % self.owner.username, + "owner_full_name": "%s" % self.owner.get_full_name(), + "date_added": ( + "%s" % self.date_added.strftime("%Y-%m-%dT%H:%M:%S") + if self.date_added + else None + ), + "date_evt": ( + "%s" % self.date_evt.strftime("%Y-%m-%dT%H:%M:%S") + if self.date_evt + else None + ), + "description": "%s" % self.description, + "thumbnail": "%s" % self.get_thumbnail_url(), + "duration": "%s" % self.duration, + "tags": list(self.tags.all().values("name", "slug")), + "type": {"title": self.type.title, "slug": self.type.slug}, + "disciplines": list( + self.discipline.all() + .filter(site=current_site) + .values("title", "slug") + ), + "channels": list( + self.channel.all().filter(site=current_site).values("title", "slug") + ), + "themes": list(self.theme.all().values("title", "slug")), + "contributors": list(self.contributor_set.values("name", "role")), + "chapters": list(self.chapter_set.values("title", "slug")), + "overlays": list(self.overlay_set.values("title", "slug")), + "full_url": self.get_full_url(), + "is_restricted": self.is_restricted, + "password": True if self.password != "" else False, + "duration_in_time": self.duration_in_time, + "mediatype": "video" if self.is_video else "audio", + "cursus": "%s" % __CURSUS_CODES_DICT__[self.cursus], + "main_lang": "%s" % __LANG_CHOICES_DICT__[self.main_lang], + } + return json.dumps(data_to_dump) + except ObjectDoesNotExist as e: + logger.error( + "An error occured during get_json_to_index" + " for video %s: %s" % (self.id, e) + ) + return json.dumps({}) + + def get_json_to_video_view(video, other_data_to_dump) -> str: + try: + video_src = {} + if video.is_video: + video_src["mp4"] = video.get_video_mp4_json() + m3u8 = video.get_playlist_master() + if m3u8: + video_src["m3u8"] = { + "src": m3u8.source_file.url, + "type": m3u8.encoding_format, + } + else: + m4a = video.get_video_m4a() + video_src["m4a"] = { + "src": m4a.source_file.url, + "type": m4a.encoding_format, + } + + list_track = list( + map( + lambda t: { + "id": t.id, + "kind": t.kind, + "src": t.src.file.url, + "srclang": t.lang, + "label": t.get_label_lang(), + }, + video.track_set.all(), + ), + ) + list_overlay = list( + map( + lambda o: { + "start": o.time_start, + "end": o.time_end, + "content": o.content, + "align": o.position, + "showBackground": o.background, + }, + video.overlay_set.all(), + ), + ) + list_chapter = list( + map( + lambda c: {"time_start": c.time_start, "id": c.id, "title": c.title}, + video.chapter_set.all(), + ), + ) + overview = video.overview.url if (video.overview) else "" + data_to_dump = { + "status": "ok", + "version": video.get_version, + "slug": video.slug, + "title": video.title, + "tracks": list_track, + "is_video": video.is_video, + "src": video_src, + "is_360": video.is_360, + "thumbnail": video.get_thumbnail_url(), + "overview": overview, + "overlay": list_overlay, + "chapter": list_chapter, + } + if hasattr(video, "enrichmentvtt"): + data_to_dump["enrichtracksrc"] = video.enrichmentvtt.src.file.url + for k in other_data_to_dump: + data_to_dump[k] = other_data_to_dump[k] + return json.dumps(data_to_dump) + except ObjectDoesNotExist as e: + logger.error( + "An error occured during get_json_to_video_view" + " for video %s: %s" % (video.id, e) + ) + return json.dumps({}) + + def get_main_lang(self) -> str: + return "%s" % __LANG_CHOICES_DICT__[self.main_lang] + + def get_cursus(self) -> str: + return "%s" % __CURSUS_CODES_DICT__[self.cursus] + + def get_licence(self) -> str: + return "%s" % __LICENCE_CHOICES_DICT__[self.licence] + + def get_dublin_core(self) -> dict: + """Export Dublin Core items for current video.""" + contributors = [] + current_site = Site.objects.get_current() + for contrib in self.contributor_set.values_list("name", "role"): + contributors.append(" ".join(contrib)) + try: + data_to_dump = { + "dc.title": "%s" % escape(self.title), + "dc.creator": "%s" % self.owner.get_full_name(), + "dc.description": "%s" % escape(self.description), + "dc.subject": "%s, %s" + % ( + self.type.title, + ", ".join( + self.discipline.all() + .filter(site=current_site) + .values_list("title", flat=True) + ), + ), + "dc.publisher": __TITLE_ETB__, + "dc.contributor": ", ".join(contributors), + "dc.date": "%s" % self.date_added.strftime("%Y/%m/%d"), + "dc.type": "video" if self.is_video else "audio", + "dc.identifier": "http:%s" % self.get_full_url(), + "dc.language": "%s" % self.main_lang, + "dc.coverage": DEFAULT_DC_COVERAGE, + "dc.rights": self.licence if (self.licence) else DEFAULT_DC_RIGHTS, + "dc.format": "video/mp4" if self.is_video else "audio/mp3", + } + return data_to_dump + except ObjectDoesNotExist as e: + logger.error( + "An error occured during get_dublin_core" + " for video %s: %s" % (self.id, e) + ) + return {} + + def get_or_create_video_folder(self): + """Get or create a UserFolder associated to current video.""" + if USE_PODFILE: + videodir, created = UserFolder.objects.get_or_create( + name=self.slug, owner=self.owner + ) + return videodir + + def get_tag_list(self) -> str: + """Return a list of comma separated tag names.""" + return ", ".join(tag.name for tag in self.tags.all()) + + def update_additional_owners_rights(self) -> None: + """Update folder rights for additional video owners.""" + if USE_PODFILE: + try: + # First, search for exact Userfolder match + videodir = UserFolder.objects.get(name="%s" % self.slug, owner=self.owner) + # Ensure all additional users will get access to this folder + videodir.users.set(self.additional_owners.all()) + videodir.save() + except UserFolder.DoesNotExist: + # Search for Userfolder previously associated to current vid + # (if vid changed slug or owner) + slugid = "%04d-" % self.id + videodirs = UserFolder.objects.filter(name__startswith=slugid) + if videodirs.all().count() > 0: + # We found a match + videodir = videodirs.first() + # Re-sync folder metadata with video + videodir.users.set(self.additional_owners.all()) + videodir.name = self.slug + videodir.owner = self.owner + videodir.save() + + +class UpdateOwner(models.Model): + class Meta: + verbose_name = _("Update Owner") + verbose_name_plural = _("Update Owners") + + +@receiver(post_save, sender=Video) +def default_site(sender, instance, created: bool, **kwargs) -> None: + if instance.sites.count() == 0: + instance.sites.add(Site.objects.get_current()) + + +@receiver(pre_delete, sender=Video, dispatch_uid="pre_delete-video_files_removal") +def video_files_removal(sender, instance, using, **kwargs) -> None: + """Remove files created after encoding.""" + remove_video_file(instance) + + models_to_delete = [ + instance.encodingvideo_set.model, + instance.encodingaudio_set.model, + instance.playlistvideo_set.model, + ] + for model in models_to_delete: + previous_encoding_video = model.objects.filter(video=instance) + if len(previous_encoding_video) > 0: + for encoding in previous_encoding_video: + encoding.delete() + + +def remove_video_file(video: Video) -> None: + """Remove video file linked to video.""" + if video.overview: + image_overview = os.path.join( + os.path.dirname(video.overview.path), "overview.png" + ) + if os.path.isfile(image_overview): + os.remove(image_overview) + video.overview.delete() + + log_file = os.path.join( + os.path.dirname(video.video.path), "%04d" % video.id, "info_video.json" + ) + if os.path.isfile(log_file): + os.remove(log_file) + + +@receiver(post_delete, sender=Video, dispatch_uid="post_delete-video_podfiles_removal") +def video_podfiles_removal(sender, instance, **kwargs) -> None: + """Delete UserFolder associated to current video.""" + if instance.video and os.path.isfile(instance.video.path): + os.remove(instance.video.path) + video_dir = os.path.join(os.path.dirname(instance.video.path), "%04d" % instance.id) + if os.path.isdir(video_dir) and not os.listdir(video_dir): + os.rmdir(video_dir) + if USE_PODFILE: + video_folder = UserFolder.objects.filter( + name=instance.slug, + owner=instance.owner, + ) + if video_folder: + video_folder[0].delete() + + +class ViewCount(models.Model): + video = models.ForeignKey( + Video, verbose_name=_("Video"), editable=False, on_delete=models.CASCADE + ) + date = models.DateField(_("Date"), default=date.today, editable=False) + count = models.IntegerField(_("Number of view"), default=0, editable=False) + + @property + def sites(self): + return self.video.sites + + class Meta: + unique_together = ("video", "date") + verbose_name = _("View count") + verbose_name_plural = _("View counts") + + +class UserMarkerTime(models.Model): + """Record the time of video played by a user.""" + + video = models.ForeignKey( + Video, verbose_name=_("Video"), editable=False, on_delete=models.CASCADE + ) + user = models.ForeignKey( + User, verbose_name=_("User"), editable=False, on_delete=models.CASCADE + ) + markerTime = models.IntegerField(_("Marker time"), default=0, editable=False) + + @property + def sites(self): + """Return the sites of the video.""" + return self.video.sites + + def __str__(self) -> str: + """Render the user marker time as string.""" + return "Marker time for user %s and video %s: %s" % ( + self.user, + self.video, + self.markerTime, + ) + + class Meta: + unique_together = ("video", "user") + verbose_name = _("User viewing time marker of video") + verbose_name_plural = _("Users viewing time marker of video") + + +class VideoVersion(models.Model): + video = models.OneToOneField( + Video, verbose_name=_("Video"), editable=False, on_delete=models.CASCADE + ) + version = models.CharField( + _("Video version"), + max_length=1, + blank=True, + choices=__VERSION_CHOICES__, + default="O", + help_text=_("Video default version."), + ) + + class Meta: + verbose_name = _("Video version") + verbose_name_plural = _("Video versions") + + @property + def sites(self): + return self.video.sites + + @property + def sites_all(self): + return self.video.sites_set.all() + + def __str__(self) -> str: + """Render the video version as string.""" + return "Choice for default video version: %s - %s" % ( + self.video.id, + self.version, + ) + + +class Notes(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + video = models.ForeignKey(Video, on_delete=models.CASCADE) + note = models.TextField(_("Note"), null=True, blank=True) + + @property + def sites(self): + return self.video.sites + + @property + def sites_all(self): + return self.video.sites_set.all() + + class Meta: + """Note Metadata.""" + + verbose_name = _("Note") + verbose_name_plural = _("Notes") + unique_together = ("video", "user") + + def __str__(self) -> str: + """Render the Note as string.""" + return "%s-%s" % (self.user.username, self.video) + + +class AdvancedNotes(models.Model): + """Advanced video notes Model.""" + + user = models.ForeignKey(User, on_delete=models.CASCADE) + video = models.ForeignKey(Video, on_delete=models.CASCADE) + status = models.CharField( + _("Availability level"), + max_length=1, + choices=NOTES_STATUS, + default="0", + help_text=_("Select an availability level for the note."), + ) + note = models.TextField(_("Note"), null=True, blank=True) + timestamp = models.IntegerField(_("Timestamp"), null=True, blank=True) + added_on = models.DateTimeField(_("Date added"), default=timezone.now) + modified_on = models.DateTimeField(_("Date modified"), default=timezone.now) + + class Meta: + """AdvancedNotes Metadata.""" + + verbose_name = _("Advanced Note") + verbose_name_plural = _("Advanced Notes") + unique_together = ("video", "user", "timestamp", "status") + + @property + def sites(self): + return self.video.sites + + @property + def sites_all(self): + return self.video.sites_set.all() + + def __str__(self) -> str: + """Render the advanced note as string.""" + return "%s-%s-%s" % (self.user.username, self.video, self.timestamp) + + def clean(self) -> None: + """Validate AdvancedNotes fields.""" + if not self.note: + raise ValidationError( + AdvancedNotes._meta.get_field("note").help_text, code="invalid_note" + ) + if not self.status or self.status not in dict(NOTES_STATUS): + raise ValidationError( + AdvancedNotes._meta.get_field("status").help_text, code="invalid_status" + ) + if ( + self.timestamp is None + or self.timestamp < 0 + or (self.video.duration and self.timestamp > self.video.duration) + ): + raise ValidationError( + AdvancedNotes._meta.get_field("timestamp").help_text, + code="invalid_timestamp", + ) + + def timestampstr(self) -> str: + if self.timestamp is None: + return "--:--:--" + seconds = int(self.timestamp) + hours = int(seconds / 3600) + seconds -= hours * 3600 + minutes = int(seconds / 60) + seconds -= minutes * 60 + hours = "0" + str(hours) if hours < 10 else str(hours) + minutes = "0" + str(minutes) if minutes < 10 else str(minutes) + seconds = "0" + str(seconds) if seconds < 10 else str(seconds) + return hours + ":" + minutes + ":" + seconds + + +class NoteComments(models.Model): + """Comments for Advanced video notes model.""" + + user = models.ForeignKey(User, on_delete=models.CASCADE) + parentNote = models.ForeignKey(AdvancedNotes, on_delete=models.CASCADE) + parentCom = models.ForeignKey( + "NoteComments", blank=True, null=True, on_delete=models.CASCADE + ) + status = models.CharField( + _("Availability level"), + max_length=1, + choices=NOTES_STATUS, + default="0", + help_text=_("Select an availability level for the comment."), + ) + comment = models.TextField(_("Comment"), null=True, blank=True) + added_on = models.DateTimeField(_("Date added"), default=timezone.now) + modified_on = models.DateTimeField(_("Date modified"), default=timezone.now) + + class Meta: + verbose_name = _("Note comment") + verbose_name_plural = _("Note comments") + + def __str__(self) -> str: + """Render the note comment as string.""" + return "%s-%s-%s" % (self.user.username, self.parentNote, self.comment) + + def clean(self) -> None: + """Validate NoteComments fields.""" + if not self.comment: + raise ValidationError( + NoteComments._meta.get_field("comment").help_text, code="invalid_comment" + ) + if not self.status or self.status not in dict(NOTES_STATUS): + raise ValidationError( + NoteComments._meta.get_field("status").help_text, code="invalid_status" + ) + + +class VideoToDelete(models.Model): + date_deletion = models.DateField( + _("Date for deletion"), default=date.today, unique=True + ) + video = models.ManyToManyField(Video, verbose_name=_("Videos")) + + class Meta: + verbose_name = _("Video to delete") + verbose_name_plural = _("Videos to delete") + + def __str__(self) -> str: + """Render the video to delete as string.""" + return "%s - nb videos: %s" % (self.date_deletion, self.video.count()) + + +class Comment(models.Model): + """Video comment model.""" + + author = models.ForeignKey(User, on_delete=models.CASCADE) + content = models.TextField() + parent = models.ForeignKey( + "self", + related_name="children", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + direct_parent = models.ForeignKey( + "self", + related_name="direct_children", + null=True, + blank=True, + on_delete=models.CASCADE, + ) + video = models.ForeignKey(Video, on_delete=models.CASCADE) + added = models.DateTimeField(auto_now_add=True) + + class Meta: + """Video comment metadata.""" + + verbose_name = _("Comment") + verbose_name_plural = _("Comments") + + @property + def number_vote(self) -> None: + self.vote_set.all().count() + + @property + def get_children(self): + return Comment.objects.filter(parent_id=self.id).order_by("id") + + def get_json_children(self, user_id) -> list: + return list( + self.get_children.annotate(nbr_vote=Count("vote", distinct=True)) + .annotate( + author_name=Concat("author__first_name", Value(" "), "author__last_name") + ) + .annotate( + is_owner=Case( + When(author__id=user_id, then=Value(True)), + default=Value(False), + output_field=BooleanField(), + ) + ) + .values( + "id", + "parent__id", + "direct_parent__id", + "is_owner", + "author_name", + "added", + "content", + "nbr_vote", + ) + ) + + def __str__(self) -> str: + """Render the comment as string.""" + return self.content + + +class Vote(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + comment = models.ForeignKey(Comment, on_delete=models.CASCADE) + + class Meta: + verbose_name = _("Vote") + verbose_name_plural = _("Votes") + + def __str__(self) -> str: + """Render the vote as string.""" + return str(self.user) + + +class Category(models.Model): + """Video category Model.""" + + title = models.CharField( + _("Category title"), + max_length=100, + help_text=_( + "Please choose a title as short and accurate as " + "possible, reflecting the main subject / context " + "of the content. (max length: 100 characters)" + ), + ) + owner = models.ForeignKey(User, on_delete=models.CASCADE) + video = models.ManyToManyField( + Video, + verbose_name=_("Videos"), + blank=True, + help_text=_( + 'Hold down "Control", or "Command" on a Mac, to select more than one.' + ), + ) + slug = models.SlugField( + _("Slug"), + unique=True, + max_length=110, + help_text=_( + 'Used to access this instance, the "slug" is a short label ' + + "containing only letters, numbers, underscore or dash top." + ), + editable=False, + ) + + def save(self, *args, **kwargs) -> None: + """Set a slug and save the category instance.""" + self.slug = "%s-%s" % (self.owner.id, slugify(self.title)) + super(Category, self).save(*args, **kwargs) + + def __str__(self) -> str: + """Render the category as string.""" + return self.title + + class Meta: + """Category Metadata.""" + + ordering = ["title", "id"] + verbose_name = _("Category") + verbose_name_plural = _("Categories") + + +class VideoAccessToken(models.Model): + """Video access token model.""" + + video = models.ForeignKey(Video, on_delete=models.CASCADE) + token = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField( + verbose_name=_("Token name"), max_length=100, blank=True, default=_("Change me!") + ) + + class Meta: + """Video access token Metadata.""" + + ordering = ["video", "token"] + verbose_name = _("Video access token") + verbose_name_plural = _("Video access tokens") + unique_together = ["video", "token"] diff --git a/pod/video/templates/channel/list_theme.html b/pod/video/templates/channel/list_theme.html index bbeefafdfc..e111935f98 100644 --- a/pod/video/templates/channel/list_theme.html +++ b/pod/video/templates/channel/list_theme.html @@ -40,7 +40,8 @@ - + {% csrf_token %} diff --git a/pod/video/templates/channel/theme_edit.html b/pod/video/templates/channel/theme_edit.html index a68f2bff05..d62c0b92e3 100644 --- a/pod/video/templates/channel/theme_edit.html +++ b/pod/video/templates/channel/theme_edit.html @@ -7,9 +7,9 @@ {% endblock page_extra_head %} @@ -38,9 +38,9 @@

{% trans "Editing the channel’s themes" %} {{channel.ti
{% include 'channel/list_theme.html' with list_theme=channel.themes.all %}
-

-


-

+ +
+
{% if form_theme %} {% include 'channel/form_theme.html' %} diff --git a/pod/video/templates/videos/add_video.html b/pod/video/templates/videos/add_video.html index ec05b7a930..0f489b9ed9 100644 --- a/pod/video/templates/videos/add_video.html +++ b/pod/video/templates/videos/add_video.html @@ -1,7 +1,6 @@ {% extends 'base.html' %} {% load i18n %} {% load static %} -{% load tagging_tags %} {% block breadcrumbs %}{{ block.super }} diff --git a/pod/video/templates/videos/card_select.html b/pod/video/templates/videos/card_select.html index 05f184b3fd..fe2b68d2af 100644 --- a/pod/video/templates/videos/card_select.html +++ b/pod/video/templates/videos/card_select.html @@ -1,6 +1,4 @@ -{% load i18n %} -{% load video_tags %} -{% load video_quiz %} +{% load i18n video_tags video_quiz %} {% spaceless %}
diff --git a/pod/video/templates/videos/filter_aside.html b/pod/video/templates/videos/filter_aside.html index bb7f78abb6..2da17edd67 100644 --- a/pod/video/templates/videos/filter_aside.html +++ b/pod/video/templates/videos/filter_aside.html @@ -1,6 +1,4 @@ -{% load i18n %} -{% load video_tags %} -{% load thumbnail %} +{% load i18n video_tags thumbnail %} {% spaceless %}
@@ -92,33 +90,34 @@

{% endif %} {% if HIDE_TAGS == False %} -
-  {% trans 'Tags' %} -
- {% tags_for_model video.Video as tagscloud with counts %} - {% with tagslist=tagscloud|dictsortreversed:"count"|slice:":20" %} -
- {% for tag in tagslist %} -
- - + {% if TAGS|length > 0 %} +
+ +  {% trans 'Tags' %} + +
+
+ {% for tag in TAGS %} +
+ + +
+ {% endfor %}
- {% endfor %} -
- {% if tagslist|length > 5 %} - - - - {% endif %} - {% endwith %} -
-
+ {% if TAGS|length > 5 %} + + + + {% endif %} +

+ + {% endif %} {% endif %} {% if HIDE_CURSUS == False %}
diff --git a/pod/video/templates/videos/video-all-info.html b/pod/video/templates/videos/video-all-info.html index cb65ee247a..bdbaf53e24 100644 --- a/pod/video/templates/videos/video-all-info.html +++ b/pod/video/templates/videos/video-all-info.html @@ -1,9 +1,9 @@ {% load i18n %} -{% load tagging_tags %}

- {% if video.licence %} {% include "videos/video_licencebox.html" %}{% endif %} {{video.title|capfirst}} + {% if video.licence %} {% include "videos/video_licencebox.html" %}{% endif %} + {{video.title|capfirst}}

{% if video.date_evt %}{{ video.date_evt }}{% endif %}
@@ -19,7 +19,7 @@

{% trans 'Tags:' %} + {% for tag in video.tags.all %} + diff --git a/pod/video/templates/videos/video.html b/pod/video/templates/videos/video.html index 2475b2c652..4a404a19f9 100644 --- a/pod/video/templates/videos/video.html +++ b/pod/video/templates/videos/video.html @@ -1,10 +1,6 @@ {% extends 'base.html' %} -{% load i18n %} -{% load static custom_tags %} -{% load tagging_tags %} -{% load thumbnail %} -{% load video_filters %} -{% load video_tags %} +{% load i18n static custom_tags thumbnail %} +{% load video_filters video_tags %} {% load favorites_playlist %} {% block opengraph %} diff --git a/pod/video/templates/videos/video_edit.html b/pod/video/templates/videos/video_edit.html index 324bac9c5b..14ad3b5e88 100644 --- a/pod/video/templates/videos/video_edit.html +++ b/pod/video/templates/videos/video_edit.html @@ -1,7 +1,13 @@ {% extends 'base.html' %} {% load i18n %} {% load static %} -{% load tagging_tags %} + +{% block page_extra_head %} + {# form.media.css #} + + +{% endblock %} {% block breadcrumbs %}{{ block.super }}

@@ -49,7 +55,6 @@

{% endif %} accept-charset="utf-8" enctype="multipart/form-data" class="needs-validation" novalidate data-morecheck="videocheck"> {% csrf_token %} - {% if form.errors %}
@@ -100,7 +105,6 @@

{{ options.legend|safe }} {% endif %} - {% for field in form.visible_fields %} {% if field.name in options.fields %}
@@ -258,10 +262,8 @@

{% trans "Help for fo + {{ form.media.js }} - - {{form.media}} -