Merge branch 'entreprises' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises

This commit is contained in:
Arthur ZHU 2022-07-07 14:46:41 +02:00
commit 0140c404ea
15 changed files with 329 additions and 199 deletions

View File

@ -450,7 +450,7 @@ def etudiant_bulletin_semestre(
app.set_sco_dept(dept.acronym) app.set_sco_dept(dept.acronym)
return sco_bulletins.get_formsemestre_bulletin_etud_json( return sco_bulletins.get_formsemestre_bulletin_etud_json(
formsemestre, etud, version formsemestre, etud, version=version
) )

View File

@ -68,7 +68,7 @@ from flask import g, url_for
from app import db from app import db
from app import log from app import log
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem from app.comp import inscr_mod, res_sem
from app.models import formsemestre from app.models import formsemestre
from app.models.but_refcomp import ( from app.models.but_refcomp import (
@ -219,15 +219,16 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"le 1er semestre de l'année scolaire considérée (S1, S3, S5)" "le 1er semestre de l'année scolaire considérée (S1, S3, S5)"
self.formsemestre_pair = formsemestre_pair self.formsemestre_pair = formsemestre_pair
"le second formsemestre de la même année scolaire (S2, S4, S6)" "le second formsemestre de la même année scolaire (S2, S4, S6)"
self.annee_but = ( formsemestre_last = formsemestre_pair or formsemestre_impair
(formsemestre_impair.semestre_id + 1) // 2 "le formsemestre le plus avancé dans cette année"
if formsemestre_impair
else (formsemestre_pair.semestre_id + 1) // 2 self.annee_but = (formsemestre_last.semestre_id + 1) // 2
)
"le rang de l'année dans le BUT: 1, 2, 3" "le rang de l'année dans le BUT: 1, 2, 3"
assert self.annee_but in (1, 2, 3) assert self.annee_but in (1, 2, 3)
self.rcues_annee = [] self.rcues_annee = []
"RCUEs de l'année" "RCUEs de l'année"
self.inscription_etat = etud.inscription_etat(formsemestre_last.id)
if self.formsemestre_impair is not None: if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by( self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id, etudid=self.etud.id,
@ -255,13 +256,17 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all
self.decisions_ues = { self.decisions_ues = {
ue.id: DecisionsProposeesUE(etud, formsemestre_impair, ue) ue.id: DecisionsProposeesUE(
etud, formsemestre_impair, ue, self.inscription_etat
)
for ue in self.ues_impair for ue in self.ues_impair
} }
"{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année" "{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année"
self.decisions_ues.update( self.decisions_ues.update(
{ {
ue.id: DecisionsProposeesUE(etud, formsemestre_pair, ue) ue.id: DecisionsProposeesUE(
etud, formsemestre_pair, ue, self.inscription_etat
)
for ue in self.ues_pair for ue in self.ues_pair
} }
) )
@ -291,8 +296,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
[rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()] [rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()]
) )
"le nb de comp. sous la barre de 8/20" "le nb de comp. sous la barre de 8/20"
# année ADM si toutes RCUE validées (sinon PASD) # année ADM si toutes RCUE validées (sinon PASD) et non DEM ou DEF
self.admis = self.nb_validables == self.nb_competences self.admis = (self.nb_validables == self.nb_competences) and (
self.inscription_etat == scu.INSCRIT
)
"vrai si l'année est réussie, tous niveaux validables" "vrai si l'année est réussie, tous niveaux validables"
self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2) self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2)
# Peut passer si plus de la moitié validables et tous > 8 # Peut passer si plus de la moitié validables et tous > 8
@ -310,6 +317,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.admis: if self.admis:
self.codes = [sco_codes.ADM] + self.codes self.codes = [sco_codes.ADM] + self.codes
self.explanation = expl_rcues self.explanation = expl_rcues
elif self.inscription_etat != scu.INSCRIT:
self.codes = [
sco_codes.DEM
if self.inscription_etat == scu.DEMISSION
else sco_codes.DEF,
# propose aussi d'autres codes, au cas où...
sco_codes.DEM
if self.inscription_etat != scu.DEMISSION
else sco_codes.DEF,
sco_codes.ABAN,
sco_codes.ABL,
sco_codes.EXCLU,
]
elif self.passage_de_droit: elif self.passage_de_droit:
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
self.explanation = expl_rcues self.explanation = expl_rcues
@ -482,6 +502,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
ue_impair, ue_impair,
self.formsemestre_pair, self.formsemestre_pair,
ue_pair, ue_pair,
self.inscription_etat,
) )
ues_impair_sans_rcue.discard(ue_impair.id) ues_impair_sans_rcue.discard(ue_impair.id)
break break
@ -509,7 +530,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
rcue = rc rcue = rc
break break
if rcue is not None: if rcue is not None:
dec_rcue = DecisionsProposeesRCUE(self, rcue) dec_rcue = DecisionsProposeesRCUE(self, rcue, self.inscription_etat)
rc_niveaux.append((dec_rcue, niveau.id)) rc_niveaux.append((dec_rcue, niveau.id))
# prévient les UE concernées :-) # prévient les UE concernées :-)
self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue) self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue)
@ -724,14 +745,26 @@ class DecisionsProposeesRCUE(DecisionsProposees):
] ]
def __init__( def __init__(
self, dec_prop_annee: DecisionsProposeesAnnee, rcue: RegroupementCoherentUE self,
dec_prop_annee: DecisionsProposeesAnnee,
rcue: RegroupementCoherentUE,
inscription_etat: str = scu.INSCRIT,
): ):
super().__init__(etud=dec_prop_annee.etud) super().__init__(etud=dec_prop_annee.etud)
self.rcue = rcue self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = [] self.codes = []
return return
self.inscription_etat = inscription_etat
"inscription: I, DEM, DEF"
self.parcour = dec_prop_annee.parcour self.parcour = dec_prop_annee.parcour
if inscription_etat != scu.INSCRIT:
self.validation = None # cache toute validation
self.explanation = "non incrit (dem. ou déf.)"
self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
]
return
self.validation = rcue.query_validations().first() self.validation = rcue.query_validations().first()
if self.validation is not None: if self.validation is not None:
self.code_valide = self.validation.code self.code_valide = self.validation.code
@ -828,12 +861,27 @@ class DecisionsProposeesUE(DecisionsProposees):
etud: Identite, etud: Identite,
formsemestre: FormSemestre, formsemestre: FormSemestre,
ue: UniteEns, ue: UniteEns,
inscription_etat: str = scu.INSCRIT,
): ):
super().__init__(etud=etud) super().__init__(etud=etud)
self.formsemestre = formsemestre self.formsemestre = formsemestre
self.ue: UniteEns = ue self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None self.rcue: RegroupementCoherentUE = None
"Le rcu auquel est rattaché cette UE, ou None" "Le rcu auquel est rattaché cette UE, ou None"
self.inscription_etat = inscription_etat
"inscription: I, DEM, DEF"
if ue.type == sco_codes.UE_SPORT:
self.explanation = "UE bonus, pas de décision de jury"
self.codes = [] # aucun code proposé
return
if inscription_etat != scu.INSCRIT:
self.validation = None # cache toute validation
self.explanation = "non incrit (dem. ou déf.)"
self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
]
self.moy_ue = "-"
return
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non) # Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
# mais ici on a restreint au formsemestre donc une seule (prend la première) # mais ici on a restreint au formsemestre donc une seule (prend la première)
self.validation = ScolarFormSemestreValidation.query.filter_by( self.validation = ScolarFormSemestreValidation.query.filter_by(
@ -841,10 +889,6 @@ class DecisionsProposeesUE(DecisionsProposees):
).first() ).first()
if self.validation is not None: if self.validation is not None:
self.code_valide = self.validation.code self.code_valide = self.validation.code
if ue.type == sco_codes.UE_SPORT:
self.explanation = "UE bonus, pas de décision de jury"
self.codes = [] # aucun code proposé
return
# Moyenne de l'UE ? # Moyenne de l'UE ?
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
@ -863,6 +907,8 @@ class DecisionsProposeesUE(DecisionsProposees):
def compute_codes(self): def compute_codes(self):
"""Calcul des .codes attribuables et de l'explanation associée""" """Calcul des .codes attribuables et de l'explanation associée"""
if self.inscription_etat != scu.INSCRIT:
return
if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE): if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE):
self.codes.insert(0, sco_codes.ADM) self.codes.insert(0, sco_codes.ADM)
self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",) self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",)

View File

@ -409,7 +409,9 @@ def get_table_jury_but(
)}" class="stdlink"> )}" class="stdlink">
{"voir" if read_only else ("modif." if deca.code_valide else "saisie")} {"voir" if read_only else ("modif." if deca.code_valide else "saisie")}
décision</a> décision</a>
""", """
if deca.inscription_etat == scu.INSCRIT
else deca.inscription_etat,
"col_lien_saisie_but", "col_lien_saisie_but",
) )
rows.append(row) rows.append(row)

View File

@ -650,7 +650,7 @@ class ResultatsSemestre(ResultatsCache):
elif nb_ues_validables < len(ues_sans_bonus): elif nb_ues_validables < len(ues_sans_bonus):
row["_ues_validables_class"] += " moy_inf" row["_ues_validables_class"] += " moy_inf"
row["_ues_validables_order"] = nb_ues_validables # pour tri row["_ues_validables_order"] = nb_ues_validables # pour tri
if mode_jury: if mode_jury and self.validations:
dec_sem = self.validations.decisions_jury.get(etudid) dec_sem = self.validations.decisions_jury.get(etudid)
jury_code_sem = dec_sem["code"] if dec_sem else "" jury_code_sem = dec_sem["code"] if dec_sem else ""
idx = add_cell( idx = add_cell(

View File

@ -1419,7 +1419,7 @@ def get_import_donnees_file_sample():
@permission_required(Permission.RelationsEntreprisesExport) @permission_required(Permission.RelationsEntreprisesExport)
def import_donnees(): def import_donnees():
""" """
Permet d'importer des entreprises a l'aide d'un fichier excel (.xlsx) Permet d'importer des entreprises à partir d'un fichier excel (.xlsx)
""" """
form = ImportForm() form = ImportForm()
if form.validate_on_submit(): if form.validate_on_submit():
@ -1428,7 +1428,7 @@ def import_donnees():
Config.SCODOC_VAR_DIR, "tmp", secure_filename(file.filename) Config.SCODOC_VAR_DIR, "tmp", secure_filename(file.filename)
) )
file.save(file_path) file.save(file_path)
diag, lm = sco_excel.excel_file_to_list_are(file_path) diag, lm = sco_excel.excel_workbook_to_list(file_path)
os.remove(file_path) os.remove(file_path)
if lm is None or len(lm) < 2: if lm is None or len(lm) < 2:
flash("Veuillez utilisez la feuille excel à remplir") flash("Veuillez utilisez la feuille excel à remplir")

View File

@ -16,6 +16,7 @@ from app.models.ues import UniteEns
from app.models.formations import Formation from app.models.formations import Formation
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours as sco_codes from app.scodoc import sco_codes_parcours as sco_codes
from app.scodoc import sco_utils as scu
class ApcValidationRCUE(db.Model): class ApcValidationRCUE(db.Model):
@ -42,6 +43,7 @@ class ApcValidationRCUE(db.Model):
formsemestre_id = db.Column( formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
) )
"formsemestre pair du RCUE"
# Les deux UE associées à ce niveau: # Les deux UE associées à ce niveau:
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
@ -84,6 +86,7 @@ class RegroupementCoherentUE:
ue_1: UniteEns, ue_1: UniteEns,
formsemestre_2: FormSemestre, formsemestre_2: FormSemestre,
ue_2: UniteEns, ue_2: UniteEns,
inscription_etat: str,
): ):
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
@ -109,6 +112,11 @@ class RegroupementCoherentUE:
"semestre pair" "semestre pair"
self.ue_2 = ue_2 self.ue_2 = ue_2
# Stocke les moyennes d'UE # Stocke les moyennes d'UE
if inscription_etat != scu.INSCRIT:
self.moy_rcue = None
self.moy_ue_1 = self.moy_ue_2 = "-"
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
return
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]: if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id] self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
@ -201,8 +209,9 @@ class RegroupementCoherentUE:
return None return None
# unused
def find_rcues( def find_rcues(
formsemestre: FormSemestre, ue: UniteEns, etud: Identite formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
) -> list[RegroupementCoherentUE]: ) -> list[RegroupementCoherentUE]:
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
ce semestre pour cette UE. ce semestre pour cette UE.
@ -250,7 +259,9 @@ def find_rcues(
other_ue = UniteEns.query.get(ue_id) other_ue = UniteEns.query.get(ue_id)
other_formsemestre = FormSemestre.query.get(formsemestre_id) other_formsemestre = FormSemestre.query.get(formsemestre_id)
rcues.append( rcues.append(
RegroupementCoherentUE(etud, formsemestre, ue, other_formsemestre, other_ue) RegroupementCoherentUE(
etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
)
) )
# safety check: 1 seul niveau de comp. concerné: # safety check: 1 seul niveau de comp. concerné:
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1 assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1

View File

@ -58,7 +58,6 @@ from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_photos
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury from app.scodoc import sco_pvjury
from app.scodoc import sco_users from app.scodoc import sco_users
@ -66,15 +65,6 @@ import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType, fmt_note from app.scodoc.sco_utils import ModuleType, fmt_note
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
# ----- CLASSES DE BULLETINS DE NOTES
from app.scodoc import sco_bulletins_standard
from app.scodoc import sco_bulletins_legacy
# import sco_bulletins_example # format exemple (à désactiver en production)
# ... ajouter ici vos modules ...
from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun
def get_formsemestre_bulletin_etud_json( def get_formsemestre_bulletin_etud_json(
formsemestre: FormSemestre, formsemestre: FormSemestre,

View File

@ -92,7 +92,6 @@ def formsemestre_bulletinetud_published_dict(
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
d = {"type": "classic", "version": "0"} d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing: if (not sem["bul_hide_xml"]) or force_publishing:
published = True published = True
else: else:
@ -134,6 +133,7 @@ def formsemestre_bulletinetud_published_dict(
) )
d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients
# Disponible pour publication ? # Disponible pour publication ?
d["publie"] = published
if not published: if not published:
return d # stop ! return d # stop !
@ -364,8 +364,35 @@ def formsemestre_bulletinetud_published_dict(
return d return d
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False): def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict:
"dict avec decision pour bulletins json" """dict avec decision pour bulletins json
- decision : décision semestre
- decision_ue : list des décisions UE
- situation
with_decision donne les décision même si bul_show_decision est faux.
Exemple:
{
'autorisation_inscription': [{'semestre_id': 4}],
'decision': {'code': 'ADM',
'compense_formsemestre_id': None,
'date': '2022-01-21',
'etat': 'I'},
'decision_ue': [
{
'acronyme': 'UE31',
'code': 'ADM',
'ects': 16.0,
'numero': 23,
'titre': 'Approfondissement métiers',
'ue_id': 1787
},
...
],
'situation': 'Inscrit le 25/06/2021. Décision jury: Validé. UE acquises: '
'UE31, UE32. Diplôme obtenu.'}
"""
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
d = {} d = {}

View File

@ -40,10 +40,9 @@ from openpyxl.comments import Comment
from openpyxl import Workbook, load_workbook from openpyxl import Workbook, load_workbook
from openpyxl.cell import WriteOnlyCell from openpyxl.cell import WriteOnlyCell
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from openpyxl.worksheet.worksheet import Worksheet
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import notesdb
from app.scodoc import sco_preferences
from app import log from app import log
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -593,71 +592,87 @@ def excel_feuille_saisie(e, titreannee, description, lines):
def excel_bytes_to_list(bytes_content): def excel_bytes_to_list(bytes_content):
try: try:
filelike = io.BytesIO(bytes_content) filelike = io.BytesIO(bytes_content)
return _excel_to_list(filelike) except Exception as exc:
except:
raise ScoValueError( raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible ! """Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..) Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..)
""" """
) ) from exc
return _excel_to_list(filelike)
def excel_file_to_list(filename): def excel_file_to_list(filename):
try: try:
return _excel_to_list(filename) return _excel_to_list(filename)
except: except Exception as exc:
raise ScoValueError( raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible ! """Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
""" """
) ) from exc
def excel_file_to_list_are(filename): def excel_workbook_to_list(filename):
try: try:
return _excel_to_list_are(filename) return _excel_workbook_to_list(filename)
except: except Exception as exc:
raise ScoValueError( raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible ! """Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
""" """
) ) from exc
def _open_workbook(filelike, dump_debug=False) -> Workbook:
"""Open document.
On error, if dump-debug is True, dump data in /tmp for debugging purpose
"""
try:
workbook = load_workbook(filename=filelike, read_only=True, data_only=True)
except Exception as exc:
log("Excel_to_list: failure to import document")
if dump_debug:
dump_filename = "/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX
log(f"Dumping problemetic file on {dump_filename}")
with open(dump_filename, "wb") as f:
f.write(filelike)
raise ScoValueError(
"Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel xlsx !"
) from exc
return workbook
def _excel_to_list(filelike): def _excel_to_list(filelike):
"""returns list of list """returns list of list"""
convert_to_string is a conversion function applied to all non-string values (ie numbers) workbook = _open_workbook(filelike)
"""
try:
wb = load_workbook(filename=filelike, read_only=True, data_only=True)
except:
log("Excel_to_list: failure to import document")
with open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb") as f:
f.write(filelike)
raise ScoValueError(
"Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !"
)
diag = [] # liste de chaines pour former message d'erreur diag = [] # liste de chaines pour former message d'erreur
# n'utilise que la première feuille if len(workbook.get_sheet_names()) < 1:
if len(wb.get_sheet_names()) < 1:
diag.append("Aucune feuille trouvée dans le classeur !") diag.append("Aucune feuille trouvée dans le classeur !")
return diag, None return diag, None
if len(wb.get_sheet_names()) > 1: # n'utilise que la première feuille:
if len(workbook.get_sheet_names()) > 1:
diag.append("Attention: n'utilise que la première feuille du classeur !") diag.append("Attention: n'utilise que la première feuille du classeur !")
sheet_name = workbook.get_sheet_names()[0]
ws = workbook[sheet_name]
matrix, diag_sheet = _excel_sheet_to_list(ws, sheet_name)
diag += diag_sheet
return diag, matrix
def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list]:
"""read a spreadsheet sheet, and returns:
- diag : a list of strings (error messages aimed at helping the user)
- a list of lists: the spreadsheet cells
"""
diag = []
# fill matrix # fill matrix
sheet_name = wb.get_sheet_names()[0]
ws = wb.get_sheet_by_name(sheet_name)
sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace")
values = {} values = {}
for row in ws.iter_rows(): for row in sheet.iter_rows():
for cell in row: for cell in row:
if cell.value is not None: if cell.value is not None:
values[(cell.row - 1, cell.column - 1)] = str(cell.value) values[(cell.row - 1, cell.column - 1)] = str(cell.value)
if not values: if not values:
diag.append( diag.append(f"Aucune valeur trouvée dans la feuille {sheet_name} !")
"Aucune valeur trouvée dans la feuille %s !"
% sheet_name.decode(scu.SCO_ENCODING)
)
return diag, None return diag, None
indexes = list(values.keys()) indexes = list(values.keys())
# search numbers of rows and cols # search numbers of rows and cols
@ -665,76 +680,38 @@ def _excel_to_list(filelike):
cols = [x[1] for x in indexes] cols = [x[1] for x in indexes]
nbcols = max(cols) + 1 nbcols = max(cols) + 1
nbrows = max(rows) + 1 nbrows = max(rows) + 1
m = [] matrix = []
for _ in range(nbrows): for _ in range(nbrows):
m.append([""] * nbcols) matrix.append([""] * nbcols)
for row_idx, col_idx in indexes: for row_idx, col_idx in indexes:
v = values[(row_idx, col_idx)] v = values[(row_idx, col_idx)]
# if isinstance(v, six.text_type): matrix[row_idx][col_idx] = v
# v = v.encode(scu.SCO_ENCODING, "backslashreplace") diag.append(f'Feuille "{sheet_name}", {len(matrix)} lignes')
# elif convert_to_string:
# v = convert_to_string(v) return diag, matrix
m[row_idx][col_idx] = v
diag.append(
'Feuille "%s", %d lignes' % (sheet_name.decode(scu.SCO_ENCODING), len(m))
)
# diag.append(str(M))
#
return diag, m
def _excel_to_list_are(filelike): def _excel_workbook_to_list(filelike):
"""returns list of list """Lit un classeur (workbook): chaque feuille est lue
convert_to_string is a conversion function applied to all non-string values (ie numbers) et est convertie en une liste de listes.
Returns:
- diag : a list of strings (error messages aimed at helping the user)
- a list of lists: the spreadsheet cells
""" """
try: workbook = _open_workbook(filelike)
wb = load_workbook(filename=filelike, read_only=True, data_only=True)
except:
log("Excel_to_list: failure to import document")
with open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb") as f:
f.write(filelike)
raise ScoValueError(
"Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !"
)
diag = [] # liste de chaines pour former message d'erreur diag = [] # liste de chaines pour former message d'erreur
if len(wb.get_sheet_names()) < 1: if len(workbook.get_sheet_names()) < 1:
diag.append("Aucune feuille trouvée dans le classeur !") diag.append("Aucune feuille trouvée dans le classeur !")
return diag, None return diag, None
lm = [] matrix_list = []
for sheet_name in wb.get_sheet_names(): for sheet_name in workbook.get_sheet_names():
# fill matrix # fill matrix
ws = wb.get_sheet_by_name(sheet_name) sheet = workbook.get_sheet_by_name(sheet_name)
sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace") matrix, diag_sheet = _excel_sheet_to_list(sheet, sheet_name)
values = {} diag += diag_sheet
for row in ws.iter_rows(): matrix_list.append(matrix)
for cell in row: return diag, matrix_list
if cell.value is not None:
values[(cell.row - 1, cell.column - 1)] = str(cell.value)
if not values:
diag.append(
"Aucune valeur trouvée dans la feuille %s !"
% sheet_name.decode(scu.SCO_ENCODING)
)
return diag, None
indexes = list(values.keys())
# search numbers of rows and cols
rows = [x[0] for x in indexes]
cols = [x[1] for x in indexes]
nbcols = max(cols) + 1
nbrows = max(rows) + 1
m = []
for _ in range(nbrows):
m.append([""] * nbcols)
for row_idx, col_idx in indexes:
v = values[(row_idx, col_idx)]
m[row_idx][col_idx] = v
diag.append(
'Feuille "%s", %d lignes' % (sheet_name.decode(scu.SCO_ENCODING), len(m))
)
lm.append(m)
return diag, lm
def excel_feuille_listeappel( def excel_feuille_listeappel(

View File

@ -35,13 +35,17 @@ from app.models.etudiants import Identite
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import db, log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.models.notes import etud_has_notes_attente from app.models.notes import etud_has_notes_attente
from app.models.validations import (
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
)
from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc.sco_codes_parcours import * from app.scodoc.sco_codes_parcours import *
@ -989,28 +993,32 @@ def do_formsemestre_validation_auto(formsemestre_id):
def formsemestre_validation_suppress_etud(formsemestre_id, etudid): def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
"""Suppression des decisions de jury pour un etudiant.""" """Suppression des décisions de jury pour un étudiant/formsemestre.
log("formsemestre_validation_suppress_etud( %s, %s)" % (formsemestre_id, etudid)) Efface toutes les décisions enregistrées concernant ce formsemestre et cet étudiant:
cnx = ndb.GetDBConnexion() code semestre, UEs, autorisations d'inscription
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) """
args = {"formsemestre_id": formsemestre_id, "etudid": etudid} log(f"formsemestre_validation_suppress_etud( {formsemestre_id}, {etudid})")
try:
# -- Validation du semestre et des UEs # Validations jury classiques (semestres, UEs, autorisations)
cursor.execute( for v in ScolarFormSemestreValidation.query.filter_by(
"""delete from scolar_formsemestre_validation etudid=etudid, formsemestre_id=formsemestre_id
where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s""", ):
args, db.session.delete(v)
) for v in ScolarAutorisationInscription.query.filter_by(
# -- Autorisations d'inscription etudid=etudid, origin_formsemestre_id=formsemestre_id
cursor.execute( ):
"""delete from scolar_autorisation_inscription db.session.delete(v)
where etudid = %(etudid)s and origin_formsemestre_id=%(formsemestre_id)s""", # Validations jury spécifiques BUT
args, for v in ApcValidationRCUE.query.filter_by(
) etudid=etudid, formsemestre_id=formsemestre_id
cnx.commit() ):
except: db.session.delete(v)
cnx.rollback() for v in ApcValidationAnnee.query.filter_by(
raise etudid=etudid, formsemestre_id=formsemestre_id
):
db.session.delete(v)
db.session.commit()
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
_invalidate_etud_formation_caches( _invalidate_etud_formation_caches(

View File

@ -224,9 +224,26 @@ class releveBUT extends HTMLElement {
<div class=abs>Non justifiées</div> <div class=abs>Non justifiées</div>
<div>${data.semestre.absences?.injustifie ?? "-"}</div> <div>${data.semestre.absences?.injustifie ?? "-"}</div>
<div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div> <div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div>
</div> </div>`;
<a class=photo href="${data.etudiant.fiche_url}"><img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"></a> if(data.semestre.decision_rcue.length){
`; output += `
<div>
<div class=enteteSemestre>RCUE</div><div></div>
${(()=>{
let output = "";
data.semestre.decision_rcue.forEach(competence=>{
output += `<div class=rang>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`;
})
return output;
})()}
</div>
</div>`
}
output += `
<a class=photo href="${data.etudiant.fiche_url}">
<img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0">
</a>`;
/*${data.semestre.groupes.map(groupe => { /*${data.semestre.groupes.map(groupe => {
return ` return `
<div> <div>
@ -240,9 +257,11 @@ class releveBUT extends HTMLElement {
}).join("") }).join("")
}*/ }*/
this.shadow.querySelector(".infoSemestre").innerHTML = output; this.shadow.querySelector(".infoSemestre").innerHTML = output;
if(data.semestre.decision_annee?.code){
/*if(data.semestre.decision_annee?.code){
this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code]; this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code];
} }*/
this.shadow.querySelector(".decision").innerHTML = data.semestre.situation || ""; this.shadow.querySelector(".decision").innerHTML = data.semestre.situation || "";
/*if (data.semestre.decision?.code) { /*if (data.semestre.decision?.code) {

View File

@ -1,9 +1,12 @@
<div class="but_doc_codes"> <div class="but_doc_codes">
<p><em>Ci-dessous la signification de chaque code est expliquée, <p><em>Ci-dessous la signification de chaque code est expliquée,
ainsi que la correspondance avec les codes préconisés par ainsi que la correspondance avec certains codes préconisés par
l'AMUE pour Apogée dans un document informel qui a circulé début l'AMUE et l'ADIUT pour Apogée.
2022 (les éventuelles erreurs n'engagent personne). </em>
</em></p> On distingue les codes ScoDoc (utilisés ci-dessus et dans les différentes
tables générées par ScoDoc) et leur transcription vers Apogée lors des exports
(transcription paramétrable par votre administrateur ScoDoc).
</p>
<div class="but_doc_section">Codes d'année</div> <div class="but_doc_section">Codes d'année</div>
<div class="but_doc"> <div class="but_doc">
<table> <table>
@ -230,4 +233,34 @@
</tr> </tr>
</table> </table>
</div> </div>
<div class="but_doc_section">Rappels de l'arrêté BUT (extraits)</div>
<div class="but_doc">
<ul>
<li>Au sein de chaque regroupement cohérent dUE, la compensation est intégrale.
Si une UE na pas été acquise en raison dune moyenne inférieure à 10,
cette UE sera acquise par compensation si et seulement si létudiant
a obtenu la moyenne au regroupement cohérent auquel lUE appartient.</li>
<li>La poursuite d'études dans un semestre pair dune même année est de droit
pour tout étudiant.
La poursuite détudes dans un semestre impair est possible
<em>si et seulement si</em> létudiant a obtenu :
<ul>
<li>la moyenne à plus de la moitié des regroupements cohérents dUE</li>
<li>et une moyenne égale ou supérieure à 8 sur 20 à chaque regroupement cohérent dUE.</li>
</ul>
</li>
<li>La poursuite d'études dans le semestre 5 nécessite de plus la validation de toutes les UE des
semestres 1 et 2 dans les conditions de validation des points 4.3 et 4.4, ou par décision de jury.</li>
</ul>
<b>Textes de référence:</b>
<ul>
<li><a href="https://www.enseignementsup-recherche.gouv.fr/fr/bo/21/Special4/ESRS2114777A.htm">Bulletin
officiel spécial n°4 du 17 juin 2021</a></li>
<li><a
href="https://cache.media.enseignementsup-recherche.gouv.fr//file/SPE4-MESRI-17-6-2021/19/4/SP4_ESR_17_6_2021_1413194.pdf">Version
pdf complète</a></li>
</ul>
</div>
</div> </div>

View File

@ -57,7 +57,7 @@ from app.models.ues import UniteEns
from app import api from app import api
from app import db from app import db
from app import models from app import models
from app.models import ScolarNews from app.models import ScolarNews, but_validations
from app.auth.models import User from app.auth.models import User
from app.but import apc_edit_ue, jury_but_recap from app.but import apc_edit_ue, jury_but_recap
from app.decorators import ( from app.decorators import (
@ -71,7 +71,7 @@ from app.views import notes_bp as bp
# --------------- # ---------------
from app.scodoc import sco_utils as scu from app.scodoc import sco_bulletins_json, sco_utils as scu
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm from app import log, send_scodoc_alarm
@ -2515,51 +2515,68 @@ def do_formsemestre_validation_auto(formsemestre_id):
def formsemestre_validation_suppress_etud( def formsemestre_validation_suppress_etud(
formsemestre_id, etudid, dialog_confirmed=False formsemestre_id, etudid, dialog_confirmed=False
): ):
"""Suppression des decisions de jury pour un etudiant.""" """Suppression des décisions de jury pour un étudiant."""
if not sco_permissions_check.can_validate_sem(formsemestre_id): if not sco_permissions_check.can_validate_sem(formsemestre_id):
return scu.confirm_dialog( return scu.confirm_dialog(
message="<p>Opération non autorisée pour %s</h2>" % current_user, message="<p>Opération non autorisée pour %s</h2>" % current_user,
dest_url=scu.ScoURL(), dest_url=scu.ScoURL(),
) )
etud = Identite.query.get_or_404(etudid)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc():
next_url = url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
)
else:
next_url = url_for(
"notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if not dialog_confirmed: if not dialog_confirmed:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] d = sco_bulletins_json.dict_decision_jury(
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) etudid, formsemestre_id, with_decisions=True
sem = formsemestre.to_dict() )
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) d.update(but_validations.dict_decision_jury(etud, formsemestre))
decision_jury = nt.get_etud_decision_sem(etudid)
if decision_jury: descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])]
existing = ( dec_annee = d.get("decision_annee")
"<p>Décision existante: %(code)s du %(event_date)s</p>" % decision_jury if dec_annee:
) descr_annee = dec_annee.get("code", "-")
else: else:
existing = "" descr_annee = "-"
existing = f"""
<ul>
<li>Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}</li>
<li>Année BUT: {descr_annee}</li>
<li>UEs : {", ".join(descr_ues)}</li>
<li>RCUEs: {len(d.get("decision_rcue", []))} décisions</li>
</ul>
"""
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?</h2>%s f"""<h2>Confirmer la suppression des décisions du semestre
<p>Cette opération est irréversible. {formsemestre.titre_mois()} pour {etud.nomprenom}
</p> </h2>
""" <p>Cette opération est irréversible.</p>
% ( <div>
sem["titre_num"], {existing}
sem["date_debut"], </div>
sem["date_fin"], """,
etud["nomprenom"],
existing,
),
OK="Supprimer", OK="Supprimer",
dest_url="", dest_url="",
cancel_url="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s" cancel_url=next_url,
% (formsemestre_id, etudid),
parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, parameters={"etudid": etudid, "formsemestre_id": formsemestre_id},
) )
sco_formsemestre_validation.formsemestre_validation_suppress_etud( sco_formsemestre_validation.formsemestre_validation_suppress_etud(
formsemestre_id, etudid formsemestre_id, etudid
) )
return flask.redirect( flash("Décisions supprimées")
scu.ScoURL() return flask.redirect(next_url)
+ "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&head_message=Décision%%20supprimée"
% (formsemestre_id, etudid)
)
# ------------- PV de JURY et archives # ------------- PV de JURY et archives

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.3.13" SCOVERSION = "9.3.15"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -33,7 +33,7 @@ except NameError:
load_dotenv(os.path.join(BASEDIR, ".env")) load_dotenv(os.path.join(BASEDIR, ".env"))
CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000" SCODOC_URL = os.environ.get("SCODOC_URL") or "http://localhost:5000"
API_URL = SCODOC_URL + "/ScoDoc/api" API_URL = SCODOC_URL + "/ScoDoc/api"
SCODOC_USER = os.environ["SCODOC_USER"] SCODOC_USER = os.environ["SCODOC_USER"]
SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"] SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"]
@ -85,13 +85,13 @@ if r.status_code != 200:
print(f"{len(r.json())} étudiants courants") print(f"{len(r.json())} étudiants courants")
# Bulletin d'un BUT # Bulletin d'un BUT
formsemestre_id = 1052 # A adapter formsemestre_id = 1063 # A adapter
etudid = 16400 etudid = 16450
bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin")
# d'un DUT # d'un DUT
formsemestre_id = 1028 # A adapter formsemestre_id = 1062 # A adapter
etudid = 14721 etudid = 16309
bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin")