# -*- 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 # ############################################################################## """Gestion des ensembles de semestres: class SemSet: un ensemble de semestres d'un département, à exporter ves Apogée. En principe de la meme annee scolaire. SemSet.annees_scolaires() : les annees scolaires. e.g. [ 2015, 2016 ], ou le plus souvent, une seule: [2016] SemSet.list_etapes(): listes des étapes apogee et vdi des semestres (instances de ApoEtapeVDI) SemSet.add(sem): ajoute un semestre à l'ensemble sem_set_list() """ import flask from flask import g, url_for from app import db, log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.scodoc import html_sco_header from app.scodoc import sco_etape_apogee from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_status from app.scodoc import sco_portal_apogee from app.scodoc import sco_preferences from app.scodoc.gen_tables import GenTable from app.scodoc.sco_etape_bilan import EtapeBilan from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_vdi import ApoEtapeVDI import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu _semset_editor = ndb.EditableTable( "notes_semset", "semset_id", ("semset_id", "title", "annee_scolaire", "sem_id"), filter_dept=True, ) semset_create = _semset_editor.create semset_edit = _semset_editor.edit semset_list = _semset_editor.list semset_delete = _semset_editor.delete class SemSet(dict): def __init__(self, semset_id=None, title="", annee_scolaire="", sem_id=""): """Load and init, or, if semset_id is not specified, create""" super().__init__() if not annee_scolaire and not semset_id: # on autorise annee_scolaire null si sem_id pour pouvoir lire les anciens semsets # mal construits... raise ScoValueError("Année scolaire invalide !") self.semset_id = semset_id self["semset_id"] = semset_id self.sems = [] self.formsemestres = [] # modernisation en cours... self.is_apc = False self.formsemestre_ids = set() cnx = ndb.GetDBConnexion() if semset_id: # read existing set semsets = semset_list(cnx, args={"semset_id": semset_id}) if not semsets: raise ScoValueError(f"Ensemble inexistant ! (semset {semset_id})") self["title"] = semsets[0]["title"] self["annee_scolaire"] = semsets[0]["annee_scolaire"] self["sem_id"] = semsets[0]["sem_id"] r = ndb.SimpleDictFetch( "SELECT formsemestre_id FROM notes_semset_formsemestre WHERE semset_id = %(semset_id)s", {"semset_id": semset_id}, ) if r: self.formsemestre_ids = {x["formsemestre_id"] for x in r} # a set else: # create a new empty set self.semset_id = semset_create( cnx, {"title": title, "annee_scolaire": annee_scolaire, "sem_id": sem_id}, ) log(f"created new semset_id={self.semset_id}") self.load_sems() # Analyse des semestres pour construire le bilan par semestre et par étape self.bilan = EtapeBilan() for sem in self.sems: self.bilan.add_sem(sem) def delete(self): """delete""" cnx = ndb.GetDBConnexion() semset_delete(cnx, self.semset_id) def edit(self, args): cnx = ndb.GetDBConnexion() semset_edit(cnx, args) def load_sems(self): """Load formsemestres""" self.sems = [] self.formsemestres = [] for formsemestre_id in self.formsemestre_ids: formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) self.formsemestres.append(formsemestre) sem = sco_formsemestre.get_formsemestre(formsemestre_id) self.sems.append(sem) self["is_apc"] = formsemestre.formation.is_apc() if self.sems: self["date_debut"] = min([sem["date_debut_iso"] for sem in self.sems]) self["date_fin"] = max([sem["date_fin_iso"] for sem in self.sems]) else: self["date_debut"] = "" self["date_fin"] = "" self["etapes"] = self.list_etapes() self["semtitles"] = [sem["titre_num"] for sem in self.sems] # Construction du ou des lien(s) vers le semestre self["semlinks"] = [ formsemestre.html_link_status() for formsemestre in self.formsemestres ] self["semtitles_str"] = "<br>".join(self["semlinks"]) def fill_formsemestres(self): for sem in self.sems: sco_formsemestre_status.fill_formsemestre(sem) ets = sco_etape_apogee.apo_get_sem_etapes(sem) sem["etapes_apo_str"] = sco_formsemestre.etapes_apo_str(sorted(list(ets))) def add(self, formsemestre_id): "Ajoute ce semestre à l'ensemble" # check for valid formsemestre_id formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) # check if formsemestre_id in self.formsemestre_ids: return # already there if formsemestre_id not in [ sem["formsemestre_id"] for sem in self.list_possible_sems() ]: raise ValueError( f"can't add {formsemestre_id} to set {self.semset_id}: incompatible sem_id" ) if self.formsemestre_ids and formsemestre.formation.is_apc() != self["is_apc"]: raise ScoValueError( """On ne peut pas mélanger des semestres BUT/APC avec des semestres ordinaires dans le même export.""", dest_url=url_for( "notes.apo_semset_maq_status", scodoc_dept=g.scodoc_dept, semset_id=self.semset_id, ), ) ndb.SimpleQuery( """INSERT INTO notes_semset_formsemestre (formsemestre_id, semset_id) VALUES (%(formsemestre_id)s, %(semset_id)s) """, { "formsemestre_id": formsemestre_id, "semset_id": self.semset_id, }, ) self.load_sems() # update our list def remove(self, formsemestre_id): ndb.SimpleQuery( """DELETE FROM notes_semset_formsemestre WHERE semset_id=%(semset_id)s AND formsemestre_id=%(formsemestre_id)s """, {"formsemestre_id": formsemestre_id, "semset_id": self.semset_id}, ) self.load_sems() # update our list def annees_scolaires(self): """Les annees scolaires. e.g. [ 2015, 2016 ], ou le plus souvent, une seule: [2016] L'année scolaire est l'année de début du semestre (2015 pour 2015-2016) """ annees = list(set([int(s["annee_debut"]) for s in self.sems])) annees.sort() return annees def list_etapes(self): """Listes triée des étapes Apogée des semestres (instances de ApoEtapeVDI). Chaque étape apparait une seule fois, dans sa forme la plus générale. Si on a [ 'V1RT', 'V1RT!111' ], le résultat sera [ 'V1RT' ] Si on a [ 'V1RT!111', 'V1RT!112' ], le résultat sera [ 'V1RT!111', 'V1RT!112' ] """ D = {} # { etape : { versions vdi } } for s in self.sems: for et in s["etapes"]: if et: if et.etape in D: D[et.etape].add(et.vdi) else: D[et.etape] = {et.vdi} # enlève les versions excédentaires: for etape in D: if "" in D[etape]: D[etape] = [""] # forme liste triée d'instances: etapes = [] for etape in D: for vdi in D[etape]: etapes.append(ApoEtapeVDI(etape=etape, vdi=vdi)) etapes.sort() return etapes def list_possible_sems(self): """List sems that can be added to this set""" sems = sco_formsemestre.do_formsemestre_list() # remove sems already here: sems = [ sem for sem in sems if sem["formsemestre_id"] not in self.formsemestre_ids ] # filter annee, sem_id: # Remplacement du filtre de proposition des semestres potentiels # au lieu de la parité (sem 1 et 3 / sem 2 et 4) on filtre sur la date de # debut du semestre: ceci permet d'ajouter les semestres décalés if self["annee_scolaire"]: sems = [ sem for sem in sems if sco_formsemestre.sem_in_semestre_scolaire( sem, year=self["annee_scolaire"], # Indiquer ici les valeur des dates pivots année et période periode=self["sem_id"], ) ] return sems def load_etuds(self): self["etuds_without_nip"] = set() # etudids self["jury_ok"] = True self["jury_nb_missing"] = 0 is_apc = None for sem in self.sems: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if is_apc is not None and is_apc != nt.is_apc: raise ScoValueError( "Incohérence: semestre APC (BUT) et ordinaires mélangés !" ) else: is_apc = nt.is_apc sem["etuds"] = list(nt.identdict.values()) sem["nips"] = {e["code_nip"] for e in sem["etuds"] if e["code_nip"]} sem["etuds_without_nip"] = { e["etudid"] for e in sem["etuds"] if not e["code_nip"] } self["etuds_without_nip"] |= sem["etuds_without_nip"] sem["etudids_no_jury"] = nt.etudids_without_decisions() sem["jury_ok"] = not sem["etudids_no_jury"] self["jury_ok"] &= sem["jury_ok"] self["jury_nb_missing"] += len(sem["etudids_no_jury"]) self["is_apc"] = bool(is_apc) def html_descr(self): """Short HTML description""" H = [ """<span class="box_title">Ensemble de semestres %(title)s</span>""" % self ] if self["annee_scolaire"]: H.append("<p>Année scolaire: %(annee_scolaire)s</p>" % self) else: H.append( "<p>Année(s) scolaire(s) présentes: %s" % ", ".join([str(x) for x in self.annees_scolaires()]) ) if len(self.annees_scolaires()) > 1: H.append( ' <span class="redboldtext">(attention, plusieurs années !)</span>' ) H.append("</p>") if self["sem_id"] == 1: periode = "1re période (S1, S3, S5)" elif self["sem_id"] == 2: periode = "2de période (S2, S4, S6)" else: periode = "non semestrialisée (LP, ...). Incompatible avec BUT." H.append( f""" <p>Période: <b>{periode}</b></p> <p>Etapes: <tt>{sco_formsemestre.etapes_apo_str(self.list_etapes())}</tt></p> <h4>Semestres de l'ensemble:</h4><ul class="semset_listsems"> """ ) for sem in self.sems: H.append( """<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a> %(mois_debut)s - %(mois_fin)s""" % sem ) H.append( ' <a class="stdlink" href="do_semset_remove_sem?semset_id=%s&formsemestre_id=%s">(retirer)</a>' % (self["semset_id"], sem["formsemestre_id"]) ) H.append( "<br>Etapes: <tt>%(etapes_apo_str)s</tt>, %(nbinscrits)s inscrits" % sem ) H.append("<br>Elément Apogée année: ") if sem["elt_annee_apo"]: H.append("<tt>%(elt_annee_apo)s</tt>" % sem) else: H.append('<span style="color: red;">manquant</span>') H.append("<br>Elément Apogée semestre: ") if sem["elt_sem_apo"]: H.append("<tt>%(elt_sem_apo)s</tt>" % sem) else: H.append('<span style="color: red;">manquant</span>') H.append("</br><em>vérifier les semestres antécédents !</em>") H.append("</li>") return "\n".join(H) def html_form_sems(self): """HTML form to manage sems""" H = [] possible_sems = self.list_possible_sems() if possible_sems: menu_sem = """<select name="formsemestre_id"> <option value="" selected>(semestre)</option>""" for sem in possible_sems: menu_sem += ( """<option value="%(formsemestre_id)s">%(titreannee)s</option>\n""" % sem ) menu_sem += """</select>""" H.append( '<form action="do_semset_add_sem" method="post">Ajouter un semestre:' ) H.append(menu_sem) H.append( '<input type="hidden" name="semset_id" value="%s"/>' % self.semset_id ) H.append('<input type="submit" value="Ajouter"/>') H.append("</form>") else: H.append("<em>pas de semestres à ajouter</em>") return "\n".join(H) def html_diagnostic(self): """Affichage de la partie Effectifs et Liste des étudiants (actif seulement si un portail est configuré) """ return self.bilan.html_diagnostic() return "" def get_semsets_list(): """Liste de tous les semsets Trié par date_debut, le plus récent d'abord """ cnx = ndb.GetDBConnexion() L = [] for s in semset_list(cnx): L.append(SemSet(semset_id=s["semset_id"])) L.sort(key=lambda s: s["date_debut"], reverse=True) return L def do_semset_create(title="", annee_scolaire=None, sem_id=None): """Create new setset""" log( "do_semset_create(title=%s, annee_scolaire=%s, sem_id=%s)" % (title, annee_scolaire, sem_id) ) SemSet(title=title, annee_scolaire=annee_scolaire, sem_id=sem_id) return flask.redirect("semset_page") def do_semset_delete(semset_id, dialog_confirmed=False): """Delete a semset""" if not semset_id: raise ScoValueError("empty semset_id") s = SemSet(semset_id=semset_id) if not dialog_confirmed: return scu.confirm_dialog( "<h2>Suppression de l'ensemble %(title)s ?</h2>" % s, dest_url="", parameters={"semset_id": semset_id}, cancel_url="semset_page", ) s.delete() return flask.redirect("semset_page") def edit_semset_set_title(id=None, value=None): """Change title of semset""" title = value.strip() if not id: raise ScoValueError("empty semset_id") SemSet(semset_id=id) cnx = ndb.GetDBConnexion() semset_edit(cnx, {"semset_id": id, "title": title}) return title def do_semset_add_sem(semset_id, formsemestre_id): """Add a sem to a semset""" if not semset_id: raise ScoValueError("empty semset_id") if formsemestre_id == "": raise ScoValueError("pas de semestre choisi !") semset = SemSet(semset_id=semset_id) semset.add(formsemestre_id) return flask.redirect( url_for( "notes.apo_semset_maq_status", scodoc_dept=g.scodoc_dept, semset_id=semset_id, ) ) def do_semset_remove_sem(semset_id, formsemestre_id): """Add a sem to a semset""" if not semset_id: raise ScoValueError("empty semset_id") s = SemSet(semset_id) s.remove(formsemestre_id) return flask.redirect("apo_semset_maq_status?semset_id=%s" % semset_id) # ---------------------------------------- def semset_page(fmt="html"): """Page avec liste semsets: Table avec : date_debut date_fin titre liste des semestres """ semsets = get_semsets_list() for s in semsets: s["suppress"] = scu.icontag( "delete_small_img", border="0", alt="supprimer", title="Supprimer" ) s["_suppress_target"] = "do_semset_delete?semset_id=%s" % (s["semset_id"]) s["export_link"] = "Export Apogée" s["_export_link_target"] = "apo_semset_maq_status?semset_id=%s" % s.semset_id s["_export_link_link_class"] = "stdlink" # Le lien associé au nom de semestre redirigeait vers le semset # (remplacé par n liens vers chacun des semestres) # s['_semtitles_str_target'] = s['_export_link_target'] # Experimental: s["_title_td_attrs"] = ( 'class="inplace_edit" data-url="edit_semset_set_title" id="%s"' % (s["semset_id"]) ) tab = GenTable( rows=semsets, titles={ "annee_scolaire": "Année scolaire", "sem_id": "P", "date_debut": "Début", "date_fin": "Fin", "title": "Titre", "export_link": "", "semtitles_str": "semestres", }, columns_ids=[ "suppress", "annee_scolaire", "sem_id", "date_debut", "date_fin", "title", "export_link", "semtitles_str", ], html_sortable=True, html_class="table_leftalign", filename="semsets", preferences=sco_preferences.SemPreferences(), table_id="table-semsets", ) if fmt != "html": return tab.make_page(fmt=fmt) page_title = "Ensembles de semestres" H = [ html_sco_header.sco_header( page_title=page_title, init_qtip=True, javascripts=["libjs/jinplace-1.2.1.min.js"], ), """<script>$(function() { $('.inplace_edit').jinplace(); }); </script>""", "<h2>%s</h2>" % page_title, ] H.append(tab.html()) annee_courante = int(scu.annee_scolaire()) menu_annee = "\n".join( [ '<option value="%s">%s</option>' % (i, i) for i in range(2014, annee_courante + 1) ] ) H.append( """ <div style="margin-top:20px;"> <h4>Création nouvel ensemble</h4> <form method="POST" action="do_semset_create"> <select name="annee_scolaire"> <option value="" selected>(année scolaire)</option>""" ) H.append(menu_annee) H.append( """</select> <select name="sem_id"> <option value="1">1re période (S1, S3)</option> <option value="2">2de période (S2, S4)</option> <option value="0">non semestrialisée (LP, ... mais pas pour le BUT !)</option> </select> <input type="text" name="title" size="32"/> <input type="submit" value="Créer"/> </form></div> """ ) H.append( """ <div> <h4>Autres opérations:</h4> <ul> <li><a class="stdlink" href="scodoc_table_results"> Table des résultats de tous les semestres </a></li> <li><a class="stdlink" href="apo_compare_csv_form"> Comparaison de deux maquettes Apogée </a></li> </ul> </div> """ ) return "\n".join(H) + html_sco_header.sco_footer()