change multi-select

This commit is contained in:
Iziram 2024-07-31 13:43:39 +02:00
parent 7f08f84934
commit 00eb37e8ac
8 changed files with 557 additions and 190 deletions

View File

@ -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(

View File

@ -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."""

View File

@ -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();
}
});

View File

@ -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);

View File

@ -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&nbsp;: {{grp|safe}}</div>
<div>
<button class="btn_date" onclick="jourSuivant(true)">
<button class="btn_date btn btn-secondary" onclick="jourSuivant(true)">
&LeftArrowBar;
</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)">
&RightArrowBar;
</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>

View File

@ -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>

View File

@ -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(),

View File

@ -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),
)