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

This commit is contained in:
Arthur ZHU 2022-07-11 20:17:49 +02:00
commit 0d8e7f4268
52 changed files with 958 additions and 638 deletions

View File

@ -204,7 +204,7 @@ class ReverseProxied(object):
def create_app(config_class=DevConfig): def create_app(config_class=DevConfig):
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static") app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
app.wsgi_app = ReverseProxied(app.wsgi_app) app.wsgi_app = ReverseProxied(app.wsgi_app)
app.logger.setLevel(logging.DEBUG) app.logger.setLevel(logging.INFO)
# Evite de logguer toutes les requetes dans notre log # Evite de logguer toutes les requetes dans notre log
logging.getLogger("werkzeug").disabled = True logging.getLogger("werkzeug").disabled = True

View File

@ -70,7 +70,9 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
<div class="ue_choix_niveau"> <div class="ue_choix_niveau">
<form id="form_ue_choix_niveau"> <form id="form_ue_choix_niveau">
<b>Niveau de compétence associé:</b> <b>Niveau de compétence associé:</b>
<select onchange="set_ue_niveau_competence();" data-setter="{ <select onchange="set_ue_niveau_competence(this);"
data-ue_id="{ue.id}"
data-setter="{
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept) url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
}"> }">
<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option> <option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>
@ -83,7 +85,6 @@ def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
def set_ue_niveau_competence(ue_id: int, niveau_id: int): def set_ue_niveau_competence(ue_id: int, niveau_id: int):
"""Associe le niveau et l'UE""" """Associe le niveau et l'UE"""
log(f"set_ue_niveau_competence( {ue_id}, {niveau_id} )")
ue = UniteEns.query.get_or_404(ue_id) ue = UniteEns.query.get_or_404(ue_id)
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx) autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
@ -96,6 +97,7 @@ def set_ue_niveau_competence(ue_id: int, niveau_id: int):
) )
return "", 409 # conflict return "", 409 # conflict
if niveau_id == "": if niveau_id == "":
niveau = ""
# suppression de l'association # suppression de l'association
ue.niveau_competence = None ue.niveau_competence = None
else: else:
@ -103,4 +105,6 @@ def set_ue_niveau_competence(ue_id: int, niveau_id: int):
ue.niveau_competence = niveau ue.niveau_competence = niveau
db.session.add(ue) db.session.add(ue)
db.session.commit() db.session.commit()
log(f"set_ue_niveau_competence( {ue}, {niveau} )")
return "", 204 return "", 204

View File

@ -326,10 +326,7 @@ class BulletinBUT:
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot} semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
if sco_preferences.get_preference("bul_show_decision", formsemestre.id): if sco_preferences.get_preference("bul_show_decision", formsemestre.id):
semestre_infos.update( semestre_infos.update(
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id) sco_bulletins_json.dict_decision_jury(etud, formsemestre)
)
semestre_infos.update(
but_validations.dict_decision_jury(etud, formsemestre)
) )
if etat_inscription == scu.INSCRIT: if etat_inscription == scu.INSCRIT:
# moyenne des moyennes générales du semestre # moyenne des moyennes générales du semestre

67
app/but/cursus_but.py Normal file
View File

@ -0,0 +1,67 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Cursus en BUT
Classe raccordant avec ScoDoc 7:
ScoDoc 7 utilisait sco_cursus_dut.SituationEtudCursus
Ce module définit une classe SituationEtudCursusBUT
avec la même interface.
"""
from typing import Union
from flask import g, url_for
from app import db
from app import log
from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat
from app.comp import res_sem
from app.models import formsemestre
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcCompetence,
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
)
from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
ApcValidationAnnee,
ApcValidationRCUE,
RegroupementCoherentUE,
)
from app.models.etudiants import Identite
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_codes_parcours as sco_codes
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException, ScoValueError
from app.scodoc import sco_cursus_dut
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
super().__init__(etud, formsemestre_id, res)
# Ajustements pour le BUT
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT
def check_compensation_dut(self, semc: dict, ntc: NotesTableCompat):
"Jamais de compensation façon DUT"
return False
def parcours_validated(self):
"True si le parcours est validé"
return False # XXX TODO

View File

@ -89,6 +89,7 @@ from app.models.formations import Formation
from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours as sco_codes from app.scodoc import sco_codes_parcours as sco_codes
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -573,31 +574,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
Si les code_rcue et le code_annee ne sont pas fournis, Si les code_rcue et le code_annee ne sont pas fournis,
et qu'il n'y en a pas déjà, enregistre ceux par défaut. et qu'il n'y en a pas déjà, enregistre ceux par défaut.
""" """
for key in form: log("jury_but.DecisionsProposeesAnnee.record_form")
code = form[key] with sco_cache.DeferredSemCacheManager():
# Codes d'UE for key in form:
m = re.match(r"^code_ue_(\d+)$", key) code = form[key]
if m: # Codes d'UE
ue_id = int(m.group(1)) m = re.match(r"^code_ue_(\d+)$", key)
dec_ue = self.decisions_ues.get(ue_id)
if not dec_ue:
raise ScoValueError(f"UE invalide ue_id={ue_id}")
dec_ue.record(code)
else:
# Codes de RCUE
m = re.match(r"^code_rcue_(\d+)$", key)
if m: if m:
niveau_id = int(m.group(1)) ue_id = int(m.group(1))
dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id) dec_ue = self.decisions_ues.get(ue_id)
if not dec_rcue: if not dec_ue:
raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}") raise ScoValueError(f"UE invalide ue_id={ue_id}")
dec_rcue.record(code) dec_ue.record(code)
elif key == "code_annee": else:
# Code annuel # Codes de RCUE
self.record(code) m = re.match(r"^code_rcue_(\d+)$", key)
if m:
niveau_id = int(m.group(1))
dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id)
if not dec_rcue:
raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}")
dec_rcue.record(code)
elif key == "code_annee":
# Code annuel
self.record(code)
self.record_all() self.record_all()
db.session.commit() db.session.commit()
def record(self, code: str, no_overwrite=False): def record(self, code: str, no_overwrite=False):
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription. """Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
@ -647,6 +650,16 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) )
self.recorded = True self.recorded = True
self.invalidate_formsemestre_cache()
def invalidate_formsemestre_cache(self):
"invalide le résultats des deux formsemestres"
if self.formsemestre_impair is not None:
sco_cache.invalidate_formsemestre(
formsemestre_id=self.formsemestre_impair.id
)
if self.formsemestre_pair is not None:
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
def record_all(self): def record_all(self):
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire, et sont donc en mode "automatique" """ """Enregistre les codes qui n'ont pas été spécifiés par le formulaire, et sont donc en mode "automatique" """
@ -687,6 +700,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
for validation in validations: for validation in validations:
db.session.delete(validation) db.session.delete(validation)
db.session.flush() db.session.flush()
self.invalidate_formsemestre_cache()
def get_autorisations_passage(self) -> list[int]: def get_autorisations_passage(self) -> list[int]:
"""Les liste des indices de semestres auxquels on est autorisé à """Les liste des indices de semestres auxquels on est autorisé à
@ -805,6 +819,14 @@ class DecisionsProposeesRCUE(DecisionsProposees):
msg=f"Validation RCUE {repr(self.rcue)}", msg=f"Validation RCUE {repr(self.rcue)}",
) )
db.session.add(self.validation) db.session.add(self.validation)
if self.rcue.formsemestre_1 is not None:
sco_cache.invalidate_formsemestre(
formsemestre_id=self.rcue.formsemestre_1.id
)
if self.rcue.formsemestre_2 is not None:
sco_cache.invalidate_formsemestre(
formsemestre_id=self.rcue.formsemestre_2.id
)
self.recorded = True self.recorded = True
def erase(self): def erase(self):
@ -948,6 +970,8 @@ class DecisionsProposeesUE(DecisionsProposees):
msg=f"Validation UE {self.ue.id}", msg=f"Validation UE {self.ue.id}",
) )
db.session.add(self.validation) db.session.add(self.validation)
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
self.recorded = True self.recorded = True
def erase(self): def erase(self):

View File

@ -101,10 +101,17 @@ def formsemestre_saisie_jury_but(
f"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3> f"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3>
<div class="table_jury_but_links"> <div class="table_jury_but_links">
<div> <div>
<a href="{url_for( <ul>
<li><a href="{url_for(
"notes.pvjury_table_but", "notes.pvjury_table_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}" class="stdlink">tableau PV de jury</a> }" class="stdlink">Tableau PV de jury</a>
</li>
<li><a href="{url_for(
"notes.formsemestre_lettres_individuelles",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}" class="stdlink">Courriers individuels (classeur pdf)</a>
</li>
</div> </div>
</div> </div>
""" """

View File

@ -4,15 +4,14 @@
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""Jury BUT: clacul des décisions de jury annuelles "automatiques" """Jury BUT: calcul des décisions de jury annuelles "automatiques"
""" """
from flask import g, url_for
from app import db from app import db
from app.but import jury_but from app.but import jury_but
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -23,12 +22,13 @@ def formsemestre_validation_auto_but(formsemestre: FormSemestre) -> int:
if not formsemestre.formation.is_apc(): if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT") raise ScoValueError("fonction réservée aux formations BUT")
nb_admis = 0 nb_admis = 0
for etudid in formsemestre.etuds_inscriptions: with sco_cache.DeferredSemCacheManager():
etud: Identite = Identite.query.get(etudid) for etudid in formsemestre.etuds_inscriptions:
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) etud: Identite = Identite.query.get(etudid)
if deca.admis: # année réussie deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
deca.record_all() if deca.admis: # année réussie
nb_admis += 1 deca.record_all()
nb_admis += 1
db.session.commit() db.session.commit()
return nb_admis return nb_admis

View File

@ -285,7 +285,17 @@ class ResultatsSemestre(ResultatsCache):
else: else:
return None return None
# converti la Series en dict, afin que les np.int64 reviennent en int # converti la Series en dict, afin que les np.int64 reviennent en int
return ue_cap.to_dict() # et remplace les NaN (venant des NULL en base) par des None
ue_cap_dict = ue_cap.to_dict()
if ue_cap_dict["formsemestre_id"] is not None and np.isnan(
ue_cap_dict["formsemestre_id"]
):
ue_cap_dict["formsemestre_id"] = None
if ue_cap_dict["compense_formsemestre_id"] is not None and np.isnan(
ue_cap_dict["compense_formsemestre_id"]
):
ue_cap_dict["compense_formsemestre_id"] = None
return ue_cap_dict
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict: def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant. """L'état de l'UE pour cet étudiant.
@ -651,8 +661,19 @@ class ResultatsSemestre(ResultatsCache):
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 and self.validations: if mode_jury and self.validations:
dec_sem = self.validations.decisions_jury.get(etudid) if self.is_apc:
jury_code_sem = dec_sem["code"] if dec_sem else "" # formations BUT: pas de code semestre, concatene ceux des UE
dec_ues = self.validations.decisions_jury_ues.get(etudid)
if dec_ues:
jury_code_sem = ",".join(
[dec_ues[ue_id].get("code", "") for ue_id in dec_ues]
)
else:
jury_code_sem = ""
else:
# formations classiqes: code semestre
dec_sem = self.validations.decisions_jury.get(etudid)
jury_code_sem = dec_sem["code"] if dec_sem else ""
idx = add_cell( idx = add_cell(
row, row,
"jury_code_sem", "jury_code_sem",
@ -668,7 +689,7 @@ class ResultatsSemestre(ResultatsCache):
f"""<a href="{url_for('notes.formsemestre_validation_etud_form', f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
) )
}">saisir décision</a>""", }">{"saisir" if not jury_code_sem else "modifier"} décision</a>""",
"col_jury_link", "col_jury_link",
idx, idx,
) )

View File

@ -47,7 +47,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
def load_formsemestre_validations(formsemestre: FormSemestre) -> ValidationsSemestre: def load_formsemestre_validations(formsemestre: FormSemestre) -> ValidationsSemestre:
"""Charge les résultats de jury de ce semestre. """Charge les résultats de jury de ce semestre.
Search in local cache (g.formsemestre_result_cache) Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it. If not in cache, build it and cache it (in g).
""" """
if not hasattr(g, "formsemestre_validation_cache"): if not hasattr(g, "formsemestre_validation_cache"):
g.formsemestre_validations_cache = {} # pylint: disable=C0237 g.formsemestre_validations_cache = {} # pylint: disable=C0237

View File

@ -84,7 +84,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
formations = db.relationship("Formation", backref="referentiel_competence") formations = db.relationship("Formation", backref="referentiel_competence")
def __repr__(self): def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite!r}>" return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
def to_dict(self): def to_dict(self):
"""Représentation complète du ref. de comp. """Représentation complète du ref. de comp.
@ -269,7 +269,7 @@ class ApcNiveau(db.Model, XMLModel):
ues = db.relationship("UniteEns", back_populates="niveau_competence") ues = db.relationship("UniteEns", back_populates="niveau_competence")
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__} ordre={self.ordre!r} annee={ return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>""" self.annee!r} {self.competence!r}>"""
def to_dict(self): def to_dict(self):
@ -432,7 +432,7 @@ class ApcParcours(db.Model, XMLModel):
) )
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.code!r}>" return f"<{self.__class__.__name__} {self.id} {self.code!r} ref={self.referentiel}>"
def to_dict(self): def to_dict(self):
return { return {
@ -452,7 +452,7 @@ class ApcAnneeParcours(db.Model, XMLModel):
"numéro de l'année: 1, 2, 3" "numéro de l'année: 1, 2, 3"
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} ordre={self.ordre!r} parcours={self.parcours.code!r}>" return f"<{self.__class__.__name__} {self.id} ordre={self.ordre!r} parcours={self.parcours.code!r}>"
def to_dict(self): def to_dict(self):
return { return {

View File

@ -67,7 +67,7 @@ class ApcValidationRCUE(db.Model):
return self.ue2.niveau_competence return self.ue2.niveau_competence
def to_dict_bul(self) -> dict: def to_dict_bul(self) -> dict:
"Export dict pour bulletins" "Export dict pour bulletins: le code et le niveau de compétence"
return {"code": self.code, "niveau": self.niveau().to_dict_bul()} return {"code": self.code, "niveau": self.niveau().to_dict_bul()}
@ -309,7 +309,9 @@ class ApcValidationAnnee(db.Model):
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
""" """
Un dict avec les décisions de jury BUT enregistrées. Un dict avec les décisions de jury BUT enregistrées:
- decision_rcue : list[dict]
- decision_annee : dict
Ne reprend pas les décisions d'UE, non spécifiques au BUT. Ne reprend pas les décisions d'UE, non spécifiques au BUT.
""" """
decisions = {} decisions = {}
@ -320,8 +322,19 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
etudid=etud.id, formsemestre_id=formsemestre.id etudid=etud.id, formsemestre_id=formsemestre.id
) )
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues] decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
decisions["descr_decisions_rcue"] = ", ".join(
[
f"""{dec_rcue["niveau"]["competence"]["titre"]}&nbsp;{dec_rcue["niveau"]["ordre"]}:&nbsp;{dec_rcue["code"]}"""
for dec_rcue in decisions["decision_rcue"]
]
)
decisions["descr_decisions_niveaux"] = (
"Niveaux de compétences: " + decisions["descr_decisions_rcue"]
)
else: else:
decisions["decision_rcue"] = [] decisions["decision_rcue"] = []
decisions["descr_decisions_rcue"] = ""
decisions["descr_decisions_niveaux"] = ""
# --- Année: prend la validation pour l'année scolaire de ce semestre # --- Année: prend la validation pour l'année scolaire de ce semestre
validation = ( validation = (
ApcValidationAnnee.query.filter_by( ApcValidationAnnee.query.filter_by(

View File

@ -31,6 +31,9 @@ class Departement(db.Model):
"ScoPreference", lazy="dynamic", backref="departement" "ScoPreference", lazy="dynamic", backref="departement"
) )
semsets = db.relationship("NotesSemSet", lazy="dynamic", backref="departement") semsets = db.relationship("NotesSemSet", lazy="dynamic", backref="departement")
referentiels = db.relationship(
"ApcReferentielCompetences", lazy="dynamic", backref="departement"
)
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>" return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"

View File

@ -5,7 +5,7 @@
import datetime import datetime
from functools import cached_property from functools import cached_property
from flask import flash from flask import flash, g
import flask_sqlalchemy import flask_sqlalchemy
from sqlalchemy.sql import text from sqlalchemy.sql import text
@ -19,11 +19,11 @@ from app.models.but_refcomp import (
ApcNiveau, ApcNiveau,
ApcParcours, ApcParcours,
ApcParcoursNiveauCompetence, ApcParcoursNiveauCompetence,
ApcReferentielCompetences,
) )
from app.models.groups import GroupDescr, Partition from app.models.groups import GroupDescr, Partition
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.models.but_refcomp import ApcParcours
from app.models.but_refcomp import parcours_formsemestre from app.models.but_refcomp import parcours_formsemestre
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.modules import Module from app.models.modules import Module
@ -577,7 +577,11 @@ class FormSemestre(db.Model):
) )
# Inscrit les étudiants des groupes de parcours: # Inscrit les étudiants des groupes de parcours:
for group in partition.groups: for group in partition.groups:
query = ApcParcours.query.filter_by(code=group.group_name) query = (
ApcParcours.query.filter_by(code=group.group_name)
.join(ApcReferentielCompetences)
.filter_by(dept_id=g.scodoc_dept_id)
)
if query.count() != 1: if query.count() != 1:
log( log(
f"""update_inscriptions_parcours_from_groups: { f"""update_inscriptions_parcours_from_groups: {

View File

@ -57,6 +57,11 @@ class ScolarFormSemestreValidation(db.Model):
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})" return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
def to_dict(self) -> dict:
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
class ScolarAutorisationInscription(db.Model): class ScolarAutorisationInscription(db.Model):
"""Autorisation d'inscription dans un semestre""" """Autorisation d'inscription dans un semestre"""
@ -78,6 +83,11 @@ class ScolarAutorisationInscription(db.Model):
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),
) )
def to_dict(self) -> dict:
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
@classmethod @classmethod
def autorise_etud( def autorise_etud(
cls, cls,
@ -146,3 +156,8 @@ class ScolarEvent(db.Model):
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),
) )
def to_dict(self) -> dict:
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d

View File

@ -54,22 +54,22 @@ from app.scodoc.sco_codes_parcours import (
ue_is_fondamentale, ue_is_fondamentale,
ue_is_professionnelle, ue_is_professionnelle,
) )
from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_compute_moy from app.scodoc import sco_compute_moy
from app.scodoc import sco_cache from app.scodoc.sco_cursus import formsemestre_get_etud_capitalisation
from app.scodoc import sco_cursus_dut
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud
def comp_ranks(T): def comp_ranks(T):
@ -1175,7 +1175,7 @@ class NotesTable:
): ):
if not cnx: if not cnx:
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
sco_parcours_dut.do_formsemestre_validate_ue( sco_cursus_dut.do_formsemestre_validate_ue(
cnx, cnx,
nt_cap, nt_cap,
ue_cap["formsemestre_id"], ue_cap["formsemestre_id"],

View File

@ -265,9 +265,9 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu
cursor.execute(req, vals) cursor.execute(req, vals)
# log('req=%s\n'%req) # log('req=%s\n'%req)
# log('vals=%s\n'%vals) # log('vals=%s\n'%vals)
except psycopg2.errors.StringDataRightTruncation: except psycopg2.errors.StringDataRightTruncation as exc:
cnx.rollback() cnx.rollback()
raise ScoValueError("champs de texte trop long !") raise ScoValueError("champs de texte trop long !") from exc
except: except:
cnx.rollback() # get rid of this transaction cnx.rollback() # get rid of this transaction
log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals)) log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals))

View File

@ -112,8 +112,8 @@ from app.scodoc.sco_codes_parcours import (
NAR, NAR,
RAT, RAT,
) )
from app.scodoc import sco_cursus
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud from app.scodoc import sco_etud
APO_PORTAL_ENCODING = ( APO_PORTAL_ENCODING = (
@ -413,7 +413,7 @@ class ApoEtud(dict):
export_res_etape = self.export_res_etape export_res_etape = self.export_res_etape
if (not export_res_etape) and cur_sem: if (not export_res_etape) and cur_sem:
# exporte toujours le résultat de l'étape si l'étudiant est diplômé # exporte toujours le résultat de l'étape si l'étudiant est diplômé
Se = sco_parcours_dut.SituationEtudParcours( Se = sco_cursus.get_situation_etud_cursus(
self.etud, cur_sem["formsemestre_id"] self.etud, cur_sem["formsemestre_id"]
) )
export_res_etape = Se.all_other_validated() export_res_etape = Se.all_other_validated()

View File

@ -347,23 +347,23 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
u[ u[
"modules_capitalized" "modules_capitalized"
] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée) ] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée)
if ue_status["is_capitalized"]: if ue_status["is_capitalized"] and ue_status["formsemestre_id"] is not None:
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"]) sem_origin = FormSemestre.query.get(ue_status["formsemestre_id"])
u["ue_descr_txt"] = "capitalisée le %s" % ndb.DateISOtoDMY(
ue_status["event_date"]
)
u[ u[
"ue_descr_html" "ue_descr_txt"
] = f"""<a href="{ url_for( 'notes.formsemestre_bulletinetud', ] = f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}'
scodoc_dept=g.scodoc_dept, formsemestre_id=sem_origin['formsemestre_id'], etudid=etudid)}" u["ue_descr_html"] = (
title="{sem_origin['titreannee']}" class="bull_link" f"""<a href="{ url_for( 'notes.formsemestre_bulletinetud',
>{u["ue_descr_txt"]} pouet</a> scodoc_dept=g.scodoc_dept, formsemestre_id=sem_origin.id, etudid=etudid)}"
title="{sem_origin.titre_annee()}" class="bull_link"
>{u["ue_descr_txt"]}</a>
""" """
if ue_status["moy"] != "NA" and ue_status["formsemestre_id"]: if sem_origin
else ""
)
if ue_status["moy"] != "NA":
# détail des modules de l'UE capitalisée # détail des modules de l'UE capitalisée
formsemestre_cap = FormSemestre.query.get_or_404( formsemestre_cap = FormSemestre.query.get(ue_status["formsemestre_id"])
ue_status["formsemestre_id"]
)
nt_cap: NotesTableCompat = res_sem.load_formsemestre_results( nt_cap: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre_cap formsemestre_cap
) )
@ -434,7 +434,7 @@ def _get_etud_etat_html(etat: str) -> str:
elif etat == scu.DEF: # "DEF" elif etat == scu.DEF: # "DEF"
return ' <font color="red">(DEFAILLANT)</font> ' return ' <font color="red">(DEFAILLANT)</font> '
else: else:
return ' <font color="red">(%s)</font> ' % etat return f' <font color="red">({etat})</font> '
def _sort_mod_by_matiere(modlist, nt, etudid): def _sort_mod_by_matiere(modlist, nt, etudid):
@ -707,11 +707,11 @@ def etud_descr_situation_semestre(
descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
""" """
# Fonction utilisée par tous les bulletins (APC ou classiques)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
infos = scu.DictDefault(defaultvalue="") infos = scu.DictDefault(defaultvalue="")
# --- Situation et décisions jury # --- Situation et décisions jury
# démission/inscription ? # démission/inscription ?
events = sco_etud.scolar_events_list( events = sco_etud.scolar_events_list(
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
@ -728,8 +728,7 @@ def etud_descr_situation_semestre(
# il y a eu une erreur qui a laissé un event 'inscription' # il y a eu une erreur qui a laissé un event 'inscription'
# on l'efface: # on l'efface:
log( log(
"etud_descr_situation_semestre: removing duplicate INSCRIPTION event for etudid=%s !" f"etud_descr_situation_semestre: removing duplicate INSCRIPTION event for etudid={etudid} !"
% etudid
) )
sco_etud.scolar_events_delete(cnx, event["event_id"]) sco_etud.scolar_events_delete(cnx, event["event_id"])
else: else:
@ -738,8 +737,7 @@ def etud_descr_situation_semestre(
# assert date_dem == None, 'plusieurs démissions !' # assert date_dem == None, 'plusieurs démissions !'
if date_dem: # cela ne peut pas arriver sauf bug (signale a Evry 2013?) if date_dem: # cela ne peut pas arriver sauf bug (signale a Evry 2013?)
log( log(
"etud_descr_situation_semestre: removing duplicate DEMISSION event for etudid=%s !" f"etud_descr_situation_semestre: removing duplicate DEMISSION event for etudid={etudid} !"
% etudid
) )
sco_etud.scolar_events_delete(cnx, event["event_id"]) sco_etud.scolar_events_delete(cnx, event["event_id"])
else: else:
@ -747,8 +745,7 @@ def etud_descr_situation_semestre(
elif event_type == "DEFAILLANCE": elif event_type == "DEFAILLANCE":
if date_def: if date_def:
log( log(
"etud_descr_situation_semestre: removing duplicate DEFAILLANCE event for etudid=%s !" f"etud_descr_situation_semestre: removing duplicate DEFAILLANCE event for etudid={etudid} !"
% etudid
) )
sco_etud.scolar_events_delete(cnx, event["event_id"]) sco_etud.scolar_events_delete(cnx, event["event_id"])
else: else:
@ -756,33 +753,37 @@ def etud_descr_situation_semestre(
if show_date_inscr: if show_date_inscr:
if not date_inscr: if not date_inscr:
infos["date_inscription"] = "" infos["date_inscription"] = ""
infos["descr_inscription"] = "Pas inscrit%s." % ne infos["descr_inscription"] = f"Pas inscrit{ne}."
else: else:
infos["date_inscription"] = date_inscr infos["date_inscription"] = date_inscr
infos["descr_inscription"] = "Inscrit%s le %s." % (ne, date_inscr) infos["descr_inscription"] = f"Inscrit{ne} le {date_inscr}."
else: else:
infos["date_inscription"] = "" infos["date_inscription"] = ""
infos["descr_inscription"] = "" infos["descr_inscription"] = ""
infos["situation"] = infos["descr_inscription"] infos["situation"] = infos["descr_inscription"]
# Décision: valeurs par defaut vides:
infos["decision_jury"] = infos["descr_decision_jury"] = ""
infos["decision_sem"] = ""
infos["decisions_ue"] = infos["descr_decisions_ue"] = ""
infos["descr_decisions_niveaux"] = infos["descr_decisions_rcue"] = ""
infos["descr_decision_annee"] = ""
if date_dem: if date_dem:
infos["descr_demission"] = "Démission le %s." % date_dem infos["descr_demission"] = f"Démission le {date_dem}."
infos["date_demission"] = date_dem infos["date_demission"] = date_dem
infos["descr_decision_jury"] = "Démission" infos["decision_jury"] = infos["descr_decision_jury"] = "Démission"
infos["situation"] += " " + infos["descr_demission"] infos["situation"] += " " + infos["descr_demission"]
return infos, None # ne donne pas les dec. de jury pour les demissionnaires return infos, None # ne donne pas les dec. de jury pour les demissionnaires
if date_def: if date_def:
infos["descr_defaillance"] = "Défaillant%s" % ne infos["descr_defaillance"] = f"Défaillant{ne}"
infos["date_defaillance"] = date_def infos["date_defaillance"] = date_def
infos["descr_decision_jury"] = "Défaillant%s" % ne infos["descr_decision_jury"] = f"Défaillant{ne}"
infos["situation"] += " " + infos["descr_defaillance"] infos["situation"] += " " + infos["descr_defaillance"]
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid]) dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid])
if dpv: if dpv:
infos["decision_sem"] = dpv["decisions"][0]["decision_sem"] infos["decision_sem"] = dpv["decisions"][0]["decision_sem"]
else:
infos["decision_sem"] = ""
if not show_decisions: if not show_decisions:
return infos, dpv return infos, dpv
@ -798,25 +799,37 @@ def etud_descr_situation_semestre(
dec = infos["descr_decision_jury"] dec = infos["descr_decision_jury"]
else: else:
infos["descr_decision_jury"] = "" infos["descr_decision_jury"] = ""
infos["decision_jury"] = ""
if pv["decisions_ue_descr"] and show_uevalid: if pv["decisions_ue_descr"] and show_uevalid:
infos["decisions_ue"] = pv["decisions_ue_descr"] infos["decisions_ue"] = pv["decisions_ue_descr"]
infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"] + ". " infos["descr_decisions_ue"] = " UE acquises: " + pv["decisions_ue_descr"] + ". "
dec += infos["descr_decisions_ue"] dec += infos["descr_decisions_ue"]
else: else:
# infos['decisions_ue'] = None infos["decisions_ue"] = ""
infos["descr_decisions_ue"] = "" infos["descr_decisions_ue"] = ""
infos["mention"] = pv["mention"] infos["mention"] = pv["mention"]
if pv["mention"] and show_mention: if pv["mention"] and show_mention:
dec += "Mention " + pv["mention"] + ". " dec += f"Mention {pv['mention']}."
# Décisions APC / BUT
if pv.get("decision_annee", {}):
infos["descr_decision_annee"] = "Décision année: " + pv.get(
"decision_annee", {}
).get("code", "")
else:
infos["descr_decision_annee"] = ""
infos["descr_decisions_rcue"] = pv.get("descr_decisions_rcue", "")
infos["descr_decisions_niveaux"] = pv.get("descr_decisions_niveaux", "")
infos["situation"] += " " + dec infos["situation"] += " " + dec
if not pv["validation_parcours"]: # parcours non terminé if not pv["validation_parcours"]: # parcours non terminé
if pv["autorisations_descr"]: if pv["autorisations_descr"]:
infos["situation"] += ( infos[
" Autorisé à s'inscrire en %s." % pv["autorisations_descr"] "situation"
) ] += f" Autorisé à s'inscrire en {pv['autorisations_descr']}."
else: else:
infos["situation"] += " Diplôme obtenu." infos["situation"] += " Diplôme obtenu."
return infos, dpv return infos, dpv
@ -832,9 +845,20 @@ def formsemestre_bulletinetud(
force_publishing=False, # force publication meme si semestre non publie sur "portail" force_publishing=False, # force publication meme si semestre non publie sur "portail"
prefer_mail_perso=False, prefer_mail_perso=False,
): ):
"""Page bulletin de notes """Page bulletin de notes pour
pour les formations classiques hors BUT (page HTML) - HTML des formations classiques (non BUT)
ou le format "oldjson". - le format "oldjson" (les "json" sont générés à part, voir get_formsemestre_bulletin_etud_json)
- les formats PDF, XML et mail pdf (toutes formations)
Note: le format XML n'est plus maintenu et pour les BUT ne contient pas
toutes les informations. Privilégier le format JSON.
Paramètres:
- version: pour les formations classqiues, versions short/selectedevals/long
- xml_with_decisions: inclue ou non les
- force_publishing: renvoie le bulletin même si semestre non publie sur "portail"
- prefer_mail_perso: pour pdfmail, utilise adresse mail perso en priorité.
""" """
format = format or "html" format = format or "html"
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
@ -893,7 +917,12 @@ def do_formsemestre_bulletinetud(
prefer_mail_perso=False, # mails envoyés sur adresse perso si non vide prefer_mail_perso=False, # mails envoyés sur adresse perso si non vide
): ):
"""Génère le bulletin au format demandé. """Génère le bulletin au format demandé.
Retourne: (bul, filigranne) Utilisé pour:
- HTML des formations classiques (non BUT)
- le format "oldjson" (les json sont générés à part, voir get_formsemestre_bulletin_etud_json)
- les formats PDF, XML et mail pdf (toutes formations)
Résultat: (bul, filigranne)
bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json) bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json)
et filigranne est un message à placer en "filigranne" (eg "Provisoire"). et filigranne est un message à placer en "filigranne" (eg "Provisoire").
""" """
@ -909,7 +938,7 @@ def do_formsemestre_bulletinetud(
return bul, "" return bul, ""
elif format == "json": elif format == "json": # utilisé pour classic et "oldjson"
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud( bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
formsemestre.id, formsemestre.id,
etudid, etudid,
@ -922,20 +951,20 @@ def do_formsemestre_bulletinetud(
if formsemestre.formation.is_apc(): if formsemestre.formation.is_apc():
etudiant = Identite.query.get(etudid) etudiant = Identite.query.get(etudid)
r = bulletin_but.BulletinBUT(formsemestre) r = bulletin_but.BulletinBUT(formsemestre)
I = r.bulletin_etud_complet(etudiant, version=version) infos = r.bulletin_etud_complet(etudiant, version=version)
else: else:
I = formsemestre_bulletinetud_dict(formsemestre.id, etudid) infos = formsemestre_bulletinetud_dict(formsemestre.id, etudid)
etud = I["etud"] etud = infos["etud"]
if format == "html": if format == "html":
htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud( htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud(
I, version=version, format="html" infos, version=version, format="html"
) )
return htm, I["filigranne"] return htm, infos["filigranne"]
elif format == "pdf" or format == "pdfpart": elif format == "pdf" or format == "pdfpart":
bul, filename = sco_bulletins_generator.make_formsemestre_bulletinetud( bul, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
I, infos,
version=version, version=version,
format="pdf", format="pdf",
stand_alone=(format != "pdfpart"), stand_alone=(format != "pdfpart"),
@ -943,10 +972,10 @@ def do_formsemestre_bulletinetud(
if format == "pdf": if format == "pdf":
return ( return (
scu.sendPDFFile(bul, filename), scu.sendPDFFile(bul, filename),
I["filigranne"], infos["filigranne"],
) # unused ret. value ) # unused ret. value
else: else:
return bul, I["filigranne"] return bul, infos["filigranne"]
elif format == "pdfmail": elif format == "pdfmail":
# format pdfmail: envoie le pdf par mail a l'etud, et affiche le html # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
@ -955,7 +984,7 @@ def do_formsemestre_bulletinetud(
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud( pdfdata, filename = sco_bulletins_generator.make_formsemestre_bulletinetud(
I, version=version, format="pdf" infos, version=version, format="pdf"
) )
if prefer_mail_perso: if prefer_mail_perso:
@ -965,21 +994,21 @@ def do_formsemestre_bulletinetud(
if not recipient_addr: if not recipient_addr:
flash(f"{etud['nomprenom']} n'a pas d'adresse e-mail !") flash(f"{etud['nomprenom']} n'a pas d'adresse e-mail !")
return False, I["filigranne"] return False, infos["filigranne"]
else: else:
mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr) mail_bulletin(formsemestre.id, infos, pdfdata, filename, recipient_addr)
flash(f"mail envoyé à {recipient_addr}") flash(f"mail envoyé à {recipient_addr}")
return True, I["filigranne"] return True, infos["filigranne"]
raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format) raise ValueError("do_formsemestre_bulletinetud: invalid format (%s)" % format)
def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
"""Send bulletin by email to etud """Send bulletin by email to etud
If bul_mail_list_abs pref is true, put list of absences in mail body (text). If bul_mail_list_abs pref is true, put list of absences in mail body (text).
""" """
etud = I["etud"] etud = infos["etud"]
webmaster = sco_preferences.get_preference("bul_mail_contact_addr", formsemestre_id) webmaster = sco_preferences.get_preference("bul_mail_contact_addr", formsemestre_id)
dept = scu.unescape_html( dept = scu.unescape_html(
sco_preferences.get_preference("DeptName", formsemestre_id) sco_preferences.get_preference("DeptName", formsemestre_id)
@ -1006,7 +1035,7 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
etud["etudid"], with_evals=False, format="text" etud["etudid"], with_evals=False, format="text"
) )
subject = "Relevé de notes de %s" % etud["nomprenom"] subject = f"""Relevé de notes de {etud["nomprenom"]}"""
recipients = [recipient_addr] recipients = [recipient_addr]
sender = sco_preferences.get_preference("email_from_addr", formsemestre_id) sender = sco_preferences.get_preference("email_from_addr", formsemestre_id)
if copy_addr: if copy_addr:
@ -1015,7 +1044,7 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
bcc = "" bcc = ""
# Attach pdf # Attach pdf
log("mail bulletin a %s" % recipient_addr) log(f"""mail bulletin a {recipient_addr}""")
email.send_email( email.send_email(
subject, subject,
sender, sender,

View File

@ -25,7 +25,7 @@
# #
############################################################################## ##############################################################################
"""Génération du bulletin en format JSON (beta, non completement testé) """Génération du bulletin en format JSON
""" """
import datetime import datetime
@ -33,8 +33,9 @@ import json
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.formsemestre import FormSemestre from app.models import but_validations
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -139,7 +140,7 @@ def formsemestre_bulletinetud_published_dict(
etat_inscription = etud.inscription_etat(formsemestre.id) etat_inscription = etud.inscription_etat(formsemestre.id)
if etat_inscription != scu.INSCRIT: if etat_inscription != scu.INSCRIT:
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True)) d.update(dict_decision_jury(etud, formsemestre, with_decisions=True))
return d return d
# Groupes: # Groupes:
@ -343,9 +344,7 @@ def formsemestre_bulletinetud_published_dict(
d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust) d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust)
# --- Decision Jury # --- Decision Jury
d.update( d.update(dict_decision_jury(etud, formsemestre, with_decisions=xml_with_decisions))
dict_decision_jury(etudid, formsemestre_id, with_decisions=xml_with_decisions)
)
# --- Appreciations # --- Appreciations
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
apprecs = sco_etud.appreciations_list( apprecs = sco_etud.appreciations_list(
@ -364,7 +363,9 @@ def formsemestre_bulletinetud_published_dict(
return d return d
def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict: def dict_decision_jury(
etud: Identite, formsemestre: FormSemestre, with_decisions: bool = False
) -> dict:
"""dict avec decision pour bulletins json """dict avec decision pour bulletins json
- decision : décision semestre - decision : décision semestre
- decision_ue : list des décisions UE - decision_ue : list des décisions UE
@ -372,6 +373,8 @@ def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict:
with_decision donne les décision même si bul_show_decision est faux. with_decision donne les décision même si bul_show_decision est faux.
Si formation APC, indique aussi validations année et RCUEs
Exemple: Exemple:
{ {
'autorisation_inscription': [{'semestre_id': 4}], 'autorisation_inscription': [{'semestre_id': 4}],
@ -397,14 +400,14 @@ def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict:
d = {} d = {}
if ( if (
sco_preferences.get_preference("bul_show_decision", formsemestre_id) sco_preferences.get_preference("bul_show_decision", formsemestre.id)
or with_decisions or with_decisions
): ):
infos, dpv = sco_bulletins.etud_descr_situation_semestre( infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etudid, etud.id,
formsemestre_id, formsemestre.id,
show_uevalid=sco_preferences.get_preference( show_uevalid=sco_preferences.get_preference(
"bul_show_uevalid", formsemestre_id "bul_show_uevalid", formsemestre.id
), ),
) )
d["situation"] = infos["situation"] d["situation"] = infos["situation"]
@ -456,4 +459,7 @@ def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict:
) )
else: else:
d["decision"] = dict(code="", etat="DEM") d["decision"] = dict(code="", etat="DEM")
# Ajout jury BUT:
if formsemestre.formation.is_apc():
d.update(but_validations.dict_decision_jury(etud, formsemestre))
return d return d

View File

@ -140,12 +140,23 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
text = (field or "") % scu.WrapDict( text = (field or "") % scu.WrapDict(
cdict cdict
) # note that None values are mapped to empty strings ) # note that None values are mapped to empty strings
except: except KeyError as exc:
log(
f"""process_field: KeyError on field={field!r}
values={pprint.pformat(cdict)}
"""
)
if len(exc.args) > 0:
missing_field = exc.args[0]
text = f"""<para><i>format invalide: champs</i> {missing_field} <i>inexistant !</i></para>"""
except: # pylint: disable=bare-except
log( log(
f"""process_field: invalid format. field={field!r} f"""process_field: invalid format. field={field!r}
values={pprint.pformat(cdict)} values={pprint.pformat(cdict)}
""" """
) )
# ne sera pas visible si lien vers pdf:
scu.flash_once(f"Attention: format PDF invalide (champs {field}")
text = ( text = (
"<para><i>format invalide !</i></para><para>" "<para><i>format invalide !</i></para><para>"
+ traceback.format_exc() + traceback.format_exc()

View File

@ -231,29 +231,29 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
"""expire cache pour un semestre (ou tous si formsemestre_id non spécifié). """expire cache pour un semestre (ou tous si formsemestre_id non spécifié).
Si pdfonly, n'expire que les bulletins pdf cachés. Si pdfonly, n'expire que les bulletins pdf cachés.
""" """
from app.scodoc import sco_parcours_dut from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cursus
if getattr(g, "defer_cache_invalidation", False): assert isinstance(formsemestre_id, int) or formsemestre_id is None
if getattr(g, "defer_cache_invalidation", 0) > 0:
g.sem_to_invalidate.add(formsemestre_id) g.sem_to_invalidate.add(formsemestre_id)
return return
log(f"inval_cache, formsemestre_id={formsemestre_id} pdfonly={pdfonly}")
if formsemestre_id is None: if formsemestre_id is None:
# clear all caches # clear all caches
log("----- invalidate_formsemestre: clearing all caches -----") log(
f"----- invalidate_formsemestre: clearing all caches. pdfonly={pdfonly}-----"
)
formsemestre_ids = [ formsemestre_ids = [
x[0] formsemestre.id
for x in ndb.SimpleQuery( for formsemestre in FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
"""SELECT id FROM notes_formsemestre s
WHERE s.dept_id=%(dept_id)s
""",
{"dept_id": g.scodoc_dept_id},
)
] ]
else: else:
formsemestre_ids = [ formsemestre_ids = [
formsemestre_id formsemestre_id
] + sco_parcours_dut.list_formsemestre_utilisateurs_uecap(formsemestre_id) ] + sco_cursus.list_formsemestre_utilisateurs_uecap(formsemestre_id)
log(f"----- invalidate_formsemestre: clearing {formsemestre_ids} -----") log(
f"----- invalidate_formsemestre: clearing {formsemestre_ids}. pdfonly={pdfonly} -----"
)
if not pdfonly: if not pdfonly:
# Delete cached notes and evaluations # Delete cached notes and evaluations
@ -289,17 +289,19 @@ class DeferredSemCacheManager:
""" """
def __enter__(self): def __enter__(self):
assert not hasattr(g, "defer_cache_invalidation") if not hasattr(g, "defer_cache_invalidation"):
g.defer_cache_invalidation = True g.defer_cache_invalidation = 0
g.sem_to_invalidate = set() g.sem_to_invalidate = set()
g.defer_cache_invalidation += 1
return True return True
def __exit__(self, exc_type, exc_value, exc_traceback): def __exit__(self, exc_type, exc_value, exc_traceback):
assert g.defer_cache_invalidation assert g.defer_cache_invalidation
g.defer_cache_invalidation = False g.defer_cache_invalidation -= 1
while g.sem_to_invalidate: if g.defer_cache_invalidation == 0:
formsemestre_id = g.sem_to_invalidate.pop() while g.sem_to_invalidate:
invalidate_formsemestre(formsemestre_id) formsemestre_id = g.sem_to_invalidate.pop()
invalidate_formsemestre(formsemestre_id)
# ---- Nouvelles classes ScoDoc 9.2 # ---- Nouvelles classes ScoDoc 9.2

View File

@ -216,7 +216,7 @@ def code_semestre_attente(code: str) -> bool:
def code_ue_validant(code: str) -> bool: def code_ue_validant(code: str) -> bool:
"Vrai si ce code entraine la validation des UEs du semestre." "Vrai si ce code d'UE est validant (ie attribue les ECTS)"
return CODES_UE_VALIDES.get(code, False) return CODES_UE_VALIDES.get(code, False)

134
app/scodoc/sco_cursus.py Normal file
View File

@ -0,0 +1,134 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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 cursus (jurys suivant la formation)
"""
from app.but import cursus_but
from app.scodoc import sco_cursus_dut
from app.comp.res_compat import NotesTableCompat
from app.comp import res_sem
from app.models import FormSemestre
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formations
import app.scodoc.notesdb as ndb
# SituationEtudParcours -> get_situation_etud_cursus
def get_situation_etud_cursus(
etud: dict, formsemestre_id: int
) -> sco_cursus_dut.SituationEtudCursus:
"""renvoie une instance de SituationEtudCursus (ou sous-classe spécialisée)"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if formsemestre.formation.is_apc():
return cursus_but.SituationEtudCursusBUT(etud, formsemestre_id, nt)
parcours = nt.parcours
if parcours.ECTS_ONLY:
return sco_cursus_dut.SituationEtudCursusECTS(etud, formsemestre_id, nt)
return sco_cursus_dut.SituationEtudCursusClassic(etud, formsemestre_id, nt)
def formsemestre_get_etud_capitalisation(
formation_id: int, semestre_idx: int, date_debut, etudid: int
) -> list[dict]:
"""Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant.
Recherche dans les semestres de la même formation (code) avec le même
semestre_id et une date de début antérieure à celle du semestre mentionné.
Et aussi les UE externes validées.
Resultat: [ { 'formsemestre_id' :
'ue_id' : ue_id dans le semestre origine
'ue_code' :
'moy_ue' :
'event_date' :
'is_external'
} ]
"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""
SELECT DISTINCT SFV.*, ue.ue_code
FROM notes_ue ue, notes_formations nf,
notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
WHERE ue.formation_id = nf.id
and nf.formation_code = nf2.formation_code
and nf2.id=%(formation_id)s
and SFV.ue_id = ue.id
and SFV.code = 'ADM'
and SFV.etudid = %(etudid)s
and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s )
or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
) )
""",
{
"etudid": etudid,
"formation_id": formation_id,
"semestre_id": semestre_idx,
"date_debut": date_debut,
},
)
return cursor.dictfetchall()
def list_formsemestre_utilisateurs_uecap(formsemestre_id):
"""Liste des formsemestres pouvant utiliser une UE capitalisee de ce semestre
(et qui doivent donc etre sortis du cache si l'on modifie ce
semestre): meme code formation, meme semestre_id, date posterieure"""
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT sem.id
FROM notes_formsemestre sem, notes_formations F
WHERE sem.formation_id = F.id
and F.formation_code = %(formation_code)s
and sem.semestre_id = %(semestre_id)s
and sem.date_debut >= %(date_debut)s
and sem.id != %(formsemestre_id)s;
""",
{
"formation_code": F["formation_code"],
"semestre_id": sem["semestre_id"],
"formsemestre_id": formsemestre_id,
"date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
},
)
return [x[0] for x in cursor.fetchall()]

View File

@ -28,9 +28,10 @@
"""Semestres: gestion parcours DUT (Arreté du 13 août 2005) """Semestres: gestion parcours DUT (Arreté du 13 août 2005)
""" """
from app import db
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, UniteEns from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -105,27 +106,14 @@ class DecisionSem(object):
) )
) )
) )
# xxx debug
# log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) )
def SituationEtudParcours(etud: dict, formsemestre_id: int): class SituationEtudCursus:
"""renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)""" "Semestre dans un cursus"
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) pass
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# if formsemestre.formation.is_apc():
# return SituationEtudParcoursBUT(etud, formsemestre_id, nt)
parcours = nt.parcours
#
if parcours.ECTS_ONLY:
return SituationEtudParcoursECTS(etud, formsemestre_id, nt)
else:
return SituationEtudParcoursGeneric(etud, formsemestre_id, nt)
class SituationEtudParcoursGeneric: class SituationEtudCursusClassic(SituationEtudCursus):
"Semestre dans un parcours" "Semestre dans un parcours"
def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat): def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
@ -353,9 +341,7 @@ class SituationEtudParcoursGeneric:
)[0]["formation_code"] )[0]["formation_code"]
# si sem peut servir à compenser le semestre courant, positionne # si sem peut servir à compenser le semestre courant, positionne
# can_compensate # can_compensate
sem["can_compensate"] = check_compensation( sem["can_compensate"] = self.check_compensation_dut(sem, nt)
self.etudid, self.sem, self.nt, sem, nt
)
self.ue_acros = list(ue_acros.keys()) self.ue_acros = list(ue_acros.keys())
self.ue_acros.sort() self.ue_acros.sort()
@ -454,8 +440,7 @@ class SituationEtudParcoursGeneric:
break break
if not cur or cur["formsemestre_id"] != self.formsemestre_id: if not cur or cur["formsemestre_id"] != self.formsemestre_id:
log( log(
"*** SituationEtudParcours: search_prev: cur not found (formsemestre_id=%s, etudid=%s)" f"*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={self.formsemestre_id}, etudid={self.etudid})"
% (self.formsemestre_id, self.etudid)
) )
return None # pas de semestre courant !!! return None # pas de semestre courant !!!
# Cherche semestre antérieur de même formation (code) et semestre_id precedent # Cherche semestre antérieur de même formation (code) et semestre_id precedent
@ -633,31 +618,27 @@ class SituationEtudParcoursGeneric:
formsemestre_id=self.prev["formsemestre_id"] formsemestre_id=self.prev["formsemestre_id"]
) # > modif decisions jury (sem, UE) ) # > modif decisions jury (sem, UE)
# -- supprime autorisations venant de ce formsemestre
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
try: try:
cursor.execute( # -- Supprime autorisations venant de ce formsemestre
"""delete from scolar_autorisation_inscription autorisations = ScolarAutorisationInscription.query.filter_by(
where etudid = %(etudid)s and origin_formsemestre_id=%(origin_formsemestre_id)s etudid=self.etudid, origin_formsemestre_id=self.formsemestre_id
""",
{"etudid": self.etudid, "origin_formsemestre_id": self.formsemestre_id},
) )
for autorisation in autorisations:
# -- enregistre autorisations inscription db.session.delete(autorisation)
db.session.flush()
# -- Enregistre autorisations inscription
next_semestre_ids = self.get_next_semestre_ids(decision.devenir) next_semestre_ids = self.get_next_semestre_ids(decision.devenir)
for next_semestre_id in next_semestre_ids: for next_semestre_id in next_semestre_ids:
_scolar_autorisation_inscription_editor.create( autorisation = ScolarAutorisationInscription(
cnx, etudid=self.etudid,
{ formation_code=self.formation.formation_code,
"etudid": self.etudid, semestre_id=next_semestre_id,
"formation_code": self.formation.formation_code, origin_formsemestre_id=self.formsemestre_id,
"semestre_id": next_semestre_id,
"origin_formsemestre_id": self.formsemestre_id,
},
) )
cnx.commit() db.session.add(autorisation)
db.session.commit()
except: except:
cnx.rollback() cnx.session.rollback()
raise raise
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
formsemestre_id=self.formsemestre_id formsemestre_id=self.formsemestre_id
@ -672,12 +653,52 @@ class SituationEtudParcoursGeneric:
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) # > modif decision jury ) # > modif decision jury
def check_compensation_dut(self, semc: dict, ntc: NotesTableCompat):
"""Compensations DUT
Vérifie si le semestre sem peut se compenser en utilisant semc
- semc non utilisé par un autre semestre
- decision du jury prise ADM ou ADJ ou ATT ou ADC
- barres UE (moy ue > 8) dans sem et semc
- moyenne des moy_gen > 10
Return boolean
"""
# -- deja utilise ?
decc = ntc.get_etud_decision_sem(self.etudid)
if (
decc
and decc["compense_formsemestre_id"]
and decc["compense_formsemestre_id"] != self.sem["formsemestre_id"]
):
return False
# -- semestres consecutifs ?
if abs(self.sem["semestre_id"] - semc["semestre_id"]) != 1:
return False
# -- decision jury:
if decc and not decc["code"] in (ADM, ADJ, ATT, ADC):
return False
# -- barres UE et moyenne des moyennes:
moy_gen = self.nt.get_etud_moy_gen(self.etudid)
moy_genc = ntc.get_etud_moy_gen(self.etudid)
try:
moy_moy = (moy_gen + moy_genc) / 2
except: # un des semestres sans aucune note !
return False
class SituationEtudParcoursECTS(SituationEtudParcoursGeneric): if (
self.nt.etud_check_conditions_ues(self.etudid)[0]
and ntc.etud_check_conditions_ues(self.etudid)[0]
and moy_moy >= NOTES_BARRE_GEN_COMPENSATION
):
return True
else:
return False
class SituationEtudCursusECTS(SituationEtudCursusClassic):
"""Gestion parcours basés sur ECTS""" """Gestion parcours basés sur ECTS"""
def __init__(self, etud, formsemestre_id, nt): def __init__(self, etud, formsemestre_id, nt):
SituationEtudParcoursGeneric.__init__(self, etud, formsemestre_id, nt) SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt)
def could_be_compensated(self): def could_be_compensated(self):
return False # jamais de compensations dans ce parcours return False # jamais de compensations dans ce parcours
@ -718,47 +739,6 @@ class SituationEtudParcoursECTS(SituationEtudParcoursGeneric):
return choices return choices
#
def check_compensation(etudid, sem, nt, semc, ntc):
"""Verifie si le semestre sem peut se compenser en utilisant semc
- semc non utilisé par un autre semestre
- decision du jury prise ADM ou ADJ ou ATT ou ADC
- barres UE (moy ue > 8) dans sem et semc
- moyenne des moy_gen > 10
Return boolean
"""
# -- deja utilise ?
decc = ntc.get_etud_decision_sem(etudid)
if (
decc
and decc["compense_formsemestre_id"]
and decc["compense_formsemestre_id"] != sem["formsemestre_id"]
):
return False
# -- semestres consecutifs ?
if abs(sem["semestre_id"] - semc["semestre_id"]) != 1:
return False
# -- decision jury:
if decc and not decc["code"] in (ADM, ADJ, ATT, ADC):
return False
# -- barres UE et moyenne des moyennes:
moy_gen = nt.get_etud_moy_gen(etudid)
moy_genc = ntc.get_etud_moy_gen(etudid)
try:
moy_moy = (moy_gen + moy_genc) / 2
except: # un des semestres sans aucune note !
return False
if (
nt.etud_check_conditions_ues(etudid)[0]
and ntc.etud_check_conditions_ues(etudid)[0]
and moy_moy >= NOTES_BARRE_GEN_COMPENSATION
):
return True
else:
return False
# ------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------
@ -1020,9 +1000,9 @@ def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id):
""" """
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"""SELECT mi.* """SELECT mi.*
FROM notes_moduleimpl mi, notes_modules mo, notes_ue ue, notes_moduleimpl_inscription i FROM notes_moduleimpl mi, notes_modules mo, notes_ue ue, notes_moduleimpl_inscription i
WHERE i.etudid = %(etudid)s WHERE i.etudid = %(etudid)s
and i.moduleimpl_id=mi.id and i.moduleimpl_id=mi.id
and mi.formsemestre_id = %(formsemestre_id)s and mi.formsemestre_id = %(formsemestre_id)s
and mi.module_id = mo.id and mi.module_id = mo.id
@ -1032,102 +1012,3 @@ def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id):
) )
return len(cursor.fetchall()) return len(cursor.fetchall())
_scolar_autorisation_inscription_editor = ndb.EditableTable(
"scolar_autorisation_inscription",
"autorisation_inscription_id",
("etudid", "formation_code", "semestre_id", "date", "origin_formsemestre_id"),
output_formators={"date": ndb.DateISOtoDMY},
input_formators={"date": ndb.DateDMYtoISO},
)
scolar_autorisation_inscription_list = _scolar_autorisation_inscription_editor.list
def formsemestre_get_autorisation_inscription(etudid, origin_formsemestre_id):
"""Liste des autorisations d'inscription pour cet étudiant
émanant du semestre indiqué.
"""
cnx = ndb.GetDBConnexion()
return scolar_autorisation_inscription_list(
cnx, {"origin_formsemestre_id": origin_formsemestre_id, "etudid": etudid}
)
def formsemestre_get_etud_capitalisation(
formation_id: int, semestre_idx: int, date_debut, etudid: int
) -> list[dict]:
"""Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant.
Recherche dans les semestres de la même formation (code) avec le même
semestre_id et une date de début antérieure à celle du semestre mentionné.
Et aussi les UE externes validées.
Resultat: [ { 'formsemestre_id' :
'ue_id' : ue_id dans le semestre origine
'ue_code' :
'moy_ue' :
'event_date' :
'is_external'
} ]
"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""
SELECT DISTINCT SFV.*, ue.ue_code
FROM notes_ue ue, notes_formations nf,
notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
WHERE ue.formation_id = nf.id
and nf.formation_code = nf2.formation_code
and nf2.id=%(formation_id)s
and SFV.ue_id = ue.id
and SFV.code = 'ADM'
and SFV.etudid = %(etudid)s
and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s )
or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
) )
""",
{
"etudid": etudid,
"formation_id": formation_id,
"semestre_id": semestre_idx,
"date_debut": date_debut,
},
)
return cursor.dictfetchall()
def list_formsemestre_utilisateurs_uecap(formsemestre_id):
"""Liste des formsemestres pouvant utiliser une UE capitalisee de ce semestre
(et qui doivent donc etre sortis du cache si l'on modifie ce
semestre): meme code formation, meme semestre_id, date posterieure"""
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT sem.id
FROM notes_formsemestre sem, notes_formations F
WHERE sem.formation_id = F.id
and F.formation_code = %(formation_code)s
and sem.semestre_id = %(semestre_id)s
and sem.date_debut >= %(date_debut)s
and sem.id != %(formsemestre_id)s;
""",
{
"formation_code": F["formation_code"],
"semestre_id": sem["semestre_id"],
"formsemestre_id": formsemestre_id,
"date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
},
)
return [x[0] for x in cursor.fetchall()]

View File

@ -31,6 +31,7 @@ from flask import g, request
from flask_login import current_user from flask_login import current_user
from app import db from app import db
from app.but import apc_edit_ue
from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl
from app.models.validations import ScolarFormSemestreValidation from app.models.validations import ScolarFormSemestreValidation
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
@ -109,6 +110,7 @@ def html_edit_formation_apc(
icons=icons, icons=icons,
ues_by_sem=ues_by_sem, ues_by_sem=ues_by_sem,
ects_by_sem=ects_by_sem, ects_by_sem=ects_by_sem,
form_ue_choix_niveau=apc_edit_ue.form_ue_choix_niveau,
), ),
] ]
for semestre_idx in semestre_ids: for semestre_idx in semestre_ids:

View File

@ -292,21 +292,25 @@ def do_formation_create(args):
def do_formation_edit(args): def do_formation_edit(args):
"edit a formation" "edit a formation"
# log('do_formation_edit( args=%s )'%args)
# On autorise la modif de la formation meme si elle est verrouillee
# car cela ne change que du cosmetique, (sauf eventuellement le code formation ?)
# mais si verrouillée on ne peut changer le type de parcours
if sco_formations.formation_has_locked_sems(args["formation_id"]):
if "type_parcours" in args:
del args["type_parcours"]
# On ne peut jamais supprimer le code formation: # On ne peut jamais supprimer le code formation:
if "formation_code" in args and not args["formation_code"]: if "formation_code" in args and not args["formation_code"]:
del args["formation_code"] del args["formation_code"]
cnx = ndb.GetDBConnexion() formation: Formation = Formation.query.get_or_404(args["formation_id"])
sco_formations._formationEditor.edit(cnx, args) # On autorise la modif de la formation meme si elle est verrouillee
formation: Formation = Formation.query.get(args["formation_id"]) # car cela ne change que du cosmetique, (sauf eventuellement le code formation ?)
# mais si verrouillée on ne peut changer le type de parcours
if formation.has_locked_sems():
if "type_parcours" in args:
del args["type_parcours"]
for field in formation.__dict__:
if field and field[0] != "_" and field in args:
setattr(formation, field, args[field])
db.session.add(formation)
db.session.commit()
formation.invalidate_cached_sems() formation.invalidate_cached_sems()

View File

@ -140,7 +140,7 @@ def do_ue_create(args):
def do_ue_delete(ue_id, delete_validations=False, force=False): def do_ue_delete(ue_id, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)" "delete UE and attached matieres (but not modules)"
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus_dut
ue = UniteEns.query.get_or_404(ue_id) ue = UniteEns.query.get_or_404(ue_id)
formation = ue.formation formation = ue.formation
@ -164,7 +164,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
# raise ScoLockedFormError() # raise ScoLockedFormError()
# Il y a-t-il des etudiants ayant validé cette UE ? # Il y a-t-il des etudiants ayant validé cette UE ?
# si oui, propose de supprimer les validations # si oui, propose de supprimer les validations
validations = sco_parcours_dut.scolar_formsemestre_validation_list( validations = sco_cursus_dut.scolar_formsemestre_validation_list(
cnx, args={"ue_id": ue.id} cnx, args={"ue_id": ue.id}
) )
if validations and not delete_validations and not force: if validations and not delete_validations and not force:
@ -466,7 +466,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
+ ue_div + ue_div
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
elif tf[2]: elif tf[0] == 1:
if create: if create:
if not tf[2]["ue_code"]: if not tf[2]["ue_code"]:
del tf[2]["ue_code"] del tf[2]["ue_code"]
@ -684,6 +684,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
javascripts=[ javascripts=[
"libjs/jinplace-1.2.1.min.js", "libjs/jinplace-1.2.1.min.js",
"js/ue_list.js", "js/ue_list.js",
"js/edit_ue.js",
"libjs/jQuery-tagEditor/jquery.tag-editor.min.js", "libjs/jQuery-tagEditor/jquery.tag-editor.min.js",
"libjs/jQuery-tagEditor/jquery.caret.min.js", "libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js", "js/module_tag_editor.js",

View File

@ -45,6 +45,7 @@ from openpyxl.worksheet.worksheet import Worksheet
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import notesdb, sco_preferences
class COLORS(Enum): class COLORS(Enum):
@ -793,7 +794,7 @@ def excel_feuille_listeappel(
# ligne 3 # ligne 3
cell_2 = ws.make_cell("Enseignant :", style2) cell_2 = ws.make_cell("Enseignant :", style2)
cell_6 = ws.make_cell(("Groupe %s" % groupname), style3) cell_6 = ws.make_cell(f"Groupe {groupname}", style3)
ws.append_row([None, cell_2, None, None, None, None, cell_6]) ws.append_row([None, cell_2, None, None, None, None, cell_6])
# ligne 4: Avertissement pour ne pas confondre avec listes notes # ligne 4: Avertissement pour ne pas confondre avec listes notes

View File

@ -101,7 +101,7 @@ def get_formsemestre(formsemestre_id, raise_soft_exc=False):
return g.stored_get_formsemestre[formsemestre_id] return g.stored_get_formsemestre[formsemestre_id]
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
log(f"get_formsemestre: invalid id '{formsemestre_id}'") log(f"get_formsemestre: invalid id '{formsemestre_id}'")
raise ScoInvalidIdType("formsemestre_id must be an integer !") raise ScoInvalidIdType("get_formsemestre: formsemestre_id must be an integer !")
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id}) sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
if not sems: if not sems:
log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id) log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id)

View File

@ -60,7 +60,7 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups_copy from app.scodoc import sco_groups_copy
from app.scodoc import sco_modalites from app.scodoc import sco_modalites
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus_dut
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -1362,14 +1362,14 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
if e["ue_id"]: if e["ue_id"]:
e["ue_id"] = ues_old2new[e["ue_id"]] e["ue_id"] = ues_old2new[e["ue_id"]]
sco_etud.scolar_events_edit(cnx, e) sco_etud.scolar_events_edit(cnx, e)
validations = sco_parcours_dut.scolar_formsemestre_validation_list( validations = sco_cursus_dut.scolar_formsemestre_validation_list(
cnx, args={"formsemestre_id": formsemestre_id} cnx, args={"formsemestre_id": formsemestre_id}
) )
for e in validations: for e in validations:
if e["ue_id"]: if e["ue_id"]:
e["ue_id"] = ues_old2new[e["ue_id"]] e["ue_id"] = ues_old2new[e["ue_id"]]
# log('e=%s' % e ) # log('e=%s' % e )
sco_parcours_dut.scolar_formsemestre_validation_edit(cnx, e) sco_cursus_dut.scolar_formsemestre_validation_edit(cnx, e)
def formsemestre_delete(formsemestre_id): def formsemestre_delete(formsemestre_id):

View File

@ -51,7 +51,7 @@ from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus_dut
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -450,7 +450,7 @@ def _list_ue_with_coef_and_validations(sem, etudid):
else: else:
ue["uecoef"] = {} ue["uecoef"] = {}
# add validation # add validation
validation = sco_parcours_dut.scolar_formsemestre_validation_list( validation = sco_cursus_dut.scolar_formsemestre_validation_list(
cnx, cnx,
args={ args={
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,

View File

@ -59,8 +59,9 @@ from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus
from app.scodoc.sco_parcours_dut import etud_est_inscrit_ue from app.scodoc import sco_cursus_dut
from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
from app.scodoc import sco_photos 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
@ -108,7 +109,7 @@ def formsemestre_validation_etud_form(
check = True check = True
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
if not Se.sem["etat"]: if not Se.sem["etat"]:
raise ScoValueError("validation: semestre verrouille") raise ScoValueError("validation: semestre verrouille")
@ -274,15 +275,12 @@ def formsemestre_validation_etud_form(
ass = "non assidu" ass = "non assidu"
H.append("<p>Décision existante du %(event_date)s: %(code)s" % decision_jury) H.append("<p>Décision existante du %(event_date)s: %(code)s" % decision_jury)
H.append(" (%s)" % ass) H.append(" (%s)" % ass)
auts = sco_parcours_dut.formsemestre_get_autorisation_inscription( autorisations = ScolarAutorisationInscription.query.filter_by(
etudid, formsemestre_id etudid=etudid, origin_formsemestre_id=formsemestre_id
) ).all()
if auts: if autorisations:
H.append(". Autorisé%s à s'inscrire en " % etud["ne"]) H.append(". Autorisé%s à s'inscrire en " % etud["ne"])
alist = [] H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".")
for aut in auts:
alist.append(str(aut["semestre_id"]))
H.append(", ".join(["S%s" % x for x in alist]) + ".")
H.append("</p>") H.append("</p>")
# Cas particulier pour ATJ: corriger precedent avant de continuer # Cas particulier pour ATJ: corriger precedent avant de continuer
@ -382,7 +380,7 @@ def formsemestre_validation_etud(
): ):
"""Enregistre validation""" """Enregistre validation"""
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
# retrouve la decision correspondant au code: # retrouve la decision correspondant au code:
choices = Se.get_possible_choices(assiduite=True) choices = Se.get_possible_choices(assiduite=True)
choices += Se.get_possible_choices(assiduite=False) choices += Se.get_possible_choices(assiduite=False)
@ -415,7 +413,7 @@ def formsemestre_validation_etud_manu(
if assidu: if assidu:
assidu = True assidu = True
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
if code_etat in Se.parcours.UNUSED_CODES: if code_etat in Se.parcours.UNUSED_CODES:
raise ScoValueError("code decision invalide dans ce parcours") raise ScoValueError("code decision invalide dans ce parcours")
# Si code ADC, extrait le semestre utilisé: # Si code ADC, extrait le semestre utilisé:
@ -430,7 +428,7 @@ def formsemestre_validation_etud_manu(
formsemestre_id_utilise_pour_compenser = None formsemestre_id_utilise_pour_compenser = None
# Construit le choix correspondant: # Construit le choix correspondant:
choice = sco_parcours_dut.DecisionSem( choice = sco_cursus_dut.DecisionSem(
code_etat=code_etat, code_etat=code_etat,
new_code_prev=new_code_prev, new_code_prev=new_code_prev,
devenir=devenir, devenir=devenir,
@ -873,10 +871,9 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
# ----------- # -----------
def formsemestre_validation_auto(formsemestre_id): def formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre" "Formulaire saisie automatisee des decisions d'un semestre"
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [ H = [
html_sco_header.html_sem_header("Saisie automatique des décisions du semestre"), html_sco_header.html_sem_header("Saisie automatique des décisions du semestre"),
""" f"""
<ul> <ul>
<li>Seuls les étudiants qui obtiennent le semestre seront affectés (code ADM, moyenne générale et <li>Seuls les étudiants qui obtiennent le semestre seront affectés (code ADM, moyenne générale et
toutes les barres, semestre précédent validé);</li> toutes les barres, semestre précédent validé);</li>
@ -888,12 +885,11 @@ def formsemestre_validation_auto(formsemestre_id):
<p>Il est donc vivement conseillé de relire soigneusement les décisions à l'issue <p>Il est donc vivement conseillé de relire soigneusement les décisions à l'issue
de cette procédure !</p> de cette procédure !</p>
<form action="do_formsemestre_validation_auto"> <form action="do_formsemestre_validation_auto">
<input type="hidden" name="formsemestre_id" value="%s"/> <input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
<input type="submit" value="Calculer automatiquement ces décisions"/> <input type="submit" value="Calculer automatiquement ces décisions"/>
<p><em>Le calcul prend quelques minutes, soyez patients !</em></p> <p><em>Le calcul prend quelques minutes, soyez patients !</em></p>
</form> </form>
""" """,
% formsemestre_id,
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
return "\n".join(H) return "\n".join(H)
@ -908,55 +904,56 @@ def do_formsemestre_validation_auto(formsemestre_id):
etudids = nt.get_etudids() etudids = nt.get_etudids()
nb_valid = 0 nb_valid = 0
conflicts = [] # liste des etudiants avec decision differente déjà saisie conflicts = [] # liste des etudiants avec decision differente déjà saisie
for etudid in etudids: with sco_cache.DeferredSemCacheManager():
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] for etudid in etudids:
Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
{"etudid": etudid, "formsemestre_id": formsemestre_id} ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
)[0] {"etudid": etudid, "formsemestre_id": formsemestre_id}
)[0]
# Conditions pour validation automatique: # Conditions pour validation automatique:
if ins["etat"] == "I" and ( if ins["etat"] == "I" and (
( (
(not Se.prev) (not Se.prev)
or (Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ)) or (
) Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ)
and Se.barre_moy_ok )
and Se.barres_ue_ok
and not etud_has_notes_attente(etudid, formsemestre_id)
):
# check: s'il existe une decision ou autorisation et qu'elles sont differentes,
# warning (et ne fait rien)
decision_sem = nt.get_etud_decision_sem(etudid)
ok = True
if decision_sem and decision_sem["code"] != ADM:
ok = False
conflicts.append(etud)
autorisations = sco_parcours_dut.formsemestre_get_autorisation_inscription(
etudid, formsemestre_id
)
if (
len(autorisations) != 0
): # accepte le cas ou il n'y a pas d'autorisation : BUG 23/6/7, A RETIRER ENSUITE
if (
len(autorisations) != 1
or autorisations[0]["semestre_id"] != next_semestre_id
):
if ok:
conflicts.append(etud)
ok = False
# ok, valide !
if ok:
formsemestre_validation_etud_manu(
formsemestre_id,
etudid,
code_etat=ADM,
devenir="NEXT",
assidu=True,
redirect=False,
) )
nb_valid += 1 and Se.barre_moy_ok
and Se.barres_ue_ok
and not etud_has_notes_attente(etudid, formsemestre_id)
):
# check: s'il existe une decision ou autorisation et qu'elles sont differentes,
# warning (et ne fait rien)
decision_sem = nt.get_etud_decision_sem(etudid)
ok = True
if decision_sem and decision_sem["code"] != ADM:
ok = False
conflicts.append(etud)
autorisations = ScolarAutorisationInscription.query.filter_by(
etudid=etudid, origin_formsemestre_id=formsemestre_id
).all()
if len(autorisations) != 0:
if (
len(autorisations) > 1
or autorisations[0].semestre_id != next_semestre_id
):
if ok:
conflicts.append(etud)
ok = False
# ok, valide !
if ok:
formsemestre_validation_etud_manu(
formsemestre_id,
etudid,
code_etat=ADM,
devenir="NEXT",
assidu=True,
redirect=False,
)
nb_valid += 1
log( log(
"do_formsemestre_validation_auto: %d validations, %d conflicts" "do_formsemestre_validation_auto: %d validations, %d conflicts"
% (nb_valid, len(conflicts)) % (nb_valid, len(conflicts))
@ -1176,7 +1173,7 @@ def do_formsemestre_validate_previous_ue(
) )
else: else:
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id) sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id)
sco_parcours_dut.do_formsemestre_validate_ue( sco_cursus_dut.do_formsemestre_validate_ue(
cnx, cnx,
nt, nt,
formsemestre_id, # "importe" cette UE dans le semestre (new 3/2015) formsemestre_id, # "importe" cette UE dans le semestre (new 3/2015)

View File

@ -56,8 +56,9 @@ import app.scodoc.notesdb as ndb
from app import log, cache from app import log, cache
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cursus
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_xml from app.scodoc import sco_xml
@ -1489,13 +1490,13 @@ def _get_prev_moy(etudid, formsemestre_id):
"""Donne la derniere moyenne generale calculee pour cette étudiant, """Donne la derniere moyenne generale calculee pour cette étudiant,
ou 0 si on n'en trouve pas (nouvel inscrit,...). ou 0 si on n'en trouve pas (nouvel inscrit,...).
""" """
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus_dut
info = sco_etud.get_etud_info(etudid=etudid, filled=True) info = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not info: if not info:
raise ScoValueError("etudiant invalide: etudid=%s" % etudid) raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
etud = info[0] etud = info[0]
Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
if Se.prev: if Se.prev:
prev_sem = FormSemestre.query.get(Se.prev["formsemestre_id"]) prev_sem = FormSemestre.query.get(Se.prev["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem) nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)

View File

@ -49,7 +49,7 @@ from app.scodoc import sco_excel
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_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -776,7 +776,7 @@ def groups_table(
m.update(etud) m.update(etud)
sco_etud.etud_add_lycee_infos(etud) sco_etud.etud_add_lycee_infos(etud)
# et ajoute le parcours # et ajoute le parcours
Se = sco_parcours_dut.SituationEtudParcours( Se = sco_cursus.get_situation_etud_cursus(
etud, groups_infos.formsemestre_id etud, groups_infos.formsemestre_id
) )
m["parcours"] = Se.get_parcours_descr() m["parcours"] = Se.get_parcours_descr()

View File

@ -39,7 +39,7 @@ from app.models import ModuleImpl
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoInvalidIdType from app.scodoc.sco_exceptions import ScoInvalidIdType
from app.scodoc.sco_parcours_dut import formsemestre_has_decisions from app.scodoc.sco_cursus_dut import formsemestre_has_decisions
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header from app.scodoc import html_sco_header

View File

@ -46,7 +46,7 @@ from app.scodoc import sco_codes_parcours
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus
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_photos
from app.scodoc import sco_users from app.scodoc import sco_users
@ -269,7 +269,7 @@ def ficheEtud(etudid=None):
sem_info[sem["formsemestre_id"]] = grlink sem_info[sem["formsemestre_id"]] = grlink
if info["sems"]: if info["sems"]:
Se = sco_parcours_dut.SituationEtudParcours(etud, info["last_formsemestre_id"]) Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"])
info["liste_inscriptions"] = formsemestre_recap_parcours_table( info["liste_inscriptions"] = formsemestre_recap_parcours_table(
Se, Se,
etudid, etudid,

View File

@ -55,6 +55,7 @@ from reportlab.lib import styles
from flask import g from flask import g
from app.scodoc import sco_utils as scu
from app.scodoc.sco_utils import CONFIG from app.scodoc.sco_utils import CONFIG
from app import log from app import log
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
@ -67,7 +68,7 @@ PAGE_WIDTH = defaultPageSize[0]
DEFAULT_PDF_FOOTER_TEMPLATE = CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE DEFAULT_PDF_FOOTER_TEMPLATE = CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE
def SU(s): def SU(s: str) -> str:
"convert s from string to string suitable for ReportLab" "convert s from string to string suitable for ReportLab"
if not s: if not s:
return "" return ""
@ -145,9 +146,9 @@ def makeParas(txt, style, suppress_empty=False):
) from e ) from e
else: else:
raise e raise e
except Exception as e: except Exception as exc:
log(traceback.format_exc()) log(traceback.format_exc())
log("Invalid pdf para format: %s" % txt) log(f"Invalid pdf para format: {txt}")
try: try:
result = [ result = [
Paragraph( Paragraph(
@ -155,13 +156,14 @@ def makeParas(txt, style, suppress_empty=False):
style, style,
) )
] ]
except ValueError as e: # probleme font ? essaye sans style except ValueError as exc2: # probleme font ? essaye sans style
# recupere font en cause ? # recupere font en cause ?
m = re.match(r".*family/bold/italic for (.*)", e.args[0], re.DOTALL) m = re.match(r".*family/bold/italic for (.*)", e.args[0], re.DOTALL)
if m: if m:
message = f"police non disponible: {m[1]}" message = f"police non disponible: {m[1]}"
else: else:
message = "format invalide" message = "format invalide"
scu.flash_once(f"problème génération PDF: {message}")
return [ return [
Paragraph( Paragraph(
SU(f'<font color="red"><b>Erreur: {message}</b></font>'), SU(f'<font color="red"><b>Erreur: {message}</b></font>'),

View File

@ -24,14 +24,14 @@ def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
seul le directeur des études peut saisir des notes (et il ne devrait pas). seul le directeur des études peut saisir des notes (et il ne devrait pas).
""" """
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus_dut
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if not sem["etat"]: if not sem["etat"]:
return False # semestre verrouillé return False # semestre verrouillé
if sco_parcours_dut.formsemestre_has_decisions(sem["formsemestre_id"]): if sco_cursus_dut.formsemestre_has_decisions(sem["formsemestre_id"]):
# il y a des décisions de jury dans ce semestre ! # il y a des décisions de jury dans ce semestre !
return ( return (
authuser.has_permission(Permission.ScoEditAllNotes) authuser.has_permission(Permission.ScoEditAllNotes)

View File

@ -37,14 +37,14 @@ from flask_login import current_user
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, Identite from app.models import FormSemestre, Identite, ScolarAutorisationInscription
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import sco_version import sco_version
@ -78,7 +78,7 @@ def feuille_preparation_jury(formsemestre_id):
nbabs = {} nbabs = {}
nbabsjust = {} nbabsjust = {}
for etud in etuds: for etud in etuds:
Se = sco_parcours_dut.SituationEtudParcours( Se = sco_cursus.get_situation_etud_cursus(
etud.to_dict_scodoc7(), formsemestre_id etud.to_dict_scodoc7(), formsemestre_id
) )
if Se.prev: if Se.prev:
@ -103,14 +103,14 @@ def feuille_preparation_jury(formsemestre_id):
moy[etud.id] = nt.get_etud_moy_gen(etud.id) moy[etud.id] = nt.get_etud_moy_gen(etud.id)
for ue in nt.get_ues_stat_dict(filter_sport=True): for ue in nt.get_ues_stat_dict(filter_sport=True):
ue_status = nt.get_etud_ue_status(etud.id, ue["ue_id"]) ue_status = nt.get_etud_ue_status(etud.id, ue["ue_id"])
ue_code_s = ue["ue_code"] + "_%s" % nt.sem["semestre_id"] ue_code_s = f'{ue["ue_code"]}_{nt.sem["semestre_id"]}'
moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else "" moy_ue[ue_code_s][etud.id] = ue_status["moy"] if ue_status else ""
ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"]) ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"])
if Se.prev: if Se.prev:
try: try:
moy_inter[etud.id] = (moy[etud.id] + prev_moy[etud.id]) / 2.0 moy_inter[etud.id] = (moy[etud.id] + prev_moy[etud.id]) / 2.0
except: except (KeyError, TypeError):
pass pass
decision = nt.get_etud_decision_sem(etud.id) decision = nt.get_etud_decision_sem(etud.id)
@ -119,10 +119,13 @@ def feuille_preparation_jury(formsemestre_id):
if decision["compense_formsemestre_id"]: if decision["compense_formsemestre_id"]:
code[etud.id] += "+" # indique qu'il a servi a compenser code[etud.id] += "+" # indique qu'il a servi a compenser
assidu[etud.id] = {False: "Non", True: "Oui"}.get(decision["assidu"], "") assidu[etud.id] = {False: "Non", True: "Oui"}.get(decision["assidu"], "")
aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription(
etud.id, formsemestre_id autorisations_etud = ScolarAutorisationInscription.query.filter_by(
etudid=etud.id, origin_formsemestre_id=formsemestre_id
).all()
autorisations[etud.id] = ", ".join(
[f"S{x.semestre_id}" for x in autorisations_etud]
) )
autorisations[etud.id] = ", ".join(["S%s" % x["semestre_id"] for x in aut_list])
# parcours: # parcours:
parcours[etud.id] = Se.get_parcours_descr() parcours[etud.id] = Se.get_parcours_descr()
# groupe principal (td) # groupe principal (td)
@ -153,11 +156,11 @@ def feuille_preparation_jury(formsemestre_id):
sid = sem["semestre_id"] sid = sem["semestre_id"]
sn = sp = "" sn = sp = ""
if sid >= 0: if sid >= 0:
sn = "S%s" % sid sn = f"S{sid}"
if prev_moy: # si qq chose dans precedent if prev_moy: # si qq chose dans precedent
sp = "S%s" % (sid - 1) sp = f"S{sid - 1}"
ws = sco_excel.ScoExcelSheet(sheet_name="Prepa Jury %s" % sn) sheet = sco_excel.ScoExcelSheet(sheet_name=f"Prepa Jury {sn}")
# génération des styles # génération des styles
style_bold = sco_excel.excel_make_style(size=10, bold=True) style_bold = sco_excel.excel_make_style(size=10, bold=True)
style_center = sco_excel.excel_make_style(halign="center") style_center = sco_excel.excel_make_style(halign="center")
@ -173,10 +176,10 @@ def feuille_preparation_jury(formsemestre_id):
) )
# Première ligne # Première ligne
ws.append_single_cell_row( sheet.append_single_cell_row(
"Feuille préparation Jury %s" % scu.unescape_html(sem["titreannee"]), style_bold "Feuille préparation Jury %s" % scu.unescape_html(sem["titreannee"]), style_bold
) )
ws.append_blank_row() sheet.append_blank_row()
# Ligne de titre # Ligne de titre
titles = ["Rang"] titles = ["Rang"]
@ -198,25 +201,25 @@ def feuille_preparation_jury(formsemestre_id):
] ]
if prev_moy: # si qq chose dans precedent if prev_moy: # si qq chose dans precedent
titles += [prev_ue_acro[x][1] for x in ue_prev_codes] + [ titles += [prev_ue_acro[x][1] for x in ue_prev_codes] + [
"Moy %s" % sp, f"Moy {sp}",
"Décision %s" % sp, f"Décision {sp}",
] ]
titles += [ue_acro[x][1] for x in ue_codes] + ["Moy %s" % sn] titles += [ue_acro[x][1] for x in ue_codes] + [f"Moy {sn}"]
if moy_inter: if moy_inter:
titles += ["Moy %s-%s" % (sp, sn)] titles += [f"Moy {sp}-{sn}"]
titles += ["Abs", "Abs Injust."] titles += ["Abs", "Abs Injust."]
if code: if code:
titles.append("Proposit. %s" % sn) titles.append("Proposit. {sn}")
if autorisations: if autorisations:
titles.append("Autorisations") titles.append("Autorisations")
# titles.append('Assidu') # titles.append('Assidu')
ws.append_row(ws.make_row(titles, style_boldcenter)) sheet.append_row(sheet.make_row(titles, style_boldcenter))
if prev_moy: # if prev_moy:
tit_prev_moy = "Moy " + sp # tit_prev_moy = "Moy " + sp
col_prev_moy = titles.index(tit_prev_moy) # # col_prev_moy = titles.index(tit_prev_moy)
tit_moy = "Moy " + sn # tit_moy = "Moy " + sn
col_moy = titles.index(tit_moy) # col_moy = titles.index(tit_moy)
col_abs = titles.index("Abs") # col_abs = titles.index("Abs")
def fmt(x): def fmt(x):
"reduit les notes a deux chiffres" "reduit les notes a deux chiffres"
@ -229,13 +232,13 @@ def feuille_preparation_jury(formsemestre_id):
i = 1 # numero etudiant i = 1 # numero etudiant
for etud in etuds: for etud in etuds:
cells = [] cells = []
cells.append(ws.make_cell(str(i))) cells.append(sheet.make_cell(str(i)))
if sco_preferences.get_preference("prepa_jury_nip"): if sco_preferences.get_preference("prepa_jury_nip"):
cells.append(ws.make_cell(etud.code_nip)) cells.append(sheet.make_cell(etud.code_nip))
if sco_preferences.get_preference("prepa_jury_ine"): if sco_preferences.get_preference("prepa_jury_ine"):
cells.append(ws.make_cell(etud.code_ine)) cells.append(sheet.make_cell(etud.code_ine))
admission = etud.admission.first() admission = etud.admission.first()
cells += ws.make_row( cells += sheet.make_row(
[ [
etud.id, etud.id,
etud.civilite_str, etud.civilite_str,
@ -253,50 +256,52 @@ def feuille_preparation_jury(formsemestre_id):
if prev_moy: if prev_moy:
for ue_acro in ue_prev_codes: for ue_acro in ue_prev_codes:
cells.append( cells.append(
ws.make_cell( sheet.make_cell(
fmt(prev_moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note fmt(prev_moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note
) )
) )
co += 1 co += 1
cells.append( cells.append(
ws.make_cell(fmt(prev_moy.get(etud.id, "")), style_bold) sheet.make_cell(fmt(prev_moy.get(etud.id, "")), style_bold)
) # moy gen prev ) # moy gen prev
cells.append( cells.append(
ws.make_cell(fmt(prev_code.get(etud.id, "")), style_moy) sheet.make_cell(fmt(prev_code.get(etud.id, "")), style_moy)
) # decision prev ) # decision prev
co += 2 co += 2
for ue_acro in ue_codes: for ue_acro in ue_codes:
cells.append( cells.append(
ws.make_cell(fmt(moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note) sheet.make_cell(
fmt(moy_ue.get(ue_acro, {}).get(etud.id, "")), style_note
)
) )
co += 1 co += 1
cells.append( cells.append(
ws.make_cell(fmt(moy.get(etud.id, "")), style_note_bold) sheet.make_cell(fmt(moy.get(etud.id, "")), style_note_bold)
) # moy gen ) # moy gen
co += 1 co += 1
if moy_inter: if moy_inter:
cells.append(ws.make_cell(fmt(moy_inter.get(etud.id, "")), style_note)) cells.append(sheet.make_cell(fmt(moy_inter.get(etud.id, "")), style_note))
cells.append(ws.make_cell(str(nbabs.get(etud.id, "")), style_center)) cells.append(sheet.make_cell(str(nbabs.get(etud.id, "")), style_center))
cells.append(ws.make_cell(str(nbabsjust.get(etud.id, "")), style_center)) cells.append(sheet.make_cell(str(nbabsjust.get(etud.id, "")), style_center))
if code: if code:
cells.append(ws.make_cell(code.get(etud.id, ""), style_moy)) cells.append(sheet.make_cell(code.get(etud.id, ""), style_moy))
cells.append(ws.make_cell(autorisations.get(etud.id, ""), style_moy)) cells.append(sheet.make_cell(autorisations.get(etud.id, ""), style_moy))
# l.append(assidu.get(etud.id, '')) # l.append(assidu.get(etud.id, ''))
ws.append_row(cells) sheet.append_row(cells)
i += 1 i += 1
# #
ws.append_blank_row() sheet.append_blank_row()
# Explications des codes # Explications des codes
codes = list(sco_codes_parcours.CODES_EXPL.keys()) codes = list(sco_codes_parcours.CODES_EXPL.keys())
codes.sort() codes.sort()
ws.append_single_cell_row("Explication des codes") sheet.append_single_cell_row("Explication des codes")
for code in codes: for code in codes:
ws.append_row( sheet.append_row(
ws.make_row(["", "", "", code, sco_codes_parcours.CODES_EXPL[code]]) sheet.make_row(["", "", "", code, sco_codes_parcours.CODES_EXPL[code]])
) )
ws.append_row( sheet.append_row(
ws.make_row( sheet.make_row(
[ [
"", "",
"", "",
@ -307,16 +312,16 @@ def feuille_preparation_jury(formsemestre_id):
) )
) )
# UE : Correspondances acronyme et titre complet # UE : Correspondances acronyme et titre complet
ws.append_blank_row() sheet.append_blank_row()
ws.append_single_cell_row("Titre des UE") sheet.append_single_cell_row("Titre des UE")
if prev_moy: if prev_moy:
for ue in ntp.get_ues_stat_dict(filter_sport=True): for ue in ntp.get_ues_stat_dict(filter_sport=True):
ws.append_row(ws.make_row(["", "", "", ue["acronyme"], ue["titre"]])) sheet.append_row(sheet.make_row(["", "", "", ue["acronyme"], ue["titre"]]))
for ue in nt.get_ues_stat_dict(filter_sport=True): for ue in nt.get_ues_stat_dict(filter_sport=True):
ws.append_row(ws.make_row(["", "", "", ue["acronyme"], ue["titre"]])) sheet.append_row(sheet.make_row(["", "", "", ue["acronyme"], ue["titre"]]))
# #
ws.append_blank_row() sheet.append_blank_row()
ws.append_single_cell_row( sheet.append_single_cell_row(
"Préparé par %s le %s sur %s pour %s" "Préparé par %s le %s sur %s pour %s"
% ( % (
sco_version.SCONAME, sco_version.SCONAME,
@ -325,7 +330,7 @@ def feuille_preparation_jury(formsemestre_id):
current_user, current_user,
) )
) )
xls = ws.generate() xls = sheet.generate()
flash("Feuille préparation jury générée") flash("Feuille préparation jury générée")
return scu.send_file( return scu.send_file(
xls, xls,

View File

@ -57,32 +57,38 @@ from flask import g, request
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, UniteEns from app.models import (
FormSemestre,
UniteEns,
ScolarAutorisationInscription,
but_validations,
)
from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache from app.scodoc import sco_cursus
from app.scodoc import sco_cursus_dut
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_formations 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_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_pvpdf from app.scodoc import sco_pvpdf
from app.scodoc import sco_etud
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID
from app.scodoc.sco_pdf import PDFLOCK from app.scodoc.sco_pdf import PDFLOCK
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem): def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]:
"""Liste des UE validées dans ce semestre""" """Liste des UE validées dans ce semestre (incluant les UE capitalisées)"""
if not decisions_ue: if not decisions_ue:
return [] return []
uelist = [] uelist = []
@ -90,17 +96,20 @@ def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem):
for ue_id in decisions_ue.keys(): for ue_id in decisions_ue.keys():
try: try:
if decisions_ue[ue_id] and ( if decisions_ue[ue_id] and (
decisions_ue[ue_id]["code"] == sco_codes_parcours.ADM sco_codes_parcours.code_ue_validant(decisions_ue[ue_id]["code"])
or ( or (
# XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8 # XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8
scu.CONFIG.CAPITALIZE_ALL_UES decision_sem
and scu.CONFIG.CAPITALIZE_ALL_UES
and sco_codes_parcours.code_semestre_validant(decision_sem["code"]) and sco_codes_parcours.code_semestre_validant(decision_sem["code"])
) )
): ):
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
uelist.append(ue) uelist.append(ue)
except: except:
log("descr_decisions_ues: ue_id=%s decisions_ue=%s" % (ue_id, decisions_ue)) log(
f"Exception in descr_decisions_ues: ue_id={ue_id} decisions_ue={decisions_ue}"
)
# Les UE capitalisées dans d'autres semestres: # Les UE capitalisées dans d'autres semestres:
if etudid in nt.validations.ue_capitalisees.index: if etudid in nt.validations.ue_capitalisees.index:
for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]: for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]:
@ -138,12 +147,9 @@ def _descr_decision_sem_abbrev(etat, decision_sem):
return decision return decision
def descr_autorisations(autorisations): def descr_autorisations(autorisations: list[ScolarAutorisationInscription]) -> str:
"résumé textuel des autorisations d'inscription (-> 'S1, S3' )" "résumé textuel des autorisations d'inscription (-> 'S1, S3' )"
alist = [] return ", ".join([f"S{a.semestre_id}" for a in autorisations])
for aut in autorisations:
alist.append("S" + str(aut["semestre_id"]))
return ", ".join(alist)
def _comp_ects_by_ue_code(nt, decision_ues): def _comp_ects_by_ue_code(nt, decision_ues):
@ -233,8 +239,11 @@ def dict_pvjury(
L = [] L = []
D = {} # même chose que L, mais { etudid : dec } D = {} # même chose que L, mais { etudid : dec }
for etudid in etudids: for etudid in etudids:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] # etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id) etud: Identite = Identite.query.get(etudid)
Se = sco_cursus.get_situation_etud_cursus(
etud.to_dict_scodoc7(), formsemestre_id
)
semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal
d = {} d = {}
d["identite"] = nt.identdict[etudid] d["identite"] = nt.identdict[etudid]
@ -243,6 +252,8 @@ def dict_pvjury(
) # I|D|DEF (inscription ou démission ou défaillant) ) # I|D|DEF (inscription ou démission ou défaillant)
d["decision_sem"] = nt.get_etud_decision_sem(etudid) d["decision_sem"] = nt.get_etud_decision_sem(etudid)
d["decisions_ue"] = nt.get_etud_decision_ues(etudid) d["decisions_ue"] = nt.get_etud_decision_ues(etudid)
if formsemestre.formation.is_apc():
d.update(but_validations.dict_decision_jury(etud, formsemestre))
d["last_formsemestre_id"] = Se.get_semestres()[ d["last_formsemestre_id"] = Se.get_semestres()[
-1 -1
] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit ] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit
@ -280,17 +291,18 @@ def dict_pvjury(
else: else:
d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"]) d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"])
d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription( autorisations = ScolarAutorisationInscription.query.filter_by(
etudid, formsemestre_id etudid=etudid, origin_formsemestre_id=formsemestre_id
) ).all()
d["autorisations_descr"] = descr_autorisations(d["autorisations"]) d["autorisations"] = [a.to_dict() for a in autorisations]
d["autorisations_descr"] = descr_autorisations(autorisations)
d["validation_parcours"] = Se.parcours_validated() d["validation_parcours"] = Se.parcours_validated()
d["parcours"] = Se.get_parcours_descr(filter_futur=True) d["parcours"] = Se.get_parcours_descr(filter_futur=True)
if with_parcours_decisions: if with_parcours_decisions:
d["parcours_decisions"] = Se.get_parcours_decisions() d["parcours_decisions"] = Se.get_parcours_decisions()
# Observations sur les compensations: # Observations sur les compensations:
compensators = sco_parcours_dut.scolar_formsemestre_validation_list( compensators = sco_cursus_dut.scolar_formsemestre_validation_list(
cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid} cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
) )
obs = [] obs = []
@ -307,12 +319,7 @@ def dict_pvjury(
d["decision_sem"]["compense_formsemestre_id"] d["decision_sem"]["compense_formsemestre_id"]
) )
obs.append( obs.append(
"%s compense %s (%s)" f"""{sem["sem_id_txt"]} compense {compensed["sem_id_txt"]} ({compensed["anneescolaire"]})"""
% (
sem["sem_id_txt"],
compensed["sem_id_txt"],
compensed["anneescolaire"],
)
) )
d["observation"] = ", ".join(obs) d["observation"] = ", ".join(obs)

View File

@ -30,9 +30,11 @@
import io import io
import re import re
from PIL import Image as PILImage
import reportlab import reportlab
from reportlab.lib.units import cm, mm from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_RIGHT, TA_JUSTIFY from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_JUSTIFY
from reportlab.platypus import Paragraph, Spacer, Frame, PageBreak from reportlab.platypus import Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image from reportlab.platypus import Table, TableStyle, Image
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
@ -41,16 +43,16 @@ from reportlab.lib import styles
from reportlab.lib.colors import Color from reportlab.lib.colors import Color
from flask import g from flask import g
from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_parcours_dut import SituationEtudParcours from app.scodoc.sco_cursus_dut import SituationEtudCursus
from app.scodoc.sco_pdf import SU from app.scodoc.sco_pdf import SU
import sco_version import sco_version
@ -125,6 +127,7 @@ def page_footer(canvas, doc, logo, preferences, with_page_numbers=True):
def page_header(canvas, doc, logo, preferences, only_on_first_page=False): def page_header(canvas, doc, logo, preferences, only_on_first_page=False):
"Ajoute au canvas le frame avec le logo"
if only_on_first_page and int(doc.page) > 1: if only_on_first_page and int(doc.page) > 1:
return return
height = doc.pagesize[1] height = doc.pagesize[1]
@ -147,12 +150,12 @@ def page_header(canvas, doc, logo, preferences, only_on_first_page=False):
class CourrierIndividuelTemplate(PageTemplate): class CourrierIndividuelTemplate(PageTemplate):
"""Template pour courrier avisant des decisions de jury (1 page /etudiant)""" """Template pour courrier avisant des decisions de jury (1 page par étudiant)"""
def __init__( def __init__(
self, self,
document, document,
pagesbookmarks={}, pagesbookmarks=None,
author=None, author=None,
title=None, title=None,
subject=None, subject=None,
@ -163,7 +166,7 @@ class CourrierIndividuelTemplate(PageTemplate):
template_name="CourrierJuryTemplate", template_name="CourrierJuryTemplate",
): ):
"""Initialise our page template.""" """Initialise our page template."""
self.pagesbookmarks = pagesbookmarks self.pagesbookmarks = pagesbookmarks or {}
self.pdfmeta_author = author self.pdfmeta_author = author
self.pdfmeta_title = title self.pdfmeta_title = title
self.pdfmeta_subject = subject self.pdfmeta_subject = subject
@ -237,32 +240,32 @@ class CourrierIndividuelTemplate(PageTemplate):
width=LOGO_HEADER_WIDTH, width=LOGO_HEADER_WIDTH,
) )
def beforeDrawPage(self, canvas, doc): def beforeDrawPage(self, canv, doc):
"""Draws a logo and an contribution message on each page.""" """Draws a logo and an contribution message on each page."""
# ---- Add some meta data and bookmarks # ---- Add some meta data and bookmarks
if self.pdfmeta_author: if self.pdfmeta_author:
canvas.setAuthor(SU(self.pdfmeta_author)) canv.setAuthor(SU(self.pdfmeta_author))
if self.pdfmeta_title: if self.pdfmeta_title:
canvas.setTitle(SU(self.pdfmeta_title)) canv.setTitle(SU(self.pdfmeta_title))
if self.pdfmeta_subject: if self.pdfmeta_subject:
canvas.setSubject(SU(self.pdfmeta_subject)) canv.setSubject(SU(self.pdfmeta_subject))
bm = self.pagesbookmarks.get(doc.page, None) bm = self.pagesbookmarks.get(doc.page, None)
if bm != None: if bm != None:
key = bm key = bm
txt = SU(bm) txt = SU(bm)
canvas.bookmarkPage(key) canv.bookmarkPage(key)
canvas.addOutlineEntry(txt, bm) canv.addOutlineEntry(txt, bm)
# ---- Background image # ---- Background image
if self.background_image_filename and self.with_page_background: if self.background_image_filename and self.with_page_background:
canvas.drawImage( canv.drawImage(
self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1] self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1]
) )
# ---- Header/Footer # ---- Header/Footer
if self.with_header: if self.with_header:
page_header( page_header(
canvas, canv,
doc, doc,
self.logo_header, self.logo_header,
self.preferences, self.preferences,
@ -270,7 +273,7 @@ class CourrierIndividuelTemplate(PageTemplate):
) )
if self.with_footer: if self.with_footer:
page_footer( page_footer(
canvas, canv,
doc, doc,
self.logo_footer, self.logo_footer,
self.preferences, self.preferences,
@ -332,6 +335,42 @@ class PVTemplate(CourrierIndividuelTemplate):
# self.__pageNum += 1 # self.__pageNum += 1
def _simulate_br(paragraph_txt: str, para="<para>") -> str:
"""Reportlab bug turnaround (could be removed in a future version).
p is a string with Reportlab intra-paragraph XML tags.
Replaces <br/> (currently ignored by Reportlab) by </para><para>
Also replaces <br> by <br/>
"""
return ("</para>" + para).join(
re.split(r"<.*?br.*?/>", paragraph_txt.replace("<br>", "<br/>"))
)
def _make_signature_image(signature, leftindent, formsemestre_id) -> Table:
"crée un paragraphe avec l'image signature"
# cree une image PIL pour avoir la taille (W,H)
f = io.BytesIO(signature)
img = PILImage.open(f)
width, height = img.size
pdfheight = (
1.0
* sco_preferences.get_preference("pv_sig_image_height", formsemestre_id)
* mm
)
f.seek(0, 0)
style = styles.ParagraphStyle({})
style.leading = 1.0 * sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre_id
) # vertical space
style.leftIndent = leftindent
return Table(
[("", Image(f, width=width * pdfheight / float(height), height=pdfheight))],
colWidths=(9 * cm, 7 * cm),
)
def pdf_lettres_individuelles( def pdf_lettres_individuelles(
formsemestre_id, formsemestre_id,
etudids=None, etudids=None,
@ -352,7 +391,7 @@ def pdf_lettres_individuelles(
etuds = [x["identite"] for x in dpv["decisions"]] etuds = [x["identite"] for x in dpv["decisions"]]
sco_etud.fill_etuds_info(etuds) sco_etud.fill_etuds_info(etuds)
# #
sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
prefs = sco_preferences.SemPreferences(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id)
params = { params = {
"date_jury": date_jury, "date_jury": date_jury,
@ -363,18 +402,22 @@ def pdf_lettres_individuelles(
} }
# copie preferences # copie preferences
for name in sco_preferences.get_base_preferences().prefs_name: for name in sco_preferences.get_base_preferences().prefs_name:
params[name] = sco_preferences.get_preference(name, sem["formsemestre_id"]) params[name] = sco_preferences.get_preference(name, formsemestre_id)
bookmarks = {} bookmarks = {}
objects = [] # list of PLATYPUS objects objects = [] # list of PLATYPUS objects
npages = 0 npages = 0
for e in dpv["decisions"]: for decision in dpv["decisions"]:
if e["decision_sem"]: # decision prise if (
etud = sco_etud.get_etud_info(e["identite"]["etudid"], filled=True)[0] decision["decision_sem"]
params["nomEtud"] = etud["nomprenom"] or decision.get("decision_annee")
bookmarks[npages + 1] = scu.suppress_accents(etud["nomprenom"]) or decision.get("decision_rcue")
): # decision prise
etud: Identite = Identite.query.get(decision["identite"]["etudid"])
params["nomEtud"] = etud.nomprenom
bookmarks[npages + 1] = scu.suppress_accents(etud.nomprenom)
objects += pdf_lettre_individuelle( objects += pdf_lettre_individuelle(
dpv["formsemestre"], e, etud, params, signature dpv["formsemestre"], decision, etud, params, signature
) )
objects.append(PageBreak()) objects.append(PageBreak())
npages += 1 npages += 1
@ -394,8 +437,8 @@ def pdf_lettres_individuelles(
document.addPageTemplates( document.addPageTemplates(
CourrierIndividuelTemplate( CourrierIndividuelTemplate(
document, document,
author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION), author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
title="Lettres décision %s" % sem["titreannee"], title=f"Lettres décision {formsemestre.titre_annee()}",
subject="Décision jury", subject="Décision jury",
margins=margins, margins=margins,
pagesbookmarks=bookmarks, pagesbookmarks=bookmarks,
@ -408,36 +451,41 @@ def pdf_lettres_individuelles(
return data return data
def _descr_jury(sem, diplome): def _descr_jury(formsemestre: FormSemestre, diplome):
if not diplome: if not diplome:
t = "passage de Semestre %d en Semestre %d" % ( if formsemestre.formation.is_apc():
sem["semestre_id"], t = f"""BUT{(formsemestre.semestre_id+1)//2}"""
sem["semestre_id"] + 1, s = t
) else:
s = "passage de semestre" t = f"""passage de Semestre {formsemestre.semestre_id} en Semestre {formsemestre.semestre_id + 1}"""
s = "passage de semestre"
else: else:
t = "délivrance du diplôme" t = "délivrance du diplôme"
s = t s = t
return t, s # titre long, titre court return t, s # titre long, titre court
def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=None):
""" """
Renvoie une liste d'objets PLATYPUS pour intégration Renvoie une liste d'objets PLATYPUS pour intégration
dans un autre document. dans un autre document.
""" """
# #
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
Se: SituationEtudParcours = decision["Se"] formsemestre = FormSemestre.query.get(formsemestre_id)
t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal) Se: SituationEtudCursus = decision["Se"]
t, s = _descr_jury(
formsemestre, Se.parcours_validated() or not Se.semestre_non_terminal
)
objects = [] objects = []
style = reportlab.lib.styles.ParagraphStyle({}) style = reportlab.lib.styles.ParagraphStyle({})
style.fontSize = 14 style.fontSize = 14
style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id) style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
style.leading = 18 style.leading = 18
style.alignment = TA_JUSTIFY style.alignment = TA_LEFT
params["semestre_id"] = sem["semestre_id"] params["semestre_id"] = formsemestre.semestre_id
params["decision_sem_descr"] = decision["decision_sem_descr"] params["decision_sem_descr"] = decision["decision_sem_descr"]
params["type_jury"] = t # type de jury (passage ou delivrance) params["type_jury"] = t # type de jury (passage ou delivrance)
params["type_jury_abbrv"] = s # idem, abbrégé params["type_jury_abbrv"] = s # idem, abbrégé
@ -450,28 +498,18 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
params["INSTITUTION_CITY"] = ( params["INSTITUTION_CITY"] = (
sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or "" sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or ""
) )
if decision["prev_decision_sem"]: if decision["prev_decision_sem"]:
params["prev_semestre_id"] = decision["prev"]["semestre_id"] params["prev_semestre_id"] = decision["prev"]["semestre_id"]
params["prev_code_descr"] = decision["prev_code_descr"]
params["prev_decision_sem_txt"] = ""
params["decision_orig"] = ""
params.update(decision["identite"]) params.update(decision["identite"])
# fix domicile # fix domicile
if params["domicile"]: if params["domicile"]:
params["domicile"] = params["domicile"].replace("\\n", "<br/>") params["domicile"] = params["domicile"].replace("\\n", "<br/>")
# Décision semestre courant:
if sem["semestre_id"] >= 0:
params["decision_orig"] = "du semestre S%s" % sem["semestre_id"]
else:
params["decision_orig"] = ""
if decision["prev_decision_sem"]:
params["prev_decision_sem_txt"] = (
"""<b>Décision du semestre antérieur S%(prev_semestre_id)s :</b> %(prev_code_descr)s"""
% params
)
else:
params["prev_decision_sem_txt"] = ""
# UE capitalisées: # UE capitalisées:
if decision["decisions_ue"] and decision["decisions_ue_descr"]: if decision["decisions_ue"] and decision["decisions_ue_descr"]:
params["decision_ue_txt"] = ( params["decision_ue_txt"] = (
@ -498,7 +536,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
params[ params[
"autorisations_txt" "autorisations_txt"
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % ( ] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
etud["ne"], etud.e,
s, s,
s, s,
decision["autorisations_descr"], decision["autorisations_descr"],
@ -513,6 +551,14 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
else: else:
params["diplome_txt"] = "" params["diplome_txt"] = ""
# Les fonctions ci-dessous ajoutent ou modifient des champs:
if formsemestre.formation.is_apc():
# ajout champs spécifiques PV BUT
add_apc_infos(formsemestre, params, decision)
else:
# ajout champs spécifiques PV DUT
add_classic_infos(formsemestre, params, decision)
# Corps de la lettre: # Corps de la lettre:
objects += sco_bulletins_pdf.process_field( objects += sco_bulletins_pdf.process_field(
sco_preferences.get_preference("PV_LETTER_TEMPLATE", sem["formsemestre_id"]), sco_preferences.get_preference("PV_LETTER_TEMPLATE", sem["formsemestre_id"]),
@ -567,39 +613,30 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
return objects return objects
def _simulate_br(p, para="<para>"): def add_classic_infos(formsemestre: FormSemestre, params: dict, decision: dict):
"""Reportlab bug turnaround (could be removed in a future version). """Ajoute les champs pour les formations classiques, donc avec codes semestres"""
p is a string with Reportlab intra-paragraph XML tags. if decision["prev_decision_sem"]:
Replaces <br/> (currently ignored by Reportlab) by </para><para> params["prev_code_descr"] = decision["prev_code_descr"]
params[
"prev_decision_sem_txt"
] = f"""<b>Décision du semestre antérieur S{params['prev_semestre_id']} :</b> {params['prev_code_descr']}"""
# Décision semestre courant:
if formsemestre.semestre_id >= 0:
params["decision_orig"] = f"du semestre S{formsemestre.semestre_id}"
else:
params["decision_orig"] = ""
def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict):
"""Ajoute les champs pour les formations APC (BUT), donc avec codes RCUE et année"""
annee_but = (formsemestre.semestre_id + 1) // 2
params["decision_orig"] = f"année BUT{annee_but}"
params["decision_sem_descr"] = decision.get("decision_annee", {}).get("code", "")
params[
"decision_ue_txt"
] = f"""{params["decision_ue_txt"]}<br/>
<b>Niveaux de compétences:</b><br/> {decision.get("descr_decisions_rcue", "")}
""" """
l = re.split(r"<.*?br.*?/>", p)
return ("</para>" + para).join(l)
def _make_signature_image(signature, leftindent, formsemestre_id):
"cree un paragraphe avec l'image signature"
# cree une image PIL pour avoir la taille (W,H)
from PIL import Image as PILImage
f = io.BytesIO(signature)
im = PILImage.open(f)
width, height = im.size
pdfheight = (
1.0
* sco_preferences.get_preference("pv_sig_image_height", formsemestre_id)
* mm
)
f.seek(0, 0)
style = styles.ParagraphStyle({})
style.leading = 1.0 * sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre_id
) # vertical space
style.leftIndent = leftindent
return Table(
[("", Image(f, width=width * pdfheight / float(height), height=pdfheight))],
colWidths=(9 * cm, 7 * cm),
)
# ---------------------------------------------- # ----------------------------------------------
@ -699,7 +736,8 @@ def _pvjury_pdf_type(
sem = dpv["formsemestre"] sem = dpv["formsemestre"]
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
titre_jury, _ = _descr_jury(sem, diplome) formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
titre_jury, _ = _descr_jury(formsemestre, diplome)
titre_diplome = pv_title or dpv["formation"]["titre_officiel"] titre_diplome = pv_title or dpv["formation"]["titre_officiel"]
objects = [] objects = []

View File

@ -386,6 +386,7 @@ def gen_formsemestre_recapcomplet_html(
table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
else: else:
table_html = sco_cache.TableRecapCache.get(formsemestre.id) table_html = sco_cache.TableRecapCache.get(formsemestre.id)
# en mode jury ne cache pas la table html
if mode_jury or (table_html is None): if mode_jury or (table_html is None):
table_html = _gen_formsemestre_recapcomplet_html( table_html = _gen_formsemestre_recapcomplet_html(
formsemestre, formsemestre,

View File

@ -41,7 +41,7 @@ import pydot
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, ScolarAutorisationInscription
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.models import FormationModalite from app.models import FormationModalite
@ -51,7 +51,6 @@ from app.scodoc import sco_codes_parcours
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
import sco_version import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
@ -81,10 +80,10 @@ def formsemestre_etuds_stats(sem, only_primo=False):
if "codedecision" not in etud: if "codedecision" not in etud:
etud["codedecision"] = "(nd)" # pas de decision jury etud["codedecision"] = "(nd)" # pas de decision jury
# Ajout devenir (autorisations inscriptions), utile pour stats passage # Ajout devenir (autorisations inscriptions), utile pour stats passage
aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription( aut_list = ScolarAutorisationInscription.query.filter_by(
etudid, sem["formsemestre_id"] etudid=etudid, origin_formsemestre_id=sem["formsemestre_id"]
) ).all()
autorisations = ["S%s" % x["semestre_id"] for x in aut_list] autorisations = [f"S{a.semestre_id}" for a in aut_list]
autorisations.sort() autorisations.sort()
autorisations_str = ", ".join(autorisations) autorisations_str = ", ".join(autorisations)
etud["devenir"] = autorisations_str etud["devenir"] = autorisations_str

View File

@ -664,6 +664,15 @@ def flash_errors(form):
# see https://getbootstrap.com/docs/4.0/components/alerts/ # see https://getbootstrap.com/docs/4.0/components/alerts/
def flash_once(message: str):
"""Flash the message, but only once per request"""
if not hasattr(g, "sco_flashed_once"):
g.sco_flashed_once = set()
if not message in g.sco_flashed_once:
flash(message)
g.sco_flashed_once.add(message)
def sendCSVFile(data, filename): # DEPRECATED utiliser send_file def sendCSVFile(data, filename): # DEPRECATED utiliser send_file
"""publication fichier CSV.""" """publication fichier CSV."""
return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True) return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True)

View File

@ -169,7 +169,7 @@ section>div:nth-child(1){
border: none; border: none;
margin-left: auto; margin-left: auto;
} }
.rang{ .rang, .competence{
font-weight: bold; font-weight: bold;
} }
.ue .rang{ .ue .rang{

View File

@ -2210,6 +2210,7 @@ ul.notes_module_list {
list-style-type: none; list-style-type: none;
} }
/*Choix niveau dans form edit UE */
div.ue_choix_niveau { div.ue_choix_niveau {
background-color: rgb(191, 242, 255); background-color: rgb(191, 242, 255);
border: 1px solid blue; border: 1px solid blue;
@ -2219,6 +2220,19 @@ div.ue_choix_niveau {
margin-right: 15px; margin-right: 15px;
} }
/* Choix niveau dans edition programme (ue_table) */
div.formation_list_ues div.ue_choix_niveau {
margin-left: 64px;
margin-right: 64px;
margin-top: 2px;
padding: 4px;
font-size: 14px;
}
div.formation_list_ues div.ue_choix_niveau b {
font-weight: normal;
}
div#ue_list_modules { div#ue_list_modules {
background-color: rgb(251, 225, 165); background-color: rgb(251, 225, 165);
border: 1px solid blue; border: 1px solid blue;

View File

@ -1,13 +1,16 @@
// Affiche et met a jour la liste des UE partageant le meme code // Affiche et met a jour la liste des UE partageant le meme code
$().ready(function () { $().ready(function () {
update_ue_list(); if (document.querySelector("#tf_ue_id")) {
$("#tf_ue_code").bind("keyup", update_ue_list); /* fonctions spécifiques pour edition UE */
update_ue_list();
$("#tf_ue_code").bind("keyup", update_ue_list);
$("select#tf_type").change(function () { $("select#tf_type").change(function () {
update_bonus_description();
});
update_bonus_description(); update_bonus_description();
}); }
update_bonus_description();
}); });
function update_bonus_description() { function update_bonus_description() {
@ -33,11 +36,10 @@ function update_ue_list() {
}); });
} }
function set_ue_niveau_competence() { function set_ue_niveau_competence(elem) {
let ue_id = document.querySelector("#tf_ue_id").value; let ue_id = elem.dataset.ue_id;
let select = document.querySelector("#form_ue_choix_niveau select"); let niveau_id = elem.value;
let niveau_id = select.value; let set_ue_niveau_competence_url = elem.dataset.setter;
let set_ue_niveau_competence_url = select.dataset.setter;
$.post(set_ue_niveau_competence_url, $.post(set_ue_niveau_competence_url,
{ {
ue_id: ue_id, ue_id: ue_id,

View File

@ -232,7 +232,21 @@ class releveBUT extends HTMLElement {
${(()=>{ ${(()=>{
let output = ""; let output = "";
data.semestre.decision_rcue.forEach(competence=>{ data.semestre.decision_rcue.forEach(competence=>{
output += `<div class=rang>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`; output += `<div class=competence>${competence.niveau.competence.titre}</div><div>${competence.code}</div>`;
})
return output;
})()}
</div>
</div>`
}
if(data.semestre.decision_ue?.length){
output += `
<div>
<div class=enteteSemestre>UE</div><div></div>
${(()=>{
let output = "";
data.semestre.decision_ue.forEach(ue=>{
output += `<div class=competence>${ue.acronyme}</div><div>${ue.code}</div>`;
}) })
return output; return output;
})()} })()}

View File

@ -30,7 +30,7 @@
<span class="ue_type_{{ue.type}}"> <span class="ue_type_{{ue.type}}">
<span class="ue_color_indicator" style="background:{{ <span class="ue_color_indicator" style="background:{{
ue.color if ue.color is not none else 'blue'}}"></span> ue.color if ue.color is not none else 'blue'}}"></span>
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{ <b>{{ue.acronyme}} <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}" url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
title="{{ue.acronyme}}: {{ title="{{ue.acronyme}}: {{
('pas de compétence associée' ('pas de compétence associée'
@ -40,6 +40,7 @@
else '' else ''
}}" }}"
>{{ue.titre}}</a> >{{ue.titre}}</a>
</b>
{% set virg = joiner(", ") %} {% set virg = joiner(", ") %}
<span class="ue_code">( <span class="ue_code">(
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%} {%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%}
@ -53,16 +54,16 @@
) )
</span> </span>
</span> </span>
{% if (ue.niveau_competence is none) and ue.type == 0 %}
<span class="fontred">pas de compétence associée</span>
{% endif %}
{% if editable and not ue.is_locked() %} {% if editable and not ue.is_locked() %}
<a class="stdlink" href="{{ url_for('notes.ue_edit', <a class="stdlink" href="{{ url_for('notes.ue_edit',
scodoc_dept=g.scodoc_dept, ue_id=ue.id) scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">modifier</a> }}">modifier</a>
{% endif %} {% endif %}
{{ form_ue_choix_niveau(formation, ue)|safe }}
{% if ue.type == 1 and ue.modules.count() == 0 %} {% if ue.type == 1 and ue.modules.count() == 0 %}
<span class="warning" title="pas de module, donc pas de bonus calculé">aucun module rattaché !</span> <span class="warning" title="pas de module, donc pas de bonus calculé">aucun module rattaché !</span>
{% endif %} {% endif %}

View File

@ -296,7 +296,9 @@ def formsemestre_bulletinetud(
format = format or "html" format = format or "html"
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
raise ScoInvalidIdType("formsemestre_id must be an integer !") raise ScoInvalidIdType(
"formsemestre_bulletinetud: formsemestre_id must be an integer !"
)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if etudid: if etudid:
etud = models.Identite.query.get_or_404(etudid) etud = models.Identite.query.get_or_404(etudid)
@ -826,7 +828,9 @@ def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None):
if not formsemestre_id: if not formsemestre_id:
return flask.abort(404, "argument manquant: formsemestre_id") return flask.abort(404, "argument manquant: formsemestre_id")
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
return flask.abort(404, "formsemestre_id must be an integer !") return flask.abort(
404, "XMLgetFormsemestres: formsemestre_id must be an integer !"
)
args = {} args = {}
if etape_apo: if etape_apo:
args["etape_apo"] = etape_apo args["etape_apo"] = etape_apo
@ -2548,9 +2552,8 @@ def formsemestre_validation_suppress_etud(
) )
if not dialog_confirmed: if not dialog_confirmed:
d = sco_bulletins_json.dict_decision_jury( d = sco_bulletins_json.dict_decision_jury(
etudid, formsemestre_id, with_decisions=True etud, formsemestre, with_decisions=True
) )
d.update(but_validations.dict_decision_jury(etud, formsemestre))
descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])] descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])]
dec_annee = d.get("decision_annee") dec_annee = d.get("decision_annee")
@ -2661,6 +2664,7 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
deca.erase() deca.erase()
db.session.commit() db.session.commit()
log(f"formsemestre_jury_but_erase({formsemestre_id}, {etudid})")
flash("décisions de jury effacées") flash("décisions de jury effacées")
return redirect(dest_url) return redirect(dest_url)

View File

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

View File

@ -20,6 +20,7 @@ from config import TestConfig
from tests.unit import sco_fake_gen from tests.unit import sco_fake_gen
import app import app
from app import db
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
@ -31,8 +32,7 @@ from app.scodoc import sco_codes_parcours
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_parcours_dut from app.scodoc import sco_cursus_dut
from app.scodoc import sco_cache
from app.scodoc import sco_saisie_notes from app.scodoc import sco_saisie_notes
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -194,20 +194,20 @@ def run_sco_basic(verbose=False):
# --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance # --- Permission saisie notes et décisions de jury, avec ou sans démission ou défaillance
# on n'a pas encore saisi de décisions # on n'a pas encore saisi de décisions
assert not sco_parcours_dut.formsemestre_has_decisions(formsemestre_id) assert not sco_cursus_dut.formsemestre_has_decisions(formsemestre_id)
# Saisie d'un décision AJ, non assidu # Saisie d'un décision AJ, non assidu
etudid = etuds[-1]["etudid"] etudid = etuds[-1]["etudid"]
sco_parcours_dut.formsemestre_validate_ues( sco_cursus_dut.formsemestre_validate_ues(
formsemestre_id, etudid, sco_codes_parcours.AJ, False formsemestre_id, etudid, sco_codes_parcours.AJ, False
) )
assert sco_parcours_dut.formsemestre_has_decisions( assert sco_cursus_dut.formsemestre_has_decisions(
formsemestre_id formsemestre_id
), "décisions manquantes" ), "décisions manquantes"
# Suppression de la décision # Suppression de la décision
sco_formsemestre_validation.formsemestre_validation_suppress_etud( sco_formsemestre_validation.formsemestre_validation_suppress_etud(
formsemestre_id, etudid formsemestre_id, etudid
) )
assert not sco_parcours_dut.formsemestre_has_decisions( assert not sco_cursus_dut.formsemestre_has_decisions(
formsemestre_id formsemestre_id
), "décisions non effacées" ), "décisions non effacées"