forked from ScoDoc/ScoDoc
change multi-select
This commit is contained in:
parent
7f08f84934
commit
00eb37e8ac
@ -56,12 +56,15 @@ from app.scodoc.gen_tables import GenTable
|
|||||||
from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied
|
from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied
|
||||||
from app.scodoc.sco_permissions import Permission
|
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/etud_info.js",
|
||||||
"js/groups_view.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:
|
# view:
|
||||||
@ -115,13 +118,10 @@ def groups_view(
|
|||||||
return f"""
|
return f"""
|
||||||
{ html_sco_header.sco_header(
|
{ html_sco_header.sco_header(
|
||||||
javascripts=JAVASCRIPTS,
|
javascripts=JAVASCRIPTS,
|
||||||
cssstyles=CSSSTYLES,
|
cssstyles=CSSSTYLES
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<style>
|
<style>
|
||||||
div.multiselect-container.dropdown-menu {{
|
|
||||||
min-width: 180px;
|
|
||||||
}}
|
|
||||||
span.warning_unauthorized {{
|
span.warning_unauthorized {{
|
||||||
color: pink;
|
color: pink;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@ -221,47 +221,70 @@ def form_groups_choice(
|
|||||||
|
|
||||||
|
|
||||||
def menu_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
|
"""menu pour selection groupes
|
||||||
group_ids est la liste des groupes actuellement sélectionnés
|
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é.
|
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)
|
(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)
|
default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
|
||||||
n_members = len(sco_groups.get_group_members(default_group_id))
|
n_members = len(sco_groups.get_group_members(default_group_id))
|
||||||
|
|
||||||
H = [
|
values: dict = {
|
||||||
f"""<select name="group_ids" id="group_ids_sel"
|
# Choix : Tous (tous les groupes)
|
||||||
class="multiselect
|
"": [
|
||||||
{'submit_on_change' if submit_on_change else ''}
|
{
|
||||||
{'default_deselect_others' if default_deselect_others else ''}
|
"value": default_group_id,
|
||||||
"
|
"label": f"Tous ({n_members})",
|
||||||
multiple="multiple">
|
"selected": default_group_id in groups_infos.group_ids,
|
||||||
<option class="default_group"
|
"single": default_deselect_others,
|
||||||
value="{default_group_id}"
|
}
|
||||||
{'selected' if default_group_id in groups_infos.group_ids else ''}
|
|
||||||
>Tous ({n_members})</option>
|
|
||||||
"""
|
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
|
||||||
for partition in groups_infos.partitions:
|
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:
|
# Les groupes dans cette partition:
|
||||||
for g in sco_groups.get_partition_groups(partition):
|
for grp in sco_groups.get_partition_groups(partition):
|
||||||
if g["group_id"] in groups_infos.group_ids:
|
selected: bool = grp["group_id"] in groups_infos.group_ids
|
||||||
selected = "selected"
|
if grp["group_name"]:
|
||||||
else:
|
vals.append(
|
||||||
selected = ""
|
{
|
||||||
if g["group_name"]:
|
"value": grp["group_id"],
|
||||||
n_members = len(sco_groups.get_group_members(g["group_id"]))
|
"label": f"{grp['group_name']} ({len(sco_groups.get_group_members(grp['group_id']))})",
|
||||||
H.append(
|
"selected": selected,
|
||||||
'<option value="%s" %s>%s (%s)</option>'
|
}
|
||||||
% (g["group_id"], selected, g["group_name"], n_members)
|
|
||||||
)
|
)
|
||||||
H.append("</optgroup>")
|
|
||||||
H.append("</select> ")
|
values[p_name] = vals
|
||||||
return "\n".join(H)
|
|
||||||
|
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):
|
def menu_group_choice(group_id=None, formsemestre_id=None):
|
||||||
@ -698,7 +721,6 @@ def groups_table(
|
|||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
if groups_infos.members:
|
if groups_infos.members:
|
||||||
menu_options = []
|
|
||||||
options = {
|
options = {
|
||||||
"with_codes": "Affiche codes",
|
"with_codes": "Affiche codes",
|
||||||
}
|
}
|
||||||
@ -711,34 +733,33 @@ def groups_table(
|
|||||||
"with_bourse": "Statut boursier",
|
"with_bourse": "Statut boursier",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
valeurs: list[tuple[str, str]] = []
|
||||||
for option, label in options.items():
|
for option, label in options.items():
|
||||||
if locals().get(option, False):
|
selected = locals().get(option, False)
|
||||||
selected = "selected"
|
valeurs.append(
|
||||||
else:
|
{
|
||||||
selected = ""
|
"value": option,
|
||||||
menu_options.append(
|
"label": label,
|
||||||
f"""<option value="{option}" {selected}>{label}</option>"""
|
"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(
|
H.extend(
|
||||||
|
# ;
|
||||||
[
|
[
|
||||||
"""<span style="margin-left: 2em;">
|
f"""
|
||||||
<select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
|
<span style="margin-left: 2em;">
|
||||||
"\n".join(menu_options),
|
|
||||||
"""</select></span>
|
{multi_select.html()}
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function() {
|
</span>
|
||||||
$('#group_list_options').multiselect(
|
|
||||||
{
|
|
||||||
includeSelectAllOption: false,
|
|
||||||
nonSelectedText:'Options...',
|
|
||||||
onChange: function(element, checked){
|
|
||||||
change_list_options();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
"""<span class="warning_unauthorized">accès aux données personnelles interdit</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(
|
url_feuille_appel: str = (
|
||||||
|
url_for(
|
||||||
"scolar.formulaire_feuille_appel",
|
"scolar.formulaire_feuille_appel",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=groups_infos.formsemestre_id,
|
formsemestre_id=groups_infos.formsemestre_id,
|
||||||
group_ids=group_ids,
|
)
|
||||||
|
+ "&"
|
||||||
|
+ groups_infos.groups_query_args
|
||||||
)
|
)
|
||||||
|
|
||||||
H.extend(
|
H.extend(
|
||||||
|
@ -26,8 +26,8 @@
|
|||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
""" Common definitions
|
"""Common definitions"""
|
||||||
"""
|
|
||||||
import base64
|
import base64
|
||||||
import bisect
|
import bisect
|
||||||
import collections
|
import collections
|
||||||
@ -445,6 +445,121 @@ def translate_assiduites_metric(metric, inverse=True, short=True) -> str:
|
|||||||
return None
|
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
|
# Types de modules
|
||||||
class ModuleType(IntEnum):
|
class ModuleType(IntEnum):
|
||||||
"""Code des types de module."""
|
"""Code des types de module."""
|
||||||
|
@ -13,72 +13,41 @@ $().ready(function () {
|
|||||||
// (ne fonctionne que pour les requetes GET: manipule la query string)
|
// (ne fonctionne que pour les requetes GET: manipule la query string)
|
||||||
|
|
||||||
function groups_view_url() {
|
function groups_view_url() {
|
||||||
var url = $.url();
|
let url = new URL(location.href);
|
||||||
delete url.param()["group_ids"]; // retire anciens groupes de l'URL
|
let urlParams = url.searchParams;
|
||||||
delete url.param()["curtab"]; // retire ancien tab actif
|
// ajout du formsemestre
|
||||||
if (CURRENT_TAB_HASH) {
|
urlParams.set(
|
||||||
url.param()["curtab"] = CURRENT_TAB_HASH;
|
"formsemestre_id",
|
||||||
}
|
$("#group_selector")[0].formsemestre_id.value
|
||||||
delete url.param()["formsemestre_id"];
|
);
|
||||||
url.param()["formsemestre_id"] =
|
// ajout du tab actif
|
||||||
$("#group_selector")[0].formsemestre_id.value;
|
const tabActif = document.querySelector(
|
||||||
|
'[role="tab"][aria-selected="true"]'
|
||||||
var selected_groups = $("#group_selector select#group_ids_sel").val();
|
).id;
|
||||||
url.param()["group_ids"] = selected_groups; // remplace par groupes selectionnes
|
urlParams.set("tab", tabActif);
|
||||||
|
urlParams.delete("group_ids");
|
||||||
return url;
|
// ajout des groupes selectionnes
|
||||||
}
|
var selected_groups = document.getElementById("group_ids_sel").value;
|
||||||
|
url.search = urlParams.toString() + selected_groups;
|
||||||
// Sélectionne le groupe "tous" et recharge la page:
|
return url.href;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recharge la page sans arguments group_ids
|
// Recharge la page sans arguments group_ids
|
||||||
function remove_group_filter() {
|
function remove_group_filter() {
|
||||||
var url = groups_view_url();
|
var url = new URL(location.href);
|
||||||
delete url.param()["group_ids"];
|
url.searchParams.delete("group_ids");
|
||||||
var query_string = $.param(url.param(), (traditional = true));
|
|
||||||
window.location = url.attr("base") + url.attr("path") + "?" + query_string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// L'URL pour l'état courant de la page:
|
window.location = url.href;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recharge la page en changeant les groupes selectionnés et en conservant le tab actif:
|
// Recharge la page en changeant les groupes selectionnés et en conservant le tab actif:
|
||||||
function submit_group_selector() {
|
function submit_group_selector() {
|
||||||
window.location = get_current_url();
|
window.location = groups_view_url();
|
||||||
}
|
}
|
||||||
|
|
||||||
function show_current_tab() {
|
function change_list_options(selected_options) {
|
||||||
if (document.getElementsByClassName("nav-tabs").length > 0) {
|
var url = new URL(groups_view_url());
|
||||||
$('.nav-tabs [href="#' + CURRENT_TAB_HASH + '"]').tab("show");
|
var urlParams = url.searchParams;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
var options = [
|
var options = [
|
||||||
"with_paiement",
|
"with_paiement",
|
||||||
"with_archives",
|
"with_archives",
|
||||||
@ -88,13 +57,11 @@ function change_list_options() {
|
|||||||
];
|
];
|
||||||
for (var i = 0; i < options.length; i++) {
|
for (var i = 0; i < options.length; i++) {
|
||||||
var option = options[i];
|
var option = options[i];
|
||||||
delete url.param()[option];
|
|
||||||
if ($.inArray(option, selected_options) >= 0) {
|
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.href;
|
||||||
window.location = url.attr("base") + url.attr("path") + "?" + query_string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu choix groupe:
|
// Menu choix groupe:
|
||||||
@ -134,57 +101,6 @@ function toggle_visible_etuds() {
|
|||||||
.forEach((elem) => (elem.value = group_ids_str));
|
.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
|
// Trombinoscope
|
||||||
$().ready(function () {
|
$().ready(function () {
|
||||||
var elems = $(".trombi-photo");
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
303
app/static/js/multi-select.js
Normal file
303
app/static/js/multi-select.js
Normal 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);
|
@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ 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}}/libjs/purl.js"></script>
|
||||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||||
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
|
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
|
||||||
@ -73,9 +72,10 @@
|
|||||||
creerTousLesEtudiants(etuds);
|
creerTousLesEtudiants(etuds);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#group_ids_sel").on("change", ()=>{
|
const group_sel = document.querySelector("#group_ids_sel")
|
||||||
|
group_sel.on((values)=>{
|
||||||
main();
|
main();
|
||||||
})
|
});
|
||||||
|
|
||||||
const moduleimpls = {};
|
const moduleimpls = {};
|
||||||
const inscriptionsModules = new Map();
|
const inscriptionsModules = new Map();
|
||||||
@ -83,7 +83,8 @@
|
|||||||
|
|
||||||
async function main(){
|
async function main(){
|
||||||
dateCouranteEstTravaillee();
|
dateCouranteEstTravaillee();
|
||||||
etuds = await recupEtuds($('#group_ids_sel').val());
|
let group_ids = group_sel.value;
|
||||||
|
etuds = await recupEtuds(group_ids);
|
||||||
if (etuds.size != 0){
|
if (etuds.size != 0){
|
||||||
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
|
await recupAssiduites(etuds, $("#date").datepicker("getDate"));
|
||||||
}
|
}
|
||||||
@ -101,8 +102,7 @@
|
|||||||
|
|
||||||
{% block styles %}
|
{% block styles %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap-theme.min.css">
|
{# <link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.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}}/css/assiduites.css">
|
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||||
|
|
||||||
@ -153,12 +153,12 @@
|
|||||||
<div class="infos">
|
<div class="infos">
|
||||||
<div class="infos-button">Groupes : {{grp|safe}}</div>
|
<div class="infos-button">Groupes : {{grp|safe}}</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn_date" onclick="jourSuivant(true)">
|
<button class="btn_date btn btn-secondary" onclick="jourSuivant(true)">
|
||||||
⇤
|
⇤
|
||||||
</button>
|
</button>
|
||||||
<input type="text" name="date" id="date" class="datepicker" value="{{date}}">
|
<input type="text" name="date" id="date" class="datepicker" value="{{date}}">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn_date" onclick="jourSuivant(false)">
|
<button class="btn_date btn btn-secondary" onclick="jourSuivant(false)">
|
||||||
⇥
|
⇥
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -166,8 +166,8 @@
|
|||||||
<div style="display: {{'none' if readonly == 'true' else 'block'}};">
|
<div style="display: {{'none' if readonly == 'true' else 'block'}};">
|
||||||
{{timeline|safe}}
|
{{timeline|safe}}
|
||||||
<div>
|
<div>
|
||||||
<button onclick="setPeriodValues(t_start, t_mid)">Matin</button>
|
<button class=" btn btn-secondary" 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_mid, t_end)">Après-Midi</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -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/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/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||||
<script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script>
|
<script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script>
|
||||||
|
<script src="{{scu.STATIC_DIR}}/js/multi-select.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}";
|
const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}";
|
||||||
</script>
|
</script>
|
||||||
|
@ -94,7 +94,7 @@ from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
|
|||||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
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_date_fin=str(formsemestre.date_fin),
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
gr_tit=gr_tit,
|
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(),
|
minitimeline=_mini_timeline(),
|
||||||
moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
|
moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
|
||||||
nonworkdays=_non_work_days(),
|
nonworkdays=_non_work_days(),
|
||||||
|
@ -45,7 +45,7 @@ def formulaire_feuille_appel():
|
|||||||
|
|
||||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
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(
|
form: groups_form.FeuilleAppelPreForm = groups_form.FeuilleAppelPreForm(
|
||||||
request.form
|
request.form
|
||||||
@ -65,7 +65,7 @@ def formulaire_feuille_appel():
|
|||||||
"ens": form.ens.data or "",
|
"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:
|
if form_group_ids:
|
||||||
groups_infos = DisplayedGroupsInfos(
|
groups_infos = DisplayedGroupsInfos(
|
||||||
form_group_ids,
|
form_group_ids,
|
||||||
@ -89,5 +89,5 @@ def formulaire_feuille_appel():
|
|||||||
sco_data=ScoData(formsemestre=formsemestre),
|
sco_data=ScoData(formsemestre=formsemestre),
|
||||||
form=form,
|
form=form,
|
||||||
group_name=groups_infos.groups_titles,
|
group_name=groups_infos.groups_titles,
|
||||||
grp=menu_groups_choice(groups_infos),
|
grp=menu_groups_choice(groups_infos, html_export=False),
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user