# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2023 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 # ############################################################################## """Operations de base sur les formsemestres """ import datetime import time from operator import itemgetter from flask import g, request, url_for import app import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log from app.models import Departement from app.models import FormSemestre from app.scodoc import sco_cache, sco_codes_parcours, sco_formations, sco_preferences from app.scodoc.gen_tables import GenTable from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID from app.scodoc.sco_exceptions import ScoInvalidIdType, ScoValueError from app.scodoc.sco_vdi import ApoEtapeVDI _formsemestreEditor = ndb.EditableTable( "notes_formsemestre", "formsemestre_id", ( "formsemestre_id", "semestre_id", "formation_id", "titre", "date_debut", "date_fin", "gestion_compensation", "gestion_semestrielle", "etat", "bul_hide_xml", "block_moyennes", "block_moyenne_generale", "bul_bgcolor", "modalite", "resp_can_edit", "resp_can_change_ens", "ens_can_edit_eval", "elt_sem_apo", "elt_annee_apo", ), filter_dept=True, sortkey="date_debut", output_formators={ "date_debut": ndb.DateISOtoDMY, "date_fin": ndb.DateISOtoDMY, }, input_formators={ "date_debut": ndb.DateDMYtoISO, "date_fin": ndb.DateDMYtoISO, "etat": bool, "bul_hide_xml": bool, "block_moyennes": bool, "block_moyenne_generale": bool, "gestion_compensation": bool, "gestion_semestrielle": bool, "resp_can_edit": bool, "resp_can_change_ens": bool, "ens_can_edit_eval": bool, "bul_bgcolor": lambda color: color or "white", "titre": lambda titre: titre or "sans titre", }, ) def get_formsemestre(formsemestre_id, raise_soft_exc=False): "list ONE formsemestre" if formsemestre_id is None: raise ValueError("get_formsemestre: id manquant") if formsemestre_id in g.stored_get_formsemestre: return g.stored_get_formsemestre[formsemestre_id] if not isinstance(formsemestre_id, int): log(f"get_formsemestre: invalid id '{formsemestre_id}'") raise ScoInvalidIdType("get_formsemestre: formsemestre_id must be an integer !") sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id}) if not sems: log(f"get_formsemestre: invalid formsemestre_id ({formsemestre_id})") if raise_soft_exc: raise ScoValueError(f"semestre {formsemestre_id} inconnu !") else: raise ValueError(f"semestre {formsemestre_id} inconnu !") g.stored_get_formsemestre[formsemestre_id] = sems[0] return sems[0] def do_formsemestre_list(*a, **kw): "list formsemestres" # log('do_formsemestre_list: a=%s kw=%s' % (str(a),str(kw))) cnx = ndb.GetDBConnexion() sems = _formsemestreEditor.list(cnx, *a, **kw) # Ajoute les étapes Apogee et les responsables: for sem in sems: sem["etapes"] = read_formsemestre_etapes(sem["formsemestre_id"]) sem["responsables"] = read_formsemestre_responsables(sem["formsemestre_id"]) # Filtre sur code etape si indiqué: if "args" in kw: etape = kw["args"].get("etape_apo", None) if etape: sems = [sem for sem in sems if etape in sem["etapes"]] for sem in sems: _formsemestre_enrich(sem) # tri par date, le plus récent d'abord sems.sort(key=itemgetter("dateord", "semestre_id"), reverse=True) return sems def _formsemestre_enrich(sem): """Ajoute champs souvent utiles: titre + annee et dateord (pour tris). XXX obsolete: préférer formsemestre.to_dict() ou, mieux, les méthodes de FormSemestre. """ # imports ici pour eviter refs circulaires from app.scodoc import sco_formsemestre_edit formations = sco_formations.formation_list( args={"formation_id": sem["formation_id"]} ) if not formations: raise ScoValueError("pas de formation pour ce semestre !") F = formations[0] parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) # 'S1', 'S2', ... ou '' pour les monosemestres if sem["semestre_id"] != NO_SEMESTRE_ID: sem["sem_id_txt"] = "S%s" % sem["semestre_id"] else: sem["sem_id_txt"] = "" # Nom avec numero semestre: sem["titre_num"] = sem["titre"] # eg "DUT Informatique" if sem["semestre_id"] != NO_SEMESTRE_ID: sem["titre_num"] += " %s %s" % ( parcours.SESSION_NAME, sem["semestre_id"], ) # eg "DUT Informatique semestre 2" sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"]) sem["date_fin_iso"] = ndb.DateDMYtoISO(sem["date_fin"]) sem["dateord"] = sem["date_debut_iso"] # pour les tris try: mois_debut, annee_debut = sem["date_debut"].split("/")[1:] except: mois_debut, annee_debut = "", "" try: mois_fin, annee_fin = sem["date_fin"].split("/")[1:] except: mois_fin, annee_fin = "", "" sem["annee_debut"] = annee_debut sem["annee_fin"] = annee_fin sem["mois_debut_ord"] = int(mois_debut) sem["mois_fin_ord"] = int(mois_fin) sem["annee"] = annee_debut # 2007 ou 2007-2008: sem["anneescolaire"] = scu.annee_scolaire_repr( int(annee_debut), sem["mois_debut_ord"] ) # La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre # devrait sans doute pouvoir etre changé... if sem["mois_debut_ord"] >= 8 and sem["mois_debut_ord"] <= 10: sem["periode"] = 1 # typiquement, début en septembre: S1, S3... else: sem["periode"] = 2 # typiquement, début en février: S2, S4... sem["titreannee"] = "%s %s %s" % ( sem["titre_num"], sem.get("modalite", ""), annee_debut, ) if annee_fin != annee_debut: sem["titreannee"] += "-" + annee_fin sem["annee"] += "-" + annee_fin # et les dates sous la forme "oct 2007 - fev 2008" months = scu.MONTH_NAMES_ABBREV if mois_debut: mois_debut = months[int(mois_debut) - 1] if mois_fin: mois_fin = months[int(mois_fin) - 1] sem["mois_debut"] = mois_debut + " " + annee_debut sem["mois_fin"] = mois_fin + " " + annee_fin sem["titremois"] = "%s %s (%s - %s)" % ( sem["titre_num"], sem.get("modalite", ""), sem["mois_debut"], sem["mois_fin"], ) sem["session_id"] = sco_formsemestre_edit.get_formsemestre_session_id( sem, F, parcours ) sem["etapes"] = read_formsemestre_etapes(sem["formsemestre_id"]) sem["etapes_apo_str"] = formsemestre_etape_apo_str(sem) sem["responsables"] = read_formsemestre_responsables(sem["formsemestre_id"]) def formsemestre_etape_apo_str(sem): "chaine décrivant le(s) codes étapes Apogée" return etapes_apo_str(sem["etapes"]) def etapes_apo_str(etapes): "Chaine decrivant une liste d'instance de ApoEtapeVDI" return ", ".join([str(x) for x in etapes]) def do_formsemestre_create(args, silent=False): "create a formsemestre" from app.models import ScolarNews from app.scodoc import sco_groups cnx = ndb.GetDBConnexion() formsemestre_id = _formsemestreEditor.create(cnx, args) if args["etapes"]: args["formsemestre_id"] = formsemestre_id write_formsemestre_etapes(args) if args["responsables"]: args["formsemestre_id"] = formsemestre_id write_formsemestre_responsables(args) # create default partition partition_id = sco_groups.partition_create( formsemestre_id, default=True, redirect=0, numero=1000000, # à la fin ) _ = sco_groups.create_group(partition_id, default=True) # news if "titre" not in args: args["titre"] = "sans titre" args["formsemestre_id"] = formsemestre_id args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args if not silent: ScolarNews.add( typ=ScolarNews.NEWS_SEM, text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args, url=args["url"], ) return formsemestre_id def do_formsemestre_edit(sem, cnx=None, **kw): """Apply modifications to formsemestre. Update etapes and resps. Invalidate cache.""" if not cnx: cnx = ndb.GetDBConnexion() _formsemestreEditor.edit(cnx, sem, **kw) write_formsemestre_etapes(sem) write_formsemestre_responsables(sem) sco_cache.invalidate_formsemestre( formsemestre_id=sem["formsemestre_id"] ) # > modif formsemestre def read_formsemestre_responsables(formsemestre_id: int) -> list[int]: # py3.9+ syntax """recupere liste des responsables de ce semestre :returns: liste d'id """ r = ndb.SimpleDictFetch( """SELECT responsable_id FROM notes_formsemestre_responsables WHERE formsemestre_id = %(formsemestre_id)s """, {"formsemestre_id": formsemestre_id}, ) return [x["responsable_id"] for x in r] def write_formsemestre_responsables(sem): return _write_formsemestre_aux(sem, "responsables", "responsable_id") # ---------------------- Coefs des UE _formsemestre_uecoef_editor = ndb.EditableTable( "notes_formsemestre_uecoef", "formsemestre_uecoef_id", ("formsemestre_uecoef_id", "formsemestre_id", "ue_id", "coefficient"), ) formsemestre_uecoef_create = _formsemestre_uecoef_editor.create formsemestre_uecoef_edit = _formsemestre_uecoef_editor.edit formsemestre_uecoef_list = _formsemestre_uecoef_editor.list formsemestre_uecoef_delete = _formsemestre_uecoef_editor.delete def do_formsemestre_uecoef_edit_or_create(cnx, formsemestre_id, ue_id, coef): "modify or create the coef" coefs = formsemestre_uecoef_list( cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue_id} ) if coefs: formsemestre_uecoef_edit( cnx, args={ "formsemestre_uecoef_id": coefs[0]["formsemestre_uecoef_id"], "coefficient": coef, }, ) else: formsemestre_uecoef_create( cnx, args={ "formsemestre_id": formsemestre_id, "ue_id": ue_id, "coefficient": coef, }, ) def do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id): "delete coef for this (ue,sem)" coefs = formsemestre_uecoef_list( cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue_id} ) if coefs: formsemestre_uecoef_delete(cnx, coefs[0]["formsemestre_uecoef_id"]) def read_formsemestre_etapes(formsemestre_id): # OBSOLETE """recupere liste des codes etapes associés à ce semestre :returns: liste d'instance de ApoEtapeVDI """ r = ndb.SimpleDictFetch( """SELECT etape_apo FROM notes_formsemestre_etapes WHERE formsemestre_id = %(formsemestre_id)s ORDER BY etape_apo """, {"formsemestre_id": formsemestre_id}, ) return [ApoEtapeVDI(x["etape_apo"]) for x in r if x["etape_apo"]] def write_formsemestre_etapes(sem): return _write_formsemestre_aux(sem, "etapes", "etape_apo") def _write_formsemestre_aux(sem, fieldname, valuename): """fieldname: 'etapes' ou 'responsables' valuename: 'etape_apo' ou 'responsable_id' """ if not fieldname in sem: return # uniquify values = set([str(x) for x in sem[fieldname]]) cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) tablename = "notes_formsemestre_" + fieldname try: cursor.execute( "DELETE from " + tablename + " where formsemestre_id = %(formsemestre_id)s", {"formsemestre_id": sem["formsemestre_id"]}, ) for item in values: if item: cursor.execute( "INSERT INTO " + tablename + " (formsemestre_id, " + valuename + ") VALUES (%(formsemestre_id)s, %(" + valuename + ")s)", {"formsemestre_id": sem["formsemestre_id"], valuename: item}, ) except: log("Warning: exception in write_formsemestre_aux !") cnx.rollback() raise cnx.commit() def sem_set_responsable_name(sem): "ajoute champs responsable_name" from app.scodoc import sco_users sem["responsable_name"] = ", ".join( [ sco_users.user_info(responsable_id)["nomprenom"] for responsable_id in sem["responsables"] ] ) def sem_in_semestre_scolaire( sem, year=False, periode=None, mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE, mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2, ) -> bool: """Vrai si la date du début du semestre est dans la période indiquée (1,2,0) du semestre `periode` de l'année scolaire indiquée (ou, à défaut, de celle en cours). La période utilise les même conventions que semset["sem_id"]; * 1 : première période * 2 : deuxième période * 0 ou période non précisée: annualisé (donc inclut toutes les périodes) ) """ if not year: year = scu.annee_scolaire() # n'utilise pas le jour pivot jour_pivot_annee = jour_pivot_periode = 1 # calcule l'année universitaire et la période sem_annee, sem_periode = FormSemestre.comp_periode( datetime.datetime.fromisoformat(sem["date_debut_iso"]), mois_pivot_annee, mois_pivot_periode, jour_pivot_annee, jour_pivot_periode, ) if periode is None or periode == 0: return sem_annee == year return sem_annee == year and sem_periode == periode def sem_in_annee_scolaire(sem, year=False): """Test si sem appartient à l'année scolaire year (int). N'utilise que la date de début, pivot au 1er août. Si année non specifiée, année scolaire courante """ return sem_in_semestre_scolaire(sem, year, periode=0) def sem_est_courant(sem): # -> FormSemestre.est_courant """Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)""" now = time.strftime("%Y-%m-%d") debut = ndb.DateDMYtoISO(sem["date_debut"]) fin = ndb.DateDMYtoISO(sem["date_fin"]) return debut <= now <= fin def scodoc_get_all_unlocked_sems(): """Liste de tous les semestres non verrouillés de _tous_ les départements (utilisé pour rapports d'activités) """ cur_dept = g.scodoc_dept depts = Departement.query.filter_by(visible=True).all() semdepts = [] try: for dept in depts: app.set_sco_dept(dept.acronym) semdepts += [(sem, dept) for sem in do_formsemestre_list() if sem["etat"]] finally: app.set_sco_dept(cur_dept) return semdepts def table_formsemestres( sems: list[dict], columns_ids=(), sup_columns_ids=(), html_title="<h2>Semestres</h2>", html_next_section="", ): """Une table presentant des semestres""" for sem in sems: sem_set_responsable_name(sem) sem["_titre_num_target"] = url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=sem["formsemestre_id"], ) if not columns_ids: columns_ids = ( "etat", "modalite", "mois_debut", "mois_fin", "titre_num", "responsable_name", "etapes_apo_str", ) columns_ids += sup_columns_ids titles = { "modalite": "", "mois_debut": "Début", "mois_fin": "Fin", "titre_num": "Semestre", "responsable_name": "Resp.", "etapes_apo_str": "Apo.", } if sems: preferences = sco_preferences.SemPreferences(sems[0]["formsemestre_id"]) else: preferences = sco_preferences.SemPreferences() tab = GenTable( columns_ids=columns_ids, rows=sems, titles=titles, html_class="table_leftalign", html_sortable=True, html_title=html_title, html_next_section=html_next_section, html_empty_element="<p><em>aucun résultat</em></p>", page_title="Semestres", preferences=preferences, ) return tab def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False) -> list[dict]: """Liste des semestres de cette etape, pour l'annee scolaire indiquée (sinon, pour toutes). """ ds = {} # formsemestre_id : sem if etape_apo: sems = do_formsemestre_list(args={"etape_apo": etape_apo}) for sem in sems: if annee_scolaire: # restriction annee scolaire if sem_in_annee_scolaire(sem, year=int(annee_scolaire)): ds[sem["formsemestre_id"]] = sem sems = list(ds.values()) else: sems = do_formsemestre_list() if annee_scolaire: sems = [ sem for sem in sems if sem_in_annee_scolaire(sem, year=int(annee_scolaire)) ] sems.sort(key=lambda s: (s["modalite"], s["dateord"])) return sems def view_formsemestre_by_etape(etape_apo=None, format="html"): """Affiche table des semestres correspondants à l'étape""" if etape_apo: html_title = f"""<h2>Semestres courants de l'étape <tt>{etape_apo}</tt></h2>""" else: html_title = """<h2>Semestres courants</h2>""" tab = table_formsemestres( list_formsemestre_by_etape( etape_apo=etape_apo, annee_scolaire=scu.annee_scolaire() ), html_title=html_title, html_next_section="""<form action="view_formsemestre_by_etape"> Etape: <input name="etape_apo" type="text" size="8"></input> </form>""", ) tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "") return tab.make_page(format=format) def sem_has_etape(sem, code_etape): return code_etape in sem["etapes"]