Compare commits

...

2 Commits

Author SHA1 Message Date
Iziram
00eb37e8ac change multi-select 2024-07-31 13:44:04 +02:00
Iziram
7f08f84934 suite upgrade bootstrap 2024-07-31 13:44:00 +02:00
27 changed files with 602 additions and 253 deletions

View File

@ -47,9 +47,9 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_logos import find_logo
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
JAVASCRIPTS = html_sco_header.BOOTSTRAP_JS + []
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
CSSSTYLES = html_sco_header.BOOTSTRAP_CSS
# class ItemForm(FlaskForm):
# """Unused Generic class to document common behavior for classes

View File

@ -37,19 +37,8 @@ import sco_version
# Some constants:
# Multiselect menus are used on a few pages and not loaded by default
BOOTSTRAP_MULTISELECT_JS = [
"libjs/bootstrap/js/bootstrap.min.js",
"libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.js",
"libjs/purl.js",
]
BOOTSTRAP_MULTISELECT_CSS = [
"libjs/bootstrap/css/bootstrap.min.css",
"libjs/bootstrap/css/bootstrap-theme.min.css",
"libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css",
]
BOOTSTRAP_JS = ["libjs/bootstrap/js/bootstrap.min.js", "libjs/purl.js"]
BOOTSTRAP_CSS = ["libjs/bootstrap/css/bootstrap.min.css"]
def standard_html_header():

View File

@ -28,6 +28,7 @@
"""Ajout/Modification/Suppression UE
"""
import re
import sqlalchemy as sa
@ -765,9 +766,9 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
)
H = [
html_sco_header.sco_header(
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
cssstyles=html_sco_header.BOOTSTRAP_CSS
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/ue_table.css"],
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
javascripts=html_sco_header.BOOTSTRAP_JS
+ [
"libjs/jinplace-1.2.1.min.js",
"js/ue_list.js",
@ -1129,9 +1130,7 @@ def _ue_table_ues(
scodoc_dept=g.scodoc_dept,
ue_id=ue["ue_id"],
)
ue[
"code_apogee_str"
] = f""", Apo: <span
ue["code_apogee_str"] = f""", Apo: <span
class="{klass}" data-url="{edit_url}" id="{ue['ue_id']}"
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
ue["code_apogee"] or ""

View File

@ -73,9 +73,7 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
formsemestre_ids_parcours = [sem["formsemestre_id"] for sem in semlist_parcours]
# Ensemble des étudiants
etuds_infos = (
{}
) # etudid : { formsemestre_id d'inscription le plus recent dans les dates considérées, etud }
etuds_infos = {} # etudid : { formsemestre_id d'inscription le plus recent dans les dates considérées, etud }
for formsemestre_id in formsemestre_ids_parcours:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
@ -287,9 +285,9 @@ def scodoc_table_results(
H = [
html_sco_header.sco_header(
page_title="Export résultats",
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
javascripts=html_sco_header.BOOTSTRAP_JS
+ ["js/etud_info.js", "js/export_results.js"],
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS,
cssstyles=html_sco_header.BOOTSTRAP_CSS,
),
# XXX
"""
@ -326,9 +324,9 @@ _DATE_FORM = """
</div>
<div>
<b>Types de parcours :</b>
<select name="types_parcours" id="parcours_sel" class="multiselect" multiple="multiple">
<multi-select name="types_parcours" id="parcours_sel" label="Choisir le(s) parcours...">
{menu_options}
</select>
</multi-select>
<input type="submit" name="" value=" charger " width=100/>
</form>

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(
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,
)
+ "&"
+ 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,7 +33,7 @@ div.small_form {
{% endif %}
<div class="row {{ 'small_form' if is_cas_enabled else ''}}">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
{{ wtf.quick_form(form, button_map={'submit':'secondary'}) }}
</div>
</div>

View File

@ -6,7 +6,7 @@
<h1>Création utilisateur</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
{{ wtf.quick_form(form, button_map={'submit':'secondary'}) }}
</div>
</div>
{% endblock %}

View File

@ -11,7 +11,7 @@
<div class="row" style="margin-top: 30px;">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
{{ wtf.quick_form(form, button_map={'submit':'secondary'}) }}
</div>
</div>
{% endblock %}

View File

@ -6,7 +6,7 @@
<h1>Demande d'un nouveau mot de passe</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
{{ wtf.quick_form(form, button_map={'submit':'secondary'}) }}
</div>
</div>
{% if is_cas_enabled %}

View File

@ -12,7 +12,7 @@
</div>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
{{ wtf.quick_form(form, button_map={'submit':'secondary'}) }}
</div>
</div>
{% endblock %}

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

@ -50,7 +50,7 @@
{% if current_user.has_permission(current_user.Permission.RelationsEntrepEdit, None) %}
<td>
<div class="btn-group">
<a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#">Action
<a class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" href="#">Action
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-left">

View File

@ -25,13 +25,13 @@
<div class="container boutons">
{% if current_user.has_permission(current_user.Permission.RelationsEntrepEdit, None) %}
<a class="btn btn-default" href="{{ url_for('entreprises.add_entreprise') }}">Ajouter une entreprise</a>
<a class="btn btn-secondary" href="{{ url_for('entreprises.add_entreprise') }}">Ajouter une entreprise</a>
{% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntrepExport, None) %}
<a class="btn btn-default" href="{{ url_for('entreprises.import_donnees') }}">Importer des données</a>
<a class="btn btn-secondary" href="{{ url_for('entreprises.import_donnees') }}">Importer des données</a>
{% endif %}
{% if current_user.has_permission(current_user.Permission.RelationsEntrepExport, None) and entreprises %}
<a class="btn btn-default" href="{{ url_for('entreprises.export_donnees') }}">Exporter des données</a>
<a class="btn btn-secondary" href="{{ url_for('entreprises.export_donnees') }}">Exporter des données</a>
{% endif %}
</div>
@ -76,7 +76,7 @@
{% if current_user.has_permission(current_user.Permission.RelationsEntrepEdit, None) %}
<td>
<div class="btn-group">
<a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#">Action
<a class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" href="#">Action
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-left">

View File

@ -48,7 +48,7 @@
<th>{{ entreprise.ville }}</th>
<th>{{ entreprise.pays }}</th>
<th>
<a class="btn btn-default"
<a class="btn btn-secondary"
href="{{ url_for('entreprises.fiche_entreprise_validation', entreprise_id=entreprise.id) }}">Voir</a>
</th>
</tr>

View File

@ -190,7 +190,7 @@
{% if current_user.has_permission(current_user.Permission.RelationsEntrepEdit, None) %}
<td>
<div class="btn-group">
<a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#">Action
<a class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" href="#">Action
<span class="caret"></span>
</a>
<ul class="dropdown-menu pull-left">

View File

@ -31,9 +31,9 @@
{% endfor %}
{{ form.correspondants }}
<div style="margin-bottom: 10px;">
<button class="btn btn-default" id="add-correspondant-field">Ajouter un correspondant</button>
{{ form.submit(class_="btn btn-default") }}
{{ form.cancel(class_="btn btn-default") }}
<button class="btn btn-secondary" id="add-correspondant-field">Ajouter un correspondant</button>
{{ form.submit(class_="btn btn-secondary") }}
{{ form.cancel(class_="btn btn-secondary") }}
</div>
</form>
</div>
@ -45,7 +45,7 @@
for (let i = 0; i < allCorrepondantsForm.length; i++) {
let form_id = allCorrepondantsForm[i].getElementsByTagName('table')[0].id
if (form_id.split('-')[1] != 0)
allCorrepondantsForm[i].insertAdjacentHTML('beforeend', `<div class="btn btn-default btn-remove" onclick="deleteForm('${form_id}')">Retirer ce correspondant</div>`)
allCorrepondantsForm[i].insertAdjacentHTML('beforeend', `<div class="btn btn-secondary btn-remove" onclick="deleteForm('${form_id}')">Retirer ce correspondant</div>`)
}
window.onload = function (e) {
@ -75,7 +75,7 @@
<tr><th><label for="${newFieldName}-notes">Notes</label></th><td><input class="form-control" id="${newFieldName}-notes" name="${newFieldName}-notes" type="text" value=""></td></tr>
</table>
<input id="${newFieldName}-csrf_token" name="${newFieldName}-csrf_token" type="hidden" value=${csrf_token}>
<div class="btn btn-default btn-remove" onclick="deleteForm('${newFieldName}')">Retirer ce correspondant</div>
<div class="btn btn-secondary btn-remove" onclick="deleteForm('${newFieldName}')">Retirer ce correspondant</div>
</li>
`);
});

View File

@ -24,9 +24,9 @@
{% endfor %}
{{ form.responsables }}
<div style="margin-bottom: 10px;">
<button class="btn btn-default" id="add-responsable-field">Ajouter un responsable</button>
{{ form.submit(class_="btn btn-default") }}
{{ form.cancel(class_="btn btn-default") }}
<button class="btn btn-secondary" id="add-responsable-field">Ajouter un responsable</button>
{{ form.submit(class_="btn btn-secondary") }}
{{ form.cancel(class_="btn btn-secondary") }}
</div>
</form>
</div>
@ -38,7 +38,7 @@
for (let i = 0; i < allResponsablesField.length; i++) {
let form_id = allResponsablesField[i].getElementsByTagName('input')[0].id
if (form_id.split('-')[1] != 0)
allResponsablesField[i].insertAdjacentHTML('beforeend', `<div class="btn btn-default btn-remove" onclick="deleteForm('${form_id}')">Retirer</div>`)
allResponsablesField[i].insertAdjacentHTML('beforeend', `<div class="btn btn-secondary btn-remove" onclick="deleteForm('${form_id}')">Retirer</div>`)
}
window.onload = function (e) {
@ -71,7 +71,7 @@
<li>
<label for="${newFieldName}">Responsable (*)</label>
<input class="form-control" id="${newFieldName}" name="${newFieldName}" type="text" value="" placeholder="Tapez le nom du responsable de formation">
<div class="btn btn-default btn-remove" onclick="deleteForm('${newFieldName}')">Retirer</div>
<div class="btn btn-secondary btn-remove" onclick="deleteForm('${newFieldName}')">Retirer</div>
</li>
`);
var as_r = new bsn.AutoSuggest(newFieldName, responsables_options);

View File

@ -25,7 +25,7 @@
<div class="text-center">
<a href="{{ url_for('entreprises.logs', page=logs.prev_num) }}"
class="btn btn-default {% if logs.page == 1 %}disabled{% endif %}">
class="btn btn-secondary {% if logs.page == 1 %}disabled{% endif %}">
&laquo;
</a>
{% for page_num in logs.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
@ -33,14 +33,14 @@
{% if logs.page == page_num %}
<a href="{{ url_for('entreprises.logs', page=page_num) }}" class="btn btn-inverse">{{ page_num }}</a>
{% else %}
<a href="{{ url_for('entreprises.logs', page=page_num) }}" class="btn btn-default">{{ page_num }}</a>
<a href="{{ url_for('entreprises.logs', page=page_num) }}" class="btn btn-secondary">{{ page_num }}</a>
{% endif %}
{% else %}
...
{% endif %}
{% endfor %}
<a href="{{ url_for('entreprises.logs', page=logs.next_num) }}"
class="btn btn-default {% if logs.page == logs.pages %}disabled{% endif %}">
class="btn btn-secondary {% if logs.page == logs.pages %}disabled{% endif %}">
&raquo;
</a>
</div>

View File

@ -29,7 +29,7 @@
<div class="text-center">
<a href="{{ url_for('entreprises.logs_entreprise', entreprise_id=entreprise.id, page=logs.prev_num) }}"
class="btn btn-default {% if logs.page == 1 %}disabled{% endif %}">
class="btn btn-secondary {% if logs.page == 1 %}disabled{% endif %}">
&laquo;
</a>
{% for page_num in logs.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}
@ -39,14 +39,14 @@
class="btn btn-inverse">{{ page_num }}</a>
{% else %}
<a href="{{ url_for('entreprises.logs_entreprise', entreprise_id=entreprise.id, page=page_num) }}"
class="btn btn-default">{{ page_num }}</a>
class="btn btn-secondary">{{ page_num }}</a>
{% endif %}
{% else %}
...
{% endif %}
{% endfor %}
<a href="{{ url_for('entreprises.logs_entreprise', entreprise_id=entreprise.id, page=logs.next_num) }}"
class="btn btn-default {% if logs.page == logs.pages %}disabled{% endif %}">
class="btn btn-secondary {% if logs.page == logs.pages %}disabled{% endif %}">
&raquo;
</a>
</div>

View File

@ -4,7 +4,6 @@
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/tui.calendar/toastui-calendar.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/edt.css">
{% endblock %}
@ -42,12 +41,12 @@
<div>
<span id="menu-navi">
<button type="button" class="btn btn-default btn-sm move-today"
<button type="button" class="btn btn-secondary btn-sm move-today"
data-action="move-today">Aujourd'hui</button>
<button type="button" class="btn btn-default btn-sm move-day" data-action="move-prev">
<button type="button" class="btn btn-secondary btn-sm move-day" data-action="move-prev">
<i class="calendar-icon ic-arrow-line-left" data-action="move-prev">&lt;</i>
</button>
<button type="button" class="btn btn-default btn-sm move-day" data-action="move-next">
<button type="button" class="btn btn-secondary btn-sm move-day" data-action="move-next">
<i class="calendar-icon ic-arrow-line-right" data-action="move-next">&gt;</i>
</button>
</span>
@ -77,7 +76,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}}/libjs/tui.calendar/toastui-calendar.min.js"></script>
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>

View File

@ -4,8 +4,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">
<style>
div{
@ -50,6 +49,5 @@
{{super()}}
{% include "sco_timepicker.j2" %}
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
<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>
{% endblock scripts %}

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