diff --git a/pod/authentication/tests/test_populated.py b/pod/authentication/tests/test_populated.py index 996ee8f02d..86279388af 100644 --- a/pod/authentication/tests/test_populated.py +++ b/pod/authentication/tests/test_populated.py @@ -474,7 +474,7 @@ def _authenticate_shib_user(self, u): @override_settings(DEBUG=False) def test_make_profile(self): - """Test if user attributes are retreived""" + """Test if user attributes are retrieved.""" user, shib_meta = self._authenticate_shib_user( { "username": "jdo@univ.fr", diff --git a/pod/chapter/forms.py b/pod/chapter/forms.py index b76ea4120f..3b45d60c95 100644 --- a/pod/chapter/forms.py +++ b/pod/chapter/forms.py @@ -1,3 +1,4 @@ +"""Forms to create/edit and import Esup-Pod video chapter.""" from django import forms from django.conf import settings from django.core.exceptions import ValidationError @@ -5,7 +6,7 @@ from pod.chapter.models import Chapter from pod.chapter.utils import vtt_to_chapter -from pod.main.forms_utils import add_placeholder_and_asterisk +from pod.main.forms_utils import add_placeholder_and_asterisk, add_describedby_attr if getattr(settings, "USE_PODFILE", False): __FILEPICKER__ = True @@ -17,7 +18,10 @@ class ChapterForm(forms.ModelForm): + """A form to create/edit a video chapter.""" + def __init__(self, *args, **kwargs): + """Initialize fields.""" super(ChapterForm, self).__init__(*args, **kwargs) self.fields["video"].widget = forms.HiddenInput() self.fields["time_start"].widget.attrs["min"] = 0 @@ -28,16 +32,22 @@ def __init__(self, *args, **kwargs): except Exception: self.fields["time_start"].widget.attrs["max"] = 36000 self.fields = add_placeholder_and_asterisk(self.fields) + self.fields = add_describedby_attr(self.fields) class Meta: + """Form Metadata.""" + model = Chapter fields = "__all__" class ChapterImportForm(forms.Form): + """A form to import chapters from VTT file.""" + file = forms.ModelChoiceField(queryset=CustomFileModel.objects.all()) def __init__(self, *args, **kwargs): + """Initialize fields.""" self.user = kwargs.pop("user") self.video = kwargs.pop("video") super(ChapterImportForm, self).__init__(*args, **kwargs) @@ -48,10 +58,14 @@ def __init__(self, *args, **kwargs): ) else: self.fields["file"].queryset = CustomFileModel.objects.all() + # self.fields = add_placeholder_and_asterisk(self.fields) + self.fields = add_describedby_attr(self.fields) self.fields["file"].label = _("File to import") + self.fields["file"].help_text = _("The file must be in VTT format.") def clean_file(self): + """Convert VTT to chapters and return cleaned Data.""" msg = vtt_to_chapter(self.cleaned_data["file"], self.video) if msg: - raise ValidationError("Error ! {0}".format(msg)) + raise ValidationError("Error! {0}".format(msg)) return self.cleaned_data["file"] diff --git a/pod/chapter/models.py b/pod/chapter/models.py index 3151fa8e04..1643bc3779 100644 --- a/pod/chapter/models.py +++ b/pod/chapter/models.py @@ -60,6 +60,7 @@ def verify_title_items(self): return list() def verify_time(self): + """Check that start time is included inside video duration.""" msg = list() if ( self.time_start == "" diff --git a/pod/chapter/static/js/chapters.js b/pod/chapter/static/js/chapters.js index 906c81b0cc..1a881db90c 100644 --- a/pod/chapter/static/js/chapters.js +++ b/pod/chapter/static/js/chapters.js @@ -16,14 +16,23 @@ function show_form(data) { }); fadeIn(form_chapter); + var describedby_list = []; let inputStart = document.getElementById("id_time_start"); if (inputStart) { inputStart.insertAdjacentHTML( "beforebegin", " " + gettext("Get time from the player") + - " ", + " ", ); + + if(inputStart.getAttribute("aria-describedby")) { + describedby_list = inputStart.getAttribute("aria-describedby").split(" "); + } + if (describedby_list.indexOf("chapter_time_start") === -1){ + describedby_list.push("chapter_time_start"); + } + inputStart.setAttribute("aria-describedby", describedby_list.join(" ")); } let inputEnd = document.getElementById("id_time_end"); if (inputEnd) { @@ -31,8 +40,15 @@ function show_form(data) { "beforebegin", " " + gettext("Get time from the player") + - " ", + " ", ); + if(inputEnd.getAttribute("aria-describedby")) { + describedby_list = inputEnd.getAttribute("aria-describedby").split(" "); + } + if (describedby_list.indexOf("chapter_time_end") === -1){ + describedby_list.push("chapter_time_end"); + } + inputEnd.setAttribute("aria-describedby", describedby_list.join(" ")); } } @@ -208,53 +224,90 @@ var sendform = async function (elt, action) { /*** Verify if value of field respect form field ***/ function verify_start_title_items() { + var ret = true; + + // First, check Title field. + let inputTitle = document.getElementById("id_title"); + inputTitle.classList.remove("is-invalid"); + inputTitle.classList.remove("is-valid"); + var describedby_list = []; + if(inputTitle.getAttribute("aria-describedby")) { + describedby_list = inputTitle.getAttribute("aria-describedby").split(" "); + } + + var errormsg_id = "lengthErrorMsg"; if ( inputTitle.value === "" || inputTitle.value.length < 2 || inputTitle.value.length > 100 ) { - if (typeof lengthErrorSpan === "undefined") { - lengthErrorSpan = document.createElement("span"); - lengthErrorSpan.className = "form-help-inline"; - lengthErrorSpan.innerHTML = - "  " + - gettext("Please enter a title from 2 to 100 characters."); - inputTitle.insertAdjacentHTML("beforebegin", lengthErrorSpan.outerHTML); + if (typeof lengthErrorMsg === "undefined") { + lengthErrorMsg = document.createElement("div"); + lengthErrorMsg.id = errormsg_id; + lengthErrorMsg.className = "invalid-feedback"; + lengthErrorMsg.innerHTML = gettext("Please enter a title from 2 to 100 characters."); + inputTitle.insertAdjacentHTML("afterend", lengthErrorMsg.outerHTML); inputTitle.parentNode.parentNode .querySelectorAll("div.form-group") .forEach(function (elt) { elt.classList.add("has-error"); }); } - return false; + if (describedby_list.indexOf(errormsg_id) === -1){ + describedby_list.push(errormsg_id); + } + inputTitle.classList.add("is-invalid"); + ret = false; + } else { + describedby_list.pop(errormsg_id); + inputTitle.classList.add("is-valid"); } + inputTitle.setAttribute("aria-describedby", describedby_list.join(" ")); + + // Then check inputStart field. let inputStart = document.getElementById("id_time_start"); + inputStart.classList.remove("is-invalid"); + inputStart.classList.remove("is-valid"); + + errormsg_id = "timeErrorMsg"; + if(inputStart.getAttribute("aria-describedby")) { + describedby_list = inputStart.getAttribute("aria-describedby").split(" "); + } else { + describedby_list = []; + } if ( inputStart.value === "" || inputStart.value < 0 || inputStart.value >= video_duration ) { - if (typeof timeErrorSpan === "undefined") { - timeErrorSpan = document.createElement("span"); - timeErrorSpan.className = "form-help-inline"; - timeErrorSpan.innerHTML = - "  " + + if (typeof timeErrorMsg === "undefined") { + timeErrorMsg = document.createElement("div"); + timeErrorMsg.id = errormsg_id; + timeErrorMsg.className = "invalid-feedback"; + timeErrorMsg.innerHTML = gettext("Please enter a correct start field between 0 and") + - " " + - (video_duration - 1); - inputStart.insertAdjacentHTML("beforebegin", timeErrorSpan.outerHTML); + " " + (video_duration - 1); + inputStart.insertAdjacentHTML("afterend", timeErrorMsg.outerHTML); + inputStart.setAttribute("aria-describedby", errormsg_id); inputStart.parentNode.parentNode .querySelectorAll("div.form-group") .forEach(function (elt) { elt.classList.add("has-error"); }); } - return false; + inputStart.classList.add("is-invalid"); + if (describedby_list.indexOf(errormsg_id) === -1){ + describedby_list.push(errormsg_id); + } + ret = false; + } else { + inputStart.classList.add("is-valid"); + describedby_list.pop(errormsg_id); } - timeErrorSpan = undefined; - lengthErrorSpan = undefined; - return true; + inputStart.setAttribute("aria-describedby", describedby_list.join(" ")); + + return ret; } function overlaptest() { @@ -352,10 +405,10 @@ var updateDom = function (data) { var manageSave = function () { let player = window.videojs.players.podvideoplayer; - if (player.usingPlugin("videoJsChapters")) { + if (player.usingPlugin("podVideoJsChapters")) { player.main(); } else { - player.videoJsChapters(); + player.podVideoJsChapters(); } }; @@ -366,7 +419,7 @@ var manageDelete = function () { player.main(); } else { player.controlBar.chapters.dispose(); - player.videoJsChapters().dispose(); + player.podVideoJsChapters().dispose(); } }; @@ -374,17 +427,17 @@ var manageImport = function () { let player = window.videojs.players.podvideoplayer; let n = document.querySelector("div.chapters-list"); if (n != null) { - if (player.usingPlugin("videoJsChapters")) { + if (player.usingPlugin("podVideoJsChapters")) { player.main(); } else { - player.videoJsChapters(); + player.podVideoJsChapters(); } } else { if (typeof player.controlBar.chapters != "undefined") { player.controlBar.chapters.dispose(); } - if (player.usingPlugin("videoJsChapters")) { - player.videoJsChapters().dispose(); + if (player.usingPlugin("podVideoJsChapters")) { + player.podVideoJsChapters().dispose(); } } }; diff --git a/pod/chapter/static/js/videojs-chapters.js b/pod/chapter/static/js/videojs-chapters.js index 1b405bdb9b..490b5e4c88 100644 --- a/pod/chapter/static/js/videojs-chapters.js +++ b/pod/chapter/static/js/videojs-chapters.js @@ -8,8 +8,7 @@ } (function (window, videojs) { - var videoJsChapters, - defaults = { + var defaults = { ui: true, }; @@ -29,24 +28,24 @@ videojs.dom.addClass(span, "vjs-chapters-icon"); this.el().appendChild(span); } - }; - ChapterMenuButton.prototype.handleClick = function (event) { - MenuButton.prototype.handleClick.call(this, event); - if (document.querySelectorAll(".chapters-list.inactive li").length > 0) { - document - .querySelector(".chapters-list.inactive") - .setAttribute("class", "chapters-list active"); - document.querySelector(".vjs-chapters-button button").style = - "text-shadow: 0 0 1em #fff"; - } else { - document - .querySelector(".chapters-list.active") - .setAttribute("class", "chapters-list inactive"); - - document.querySelector(".vjs-chapters-button button").style = - "text-shadow: '' "; + handleClick(event) { + MenuButton.prototype.handleClick.call(this, event); + if (document.querySelectorAll(".chapters-list.inactive li").length > 0) { + document + .querySelector(".chapters-list.inactive") + .setAttribute("class", "chapters-list active"); + document.querySelector(".vjs-chapters-button button").style = + "text-shadow: 0 0 1em #fff"; + } else { + document + .querySelector(".chapters-list.active") + .setAttribute("class", "chapters-list inactive"); + + document.querySelector(".vjs-chapters-button button").style = + "text-shadow: '' "; + } } - }; + } MenuButton.registerComponent("ChapterMenuButton", ChapterMenuButton); /** diff --git a/pod/chapter/templates/chapter/form_chapter.html b/pod/chapter/templates/chapter/form_chapter.html index 978c25f37e..a4027ecd4d 100644 --- a/pod/chapter/templates/chapter/form_chapter.html +++ b/pod/chapter/templates/chapter/form_chapter.html @@ -1,75 +1,80 @@ {# HTML for chapter form. Don't use this file alone it must be integrated into another template! #} {% load i18n %} {% load static %} -
-
-
-

{% trans 'Create / Edit chapters' %}

-
-
- {% csrf_token %} -
- {% if form_chapter.errors or form_chapter.non_field_errors %} - {% trans 'One or more errors have been found in the form:' %}
- {% for error in form_chapter.non_field_errors %} - - {{error}}
- {% endfor %} - {% endif %} - {% for field_hidden in form_chapter.hidden_fields %} - {{field_hidden}} - {% endfor %} - {% for field in form_chapter.visible_fields %} -
- -
{{field}}
-
- {% endfor %} - {% if form_chapter.instance %} - - {% endif %} - -
- - -
-
-
-   - {% if request.user.is_staff %} -
-

{% trans 'Import chapters' %}

-
-
- {% csrf_token %} -
- {% if form_import.errors or form_import.non_field_errors %} - {% trans 'One or more errors have been found in the form:' %}
- {% for error in form_import.non_field_errors %} - - {{error}}
- {% endfor %} - {% endif %} - {% for field_hidden in form_import.hidden_fields %} - {{field_hidden}} - {% endfor %} - {% for field in form_import.visible_fields %} -
- -
{{field}}
-
- {% for error in field.errors %} -
- {{error|escape}} -
- {% endfor %} - {% endfor %} - - {% trans 'The file must be in VTT format.' %} -
- -
-
-
- {{form_import.media}} - {% endif %} -
-
+
+ {% trans 'Create / Edit chapters' %} +
+ {% csrf_token %} +
+ {% if form_chapter.errors or form_chapter.non_field_errors %} + {% trans 'One or more errors have been found in the form:' %}
+ {% for error in form_chapter.non_field_errors %} + - {{error}}
+ {% endfor %} + {% endif %} + {% for field_hidden in form_chapter.hidden_fields %} + {{field_hidden}} + {% endfor %} + {% for field in form_chapter.visible_fields %} +
+ +
+ {{field}} + {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+
+ {% endfor %} + {% if form_chapter.instance %} + + {% endif %} + +
+ + +
+
+
+
+ +{% if request.user.is_staff and form_import.visible_fields %} +
+ {% trans 'Import chapters' %} +
+ {% csrf_token %} +
+ {% if form_import.errors or form_import.non_field_errors %} + {% trans 'One or more errors have been found in the form:' %}
+ {% for error in form_import.non_field_errors %} + - {{error}}
+ {% endfor %} + {% endif %} + {% for field_hidden in form_import.hidden_fields %} + {{field_hidden}} + {% endfor %} + {% for field in form_import.visible_fields %} +
+ +
+ {{field}} + {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+
+ {% for error in field.errors %} +
+ {{error|escape}} +
+ {% endfor %} + {% endfor %} + +
+ +
+
+
+ {{form_import.media}} +
+{% endif %} diff --git a/pod/chapter/templates/video_chapter.html b/pod/chapter/templates/video_chapter.html index 23ffad5e19..55afdb495e 100644 --- a/pod/chapter/templates/video_chapter.html +++ b/pod/chapter/templates/video_chapter.html @@ -4,77 +4,85 @@ {% load static %} {% block page_title %}{% trans 'Chapter video' %} "{{video.title}}" {% endblock page_title %} {% block page_extra_head %} - -{% include 'videos/video-header.html' %} - - + {% include 'videos/video-header.html' %} + + {% endblock page_extra_head %} {% block breadcrumbs %} -{{block.super}} - - - -{% endblock %} + {{block.super}} + + + +{% endblock breadcrumbs %} {% block page_content %} - - {% include 'videos/video-element.html' %} - -
-
-
-
- {% include 'chapter/list_chapter.html' %} -
-
- {% if form_chapter %} - {% include 'chapter/form_chapter.html' with form_chapter=form_chapter %} + + {% include 'videos/video-element.html' %} + +
+
+
+
+ {% include 'chapter/list_chapter.html' %} +
+
+ {% if form_chapter %} + {% include 'chapter/form_chapter.html' with form_chapter=form_chapter %} + {% endif %} +
+ + +  {% trans "Back to the video"%} + + + {% if not form_chapter %} +
+ {% csrf_token %} + + +
{% endif %}
- - -  {% trans "Back to the video"%} - - - {% if not form_chapter %} -
- {% csrf_token %} - - -
- {% endif %}
-
{% endblock page_content %} {% block page_aside %} -{% if video.owner == request.user or request.user.is_superuser or perms.chapter.add_chapter or request.user in video.additional_owners.all %} -
-

 {% trans "Manage video"%}

-
- {% include "videos/link_video.html" with hide_favorite_link=True %} + {% if video.owner == request.user or request.user.is_superuser or perms.chapter.add_chapter or request.user in video.additional_owners.all %} +
+

 {% trans "Manage video"%}

+
+ {% include "videos/link_video.html" with hide_favorite_link=True %} +
+
+ {% endif %} +
+

{% trans "Help"%}

+ +
+

{% trans '"Add a new chapter" allows you to add a new chapter, "modify" allows you to modify it and "delete" allows you to remove the chapter.' %}

+

{% trans 'Start playback of the video, pause the video and click on "Get time from the player" to fill in the field untitled "Start time".' %}

+

{% trans 'The chapters cannot start at the same time.' %}

+

{% trans 'You must save your chapters to view the result.' %}

+
-
-{% endif %} -
-

{% trans "Help"%}

- -
-

{% trans '"Add a new chapter" allows you to add a new chapter, "modify" allows you to modify it and "delete" allows you to remove the chapter.' %}

-

{% trans 'Start playback of the video, pause the video and click on "Get time from the player" to fill in the field untitled "Start time".' %}

-

{% trans 'The chapters cannot start at the same time.' %}

-

{% trans 'You must save your chapters to view the result.' %}

+
+

{% trans "Mandatory fields" %}

+
+

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

+
-
{% endblock page_aside %} {% block more_script %} -{% include 'videos/video-script.html'%} -{% endblock more_script %} \ No newline at end of file + {% include 'videos/video-script.html'%} +{% endblock more_script %} diff --git a/pod/chapter/views.py b/pod/chapter/views.py index 0f5a417834..a67010ab32 100644 --- a/pod/chapter/views.py +++ b/pod/chapter/views.py @@ -54,6 +54,7 @@ def video_chapter(request, slug): def video_chapter_new(request, video): + """Display a new video chapter form.""" list_chapter = video.chapter_set.all() form_chapter = ChapterForm(initial={"video": video}) form_import = ChapterImportForm(user=request.user, video=video) @@ -81,6 +82,7 @@ def video_chapter_new(request, video): def video_chapter_save(request, video): + """Save a video chapter form request.""" list_chapter = video.chapter_set.all() form_chapter = None @@ -149,6 +151,7 @@ def video_chapter_save(request, video): def video_chapter_modify(request, video): + """Display a video chapter modification form.""" list_chapter = video.chapter_set.all() if request.POST.get("action", "").lower() == "modify": chapter = get_object_or_404(Chapter, id=request.POST.get("id")) diff --git a/pod/enrichment/templates/enrichment/form_enrichment.html b/pod/enrichment/templates/enrichment/form_enrichment.html index 40f7a36202..eb687b32b3 100644 --- a/pod/enrichment/templates/enrichment/form_enrichment.html +++ b/pod/enrichment/templates/enrichment/form_enrichment.html @@ -7,7 +7,7 @@

{% trans 'Create / Edit enrichment' %}
{% csrf_token %} -
+
{% if form_enrichment.errors or form_enrichment.non_field_errors %}
{% trans 'One or more errors have been found in the form:' %} diff --git a/pod/enrichment/templates/enrichment/video_enrichment-script.html b/pod/enrichment/templates/enrichment/video_enrichment-script.html index 48916639cb..6b5dd73027 100644 --- a/pod/enrichment/templates/enrichment/video_enrichment-script.html +++ b/pod/enrichment/templates/enrichment/video_enrichment-script.html @@ -6,8 +6,10 @@ const tracks = player.textTracks(); const trackElts = player.remoteTextTrackEls(); let metadataTrack, i, track; + for (i = 0; i < tracks.length; i++) { track = tracks[i]; + // console.log("track: ["+track.kind + "] " + track.label); if (track.kind === 'metadata' && track.label === 'enrichment') { metadataTrack = track; metadataTrack.index = i; @@ -17,6 +19,10 @@ } player.on('loadedmetadata', function() { + if(!metadataTrack){ + console.log("No enrichment track found.") + return + } let slide = []; if(!metadataTrack.cues) { //Safari do not get cues //let tracksrc = player.el().getElementsByTagName('TRACK')[metadataTrack.index].src; diff --git a/pod/enrichment/templates/enrichment/video_enrichment.html b/pod/enrichment/templates/enrichment/video_enrichment.html index 82b12fe2e0..2b9d6f84b8 100644 --- a/pod/enrichment/templates/enrichment/video_enrichment.html +++ b/pod/enrichment/templates/enrichment/video_enrichment.html @@ -56,21 +56,21 @@ {% block page_title %}{% if channel %}{{channel.title}} - {% endif %}{% if theme %}{{theme.title}} - {% endif %}({% trans 'Enriched' %}) {{video.title}}{% endblock %} -{% block video-element %} - -{% if form %} - {% include 'videos/video-form.html' %} -{% else %} - {% include 'enrichment/video-element-enrichment.html' %} -
{% include 'videos/video-all-info.html' with third_app=True %}
-{% endif %} - -{% endblock video-element %} +{% block page_content %} + {% block video-element %} + {% if form %} + {% include 'videos/video-form.html' %} + {% else %} + {% include 'enrichment/video-element-enrichment.html' %} + {% endif %} +
{% include 'videos/video-all-info.html' with third_app=True %}
+ {% endblock video-element %} +{% endblock page_content %} {% block page_aside %}

-  {% trans 'Informations' %} +  {% trans 'Informations' %}

{% trans 'To help you, the different types of enrichments have specific colors:' %}

diff --git a/pod/package.json b/pod/package.json index 5a644dc77d..d5e288bdb9 100644 --- a/pod/package.json +++ b/pod/package.json @@ -1,7 +1,7 @@ { "license": "LGPL-3.0", "dependencies": { - "@silvermine/videojs-quality-selector": "^1.2.5", + "@silvermine/videojs-quality-selector": "^1.3.0", "blueimp-file-upload": "^10.32.0", "bootstrap": "^5.3.0", "bootstrap-icons": "^1.10.5", diff --git a/pod/podfile/templates/podfile/customfilewidget.html b/pod/podfile/templates/podfile/customfilewidget.html index 2f8ec9d760..24108c3dec 100644 --- a/pod/podfile/templates/podfile/customfilewidget.html +++ b/pod/podfile/templates/podfile/customfilewidget.html @@ -36,6 +36,11 @@ {% else %} +