diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index b4e6c32a5a9709c5753341ea3275d64b8ab82c63..ba15e0fca4ed10be2b4a3e37895dbd78866c4527 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -56,12 +56,15 @@ from app.scodoc.gen_tables import GenTable from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied from app.scodoc.sco_permissions import Permission -JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ +JAVASCRIPTS = html_sco_header.BOOTSTRAP_JS + [ "js/etud_info.js", "js/groups_view.js", + "js/multi-select.js", ] -CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS +CSSSTYLES = html_sco_header.BOOTSTRAP_CSS + [ + "libjs/bootstrap/css/bootstrap.min.css", +] # view: @@ -115,13 +118,10 @@ def groups_view( return f""" { html_sco_header.sco_header( javascripts=JAVASCRIPTS, - cssstyles=CSSSTYLES, + cssstyles=CSSSTYLES ) } <style> - div.multiselect-container.dropdown-menu {{ - min-width: 180px; - }} span.warning_unauthorized {{ color: pink; font-style: italic; @@ -221,47 +221,70 @@ def form_groups_choice( def menu_groups_choice( - groups_infos, submit_on_change=False, default_deselect_others=True + groups_infos, + submit_on_change=False, + default_deselect_others=True, + html_export=True, + change_event=None, ): """menu pour selection groupes group_ids est la liste des groupes actuellement sélectionnés et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. (utilisé pour retrouver le semestre et proposer la liste des autres groupes) + + Si html_export : + selecteur.value = &group_ids=xxx&group_ids=yyy... + sinon : + selecteur.value = [xxx, yyy, ...] + + Si change_event : + met à jour l'événement onchange du selecteur + (attend du js, plus d'informations sur scu.MultiSelect.change_event) + """ default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) n_members = len(sco_groups.get_group_members(default_group_id)) - H = [ - f"""<select name="group_ids" id="group_ids_sel" - class="multiselect - {'submit_on_change' if submit_on_change else ''} - {'default_deselect_others' if default_deselect_others else ''} - " - multiple="multiple"> - <option class="default_group" - value="{default_group_id}" - {'selected' if default_group_id in groups_infos.group_ids else ''} - >Tous ({n_members})</option> - """ - ] + values: dict = { + # Choix : Tous (tous les groupes) + "": [ + { + "value": default_group_id, + "label": f"Tous ({n_members})", + "selected": default_group_id in groups_infos.group_ids, + "single": default_deselect_others, + } + ] + } for partition in groups_infos.partitions: - H.append('<optgroup label="%s">' % partition["partition_name"]) + p_name: str = partition["partition_name"] + vals: list[tuple[str, str, bool]] = [] # Les groupes dans cette partition: - for g in sco_groups.get_partition_groups(partition): - if g["group_id"] in groups_infos.group_ids: - selected = "selected" - else: - selected = "" - if g["group_name"]: - n_members = len(sco_groups.get_group_members(g["group_id"])) - H.append( - '<option value="%s" %s>%s (%s)</option>' - % (g["group_id"], selected, g["group_name"], n_members) + for grp in sco_groups.get_partition_groups(partition): + selected: bool = grp["group_id"] in groups_infos.group_ids + if grp["group_name"]: + vals.append( + { + "value": grp["group_id"], + "label": f"{grp['group_name']} ({len(sco_groups.get_group_members(grp['group_id']))})", + "selected": selected, + } ) - H.append("</optgroup>") - H.append("</select> ") - return "\n".join(H) + + values[p_name] = vals + + multi_select: scu.MultiSelect = scu.MultiSelect( + values=values, name="group_ids", html_id="group_ids_sel" + ) + + if html_export: + multi_select.export_format('return "&group_ids="+values.join("&group_ids=")') + + if submit_on_change: + multi_select.change_event("submit_group_selector();") + + return multi_select.html() def menu_group_choice(group_id=None, formsemestre_id=None): @@ -698,7 +721,6 @@ def groups_table( """ ] if groups_infos.members: - menu_options = [] options = { "with_codes": "Affiche codes", } @@ -711,34 +733,33 @@ def groups_table( "with_bourse": "Statut boursier", } ) + valeurs: list[tuple[str, str]] = [] for option, label in options.items(): - if locals().get(option, False): - selected = "selected" - else: - selected = "" - menu_options.append( - f"""<option value="{option}" {selected}>{label}</option>""" + selected = locals().get(option, False) + valeurs.append( + { + "value": option, + "label": label, + "selected": selected, + } ) + multi_select: scu.MultiSelect = scu.MultiSelect( + values={"": valeurs}, + label="Options", + name="options", + html_id="group_list_options", + ) + multi_select.change_event("change_list_options(values)") H.extend( + # ; [ - """<span style="margin-left: 2em;"> - <select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""", - "\n".join(menu_options), - """</select></span> - <script type="text/javascript"> - $(document).ready(function() { - $('#group_list_options').multiselect( - { - includeSelectAllOption: false, - nonSelectedText:'Options...', - onChange: function(element, checked){ - change_list_options(); - } - } - ); - }); - </script> + f""" + <span style="margin-left: 2em;"> + + {multi_select.html()} + + </span> """, ( """<span class="warning_unauthorized">accès aux données personnelles interdit</span>""" @@ -929,11 +950,14 @@ def tab_absences_html(groups_infos, etat=None): """ ] - url_feuille_appel: str = url_for( - "scolar.formulaire_feuille_appel", - scodoc_dept=g.scodoc_dept, - formsemestre_id=groups_infos.formsemestre_id, - group_ids=group_ids, + url_feuille_appel: str = ( + url_for( + "scolar.formulaire_feuille_appel", + scodoc_dept=g.scodoc_dept, + formsemestre_id=groups_infos.formsemestre_id, + ) + + "&" + + groups_infos.groups_query_args ) H.extend( diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 29bccd68af4f0704a87f07fef591cfdf6a1b735d..a97834cb0ec2032daf134f90d3d9e1a0e05770ba 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -26,8 +26,8 @@ ############################################################################## -""" Common definitions -""" +"""Common definitions""" + import base64 import bisect import collections @@ -445,6 +445,121 @@ def translate_assiduites_metric(metric, inverse=True, short=True) -> str: return None +class MultiSelect: + """ + Classe pour faciliter l'utilisation du multi-select HTML/JS + + Les values sont représentées en dict { + value: "...", + label:"...", + selected: True/False (default to False), + single: True/False (default to False) + } + + Args: + values (dict[str, list[dict]]): Dictionnaire des valeurs + génère des <optgroup> pour chaque clef du dictionnaire + génère des <option> pour chaque valeur du dictionnaire + name (str, optional): Nom du multi-select. Defaults to "multi-select". + html_id (str, optional): Id HTML du multi-select. Defaults to "multi-select". + classname (str, optional): Classe CSS du multi-select. Defaults to "". + label (str, optional): Label du multi-select. Defaults to "". + export (str, optional): Format du multi-select (HTML/JS). Defaults to "js". + HTML : group_ids="val1"&group_ids="val2"... + JS : ["val1","val2", ...] + + **kwargs: Arguments supplémentaires (appliqué au multiselect en HTML <multi-select key="value" ...>) + """ + + def __init__( + self, + values: dict[str, list[dict]], + name="multi-select", + html_id="multi-select", + label="", + classname="", + **kwargs, + ) -> None: + self.values: dict[str, list[dict]] = values + self._on = "" + + self.name: str = name + self.html_id: str = html_id + self.classname: str = classname + self.label: str = label or name + + self.args: dict = kwargs + self.js: str = "" + self.export: str = "return values" + + def html(self) -> str: + """ + Génère l'HTML correspondant au multi-select + """ + opts: list[str] = [] + + for key, values in self.values.items(): + optgroup = f"<optgroup label='{key}'>" + for value in values: + selected = "selected" if value.get("selected", False) else "" + single = "single" if value.get("single", False) else "" + opt = f"<option value='{value.get('value')}' {selected} {single} >{value.get('label')}</option>" + optgroup += opt + optgroup += "</optgroup>" + opts.append(optgroup) + + args: list[str] = [f'{key}="{value}"' for key, value in self.args.items()] + js: str = "{" + self.js + "}" + export: str = "{" + self.export + "}" + return f""" + <multi-select + label="{self.label}" + id="{self.html_id}" + name="{self.name}" + class="{self.classname}" + {" ".join(args)} + > + {"".join(opts)} + </multi-select> + <script> + window.addEventListener('load', () => {{document.getElementById("{self.html_id}").on((values)=>{js}); + document.getElementById("{self.html_id}").format((values)=>{export});}} ); + </script> + """ + + def change_event(self, js: str) -> None: + """ + Met à jour l'évènement de changement de valeur du multi-select + + CallBack JS : (values) => {/*actions à effectuer*/} + + Sera retranscrit dans l'HTML comme : + + document.getElementById(%self.id%).on((values)=>{%self.js%}) + + Exemple d'utilisation : + + js : "console.log(values)" + """ + self.js: str = js + + def export_format(self, js: str) -> None: + """ + Met à jour le format de retour de valeur du multi-select + + CallBack JS : (values) => {/*actions à effectuer*/} + + Sera retranscrit dans l'HTML comme : + + document.getElementById(%self.id%).format((values)=>{%self.js%}) + + Exemple d'utilisation : + + js : "return values.map(v=> 'val:'+v)" + """ + self.export: str = js + + # Types de modules class ModuleType(IntEnum): """Code des types de module.""" diff --git a/app/static/js/groups_view.js b/app/static/js/groups_view.js index 7be2509fa0bb9fcdcd774985d7e834c93573d250..29517a325d7b57ba8dc6be2ada0b7fdc163e87f9 100644 --- a/app/static/js/groups_view.js +++ b/app/static/js/groups_view.js @@ -13,72 +13,41 @@ $().ready(function () { // (ne fonctionne que pour les requetes GET: manipule la query string) function groups_view_url() { - var url = $.url(); - delete url.param()["group_ids"]; // retire anciens groupes de l'URL - delete url.param()["curtab"]; // retire ancien tab actif - if (CURRENT_TAB_HASH) { - url.param()["curtab"] = CURRENT_TAB_HASH; - } - delete url.param()["formsemestre_id"]; - url.param()["formsemestre_id"] = - $("#group_selector")[0].formsemestre_id.value; - - var selected_groups = $("#group_selector select#group_ids_sel").val(); - url.param()["group_ids"] = selected_groups; // remplace par groupes selectionnes - - return url; -} - -// Sélectionne le groupe "tous" et recharge la page: -function select_groupe_tous() { - var url = groups_view_url(); - var default_group_id = $("#group_selector")[0].default_group_id.value; - delete url.param()["group_ids"]; - url.param()["group_ids"] = [default_group_id]; - - var query_string = $.param(url.param(), (traditional = true)); - window.location = url.attr("base") + url.attr("path") + "?" + query_string; + let url = new URL(location.href); + let urlParams = url.searchParams; + // ajout du formsemestre + urlParams.set( + "formsemestre_id", + $("#group_selector")[0].formsemestre_id.value + ); + // ajout du tab actif + const tabActif = document.querySelector( + '[role="tab"][aria-selected="true"]' + ).id; + urlParams.set("tab", tabActif); + urlParams.delete("group_ids"); + // ajout des groupes selectionnes + var selected_groups = document.getElementById("group_ids_sel").value; + url.search = urlParams.toString() + selected_groups; + return url.href; } // Recharge la page sans arguments group_ids function remove_group_filter() { - var url = groups_view_url(); - delete url.param()["group_ids"]; - var query_string = $.param(url.param(), (traditional = true)); - window.location = url.attr("base") + url.attr("path") + "?" + query_string; -} + var url = new URL(location.href); + url.searchParams.delete("group_ids"); -// L'URL pour l'état courant de la page: -function get_current_url() { - var url = groups_view_url(); - var query_string = $.param(url.param(), (traditional = true)); - return url.attr("base") + url.attr("path") + "?" + query_string; + window.location = url.href; } // Recharge la page en changeant les groupes selectionnés et en conservant le tab actif: function submit_group_selector() { - window.location = get_current_url(); + window.location = groups_view_url(); } -function show_current_tab() { - if (document.getElementsByClassName("nav-tabs").length > 0) { - $('.nav-tabs [href="#' + CURRENT_TAB_HASH + '"]').tab("show"); - } -} - -var CURRENT_TAB_HASH = $.url().param()["curtab"]; - -$().ready(function () { - $(".nav-tabs a").on("shown.bs.tab", function (e) { - CURRENT_TAB_HASH = e.target.hash.slice(1); // sans le # - }); - - show_current_tab(); -}); - -function change_list_options() { - var url = groups_view_url(); - var selected_options = $("#group_list_options").val(); +function change_list_options(selected_options) { + var url = new URL(groups_view_url()); + var urlParams = url.searchParams; var options = [ "with_paiement", "with_archives", @@ -88,13 +57,11 @@ function change_list_options() { ]; for (var i = 0; i < options.length; i++) { var option = options[i]; - delete url.param()[option]; if ($.inArray(option, selected_options) >= 0) { - url.param()[option] = 1; + urlParams.set(option, "1"); } } - var query_string = $.param(url.param(), (traditional = true)); - window.location = url.attr("base") + url.attr("path") + "?" + query_string; + window.location = url.href; } // Menu choix groupe: @@ -134,57 +101,6 @@ function toggle_visible_etuds() { .forEach((elem) => (elem.value = group_ids_str)); } -$().ready(function () { - $("#group_ids_sel").multiselect({ - includeSelectAllOption: false, - nonSelectedText: "choisir...", - // buttonContainer: '<div id="group_ids_sel_container"/>', - onChange: function (element, checked) { - // Gestion du groupe "tous" - if ( - checked == true && - $("#group_ids_sel").hasClass("default_deselect_others") - ) { - var default_group_id = $(".default_group")[0].value; - - if (element.hasClass("default_group")) { - // click sur groupe "tous" - // deselectionne les autres - $("#group_ids_sel option:selected").each(function (index, opt) { - if (opt.value != default_group_id) { - $("#group_ids_sel").multiselect("deselect", opt.value); - } - }); - } else { - // click sur un autre item - // si le groupe "tous" est selectionne et que l'on coche un autre, le deselectionner - var default_is_selected = false; - $("#group_ids_sel option:selected").each(function (index, opt) { - if (opt.value == default_group_id) { - default_is_selected = true; - return false; - } - }); - if (default_is_selected) { - $("#group_ids_sel").multiselect("deselect", default_group_id); - } - } - } - - toggle_visible_etuds(); - // referme le menu apres chaque choix: - $("#group_selector .btn-group").removeClass("open"); - - if ($("#group_ids_sel").hasClass("submit_on_change")) { - submit_group_selector(); - } - }, - }); - - // initial setup - toggle_visible_etuds(); -}); - // Trombinoscope $().ready(function () { var elems = $(".trombi-photo"); @@ -211,3 +127,11 @@ $().ready(function () { }); } }); + +// gestion du tab actif +$().ready(function () { + let tab = new URL(location.href).searchParams.get("tab"); + if (tab) { + document.getElementById(tab)?.click(); + } +}); diff --git a/app/static/js/multi-select.js b/app/static/js/multi-select.js new file mode 100644 index 0000000000000000000000000000000000000000..e10194ad24cfe753ae7252b90f6a471827d04d46 --- /dev/null +++ b/app/static/js/multi-select.js @@ -0,0 +1,303 @@ +/* <== définition Multi-Select ==> */ + +/** + * Permet d'afficher un sélecteur multiple d'options. + * Pour chaque option cela affichera un checkbox. + * Les options peuvent être regroupées dans des optgroup. + * + * + * Utilisation : + * <multi-select> + <optgroup label="Groupe A"> + <option value="val1">Option 1</option> + <option value="val2">Option 2</option> + </optgroup> + <optgroup label="Groupe B"> + <option value="valB1">Option B1</option> + <option value="valB2">Option B2</option> + </optgroup> + </multi-select> + + <multi-select>.values() => ["val1",...] + <multi-select>.values(["val1",...]) => // sélectionne les options correspondantes (ne vérifie pas les options "single") + <multi-select>.on("change", (values) => {}) => // écoute le changement de valeur + */ + +class MultiSelect extends HTMLElement { + static formAssociated = true; + + get form() { + return this._internals.form; + } + get name() { + return this.getAttribute("name"); + } + + get label() { + return this.getAttribute("label"); + } + + set label(value) { + this.setAttribute("label", value); + } + + get type() { + return this.localName; + } + + constructor() { + super(); + this.attachShadow({ mode: "open" }); + + // HTML/CSS du composant + this.shadowRoot.innerHTML = ` + <style> + *{ + box-sizing: border-box; + } + + .dropdown { + position: relative; + display: inline-block; + border-radius: 10px; + user-select: none; + } + .dropdown-button { + // padding: 10px; + // background-color: #f1f1f1; + // border: 1px solid #ccc; + cursor: pointer; + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + .dropdown-content { + display: none; + position: absolute; + background-color: #fff; + min-width: 200px; + max-height: 200px; + overflow-y: auto; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; + } + .dropdown-content .optgroup { + padding: 10px; + width: 100%; + } + .dropdown-content .optgroup div { + font-weight: bold; + } + .dropdown-content .option { + display: flex; + align-items: center; + } + .dropdown-content .option input[type="checkbox"] { + margin-right: 0.5em; + } + label.selected{ + background-color: #C2DBFB; + } + label{ + cursor: pointer; + transition: all 0.3s; + } + label:hover{ + background-color: #f1f1f1; + } + </style> + <div class="dropdown"> + <button class="dropdown-button">Select options</button> + <div class="dropdown-content multi-select-container"></div> + </div> + `; + + this.exportFormat = null; + this.observer = new MutationObserver(() => this.render()); + + this.toggleDropdown = this.toggleDropdown.bind(this); + this.handleDocumentClick = this.handleDocumentClick.bind(this); + + this._internals = this.attachInternals(); + this._internals.setFormValue([]); + } + + connectedCallback() { + this.render(); + this.observer.observe(this, { childList: true, subtree: true }); + const btn = this.shadowRoot.querySelector(".dropdown-button"); + btn.addEventListener("click", this.toggleDropdown); + document.addEventListener("click", this.handleDocumentClick); + + this._updateSelect(); + } + + disconnectedCallback() { + this.observer.disconnect(); + document.removeEventListener("click", this.handleDocumentClick); + } + + toggleDropdown(event) { + event.stopPropagation(); + const dropdownContent = this.shadowRoot.querySelector(".dropdown-content"); + dropdownContent.style.display = + dropdownContent.style.display === "block" ? "none" : "block"; + } + + handleDocumentClick(event) { + if (!this.contains(event.target)) { + this.shadowRoot.querySelector(".dropdown-content").style.display = "none"; + } + } + + render() { + const container = this.shadowRoot.querySelector(".multi-select-container"); + container.innerHTML = ""; + + const optgroups = this.querySelectorAll("optgroup"); + + optgroups.forEach((optgroup) => { + const groupDiv = document.createElement("div"); + groupDiv.className = "optgroup"; + + const groupLabel = document.createElement("div"); + groupLabel.textContent = optgroup.label; + groupDiv.appendChild(groupLabel); + + const options = optgroup.querySelectorAll("option"); + options.forEach((option) => { + const optionDiv = document.createElement("label"); + optionDiv.className = "option"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.value = option.value; + checkbox.name = this.getAttribute("name"); + if (option.hasAttribute("selected")) { + checkbox.checked = true; + optionDiv.classList.add("selected"); + } + checkbox.addEventListener("change", () => { + this.handleCheckboxChange(checkbox); + }); + + optionDiv.appendChild(checkbox); + optionDiv.appendChild(document.createTextNode(option.textContent)); + groupDiv.appendChild(optionDiv); + }); + + container.appendChild(groupDiv); + }); + + this._updateSelect(); + } + + handleCheckboxChange(checkbox) { + const opt = this.querySelector(`option[value="${checkbox.value}"]`); + const isSingle = opt.hasAttribute("single"); + if (!checkbox.checked) { + checkbox.parentElement.classList.remove("selected"); + } else { + checkbox.parentElement.classList.add("selected"); + // Gestion de l'option "single" + if (isSingle) { + // Uncheck all other checkboxes + const checkboxes = this.shadowRoot.querySelectorAll( + 'input[type="checkbox"]' + ); + checkboxes.forEach((cb) => { + if (cb !== checkbox) { + cb.checked = false; + cb.parentElement.classList.remove("selected"); + } + }); + } else { + // Uncheck the single checkbox if present + const singleCheckbox = Array.from( + this.shadowRoot.querySelectorAll('input[type="checkbox"]') + ).find((cb) => + this.querySelector(`option[value="${cb.value}"]`).hasAttribute( + "single" + ) + ); + if (singleCheckbox) { + singleCheckbox.checked = false; + singleCheckbox.parentElement.classList.remove("selected"); + } + } + } + this._updateSelect(); + } + + _updateSelect() { + const checkboxes = this.shadowRoot.querySelectorAll( + 'input[type="checkbox"]' + ); + const checkedBoxes = Array.from(checkboxes).filter( + (checkbox) => checkbox.checked + ); + + const values = checkedBoxes.map((checkbox) => checkbox.value); + + const opts = checkedBoxes.map((checkbox) => { + return this.querySelector(`option[value="${checkbox.value}"]`); + }); + + const btn = this.shadowRoot.querySelector(".dropdown-button"); + + if (checkedBoxes.length === 0) { + btn.textContent = this.label || "Select options"; + } else if (checkedBoxes.length < 4) { + btn.textContent = opts.map((opt) => opt.textContent).join(", ") + ""; + } else { + btn.textContent = `${checkedBoxes.length} sélections`; + } + + btn.textContent += " ⮛"; + + this._values(values); + + this.dispatchEvent(new Event("change")); + } + + _values(newValues = null) { + const checkboxes = this.shadowRoot.querySelectorAll( + 'input[type="checkbox"]' + ); + if (newValues === null) { + // Get selected values + const values = Array.from(checkboxes) + .filter((checkbox) => checkbox.checked) + .map((checkbox) => checkbox.value); + if (this.exportFormat) { + return this.exportFormat(values); + } + return values; + } else { + // Set selected values + checkboxes.forEach((checkbox) => { + checkbox.checked = newValues.includes(checkbox.value); + }); + + this._internals.setFormValue(this._values()); + } + } + + get value() { + return this._values(); + } + + set value(values) { + this._values(values); + } + + on(callback) { + this.addEventListener("change", callback); + } + + format(callback) { + this.exportFormat = callback; + } +} + +customElements.define("multi-select", MultiSelect); diff --git a/app/templates/assiduites/pages/signal_assiduites_group.j2 b/app/templates/assiduites/pages/signal_assiduites_group.j2 index fddae39c170aef54684d64a2ca0c4b1c09fe3bdd..b939944a7768dc1c7eb88a585982a4d10924c59a 100644 --- a/app/templates/assiduites/pages/signal_assiduites_group.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_group.j2 @@ -7,7 +7,6 @@ {% block scripts %} {{ super() }} -<script src="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.js"></script> <script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script> <script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script> <script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script> @@ -73,9 +72,10 @@ creerTousLesEtudiants(etuds); }); - $("#group_ids_sel").on("change", ()=>{ + const group_sel = document.querySelector("#group_ids_sel") + group_sel.on((values)=>{ main(); - }) + }); const moduleimpls = {}; const inscriptionsModules = new Map(); @@ -83,7 +83,8 @@ async function main(){ dateCouranteEstTravaillee(); - etuds = await recupEtuds($('#group_ids_sel').val()); + let group_ids = group_sel.value; + etuds = await recupEtuds(group_ids); if (etuds.size != 0){ await recupAssiduites(etuds, $("#date").datepicker("getDate")); } @@ -101,8 +102,7 @@ {% block styles %} {{ super() }} -<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap-theme.min.css"> -<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css"> +{# <link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.min.css"> #} <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css"> <link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css"> @@ -153,12 +153,12 @@ <div class="infos"> <div class="infos-button">Groupes : {{grp|safe}}</div> <div> - <button class="btn_date" onclick="jourSuivant(true)"> + <button class="btn_date btn btn-secondary" onclick="jourSuivant(true)"> ⇤ </button> <input type="text" name="date" id="date" class="datepicker" value="{{date}}"> </div> - <button class="btn_date" onclick="jourSuivant(false)"> + <button class="btn_date btn btn-secondary" onclick="jourSuivant(false)"> ⇥ </button> </div> @@ -166,8 +166,8 @@ <div style="display: {{'none' if readonly == 'true' else 'block'}};"> {{timeline|safe}} <div> - <button onclick="setPeriodValues(t_start, t_mid)">Matin</button> - <button onclick="setPeriodValues(t_mid, t_end)">Après-Midi</button> + <button class=" btn btn-secondary" onclick="setPeriodValues(t_start, t_mid)">Matin</button> + <button class=" btn btn-secondary" onclick="setPeriodValues(t_mid, t_end)">Après-Midi</button> </div> </div> diff --git a/app/templates/babase.j2 b/app/templates/babase.j2 index 2dc3aa89a0238ae15879374220d1b73fdfa39587..94baa48db66ad87df9ec16dec2094822ca11b0cf 100644 --- a/app/templates/babase.j2 +++ b/app/templates/babase.j2 @@ -33,6 +33,7 @@ <script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script> <script src="{{scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script> <script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script> + <script src="{{scu.STATIC_DIR}}/js/multi-select.js"></script> <script> const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}"; </script> diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 10e092cba1a02a0f72b5ab4481f4121fc4ffe7dd..c1d210ab655c20e555b13d2984b97f8afc5d6faf 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -94,7 +94,7 @@ from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids from app.scodoc.sco_archives_justificatifs import JustificatifArchiver -CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS +CSSSTYLES = html_sco_header.BOOTSTRAP_CSS # -------------------------------------------------------------------- @@ -1164,7 +1164,7 @@ def signal_assiduites_group(): formsemestre_date_fin=str(formsemestre.date_fin), formsemestre_id=formsemestre_id, gr_tit=gr_tit, - grp=sco_groups_view.menu_groups_choice(groups_infos), + grp=sco_groups_view.menu_groups_choice(groups_infos, html_export=False), minitimeline=_mini_timeline(), moduleimpl_select=_module_selector(formsemestre, moduleimpl_id), nonworkdays=_non_work_days(), diff --git a/app/views/groups.py b/app/views/groups.py index ae6bca0e7cc562ea246a032b6f5079c7cbae69dd..4f8e815dc0afb856476942dd5bed350219c5d9b9 100644 --- a/app/views/groups.py +++ b/app/views/groups.py @@ -45,7 +45,7 @@ def formulaire_feuille_appel(): formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id) - group_ids: list[int] = request.args.get("group_ids", "").split(",") + group_ids: list[int] = request.args.getlist("group_ids") form: groups_form.FeuilleAppelPreForm = groups_form.FeuilleAppelPreForm( request.form @@ -65,7 +65,7 @@ def formulaire_feuille_appel(): "ens": form.ens.data or "", } - form_group_ids: list[str] = request.form.getlist("group_ids") + form_group_ids: list[str] = request.form.get("group_ids", "").split(",") if form_group_ids: groups_infos = DisplayedGroupsInfos( form_group_ids, @@ -89,5 +89,5 @@ def formulaire_feuille_appel(): sco_data=ScoData(formsemestre=formsemestre), form=form, group_name=groups_infos.groups_titles, - grp=menu_groups_choice(groups_infos), + grp=menu_groups_choice(groups_infos, html_export=False), )