# -*- 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"""

Inscriptions au module {module.titre or "(module sans titre)"} ({module.code})

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"""
{ _make_menu(partitions, "Ajouter", "true") } { _make_menu(partitions, "Enlever", "false")}
""" ) for partition in partitions: if partition["partition_name"]: H.append(f"") H.append("") for ins in inscrits: etud = ins["etud"] if etud["etudid"] in in_module: checked = 'checked="checked"' else: checked = "" H.append( f""" """ ) groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre.id) for partition in partitions: if partition["partition_name"]: gr_name = "" for group in groups: if group["partition_id"] == partition["partition_id"]: gr_name = group["group_name"] break # gr_name == '' si etud non inscrit dans un groupe de cette partition H.append(f"") H.append("""
Nom{partition['partition_name']}
{etud['nomprenom']} {gr_name}
""") 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 ( '
' + htmlutils.make_menu(title, items, alone=True) + "
" ) 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"""

Inscriptions aux modules et UE du semestre

Inscrits au semestre: {len(inscrits)} étudiants

""" ] if options: H.append("

Modules auxquels tous les étudiants ne sont pas inscrits:

") H.append( f""" {'' if not is_apc else ""} """ ) for modimpl in options: if can_change: c_link = f"""{mod_description[modimpl.id] or "(inscrire des étudiants)"} """ else: c_link = mod_description[modimpl.id] H.append("""""") if not is_apc: H.append( f""" """ ) H.append( f""" """ ) H.append("
UECode Module Inscrits
{ modimpl.module.ue.acronyme or "" }{ modimpl.module.code or "(module sans code)" } {modimpl.module.titre or ""} { mod_nb_inscrits[modimpl.id]}{c_link}
") else: H.append( """Tous les étudiants sont inscrits à tous les modules.""" ) if commons: H.append( f"""

Modules communs (auxquels tous les étudiants sont inscrits):

{'' if not is_apc else ""} """ ) if is_apc: H.append("") H.append("""""") for modimpl in commons: if can_change: c_link = f"""{modimpl.module.titre}""" else: c_link = modimpl.module.titre H.append("""""") if not is_apc: H.append( f""" """ ) H.append( f""" """ ) if is_apc: H.append( f"""""" ) H.append("") H.append("
UECode ModuleParcours
{modimpl.module.ue.acronyme or ""}{ modimpl.module.code or "(module sans code)" }{c_link}{', '.join(p.code for p in modimpl.module.parcours)}
") # 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("
      ") for info in ues_cap_info[ue.id]: etud = Identite.get_etud(info["etudid"]) H.append( f"""
    • {etud.nomprenom}""" ) if info["ue_status"]["event_date"]: H.append( f"""(cap. le {info["ue_status"]["event_date"].strftime(scu.DATE_FMT)})""" ) if is_apc: is_inscrit_ue = (etud.id, ue.id) not in res.dispense_ues else: # CLASSIQUE is_inscrit_ue = info["is_ins"] if is_inscrit_ue: dm = ", ".join( [ m["code"] or m["abbrev"] or "pas_de_code" for m in info["is_ins"] ] ) H.append( f"""actuellement inscrit dans {len(info["is_ins"])} modules""" ) if is_inscrit_ue: if info["ue_status"]["is_capitalized"]: H.append( """
      UE actuelle moins bonne que l'UE capitalisée
      """ ) else: H.append( """
      UE actuelle meilleure que l'UE capitalisée
      """ ) if can_change: H.append( f""" """ ) else: H.append("(non réinscrit dans cette UE)") if can_change: H.append( f""" """ ) H.append("
    • ") 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

""" ] table_inscr = _table_but_ue_inscriptions(res) ue_ids = ( set.union(*(set(x.keys()) for x in table_inscr.values())) if table_inscr else set() ) ues = sorted( (db.session.get(UniteEns, ue_id) for ue_id in ue_ids), key=lambda u: (u.numero or 0, u.acronyme), ) H.append( """ """ ) for ue in ues: H.append(f"""""") H.append( """ """ ) partition_parcours: Partition = Partition.query.filter_by( formsemestre=res.formsemestre, partition_name=scu.PARTITION_PARCOURS ).first() etuds = list_etuds.etuds_sorted_from_ids(table_inscr.keys()) for etud in etuds: ues_etud = table_inscr[etud.id] H.append( f"""""" ) # Parcours: if partition_parcours: group = partition_parcours.get_etud_group(etud.id) parcours_name = group.group_name if group else "" else: parcours_name = "" H.append(f"""""") # UEs: for ue in ues: td_class = "" est_inscr = ues_etud.get(ue.id) # None si pas concerné if est_inscr is None: content = "" else: # Validations d'UE déjà enregistrées dans d'autres semestres validations_ue = ( ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) .filter( ScolarFormSemestreValidation.formsemestre_id != res.formsemestre.id, ScolarFormSemestreValidation.code.in_( codes_cursus.CODES_UE_VALIDES ), ) .join(UniteEns) .filter_by(ue_code=ue.ue_code) .all() ) validations_ue.sort( key=lambda v: codes_cursus.BUT_CODES_ORDER.get(v.code, 0) ) validation = validations_ue[-1] if validations_ue else None expl_validation = ( f"""Validée ({validation.code}) le { validation.event_date.strftime(scu.DATE_FMT)}""" if validation else "" ) td_class = ' class="ue_validee"' if validation else "" content = f""" """ H.append(f"""{content}""") H.append( """
NomParcours{ue.acronyme}
{etud.nomprenom}{parcours_name}

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