diff --git a/CONFIGURATION_EN.md b/CONFIGURATION_EN.md index c6482866aa..d35e4650b3 100644 --- a/CONFIGURATION_EN.md +++ b/CONFIGURATION_EN.md @@ -399,8 +399,8 @@ Set `USE_AI_ENHANCEMENT` to True to activate this application.
* `AI_ENHANCEMENT_CGU_URL` > default value: `` >> URL for General Terms and Conditions for API uses for the AI video enhancement.
- >> Example: 'https://aristote.univ.fr/cgu'
- >> Project Link: https://www.demainestingenieurs.centralesupelec.fr/aristote/
+ >> Example: ''
+ >> Project Link:
* `AI_ENHANCEMENT_CLIENT_ID` > default value: `mocked_id` >> The video enhancement AI client ID.
@@ -622,6 +622,15 @@ Set `USE_DRESSING` to True to activate this application.
### +### Seaker application configuration + +Speaker application to add speakers to video.
+Set `USE_SPEAKER` to True to activate this application.
+ +* `USE_SPEAKER` + > default value: `False` + >> Activation of the Speaker application
+ ### Video import application configuration Import_video app to import external videos into Pod.
diff --git a/CONFIGURATION_FR.md b/CONFIGURATION_FR.md index 0594364eeb..c86d0155c3 100644 --- a/CONFIGURATION_FR.md +++ b/CONFIGURATION_FR.md @@ -475,6 +475,9 @@ Il faudra pour cela créer un fichier de langue et traduire chaque entrée.
* `ARCHIVE_OWNER_USERNAME` > valeur par défaut : `"archive"` >> Nom de l’utilisateur pour l’archivage des vidéos.
+* `ARCHIVE_HOW_MANY_DAYS` + > valeur par défaut : `365` + >> Délai avant qu'une vidéo archivée ne soit déplacée vers archive_ROOT.
* `POD_ARCHIVE_AFFILIATION` > valeur par défaut : `[]` >> Affiliations pour lesquelles on souhaite archiver la vidéo plutôt que de la supprimer.
@@ -727,8 +730,8 @@ Mettre `USE_AI_ENHANCEMENT` à True pour activer cette application.
* `AI_ENHANCEMENT_CGU_URL` > valeur par défaut : `` >> L’URL des conditions générales d’utilisation de l’API pour l’IA d’amélioration des vidéos.
- >> Exemple : 'https://aristote.univ.fr/cgu'
- >> Lien du projet : https://www.demainestingenieurs.centralesupelec.fr/aristote/
+ >> Exemple : ''
+ >> Lien du projet :
* `AI_ENHANCEMENT_CLIENT_ID` > valeur par défaut : `mocked_id` >> L’ID du client de l’IA d’amélioration des vidéos.
@@ -1028,6 +1031,15 @@ Mettre `USE_DRESSING` à True pour activer cette application.
### Configuration de l’application enrichment +### Configuration de l’application Intervenant + +Application Intervenant permettant d'ajouter des intervenants à la vidéo.
+Mettre `USE_SPEAKER` à True pour activer cette application.
+ +* `USE_SPEAKER` + > valeur par défaut : `False` + >> Activation de l’application Intervenant
+ ### Configuration de l’application d’import vidéo Application Import_video permettant d’importer des vidéos externes dans Pod.
diff --git a/README.md b/README.md index 07558ba3b3..c5073652cc 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ ![last commit push](https://img.shields.io/github/last-commit/EsupPortail/Esup-Pod) [![Author](https://img.shields.io/badge/author-Ptitloup-blue)](https://www.linkedin.com/in/nicolas-can-a6bb7869/) - ## [FR] ### Plateforme de gestion de fichier vidéo diff --git a/pod/ai_enhancement/static/ai_enhancement/js/enrich-form.js b/pod/ai_enhancement/static/ai_enhancement/js/enrich-form.js index c43a253a55..234989ef42 100644 --- a/pod/ai_enhancement/static/ai_enhancement/js/enrich-form.js +++ b/pod/ai_enhancement/static/ai_enhancement/js/enrich-form.js @@ -4,6 +4,8 @@ * @since 3.7.0 */ +// Read-only globals defined in main.js +/* global decodeString remove_quotes removeAccentsAndLowerCase */ const BORDER_CLASS = 'border-d'; @@ -92,7 +94,7 @@ function addTogglePairInput(aiVersionElement, initialVersionElement, input, elem aiVersionElement.addEventListener('click', () => { let input = document.getElementById('id_' + element); togglePairInput(aiVersionElement, initialVersionElement, input, element); - }) + }); input.addEventListener('input', () => event__inputChange(initialVersionElement, aiVersionElement)); } @@ -182,7 +184,7 @@ function addEventListeners(videoSlug, videoTitle, videoDescription, videoDiscipl 'description', 'tags', 'disciplines', - ] + ]; const options = { method: 'GET', headers: { @@ -227,7 +229,7 @@ function addEventListeners(videoSlug, videoTitle, videoDescription, videoDiscipl }); aiVersionElement.addEventListener('click', () => { toggleMultiplePairInput(aiVersionElement, initialVersionElement, input); - }) + }); input.addEventListener('input', () => event__inputChange(initialVersionElement, aiVersionElement)); break; } diff --git a/pod/ai_enhancement/views.py b/pod/ai_enhancement/views.py index 26da7b90d2..e2d8de01d2 100644 --- a/pod/ai_enhancement/views.py +++ b/pod/ai_enhancement/views.py @@ -25,7 +25,6 @@ from pod.main.lang_settings import ALL_LANG_CHOICES, PREF_LANG_CHOICES from pod.main.utils import json_to_web_vtt from pod.main.views import in_maintenance -from pod.podfile.models import UserFolder from pod.quiz.utils import import_quiz from pod.video.models import Video, Discipline from pod.video_encode_transcript.transcript import saveVTT @@ -349,10 +348,7 @@ def enhance_subtitles(request: WSGIRequest, video_slug: str) -> HttpResponse: + str(True) ) - video_folder, created = UserFolder.objects.get_or_create( - name=video.slug, - owner=request.user, - ) + video_folder = video.get_or_create_video_folder() if enhancement_is_already_asked(video): enhancement = AIEnhancement.objects.filter(video=video).first() if enhancement.is_ready: diff --git a/pod/bbb/views.py b/pod/bbb/views.py index 2ba3685eb8..0b6df81de4 100644 --- a/pod/bbb/views.py +++ b/pod/bbb/views.py @@ -35,7 +35,6 @@ def list_meeting(request): attendee__user_id=request.user.id, recording_available=True ) meetings_list = meetings_list.order_by("-session_date") - # print(str(meetings_list.query)) page = request.GET.get("page", 1) @@ -145,7 +144,6 @@ def live_list_meeting(request): last_date_in_progress__gte=dateSince10Min, ) meetings_list = meetings_list.order_by("-session_date") - # print(str(meetings_list.query)) meetings_list = check_meetings_have_live_in_progress(meetings_list, request) diff --git a/pod/completion/models.py b/pod/completion/models.py index a3c73c76ba..f0cacb3d70 100644 --- a/pod/completion/models.py +++ b/pod/completion/models.py @@ -79,7 +79,7 @@ class Contributor(models.Model): video = models.ForeignKey(Video, verbose_name=_("video"), on_delete=models.CASCADE) name = models.CharField( - verbose_name=_("lastname / firstname"), max_length=200, default="" + verbose_name=_("last name / first name"), max_length=200, default="" ) email_address = models.EmailField( verbose_name=_("mail"), null=True, blank=True, default="" diff --git a/pod/completion/static/js/completion.js b/pod/completion/static/js/completion.js index a0ba86f4a8..5c492c94a8 100644 --- a/pod/completion/static/js/completion.js +++ b/pod/completion/static/js/completion.js @@ -49,6 +49,7 @@ var ajaxfail = function (data, form) { document.addEventListener("submit", (e) => { if ( e.target.id !== "form_new_contributor" && + e.target.id !== "form_new_speaker" && e.target.id !== "form_new_document" && e.target.id !== "form_new_track" && e.target.id !== "form_new_overlay" && @@ -219,6 +220,10 @@ var sendAndGetForm = async function (elt, action, name, form, list) { deleteConfirm = confirm( gettext("Are you sure you want to delete this contributor?"), ); + } else if (name === "speaker") { + deleteConfirm = confirm( + gettext("Are you sure you want to delete this speaker?"), + ); } else if (name === "document") { deleteConfirm = confirm( gettext("Are you sure you want to delete this document?"), diff --git a/pod/completion/templates/contributor/list_contributor.html b/pod/completion/templates/contributor/list_contributor.html index 285eb19eb8..cb8363cf73 100644 --- a/pod/completion/templates/contributor/list_contributor.html +++ b/pod/completion/templates/contributor/list_contributor.html @@ -6,7 +6,7 @@ {% trans 'List of contributors' %} ({{list_contributor|length}}) - {% trans 'Lastname / Firstname' %} + {% trans 'Last name / First name' %} {% trans 'Mail' %} {% trans 'Role' %} {% trans 'Web link' %} diff --git a/pod/completion/templates/video_completion.html b/pod/completion/templates/video_completion.html index f02ad092cd..d19c000750 100644 --- a/pod/completion/templates/video_completion.html +++ b/pod/completion/templates/video_completion.html @@ -49,6 +49,32 @@

{% if request.user.is_staff %} + {% if USE_SPEAKER %} +
+

+ +

+
+
+ {% include 'speaker/list_speaker.html' %} + + {% if form_speaker %} + {% include 'speaker/form_speaker.html' with form_speaker=form_speaker %} + {% endif %} + +
+ {% csrf_token %} + + +
+
+
+
+{% endif %} + +

+ + {% if USE_SPEAKER %} +
+ +
+

{% trans 'List of speakers related to this video.' %}

+

{% trans "You can add speakers to this video by searching by their last name, first name or job. If you can't find the speaker, contact a super admin." %}

+
+
+ {% endif %}
{% endif %}
-

{% trans "Help"%}

- -
-

{% trans 'The video cut allows you to set a start and an end to trim your video.' %}

-

{% trans 'Your original video is kept and you can therefore modify your changes at any time.' %}

-

{% trans 'When saving your cut, an encoding is restarted to replace the old one.' %}

+

{% trans "Help"%}

+ +
+

{% trans 'The video cut allows you to set a start and an end to trim your video.' %}

+

{% trans 'Your original video is kept and you can therefore modify your changes at any time.' %}

+

{% trans 'When saving your cut, an encoding is restarted to replace the old one.' %}

{% endblock page_aside %} diff --git a/pod/cut/tests/test_views.py b/pod/cut/tests/test_views.py index cec982dc02..189d68a389 100644 --- a/pod/cut/tests/test_views.py +++ b/pod/cut/tests/test_views.py @@ -12,15 +12,20 @@ from .. import views from importlib import reload +# ggignore-start +# gitguardian:ignore +PWD = "azerty1234" # nosec +# ggignore-end + class CutVideoViewsTestCase(TestCase): fixtures = [ "initial_data.json", ] - def setUp(self): - self.user = User.objects.create(username="test", password="azerty", is_staff=True) - self.user2 = User.objects.create(username="test2", password="azerty") + def setUp(self) -> None: + self.user = User.objects.create(username="test", password=PWD, is_staff=True) + self.user2 = User.objects.create(username="test2", password=PWD) self.video = Video.objects.create( title="videotest", owner=self.user, @@ -30,7 +35,7 @@ def setUp(self): ) self.video.additional_owners.add(self.user2) - def test_maintenance(self): + def test_maintenance(self) -> None: """Test Pod maintenance mode in CutVideoViewsTestCase.""" self.client.force_login(self.user) url = reverse("cut:video_cut", kwargs={"slug": self.video.slug}) @@ -46,7 +51,7 @@ def test_maintenance(self): self.assertRedirects(response, "/maintenance/") print(" ---> test_maintenance ok") - def test_get_full_duration(self): + def test_get_full_duration(self) -> None: """Test test_get_full_duration.""" CutVideo.objects.create( video=self.video, start=time(0, 0, 0), end=time(0, 0, 10), duration="00:00:10" @@ -61,7 +66,7 @@ def test_get_full_duration(self): print(" ---> test_get_full_duration ok") @override_settings(RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY=True, USE_CUT=True) - def test_restrict_edit_video_access_staff_only(self): + def test_restrict_edit_video_access_staff_only(self) -> None: """Test test_restrict_edit_video_access_staff_only.""" reload(views) self.client.force_login(self.user2) @@ -80,7 +85,7 @@ def test_restrict_edit_video_access_staff_only(self): print(" ---> test_restrict_edit_video_access_staff_only ok") - def test_post_cut_valid_form(self): + def test_post_cut_valid_form(self) -> None: """Test test_post_cut_valid_form.""" self.client.force_login(self.user) post_data = { @@ -101,7 +106,7 @@ def test_post_cut_valid_form(self): print(" ---> test_post_cut_valid_form ok") - def test_post_cut_invalid_form(self): + def test_post_cut_invalid_form(self) -> None: """Test test_post_cut_invalid_form.""" self.client.force_login(self.user) post_data = { diff --git a/pod/enrichment/forms.py b/pod/enrichment/forms.py index e8a7d8c380..fc79c3905c 100644 --- a/pod/enrichment/forms.py +++ b/pod/enrichment/forms.py @@ -62,7 +62,7 @@ def __init__(self, *args, **kwargs): self.fields[myField].widget.attrs["class"] = "form-control required" label_unicode = "{0}".format(self.fields[myField].label) self.fields[myField].label = mark_safe( - "{0} *".format(label_unicode) + "{0} *".format(label_unicode) ) else: self.fields[myField].widget.attrs["class"] = "form-control" diff --git a/pod/enrichment/models.py b/pod/enrichment/models.py index 6511db8ba9..268cbb5395 100755 --- a/pod/enrichment/models.py +++ b/pod/enrichment/models.py @@ -22,11 +22,9 @@ import datetime if getattr(settings, "USE_PODFILE", False): + __FILEPICKER__ = True from pod.podfile.models import CustomImageModel from pod.podfile.models import CustomFileModel - from pod.podfile.models import UserFolder - - __FILEPICKER__ = True else: __FILEPICKER__ = False from pod.main.models import CustomImageModel @@ -65,18 +63,16 @@ def enrichment_to_vtt(list_enrichment, video) -> str: with open(temp_vtt_file.name, "w") as f: webvtt.write(f) if __FILEPICKER__: - videodir, created = UserFolder.objects.get_or_create( - name="%s" % video.slug, owner=video.owner - ) + video_folder = video.get_or_create_video_folder() previous_enrichment_file = CustomFileModel.objects.filter( name__startswith="enrichment", - folder=videodir, + folder=video_folder, created_by=video.owner, ) for enr in previous_enrichment_file: enr.delete() # do it like this to delete file enrichment_file, created = CustomFileModel.objects.get_or_create( - name="enrichment", folder=videodir, created_by=video.owner + name="enrichment", folder=video_folder, created_by=video.owner ) if enrichment_file.file and os.path.isfile(enrichment_file.file.path): diff --git a/pod/live/templates/live/event_edit.html b/pod/live/templates/live/event_edit.html index c553c1e4f9..396955b525 100644 --- a/pod/live/templates/live/event_edit.html +++ b/pod/live/templates/live/event_edit.html @@ -206,7 +206,7 @@

{% trans "Event planning" %}

document.querySelectorAll('ul.timelist').forEach( (e) => { var $this = e; var originalHref = $this.querySelector('a').getAttribute('href'); - console.log(originalHref); + $this.querySelector('li').remove(); for (i=8; i <= 20; i++) { var newLink = '
  • \n" +"Last-Translator: SebastienCozeDev \n" "Language-Team: Pod Team cotech-esup-pod@esup-portail.org\n" "Language: fr\n" "MIME-Version: 1.0\n" @@ -95,7 +95,8 @@ msgstr "La discipline proposée par l’IA Aristote." #: pod/enrichment/templates/enrichment/list_enrichment.html pod/live/models.py #: pod/main/models.py pod/main/views.py pod/playlist/forms.py #: pod/playlist/models.py pod/quiz/forms.py pod/quiz/models.py -#: pod/video/models.py pod/video/templates/channel/list_theme.html +#: pod/speaker/models.py pod/video/models.py +#: pod/video/templates/channel/list_theme.html #: pod/video/templates/videos/video_sort_select.html msgid "Title" msgstr "Titre" @@ -935,7 +936,6 @@ msgstr "" #: pod/bbb/models.py pod/live/models.py pod/live/templates/live/event_card.html #: pod/meeting/models.py pod/recorder/models.py pod/video/forms.py -#: pod/video/models.py msgid "Restricted access" msgstr "Accès restreint" @@ -1363,6 +1363,8 @@ msgstr "Une ou plusieurs erreurs ont été trouvées dans le formulaire :" #: pod/playlist/templates/playlist/add_or_edit.html #: pod/quiz/templates/quiz/create_edit_quiz.html #: pod/recorder/templates/recorder/add_recording.html +#: pod/speaker/templates/speaker/form_speaker.html +#: pod/speaker/templates/speaker/speaker_modal.html #: pod/video/templates/channel/channel_edit.html #: pod/video/templates/channel/form_theme.html #: pod/video/templates/videos/video_note_comments_display.html @@ -1378,6 +1380,8 @@ msgstr "Sauvegarder" #: pod/completion/templates/track/form_track.html #: pod/enrichment/templates/enrichment/form_enrichment.html #: pod/playlist/templates/playlist/playlist.html +#: pod/speaker/templates/speaker/form_speaker.html +#: pod/speaker/templates/speaker/speaker_modal.html #: pod/video/templates/channel/form_theme.html #: pod/video/templates/videos/category_modal.html #: pod/video/templates/videos/dashboard_modal.html @@ -1402,6 +1406,8 @@ msgstr "Liste des chapitres" #: pod/completion/templates/track/list_track.html #: pod/dressing/templates/my_dressings.html #: pod/enrichment/templates/enrichment/list_enrichment.html +#: pod/speaker/templates/speaker/list_speaker.html +#: pod/speaker/templates/speaker/speakers_management.html #: pod/video/templates/channel/list_theme.html #: pod/video/templates/videos/video_access_tokens.html msgid "Actions" @@ -1439,6 +1445,7 @@ msgstr "Supprimer le chapitre « %(chapter_title)s »" #: pod/playlist/templates/playlist/delete.html #: pod/quiz/templates/quiz/delete_quiz.html #: pod/recorder/templates/recorder/record_delete.html +#: pod/speaker/templates/speaker/list_speaker.html #: pod/video/templates/channel/list_theme.html #: pod/video/templates/videos/category_modal.html #: pod/video/templates/videos/dashboard.html @@ -1490,6 +1497,7 @@ msgstr "Gérer la vidéo" #: pod/chapter/templates/video_chapter.html #: pod/completion/templates/video_completion.html #: pod/cut/templates/video_cut.html pod/dressing/templates/video_dressing.html +#: pod/quiz/templates/quiz/question_help_aside.html msgid "Help" msgstr "Aide" @@ -1599,7 +1607,7 @@ msgstr "Ingénieur du son" msgid "Speaking roles" msgstr "Rôles d’orateur" -#: pod/completion/models.py +#: pod/completion/models.py pod/speaker/apps.py pod/speaker/models.py msgid "Speaker" msgstr "Intervenant" @@ -1612,7 +1620,7 @@ msgid "captions" msgstr "légendes" #: pod/completion/models.py -msgid "lastname / firstname" +msgid "last name / first name" msgstr "nom / prénom" #: pod/completion/models.py @@ -1864,6 +1872,7 @@ msgstr "" #: pod/completion/templates/document/form_document.html #: pod/completion/templates/overlay/form_overlay.html #: pod/completion/templates/track/form_track.html +#: pod/speaker/templates/speaker/form_speaker.html msgid "Your form contains errors:" msgstr "Votre formulaire contient des erreurs :" @@ -1872,7 +1881,7 @@ msgid "List of contributors" msgstr "Liste des contributeurs" #: pod/completion/templates/contributor/list_contributor.html -msgid "Lastname / Firstname" +msgid "Last name / First name" msgstr "Nom / Prénom" #: pod/completion/templates/contributor/list_contributor.html @@ -1994,8 +2003,7 @@ msgid "Caption" msgstr "Sous-titre / Légende" #: pod/completion/templates/video_caption_maker.html -#: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Play" msgstr "Lecture" @@ -2115,6 +2123,14 @@ msgstr "Contributeur(s)" msgid "Add a new contributor" msgstr "Ajouter un nouveau contributeur" +#: pod/completion/templates/video_completion.html +msgid "Speaker(s)" +msgstr "Intervenant(s)" + +#: pod/completion/templates/video_completion.html +msgid "Add a new speaker" +msgstr "Ajouter un nouvel intervenant" + #: pod/completion/templates/video_completion.html msgid "Subtitle(s) and Caption(s)" msgstr "Sous-titre(s) et légende(s)" @@ -2170,6 +2186,19 @@ msgstr "" "Un contributeur doit au moins avoir un nom et un rôle. Vous pouvez aussi " "joindre son courriel ainsi qu’un lien web (site professionnel par exemple)." +#: pod/completion/templates/video_completion.html +msgid "List of speakers related to this video." +msgstr "Liste des intervenants relatifs à cette vidéo." + +#: pod/completion/templates/video_completion.html +msgid "" +"You can add speakers to this video by searching by their last name, first " +"name or job. If you can't find the speaker, contact a super admin." +msgstr "" +"Vous pouvez ajouter des intervenants à cette vidéo en effectuant une " +"recherche par leur nom, prénom ou fonction. Si vous ne trouvez pas " +"l’orateur, contactez un super administrateur." + #: pod/completion/templates/video_completion.html msgid "Subtitle(s) and/or captions(s) related to this video." msgstr "Sous-titre(s) et/ou légende(s) relatifs à cette vidéo." @@ -2307,6 +2336,15 @@ msgstr "Veuillez corriger les erreurs" msgid "Edit the contributor “%s”" msgstr "Modifier le contributeur « %s »" +#: pod/completion/views.py +#, python-format +msgid "Add a new speaker to the video “%s”" +msgstr "Ajouter un nouvel intervenant à la vidéo « %s »" + +#: pod/completion/views.py +msgid "The speaker has been saved." +msgstr "L’intervenant a été enregistré." + #: pod/cut/apps.py pod/cut/models.py msgid "Video cuts" msgstr "Découpes de vidéo" @@ -2319,8 +2357,8 @@ msgstr "Découpe de vidéo" msgid "Please select values between 00:00:00 and " msgstr "Veuillez renseigner des valeurs comprises entre 00:00:00 et " -#: pod/cut/templates/video_cut.html pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html +#: pod/cut/templates/video_cut.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Cut the video" msgstr "Découper la vidéo" @@ -2365,7 +2403,6 @@ msgstr "Réinitialiser les temps de début et de fin à leurs valeurs précéden #: pod/cut/templates/video_cut.html pod/dressing/templates/video_dressing.html #: pod/playlist/templates/playlist/filter_aside.html -#: pod/video/templates/videos/dashboard.html #: pod/video/templates/videos/filter_aside.html msgid "Reset" msgstr "Réinitialiser" @@ -2467,7 +2504,7 @@ msgstr "" #: pod/dressing/models.py pod/enrichment/forms.py pod/enrichment/models.py #: pod/live/models.py pod/meeting/models.py pod/podfile/models.py -#: pod/recorder/models.py pod/video/models.py +#: pod/recorder/models.py pod/video/forms.py pod/video/models.py msgid "Groups" msgstr "Groupes" @@ -3807,7 +3844,7 @@ msgid "Url of the embedded site to display on aside" msgstr "Lien du site à intégrer sous forme d’iframe à droite du live" #: pod/live/models.py pod/quiz/forms.py pod/quiz/models.py -#: pod/recorder/models.py pod/video/forms.py pod/video/models.py +#: pod/recorder/models.py pod/video/models.py msgid "Draft" msgstr "Brouillon" @@ -4352,13 +4389,13 @@ msgid "Sorry, no event found." msgstr "Désolé, aucun évènement trouvé." #: pod/live/templates/live/events_list.html -#: pod/video/templates/videos/category_modal.html +#: pod/video/templates/videos/category_modal_video_list.html #: pod/video/templates/videos/paginator.html msgid "Previous page" msgstr "Page précédente" #: pod/live/templates/live/events_list.html -#: pod/video/templates/videos/category_modal.html +#: pod/video/templates/videos/category_modal_video_list.html #: pod/video/templates/videos/paginator.html msgid "Next page" msgstr "Page suivante" @@ -5753,6 +5790,10 @@ msgstr "Mes fichiers" msgid "Claim a record" msgstr "Revendiquer un enregistrement" +#: pod/main/templates/navbar.html pod/speaker/views.py +msgid "Speakers management" +msgstr "Gestion des intervenants" + #: pod/main/templates/navbar.html pod/progressive_web_app/templates/debug.html msgid "Notifications settings" msgstr "Paramètres de notifications" @@ -6639,22 +6680,27 @@ msgid "Authenticate" msgstr "S’authentifier" #: pod/meeting/templates/meeting/link_meeting.html +#: pod/video/templates/videos/link_video.html +msgid "More options" +msgstr "Plus d'options" + +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Copy the direct join link" msgstr "Copiez le lien d’accès direct" -#: pod/meeting/templates/meeting/link_meeting.html +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Copy the join link" msgstr "Copiez le lien d’accès avec restriction d’accès" -#: pod/meeting/templates/meeting/link_meeting.html +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Invite to the meeting" msgstr "Inviter à la réunion" -#: pod/meeting/templates/meeting/link_meeting.html +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Get the recordings of the meeting" msgstr "Obtenir les enregistrements de la réunion" -#: pod/meeting/templates/meeting/link_meeting.html +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Delete the meeting" msgstr "Supprimer la réunion" @@ -6896,18 +6942,7 @@ msgstr "" " " #: pod/meeting/views.py -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| "

    Hello,

    \n" -#| "

    %(owner)s invites you to the meeting " -#| "%(meeting_title)s.

    \n" -#| "

    here the link to join the meeting:\n" -#| " %(join_link)s

    \n" -#| "

    You need this password to enter: %(password)s

    \n" -#| "

    Regards

    \n" -#| " " +#, python-format msgid "" "\n" "

    Hello,

    \n" @@ -6932,20 +6967,7 @@ msgstr "" " " #: pod/meeting/views.py -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| "

    Hello,

    \n" -#| "

    %(owner)s invites you to the meeting " -#| "%(meeting_title)s.

    \n" -#| "

    Start date: %(start_date_time)s

    \n" -#| "

    End date: %(end_date)s

    \n" -#| "

    here the link to join the meeting:\n" -#| " %(join_link)s

    \n" -#| "

    You need this password to enter: %(password)s

    \n" -#| "

    Regards

    \n" -#| " " +#, python-format msgid "" "\n" "

    Hello,

    \n" @@ -7161,7 +7183,7 @@ msgstr "" #: pod/playlist/models.py #: pod/playlist/templates/playlist/playlist_visibility_icon.html -#: pod/video/models.py +#: pod/video/forms.py pod/video/models.py #: pod/video/templates/videos/video_note_comments_display.html #: pod/video/templates/videos/video_note_display.html msgid "Public" @@ -7260,7 +7282,7 @@ msgstr "Accéder aux listes de lecture" #: pod/playlist/templates/playlist/add_or_edit.html #: pod/playlist/templates/playlist/playlist_link.html -#: pod/playlist/tests/test_views.py pod/playlist/views.py +#: pod/playlist/tests/test_views.py msgid "Edit the playlist" msgstr "Éditer la liste de lecture" @@ -7591,6 +7613,11 @@ msgid "The data sent to create the playlist are invalid." msgstr "" "Les données envoyées pour créer la liste de lecture ne sont pas valides." +#: pod/playlist/views.py +#, python-brace-format +msgid "Edit playlist “{playlist.name}”" +msgstr "Éditer la liste de lecture « {playlist.name} »" + #: pod/playlist/views.py msgid "JSON in wrong format" msgstr "JSON au mauvais format" @@ -7829,23 +7856,19 @@ msgstr "Autoriser" msgid "Redaction" msgstr "Rédaction" -#: pod/quiz/forms.py +#: pod/quiz/forms.py pod/quiz/templates/quiz/question_help_aside.html msgid "Short answer" msgstr "Réponse courte" -#: pod/quiz/forms.py -msgid "Long answer" -msgstr "Réponse longue" - #: pod/quiz/forms.py msgid "Choice" msgstr "Choix" -#: pod/quiz/forms.py +#: pod/quiz/forms.py pod/quiz/templates/quiz/question_help_aside.html msgid "Single choice" msgstr "Choix unique" -#: pod/quiz/forms.py +#: pod/quiz/forms.py pod/quiz/templates/quiz/question_help_aside.html msgid "Multiple choice" msgstr "Choix multiples" @@ -7944,14 +7967,6 @@ msgstr "Question à réponse courte" msgid "Write a short answer." msgstr "Écrivez une réponse courte." -#: pod/quiz/forms.py pod/quiz/models.py -msgid "Long answer question" -msgstr "Question à réponse longue" - -#: pod/quiz/forms.py -msgid "Write a long answer." -msgstr "Écrivez une réponse longue." - #: pod/quiz/models.py msgid "Choose a video associated with the quiz." msgstr "Veuillez choisir une vidéo associée au quiz." @@ -8070,14 +8085,6 @@ msgstr "Veuillez choisir une réponse entre 1 et 250 caractères." msgid "Short answer questions" msgstr "Questions à réponse courte" -#: pod/quiz/models.py -msgid "Please choose an answer." -msgstr "Veuillez choisir une réponse." - -#: pod/quiz/models.py -msgid "Long answer questions" -msgstr "Questions à réponse longue" - #: pod/quiz/templates/quiz/create_edit_quiz.html msgid "Add a question" msgstr "Ajouter une question" @@ -8118,6 +8125,58 @@ msgstr "Supprimer la question n°" msgid "One or more errors have been found in the question." msgstr "Une ou plusieurs erreurs ont été trouvées dans la question." +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"For short answer questions, ensure responses are between 1 and 250 " +"characters. This type of question is ideal for brief, specific answers." +msgstr "" +"Pour les questions à réponse courte, veillez à ce que les réponses soient " +"comprises entre 1 et 250 caractères. Ce type de question est idéal pour des " +"réponses brèves et spécifiques." + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"For short answer questions, please provide a response between 1 and 250 " +"characters. Be concise and specific." +msgstr "" +"Pour les questions à réponse courte, veuillez fournir une réponse de 1 à 250 " +"caractères. Soyez concis et précis." + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"Single choice questions require participants to select one answer from a " +"list of options. This is useful for questions with a clear, correct answer." +msgstr "" +"Les questions à choix unique demandent aux participants de sélectionner une " +"réponse parmi une liste d'options. Elles sont utiles pour les questions dont " +"la réponse est claire et correcte." + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"For single choice questions, select only one answer from the provided " +"options. Read each option carefully before making your selection." +msgstr "" +"Pour les questions à choix unique, ne sélectionnez qu'une seule réponse " +"parmi les options proposées. Lisez attentivement chaque option avant de " +"faire votre choix." + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"Multiple choice questions allow participants to select more than one answer. " +"Use this type for questions where more than one option could be correct." +msgstr "" +"Les questions à choix multiples permettent aux participants de sélectionner " +"plusieurs réponses. Utilisez ce type de question pour les questions où " +"plusieurs options peuvent être correctes." + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"For multiple choice questions, select all answers that apply. There may be " +"more than one correct answer." +msgstr "" +"Pour les questions à choix multiples, sélectionnez toutes les réponses qui " +"s'appliquent. Il peut y avoir plus d'une réponse correcte." + #: pod/quiz/templates/quiz/video_quiz.html msgid "This quiz is in draft." msgstr "Ce quiz est en mode brouillon." @@ -8147,6 +8206,14 @@ msgstr "Erreur constatée dans le formulaire" msgid "For the question “%(title)s”, %(error)s." msgstr "Pour la question « %(title)s », %(error)s." +#: pod/quiz/templates/quiz/video_quiz.html +msgid "" +"The creator of this quiz has decided not to display the answers and your " +"score." +msgstr "" +"Le créateur de ce quiz a décidé de ne pas afficher les réponses et votre " +"score." + #: pod/quiz/templates/quiz/video_quiz.html msgid "Correct answer:" msgstr "Réponse correcte :" @@ -8336,11 +8403,11 @@ msgstr "Publication automatique :" #: pod/recorder/models.py msgid "" -"If this box is checked, " -"the videos will be automatically assigned to the recorder manager." +"If this box is checked, the videos will be automatically assigned to the " +"recorder manager." msgstr "" -"Si vous cochez cette case " -"les vidéos seront automatiquement attribuées au gestionnaire de l'enregistreur." +"Si vous cochez cette case les vidéos seront automatiquement attribuées au " +"gestionnaire de l'enregistreur." #: pod/recorder/models.py msgid "Video type by default." @@ -8629,15 +8696,14 @@ msgstr "" msgid "" "

    Hello,
    a new recording has just been added on %(title_site)s from the " "recorder “%(recorder)s”.
    To assign it, just click on link below.

    %(link_url)s
    If you cannot click on the link, " -"just copy-paste it in your browser.

    Regards.

    " +"href=\"%(link_url)s\">%(link_url)s
    If you cannot click on the " +"link, just copy-paste it in your browser.

    Regards.

    " msgstr "" "

    Bonjour,
    un nouvel enregistrement a été ajouté sur la plateforme " "%(title_site)s à partir de l’enregistreur « %(recorder)s ».
    Pour " -"l’attribuer, cliquez sur le lien ci-dessous.

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

    Cordialement.

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

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

    Cordialement.

    " #: pod/recorder/views.py msgid "New recording added." @@ -8656,6 +8722,125 @@ msgstr "Suppression de l’enregistrement « %s »" msgid "Recorder for Studio not found." msgstr "Enregistreur studio non trouvé." +#: pod/speaker/forms.py +msgid "You can search speaker by first name, last name and job." +msgstr "" +"Vous pouvez rechercher un intervenant par leur prénom, nom et fonction." + +#: pod/speaker/models.py pod/speaker/templates/speaker/speakers_management.html +msgid "First name" +msgstr "Prénom" + +#: pod/speaker/models.py pod/speaker/templates/speaker/speakers_management.html +msgid "Last name" +msgstr "Nom" + +#: pod/speaker/models.py +msgid "Speakers" +msgstr "Intervenants" + +#: pod/speaker/models.py +msgid "Speaker's job" +msgstr "Fonction de l’intervenant" + +#: pod/speaker/templates/speaker/list_speaker.html +#: pod/speaker/templates/speaker/speakers_management.html +msgid "List of speakers" +msgstr "Liste des intervenants" + +#: pod/speaker/templates/speaker/list_speaker.html +msgid "First name / Last name" +msgstr "Prénom / Nom" + +#: pod/speaker/templates/speaker/list_speaker.html +#: pod/speaker/templates/speaker/speakers_management.html +msgid "Job" +msgstr "Fonction" + +#: pod/speaker/templates/speaker/list_speaker.html +#, python-format +msgid "Delete the speaker job “%(speaker_job)s”" +msgstr "Supprimer la fonction de l’intervenant « %(speaker_job)s »" + +#: pod/speaker/templates/speaker/speaker_modal.html +#: pod/speaker/templates/speaker/speakers_management.html +msgid "Add a speaker" +msgstr "Ajouter un intervenant" + +#: pod/speaker/templates/speaker/speaker_modal.html +msgid "Jobs" +msgstr "Fonctions" + +#: pod/speaker/templates/speaker/speaker_modal.html +msgid "Job title" +msgstr "Titre de la fonction" + +#: pod/speaker/templates/speaker/speaker_modal.html +msgid "Remove job" +msgstr "Supprimer la fonction" + +#: pod/speaker/templates/speaker/speaker_modal.html +msgid "Add job" +msgstr "Ajouter une fonction" + +#: pod/speaker/templates/speaker/speakers_management.html +msgid "List of speakers I can manage." +msgstr "Liste des intervenants que je peux gérer." + +#: pod/speaker/templates/speaker/speakers_management.html +msgid "No jobs assigned" +msgstr "Aucune fonction associée" + +#: pod/speaker/templates/speaker/speakers_management.html +#, python-format +msgid "Edit the speaker “%(speaker_firstname)s %(speaker_lastname)s”" +msgstr "Modifier l’intervenant « %(speaker_firstname)s %(speaker_lastname)s »" + +#: pod/speaker/templates/speaker/speakers_management.html +msgid "Please confirm you want to delete the speaker" +msgstr "Veuillez confirmer que vous souhaitez supprimer l’intervenant" + +#: pod/speaker/templates/speaker/speakers_management.html +#, python-format +msgid "Delete the speaker “%(speaker_firstname)s %(speaker_lastname)s”" +msgstr "Supprimer l’intervenant « %(speaker_firstname)s %(speaker_lastname)s »" + +#: pod/speaker/templates/speaker/speakers_management.html +msgid "No speakers found." +msgstr "Aucun intervenant trouvé." + +#: pod/speaker/views.py +msgid "You cannot access speaker management." +msgstr "Vous ne pouvez pas accéder à la gestion des intervenants." + +#: pod/speaker/views.py pod/video/tests/test_views.py pod/video/views.py +msgid "An action must be specified." +msgstr "Une action doit être précisée." + +#: pod/speaker/views.py +msgid "The speaker has been added." +msgstr "L’intervenant a été ajouté." + +#: pod/speaker/views.py +msgid "The speaker could not be added." +msgstr "L’intervenant n’a pu être ajouté." + +#: pod/speaker/views.py +msgid "The speaker has been deleted." +msgstr "L’intervenant a été supprimé." + +#: pod/speaker/views.py +msgid "The speaker could not be deleted." +msgstr "L’intervenant n’a pu être supprimé." + +#: pod/speaker/views.py +msgid "The speaker has been updated." +msgstr "L’intervenant a été modifié." + +#: pod/speaker/views.py +msgid "Speaker not found or invalid input." +msgstr "Intervenant non trouvé ou champ invalide." + #: pod/urls.py msgid "Pod Administration" msgstr "Administration de Pod" @@ -8823,29 +9008,39 @@ msgstr "" "liste, veuillez ne rien sélectionner et contactez nous pour expliquer vos " "besoins." +#: pod/video/forms.py +msgid "Visibility" +msgstr "Visibilité" + +#: pod/video/forms.py +msgid "In “Public” mode, the content is visible to everyone." +msgstr "Dans le mode « Public », le contenu est visible par tout le monde." + #: pod/video/forms.py msgid "" -"In “Draft mode”, the content shows nowhere and nobody else but you can see " -"it." +"In “Draft / Private” mode, the content shows nowhere and nobody else but you " +"can see it." msgstr "" -"En \"Mode brouillon\", le contenu n’apparaîtra nul part et uniquement vous " -"pourrez le voir." +"En mode “Brouillon / Privé“, le contenu n'apparaît nulle part et personne " +"d'autre que vous ne peut le voir. Vous pouvez ajouter des jetons pour " +"permettre un accès direct par lien." #: pod/video/forms.py msgid "" -"If you don’t select “Draft mode”, you can restrict the content access to " -"only people who can log in" +"In “Restricted access” mode, you can choose the restrictions for the video." msgstr "" -"Si vous ne sélectionnez pas le « Mode brouillon », vous pouvez restreindre " -"son accès aux personnes authentifiées" +"En mode « Accès restreint », vous pouvez choisir les restrictions pour la " +"video." #: pod/video/forms.py msgid "" -"If you don’t select “Draft mode”, you can add a password which will be asked " -"to anybody willing to watch your content." +"In “Restricted access” mode, you can add a password which will be asked to " +"anybody willing to watch your content. You can add tokens for allow direct " +"access by link." msgstr "" -"Si vous ne sélectionnez par le « Mode brouillon », vous pouvez ajouter un " -"mot de passe qui sera demandé aux utilisateurs souhaitant voir votre contenu." +"En mode « Accès restreint », vous pouvez ajouter un mot de passe qui sera " +"demandé à toute personne souhaitant regarder votre contenu. Vous pouvez " +"ajouter des jetons pour permettre un accès direct par lien." #: pod/video/forms.py msgid "" @@ -8855,6 +9050,26 @@ msgstr "" "Si votre vidéo est dans une liste de lecture vous ne pourrez pas lui ajouter " "de mot de passe." +#: pod/video/forms.py pod/video/models.py +msgid "Authentication restricted access" +msgstr "Accès restreint par authentification" + +#: pod/video/forms.py +msgid "" +"In “Restricted access” mode, you can restrict the content access to only " +"people who can log in" +msgstr "" +"En mode « Accès restreint », vous pouvez restreindre l’accès au contenu aux " +"personnes qui peuvent se connecter" + +#: pod/video/forms.py +msgid "" +"In “Restricted access” mode, you can restrict the content access to only " +"people who are in these groups." +msgstr "" +"Dans le mode « Accès restreint », vous pouvez restreindre l’accès au contenu " +"aux personnes qui sont dans ces groupes." + #: pod/video/forms.py pod/video/templates/videos/add_video.html msgid "" "Transcription is a speech recognition technology that transforms an oral " @@ -8898,6 +9113,10 @@ msgstr "" msgid "Users can only add videos to this channel" msgstr "Les utilisateurs peuvent uniquement ajouter des vidéos à cette chaîne" +#: pod/video/forms.py +msgid "Draft / Private" +msgstr "Brouillon / Privé" + #: pod/video/forms.py msgid "Source file" msgstr "Fichier source" @@ -8919,6 +9138,14 @@ msgstr "La date doit être antérieure ou égale au %(date)s." msgid "The deletion date can’t be earlier than today." msgstr "La date de suppression ne peut pas être antérieure à aujourd’hui." +#: pod/video/forms.py +msgid "" +"If you select restricted visibility for your video, you must check the " +"\"restricted access\" box or specify a password." +msgstr "" +"Si vous choisissez une visibilité restreinte pour votre vidéo, vous devez " +"cocher la case \"accès restreint\" ou spécifier un mot de passe." + #: pod/video/forms.py msgid "Delete video cannot be undo" msgstr "La suppression est définitive" @@ -9376,7 +9603,7 @@ msgstr "Votes" msgid "Category title" msgstr "Titre de la catégorie" -#: pod/video/models.py pod/video/templates/videos/category_modal.html +#: pod/video/models.py msgid "Category" msgstr "Catégorie" @@ -9646,6 +9873,11 @@ msgstr "" "Cette extension de fichier n’est pas présente dans les extensions " "autorisées :" +#: pod/video/templates/videos/card.html +#: pod/video/templates/videos/card_select.html +msgid "This content contains a quiz." +msgstr "Ce contenu contient un quiz." + #: pod/video/templates/videos/card.html #: pod/video/templates/videos/card_select.html #: pod/video/templatetags/video_tags.py @@ -9653,6 +9885,7 @@ msgid "This content is password protected." msgstr "Ce contenu est protégé par un mot de passe." #: pod/video/templates/videos/card.html +#: pod/video/templates/videos/card_select.html msgid "This content has restricted access." msgstr "Ce contenu est en accès restreint." @@ -9664,44 +9897,34 @@ msgstr "Ce contenu est chapitré." #: pod/video/templates/videos/card.html #: pod/video/templates/videos/card_select.html -#: pod/video/templates/videos/category_modal_card.html #: pod/video/templates/videos/video_row_select.html msgid "Video content." msgstr "Contenu vidéo." #: pod/video/templates/videos/card.html #: pod/video/templates/videos/card_select.html -#: pod/video/templates/videos/category_modal_card.html #: pod/video/templates/videos/video_row_select.html msgid "Audio content." msgstr "Contenu audio." #: pod/video/templates/videos/card_select.html #: pod/video/templates/videos/video_row_select.html -msgid "Selected" -msgstr "Sélectionné" - -#: pod/video/templates/videos/category_modal.html -msgid "category title" -msgstr "titre catégorie" +#, python-format +msgid "Select video “%(video_title)s”" +msgstr "Sélectionner la vidéo « %(video_title)s »" #: pod/video/templates/videos/category_modal.html msgid "Check the videos to add in the category" msgstr "Cocher les vidéos à ajouter dans la catégorie" -#: pod/video/templates/videos/category_modal.html -msgid "Save category" -msgstr "Sauvegarder catégorie" - -#: pod/video/templates/videos/category_modal.html -#: pod/video/templates/videos/filter_aside_category.html -msgid "Delete the category" -msgstr "Supprimer la catégorie" - #: pod/video/templates/videos/category_modal.html msgid "Permanently delete your category?" msgstr "Supprimer définitivement votre catégorie ?" +#: pod/video/templates/videos/category_modal.html +msgid "Save category" +msgstr "Sauvegarder la catégorie" + #: pod/video/templates/videos/change_video_owner.html msgid "Old owner" msgstr "Ancien propriétaire" @@ -9784,6 +10007,14 @@ msgid_plural "%(counter)s videos" msgstr[0] "%(counter)s vidéo" msgstr[1] "%(counter)s vidéos" +#: pod/video/templates/videos/dashboard.html +msgid "Select all" +msgstr "Tout sélectionner" + +#: pod/video/templates/videos/dashboard.html +msgid "Clear selection" +msgstr "Effacer la sélection" + #: pod/video/templates/videos/dashboard.html msgid "" "You have not uploaded any videos yet, please use the ”Add a new video” " @@ -9820,7 +10051,17 @@ msgstr "Plier/déplier les filtres par « Disciplines »" msgid "Fold/Unfold “Tags” filters" msgstr "Plier/déplier les filtres par « Mots-clés »" -#: pod/video/templates/videos/filter_aside_category.html +#: pod/video/templates/videos/filter_aside_categories_list.html +#: pod/video/views.py +msgid "Edit category" +msgstr "Éditer la catégorie" + +#: pod/video/templates/videos/filter_aside_categories_list.html +#: pod/video/views.py +msgid "Delete category" +msgstr "Supprimer la catégorie" + +#: pod/video/templates/videos/filter_aside_category.html pod/video/views.py msgid "Add new category" msgstr "Ajouter une catégorie" @@ -9828,69 +10069,58 @@ msgstr "Ajouter une catégorie" msgid "Type at least 3 chars to filter categories" msgstr "Tapez au moins 3 lettres pour filtrer vos catégories" -#: pod/video/templates/videos/filter_aside_category.html -msgid "Edit the category" -msgstr "Éditer la catégorie" - #: pod/video/templates/videos/footer_link.html msgid "Links" msgstr "Liens" #: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html msgid "Remove from playlist" msgstr "Retirer de la liste de lecture" #: pod/video/templates/videos/link_video.html #: pod/video/templates/videos/video-info.html -#: pod/video/templates/videos/video_row_select.html msgid "Remove from favorite" msgstr "Retirer des favoris" #: pod/video/templates/videos/link_video.html #: pod/video/templates/videos/video-info.html -#: pod/video/templates/videos/video_row_select.html msgid "Add in favorite" msgstr "Ajouter aux favoris" #: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html msgid "Edit the video" msgstr "Éditer la vidéo" #: pod/video/templates/videos/link_video.html +msgid "Video default version" +msgstr "Version par défaut de la vidéo" + +#: pod/video/templates/videos/link_video.html +#: pod/video/templates/videos/video-all-info.html +msgid "Original version" +msgstr "Version originale" + +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Enhance the video with AI" msgstr "Améliorer la vidéo avec l’IA" -#: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Complete the video" msgstr "Compléter la vidéo" -#: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Chapter the video" msgstr "Chapitrer la vidéo" -#: pod/video/templates/videos/link_video.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Dress the video" msgstr "Habiller la vidéo" -#: pod/video/templates/videos/link_video.html +#: pod/video/templates/videos/link_video_dropdown_menu.html #: pod/video/templates/videos/video_edit.html -#: pod/video/templates/videos/video_row_select.html msgid "Delete the video" msgstr "Supprimer la vidéo" -#: pod/video/templates/videos/link_video.html -msgid "Video default version" -msgstr "Version par défaut de la vidéo" - -#: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video-all-info.html -msgid "Original version" -msgstr "Version originale" - #: pod/video/templates/videos/video-all-info.html msgid "Report the video" msgstr "Signaler la vidéo" @@ -10022,6 +10252,10 @@ msgstr "Envoyer un courriel à « %(contributor_name)s »" msgid "Contributor web link" msgstr "Lien du contributeur" +#: pod/video/templates/videos/video-info.html +msgid "Speaker(s):" +msgstr "Intervenant(s) :" + #: pod/video/templates/videos/video-info.html msgid "Updated on:" msgstr "Mis à jour le :" @@ -10407,13 +10641,9 @@ msgstr "Description du thème" msgid "The video is currently waiting for Aristote AI treatment." msgstr "La vidéo est actuellement en attente du traitement par l’IA Aristote." -#: pod/video/templates/videos/video_page_content.html -msgid "" -"The video was treated by Aristote. You must verify and validate the " -"processing by pressing the robot icon." -msgstr "" -"La vidéo a été traitée par Aristote. Vous devez vérifier et valider le " -"traitement en appuyant sur l’icône du robot." +#: pod/video/templates/videos/video_row_select.html +msgid "Selected" +msgstr "Sélectionné" #: pod/video/templates/videos/video_row_select.html msgid "Toggle actions menu" @@ -10506,10 +10736,6 @@ msgstr "" msgid "-- sorry, no translation provided --" msgstr "-- désolé, aucune traduction fournie --" -#: pod/video/tests/test_views.py pod/video/views.py -msgid "An action must be specified." -msgstr "Une action doit être précisée." - #: pod/video/tests/test_views.py pod/video/views.py msgid "A token has been created." msgstr "Un jeton a été créé." @@ -10747,17 +10973,17 @@ msgstr "" msgid "You do not have rights to delete this comment" msgstr "Vous n’avez pas les droits pour supprimer ce commentaire" -#: pod/video/views.py -msgid "One or many videos already have a category." -msgstr "Une ou plusieurs vidéos possèdent déjà une catégorie." - #: pod/video/views.py msgid "Title field is required" msgstr "Champ de titre est requis" #: pod/video/views.py -msgid "Method Not Allowed" -msgstr "Méthode non autorisée" +msgid "One or many videos already have a category." +msgstr "Une ou plusieurs vidéos possèdent déjà une catégorie." + +#: pod/video/views.py +msgid "Category successfully added." +msgstr "Catégorie ajoutée avec succès." #: pod/video/views.py msgid "Category updated successfully." @@ -10767,6 +10993,10 @@ msgstr "Catégorie mise à jour avec succès." msgid "You do not have rights to edit this category" msgstr "Vous n’avez pas les droits pour éditer cette catégorie" +#: pod/video/views.py +msgid "Category successfully deleted." +msgstr "Categorie supprimée avec succès." + #: pod/video/views.py msgid "You do not have rights to delete this category" msgstr "Vous n’avez pas les droits pour supprimer cette catégorie" @@ -10952,5 +11182,30 @@ msgstr "Résultats de la recherche" msgid "Esup-Pod xAPI" msgstr "xAPI Esup-Pod" +#~ msgid "Long answer" +#~ msgstr "Réponse longue" + +#~ msgid "Long answer question" +#~ msgstr "Question à réponse longue" + +#~ msgid "Write a long answer." +#~ msgstr "Écrivez une réponse longue." + +#~ msgid "Please choose an answer." +#~ msgstr "Veuillez choisir une réponse." + +#~ msgid "Long answer questions" +#~ msgstr "Questions à réponse longue" + +#~ msgid "Restricted" +#~ msgstr "Restreint" + +#~ msgid "" +#~ "The video was treated by Aristote. You must verify and validate the " +#~ "processing by pressing the robot icon." +#~ msgstr "" +#~ "La vidéo a été traitée par Aristote. Vous devez vérifier et valider le " +#~ "traitement en appuyant sur l’icône du robot." + #~ msgid "Quiz(zes)" #~ msgstr "Quiz" diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.mo b/pod/locale/fr/LC_MESSAGES/djangojs.mo index 74cf27b6fb..50aa0419ad 100644 Binary files a/pod/locale/fr/LC_MESSAGES/djangojs.mo and b/pod/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.po b/pod/locale/fr/LC_MESSAGES/djangojs.po index 0be15ed84a..ea75159d85 100644 --- a/pod/locale/fr/LC_MESSAGES/djangojs.po +++ b/pod/locale/fr/LC_MESSAGES/djangojs.po @@ -5,9 +5,9 @@ msgid "" msgstr "" "Project-Id-Version: Esup-Pod\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-11 07:48+0000\n" +"POT-Creation-Date: 2024-07-17 09:34+0000\n" "PO-Revision-Date: \n" -"Last-Translator: obado \n" +"Last-Translator: AymericJak \n" "Language-Team: \n" "Language: fr\n" "MIME-Version: 1.0\n" @@ -235,6 +235,10 @@ msgstr "Êtes-vous sûr(e) de vouloir supprimer ce fichier ?" msgid "Are you sure you want to delete this contributor?" msgstr "Êtes-vous sûr(e) de vouloir supprimer ce contributeur ?" +#: pod/completion/static/js/completion.js +msgid "Are you sure you want to delete this speaker?" +msgstr "Êtes-vous sûr(e) de vouloir supprimer cet intervenant ?" + #: pod/completion/static/js/completion.js msgid "Are you sure you want to delete this document?" msgstr "Êtes-vous sûr(e) de vouloir supprimer ce document ?" @@ -415,6 +419,7 @@ msgstr "Une erreur s’est produite lors du chargement des diffuseurs…" #: pod/live/static/js/filter_aside_event_list.js #: pod/playlist/static/playlist/js/filter-aside-playlist-list-refresh.js #: pod/video/static/js/filter_aside_video_list_refresh.js +#: pod/video/static/js/video_category.js msgid "An Error occurred while processing." msgstr "Une erreur est survenue durant l’exécution du processus." @@ -556,6 +561,8 @@ msgid "Change image" msgstr "Changer d’image" #: pod/podfile/static/podfile/js/filewidget.js +#, fuzzy +#| msgid "Change" msgid "Change file" msgstr "Changer de fichier" @@ -652,14 +659,6 @@ msgstr "La réponse courte" msgid "Short answer" msgstr "Réponse courte" -#: pod/quiz/static/quiz/js/create-quiz.js -msgid "The long answer" -msgstr "La réponse longue" - -#: pod/quiz/static/quiz/js/create-quiz.js -msgid "Long answer" -msgstr "Réponse longue" - #: pod/quiz/static/quiz/js/create-quiz.js #, javascript-format msgid "Select the choice #%s as correct answer." @@ -706,6 +705,22 @@ msgstr "Réponse correcte envoyée" msgid "Incorrect answer given" msgstr "Réponse incorrecte envoyée" +#: pod/speaker/static/speaker/js/speakers-management.js +msgid "Job title" +msgstr "Titre de la fonction" + +#: pod/speaker/static/speaker/js/speakers-management.js +msgid "Remove job" +msgstr "Supprimer la fonction" + +#: pod/speaker/static/speaker/js/speakers-management.js +msgid "Edit this speaker" +msgstr "Modifier l’intervenant" + +#: pod/speaker/static/speaker/js/speakers-management.js +msgid "Add a speaker" +msgstr "Ajouter un intervenant" + #: pod/video/static/js/ajax-display-channels.js msgid "%(count)s channel" msgid_plural "%(count)s channels" @@ -828,47 +843,38 @@ msgstr[0] "%(count)s vidéo trouvée" msgstr[1] "%(count)s vidéos trouvées" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "This content is password protected." msgstr "Ce contenu est protégé par mot de passe." #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "This content is chaptered." msgstr "Ce contenu est chapitré." #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "This content is in draft." msgstr "Ce contenu est en brouillon." #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Video content." msgstr "Contenu vidéo." #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Audio content." msgstr "Contenu audio." #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Edit the video" msgstr "Éditer la vidéo" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Complete the video" msgstr "Compléter la vidéo" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Chapter the video" msgstr "Chapitrer la vidéo" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Delete the video" msgstr "Supprimer la vidéo" @@ -896,75 +902,6 @@ msgstr "Avancer de 10 secondes dans la video" msgid "Quality" msgstr "Qualité" -#: pod/video/static/js/video_category.js -msgid "Category changes saved successfully" -msgstr "Les changements sur la catégorie ont été sauvegardés avec succès" - -#: pod/video/static/js/video_category.js -msgid "You cannot add two categories with the same title." -msgstr "Vous ne pouvez pas ajouter deux catégories avec le même titre." - -#: pod/video/static/js/video_category.js -msgid "Category deleted successfully" -msgstr "Catégorie supprimée avec succès" - -#: pod/video/static/js/video_category.js -msgid "An error occured, please refresh the page and try again." -msgstr "" -"Une erreur est survenue, veuillez recharger la page et réessayer à nouveau." - -#: pod/video/static/js/video_category.js -msgid "Category title field is required." -msgstr "Le champ category titre est requis." - -#: pod/video/static/js/video_category.js -msgid "Save category" -msgstr "Sauvegarder catégorie" - -#: pod/video/static/js/video_category.js -msgid "You have no content without a category." -msgstr "Vous n’avez aucun contenu sans catégorie." - -#: pod/video/static/js/video_category.js -msgid "videos found" -msgstr "vidéos trouvées" - -#: pod/video/static/js/video_category.js -msgid "video found" -msgstr "vidéo trouvée" - -#: pod/video/static/js/video_category.js -msgid "Sorry, no video found" -msgstr "Désolé, aucune vidéo trouvée" - -#: pod/video/static/js/video_category.js -msgid "Edit the category" -msgstr "Éditer la catégorie" - -#: pod/video/static/js/video_category.js -msgid "Delete the category" -msgstr "Supprimer la catégorie" - -#: pod/video/static/js/video_category.js -msgid "Success!" -msgstr "Succès !" - -#: pod/video/static/js/video_category.js -msgid "Error…" -msgstr "Erreur…" - -#: pod/video/static/js/video_category.js -msgid "Category created successfully" -msgstr "Catégorie créée avec succès" - -#: pod/video/static/js/video_category.js -msgid "Add new category" -msgstr "Ajouter une catégorie" - -#: pod/video/static/js/video_category.js -msgid "Create category" -msgstr "Créer catégorie" - #: pod/video/static/js/video_edit.js msgid "Select the general type of the video." msgstr "Sélectionnez le type général de vidéo." @@ -1034,3 +971,9 @@ msgstr "Ajouts en favoris total depuis la création" #: pod/video/static/js/video_stats_view.js msgid "Slug" msgstr "Titre court" + +#~ msgid "The long answer" +#~ msgstr "La réponse longue" + +#~ msgid "Long answer" +#~ msgstr "Réponse longue" diff --git a/pod/locale/nl/LC_MESSAGES/django.mo b/pod/locale/nl/LC_MESSAGES/django.mo index 6aa3517b50..1080142dfd 100644 Binary files a/pod/locale/nl/LC_MESSAGES/django.mo and b/pod/locale/nl/LC_MESSAGES/django.mo differ diff --git a/pod/locale/nl/LC_MESSAGES/django.po b/pod/locale/nl/LC_MESSAGES/django.po index 2e089d5d21..0c9cb692b5 100644 --- a/pod/locale/nl/LC_MESSAGES/django.po +++ b/pod/locale/nl/LC_MESSAGES/django.po @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: Pod\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-11 07:48+0000\n" -"PO-Revision-Date: 2024-06-04 16:20+0200\n" +"POT-Creation-Date: 2024-07-17 09:34+0000\n" +"PO-Revision-Date: 2024-07-04 17:54+0200\n" "Last-Translator: obado \n" "Language-Team: \n" "Language: nl_NL\n" @@ -21,10 +21,8 @@ msgid "Artificial Intelligence Enhancement" msgstr "" #: pod/ai_enhancement/forms.py -#, fuzzy -#| msgid "Please enter a title." msgid "Choose the title." -msgstr "Voer een titel in." +msgstr "" #: pod/ai_enhancement/forms.py msgid "The initial title is loading…" @@ -43,10 +41,8 @@ msgid "The title proposed by the Aristote AI." msgstr "" #: pod/ai_enhancement/forms.py -#, fuzzy -#| msgid "Please enter a title." msgid "Choose the description." -msgstr "Voer een titel in." +msgstr "" #: pod/ai_enhancement/forms.py msgid "The initial description is loading…" @@ -99,7 +95,8 @@ msgstr "" #: pod/enrichment/templates/enrichment/list_enrichment.html pod/live/models.py #: pod/main/models.py pod/main/views.py pod/playlist/forms.py #: pod/playlist/models.py pod/quiz/forms.py pod/quiz/models.py -#: pod/video/models.py pod/video/templates/channel/list_theme.html +#: pod/speaker/models.py pod/video/models.py +#: pod/video/templates/channel/list_theme.html #: pod/video/templates/videos/video_sort_select.html msgid "Title" msgstr "Titel" @@ -120,10 +117,8 @@ msgid "Description" msgstr "" #: pod/ai_enhancement/forms.py -#, fuzzy -#| msgid "Please enter a title." msgid "Please choose a description." -msgstr "Voer een titel in." +msgstr "" #: pod/ai_enhancement/forms.py msgid "" @@ -151,10 +146,8 @@ msgid "Discipline" msgstr "" #: pod/ai_enhancement/forms.py -#, fuzzy -#| msgid "Please enter a title." msgid "Please choose the discipline of your video." -msgstr "Voer een titel in." +msgstr "" #: pod/ai_enhancement/forms.py msgid "I agree to use third-party services" @@ -196,10 +189,8 @@ msgid "Video" msgstr "" #: pod/ai_enhancement/models.py -#, fuzzy -#| msgid "Select the video to move by clicking and holding." msgid "Select the video to enhance with AI" -msgstr "Sélectionnez la vidéo à déplacer en cliquant et en maintenant." +msgstr "" #: pod/ai_enhancement/models.py msgid "Created at" @@ -500,10 +491,8 @@ msgid "No enhancement found." msgstr "" #: pod/ai_enhancement/views.py -#, fuzzy -#| msgid "Select the video to move by clicking and holding." msgid "Delete the video enhancement" -msgstr "Sélectionnez la vidéo à déplacer en cliquant et en maintenant." +msgstr "" #: pod/ai_enhancement/views.py msgid "You cannot use AI to improve this video." @@ -913,7 +902,6 @@ msgstr "" #: pod/bbb/models.py pod/live/models.py pod/live/templates/live/event_card.html #: pod/meeting/models.py pod/recorder/models.py pod/video/forms.py -#: pod/video/models.py msgid "Restricted access" msgstr "" @@ -1296,6 +1284,8 @@ msgstr "" #: pod/playlist/templates/playlist/add_or_edit.html #: pod/quiz/templates/quiz/create_edit_quiz.html #: pod/recorder/templates/recorder/add_recording.html +#: pod/speaker/templates/speaker/form_speaker.html +#: pod/speaker/templates/speaker/speaker_modal.html #: pod/video/templates/channel/channel_edit.html #: pod/video/templates/channel/form_theme.html #: pod/video/templates/videos/video_note_comments_display.html @@ -1311,6 +1301,8 @@ msgstr "" #: pod/completion/templates/track/form_track.html #: pod/enrichment/templates/enrichment/form_enrichment.html #: pod/playlist/templates/playlist/playlist.html +#: pod/speaker/templates/speaker/form_speaker.html +#: pod/speaker/templates/speaker/speaker_modal.html #: pod/video/templates/channel/form_theme.html #: pod/video/templates/videos/category_modal.html #: pod/video/templates/videos/dashboard_modal.html @@ -1335,6 +1327,8 @@ msgstr "" #: pod/completion/templates/track/list_track.html #: pod/dressing/templates/my_dressings.html #: pod/enrichment/templates/enrichment/list_enrichment.html +#: pod/speaker/templates/speaker/list_speaker.html +#: pod/speaker/templates/speaker/speakers_management.html #: pod/video/templates/channel/list_theme.html #: pod/video/templates/videos/video_access_tokens.html msgid "Actions" @@ -1372,6 +1366,7 @@ msgstr "" #: pod/playlist/templates/playlist/delete.html #: pod/quiz/templates/quiz/delete_quiz.html #: pod/recorder/templates/recorder/record_delete.html +#: pod/speaker/templates/speaker/list_speaker.html #: pod/video/templates/channel/list_theme.html #: pod/video/templates/videos/category_modal.html #: pod/video/templates/videos/dashboard.html @@ -1423,6 +1418,7 @@ msgstr "" #: pod/chapter/templates/video_chapter.html #: pod/completion/templates/video_completion.html #: pod/cut/templates/video_cut.html pod/dressing/templates/video_dressing.html +#: pod/quiz/templates/quiz/question_help_aside.html msgid "Help" msgstr "" @@ -1526,7 +1522,7 @@ msgstr "" msgid "Speaking roles" msgstr "" -#: pod/completion/models.py +#: pod/completion/models.py pod/speaker/apps.py pod/speaker/models.py msgid "Speaker" msgstr "" @@ -1539,7 +1535,7 @@ msgid "captions" msgstr "" #: pod/completion/models.py -msgid "lastname / firstname" +msgid "last name / first name" msgstr "" #: pod/completion/models.py @@ -1785,6 +1781,7 @@ msgstr "" #: pod/completion/templates/document/form_document.html #: pod/completion/templates/overlay/form_overlay.html #: pod/completion/templates/track/form_track.html +#: pod/speaker/templates/speaker/form_speaker.html msgid "Your form contains errors:" msgstr "" @@ -1793,7 +1790,7 @@ msgid "List of contributors" msgstr "" #: pod/completion/templates/contributor/list_contributor.html -msgid "Lastname / Firstname" +msgid "Last name / First name" msgstr "" #: pod/completion/templates/contributor/list_contributor.html @@ -1914,8 +1911,7 @@ msgid "Caption" msgstr "" #: pod/completion/templates/video_caption_maker.html -#: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Play" msgstr "" @@ -2035,6 +2031,14 @@ msgstr "" msgid "Add a new contributor" msgstr "" +#: pod/completion/templates/video_completion.html +msgid "Speaker(s)" +msgstr "" + +#: pod/completion/templates/video_completion.html +msgid "Add a new speaker" +msgstr "" + #: pod/completion/templates/video_completion.html msgid "Subtitle(s) and Caption(s)" msgstr "" @@ -2088,6 +2092,16 @@ msgid "" "example)." msgstr "" +#: pod/completion/templates/video_completion.html +msgid "List of speakers related to this video." +msgstr "" + +#: pod/completion/templates/video_completion.html +msgid "" +"You can add speakers to this video by searching by their last name, first " +"name or job. If you can't find the speaker, contact a super admin." +msgstr "" + #: pod/completion/templates/video_completion.html msgid "Subtitle(s) and/or captions(s) related to this video." msgstr "" @@ -2204,6 +2218,15 @@ msgstr "" msgid "Edit the contributor “%s”" msgstr "" +#: pod/completion/views.py +#, python-format +msgid "Add a new speaker to the video “%s”" +msgstr "" + +#: pod/completion/views.py +msgid "The speaker has been saved." +msgstr "" + #: pod/cut/apps.py pod/cut/models.py msgid "Video cuts" msgstr "" @@ -2216,8 +2239,8 @@ msgstr "" msgid "Please select values between 00:00:00 and " msgstr "" -#: pod/cut/templates/video_cut.html pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html +#: pod/cut/templates/video_cut.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Cut the video" msgstr "" @@ -2260,7 +2283,6 @@ msgstr "" #: pod/cut/templates/video_cut.html pod/dressing/templates/video_dressing.html #: pod/playlist/templates/playlist/filter_aside.html -#: pod/video/templates/videos/dashboard.html #: pod/video/templates/videos/filter_aside.html msgid "Reset" msgstr "" @@ -2351,7 +2373,7 @@ msgstr "" #: pod/dressing/models.py pod/enrichment/forms.py pod/enrichment/models.py #: pod/live/models.py pod/meeting/models.py pod/podfile/models.py -#: pod/recorder/models.py pod/video/models.py +#: pod/recorder/models.py pod/video/forms.py pod/video/models.py msgid "Groups" msgstr "" @@ -3569,7 +3591,7 @@ msgid "Url of the embedded site to display on aside" msgstr "" #: pod/live/models.py pod/quiz/forms.py pod/quiz/models.py -#: pod/recorder/models.py pod/video/forms.py pod/video/models.py +#: pod/recorder/models.py pod/video/models.py msgid "Draft" msgstr "" @@ -4081,13 +4103,13 @@ msgid "Sorry, no event found." msgstr "" #: pod/live/templates/live/events_list.html -#: pod/video/templates/videos/category_modal.html +#: pod/video/templates/videos/category_modal_video_list.html #: pod/video/templates/videos/paginator.html msgid "Previous page" msgstr "" #: pod/live/templates/live/events_list.html -#: pod/video/templates/videos/category_modal.html +#: pod/video/templates/videos/category_modal_video_list.html #: pod/video/templates/videos/paginator.html msgid "Next page" msgstr "" @@ -5459,6 +5481,10 @@ msgstr "" msgid "Claim a record" msgstr "" +#: pod/main/templates/navbar.html pod/speaker/views.py +msgid "Speakers management" +msgstr "" + #: pod/main/templates/navbar.html pod/progressive_web_app/templates/debug.html msgid "Notifications settings" msgstr "" @@ -6255,22 +6281,27 @@ msgid "Authenticate" msgstr "" #: pod/meeting/templates/meeting/link_meeting.html +#: pod/video/templates/videos/link_video.html +msgid "More options" +msgstr "" + +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Copy the direct join link" msgstr "" -#: pod/meeting/templates/meeting/link_meeting.html +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Copy the join link" msgstr "" -#: pod/meeting/templates/meeting/link_meeting.html +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Invite to the meeting" msgstr "" -#: pod/meeting/templates/meeting/link_meeting.html +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Get the recordings of the meeting" msgstr "" -#: pod/meeting/templates/meeting/link_meeting.html +#: pod/meeting/templates/meeting/link_meeting_dropdown_menu.html msgid "Delete the meeting" msgstr "" @@ -6674,7 +6705,7 @@ msgstr "" #: pod/playlist/models.py #: pod/playlist/templates/playlist/playlist_visibility_icon.html -#: pod/video/models.py +#: pod/video/forms.py pod/video/models.py #: pod/video/templates/videos/video_note_comments_display.html #: pod/video/templates/videos/video_note_display.html msgid "Public" @@ -6761,7 +6792,7 @@ msgstr "" #: pod/playlist/templates/playlist/add_or_edit.html #: pod/playlist/templates/playlist/playlist_link.html -#: pod/playlist/tests/test_views.py pod/playlist/views.py +#: pod/playlist/tests/test_views.py msgid "Edit the playlist" msgstr "" @@ -7081,6 +7112,11 @@ msgstr "" msgid "The data sent to create the playlist are invalid." msgstr "" +#: pod/playlist/views.py +#, python-brace-format +msgid "Edit playlist “{playlist.name}”" +msgstr "" + #: pod/playlist/views.py msgid "JSON in wrong format" msgstr "" @@ -7313,23 +7349,19 @@ msgstr "" msgid "Redaction" msgstr "" -#: pod/quiz/forms.py +#: pod/quiz/forms.py pod/quiz/templates/quiz/question_help_aside.html msgid "Short answer" msgstr "" -#: pod/quiz/forms.py -msgid "Long answer" -msgstr "" - #: pod/quiz/forms.py msgid "Choice" msgstr "" -#: pod/quiz/forms.py +#: pod/quiz/forms.py pod/quiz/templates/quiz/question_help_aside.html msgid "Single choice" msgstr "" -#: pod/quiz/forms.py +#: pod/quiz/forms.py pod/quiz/templates/quiz/question_help_aside.html msgid "Multiple choice" msgstr "" @@ -7371,10 +7403,8 @@ msgid "Question type" msgstr "" #: pod/quiz/forms.py -#, fuzzy -#| msgid "Please enter a title." msgid "Please choose the question type." -msgstr "Voer een titel in." +msgstr "" #: pod/quiz/forms.py pod/quiz/models.py msgid "Connected user only" @@ -7407,20 +7437,16 @@ msgid "Single choice question" msgstr "" #: pod/quiz/forms.py -#, fuzzy -#| msgid "Please enter a title." msgid "Please choose one answer." -msgstr "Voer een titel in." +msgstr "" #: pod/quiz/forms.py pod/quiz/models.py msgid "Multiple choice question" msgstr "" #: pod/quiz/forms.py -#, fuzzy -#| msgid "Please enter a title." msgid "Please check any answers you want." -msgstr "Voer een titel in." +msgstr "" #: pod/quiz/forms.py pod/quiz/models.py msgid "Short answer question" @@ -7430,14 +7456,6 @@ msgstr "" msgid "Write a short answer." msgstr "" -#: pod/quiz/forms.py pod/quiz/models.py -msgid "Long answer question" -msgstr "" - -#: pod/quiz/forms.py -msgid "Write a long answer." -msgstr "" - #: pod/quiz/models.py msgid "Choose a video associated with the quiz." msgstr "" @@ -7551,14 +7569,6 @@ msgstr "" msgid "Short answer questions" msgstr "" -#: pod/quiz/models.py -msgid "Please choose an answer." -msgstr "" - -#: pod/quiz/models.py -msgid "Long answer questions" -msgstr "" - #: pod/quiz/templates/quiz/create_edit_quiz.html msgid "Add a question" msgstr "" @@ -7590,15 +7600,49 @@ msgid "Question #" msgstr "" #: pod/quiz/templates/quiz/question_form.html -#, fuzzy -#| msgid "Please enter a title." msgid "Delete the question #" -msgstr "Voer een titel in." +msgstr "" #: pod/quiz/templates/quiz/question_form.html msgid "One or more errors have been found in the question." msgstr "" +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"For short answer questions, ensure responses are between 1 and 250 " +"characters. This type of question is ideal for brief, specific answers." +msgstr "" + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"For short answer questions, please provide a response between 1 and 250 " +"characters. Be concise and specific." +msgstr "" + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"Single choice questions require participants to select one answer from a " +"list of options. This is useful for questions with a clear, correct answer." +msgstr "" + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"For single choice questions, select only one answer from the provided " +"options. Read each option carefully before making your selection." +msgstr "" + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"Multiple choice questions allow participants to select more than one answer. " +"Use this type for questions where more than one option could be correct." +msgstr "" + +#: pod/quiz/templates/quiz/question_help_aside.html +msgid "" +"For multiple choice questions, select all answers that apply. There may be " +"more than one correct answer." +msgstr "" + #: pod/quiz/templates/quiz/video_quiz.html msgid "This quiz is in draft." msgstr "" @@ -7628,6 +7672,12 @@ msgstr "" msgid "For the question “%(title)s”, %(error)s." msgstr "" +#: pod/quiz/templates/quiz/video_quiz.html +msgid "" +"The creator of this quiz has decided not to display the answers and your " +"score." +msgstr "" + #: pod/quiz/templates/quiz/video_quiz.html msgid "Correct answer:" msgstr "" @@ -7695,10 +7745,8 @@ msgid "The data sent to create the quiz are invalid." msgstr "" #: pod/quiz/views.py -#, fuzzy -#| msgid "Please enter a title." msgid "You have to choose at least one answer" -msgstr "Voer een titel in." +msgstr "" #: pod/quiz/views.py #, python-format @@ -7805,6 +7853,16 @@ msgid "" "the owner except that they can’t delete the published videos." msgstr "" +#: pod/recorder/models.py +msgid "Automatic publishing:" +msgstr "" + +#: pod/recorder/models.py +msgid "" +"If this box is checked, the videos will be automatically assigned to the " +"recorder manager." +msgstr "" + #: pod/recorder/models.py msgid "Video type by default." msgstr "" @@ -8068,10 +8126,10 @@ msgstr "" #: pod/recorder/views.py #, python-format msgid "" -"

    Hello,
    a new recording has just be added on %(title_site)s from the " -"recorder “%(recorder)s”.
    To add it, just click on link below.

    %(link_url)s
    If you cannot click on link, " -"just copy-paste it in your browser.

    Regards.

    " +"

    Hello,
    a new recording has just been added on %(title_site)s from the " +"recorder “%(recorder)s”.
    To assign it, just click on link below.

    %(link_url)s
    If you cannot click on the " +"link, just copy-paste it in your browser.

    Regards.

    " msgstr "" #: pod/recorder/views.py @@ -8091,6 +8149,126 @@ msgstr "" msgid "Recorder for Studio not found." msgstr "" +#: pod/speaker/forms.py +msgid "You can search speaker by first name, last name and job." +msgstr "" + +#: pod/speaker/models.py pod/speaker/templates/speaker/speakers_management.html +msgid "First name" +msgstr "" + +#: pod/speaker/models.py pod/speaker/templates/speaker/speakers_management.html +msgid "Last name" +msgstr "" + +#: pod/speaker/models.py +msgid "Speakers" +msgstr "" + +#: pod/speaker/models.py +msgid "Speaker's job" +msgstr "" + +#: pod/speaker/templates/speaker/list_speaker.html +#: pod/speaker/templates/speaker/speakers_management.html +msgid "List of speakers" +msgstr "" + +#: pod/speaker/templates/speaker/list_speaker.html +msgid "First name / Last name" +msgstr "" + +#: pod/speaker/templates/speaker/list_speaker.html +#: pod/speaker/templates/speaker/speakers_management.html +msgid "Job" +msgstr "" + +#: pod/speaker/templates/speaker/list_speaker.html +#, python-format +msgid "Delete the speaker job “%(speaker_job)s”" +msgstr "" + +#: pod/speaker/templates/speaker/speaker_modal.html +#: pod/speaker/templates/speaker/speakers_management.html +msgid "Add a speaker" +msgstr "" + +#: pod/speaker/templates/speaker/speaker_modal.html +msgid "Jobs" +msgstr "" + +#: pod/speaker/templates/speaker/speaker_modal.html +#, fuzzy +#| msgid "title" +msgid "Job title" +msgstr "titel" + +#: pod/speaker/templates/speaker/speaker_modal.html +msgid "Remove job" +msgstr "" + +#: pod/speaker/templates/speaker/speaker_modal.html +msgid "Add job" +msgstr "" + +#: pod/speaker/templates/speaker/speakers_management.html +msgid "List of speakers I can manage." +msgstr "" + +#: pod/speaker/templates/speaker/speakers_management.html +msgid "No jobs assigned" +msgstr "" + +#: pod/speaker/templates/speaker/speakers_management.html +#, python-format +msgid "Edit the speaker “%(speaker_firstname)s %(speaker_lastname)s”" +msgstr "" + +#: pod/speaker/templates/speaker/speakers_management.html +msgid "Please confirm you want to delete the speaker" +msgstr "" + +#: pod/speaker/templates/speaker/speakers_management.html +#, python-format +msgid "Delete the speaker “%(speaker_firstname)s %(speaker_lastname)s”" +msgstr "" + +#: pod/speaker/templates/speaker/speakers_management.html +msgid "No speakers found." +msgstr "" + +#: pod/speaker/views.py +msgid "You cannot access speaker management." +msgstr "" + +#: pod/speaker/views.py pod/video/tests/test_views.py pod/video/views.py +msgid "An action must be specified." +msgstr "" + +#: pod/speaker/views.py +msgid "The speaker has been added." +msgstr "" + +#: pod/speaker/views.py +msgid "The speaker could not be added." +msgstr "" + +#: pod/speaker/views.py +msgid "The speaker has been deleted." +msgstr "" + +#: pod/speaker/views.py +msgid "The speaker could not be deleted." +msgstr "" + +#: pod/speaker/views.py +msgid "The speaker has been updated." +msgstr "" + +#: pod/speaker/views.py +msgid "Speaker not found or invalid input." +msgstr "" + #: pod/urls.py msgid "Pod Administration" msgstr "" @@ -8226,22 +8404,30 @@ msgid "" "nothing and contact us to explain your needs." msgstr "" +#: pod/video/forms.py +msgid "Visibility" +msgstr "" + +#: pod/video/forms.py +msgid "In “Public” mode, the content is visible to everyone." +msgstr "" + #: pod/video/forms.py msgid "" -"In “Draft mode”, the content shows nowhere and nobody else but you can see " -"it." +"In “Draft / Private” mode, the content shows nowhere and nobody else but you " +"can see it." msgstr "" #: pod/video/forms.py msgid "" -"If you don’t select “Draft mode”, you can restrict the content access to " -"only people who can log in" +"In “Restricted access” mode, you can choose the restrictions for the video." msgstr "" #: pod/video/forms.py msgid "" -"If you don’t select “Draft mode”, you can add a password which will be asked " -"to anybody willing to watch your content." +"In “Restricted access” mode, you can add a password which will be asked to " +"anybody willing to watch your content. You can add tokens for allow direct " +"access by link." msgstr "" #: pod/video/forms.py @@ -8250,6 +8436,22 @@ msgid "" "automatically." msgstr "" +#: pod/video/forms.py pod/video/models.py +msgid "Authentication restricted access" +msgstr "" + +#: pod/video/forms.py +msgid "" +"In “Restricted access” mode, you can restrict the content access to only " +"people who can log in" +msgstr "" + +#: pod/video/forms.py +msgid "" +"In “Restricted access” mode, you can restrict the content access to only " +"people who are in these groups." +msgstr "" + #: pod/video/forms.py pod/video/templates/videos/add_video.html msgid "" "Transcription is a speech recognition technology that transforms an oral " @@ -8285,6 +8487,10 @@ msgstr "" msgid "Users can only add videos to this channel" msgstr "" +#: pod/video/forms.py +msgid "Draft / Private" +msgstr "" + #: pod/video/forms.py msgid "Source file" msgstr "" @@ -8306,6 +8512,12 @@ msgstr "" msgid "The deletion date can’t be earlier than today." msgstr "" +#: pod/video/forms.py +msgid "" +"If you select restricted visibility for your video, you must check the " +"\"restricted access\" box or specify a password." +msgstr "" + #: pod/video/forms.py msgid "Delete video cannot be undo" msgstr "" @@ -8723,7 +8935,7 @@ msgstr "" msgid "Category title" msgstr "Categorie titel" -#: pod/video/models.py pod/video/templates/videos/category_modal.html +#: pod/video/models.py msgid "Category" msgstr "" @@ -8976,6 +9188,11 @@ msgstr "" msgid "The file extension is not in the allowed extension:" msgstr "" +#: pod/video/templates/videos/card.html +#: pod/video/templates/videos/card_select.html +msgid "This content contains a quiz." +msgstr "" + #: pod/video/templates/videos/card.html #: pod/video/templates/videos/card_select.html #: pod/video/templatetags/video_tags.py @@ -8983,6 +9200,7 @@ msgid "This content is password protected." msgstr "" #: pod/video/templates/videos/card.html +#: pod/video/templates/videos/card_select.html msgid "This content has restricted access." msgstr "" @@ -8994,42 +9212,32 @@ msgstr "" #: pod/video/templates/videos/card.html #: pod/video/templates/videos/card_select.html -#: pod/video/templates/videos/category_modal_card.html #: pod/video/templates/videos/video_row_select.html msgid "Video content." msgstr "" #: pod/video/templates/videos/card.html #: pod/video/templates/videos/card_select.html -#: pod/video/templates/videos/category_modal_card.html #: pod/video/templates/videos/video_row_select.html msgid "Audio content." msgstr "" #: pod/video/templates/videos/card_select.html #: pod/video/templates/videos/video_row_select.html -msgid "Selected" +#, python-format +msgid "Select video “%(video_title)s”" msgstr "" -#: pod/video/templates/videos/category_modal.html -msgid "category title" -msgstr "categorie titel" - #: pod/video/templates/videos/category_modal.html msgid "Check the videos to add in the category" msgstr "" #: pod/video/templates/videos/category_modal.html -msgid "Save category" -msgstr "" - -#: pod/video/templates/videos/category_modal.html -#: pod/video/templates/videos/filter_aside_category.html -msgid "Delete the category" +msgid "Permanently delete your category?" msgstr "" #: pod/video/templates/videos/category_modal.html -msgid "Permanently delete your category?" +msgid "Save category" msgstr "" #: pod/video/templates/videos/change_video_owner.html @@ -9110,6 +9318,14 @@ msgid_plural "%(counter)s videos" msgstr[0] "" msgstr[1] "" +#: pod/video/templates/videos/dashboard.html +msgid "Select all" +msgstr "" + +#: pod/video/templates/videos/dashboard.html +msgid "Clear selection" +msgstr "" + #: pod/video/templates/videos/dashboard.html msgid "" "You have not uploaded any videos yet, please use the ”Add a new video” " @@ -9142,16 +9358,22 @@ msgstr "" msgid "Fold/Unfold “Tags” filters" msgstr "" -#: pod/video/templates/videos/filter_aside_category.html -msgid "Add new category" +#: pod/video/templates/videos/filter_aside_categories_list.html +#: pod/video/views.py +msgid "Edit category" msgstr "" -#: pod/video/templates/videos/filter_aside_category.html -msgid "Type at least 3 chars to filter categories" +#: pod/video/templates/videos/filter_aside_categories_list.html +#: pod/video/views.py +msgid "Delete category" +msgstr "" + +#: pod/video/templates/videos/filter_aside_category.html pod/video/views.py +msgid "Add new category" msgstr "" #: pod/video/templates/videos/filter_aside_category.html -msgid "Edit the category" +msgid "Type at least 3 chars to filter categories" msgstr "" #: pod/video/templates/videos/footer_link.html @@ -9159,62 +9381,53 @@ msgid "Links" msgstr "" #: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html msgid "Remove from playlist" msgstr "" #: pod/video/templates/videos/link_video.html #: pod/video/templates/videos/video-info.html -#: pod/video/templates/videos/video_row_select.html msgid "Remove from favorite" msgstr "" #: pod/video/templates/videos/link_video.html #: pod/video/templates/videos/video-info.html -#: pod/video/templates/videos/video_row_select.html msgid "Add in favorite" msgstr "" #: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html msgid "Edit the video" msgstr "" #: pod/video/templates/videos/link_video.html -#, fuzzy -#| msgid "Select the video to move by clicking and holding." -msgid "Enhance the video with AI" -msgstr "Sélectionnez la vidéo à déplacer en cliquant et en maintenant." +msgid "Video default version" +msgstr "" #: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html +#: pod/video/templates/videos/video-all-info.html +msgid "Original version" +msgstr "" + +#: pod/video/templates/videos/link_video_dropdown_menu.html +msgid "Enhance the video with AI" +msgstr "" + +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Complete the video" msgstr "" -#: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video_row_select.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Chapter the video" msgstr "" -#: pod/video/templates/videos/link_video.html +#: pod/video/templates/videos/link_video_dropdown_menu.html msgid "Dress the video" msgstr "" -#: pod/video/templates/videos/link_video.html +#: pod/video/templates/videos/link_video_dropdown_menu.html #: pod/video/templates/videos/video_edit.html -#: pod/video/templates/videos/video_row_select.html msgid "Delete the video" msgstr "" -#: pod/video/templates/videos/link_video.html -msgid "Video default version" -msgstr "" - -#: pod/video/templates/videos/link_video.html -#: pod/video/templates/videos/video-all-info.html -msgid "Original version" -msgstr "" - #: pod/video/templates/videos/video-all-info.html msgid "Report the video" msgstr "" @@ -9338,6 +9551,10 @@ msgstr "" msgid "Contributor web link" msgstr "" +#: pod/video/templates/videos/video-info.html +msgid "Speaker(s):" +msgstr "" + #: pod/video/templates/videos/video-info.html msgid "Updated on:" msgstr "" @@ -9699,10 +9916,8 @@ msgstr "" msgid "The video is currently waiting for Aristote AI treatment." msgstr "" -#: pod/video/templates/videos/video_page_content.html -msgid "" -"The video was treated by Aristote. You must verify and validate the " -"processing by pressing the robot icon." +#: pod/video/templates/videos/video_row_select.html +msgid "Selected" msgstr "" #: pod/video/templates/videos/video_row_select.html @@ -9791,10 +10006,6 @@ msgstr "" msgid "-- sorry, no translation provided --" msgstr "" -#: pod/video/tests/test_views.py pod/video/views.py -msgid "An action must be specified." -msgstr "" - #: pod/video/tests/test_views.py pod/video/views.py msgid "A token has been created." msgstr "" @@ -9812,10 +10023,9 @@ msgid "Please enter a title." msgstr "Voer een titel in." #: pod/video/utils.py -#, fuzzy, python-format -#| msgid "Please enter a title." +#, python-format msgid "Please enter a %s from 2 to %s characters." -msgstr "Voer een titel in." +msgstr "" #: pod/video/views.py msgid "You cannot edit this channel." @@ -10028,16 +10238,16 @@ msgstr "" msgid "You do not have rights to delete this comment" msgstr "" -#: pod/video/views.py -msgid "One or many videos already have a category." -msgstr "" - #: pod/video/views.py msgid "Title field is required" msgstr "Titelveld is verplicht" #: pod/video/views.py -msgid "Method Not Allowed" +msgid "One or many videos already have a category." +msgstr "" + +#: pod/video/views.py +msgid "Category successfully added." msgstr "" #: pod/video/views.py @@ -10048,6 +10258,10 @@ msgstr "" msgid "You do not have rights to edit this category" msgstr "" +#: pod/video/views.py +msgid "Category successfully deleted." +msgstr "" + #: pod/video/views.py msgid "You do not have rights to delete this category" msgstr "" @@ -10226,3 +10440,6 @@ msgstr "" #: pod/xapi/apps.py msgid "Esup-Pod xAPI" msgstr "" + +#~ msgid "category title" +#~ msgstr "categorie titel" diff --git a/pod/locale/nl/LC_MESSAGES/djangojs.po b/pod/locale/nl/LC_MESSAGES/djangojs.po index dbc5a08948..35f9464b00 100644 --- a/pod/locale/nl/LC_MESSAGES/djangojs.po +++ b/pod/locale/nl/LC_MESSAGES/djangojs.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: Esup-Pod\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-11 07:48+0000\n" +"POT-Creation-Date: 2024-07-17 09:34+0000\n" "PO-Revision-Date: 2024-06-04 16:20+0200\n" "Last-Translator: obado \n" "Language-Team: \n" @@ -219,6 +219,10 @@ msgstr "" msgid "Are you sure you want to delete this contributor?" msgstr "" +#: pod/completion/static/js/completion.js +msgid "Are you sure you want to delete this speaker?" +msgstr "" + #: pod/completion/static/js/completion.js msgid "Are you sure you want to delete this document?" msgstr "" @@ -393,6 +397,7 @@ msgstr "" #: pod/live/static/js/filter_aside_event_list.js #: pod/playlist/static/playlist/js/filter-aside-playlist-list-refresh.js #: pod/video/static/js/filter_aside_video_list_refresh.js +#: pod/video/static/js/video_category.js msgid "An Error occurred while processing." msgstr "" @@ -620,14 +625,6 @@ msgstr "" msgid "Short answer" msgstr "" -#: pod/quiz/static/quiz/js/create-quiz.js -msgid "The long answer" -msgstr "" - -#: pod/quiz/static/quiz/js/create-quiz.js -msgid "Long answer" -msgstr "" - #: pod/quiz/static/quiz/js/create-quiz.js #, javascript-format msgid "Select the choice #%s as correct answer." @@ -674,6 +671,22 @@ msgstr "" msgid "Incorrect answer given" msgstr "" +#: pod/speaker/static/speaker/js/speakers-management.js +msgid "Job title" +msgstr "" + +#: pod/speaker/static/speaker/js/speakers-management.js +msgid "Remove job" +msgstr "" + +#: pod/speaker/static/speaker/js/speakers-management.js +msgid "Edit this speaker" +msgstr "" + +#: pod/speaker/static/speaker/js/speakers-management.js +msgid "Add a speaker" +msgstr "" + #: pod/video/static/js/ajax-display-channels.js msgid "%(count)s channel" msgid_plural "%(count)s channels" @@ -796,47 +809,38 @@ msgstr[0] "" msgstr[1] "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "This content is password protected." msgstr "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "This content is chaptered." msgstr "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "This content is in draft." msgstr "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Video content." msgstr "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Audio content." msgstr "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Edit the video" msgstr "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Complete the video" msgstr "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Chapter the video" msgstr "" #: pod/video/static/js/regroup_videos_by_theme.js -#: pod/video/static/js/video_category.js msgid "Delete the video" msgstr "" @@ -864,74 +868,6 @@ msgstr "" msgid "Quality" msgstr "" -#: pod/video/static/js/video_category.js -msgid "Category changes saved successfully" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "You cannot add two categories with the same title." -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Category deleted successfully" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "An error occured, please refresh the page and try again." -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Category title field is required." -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Save category" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "You have no content without a category." -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "videos found" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "video found" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Sorry, no video found" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Edit the category" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Delete the category" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Success!" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Error…" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Category created successfully" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Add new category" -msgstr "" - -#: pod/video/static/js/video_category.js -msgid "Create category" -msgstr "" - #: pod/video/static/js/video_edit.js msgid "Select the general type of the video." msgstr "" diff --git a/pod/main/configuration.json b/pod/main/configuration.json index 19a5ad51ef..2252318f63 100644 --- a/pod/main/configuration.json +++ b/pod/main/configuration.json @@ -49,13 +49,13 @@ "description": { "en": [ "URL for General Terms and Conditions for API uses for the AI video enhancement.", - "Example: 'https://aristote.univ.fr/cgu'", - "Project Link: https://www.demainestingenieurs.centralesupelec.fr/aristote/" + "Example: ''", + "Project Link: " ], "fr": [ "L’URL des conditions générales d’utilisation de l’API pour l’IA d’amélioration des vidéos.", - "Exemple : 'https://aristote.univ.fr/cgu'", - "Lien du projet : https://www.demainestingenieurs.centralesupelec.fr/aristote/" + "Exemple : ''", + "Lien du projet : " ] }, "pod_version_end": "", @@ -1061,6 +1061,37 @@ "fr": "Configuration de l’application enrichment" } }, + "speaker": { + "description": { + "en": [ + "Speaker application to add speakers to video.", + "Set `USE_SPEAKER` to True to activate this application." + ], + "fr": [ + "Application Intervenant permettant d'ajouter des intervenants à la vidéo.", + "Mettre `USE_SPEAKER` à True pour activer cette application." + ] + }, + "settings": { + "USE_SPEAKER": { + "default_value": false, + "description": { + "en": [ + "Activation of the Speaker application" + ], + "fr": [ + "Activation de l’application Intervenant" + ] + }, + "pod_version_end": "", + "pod_version_init": "3.8.0" + } + }, + "title": { + "en": "Seaker application configuration", + "fr": "Configuration de l’application Intervenant" + } + }, "import_video": { "description": { "en": [ @@ -4832,6 +4863,19 @@ "pod_version_end": "", "pod_version_init": "3.1.0" }, + "ARCHIVE_HOW_MANY_DAYS": { + "default_value": "365", + "description": { + "en": [ + "Delay before an archived video is moved to archive_ROOT." + ], + "fr": [ + "Délai avant qu'une vidéo archivée ne soit déplacée vers archive_ROOT." + ] + }, + "pod_version_end": "", + "pod_version_init": "3.8.0" + }, "POD_ARCHIVE_AFFILIATION": { "default_value": "[]", "description": { diff --git a/pod/main/forms_utils.py b/pod/main/forms_utils.py index b6e5474c5e..7528135408 100644 --- a/pod/main/forms_utils.py +++ b/pod/main/forms_utils.py @@ -81,7 +81,7 @@ def add_placeholder_and_asterisk(fields): bs_class = bs_class + " " + init_class if my_field.required: my_field.label = mark_safe( - '%s *' % my_field.label + '%s *' % my_field.label ) my_field.widget.attrs["required"] = "" my_field.widget.attrs["class"] = "required " + bs_class diff --git a/pod/main/static/css/block.css b/pod/main/static/css/block.css index dc8af4dbbf..c0f71a982e 100644 --- a/pod/main/static/css/block.css +++ b/pod/main/static/css/block.css @@ -30,11 +30,11 @@ padding: 5em 3em 3em 3em; } -.edito-carousel .video_title { +.edito-carousel .video-title { font-size: 36px; font-family: var(--bs-body-font-family); } -.edito-carousel .video_title:after { +.edito-carousel .video-title:after { content: ""; display: block; width: 90px; diff --git a/pod/main/static/css/dark.css b/pod/main/static/css/dark.css index 4b5ef6e14c..afd969ef3d 100644 --- a/pod/main/static/css/dark.css +++ b/pod/main/static/css/dark.css @@ -101,13 +101,7 @@ box-shadow: 0 0 0 0.2rem rgb(126 126 126 / 25%); } -/*[data-theme="dark"] - .category_modal_videos_list - .infinite-item - .checked_overlay { - background-color: rgb(0 0 0 / 80%); -}*/ -[data-theme="dark"] .infinite-item.selected .checked_overlay { +[data-theme="dark"] .infinite-item.selected .checked-overlay { background-color: var(--dark-select-category-video-color); } diff --git a/pod/main/static/css/pod.css b/pod/main/static/css/pod.css index 3866a593e3..a939ca95c0 100755 --- a/pod/main/static/css/pod.css +++ b/pod/main/static/css/pod.css @@ -282,6 +282,11 @@ a:not(.btn, .dropdown-item):focus { min-height: 146px; } +.link-center-pod-category > img { + min-width: 100%; + height: 100%; +} + /********* * navbar *********/ @@ -585,8 +590,9 @@ div.card a img { .card-footer-pod { position: absolute; bottom: 4rem; - right: 0; + right: 0.5rem; border: none; + border-radius: 0.375rem; display: flex; flex-wrap: wrap-reverse; justify-content: flex-end; @@ -672,13 +678,13 @@ div.card a img { } /** Dashboard **/ -#bulk_update_container { +#bulk-update-container { background-color: var(--pod-background-neutre2-bloc); padding: 1em; border-radius: var(--bs-border-radius); } -#bulk_update_container .select2 { +#bulk-update-container .select2 { width: 100% !important; } @@ -696,15 +702,11 @@ div.card a img { margin: 1rem 0; } -.dashboard-container .infinite-item { - cursor: pointer; -} - .dashboard-container .infinite-item { position: relative; } -.infinite-item .checked_overlay { +.infinite-item .checked-overlay { display: none; position: absolute; inset: 0; @@ -719,29 +721,17 @@ div.card a img { transition: all 0.3s; } -.infinite-item.selected .checked_overlay { +.infinite-item.selected .checked-overlay { display: flex; opacity: 1; } -.infinite-item .checked_overlay .card_selected { +.infinite-item .checked-overlay .card-selected { display: block; transition: all 0.3s; } -.dashboard-container .infinite-item:not(.selected), -.dashboard-container .infinite-item .checked_overlay .card_selected { - transition: transform 0.2s ease-in-out; -} - -.dashboard-container .infinite-item:not(.selected):hover, -.dashboard-container .infinite-item .checked_overlay:hover .card_selected { - transform: scale(1.03); - transition: transform 0.2s ease-in-out; - z-index: 6; -} - -.infinite-item.selected .checked_overlay .card-selected i { +.infinite-item.selected .checked-overlay .card-selected i { font-size: 3rem; color: var(--pod-link-color); } @@ -783,14 +773,14 @@ div.card a img { .pod-infinite-list-container-dashboard .list-item-video-row - .checked_overlay + .checked-overlay .card-selected { width: 100%; } .pod-infinite-list-container-dashboard .list-item-video-row.selected - .checked_overlay + .checked-overlay .card-selected i { font-size: 2rem; @@ -799,15 +789,15 @@ div.card a img { .pod-infinite-list-container-dashboard .list-item-video-row - i:not(.checked_overlay .card-selected i) { + i:not(.checked-overlay .card-selected i) { font-size: 1.2rem; padding: 0.2rem; } .thumbnail-item-video-row { - height: 50px; + height: 40px; overflow: hidden; - border-radius: 4px 0 0 4px; + border-radius: 4px; } .thumbnail-item-video-row img { @@ -844,12 +834,12 @@ span[data-bs-placement] { overflow-wrap: anywhere; } -.required_star, +.required-star, .form-help-inline { color: var(--bs-danger); } -.form-check-label > .required_star { +.form-check-label > .required-star { font-size: 1em; } @@ -891,7 +881,7 @@ span[data-bs-placement] { z-index: 1031; } -#formalertdivbottomright { +#form-alert-div-bottom-right { max-width: 95%; position: fixed; bottom: 20px; @@ -900,7 +890,7 @@ span[data-bs-placement] { z-index: 1031; } -#formalertdivbottomright > i { +#form-alert-div-bottom-right > i { margin-right: 0.8rem; font-size: 1rem; } @@ -1870,3 +1860,8 @@ body[data-admin-utc-offset] .question-container li.bi::before { margin-right: 0.5em; } + +/** LINK VIDEO MODAL **/ +#link-video-modal .modal-dialog { + font-size: 1.7em; +} diff --git a/pod/main/static/js/main.js b/pod/main/static/js/main.js index 4922ac56f6..0a4542e00e 100644 --- a/pod/main/static/js/main.js +++ b/pod/main/static/js/main.js @@ -2,7 +2,8 @@ * @file Esup-Pod Main JavaScripts */ -/* exported getParents slideToggle fadeOut linkTo_UnCryptMailto showLoader videocheck send_form_data_vanilla */ +/* exported getParents slideToggle fadeOut linkTo_UnCryptMailto showLoader videocheck */ +/* exported send_form_data_vanilla decodeString */ // Read-only globals defined in video-script.html /* global player */ @@ -1153,93 +1154,6 @@ if (btnpartageprive) { }); } -/** Restrict access **/ -/** restrict access to group */ -if (typeof id_is_restricted === "undefined") { - var id_is_restricted = document.getElementById("id_is_restricted"); -} else { - id_is_restricted = document.getElementById("id_is_restricted"); -} -if (id_is_restricted) { - id_is_restricted.addEventListener("click", function () { - restrict_access_to_groups(); - }); -} -/** - * [restrict_access_to_groups description] - * @return {[type]} [description] - */ -var restrict_access_to_groups = function () { - if (document.getElementById("id_is_restricted").checked) { - let id_restricted_to_groups = document.getElementById( - "id_restrict_access_to_groups", - ); - id_restricted_to_groups.parentElement.classList.remove("d-none"); - } else { - document - .querySelectorAll("#id_restrict_access_to_groups select") - .forEach((select) => { - select.options.forEach((option) => { - if (option.selected) { - option.selected = false; - } - }); - }); - let id_restricted_to_groups = document.getElementById( - "id_restrict_access_to_groups", - ); - id_restricted_to_groups.parentElement.classList.add("d-none"); - } -}; - -if (typeof id_is_draft === "undefined") { - var id_is_draft = document.getElementById("id_is_draft"); -} else { - id_is_draft = document.getElementById("id_is_draft"); -} -if (id_is_draft) { - id_is_draft.addEventListener("click", function () { - restricted_access(); - }); -} - -/** - * [restricted_access description] - * @return {[type]} [description] - */ -var restricted_access = function () { - document - .querySelectorAll(".restricted_access") - .forEach((restricted_access) => { - if (restricted_access) { - let is_draft = document.getElementById("id_is_draft"); - if (is_draft != null && is_draft.checked) { - restricted_access.classList.add("hide"); - restricted_access.classList.remove("show"); - document.getElementById("id_password").value; - - document - .querySelectorAll("#id_restrict_access_to_groups select") - .forEach((select) => { - select.options.forEach((option) => { - if (option.selected) { - option.selected = false; - } - }); - }); - - document.getElementById("id_is_restricted").checked = false; - } else { - restricted_access.classList.add("show"); - restricted_access.classList.remove("hide"); - } - restrict_access_to_groups(); - } - }); -}; -restricted_access(); -//restrict_access_to_groups(); - /** end restrict access **/ /*** VALID FORM ***/ (function () { diff --git a/pod/main/templates/block/carousel.html b/pod/main/templates/block/carousel.html index d7a5c80e55..3d7ad2718b 100644 --- a/pod/main/templates/block/carousel.html +++ b/pod/main/templates/block/carousel.html @@ -50,14 +50,14 @@

    {{ title |capfirst|truncatechars:43 }}

    alt=""/> {% endif %} {% endif %} diff --git a/pod/main/templates/main/mandatory_fields.html b/pod/main/templates/main/mandatory_fields.html index 67c01d2a45..eccf7623d2 100644 --- a/pod/main/templates/main/mandatory_fields.html +++ b/pod/main/templates/main/mandatory_fields.html @@ -3,7 +3,7 @@

    {% trans "Mandatory fields" %}

    - * + * {% trans "Fields marked with an asterisk are mandatory." %}

    diff --git a/pod/main/templates/navbar.html b/pod/main/templates/navbar.html index 675ee251dc..4ecde4a9b8 100644 --- a/pod/main/templates/navbar.html +++ b/pod/main/templates/navbar.html @@ -297,6 +297,9 @@
    {% if user.get_full_name != '' %}{{ user.get_ {% trans 'Perform a BigBlueButton live' %} {% endif %} {% endif %} + {% if request.user.is_superuser and USE_SPEAKER %} +  {% trans 'Speakers management' %} + {% endif %} {% if USE_NOTIFICATIONS %}
    {% if meeting.owner == request.user or request.user.is_superuser or perms.meeting.change_meeting or request.user in meeting.additional_owners.all %} -
    +
    {% include "meeting/link_meeting.html" %}
    {% endif %} diff --git a/pod/playlist/forms.py b/pod/playlist/forms.py index c975b7b023..a684b7ec92 100644 --- a/pod/playlist/forms.py +++ b/pod/playlist/forms.py @@ -146,9 +146,12 @@ def __init__(self, *args, **kwargs) -> None: super(PlaylistForm, self).__init__(*args, **kwargs) self.fields = add_placeholder_and_asterisk(self.fields) if self.user: - if not self.user.is_staff and RESTRICT_PROMOTED_PLAYLIST_ACCESS_TO_STAFF_ONLY: - if "promoted" in self.fields: - del self.fields["promoted"] + if ( + RESTRICT_PROMOTED_PLAYLIST_ACCESS_TO_STAFF_ONLY + and "promoted" in self.fields + or self.user.is_superuser + ): + del self.fields["promoted"] else: if "promoted" in self.fields: del self.fields["promoted"] diff --git a/pod/playlist/templates/playlist/playlist-list-modal.html b/pod/playlist/templates/playlist/playlist-list-modal.html index 8b927905d9..86acd1a5dd 100644 --- a/pod/playlist/templates/playlist/playlist-list-modal.html +++ b/pod/playlist/templates/playlist/playlist-list-modal.html @@ -4,49 +4,60 @@ {% load favorites_playlist %} {% block page_extra_head %} - + {% endblock page_extra_head %} diff --git a/pod/podfile/tests/test_models.py b/pod/podfile/tests/test_models.py index 132e3ad46f..daa924063b 100644 --- a/pod/podfile/tests/test_models.py +++ b/pod/podfile/tests/test_models.py @@ -14,7 +14,8 @@ class CustomFileModelTestCase(TestCase): """Test case for Pod CustomFile model.""" - def setUp(self): + def setUp(self) -> None: + """Init some values before CustomFileModel tests.""" test = User.objects.create(username="test") currentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -41,7 +42,7 @@ def setUp(self): file=simplefile, ) - def test_attributs_full(self): + def test_attributs_full(self) -> None: user = User.objects.get(id=1) file = CustomFileModel.objects.get(id=1) self.assertEqual(file.name, "testfile") @@ -59,7 +60,7 @@ def test_attributs_full(self): print(" ---> test_attributs_full: OK! --- CustomFileModel") - def test_attributs(self): + def test_attributs(self) -> None: user = User.objects.get(id=1) file = CustomFileModel.objects.get(id=2) self.assertTrue(file.name, "testfile") @@ -78,7 +79,7 @@ def test_attributs(self): print(" [ BEGIN FILEPICKER_TEST_MODELS ] ") print(" ---> test_attributs: OK! --- CustomFileModel") - def test_delete(self): + def test_delete(self) -> None: CustomFileModel.objects.get(id=1).delete() CustomFileModel.objects.get(id=2).delete() self.assertFalse(CustomFileModel.objects.all()) @@ -87,7 +88,8 @@ def test_delete(self): class CustomImageModelTestCase(TestCase): - def setUp(self): + def setUp(self) -> None: + """Init some values before CustomImageModel tests.""" test = User.objects.create(username="test") currentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) simplefile = SimpleUploadedFile( @@ -113,7 +115,7 @@ def setUp(self): file=simplefile, ) - def test_attributs_full(self): + def test_attributs_full(self) -> None: user = User.objects.get(id=1) file = CustomImageModel.objects.get(id=1) self.assertEqual(file.name, "testimage") @@ -130,7 +132,7 @@ def test_attributs_full(self): print(" ---> test_attributs_full: OK! --- CustomImageModel") - def test_attributs(self): + def test_attributs(self) -> None: user = User.objects.get(id=1) file = CustomImageModel.objects.get(id=2) self.assertTrue(file.name, "testimage") @@ -147,7 +149,7 @@ def test_attributs(self): print(" ---> test_attributs: OK! --- CustomImageModel") - def test_delete(self): + def test_delete(self) -> None: CustomImageModel.objects.get(id=1).delete() CustomImageModel.objects.get(id=2).delete() self.assertFalse(CustomImageModel.objects.all()) @@ -158,14 +160,14 @@ def test_delete(self): class UserFolderTestCase(TestCase): """Test case for UserFolder.""" - def setUp(self): + def setUp(self) -> None: """Create UserFolders to be tested.""" test = User.objects.create(username="test") UserFolder.objects.get(name="home", owner=test) UserFolder.objects.create(name="Images", owner=test) UserFolder.objects.create(name="Documents", owner=test) - def test_attributs_full(self): + def test_attributs_full(self) -> None: """Test UserFolder attributes.""" user = User.objects.get(id=1) child = UserFolder.objects.get(id=2) @@ -174,7 +176,7 @@ def test_attributs_full(self): print(" ---> test_attributs_full: OK! --- UserFolder") - def test_attributs(self): + def test_attributs(self) -> None: """Test UserFolder attributes.""" user = User.objects.get(id=1) home = UserFolder.objects.get(id=1) @@ -183,7 +185,7 @@ def test_attributs(self): print(" ---> test_attributs: OK! --- UserFolder") - def test_delete(self): + def test_delete(self) -> None: """Test UserFolder deletion.""" UserFolder.objects.get(id=1).delete() UserFolder.objects.get(id=2).delete() diff --git a/pod/podfile/tests/test_views.py b/pod/podfile/tests/test_views.py index 8017247158..cc16b984a1 100644 --- a/pod/podfile/tests/test_views.py +++ b/pod/podfile/tests/test_views.py @@ -21,7 +21,8 @@ class FolderViewTestCase(TestCase): """FOLDER VIEWS test case.""" - def setUp(self): + def setUp(self) -> None: + """Init some values before FolderView tests.""" user = User.objects.create(username="pod", password="azerty") UserFolder.objects.get(owner=user, name="home") child = UserFolder.objects.create(owner=user, name="Child") @@ -49,7 +50,7 @@ def setUp(self): user2.owner.sites.add(Site.objects.get_current()) user2.owner.save() - def test_list_folders(self): + def test_list_folders(self) -> None: self.client = Client() self.user = User.objects.get(username="pod") self.client.force_login(self.user) @@ -92,7 +93,7 @@ def test_list_folders(self): print(" ---> test_list_folders: OK!") - def test_edit_folders(self): + def test_edit_folders(self) -> None: self.client = Client() self.user = User.objects.get(username="pod") self.user.is_staff = True @@ -122,7 +123,7 @@ def test_edit_folders(self): self.assertEqual(response.context["user_folder"].count(), 2) print(" ---> test_edit_folders: OK!") - def test_delete_folders(self): + def test_delete_folders(self) -> None: self.client = Client() self.user = User.objects.get(username="pod2") self.user.is_staff = True @@ -164,7 +165,7 @@ def test_delete_folders(self): class FileViewTestCase(TestCase): """File view test case.""" - def setUp(self): + def setUp(self) -> None: pod = User.objects.create(username="pod", password="azerty") home = UserFolder.objects.get(owner=pod, name="home") child = UserFolder.objects.create(owner=pod, name="Child") @@ -203,7 +204,7 @@ def setUp(self): file=simplefile, ) - def test_list_files(self): + def test_list_files(self) -> None: self.client = Client() self.user = User.objects.get(username="pod") self.client.force_login(self.user) @@ -245,7 +246,7 @@ def test_list_files(self): print(" ---> test_list_files: OK!") - def test_edit_files(self): + def test_edit_files(self) -> None: self.client = Client() self.user = User.objects.get(username="pod") self.user.is_staff = True diff --git a/pod/podfile/utils.py b/pod/podfile/utils.py index eb529a30df..431c461f03 100644 --- a/pod/podfile/utils.py +++ b/pod/podfile/utils.py @@ -1,3 +1,5 @@ +"""Esup-Pod podfile utils.""" + from django.http import HttpResponse from django.http import HttpResponseBadRequest from .models import UserFolder diff --git a/pod/podfile/views.py b/pod/podfile/views.py index bb0e6889d5..9d3e688ed0 100644 --- a/pod/podfile/views.py +++ b/pod/podfile/views.py @@ -495,7 +495,8 @@ def file_edit_save(request, folder): @csrf_protect @staff_member_required(redirect_field_name="referrer") -def get_file(request, type): +def get_file(request, type) -> HttpResponse: + """Get file specified in request if current user can access it.""" id = None if request.method == "POST" and request.POST.get("src"): id = request.POST.get("src") diff --git a/pod/quiz/README.md b/pod/quiz/README.md index d3e49cbe7d..b8da788017 100644 --- a/pod/quiz/README.md +++ b/pod/quiz/README.md @@ -30,4 +30,4 @@ Update these functions: - Add new function: `handleYourNewTypeOfQuestionQuestion(questionForm)` - Update: `handleQuestionType` function - Add new function `handleYourNewTypeOfQuestionSubmission` - - Update `manage_form_submission` function \ No newline at end of file + - Update `manage_form_submission` function diff --git a/pod/quiz/admin.py b/pod/quiz/admin.py index 6b9137b98a..fe763dc69e 100644 --- a/pod/quiz/admin.py +++ b/pod/quiz/admin.py @@ -3,7 +3,6 @@ from django.contrib import admin from pod.quiz.models import ( - LongAnswerQuestion, MultipleChoiceQuestion, Quiz, ShortAnswerQuestion, @@ -39,11 +38,6 @@ class MultipleChoiceQuestionAdmin(BaseQuestionAdmin): """Admin configuration for MultipleChoiceQuestion.""" -@admin.register(LongAnswerQuestion) -class LongAnswerQuestionAdmin(BaseQuestionAdmin): - """Admin configuration for LongAnswerQuestion.""" - - @admin.register(ShortAnswerQuestion) class ShortAnswerQuestionAdmin(BaseQuestionAdmin): """Admin configuration for ShortAnswerQuestion.""" @@ -66,13 +60,6 @@ class MultipleChoiceQuestionInline(admin.StackedInline): extra = 0 -class LongAnswerQuestionInline(admin.StackedInline): - """Inline configuration for LongAnswerQuestion.""" - - model = LongAnswerQuestion - extra = 0 - - class ShortAnswerQuestionInline(admin.StackedInline): """Inline configuration for ShortAnswerQuestion.""" @@ -91,6 +78,5 @@ class QuizAdmin(admin.ModelAdmin): inlines = [ SingleChoiceQuestionInline, MultipleChoiceQuestionInline, - LongAnswerQuestionInline, ShortAnswerQuestionInline, ] diff --git a/pod/quiz/apps.py b/pod/quiz/apps.py index 780fb94772..b965239874 100644 --- a/pod/quiz/apps.py +++ b/pod/quiz/apps.py @@ -1,7 +1,18 @@ """Esup-Pod quiz app.""" from django.apps import AppConfig +from django.db import connection +from django.db.models.signals import pre_migrate, post_migrate from django.utils.translation import gettext_lazy as _ +from django.db import transaction + +QUESTION_DATA = { + "single_choice": [], + "multiple_choice": [], + "short_answer": [], +} + +EXECUTE_QUIZ_MIGRATIONS = False class QuizConfig(AppConfig): @@ -10,3 +21,150 @@ class QuizConfig(AppConfig): name = "pod.quiz" default_auto_field = "django.db.models.BigAutoField" verbose_name = _("Quiz") + + def ready(self) -> None: + pre_migrate.connect(self.check_quiz_migrations, sender=self) + pre_migrate.connect(self.save_previous_questions, sender=self) + pre_migrate.connect(self.remove_previous_questions, sender=self) + post_migrate.connect(self.send_previous_questions, sender=self) + + def execute_query_mapping(self, query, mapping_dict, question_type) -> None: + """ + Execute the given query and populate the mapping dictionary with the results. + + Args: + query (str): The given query to execute + mapping_dict (dict): The dictionary. + """ + from pod.quiz.models import Quiz + import json + + try: + with connection.cursor() as c: + c.execute(query) + results = c.fetchall() + for question_result in results: + quiz = Quiz.objects.get(id=question_result[6]) + question_data = question_result[5] + if question_type in {"single_choice", "multiple_choice"}: + question_data = json.loads(question_data) + mapping_dict.append( + { + "question_type": question_type, + "quiz": quiz, + "title": question_result[1], + "explanation": question_result[2], + "start_timestamp": question_result[3], + "end_timestamp": question_result[4], + "question_data": question_data, + } + ) + except Exception as e: + print(e) + pass + + def check_quiz_migrations(self, sender, **kwargs) -> None: + """Save previous questions from different tables.""" + from pod.quiz.models import ( + MultipleChoiceQuestion, + ShortAnswerQuestion, + SingleChoiceQuestion, + ) + + QUESTION_MODELS = [ + MultipleChoiceQuestion, + ShortAnswerQuestion, + SingleChoiceQuestion, + ] + + global EXECUTE_QUIZ_MIGRATIONS + quiz_exist = self.check_quiz_exist() + if not quiz_exist: + return + + for model in QUESTION_MODELS: + query = f"SELECT id FROM {model.objects.model._meta.db_table} LIMIT 1" + try: + with connection.cursor() as cursor: + cursor.execute(query) + result = cursor.fetchone() + if result and isinstance(result[0], int): + EXECUTE_QUIZ_MIGRATIONS = True + break + except Exception as e: + print(e) + pass + + def check_quiz_exist(self) -> bool: + """Check if quiz exist in database.""" + from pod.quiz.models import Quiz + + try: + quiz = Quiz.objects.first() + if not quiz: + return False + return True + except Exception: + return False + + def save_previous_questions(self, sender, **kwargs) -> None: + """Save previous questions from different tables.""" + from pod.quiz.models import ( + MultipleChoiceQuestion, + ShortAnswerQuestion, + SingleChoiceQuestion, + ) + + if not EXECUTE_QUIZ_MIGRATIONS: + return + + queries = { + "single_choice": f"SELECT id, title, explanation, start_timestamp, end_timestamp, choices, quiz_id FROM {SingleChoiceQuestion.objects.model._meta.db_table}", + "multiple_choice": f"SELECT id, title, explanation, start_timestamp, end_timestamp, choices, quiz_id FROM {MultipleChoiceQuestion.objects.model._meta.db_table}", + "short_answer": f"SELECT id, title, explanation, start_timestamp, end_timestamp, answer, quiz_id FROM {ShortAnswerQuestion.objects.model._meta.db_table}", + } + for question_type, query in queries.items(): + self.execute_query_mapping(query, QUESTION_DATA[question_type], question_type) + + def remove_previous_questions(self, sender, **kwargs) -> None: + """Remove previous questions from different tables.""" + from pod.quiz.models import ( + MultipleChoiceQuestion, + ShortAnswerQuestion, + SingleChoiceQuestion, + ) + + if not EXECUTE_QUIZ_MIGRATIONS: + return + + QUESTION_MODELS = [ + MultipleChoiceQuestion, + ShortAnswerQuestion, + SingleChoiceQuestion, + ] + + for model in QUESTION_MODELS: + model.objects.all().delete() + print("--- Previous questions deleted successfuly ---") + + @transaction.atomic + def send_previous_questions(self, sender, **kwargs) -> None: + """Insert previously saved questions from QUESTION_DATA.""" + from pod.quiz.views import create_question + + if not EXECUTE_QUIZ_MIGRATIONS: + return + + for question_type, questions in QUESTION_DATA.items(): + for question in questions: + print(question["question_data"]) + create_question( + question_type=question_type, + quiz=question["quiz"], + title=question["title"], + explanation=question["explanation"], + start_timestamp=question["start_timestamp"], + end_timestamp=question["end_timestamp"], + question_data=question["question_data"], + ) + print("--- New questions saved successfuly ---") diff --git a/pod/quiz/forms.py b/pod/quiz/forms.py index 3e31416a22..5f3fc9e05d 100644 --- a/pod/quiz/forms.py +++ b/pod/quiz/forms.py @@ -7,7 +7,6 @@ import json from pod.quiz.models import ( - LongAnswerQuestion, MultipleChoiceQuestion, ShortAnswerQuestion, SingleChoiceQuestion, @@ -22,7 +21,6 @@ class QuestionForm(forms.Form): _("Redaction"), [ ("short_answer", _("Short answer")), - ("long_answer", _("Long answer")), ], ), ( @@ -74,7 +72,7 @@ class QuestionForm(forms.Form): required=True, help_text=_("Please choose the question type."), ) - question_id = forms.IntegerField( + question_id = forms.UUIDField( widget=forms.HiddenInput(), required=False, ) @@ -88,11 +86,6 @@ class QuestionForm(forms.Form): required=False, label=_("Short answer"), ) - long_answer = forms.CharField( - widget=forms.HiddenInput(attrs={"class": "hidden-long-answer-field"}), - required=False, - label=_("Long answer"), - ) multiple_choice = forms.CharField( widget=forms.HiddenInput(attrs={"class": "hidden-multiple-choice-field"}), required=False, @@ -283,25 +276,3 @@ def __init__(self, *args, **kwargs) -> None: """Init short answer question form.""" super(ShortAnswerQuestionForm, self).__init__(*args, **kwargs) self.fields = add_placeholder_and_asterisk(self.fields) - - -class LongAnswerQuestionForm(forms.ModelForm): - """Form to show a long answer question form.""" - - user_answer = forms.CharField( - label=_("Long answer question"), - widget=forms.Textarea(), - required=True, - help_text=_("Write a long answer."), - ) - - class Meta: - """LongAnswerQuestionForm Metadata.""" - - model = LongAnswerQuestion - fields = ["user_answer"] - - def __init__(self, *args, **kwargs) -> None: - """Init long answer question form.""" - super(LongAnswerQuestionForm, self).__init__(*args, **kwargs) - self.fields = add_placeholder_and_asterisk(self.fields) diff --git a/pod/quiz/models.py b/pod/quiz/models.py index c7b6cf7ed2..8120ed85fd 100644 --- a/pod/quiz/models.py +++ b/pod/quiz/models.py @@ -1,5 +1,6 @@ """Esup-Pod quiz models.""" +import uuid from json import JSONDecodeError, loads from django.core.exceptions import ValidationError from django.db import models @@ -87,6 +88,7 @@ class Question(models.Model): end_timestamp (IntegerField): End timestamp of the answer in the video. """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) quiz = models.ForeignKey( Quiz, verbose_name=_("Quiz"), @@ -162,7 +164,9 @@ def get_question_form(self, data=None): Returns: QuestionForm: Form for the question. """ - return "This method must be redefined in child class." + raise NotImplementedError( + "Subclasses of Question must implement get_question_form method." + ) def get_answer(self) -> None: """ @@ -171,7 +175,9 @@ def get_answer(self) -> None: Returns: str: Answer for the question. """ - return None + raise NotImplementedError( + "Subclasses of Question must implement get_answer method." + ) def get_type(self) -> None: """ @@ -180,7 +186,34 @@ def get_type(self) -> None: Returns: str: Type of the question. """ - return None + raise NotImplementedError( + "Subclasses of Question must implement get_type method." + ) + + def calculate_score(self, user_answer): + """ + Calculate the score for the question based on user's answer. + + Args: + user_answer (any): Answer provided by the user. + + Returns: + float: The calculated score, a value between 0 and 1. + """ + correct_answer = self.get_answer() + + if correct_answer is None: + return 0.0 + + return self._calculate_score(user_answer, correct_answer) + + def _calculate_score(self, user_answer, correct_answer): + """ + Internal method to be implemented by subclasses to calculate score. + """ + raise NotImplementedError( + "Subclasses of Question must implement _calculate_score method." + ) class SingleChoiceQuestion(Question): @@ -262,6 +295,19 @@ def get_question_form(self, data=None): return SingleChoiceQuestionForm(data, instance=self, prefix=f"question_{self.pk}") + def _calculate_score(self, user_answer, correct_answer): + """ + Calculate the score for single choice question. + + Args: + user_answer (any): Answer provided by the user. + correct_answer (any): Correct answer for the question. + + Returns: + float: The calculated score, a value between 0 and 1. + """ + return 1.0 if user_answer.lower() == correct_answer.lower() else 0.0 + class MultipleChoiceQuestion(Question): """ @@ -346,6 +392,27 @@ def get_question_form(self, data=None): data, instance=self, prefix=f"question_{self.pk}" ) + def _calculate_score(self, user_answer, correct_answer): + """ + Calculate the score for multiple choice question. + + Args: + user_answer (list): Answers provided by the user. + correct_answer (list): Correct answers for the question. + + Returns: + float: The calculated score, a value between 0 and 1. + """ + user_answer_set = set(user_answer) if user_answer else set() + correct_answer_set = set(correct_answer) + intersection = user_answer_set & correct_answer_set + score = len(intersection) / len(correct_answer_set) + + len_incorrect = len(user_answer_set - correct_answer_set) + penalty = len_incorrect / len(correct_answer_set) + score = max(0, score - penalty) + return score + class TrueFalseQuestion(Question): # TODO """ @@ -417,46 +484,21 @@ def get_question_form(self, data=None): return ShortAnswerQuestionForm(data, instance=self, prefix=f"question_{self.pk}") - -class LongAnswerQuestion(Question): - """ - Long answer question model. - - Attributes: - answer (TextField): Answer of the question. - """ - - answer = models.TextField( - verbose_name=_("Answer"), - default="", - help_text=_("Please choose an answer."), - ) - - class Meta: - verbose_name = _("Long answer question") - verbose_name_plural = _("Long answer questions") - - def __str__(self) -> str: - """Representation the LongAnswerQuestion as string.""" - return super().__str__() - - def get_answer(self) -> str: - return self.answer - - def get_type(self): - return "long_answer" - - def get_question_form(self, data=None): + def _calculate_score(self, user_answer, correct_answer): """ - Get the form for the question. + Calculate the score for short answer question. Args: - data (dict): Form data. + user_answer (str): Answer provided by the user. + correct_answer (str): Correct answer for the question. + Returns: - LongAnswerQuestionForm: Form for the question. + float: The calculated score, a value between 0 and 1. """ - from pod.quiz.forms import ( - LongAnswerQuestionForm, - ) - - return LongAnswerQuestionForm(data, instance=self, prefix=f"question_{self.pk}") + if ( + user_answer is not None + and user_answer.strip() != "" + and correct_answer is not None + ): + return 1.0 if user_answer.lower() == correct_answer.lower() else 0.0 + return 0.0 diff --git a/pod/quiz/static/quiz/js/create-quiz.js b/pod/quiz/static/quiz/js/create-quiz.js index 42dea9fe16..05f5d48637 100644 --- a/pod/quiz/static/quiz/js/create-quiz.js +++ b/pod/quiz/static/quiz/js/create-quiz.js @@ -203,34 +203,6 @@ document.addEventListener("DOMContentLoaded", function () { questionChoicesForm.appendChild(input); } - /** - * Handles the setup for a long answer question in the question form. - * @param {HTMLElement} questionForm - The question form element. - */ - function handleLongAnswerQuestion(questionForm) { - const textarea = document.createElement("textarea"); - const textareaId = "long-answer-" + questionForm.getAttribute("data-question-index"); - const label = document.createElement("label"); - - textarea.id = textareaId; - textarea.name = textareaId; - textarea.required = true; - textarea.placeholder = gettext("The long answer"); - textarea.classList.add("long-answer-field", "form-control"); - - label.setAttribute("for", textareaId); - label.textContent = gettext("Long answer"); - - let qData = getQuestionData(questionForm); - if (qData && qData["long_answer"] != null) { - textarea.value = qData["long_answer"]; - } - - const questionChoicesForm = questionForm.querySelector(".question-choices-form"); - questionChoicesForm.appendChild(label); - questionChoicesForm.appendChild(textarea); - } - /** * Handles the setup for a single choice question in the question form. * @param {HTMLElement} questionForm - The question form element. @@ -480,9 +452,6 @@ document.addEventListener("DOMContentLoaded", function () { case "short_answer": handleShortAnswerQuestion(questionForm); break; - case "long_answer": - handleLongAnswerQuestion(questionForm); - break; case "single_choice": handleSingleChoiceQuestion(questionForm); break; @@ -557,7 +526,6 @@ document.addEventListener("DOMContentLoaded", function () { } let questionFormsList = document.querySelectorAll(".question-form"); for (let questionForm of questionFormsList) { - console.log(questionForm); const questionType = questionForm.querySelector( ".question-select-type", ).value; @@ -565,9 +533,6 @@ document.addEventListener("DOMContentLoaded", function () { case "short_answer": handleShortAnswerSubmission(questionForm); break; - case "long_answer": - handleLongAnswerSubmission(questionForm); - break; case "single_choice": handleSingleChoiceSubmission(questionForm); break; @@ -597,19 +562,6 @@ document.addEventListener("DOMContentLoaded", function () { hiddenShortAnswerInput.value = shortAnswerInput.value; } - /** - * Handles the submission process for long answer questions in the quiz form. - * - * @param {HTMLElement} questionForm - The form element representing the long answer question. - */ - function handleLongAnswerSubmission(questionForm) { - let longAnswerInput = questionForm.querySelector(".long-answer-field"); - let hiddenLongAnswerInput = questionForm.querySelector( - ".hidden-long-answer-field", - ); - hiddenLongAnswerInput.value = longAnswerInput.value; - } - /** * Handles the submission process for single choice questions in the quiz form. * diff --git a/pod/quiz/static/quiz/js/video-quiz-submit.js b/pod/quiz/static/quiz/js/video-quiz-submit.js index 587220ec5d..b5baf46d54 100644 --- a/pod/quiz/static/quiz/js/video-quiz-submit.js +++ b/pod/quiz/static/quiz/js/video-quiz-submit.js @@ -9,7 +9,7 @@ global player */ // Read-only globals defined in video_quiz.html /* -global questions_answers +global questions_answers, show_correct_answers */ const questionList = document.querySelectorAll(".question-container"); @@ -18,10 +18,10 @@ for (let questionElement of questionList) { let showResponseButton = questionElement.querySelector( ".show-response-button", ); - if(showResponseButton) { + if (showResponseButton) { showResponseButton.addEventListener("click", function (event) { event.preventDefault(); - if(player.paused()) { + if (player.paused()) { player.play(); } player.currentTime(this.attributes.start.value); @@ -34,41 +34,40 @@ for (let questionElement of questionList) { // if answer in good answer, put it in green else if user answer put it in red let allanswers = questionElement.querySelectorAll(`ul#id_${questionid}-selected_choice li input`); for (let answer of allanswers) { - answer.disabled=true; + answer.disabled = true; if (questions_answers[`${questionid}`]) { let user_answer = questions_answers[`${questionid}`][0]; - let correct_answer = questions_answers[`${questionid}`][1]; - if( (Array.isArray(correct_answer) && correct_answer.includes(answer.value)) || correct_answer === answer.value ){ - answer.closest('li').classList.add('bi', 'bi-clipboard-check', 'text-success'); - answer.closest('li').title=gettext("Correct answer given"); - } else if ((Array.isArray(user_answer) && user_answer.includes(answer.value)) || user_answer === answer.value ){ - answer.closest('li').classList.add('bi', 'bi-clipboard-x', 'text-danger'); - answer.closest('li').title=gettext("Incorrect answer given"); - } else { - answer.closest('li').classList.add('bi', 'bi-clipboard'); - } - // check if question is "single_choice", "multiple_choice", "short_answer", "long_answer" - if ((Array.isArray(user_answer) && user_answer.includes(answer.value)) || user_answer === answer.value ){ + // check if question is "single_choice", "multiple_choice", "short_answer" + if ((Array.isArray(user_answer) && user_answer.includes(answer.value)) || user_answer === answer.value) { answer.checked = true; } + if (show_correct_answers) { + let correct_answer = questions_answers[`${questionid}`][1]; + if ((Array.isArray(correct_answer) && correct_answer.includes(answer.value)) || correct_answer === answer.value) { + answer.closest('li').classList.add('bi', 'bi-clipboard-check', 'text-success'); + answer.closest('li').title = gettext("Correct answer given"); + } else if ((Array.isArray(user_answer) && user_answer.includes(answer.value)) || user_answer === answer.value) { + answer.closest('li').classList.add('bi', 'bi-clipboard-x', 'text-danger'); + answer.closest('li').title = gettext("Incorrect answer given"); + } else { + answer.closest('li').classList.add('bi', 'bi-clipboard'); + } + } } } // case user input - // Get short or long answer input + // Get short answer input let user_input = document.getElementById(`id_${questionid}-user_answer`); if (user_input) { user_input.disabled = true; } allanswersarr = Array.from(allanswers) - if(user_input && !allanswersarr.includes(user_input) && questions_answers[`${questionid}`]) { - let user_answer = questions_answers[`${questionid}`][0]; - if(user_input.tagName === 'INPUT') { - user_input.value = user_answer; - } - if(user_input.tagName === 'TEXTAREA') { - user_input.innerText = user_answer; - } + if (user_input && !allanswersarr.includes(user_input) && questions_answers[`${questionid}`]) { + let user_answer = questions_answers[`${questionid}`][0]; + if (user_input.tagName === 'INPUT') { + user_input.value = user_answer; + } } } diff --git a/pod/quiz/templates/quiz/create_edit_quiz.html b/pod/quiz/templates/quiz/create_edit_quiz.html index 1dbb7a9f06..ee82d619bb 100644 --- a/pod/quiz/templates/quiz/create_edit_quiz.html +++ b/pod/quiz/templates/quiz/create_edit_quiz.html @@ -104,6 +104,8 @@

    {% include "main/mandatory_fields.html" %} + {% include "quiz/question_help_aside.html" with creator=True %} + {% endblock page_aside %} {% block more_script %} diff --git a/pod/quiz/templates/quiz/question_help_aside.html b/pod/quiz/templates/quiz/question_help_aside.html new file mode 100644 index 0000000000..d90f8a2646 --- /dev/null +++ b/pod/quiz/templates/quiz/question_help_aside.html @@ -0,0 +1,45 @@ +{% load i18n %} + +
    +

    {% trans "Help"%}

    + + +
    +

    + {% if creator is True %} + {% trans "For short answer questions, ensure responses are between 1 and 250 characters. This type of question is ideal for brief, specific answers." %} + {% else %} + {% trans "For short answer questions, please provide a response between 1 and 250 characters. Be concise and specific." %} + {% endif %} +

    +
    + + +
    +

    + {% if creator is True %} + {% trans "Single choice questions require participants to select one answer from a list of options. This is useful for questions with a clear, correct answer." %} + {% else %} + {% trans "For single choice questions, select only one answer from the provided options. Read each option carefully before making your selection." %} + {% endif %} +

    +
    + + +
    +

    + {% if creator is True %} + {% trans "Multiple choice questions allow participants to select more than one answer. Use this type for questions where more than one option could be correct." %} + {% else %} + {% trans "For multiple choice questions, select all answers that apply. There may be more than one correct answer." %} + {% endif %} +

    +
    + +
    diff --git a/pod/quiz/templates/quiz/video_quiz.html b/pod/quiz/templates/quiz/video_quiz.html index a625097151..e0c645e5d0 100644 --- a/pod/quiz/templates/quiz/video_quiz.html +++ b/pod/quiz/templates/quiz/video_quiz.html @@ -28,7 +28,7 @@

    {% endif %} - {% if form_submitted and questions_form_errors.items|length == 0 %} + {% if quiz.show_correct_answers and form_submitted and questions_form_errors.items|length == 0 %} {% if percentage_score >= 75 %} + {% elif not quiz.show_correct_answers %} + {% endif %}
    @@ -62,7 +66,7 @@ {% csrf_token %}
    {% for question in quiz.get_questions %} -
  • {% endif %} + {% if USE_SPEAKER %} +
  • + {% trans 'Speaker(s):' %} +
    +
      + {% get_video_speaker video as speaker_in_video %} + {% for speaker in speaker_in_video %} +
    • + {{ speaker.job.speaker.firstname }} {{ speaker.job.speaker.lastname }} ({{ speaker.job.title }}) +
    • + {% endfor %} +
    +
    +
  • + {% endif %}
  • {% trans 'Updated on:' %} diff --git a/pod/video/templates/videos/video_edit.html b/pod/video/templates/videos/video_edit.html index 2a919e41fd..47675ff493 100644 --- a/pod/video/templates/videos/video_edit.html +++ b/pod/video/templates/videos/video_edit.html @@ -54,7 +54,9 @@

    {% if form.errors %}

    {% trans "One or more errors have been found in the form." %}

    - {{form.errors}} + {% for key, err in form.errors.items %} + {{err}} + {% endfor %}
    {% endif %} @@ -145,6 +147,9 @@

  • {% endif %} + {% if field.id_for_label == "id_password" %} +
    + {% endif %} {% endif %} diff --git a/pod/video/templates/videos/video_list_grid_selectable.html b/pod/video/templates/videos/video_list_grid_selectable.html index ea5ea01159..c3ea11b8b0 100644 --- a/pod/video/templates/videos/video_list_grid_selectable.html +++ b/pod/video/templates/videos/video_list_grid_selectable.html @@ -4,7 +4,7 @@
    {% for video in videos %} -
    +
    {% include "videos/card_select.html" %}
    {% empty %} @@ -26,7 +26,7 @@ {% trans "Loading…" %}
    {% endspaceless %} - + {% if USE_PLAYLIST and USE_FAVORITES %} diff --git a/pod/video/templates/videos/video_list_table_selectable.html b/pod/video/templates/videos/video_list_table_selectable.html index d9750bb193..aab1492768 100644 --- a/pod/video/templates/videos/video_list_table_selectable.html +++ b/pod/video/templates/videos/video_list_table_selectable.html @@ -4,7 +4,7 @@
      {% for video in videos %} -
    • +
    • {% include "videos/video_row_select.html" %}
    • {% empty %} @@ -27,4 +27,4 @@ {% trans "Loading…" %}
    {% endspaceless %} - + diff --git a/pod/video/templates/videos/video_page_content.html b/pod/video/templates/videos/video_page_content.html index 7425bc6ae0..fd46ae1b33 100644 --- a/pod/video/templates/videos/video_page_content.html +++ b/pod/video/templates/videos/video_page_content.html @@ -3,6 +3,7 @@ {% if video.is_draft %}
    + {% trans "This video is in draft." %}
    {% endif %} @@ -85,22 +86,26 @@

    {% endif %} {% if enr_is_already_asked %} {% endif %} {% if video.get_encoding_step == "" %} {% endif %} {% if video.encoding_in_progress %} {% endif %} {% if video.get_encoding_step == "5 : transcripting audio" %}

    + {% trans 'The video is currently being transcripted.' %}

    {% endif %} diff --git a/pod/video/templates/videos/video_row_select.html b/pod/video/templates/videos/video_row_select.html index 36feea53c2..9554d8b98b 100644 --- a/pod/video/templates/videos/video_row_select.html +++ b/pod/video/templates/videos/video_row_select.html @@ -8,17 +8,30 @@ {% load playlist_buttons %} {% can_see_playlist_video video playlist as can_see_video %} {% endif %} -
    + + + +
    - - {{ video.title|capfirst|truncatechars:20 }} + + 20 %}title="{{video.title|capfirst}}"{% endif %}>{{video.title|capfirst|truncatechars:20}} {% trans 'Duration' %} @@ -26,7 +39,7 @@ {% trans 'Date added' %} -  {{ video.date_added }} +  {{ video.date_added|date }}
    @@ -63,7 +76,7 @@ {% if video_infos.chaptered.status %} class="bi bi-card-checklist text-success" {% else %} - class="bi bi-card-list text-muted" + class="bi bi-card-list text-danger" {% endif %} aria-hidden="true"> @@ -78,78 +91,13 @@ {% endif %}
    +
    - + {% include 'videos/link_video_dropdown_menu.html' %}
    {% endspaceless %} diff --git a/pod/video/tests/test_bulk_update.py b/pod/video/tests/test_bulk_update.py index d6a8c36f27..ac3e217331 100644 --- a/pod/video/tests/test_bulk_update.py +++ b/pod/video/tests/test_bulk_update.py @@ -6,6 +6,7 @@ import json from datetime import datetime +from django.contrib.auth.models import Permission from django.contrib.messages.storage.fallback import FallbackStorage from django.contrib.sites.models import Site from django.test import RequestFactory, Client, TransactionTestCase @@ -13,6 +14,7 @@ from pod.authentication.backends import User from pod.video.models import Video, Type from pod.video.views import bulk_update +from pod.video_encode_transcript.models import PlaylistVideo class BulkUpdateTestCase(TransactionTestCase): @@ -134,6 +136,26 @@ def test_bulk_update_type(self) -> None: self.client.force_login(user1) + post_request = self.factory.post( + "/bulk_update/", + { + "type": type2.id, + "selected_videos": '["%s", "%s"]' + % ("slug_that_not_exist", "slug_that_not_exist2"), + "update_fields": '["type"]', + "update_action": "fields", + }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + post_request.user = user1 + post_request.LANGUAGE_CODE = "fr" + response = bulk_update(post_request) + self.assertEqual(response.status_code, 400) + self.assertEqual( + json.loads(response.content)["message"], "Sorry, no video found." + ) + self.assertEqual(json.loads(response.content)["updated_videos"], []) + post_request = self.factory.post( "/bulk_update/", { @@ -218,7 +240,7 @@ def test_bulk_update_owner(self): print("---> test_bulk_update_owner of BulkUpdateTestCase: OK") - def test_bulk_delete(self): + def test_bulk_delete(self) -> None: """Test bulk delete.""" video4 = Video.objects.get(title="Video4") video5 = Video.objects.get(title="Video5") @@ -250,10 +272,48 @@ def test_bulk_delete(self): "You cannot delete a video that is being encoded. 0 videos removed, 2 videos in error", ) + PlaylistVideo.objects.create( + name="playlist", + video=video4, + encoding_format="application/x-mpegURL", + source_file="test.mp4", + ) + PlaylistVideo.objects.create( + name="playlist", + video=video5, + encoding_format="application/x-mpegURL", + source_file="test.mp4", + ) + + post_request = self.factory.post( + "/bulk_update/", + { + "selected_videos": '["%s", "%s"]' % (video4.slug, video5.slug), + "update_fields": "[]", + "update_action": "delete", + }, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + + permission = Permission.objects.get(codename="delete_video") + user3.user_permissions.add(permission) + post_request.user = user3 + post_request.LANGUAGE_CODE = "fr" + setattr(post_request, "session", "session") + messages = FallbackStorage(post_request) + setattr(post_request, "_messages", messages) + response = bulk_update(post_request) + + self.assertEqual(response.status_code, 200) + self.assertEqual( + json.loads(response.content)["message"], + " 2 videos removed, 0 videos in error", + ) + print("---> test_bulk_delete of BulkUpdateTestCase: OK") self.client.logout() - def tearDown(self): + def tearDown(self) -> None: """Cleanup all created stuffs.""" del self.client del self.factory diff --git a/pod/video/tests/test_category.py b/pod/video/tests/test_category.py index 14c7533ea5..e118e67260 100644 --- a/pod/video/tests/test_category.py +++ b/pod/video/tests/test_category.py @@ -1,5 +1,5 @@ from django.core.serializers.json import DjangoJSONEncoder -from django.http import HttpResponse, HttpResponseNotAllowed +from django.http import HttpResponse from django.http import HttpResponseBadRequest from django.http import HttpResponseRedirect, HttpResponseForbidden from django.template.defaultfilters import slugify @@ -16,7 +16,7 @@ class TestCategory(TestCase): "initial_data.json", ] - def setUp(self): + def setUp(self) -> None: self.maxDiff = None self.logger = logging.getLogger("django.request") # self.previous_level = self.logger.getEffectiveLevel() @@ -71,240 +71,80 @@ def setUp(self): ) self.cat_3.video.add(self.video) - def test_getCategories(self): - # not Authenticated, should return HttpResponseRedirect:302 - response = self.client.get(reverse("video:get_categories")) - self.assertIsInstance(response, HttpResponseRedirect) - self.assertEqual(response.status_code, 302) - - # not Ajax request, should return HttpResponseForbidden:403 + def test_get_add_category_modal(self) -> None: + """Test get add new category modal.""" self.client.force_login(self.owner_user) - response = self.client.get(reverse("video:get_categories")) - self.assertIsInstance(response, HttpResponseForbidden) - self.assertEqual(response.status_code, 403) - - # Ajax request, should return HttpResponse:200 with all categories - response = self.client.get( - reverse("video:get_categories"), HTTP_X_REQUESTED_WITH="XMLHttpRequest" - ) - - actual_data = json.loads(response.content.decode("utf-8")) - expected_data = { - "success": True, - "categories": [ - { - "title": "test2Category", - "slug": self.cat_2.slug, - "videos": [], - }, - { - "title": "testCategory", - "slug": self.cat_1.slug, - "videos": [ - { - "slug": self.video.slug, - "title": self.video.title, - "duration": self.video.duration_in_time, - "thumbnail": self.video.get_thumbnail_card(), - "is_video": self.video.is_video, - "is_draft": self.video.is_draft, - "is_restricted": self.video.is_restricted, - "has_chapter": self.video.chapter_set.all().count() > 0, - "has_password": bool(self.video.password), - } - ], - }, - ], - } - self.assertIsInstance(response, HttpResponse) - self.assertEqual(response.status_code, 200) - self.assertTrue(expected_data["success"]) - for i in range(2): - self.assertEqual( - actual_data["categories"][i]["title"], - expected_data["categories"][i]["title"], - ) - self.assertEqual( - actual_data["categories"][i]["slug"], - expected_data["categories"][i]["slug"], - ) - self.assertCountEqual( - actual_data["categories"][i]["videos"], - expected_data["categories"][i]["videos"], - ) - - # Ajax request, should return HttpResponse:200 with one category - response = self.client.get( - reverse("video:get_category", kwargs={"c_slug": self.cat_1.slug}), - HTTP_X_REQUESTED_WITH="XMLHttpRequest", - ) - - actual_data = json.loads(response.content.decode("utf-8")) - expected_data = { - "success": True, - "id": self.cat_1.id, - "slug": self.cat_1.slug, - "title": self.cat_1.title, - "owner": self.cat_1.owner.id, - "videos": list( - map( - lambda v: { - "slug": v.slug, - "title": v.title, - "duration": v.duration_in_time, - "thumbnail": v.get_thumbnail_card(), - "is_video": v.is_video, - "is_draft": v.is_draft, - "is_restricted": v.is_restricted, - "has_chapter": v.chapter_set.all().count() > 0, - "has_password": bool(v.password), - }, - self.cat_1.video.all(), - ) - ), - } - - self.assertIsInstance(response, HttpResponse) - self.assertEqual(response.status_code, 200) - self.assertCountEqual(list(actual_data.keys()), list(expected_data.keys())) - self.assertCountEqual(list(actual_data.values()), list(expected_data.values())) - - # GET category as additional owner - # Ajax request, should return HttpResponse:200 with one category - self.client.force_login(self.simple_user) + url = reverse("video:add_category") response = self.client.get( - reverse("video:get_category", kwargs={"c_slug": self.cat_3.slug}), + url, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - - actual_data = json.loads(response.content.decode("utf-8")) - expected_data = { - "success": True, - "id": self.cat_3.id, - "slug": self.cat_3.slug, - "title": self.cat_3.title, - "owner": self.cat_3.owner.id, - "videos": [ - { - "slug": self.video.slug, - "title": self.video.title, - "duration": self.video.duration_in_time, - "thumbnail": self.video.get_thumbnail_card(), - "is_video": self.video.is_video, - "is_draft": self.video.is_draft, - "is_restricted": self.video.is_restricted, - "has_chapter": self.video.chapter_set.all().count() > 0, - "has_password": bool(self.video.password), - } - ], - } - - self.assertIsInstance(response, HttpResponse) self.assertEqual(response.status_code, 200) - self.assertCountEqual(actual_data, expected_data) - - # GET category as no longer additional owner - # Ajax request, should return HttpResponse:200 with one category - self.video.additional_owners.remove(self.simple_user) - response = self.client.get( - reverse("video:get_category", kwargs={"c_slug": self.cat_3.slug}), - HTTP_X_REQUESTED_WITH="XMLHttpRequest", - ) - - actual_data = json.loads(response.content.decode("utf-8")) - expected_data = { - "success": True, - "id": self.cat_3.id, - "slug": self.cat_3.slug, - "title": self.cat_3.title, - "owner": self.cat_3.owner.id, - "videos": [], + self.assertTemplateUsed(response, "videos/category_modal.html") + print(" ---> test_get_add_category_modal of TestCategory: OK!") + + def test_post_add_category(self) -> None: + """Test perform add new category.""" + data = { + "title": json.dumps("Test new category"), + "videos": json.dumps([self.video_2.slug]), } - self.assertIsInstance(response, HttpResponse) - self.assertEqual(response.status_code, 200) - self.assertCountEqual(actual_data, expected_data) - - def test_addCategory(self): - data = {"title": "Test new category", "videos": [self.video_2.slug]} # not Authenticated, should return HttpResponseRedirect:302 - response = self.client.post( - reverse("video:add_category"), - json.dumps(data), - content_type="application/json", - ) + response = self.client.post(reverse("video:add_category"), data) self.assertIsInstance(response, HttpResponseRedirect) self.assertEqual(response.status_code, 302) # not Ajax request, should return HttpResponseForbidden:403 self.client.force_login(self.owner_user) - response = self.client.post( - reverse("video:add_category"), - json.dumps(data), - content_type="application/json", - ) + response = self.client.post(reverse("video:add_category"), data) self.assertIsInstance(response, HttpResponseForbidden) self.assertEqual(response.status_code, 403) - # Ajax GET request, should return HttpResponseNotAllowed:405 - response = self.client.get( - reverse("video:add_category"), HTTP_X_REQUESTED_WITH="XMLHttpRequest" - ) - - self.assertIsInstance(response, HttpResponseNotAllowed) - self.assertEqual(response.status_code, 405) - # Ajax POST request, should return HttpResponse:200 with category data response = self.client.post( reverse("video:add_category"), - json.dumps(data), - content_type="application/json", + data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - - actual_data = json.loads(response.content.decode("utf-8")) + response_data = json.loads(response.content) expected_data = { "success": True, - "title": data["title"], - "slug": "%s-%s" % (self.owner_user.id, slugify(data["title"])), - "videos": [ - { - "slug": self.video_2.slug, - "title": self.video_2.title, - "duration": self.video_2.duration_in_time, - "thumbnail": self.video_2.get_thumbnail_card(), - "is_video": self.video_2.is_video, - "is_draft": self.video_2.is_draft, - "is_restricted": self.video_2.is_restricted, - "has_chapter": self.video_2.chapter_set.all().count() > 0, - "has_password": bool(self.video_2.password), - } - ], + "title": "Test new category", + "slug": "%s-%s" % (self.owner_user.id, slugify("Test new category")), + "video": [self.video_2], } + actual_data = Category.objects.filter( + owner=self.owner_user, title="Test new category" + ) + actual_cat = actual_data.first() + self.assertIsInstance(response, HttpResponse) self.assertEqual(response.status_code, 200) - self.assertTrue(actual_data["success"]) - self.assertEqual(actual_data["category"]["title"], expected_data["title"]) - self.assertEqual(actual_data["category"]["slug"], expected_data["slug"]) - self.assertCountEqual(actual_data["category"]["videos"], expected_data["videos"]) + self.assertEqual(response_data["success"], True) + self.assertEqual(response_data["message"], "Category successfully added.") + self.assertEqual(actual_data.count(), 1) + self.assertEqual(actual_cat.title, expected_data["title"]) + self.assertEqual(actual_cat.slug, expected_data["slug"]) + self.assertEqual(actual_cat.video.count(), 1) + self.assertEqual(list(actual_cat.video.all()), expected_data["video"]) # Add video in another category # should return HttpResponseBadRequest:400 response = self.client.post( reverse("video:add_category"), - json.dumps(data), - content_type="application/json", + data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - actual_data = json.loads(response.content.decode("utf-8")) + response_data = json.loads(response.content) self.assertIsInstance(response, HttpResponseBadRequest) self.assertEqual(response.status_code, 400) - self.assertFalse(actual_data["success"]) + self.assertFalse(response_data["success"]) self.assertEqual( - actual_data["message"], + response_data["message"], "One or many videos already have a category.", ) @@ -313,8 +153,7 @@ def test_addCategory(self): del data["title"] response = self.client.post( reverse("video:add_category"), - json.dumps(data), - content_type="application/json", + data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) @@ -323,24 +162,40 @@ def test_addCategory(self): # Ajax POST request, should return HttpResponse:409 # category already exists - data = {"title": "Test new category", "videos": []} + data = {"title": json.dumps("Test new category"), "videos": json.dumps([])} + response = self.client.post( reverse("video:add_category"), - json.dumps(data), - content_type="application/json", + data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertIsInstance(response, HttpResponse) self.assertEqual(response.status_code, 409) + print(" ---> test_post_add_category of TestCategory: OK!") - def test_editCategory(self): - data = {"title": "New Category title", "videos": [self.video_2.slug]} + def test_get_edit_category_modal(self) -> None: + """Test get edit existent category modal.""" + self.client.force_login(self.owner_user) + url = reverse("video:edit_category", args=[self.cat_1.slug]) + response = self.client.get( + url, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "videos/category_modal.html") + print(" ---> test_get_edit_category_modal of TestCategory: OK!") + + def test_post_edit_category(self) -> None: + """Test perform edit existent category.""" + data = { + "title": json.dumps("New Category title"), + "videos": json.dumps([self.video_2.slug]), + } # not Authenticated, should return HttpResponseRedirect:302 response = self.client.post( reverse("video:edit_category", kwargs={"c_slug": self.cat_1.slug}), - json.dumps(data), - content_type="application/json", + data, ) self.assertIsInstance(response, HttpResponseRedirect) @@ -350,72 +205,70 @@ def test_editCategory(self): self.client.force_login(self.owner_user) response = self.client.post( reverse("video:edit_category", kwargs={"c_slug": self.cat_1.slug}), - json.dumps(data), - content_type="application/json", + data, ) self.assertIsInstance(response, HttpResponseForbidden) self.assertEqual(response.status_code, 403) - # Ajax GET request, should return HttpResponseNotAllowed:405 - response = self.client.get( + # Ajax request but with other user should return HttpResponseForbidden:403 + self.client.force_login(self.simple_user) + response = self.client.post( reverse("video:edit_category", kwargs={"c_slug": self.cat_1.slug}), + data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - self.assertIsInstance(response, HttpResponseNotAllowed) - self.assertEqual(response.status_code, 405) + self.assertIsInstance(response, HttpResponseForbidden) + self.assertEqual(response.status_code, 403) + self.assertEqual( + json.loads(response.content)["message"], + "You do not have rights to edit this category", + ) # Ajax POST request, should return HttpResponse:200 with category data + self.client.force_login(self.owner_user) response = self.client.post( reverse("video:edit_category", kwargs={"c_slug": self.cat_1.slug}), - json.dumps(data), - content_type="application/json", + data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - - actual_data = json.loads(response.content.decode("utf-8")) + response_data = json.loads(response.content) expected_data = { "success": True, - "title": data["title"], - "slug": "%s-%s" % (self.owner_user.id, slugify(data["title"])), - "videos": [ - { - "slug": self.video_2.slug, - "title": self.video_2.title, - "duration": self.video_2.duration_in_time, - "thumbnail": self.video_2.get_thumbnail_card(), - "is_video": self.video_2.is_video, - "is_draft": self.video_2.is_draft, - "is_restricted": self.video_2.is_restricted, - "has_chapter": self.video_2.chapter_set.all().count() > 0, - "has_password": bool(self.video_2.password), - } - ], + "title": "New Category title", + "slug": "%s-%s" % (self.owner_user.id, slugify("New Category title")), + "videos": [self.video_2], } + actual_data = Category.objects.filter( + owner=self.owner_user, slug=expected_data["slug"] + ) + actual_cat = actual_data.first() self.assertIsInstance(response, HttpResponse) self.assertEqual(response.status_code, 200) - self.assertTrue(actual_data["success"]) - self.assertEqual(actual_data["title"], expected_data["title"]) - self.assertEqual(actual_data["slug"], expected_data["slug"]) - self.assertCountEqual(actual_data["videos"], expected_data["videos"]) + self.assertTrue(response_data["success"]) + self.assertEqual(response_data["message"], "Category updated successfully.") + self.assertEqual(actual_cat.title, expected_data["title"]) + self.assertEqual(actual_cat.slug, expected_data["slug"]) + self.assertEqual(actual_cat.video.count(), 1) + self.assertEqual(actual_cat.video.all().first(), self.video_2) + self.assertCountEqual(list(actual_cat.video.all()), expected_data["videos"]) - # Add video in anthoer category + # Add video in another category # should return HttpResponseBadRequest:400 response = self.client.post( reverse("video:edit_category", kwargs={"c_slug": self.cat_2.slug}), - json.dumps(data), - content_type="application/json", + data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) + response_data = json.loads(response.content) - actual_data = json.loads(response.content.decode("utf-8")) self.assertIsInstance(response, HttpResponseBadRequest) self.assertEqual(response.status_code, 400) - self.assertFalse(actual_data["success"]) + self.assertFalse(response_data["success"]) self.assertEqual( - actual_data["message"], + response_data["message"], "One or many videos already have a category.", ) @@ -424,17 +277,59 @@ def test_editCategory(self): del data["title"] response = self.client.post( reverse("video:edit_category", kwargs={"c_slug": expected_data["slug"]}), - json.dumps(data), - content_type="application/json", + data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertIsInstance(response, HttpResponseBadRequest) self.assertEqual(response.status_code, 400) + print(" ---> test_post_edit_category of TestCategory: OK!") + + def test_get_categories(self) -> None: + """Test get categories from dashboard.""" + self.client.force_login(self.owner_user) + url = reverse("video:dashboard") + response = self.client.get(url, {"categories": [self.cat_1.slug]}) + response_data = response.context + all_categories_videos = json.loads(response_data["all_categories_videos"]) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response_data["videos"].paginator.count, 1) + self.assertEqual(response_data["categories"].count(), 2) + self.assertEqual(len(all_categories_videos[self.cat_1.slug]), 1) + self.assertEqual(all_categories_videos[self.cat_1.slug][0], self.video.slug) + print(" ---> test_get_categories of TestCategory: OK!") + + def test_get_categories_aside(self) -> None: + """Test get categories for filter aside elements.""" + self.client.force_login(self.owner_user) + url = reverse("video:get_categories_list") + response = self.client.get( + url, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + response_data = response.context + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "videos/filter_aside_categories_list.html") + self.assertEqual(response_data["categories"].count(), 2) + print(" ---> test_get_categories_aside of TestCategory: OK!") - def test_deleteCategory(self): + def test_get_delete_category_modal(self) -> None: + """Test get delete existent category modal.""" + self.client.force_login(self.owner_user) + url = reverse("video:delete_category", args=[self.cat_1.slug]) + response = self.client.get( + url, + HTTP_X_REQUESTED_WITH="XMLHttpRequest", + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "videos/category_modal.html") + print(" ---> test_get_delete_category_modal of TestCategory: OK!") + + def test_post_delete_category(self) -> None: + """Test perform delete existent category.""" # not Authenticated, should return HttpResponseRedirect:302 response = self.client.post( - reverse("video:delete_category", kwargs={"c_id": self.cat_1.id}), + reverse("video:delete_category", kwargs={"c_slug": self.cat_1.slug}), content_type="application/json", ) @@ -444,60 +339,41 @@ def test_deleteCategory(self): # not Ajax request, should return HttpResponseForbidden:403 self.client.force_login(self.owner_user) response = self.client.post( - reverse("video:delete_category", kwargs={"c_id": self.cat_1.id}), + reverse("video:delete_category", kwargs={"c_slug": self.cat_1.slug}), content_type="application/json", ) self.assertIsInstance(response, HttpResponseForbidden) self.assertEqual(response.status_code, 403) - # Ajax GET request, should return HttpResponseNotAllowed:405 - response = self.client.get( - reverse("video:delete_category", kwargs={"c_id": self.cat_1.id}), - HTTP_X_REQUESTED_WITH="XMLHttpRequest", - ) - - self.assertIsInstance(response, HttpResponseNotAllowed) - self.assertEqual(response.status_code, 405) - # Ajax POST request but not category's owner, # should return HttpResponseForbidden:403 self.client.force_login(self.simple_user) response = self.client.post( - reverse("video:delete_category", kwargs={"c_id": self.cat_1.id}), + reverse("video:delete_category", kwargs={"c_slug": self.cat_1.slug}), HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) - response = self.client.get(reverse("video:get_categories")) - self.assertIsInstance(response, HttpResponseForbidden) - self.assertEqual(response.status_code, 403) self.client.force_login(self.owner_user) # Ajax POST request, should return HttpResponse:200 with category data response = self.client.post( - reverse("video:delete_category", kwargs={"c_id": self.cat_1.id}), + reverse("video:delete_category", kwargs={"c_slug": self.cat_1.slug}), content_type="application/json", HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) + response_data = json.loads(response.content) - actual_data = json.loads(response.content.decode("utf-8")) - - self.assertTrue(actual_data["success"]) + self.assertIsInstance(response, HttpResponse) self.assertEqual(response.status_code, 200) - self.assertEqual(actual_data["id"], self.cat_1.id) - self.assertCountEqual( - actual_data["videos"], - [ - { - "slug": self.video.slug, - "title": self.video.title, - "duration": self.video.duration_in_time, - "thumbnail": self.video.get_thumbnail_card(), - "is_video": self.video.is_video, - } - ], + self.assertTrue(response_data["success"]) + self.assertEqual( + response_data["message"], + "Category successfully deleted.", ) + self.assertEqual(Category.objects.filter(slug=self.cat_1.slug).count(), 0) + print(" ---> test_post_delete_category of TestCategory: OK!") - def tearDown(self): + def tearDown(self) -> None: del self.video del self.owner_user del self.admin_user diff --git a/pod/video/tests/test_models.py b/pod/video/tests/test_models.py index 6925444c04..5dc39a9ddd 100644 --- a/pod/video/tests/test_models.py +++ b/pod/video/tests/test_models.py @@ -3,49 +3,46 @@ * run with 'python manage.py test pod.video.tests.test_models' """ -from django.test import TestCase +from django.test import TestCase, Client +from django.db import transaction from django.db.models import Count, Q -from django.template.defaultfilters import slugify from django.db.models.fields.files import ImageFieldFile +from django.db.utils import IntegrityError +from django.template.defaultfilters import slugify from django.contrib.auth.models import User from django.utils.translation import ugettext_lazy as _ +from django.urls import reverse from django.conf import settings from django.core.exceptions import ValidationError -from django.db.utils import IntegrityError -from django.db import transaction -from ..models import Channel -from ..models import Theme -from ..models import Type -from ..models import Discipline -from ..models import Video -from ..models import ViewCount +from ..models import Channel, Theme, Type +from ..models import Discipline, Video, ViewCount from ..models import get_storage_path_video from ..models import VIDEOS_DIR from ..models import Notes, AdvancedNotes from ..models import UserMarkerTime, VideoAccessToken -from pod.video_encode_transcript.models import VideoRendition -from pod.video_encode_transcript.models import EncodingVideo -from pod.video_encode_transcript.models import EncodingAudio -from pod.video_encode_transcript.models import PlaylistVideo -from pod.video_encode_transcript.models import EncodingLog -from pod.video_encode_transcript.models import EncodingStep +from pod.video_encode_transcript.models import VideoRendition, PlaylistVideo +from pod.video_encode_transcript.models import EncodingVideo, EncodingAudio +from pod.video_encode_transcript.models import EncodingLog, EncodingStep -from datetime import datetime -from datetime import timedelta +from datetime import datetime, timedelta import os import uuid if getattr(settings, "USE_PODFILE", False): __FILEPICKER__ = True - from pod.podfile.models import CustomImageModel - from pod.podfile.models import UserFolder + from pod.podfile.models import CustomImageModel, UserFolder else: __FILEPICKER__ = False from pod.main.models import CustomImageModel +# ggignore-start +# gitguardian:ignore +PWD = "azerty1234" # nosec +# ggignore-end + class ChannelTestCase(TestCase): """Test the channels.""" @@ -283,7 +280,7 @@ class VideoTestCase(TestCase): def setUp(self) -> None: """Create videos to be tested.""" - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) # Video 1 with minimum attributes Video.objects.create( @@ -421,6 +418,78 @@ def test_get_dublin_core(self) -> None: print(" ---> test_get_dublin_core of Video: OK!") + def test_video_additional_owners_rights(self) -> None: + """Check that additional owners have the correct rights.""" + # Create 2nd and 3rd staff users + user2 = User.objects.create(username="user2", password=PWD) + user2.is_staff = True + user2.save() + user3 = User.objects.create(username="user3", password=PWD) + user3.is_staff = True + user3.save() + + # Get the test video and associated Userfolder + video = Video.objects.get(id=1) + video_folder = video.get_or_create_video_folder() + + # Add an additional owner to the video + video.additional_owners.set([user2]) + video.save() + + # Check user2 can access to video folder + client = Client() + client.force_login(user2) + response = client.post( + reverse("podfile:editfolder"), + { + "folderid": video_folder.id, + }, + follow=True, + ) + + print("response: %s" % response) + self.assertEqual(response.status_code, 200) # OK + # Replace aditional owner by another one + video.additional_owners.set([user3]) + video.save() + + # Check user2 no more access video folder + response = client.post( + reverse("podfile:editfolder"), + { + "folderid": video_folder.id, + }, + follow=True, + ) + self.assertEqual(response.status_code, 403) # forbidden + + print("---> test_video_additional_owners_rights of VideoTestCase: OK") + + def test_synced_user_folder(self) -> None: + """Check that UserFolder is synced with video params.""" + # Create 2nd staff user + user2 = User.objects.create(username="user2", password=PWD) + user2.is_staff = True + user2.save() + + # Get the test video and associated Userfolder + video = Video.objects.get(id=1) + video_folder = video.get_or_create_video_folder() + + # Then, change owner and rename the video + video.owner = user2 + video.title = ("Video renamed",) + video.save() + + video_folder2 = video.get_or_create_video_folder() + + # Check there is no duplicated folder + self.assertEqual(video_folder2.id, video_folder.id) + self.assertEqual(video_folder2.name, video.slug) + self.assertEqual(video_folder2.owner, video.owner) + + print("---> test_synced_user_folder of VideoTestCase: OK") + class VideoRenditionTestCase(TestCase): """Test the Video Rendition.""" @@ -429,14 +498,14 @@ class VideoRenditionTestCase(TestCase): def create_video_rendition( self, - resolution="640x360", - minrate="500k", - video_bitrate="1000k", - maxrate="2000k", - audio_bitrate="300k", - encode_mp4=False, + resolution: str = "640x360", + minrate: str = "500k", + video_bitrate: str = "1000k", + maxrate: str = "2000k", + audio_bitrate: str = "300k", + encode_mp4: bool = False, ) -> VideoRendition: - # print("create_video_rendition: %s" % resolution) + """Create a video rendition.""" return VideoRendition.objects.create( resolution=resolution, minrate=minrate, @@ -524,7 +593,7 @@ class EncodingVideoTestCase(TestCase): # fixtures = ['initial_data.json', ] def setUp(self) -> None: - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) Type.objects.create(title="test") Video.objects.create( title="Video1", @@ -607,7 +676,7 @@ class EncodingAudioTestCase(TestCase): # fixtures = ['initial_data.json', ] def setUp(self) -> None: - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) Type.objects.create(title="test") Video.objects.create( title="Video1", @@ -676,7 +745,7 @@ class PlaylistVideoTestCase(TestCase): ] def setUp(self) -> None: - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) Video.objects.create( title="Video1", owner=user, @@ -744,7 +813,7 @@ class EncodingLogTestCase(TestCase): ] def setUp(self) -> None: - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) Video.objects.create( title="Video1", owner=user, @@ -785,7 +854,7 @@ class EncodingStepTestCase(TestCase): ] def setUp(self) -> None: - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) Video.objects.create( title="Video1", owner=user, @@ -832,7 +901,7 @@ class NotesTestCase(TestCase): ] def setUp(self) -> None: - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) Video.objects.create( title="Video1", owner=user, @@ -910,7 +979,7 @@ class UserMarkerTimeTestCase(TestCase): ] def setUp(self) -> None: - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) Video.objects.create( title="Video1", owner=user, @@ -985,7 +1054,7 @@ class VideoAccessTokenTestCase(TestCase): ] def setUp(self) -> None: - user = User.objects.create(username="pod", password="pod1234pod") + user = User.objects.create(username="pod", password=PWD) print("VIDEO: %s" % Video.objects.all().count()) self.video = Video.objects.create( title="Video1", diff --git a/pod/video/tests/test_views.py b/pod/video/tests/test_views.py index d9c398f445..bc36051e86 100644 --- a/pod/video/tests/test_views.py +++ b/pod/video/tests/test_views.py @@ -852,27 +852,28 @@ def test_video_edit_post_request(self) -> None: "main_lang": "fr", "cursus": "0", "type": 1, + "visibility": "public", }, follow=True, ) self.assertEqual(response.status_code, HTTPStatus.OK) - # print(response.context["form"].errors) self.assertTrue(b"The changes have been saved." in response.content) v = Video.objects.get(title="VideoTest1") self.assertEqual(v.description, "

    bl

    ") - videofile = SimpleUploadedFile( + video_file = SimpleUploadedFile( "file.mp4", b"file_content", content_type="video/mp4" ) url = reverse("video:video_edit", kwargs={"slug": v.slug}) response = self.client.post( url, { - "video": videofile, + "video": video_file, "title": "VideoTest1", "main_lang": "fr", "cursus": "0", "type": 1, + "visibility": "public", }, follow=True, ) @@ -882,19 +883,20 @@ def test_video_edit_post_request(self) -> None: p = re.compile(r"^videos/([\d\w]+)/file([_\d\w]*).mp4$") self.assertRegex(v.video.name, p) # new one - videofile = SimpleUploadedFile( + video_file = SimpleUploadedFile( "file.mp4", b"file_content", content_type="video/mp4" ) url = reverse("video:video_edit", kwargs={}) self.client.post( url, { - "video": videofile, + "video": video_file, "title": "VideoTest2", "description": "

    coucou

    \r\n", "main_lang": "fr", "cursus": "0", "type": 1, + "visibility": "public", }, ) self.assertEqual(response.status_code, HTTPStatus.OK) @@ -916,6 +918,7 @@ def test_video_edit_post_request(self) -> None: "cursus": "0", "type": 1, "additional_owners": [self.user.pk], + "visibility": "public", }, follow=True, ) @@ -924,19 +927,20 @@ def test_video_edit_post_request(self) -> None: v = Video.objects.get(title="VideoTest3") self.assertEqual(v.description, "

    bl

    ") - videofile = SimpleUploadedFile( + video_file = SimpleUploadedFile( "file.mp4", b"file_content", content_type="video/mp4" ) url = reverse("video:video_edit", kwargs={"slug": v.slug}) response = self.client.post( url, { - "video": videofile, + "video": video_file, "title": "VideoTest3", "main_lang": "fr", "cursus": "0", "type": 1, "additional_owners": [self.user.pk], + "visibility": "public", }, follow=True, ) @@ -945,7 +949,7 @@ def test_video_edit_post_request(self) -> None: print(" ---> test_video_edit_post_request of VideoEditTestView: OK!") -class video_deleteTestView(TestCase): +class VideoDeleteTestView(TestCase): fixtures = [ "initial_data.json", ] diff --git a/pod/video/urls.py b/pod/video/urls.py index ed1bb6d83b..99525a0308 100644 --- a/pod/video/urls.py +++ b/pod/video/urls.py @@ -14,7 +14,7 @@ video_count, video_marker, video_version, - get_categories, + get_categories_list, add_category, edit_category, delete_category, @@ -113,23 +113,16 @@ # VIDEO CATEGORY if getattr(settings, "USER_VIDEO_CATEGORY", False): urlpatterns += [ - url(r"^my/categories/add/$", add_category, name="add_category"), + url(r"^categories/$", get_categories_list, name="get_categories_list"), + url(r"^category/add/$", add_category, name="add_category"), url( - r"^my/categories/edit/(?P[\-\d\w]+)/$", - edit_category, - name="edit_category", + r"^category/edit/(?P[\-\d\w]+)/$", edit_category, name="edit_category" ), url( - r"^my/categories/delete/(?P[\d]+)/$", + r"^category/delete/(?P[\-\d\w]+)/$", delete_category, name="delete_category", ), - url( - r"^my/categories/(?P[\-\d\w]+)/$", - get_categories, - name="get_category", - ), - url(r"^my/categories/$", get_categories, name="get_categories"), ] if getattr(settings, "USE_STATS_VIEW", False): diff --git a/pod/video/views.py b/pod/video/views.py index b477d42a9d..f50ceefefb 100644 --- a/pod/video/views.py +++ b/pod/video/views.py @@ -11,7 +11,7 @@ from django.shortcuts import get_object_or_404 from django.shortcuts import render from django.http import HttpResponse, JsonResponse -from django.http import HttpResponseNotAllowed, HttpResponseNotFound +from django.http import HttpResponseNotFound from django.http import HttpResponseForbidden, HttpResponseBadRequest from django.http import QueryDict, Http404 from django.views.decorators.csrf import csrf_protect @@ -84,6 +84,7 @@ from datetime import date from chunked_upload.models import ChunkedUpload from chunked_upload.views import ChunkedUploadView, ChunkedUploadCompleteView +from itertools import chain from django.db import IntegrityError from django.db.models import QuerySet @@ -542,44 +543,19 @@ def dashboard(request): display_mode, etc... """ data_context = {} - site = get_current_site(request) - # Videos list which user is the owner + which user is an additional owner - videos_list = request.user.video_set.all().filter( - sites=site - ) | request.user.owners_videos.all().filter(sites=site) - videos_list = videos_list.distinct() + videos_list = get_videos_for_owner(request) if USER_VIDEO_CATEGORY: - cats = Category.objects.prefetch_related("video").filter(owner=request.user) - """ - " user's videos categories format => - " [{ - " 'title': cat_title, - " 'slug': cat_slug, - " 'videos': [v_slug, v_slug...] },] - """ - if request.GET.get("category") is not None: - category_checked = request.GET.get("category") - videos_list = get_object_or_404( - Category, slug=category_checked, owner=request.user - ).video.all() - - videos_without_cat = videos_list.exclude(category__in=cats) - cats = list( - map( - lambda c: { - "id": c.id, - "title": c.title, - "slug": c.slug, - "videos": list(c.video.values_list("slug", flat=True)), - }, - cats, - ) - ) - cats.insert(0, len(videos_list)) - cats = json.dumps(cats, ensure_ascii=False) - data_context["categories"] = cats - data_context["videos_without_cat"] = videos_without_cat + categories = Category.objects.prefetch_related("video").filter(owner=request.user) + if len(request.GET.getlist("categories")): + categories_checked = request.GET.getlist("categories") + categories_videos = categories.filter( + slug__in=categories_checked + ).values_list("video", flat=True) + videos_list = videos_list.filter(pk__in=categories_videos) + + data_context["categories"] = categories + data_context["all_categories_videos"] = get_json_videos_categories(request) page = request.GET.get("page", 1) full_path = "" @@ -2514,6 +2490,24 @@ def get_videos(p_slug, target, p_slug_t=None): return (videos, title) +def get_videos_for_owner(request: WSGIRequest): + """ + Retrieve a list of videos associated with the current user. + + Args: + request (HttpRequest): The HTTP request object containing the user. + + Returns: + list: A video list of specific user. + """ + site = get_current_site(request) + # Videos list which user is the owner + which user is an additional owner + videos_list = request.user.video_set.filter( + sites=site + ) | request.user.owners_videos.filter(sites=site) + return videos_list.distinct() + + def view_stats_if_authenticated(user): if VIEW_STATS_AUTH and user.__str__() == "AnonymousUser": return False @@ -2937,73 +2931,98 @@ def delete_comment(request, video_slug, comment_id): ) +@login_required(redirect_field_name="referrer") +def get_videos_for_category(request, videos_list: dict, category=None): + """ + Get paginated videos for category modal. + + Args: + request (::class::`django.core.handlers.wsgi.WSGIRequest`): The WSGI request. + videos_list (::class::`django.http.QueryDict`): The video list. + category (::class::`pod.video.models.Category`): Optional category object. + + Returns: + Return paginated videos in paginator object. + """ + cats = Category.objects.prefetch_related("video").filter(owner=request.user) + videos = videos_list.exclude(category__in=cats) + + if category is not None: + videos = list(chain(category.video.all(), videos)) + + page = request.GET.get("page", 1) + + paginator = Paginator(videos, 12) + paginated_videos_without_cat = get_paginated_videos(paginator, page) + + return paginated_videos_without_cat + + @login_required(redirect_field_name="referrer") @ajax_required -def get_categories(request, c_slug=None): - """Get categories.""" - response = {"success": False} - c_user = request.user # connected user +def get_categories_list(request): + """ + Get actual categories list for filter_aside elements. - # GET method - if c_slug: # get category with slug - cat = get_object_or_404(Category, slug=c_slug) - response["success"] = True - response["id"] = cat.id - response["title"] = cat.title - response["owner"] = cat.owner.id - response["slug"] = cat.slug - response["videos"] = [] - for v in cat.video.all(): - if v.owner == cat.owner or cat.owner in v.additional_owners.all(): - response["videos"].append(get_video_data(v)) - else: - # delete if user is no longer owner - # or additional owner of the video - cat.video.remove(v) + Args: + request (::class::`django.core.handlers.wsgi.WSGIRequest`): The WSGI request. - return HttpResponse( - json.dumps(response, cls=DjangoJSONEncoder), - content_type="application/json", - ) + Returns: + Template of categories list item in filter aside. + """ + data_context = {} + categories = Category.objects.prefetch_related("video").filter(owner=request.user) + data_context["categories"] = categories + return render(request, "videos/filter_aside_categories_list.html", data_context) - else: # get all categories of connected user - cats = Category.objects.prefetch_related("video").filter(owner=c_user) - cats = list( - map( - lambda c: { - "title": c.title, - "slug": c.slug, - "videos": list( - map( - lambda v: get_video_data(v), - c.video.all(), - ) - ), - }, - cats, - ) - ) - response["success"] = True - response["categories"] = cats +@login_required(redirect_field_name="referrer") +def get_json_videos_categories(request): + """ + Get categories with associated videos in json object. - return HttpResponse( - json.dumps(response, cls=DjangoJSONEncoder), - content_type="application/json", - ) + Args: + request (::class::`django.core.handlers.wsgi.WSGIRequest`): The WSGI request. + + Returns: + Json object with category slug as key and array of video(s) as value. + """ + categories = Category.objects.prefetch_related("video").filter(owner=request.user) + all_categories_videos = {} + for cat in categories: + videos = list(cat.video.all().values_list("slug", flat=True)) + all_categories_videos[cat.slug] = videos + return json.dumps(all_categories_videos) @login_required(redirect_field_name="referrer") @ajax_required def add_category(request): - """Add category.""" - response = {"success": False} - c_user = request.user # connected user + """ + Add category managment. Get method return datas to fill the modal interface. Post method perform the insert. + + Args: + request (::class::`django.core.handlers.wsgi.WSGIRequest`): The WSGI request. + + Returns: + ::class::`django.http.HttpResponse`: The HTTP response. + """ + if request.method == "POST": + + response = {"success": False} + c_user = request.user + + if not request.POST.get("title") or json.loads(request.POST.get("title")) == "": + response["message"] = _("Title field is required") + return HttpResponseBadRequest( + json.dumps(response, cls=DjangoJSONEncoder), + content_type="application/json", + ) - if request.method == "POST": # create new category - data = json.loads(request.body.decode("utf-8")) + title = json.loads(request.POST.get("title")) + videos_slugs = json.loads(request.POST.get("videos")) - videos = Video.objects.filter(slug__in=data.get("videos", [])) + videos = Video.objects.filter(slug__in=videos_slugs) # constraint, video can be only in one of user's categories user_cats = Category.objects.filter(owner=c_user) @@ -3017,58 +3036,72 @@ def add_category(request): content_type="application/json", ) - if "title" in data and data["title"].strip() != "": - try: - cat = Category.objects.create(title=data["title"], owner=c_user) - cat.video.add(*videos) - cat.save() - except IntegrityError: # cannot duplicate category - return HttpResponse(status=409) - - response["category"] = {} - response["category"]["id"] = cat.id - response["category"]["title"] = cat.title - response["category"]["slug"] = cat.slug - response["success"] = True - response["category"]["videos"] = list( - map( - lambda v: get_video_data(v), - cat.video.all(), - ) - ) + try: + cat = Category.objects.create(title=title, owner=c_user) + cat.video.add(*videos) + cat.save() + except IntegrityError: # cannot duplicate category + return HttpResponse(status=409) - return HttpResponse( - json.dumps(response, cls=DjangoJSONEncoder), - content_type="application/json", - ) + response["success"] = True + response["message"] = _("Category successfully added.") - response["message"] = _("Title field is required") - return HttpResponseBadRequest( + return HttpResponse( json.dumps(response, cls=DjangoJSONEncoder), content_type="application/json", ) + else: + data_context = {} - return HttpResponseNotAllowed(_("Method Not Allowed")) + videos_list = get_videos_for_owner(request) + videos = get_videos_for_category(request, videos_list) + data_context["videos"] = videos + + if request.GET.get("page"): + return render(request, "videos/category_modal_video_list.html", data_context) + + data_context = { + "modal_action": "add", + "modal_title": _("Add new category"), + "videos": videos, + } + return render(request, "videos/category_modal.html", data_context) @login_required(redirect_field_name="referrer") @ajax_required -def edit_category(request, c_slug): - """Edit category.""" - response = {"success": False} - c_user = request.user # connected user +def edit_category(request, c_slug=None): + """ + Edit category managment. Get method return datas to fill the modal interface. Post method perform the update. + + Args: + request (::class::`django.core.handlers.wsgi.WSGIRequest`): The WSGI request. + c_slug (str): The optionnal category's slug. - if request.method == "POST": # edit current category + Returns: + ::class::`django.http.HttpResponse`: The HTTP response. + """ + if request.method == "POST": + response = {"success": False} + c_user = request.user cat = get_object_or_404(Category, slug=c_slug) - data = json.loads(request.body.decode("utf-8")) - new_videos = Video.objects.filter(slug__in=data.get("videos", [])) + if not request.POST.get("title") or json.loads(request.POST.get("title")) == "": + response["message"] = _("Title field is required") + return HttpResponseBadRequest( + json.dumps(response, cls=DjangoJSONEncoder), + content_type="application/json", + ) + + title = json.loads(request.POST.get("title")) + videos_slugs = json.loads(request.POST.get("videos")) + + new_videos = Video.objects.filter(slug__in=videos_slugs) # constraint, video can be only in one of user's categories, - # excepte current category + # except current category user_cats = Category.objects.filter(owner=c_user).exclude(id=cat.id) v_already_in_user_cat = new_videos.filter(category__in=user_cats) - if v_already_in_user_cat: response["message"] = _("One or many videos already have a category.") @@ -3077,55 +3110,69 @@ def edit_category(request, c_slug): content_type="application/json", ) - if "title" in data and data["title"].strip() != "": - if c_user == cat.owner or c_user.is_superuser: - cat.title = data["title"] - cat.video.set(list(new_videos)) - cat.save() - response["id"] = cat.id - response["title"] = cat.title - response["slug"] = cat.slug - response["success"] = True - response["message"] = _("Category updated successfully.") - response["videos"] = list( - map( - lambda v: get_video_data(v), - cat.video.all(), - ) - ) + if c_user == cat.owner or c_user.is_superuser: + cat.title = title + cat.video.set(list(new_videos)) + cat.save() - return HttpResponse( - json.dumps(response, cls=DjangoJSONEncoder), - content_type="application/json", - ) + response["success"] = True + response["message"] = _("Category updated successfully.") + response["all_categories_videos"] = get_json_videos_categories(request) - response["message"] = _("You do not have rights to edit this category") - return HttpResponseForbidden( + return HttpResponse( json.dumps(response, cls=DjangoJSONEncoder), content_type="application/json", ) - response["message"] = _("Title field is required") - return HttpResponseBadRequest( + response["message"] = _("You do not have rights to edit this category") + return HttpResponseForbidden( json.dumps(response, cls=DjangoJSONEncoder), content_type="application/json", ) + else: + category = get_object_or_404(Category, slug=c_slug, owner=request.user) + category_videos = list(category.video.all().values_list("id", flat=True)) - response["message"] = _("Method Not Allowed") - return HttpResponseNotAllowed( - json.dumps(response, cls=DjangoJSONEncoder), - content_type="application/json", - ) + videos_list = get_videos_for_owner(request) + videos = get_videos_for_category(request, videos_list, category) + + if request.GET.get("page"): + return render( + request, + "videos/category_modal_video_list.html", + { + "videos": videos, + "category_videos": category_videos, + }, + ) + + data_context = { + "modal_action": "edit", + "modal_title": _("Edit category") + " " + category.title, + "videos": videos, + "category": category, + "category_videos": category_videos, + } + return render(request, "videos/category_modal.html", data_context) @login_required(redirect_field_name="referrer") @ajax_required -def delete_category(request, c_id): - response = {"success": False} - c_user = request.user # connected user +def delete_category(request, c_slug): + """ + Delete category managment. Get method return datas to fill the modal interface. Post method perform the deletion. - if request.method == "POST": # create new category - cat = get_object_or_404(Category, id=c_id) + Args: + request (::class::`django.core.handlers.wsgi.WSGIRequest`): The WSGI request. + c_slug (str): The category's slug. + + Returns: + ::class::`django.http.HttpResponse`: The HTTP response. + """ + if request.method == "POST": + response = {"success": False} + c_user = request.user # connected user + cat = get_object_or_404(Category, slug=c_slug) if cat.owner == c_user: response["id"] = cat.id @@ -3144,6 +3191,7 @@ def delete_category(request, c_id): cat.delete() response["success"] = True + response["message"] = _("Category successfully deleted.") return HttpResponse( json.dumps(response, cls=DjangoJSONEncoder), @@ -3156,8 +3204,15 @@ def delete_category(request, c_id): json.dumps(response, cls=DjangoJSONEncoder), content_type="application/json", ) + else: + category = get_object_or_404(Category, slug=c_slug, owner=request.user) - return HttpResponseNotAllowed(_("Method Not Allowed")) + data_context = { + "modal_action": "delete", + "modal_title": _("Delete category") + " " + category.title, + "category": category, + } + return render(request, "videos/category_modal.html", data_context) class PodChunkedUploadView(ChunkedUploadView): diff --git a/pod/video_encode_transcript/Encoding_video_model.py b/pod/video_encode_transcript/Encoding_video_model.py index 7781c4995a..deaa5712a8 100644 --- a/pod/video_encode_transcript/Encoding_video_model.py +++ b/pod/video_encode_transcript/Encoding_video_model.py @@ -48,7 +48,6 @@ if getattr(settings, "USE_PODFILE", False): __FILEPICKER__ = True from pod.podfile.models import CustomImageModel - from pod.podfile.models import UserFolder from pod.podfile.models import CustomFileModel else: __FILEPICKER__ = False @@ -59,7 +58,7 @@ class Encoding_video_model(Encoding_video): """Encoding video model.""" - def remove_old_data(self): + def remove_old_data(self) -> None: """Remove data from previous encoding.""" video_to_encode = Video.objects.get(id=self.id) video_to_encode.thumbnail = None @@ -91,7 +90,7 @@ def remove_previous_encoding_log(self, video_to_encode): msg += "Audio: Nothing to delete" return msg - def remove_previous_encoding_objects(self, model_class, video_to_encode): + def remove_previous_encoding_objects(self, model_class, video_to_encode) -> str: """Remove previously encoded objects of the given model.""" msg = "\n" object_type = model_class.__name__ @@ -106,15 +105,15 @@ def remove_previous_encoding_objects(self, model_class, video_to_encode): msg += "Video: Nothing to delete" return msg - def remove_previous_encoding_video(self, video_to_encode): + def remove_previous_encoding_video(self, video_to_encode) -> str: """Remove previously encoded video.""" return self.remove_previous_encoding_objects(EncodingVideo, video_to_encode) - def remove_previous_encoding_audio(self, video_to_encode): + def remove_previous_encoding_audio(self, video_to_encode) -> str: """Remove previously encoded audio.""" return self.remove_previous_encoding_objects(EncodingAudio, video_to_encode) - def remove_previous_encoding_playlist(self, video_to_encode): + def remove_previous_encoding_playlist(self, video_to_encode) -> str: """Remove previously encoded playlist.""" return self.remove_previous_encoding_objects(PlaylistVideo, video_to_encode) @@ -122,7 +121,7 @@ def get_true_path(self, original): """Get the true path by replacing the MEDIA_ROOT from the original path.""" return original.replace(os.path.join(settings.MEDIA_ROOT, ""), "") - def store_json_list_mp3_m4a_files(self, info_video, video_to_encode): + def store_json_list_mp3_m4a_files(self, info_video, video_to_encode) -> None: """Store JSON list of MP3 and M4A files for encoding.""" encoding_list = ["list_m4a_files", "list_mp3_files"] for encode_item in encoding_list: @@ -140,7 +139,7 @@ def store_json_list_mp3_m4a_files(self, info_video, video_to_encode): source_file=self.get_true_path(mp3_files[audio_file]), ) - def store_json_list_mp4_hls_files(self, info_video, video_to_encode): + def store_json_list_mp4_hls_files(self, info_video, video_to_encode) -> None: mp4_files = info_video["list_mp4_files"] for video_file in mp4_files: if not check_file(mp4_files[video_file]): @@ -188,7 +187,7 @@ def store_json_list_mp4_hls_files(self, info_video, video_to_encode): source_file=playlist_file, ) - def store_json_encoding_log(self, info_video, video_to_encode): + def store_json_encoding_log(self, info_video, video_to_encode) -> None: # Need to modify start and stop log_to_text = "" # logs = info_video["encoding_log"] @@ -219,13 +218,10 @@ def store_json_encoding_log(self, info_video, video_to_encode): ) encoding_log.save() - def store_json_list_subtitle_files(self, info_video, video_to_encode): + def store_json_list_subtitle_files(self, info_video, video_to_encode) -> None: list_subtitle_files = info_video["list_subtitle_files"] if __FILEPICKER__: - videodir, created = UserFolder.objects.get_or_create( - name="%s" % video_to_encode.slug, - owner=video_to_encode.owner, - ) + videodir = video_to_encode.get_or_create_video_folder() for sub in list_subtitle_files: if not check_file(list_subtitle_files[sub][1]): @@ -259,16 +255,13 @@ def store_json_list_subtitle_files(self, info_video, video_to_encode): enrich_ready=True, ) - def store_json_list_thumbnail_files(self, info_video): + def store_json_list_thumbnail_files(self, info_video) -> Video: """store_json_list_thumbnail_files.""" video = Video.objects.get(id=self.id) list_thumbnail_files = info_video["list_thumbnail_files"] thumbnail = CustomImageModel() if __FILEPICKER__: - videodir, created = UserFolder.objects.get_or_create( - name="%s" % video.slug, - owner=video.owner, - ) + videodir = video.get_or_create_video_folder() thumbnail = CustomImageModel(folder=videodir, created_by=video.owner) for index, thumbnail_path in enumerate(list_thumbnail_files): if check_file(list_thumbnail_files[thumbnail_path]): @@ -298,7 +291,7 @@ def store_json_list_overview_files(self, info_video) -> Video: video.save() return video - def wait_for_file(self, filepath): + def wait_for_file(self, filepath) -> None: time_to_wait = 40 time_counter = 0 while not os.path.exists(filepath): @@ -367,7 +360,7 @@ def get_create_thumbnail_command_from_video(self, video_to_encode): encoding_log.save() return thumbnail_command - def recreate_thumbnail(self): + def recreate_thumbnail(self) -> None: self.create_output_dir() self.get_video_data() info_video = {} diff --git a/pod/video_encode_transcript/encoding_settings.py b/pod/video_encode_transcript/encoding_settings.py index d61ab663e5..2b32558e55 100644 --- a/pod/video_encode_transcript/encoding_settings.py +++ b/pod/video_encode_transcript/encoding_settings.py @@ -60,7 +60,7 @@ # FFMPEG_CREATE_THUMBNAIL = # '-map 0:%(index)s -vframes 1 -an -ss %(time)s -y "%(output)s" ' FFMPEG_CREATE_THUMBNAIL = ( - '-vf "fps=1/(%(duration)s/%(nb_thumbnail)s)" -vsync vfr "%(output)s_%%04d.png"' + '-vf "fps=1/(%(duration)s/%(nb_thumbnail)s)" -vsync vfr -y "%(output)s_%%04d.png"' ) FFMPEG_EXTRACT_SUBTITLE = '-map 0:%(index)s -f webvtt -y "%(output)s" ' @@ -69,7 +69,7 @@ "'fps=fps=(%(image_count)s/%(duration)s), " "scale=%(width)sx%(height)s, " "tile=%(image_count)sx1' " - "-frames:v 1 '%(output)s' " + "-frames:v 1 -y '%(output)s' " ) FFMPEG_DRESSING_OUTPUT = ' -c:v libx264 -y -vsync 0 "%(output)s" ' diff --git a/pod/video_encode_transcript/encoding_tasks.py b/pod/video_encode_transcript/encoding_tasks.py index ea532b0a5a..a9430d51ae 100644 --- a/pod/video_encode_transcript/encoding_tasks.py +++ b/pod/video_encode_transcript/encoding_tasks.py @@ -47,9 +47,9 @@ # celery -A pod.video_encode_transcript.encoding_tasks worker -l INFO -Q encoding -@encoding_app.task +@encoding_app.task(bind=True) def start_encoding_task( - video_id, video_path, cut_start, cut_end, json_dressing, dressing_input + self, video_id, video_path, cut_start, cut_end, json_dressing, dressing_input ): """Start the encoding of the video.""" print("Start the encoding of the video") @@ -73,22 +73,23 @@ def start_encoding_task( "json_dressing": json_dressing, "dressing_input": dressing_input, } + msg = "Task id : %s\n" % self.request.id try: response = requests.post(url, json=data, headers=Headers) if response.status_code != 200: - msg = "Calling store remote encoding error: {} {}".format( + msg += "Calling store remote encoding error: {} {}".format( response.status_code, response.reason ) logger.error(msg + "\n" + str(response.content)) else: - logger.info("Call importing encoded task ok") + logger.info(msg + "Call importing encoding task ok") except ( requests.exceptions.HTTPError, requests.exceptions.ConnectionError, requests.exceptions.InvalidURL, requests.exceptions.Timeout, ) as exception: - msg = "Exception: {}".format(type(exception).__name__) + msg += "Exception: {}".format(type(exception).__name__) msg += "\nException message: {}".format(exception) logger.error(msg) diff --git a/pod/video_encode_transcript/transcript.py b/pod/video_encode_transcript/transcript.py index 1cda78785e..f55bbb1ed0 100644 --- a/pod/video_encode_transcript/transcript.py +++ b/pod/video_encode_transcript/transcript.py @@ -38,7 +38,6 @@ if getattr(settings, "USE_PODFILE", False): __FILEPICKER__ = True from pod.podfile.models import CustomFileModel - from pod.podfile.models import UserFolder else: __FILEPICKER__ = False from pod.main.models import CustomFileModel @@ -160,9 +159,7 @@ def saveVTT(video: Video, webvtt: WebVTT, lang_code: str = None): improveCaptionsAccessibility(webvtt) msg += "\nstore vtt file in bdd with CustomFileModel model file field" if __FILEPICKER__: - videodir, created = UserFolder.objects.get_or_create( - name="%s" % video.slug, owner=video.owner - ) + videodir = video.get_or_create_video_folder() """ previousSubtitleFile = CustomFileModel.objects.filter( name__startswith="subtitle_%s" % lang, diff --git a/pod/video_encode_transcript/transcript_model.py b/pod/video_encode_transcript/transcript_model.py index cb48a84c21..51b1bf8797 100644 --- a/pod/video_encode_transcript/transcript_model.py +++ b/pod/video_encode_transcript/transcript_model.py @@ -418,7 +418,7 @@ def main_whisper_transcript(norm_mp3_file, duration, lang): "download_root" ], ) - + """ for start_trim in range(0, duration, TRANSCRIPTION_AUDIO_SPLIT_TIME): log.info("start_trim: " + str(start_trim)) audio = convert_samplerate( @@ -436,7 +436,16 @@ def main_whisper_transcript(norm_mp3_file, duration, lang): segment["text"], ) webvtt.captions.append(caption) - + """ + audio = convert_samplerate(norm_mp3_file, desired_sample_rate, 0, duration) + transcription = model.transcribe(audio, language=lang) + for segment in transcription["segments"]: + caption = Caption( + sec_to_timestamp(segment["start"]), + sec_to_timestamp(segment["end"]), + segment["text"], + ) + webvtt.captions.append(caption) inference_end = timer() - inference_start msg += "\nInference took %0.3fs." % inference_end return msg, webvtt, all_text diff --git a/pod/video_encode_transcript/transcripting_tasks.py b/pod/video_encode_transcript/transcripting_tasks.py index fefa0435e5..b059e50167 100644 --- a/pod/video_encode_transcript/transcripting_tasks.py +++ b/pod/video_encode_transcript/transcripting_tasks.py @@ -33,7 +33,7 @@ mailhost=EMAIL_HOST, fromaddr=DEFAULT_FROM_EMAIL, toaddrs=admins_email, - subject="[POD ENCODING] Encoding Log Mail", + subject="[POD TRANSCRIPT] Transcripting Log Mail", ) if not TEST_REMOTE_ENCODE: logger.addHandler(smtp_handler) @@ -57,8 +57,8 @@ # celery \ # -A pod.video_encode_transcript.transcripting_tasks worker \ # -l INFO -Q transcripting -@transcripting_app.task -def start_transcripting_task(video_id, mp3filepath, duration, lang): +@transcripting_app.task(bind=True) +def start_transcripting_task(self, video_id, mp3filepath, duration, lang): """Start the transcripting of the video.""" from .transcript_model import start_transcripting from ..main.settings import MEDIA_ROOT @@ -76,21 +76,22 @@ def start_transcripting_task(video_id, mp3filepath, duration, lang): Headers = {"Authorization": "Token %s" % POD_API_TOKEN} url = POD_API_URL.strip("/") + "/store_remote_transcripted_video/?id=%s" % video_id data = {"video_id": video_id, "msg": msg, "temp_vtt_file": temp_vtt_file.name} + msg = "Task id : %s\n" % self.request.id try: response = requests.post(url, json=data, headers=Headers) if response.status_code != 200: - msg = "Calling store remote transcoding error: {} {}".format( + msg += "Calling store remote transcoding error: {} {}".format( response.status_code, response.reason ) logger.error(msg + "\n" + str(response.content)) else: - logger.info("Call importing transcript task ok") + logger.info(msg + "Call importing transcript task ok") except ( requests.exceptions.HTTPError, requests.exceptions.ConnectionError, requests.exceptions.InvalidURL, requests.exceptions.Timeout, ) as exception: - msg = "Exception: {}".format(type(exception).__name__) + msg += "Exception: {}".format(type(exception).__name__) msg += "\nException message: {}".format(exception) logger.error(msg) diff --git a/pod/video_search/management/commands/create_pod_index.py b/pod/video_search/management/commands/create_pod_index.py index 405850e553..a30bd6fcc5 100644 --- a/pod/video_search/management/commands/create_pod_index.py +++ b/pod/video_search/management/commands/create_pod_index.py @@ -1,15 +1,12 @@ """create_pod_index management command.""" from django.core.management.base import BaseCommand -from django.conf import settings from pod.video_search.utils import create_index_es, delete_index_es - +from elasticsearch import exceptions import logging logger = logging.getLogger(__name__) -ES_URL = getattr(settings, "ES_URL", ["http://elasticsearch.localhost:9200/"]) - class Command(BaseCommand): """Command called by `python manage.py create_pod_index`.""" @@ -19,6 +16,12 @@ class Command(BaseCommand): def handle(self, *args, **options): """Create the Elasticsearch Pod index.""" - delete_index_es() + try: + delete_index_es() + self.stdout.write(self.style.WARNING("The Pod index has been deleted.")) + except exceptions.NotFoundError: + self.stdout.write( + self.style.WARNING("Pod index not found on ElasticSearch server.") + ) create_index_es() self.stdout.write(self.style.SUCCESS("Video index successfully created on ES.")) diff --git a/requirements.txt b/requirements.txt index 2c82f19d2a..b67ad8056b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ django-modeltranslation==0.18.7 django-cas-client==1.5.3 ldap3==2.9 django-simple-captcha==0.5.20 -urllib3==1.26.18 +urllib3==1.26.19 elasticsearch==6.3.1 djangorestframework==3.14.0 django-filter==22.1