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 '"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 '"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 "Fields marked with an asterisk are mandatory." %} +
+