# -*- 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
#
##############################################################################
"""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.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 import log
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.formsemestre_ids = []
cnx = ndb.GetDBConnexion()
if semset_id: # read existing set
L = semset_list(cnx, args={"semset_id": semset_id})
if not L:
raise ScoValueError(f"Ensemble inexistant ! (semset {semset_id})")
self["title"] = L[0]["title"]
self["annee_scolaire"] = L[0]["annee_scolaire"]
self["sem_id"] = L[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 = []
for formsemestre_id in self.formsemestre_ids:
self.sems.append(sco_formsemestre.get_formsemestre(formsemestre_id))
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"] = [
f"""{sem["titreannee"]}
"""
for sem in self.sems
]
self["semtitles_str"] = "
".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:
formsemestre_1 = formsemestre.query.get(self.formsemestre_ids[0])
if formsemestre.formation.is_apc() != formsemestre_1.formation.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 = [
"""Ensemble de semestres %(title)s""" % self
]
if self["annee_scolaire"]:
H.append("
Année scolaire: %(annee_scolaire)s
" % self) else: H.append( "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( ' (attention, plusieurs années !)' ) H.append("
") if self["sem_id"] == 1: periode = "1re période (S1, S3)" elif self["sem_id"] == 1: periode = "2de période (S2, S4)" else: periode = "non semestrialisée (LP, ...). Incompatible avec BUT." H.append( f"""Période: {periode}
Etapes: {sco_formsemestre.etapes_apo_str(self.list_etapes())}