EDT: filtrage par groupe scodoc

This commit is contained in:
Emmanuel Viennet 2023-11-16 23:34:47 +01:00
parent b0daccb32e
commit 1f319dfeba
7 changed files with 251 additions and 143 deletions

View File

@ -565,11 +565,14 @@ def save_groups_auto_assignment(formsemestre_id: int):
@permission_required(Permission.ScoView)
@as_json
def formsemestre_edt(formsemestre_id: int):
"""l'emploi du temps du semestre
"""l'emploi du temps du semestre.
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
group_ids permet de filtrer sur les groupes ScoDoc.
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
return sco_edt_cal.formsemestre_edt_dict(formsemestre)
group_ids = request.args.getlist("group_ids", int)
return sco_edt_cal.formsemestre_edt_dict(formsemestre, group_ids=group_ids)

View File

@ -111,11 +111,17 @@ _COLOR_PALETTE = [
_EVENT_DEFAULT_COLOR = "rgb(214, 233, 248)"
def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]:
def formsemestre_edt_dict(
formsemestre: FormSemestre, group_ids: list[int] = None
) -> list[dict]:
"""EDT complet du semestre, comme une liste de dict serialisable en json.
Fonction appellée par l'API /formsemestre/<int:formsemestre_id>/edt
Fonction appelée par l'API /formsemestre/<int:formsemestre_id>/edt
group_ids indiquer les groupes ScoDoc à afficher (les autres sont filtrés).
Les évènements pour lesquels le groupe ScoDoc n'est pas reconnu sont
toujours présents.
TODO: spécifier intervalle de dates start et end
"""
group_ids_set = set(group_ids) if group_ids else set()
try:
events_scodoc = _load_and_convert_ics(formsemestre)
except ScoValueError as exc:
@ -138,9 +144,12 @@ def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]:
{scu.EMO_WARNING} non reconnu</span>
</div>"""
)
if group and group_ids_set and group.id not in group_ids_set:
continue # ignore cet évènement
modimpl: ModuleImpl | bool = event["modimpl"]
if modimpl is False:
mod_disp = f"""<div class="module-edt" title="extraction emploi du temps non configurée">
mod_disp = f"""<div class="module-edt"
title="extraction emploi du temps non configurée">
{scu.EMO_WARNING} non configuré
</div>"""
else:

View File

@ -290,8 +290,9 @@ def get_group_members(group_id, etat=None):
return r
def get_group_infos(group_id, etat=None): # was _getlisteetud
"""legacy code: used by group_list and trombino"""
def get_group_infos(group_id, etat: str | None = None): # was _getlisteetud
"""legacy code: used by group_list and trombino.
etat: état de l'inscription."""
from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion()

View File

@ -153,63 +153,79 @@ def groups_view(
return "\n".join(H)
def form_groups_choice(groups_infos, with_selectall_butt=False, submit_on_change=False):
def form_groups_choice(
groups_infos,
with_selectall_butt=False,
with_deselect_butt=False,
submit_on_change=False,
default_deselect_others=True,
):
"""form 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 submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS
Si submit_on_change, soumet (recharge la page) à chaque modif.
Si default_deselect_others, désélectionne le groupe "Tous" quand on sélectionne un autre groupe.
Ces deux options ajoutent des classes utilisées en JS pour la gestion du formulaire.
"""
default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
H = [
"""<form id="group_selector" method="get">
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
<input type="hidden" name="default_group_id" id="default_group_id" value="%s"/>
f"""
<form id="group_selector" method="get">
<input type="hidden" name="formsemestre_id" id="formsemestre_id"
value="{groups_infos.formsemestre_id}"/>
<input type="hidden" name="default_group_id" id="default_group_id"
value="{default_group_id}"/>
Groupes:
{
menu_groups_choice(
groups_infos,
submit_on_change=submit_on_change,
default_deselect_others=default_deselect_others,
)
}
"""
% (groups_infos.formsemestre_id, default_group_id)
]
H.append(menu_groups_choice(groups_infos, submit_on_change=submit_on_change))
if with_selectall_butt:
H.append(
"""<input type="button" value="sélectionner tous" onmousedown="select_tous();"/>"""
"""<input type="button" value="sélectionner tous" onmousedown="select_groupe_tous();"/>"""
)
if with_deselect_butt:
H.append(
"""<input type="button" value="ne pas filtrer" onmousedown="remove_group_filter();"/>"""
)
H.append("</form>")
return "\n".join(H)
def menu_groups_choice(groups_infos, submit_on_change=False):
def menu_groups_choice(
groups_infos, submit_on_change=False, default_deselect_others=True
):
"""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)
"""
default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id)
if submit_on_change:
klass = "submit_on_change"
else:
klass = ""
H = [
"""<select name="group_ids" id="group_ids_sel" class="multiselect %s" multiple="multiple">
"""
% (klass,)
]
n_members = len(sco_groups.get_group_members(default_group_id))
if default_group_id in groups_infos.group_ids:
selected = "selected"
else:
selected = ""
H.append(
'<option class="default_group" value="%s" %s>%s (%s)</option>'
% (default_group_id, selected, "Tous", n_members)
)
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>
"""
]
for partition in groups_infos.partitions:
H.append('<optgroup label="%s">' % partition["partition_name"])
@ -289,14 +305,19 @@ class DisplayedGroupsInfos:
.formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste)
.members
.groups_titles
etat: filtrage selon l'état de l'inscription
select_all_when_unspecified : sélectionne le groupe "tous" si aucun groupe n'est indiqué.
empty_list_select_all: si vrai (défaut) on sélectionne le groupe tous si aucun groupe indiqué.
"""
def __init__(
self,
group_ids=(), # groupes specifies dans l'URL, ou un seul int
formsemestre_id=None,
etat=None,
formsemestre_id: int | None = None,
etat: str | None = None,
select_all_when_unspecified=False,
empty_list_select_all=True,
moduleimpl_id=None, # used to find formsemestre when unspecified
):
if isinstance(group_ids, int):
@ -317,21 +338,26 @@ class DisplayedGroupsInfos:
if not group_ids: # appel sans groupe (eg page accueil)
if not formsemestre_id:
raise Exception("missing parameter formsemestre_id or group_ids")
if select_all_when_unspecified:
group_ids = [
sco_groups.get_default_group(formsemestre_id, fix_if_missing=True)
]
else:
# selectionne le premier groupe trouvé, s'il y en a un
partition = sco_groups.get_partitions_list(
formsemestre_id, with_default=True
)[0]
groups = sco_groups.get_partition_groups(partition)
if groups:
group_ids = [groups[0]["group_id"]]
raise ValueError("missing parameter formsemestre_id or group_ids")
if empty_list_select_all:
if select_all_when_unspecified:
group_ids = [
sco_groups.get_default_group(
formsemestre_id, fix_if_missing=True
)
]
else:
group_ids = [sco_groups.get_default_group(formsemestre_id)]
# selectionne le premier groupe trouvé, s'il y en a un
partition = sco_groups.get_partitions_list(
formsemestre_id, with_default=True
)[0]
groups = sco_groups.get_partition_groups(partition)
if groups:
group_ids = [groups[0]["group_id"]]
else:
group_ids = [sco_groups.get_default_group(formsemestre_id)]
else:
group_ids = []
gq = []
for group_id in group_ids:
@ -375,7 +401,8 @@ class DisplayedGroupsInfos:
if not self.formsemestre: # aucun groupe selectionne
self.formsemestre = sco_formsemestre.get_formsemestre(formsemestre_id)
if formsemestre_id not in self.sems:
self.sems[formsemestre_id] = self.formsemestre
self.sortuniq()
if len(self.sems) > 1:

View File

@ -5,46 +5,54 @@ $().ready(function () {
for (var i = 0; i < spans.length; i++) {
var sp = spans[i];
var etudid = sp.id;
$(sp).load(SCO_URL + '/etud_photo_html?etudid=' + etudid);
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid);
}
});
// L'URL pour recharger l'état courant de la page (groupes et tab selectionnes)
// (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
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;
url.param()["curtab"] = CURRENT_TAB_HASH;
}
delete url.param()['formsemestre_id'];
url.param()['formsemestre_id'] = $("#group_selector")[0].formsemestre_id.value;
delete url.param()["formsemestre_id"];
url.param()["formsemestre_id"] =
$("#group_selector")[0].formsemestre_id.value;
var selected_groups = $("#group_selector select").val();
url.param()['group_ids'] = selected_groups; // remplace par groupes selectionnes
url.param()["group_ids"] = selected_groups; // remplace par groupes selectionnes
return url;
}
// Selectionne tous les etudiants et recharge la page:
function select_tous() {
// 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];
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;
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
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;
}
// 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;
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:
@ -53,13 +61,15 @@ function submit_group_selector() {
}
function show_current_tab() {
$('.nav-tabs [href="#' + CURRENT_TAB_HASH + '"]').tab('show');
if (document.getElementsByClassName("nav-tabs").length < 0) {
$('.nav-tabs [href="#' + CURRENT_TAB_HASH + '"]').tab("show");
}
}
var CURRENT_TAB_HASH = $.url().param()['curtab'];
var CURRENT_TAB_HASH = $.url().param()["curtab"];
$().ready(function () {
$('.nav-tabs a').on('shown.bs.tab', function (e) {
$(".nav-tabs a").on("shown.bs.tab", function (e) {
CURRENT_TAB_HASH = e.target.hash.slice(1); // sans le #
});
@ -69,7 +79,13 @@ $().ready(function () {
function change_list_options() {
var url = groups_view_url();
var selected_options = $("#group_list_options").val();
var options = ["with_paiement", "with_archives", "with_annotations", "with_codes", "with_bourse"];
var options = [
"with_paiement",
"with_archives",
"with_annotations",
"with_codes",
"with_bourse",
];
for (var i = 0; i < options.length; i++) {
var option = options[i];
delete url.param()[option];
@ -77,8 +93,8 @@ function change_list_options() {
url.param()[option] = 1;
}
}
var query_string = $.param(url.param(), traditional = true);
window.location = url.attr('base') + url.attr('path') + '?' + query_string;
var query_string = $.param(url.param(), (traditional = true));
window.location = url.attr("base") + url.attr("path") + "?" + query_string;
}
// Menu choix groupe:
@ -95,64 +111,87 @@ function toggle_visible_etuds() {
var input_eval = $("#formnotes_evaluation_id");
if (input_eval.length > 0) {
var evaluation_id = input_eval[0].value;
$("#menu_saisie_tableur a").attr("href", "saisie_notes_tableur?evaluation_id=" + evaluation_id + qargs);
$("#menu_saisie_tableur a").attr(
"href",
"saisie_notes_tableur?evaluation_id=" + evaluation_id + qargs
);
// lien feuille excel:
$("#lnk_feuille_saisie").attr("href", "feuille_saisie_notes?evaluation_id=" + evaluation_id + qargs);
$("#lnk_feuille_saisie").attr(
"href",
"feuille_saisie_notes?evaluation_id=" + evaluation_id + qargs
);
}
// Update champs form group_ids_str
let group_ids_str = Array.from(
document.querySelectorAll("#group_ids_sel option:checked")
).map(
function (elem) { return elem.value; }
).join();
document.querySelectorAll("input.group_ids_str").forEach(elem => elem.value = group_ids_str);
)
.map(function (elem) {
return elem.value;
})
.join();
document
.querySelectorAll("input.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) {
if (checked == true) {
var default_group_id = $(".default_group")[0].value;
$("#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);
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();
}
}
}
);
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();
@ -160,27 +199,27 @@ $().ready(function () {
// Trombinoscope
$().ready(function () {
var elems = $(".trombi-photo");
for (var i = 0; i < elems.length; i++) {
$(elems[i]).qtip(
{
content: {
ajax: {
url: SCO_URL + "/etud_info_html?with_photo=0&etudid=" + get_etudid_from_elem(elems[i])
},
text: "Loading..."
$(elems[i]).qtip({
content: {
ajax: {
url:
SCO_URL +
"/etud_info_html?with_photo=0&etudid=" +
get_etudid_from_elem(elems[i]),
},
position: {
at: "right",
my: "left top"
},
style: {
classes: 'qtip-etud'
},
// utile pour debugguer le css:
// hide: { event: 'unfocus' }
}
);
text: "Loading...",
},
position: {
at: "right",
my: "left top",
},
style: {
classes: "qtip-etud",
},
// utile pour debugguer le css:
// hide: { event: 'unfocus' }
});
}
});

View File

@ -3,8 +3,9 @@
{% block styles %}
{{super()}}
<link href="{{scu.STATIC_DIR}}/libjs/tui.calendar/toastui-calendar.min.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="{{ scu.STATIC_DIR }}/css/edt.css" type="text/css">
<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 %}
@ -13,6 +14,8 @@
<div class="tab-content">
<h2>Expérimental: emploi du temps</h2>
{{ form_groups_choice|safe }}
<div>
<span id="menu-navi">
<button type="button" class="btn btn-default btn-sm move-today"
@ -28,12 +31,24 @@
</div>
<div id="calendar" style="height: calc(100vh - 180px);"></div>
<div class="help">
<ul>
<li>Les heures sont toujours affichées dans le fuseau horaire du serveur,
qui est en principe celui des étudiants.
</li>
<li>Si vous filtrez par groupe, les évènements dont le groupe n'est pas reconnu seront affichés.
</li>
</ul>
</div>
</div>
{% endblock app_content %}
{% 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>
<script>
let hm_formatter = new Intl.DateTimeFormat('default', {
hour: '2-digit',
@ -92,7 +107,7 @@ document.addEventListener('DOMContentLoaded', function() {
const calendar = new Calendar(container, options);
fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt`)
fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt?{{groups_query_args|safe}}`)
.then(r=>{return r.json()})
.then(events=>{
if (typeof events == 'string') {

View File

@ -40,6 +40,7 @@ from app.decorators import (
from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo
from app.models import Formation, FormSemestre, ScoDocSiteConfig
from app.scodoc import sco_formations, sco_formation_versions
from app.scodoc import sco_groups_view
from app.scodoc.sco_permissions import Permission
from app.views import notes_bp as bp
from app.views import ScoData
@ -162,10 +163,23 @@ def formsemestre_edt(formsemestre_id: int):
hour_start = cfg.value.split(":")[0].lstrip(" 0") if cfg else "7"
cfg = ScoDocSiteConfig.query.filter_by(name="assi_afternoon_time").first()
hour_end = cfg.value.split(":")[0].lstrip(" 0") if cfg else "18"
group_ids = request.args.getlist("group_ids", int)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids=group_ids,
formsemestre_id=formsemestre_id,
empty_list_select_all=False,
)
return render_template(
"formsemestre/edt.j2",
formsemestre=formsemestre,
hour_start=hour_start,
hour_end=hour_end,
form_groups_choice=sco_groups_view.form_groups_choice(
groups_infos,
submit_on_change=True,
default_deselect_others=False,
with_deselect_butt=True,
),
groups_query_args=groups_infos.groups_query_args,
sco=ScoData(formsemestre=formsemestre),
)