# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
"""
import collections
from operator import attrgetter
import flask
from flask import url_for, g, render_template, request
from flask_login import current_user
from app import db, log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import (
FormSemestre,
Identite,
ModuleImpl,
Partition,
ScolarFormSemestreValidation,
UniteEns,
Scolog,
)
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cache
from app.scodoc import codes_cursus
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
from app.tables import list_etuds
def moduleimpl_inscriptions_edit(
moduleimpl_id, etudids: list[int] | None = None, submitted=False
):
"""Formulaire inscription des etudiants a ce module
* Gestion des inscriptions
Nom TD TA TP (triable)
[x] M. XXX YYY - - -
ajouter TD A, TD B, TP 1, TP 2 ...
supprimer TD A, TD B, TP 1, TP 2 ...
* Si pas les droits: idem en readonly
"""
etudids = etudids or []
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
module = modimpl.module
formsemestre = modimpl.formsemestre
# -- check permission (and lock)
if not modimpl.can_change_inscriptions():
return # can_change_inscriptions raises exception
header = html_sco_header.sco_header(
page_title="Inscription au module",
)
footer = html_sco_header.sco_footer()
H = [
header,
f"""
Cette page permet d'éditer les étudiants inscrits à ce module
(ils doivent évidemment être inscrits au semestre).
Les étudiants cochés sont (ou seront) inscrits. Vous pouvez inscrire ou
désinscrire tous les étudiants d'un groupe à l'aide des menus "Ajouter" et "Enlever".
Aucune modification n'est prise en compte tant que l'on n'appuie pas
sur le bouton "Appliquer les modifications".
""",
]
# Liste des inscrits à ce semestre
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
formsemestre.id
)
for ins in inscrits:
etuds_info = sco_etud.get_etud_info(etudid=ins["etudid"], filled=1)
if not etuds_info:
log(
f"""moduleimpl_inscriptions_edit: inconsistency for etudid={ins['etudid']} !"""
)
raise ScoValueError(
f"""Étudiant {ins['etudid']} inscrit mais inconnu dans la base !"""
)
ins["etud"] = etuds_info[0]
inscrits.sort(key=lambda inscr: sco_etud.etud_sort_key(inscr["etud"]))
in_m = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
in_module = {x["etudid"] for x in in_m}
#
partitions = sco_groups.get_partitions_list(formsemestre.id)
#
if not submitted:
H.append(
"""
"""
)
H.append(
f"""""")
else: # SUBMISSION
# inscrit a ce module tous les etuds selectionnes
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
moduleimpl_id, formsemestre.id, etudids, reset=True
)
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id,
)
)
#
H.append(footer)
return "\n".join(H)
def _make_menu(partitions: list[dict], title="", check="true") -> str:
"""Menu with list of all groups"""
items = [{"title": "Tous", "attr": f"onclick=\"group_select('', -1, {check})\""}]
p_idx = 0
for partition in partitions:
if partition["partition_name"] is not None:
p_idx += 1
for group in sco_groups.get_partition_groups(partition):
items.append(
{
"title": "%s %s"
% (partition["partition_name"], group["group_name"]),
"attr": "onclick=\"group_select('%s', %s, %s)\""
% (group["group_name"], p_idx, check),
}
)
return (
'"
)
def moduleimpl_inscriptions_stats(formsemestre_id):
"""Affiche quelques informations sur les inscriptions
aux modules de ce semestre.
Inscrits au semestre:
Modules communs (tous inscrits): : ()
...
En APC, n'affiche pas la colonne UE, car le rattachement n'a pas
d'importance pédagogique.
descriptions:
groupes de TD A, B et C
tous sauf groupe de TP Z (?)
tous sauf
"""
authuser = current_user
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
is_apc = formsemestre.formation.is_apc()
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
)
set_all = set([x["etudid"] for x in inscrits])
partitions, _ = sco_groups.get_formsemestre_groups(formsemestre_id)
can_change = authuser.has_permission(Permission.EtudInscrit) and formsemestre.etat
# Décrit les inscriptions aux modules:
commons = [] # modules communs a tous les etuds du semestre
options = [] # modules ou seuls quelques etudiants sont inscrits
mod_description = {} # modimplid : str
mod_nb_inscrits = {} # modimplid : int
if is_apc:
modimpls = sorted(formsemestre.modimpls, key=lambda m: m.module.sort_key_apc())
else:
modimpls = formsemestre.modimpls_sorted
for modimpl in modimpls:
tous_inscrits, nb_inscrits, descr = descr_inscrs_module(
modimpl.id,
set_all,
partitions,
)
if tous_inscrits:
commons.append(modimpl)
else:
mod_description[modimpl.id] = descr
mod_nb_inscrits[modimpl.id] = nb_inscrits
options.append(modimpl)
# Page HTML:
H = [
f"""
Inscrits au semestre: {len(inscrits)} étudiants
"""
]
if options:
H.append("Modules auxquels tous les étudiants ne sont pas inscrits:
")
H.append(
f"""")
else:
H.append(
"""Tous les étudiants sont inscrits à tous les modules."""
)
if commons:
H.append(
f"""Modules communs (auxquels tous les étudiants sont inscrits):
")
# Etudiants "dispensés" d'une UE (capitalisée)
ues_cap_info = get_etuds_with_capitalized_ue(formsemestre_id)
if ues_cap_info:
H.append(
'Étudiants avec UEs capitalisées (ADM):
'
)
ues = [UniteEns.query.get_or_404(ue_id) for ue_id in ues_cap_info.keys()]
ues.sort(key=lambda u: u.numero)
for ue in ues:
H.append(
f"""- {ue.acronyme}: {ue.titre or ''}"""
)
H.append("
")
H.append("
")
# BUT: propose dispense de toutes UEs
if is_apc:
H.append(_list_but_ue_inscriptions(res, read_only=not can_change))
H.append(
"""
Cette page décrit les inscriptions actuelles.
Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en
cliquant sur la ligne du module.
Note: la déinscription d'un module ne perd pas les notes. Ainsi, si
l'étudiant est ensuite réinscrit au même module, il retrouvera ses notes.
"""
)
return render_template(
"sco_page.j2",
title="Inscriptions aux modules et UE du semestre",
javascripts=["js/moduleimpl_inscriptions_stats.js"],
content="\n".join(H),
)
def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) -> str:
"""HTML pour dispenser/reinscrire chaque étudiant à chaque UE du BUT"""
H = [
"""
Inscriptions/déinscription aux UEs du BUT
L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules
mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours.
Il peut s'agir d'étudiants redoublants ayant déjà acquis l'UE, ou d'une UE
présente dans le semestre mais pas dans le parcours de l'étudiant, ou bien d'autres
cas particuliers.
La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre)
et n'affecte pas les notes saisies.
"""
)
return "\n".join(H)
def _table_but_ue_inscriptions(res: NotesTableCompat) -> dict[int, dict]:
""" "table" avec les inscriptions aux UEs de chaque étudiant
{
etudid : { ue_id : True | False }
}
"""
return {
etudid: {
ue_id: (etudid, ue_id) not in res.dispense_ues
for ue_id in res.etud_ues_ids(etudid)
}
for etudid, inscr in res.formsemestre.etuds_inscriptions.items()
if inscr.etat == scu.INSCRIT
}
def descr_inscrs_module(moduleimpl_id, set_all, partitions):
"""returns tous_inscrits, nb_inscrits, descr"""
ins = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id)
set_m = set([x["etudid"] for x in ins]) # ens. des inscrits au module
non_inscrits = set_all - set_m
if len(non_inscrits) == 0:
return True, len(ins), "" # tous inscrits
if len(non_inscrits) <= 7: # seuil arbitraire
return False, len(ins), "tous sauf " + _fmt_etud_set(non_inscrits)
# Cherche les groupes:
gr = [] # [ ( partition_name , [ group_names ] ) ]
for partition in partitions:
grp = [] # groupe de cette partition
for group in sco_groups.get_partition_groups(partition):
members = sco_groups.get_group_members(group["group_id"])
set_g = set([m["etudid"] for m in members])
if set_g.issubset(set_m):
grp.append(group["group_name"])
set_m = set_m - set_g
gr.append((partition["partition_name"], grp))
#
d = []
for partition_name, grp in gr:
if grp:
d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
r = []
if d:
r.append(", ".join(d))
if set_m:
r.append(_fmt_etud_set(set_m))
#
return False, len(ins), " et ".join(r)
def _fmt_etud_set(etudids, max_list_size=7) -> str:
# max_list_size est le nombre max de noms d'etudiants listés
# au delà, on indique juste le nombre, sans les noms.
if len(etudids) > max_list_size:
return f"{len(etudids)} étudiants"
etuds = []
for etudid in etudids:
etud = db.session.get(Identite, etudid)
if etud:
etuds.append(etud)
return ", ".join(
[
f"""{etud.nomprenom}"""
for etud in sorted(etuds, key=attrgetter("sort_key"))
]
)
def get_etuds_with_capitalized_ue(formsemestre_id: int) -> list[dict]:
"""For each UE, computes list of students capitalizing the UE.
returns { ue_id : [ { infos } ] }
"""
ues_cap_info = collections.defaultdict(list)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
)
ues = nt.get_ues_stat_dict()
for ue in ues:
for etud in inscrits:
ue_status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"])
if ue_status and ue_status["was_capitalized"]:
ues_cap_info[ue["ue_id"]].append(
{
"etudid": etud["etudid"],
"ue_status": ue_status,
"is_ins": etud_modules_ue_inscr(
etud["etudid"], formsemestre_id, ue["ue_id"]
),
}
)
return ues_cap_info
def etud_modules_ue_inscr(etudid, formsemestre_id, ue_id) -> list[int]:
"""Modules de cette UE dans ce semestre
auxquels l'étudiant est inscrit.
Utile pour formations classiques seulement.
"""
r = ndb.SimpleDictFetch(
"""SELECT mod.id AS module_id, mod.*
FROM notes_moduleimpl mi, notes_modules mod,
notes_formsemestre sem, notes_moduleimpl_inscription i
WHERE sem.id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.id
AND mod.id = mi.module_id
AND mod.ue_id = %(ue_id)s
AND i.moduleimpl_id = mi.id
AND i.etudid = %(etudid)s
ORDER BY mod.numero
""",
{"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
)
return r
def do_etud_desinscrit_ue_classic(etudid, formsemestre_id, ue_id):
"""Désinscrit l'etudiant de tous les modules de cette UE dans ce semestre.
N'utiliser que pour les formations classiques, pas APC.
"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""DELETE FROM notes_moduleimpl_inscription
WHERE id IN (
SELECT i.id FROM
notes_moduleimpl mi, notes_modules mod,
notes_formsemestre sem, notes_moduleimpl_inscription i
WHERE sem.id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.id
AND mod.id = mi.module_id
AND mod.ue_id = %(ue_id)s
AND i.moduleimpl_id = mi.id
AND i.etudid = %(etudid)s
)
""",
{"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
)
Scolog.logdb(
method="etud_desinscrit_ue",
etudid=etudid,
msg=f"desinscription UE {ue_id}",
commit=True,
)
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > desinscription etudiant des modules
def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id):
"""Incrit l'etudiant de tous les modules de cette UE dans ce semestre."""
# Verifie qu'il est bien inscrit au semestre
insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id, "etudid": etudid}
)
if not insem:
raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid)
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT mi.id
FROM notes_moduleimpl mi, notes_modules mod, notes_formsemestre sem
WHERE sem.id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.id
AND mod.id = mi.module_id
AND mod.ue_id = %(ue_id)s
""",
{"formsemestre_id": formsemestre_id, "ue_id": ue_id},
)
res = cursor.dictfetchall()
for moduleimpl_id in [x["id"] for x in res]:
sco_moduleimpl.do_moduleimpl_inscription_create(
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
formsemestre_id=formsemestre_id,
)