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

This commit is contained in:
leonard_montalbano 2022-06-29 11:22:39 +02:00
commit bab08c8cd8
93 changed files with 5585 additions and 951 deletions

View File

@ -205,8 +205,18 @@ 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.DEBUG)
# Evite de logguer toutes les requetes dans notre log
logging.getLogger("werkzeug").disabled = True
app.config.from_object(config_class) app.config.from_object(config_class)
# Vérifie/crée lien sym pour les URL statiques
link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}"
if not os.path.exists(link_filename):
app.logger.info(f"creating symlink {link_filename}")
os.symlink("..", link_filename)
db.init_app(app) db.init_app(app)
migrate.init_app(app, db) migrate.init_app(app, db)
login.init_app(app) login.init_app(app)

106
app/but/apc_edit_ue.py Normal file
View File

@ -0,0 +1,106 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""
Edition associations UE <-> Ref. Compétence
"""
from flask import g, url_for
from app import db, log
from app.models import Formation, UniteEns
from app.models.but_refcomp import ApcNiveau
from app.scodoc import sco_codes_parcours
def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
"""Form. HTML pour associer une UE à un niveau de compétence"""
if ue.type != sco_codes_parcours.UE_STANDARD:
return ""
ref_comp = ue.formation.referentiel_competence
if ref_comp is None:
return f"""<div class="ue_choix_niveau">
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
}">associer un référentiel de compétence</a>
</div>
</div>"""
annee = (ue.semestre_idx + 1) // 2 # 1, 2, 3
niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
# Les niveaux déjà associés à d'autres UE du même semestre
autres_ues = formation.ues.filter_by(semestre_idx=ue.semestre_idx)
niveaux_autres_ues = {
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
}
options = []
if niveaux_by_parcours["TC"]: # TC pour Tronc Commun
options.append("""<optgroup label="Tronc commun">""")
for n in niveaux_by_parcours["TC"]:
if n.id in niveaux_autres_ues:
disabled = "disabled"
else:
disabled = ""
options.append(
f"""<option value="{n.id}" {'selected'
if ue.niveau_competence == n else ''}
{disabled}>{n.annee} {n.competence.titre_long}
niveau {n.ordre}</option>"""
)
options.append("""</optgroup>""")
for parcour in ref_comp.parcours:
if len(niveaux_by_parcours[parcour.id]):
options.append(f"""<optgroup label="Parcours {parcour.libelle}">""")
for n in niveaux_by_parcours[parcour.id]:
if n.id in niveaux_autres_ues:
disabled = "disabled"
else:
disabled = ""
options.append(
f"""<option value="{n.id}" {'selected'
if ue.niveau_competence == n else ''}
{disabled}>{n.annee} {n.competence.titre_long}
niveau {n.ordre}</option>"""
)
options.append("""</optgroup>""")
options_str = "\n".join(options)
return f"""
<div class="ue_choix_niveau">
<form id="form_ue_choix_niveau">
<b>Niveau de compétence associé:</b>
<select onchange="set_ue_niveau_competence();" data-setter="{
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
}">
<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>
{options_str}
</select>
</form>
</div>
"""
def set_ue_niveau_competence(ue_id: int, niveau_id: int):
"""Associe le niveau et l'UE"""
log(f"set_ue_niveau_competence( {ue_id}, {niveau_id} )")
ue = UniteEns.query.get_or_404(ue_id)
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
niveaux_autres_ues = {
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
}
if niveau_id in niveaux_autres_ues:
log(
f"set_ue_niveau_competence: denying association of {ue} to already associated {niveau_id}"
)
return "", 409 # conflict
if niveau_id == "":
# suppression de l'association
ue.niveau_competence = None
else:
niveau = ApcNiveau.query.get_or_404(niveau_id)
ue.niveau_competence = niveau
db.session.add(ue)
db.session.commit()
return "", 204

View File

@ -244,7 +244,7 @@ class BulletinBUT:
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}" f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
for ue in res.ues for ue in res.ues
if ue.type != UE_SPORT if ue.type != UE_SPORT
and res.modimpls_in_ue(ue.id, etudid) and res.modimpls_in_ue(ue, etudid)
and ue.id in res.bonus_ues and ue.id in res.bonus_ues
and bonus_vect[ue.id] > 0.0 and bonus_vect[ue.id] > 0.0
] ]
@ -274,6 +274,11 @@ class BulletinBUT:
etat_inscription = etud.inscription_etat(formsemestre.id) etat_inscription = etud.inscription_etat(formsemestre.id)
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT] nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
published = (not formsemestre.bul_hide_xml) or force_publishing published = (not formsemestre.bul_hide_xml) or force_publishing
if formsemestre.formation.referentiel_competence is None:
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
else:
etud_ues_ids = res.etud_ues_ids(etud.id)
d = { d = {
"version": "0", "version": "0",
"type": "BUT", "type": "BUT",
@ -365,10 +370,7 @@ class BulletinBUT:
) )
for ue in res.ues for ue in res.ues
# si l'UE comporte des modules auxquels on est inscrit: # si l'UE comporte des modules auxquels on est inscrit:
if ( if ((ue.type == UE_SPORT) or ue.id in etud_ues_ids)
(ue.type == UE_SPORT)
or self.res.modimpls_in_ue(ue.id, etud.id)
)
}, },
"semestre": semestre_infos, "semestre": semestre_infos,
}, },

View File

@ -0,0 +1,18 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9.3 : Formulaires / jurys BUT
"""
from flask_wtf import FlaskForm
from wtforms import SubmitField
class FormSemestreValidationAutoBUTForm(FlaskForm):
"simple form de confirmation"
submit = SubmitField("Lancer le calcul")
cancel = SubmitField("Annuler")

View File

@ -13,7 +13,9 @@ from wtforms import SelectField, SubmitField
class FormationRefCompForm(FlaskForm): class FormationRefCompForm(FlaskForm):
referentiel_competence = SelectField("Référentiels déjà chargés") referentiel_competence = SelectField(
"Choisir parmi les référentiels déjà chargés :"
)
submit = SubmitField("Valider") submit = SubmitField("Valider")
cancel = SubmitField("Annuler") cancel = SubmitField("Annuler")
@ -23,7 +25,7 @@ class RefCompLoadForm(FlaskForm):
"Choisir un référentiel de compétences officiel BUT" "Choisir un référentiel de compétences officiel BUT"
) )
upload = FileField( upload = FileField(
label="Ou bien sélectionner un fichier XML au format Orébut", label="... ou bien sélectionner un fichier XML au format Orébut (réservé aux développeurs !)",
validators=[ validators=[
FileAllowed( FileAllowed(
[ [

View File

@ -4,7 +4,6 @@
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
from xml.etree import ElementTree from xml.etree import ElementTree
from typing import TextIO
import sqlalchemy import sqlalchemy
@ -57,13 +56,13 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
try: try:
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib)) c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
db.session.flush() db.session.flush()
except sqlalchemy.exc.IntegrityError: except sqlalchemy.exc.IntegrityError as exc:
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile # ne devrait plus se produire car pas d'unicité de l'id: donc inutile
db.session.rollback() db.session.rollback()
raise ScoValueError( raise ScoValueError(
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]}) f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
""" """
) ) from exc
ref.competences.append(c) ref.competences.append(c)
# --- SITUATIONS # --- SITUATIONS
situations = competence.find("situations") situations = competence.find("situations")

939
app/but/jury_but.py Normal file
View File

@ -0,0 +1,939 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT: logique de gestion
Utilisation:
1) chargement page jury, pour un étudiant et un formsemestre BUT quelconque
- DecisionsProposeesAnnee(formsemestre)
cherche l'autre formsemestre de la même année scolaire (peut ne pas exister)
cherche les RCUEs de l'année (BUT1, 2, 3)
pour un redoublant, le RCUE peut considérer un formsemestre d'une année antérieure.
on instancie des DecisionsProposees pour les
différents éléments (UEs, RCUEs, Année, Diplôme)
Cela donne
- les codes possibles (dans .codes)
- le code actuel si une décision existe déjà (dans code_valide)
- pour les UEs, le rcue s'il y en a un)
2) Validation pour l'utilisateur (form)) => enregistrement code
- on vérifie que le code soumis est bien dans les codes possibles
- on enregistre la décision (dans ScolarFormSemestreValidation pour les UE,
ApcValidationRCUE pour les RCUE, et ApcValidationAnnee pour les années)
- Si RCUE validé, on déclenche d'éventuelles validations:
("La validation des deux UE du niveau dune compétence emporte la validation
de lensemble des UE du niveau inférieur de cette même compétence.")
Les jurys de semestre BUT impairs entrainent systématiquement la génération d'une
autorisation d'inscription dans le semestre pair suivant: `ScolarAutorisationInscription`.
Les jurys de semestres pairs non (S2, S4, S6): il y a une décision sur l'année (ETP)
- autorisation en S_2n+1 (S3 ou S5) si: ADM, ADJ, PASD, PAS1CN
- autorisation en S2n-1 (S1, S3 ou S5) si: RED
- rien si pour les autres codes d'année.
Le formulaire permet de choisir des codes d'UE, RCUE et Année (ETP).
Mais normalement, les codes d'UE sont à choisir: les RCUE et l'année s'en déduisent.
Si l'utilisateur coche "décision manuelle", il peut alors choisir les codes RCUE et années.
La soumission du formulaire:
- etud, formation
- UEs: [(formsemestre, ue, code), ...]
- RCUE: [(formsemestre, ue, code), ...] le formsemestre est celui d'indice pair du niveau
(S2, S4 ou S6), il sera regoupé avec celui impair de la même année ou de la suivante.
- Année: [(formsemestre, code)]
DecisionsProposeesAnnee:
si 1/2 des rcue et aucun < 8 + pour S5 condition sur les UE de BUT1 et BUT2
=> charger les DecisionsProposeesRCUE
DecisionsProposeesRCUE: les RCUEs pour cette année
validable, compensable, ajourné. Utilise classe RegroupementCoherentUE
DecisionsProposeesUE: décisions de jury sur une UE du BUT
initialisation sans compensation (ue isolée), mais
DecisionsProposeesRCUE appelera .set_compensable()
si on a la possibilité de la compenser dans le RCUE.
"""
import html
from operator import attrgetter
import re
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 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
class NoRCUEError(ScoValueError):
"""Erreur en cas de RCUE manquant"""
def __init__(self, deca: "DecisionsProposeesAnnee", ue: UniteEns):
if all(u.niveau_competence for u in deca.ues_pair):
warning_pair = ""
else:
warning_pair = """<div class="warning">certaines UE du semestre pair ne sont pas associées à un niveau de compétence</div>"""
if all(u.niveau_competence for u in deca.ues_impair):
warning_impair = ""
else:
warning_impair = """<div class="warning">certaines UE du semestre impair ne sont pas associées à un niveau de compétence</div>"""
msg = (
f"""<h3>Pas de RCUE pour l'UE {ue.acronyme}</h3>
{warning_impair}
{warning_pair}
<div><b>UE {ue.acronyme}</b>: niveau {html.escape(str(ue.niveau_competence))}</div>
<div><b>UEs impaires:</b> {html.escape(', '.join(str(u.niveau_competence or "pas de niveau")
for u in deca.ues_impair))}
</div>
"""
+ deca.infos()
)
super().__init__(msg)
class DecisionsProposees:
"""Une décision de jury proposé, constituée d'une liste de codes et d'une explication.
Super-classe, spécialisée pour les UE, les RCUE, les années et le diplôme.
validation : None ou une instance de d'une classe avec un champ code
ApcValidationRCUE, ApcValidationAnnee ou ScolarFormSemestreValidation
"""
# Codes toujours proposés sauf si include_communs est faux:
codes_communs = [
sco_codes.RAT,
sco_codes.DEF,
sco_codes.ABAN,
sco_codes.DEM,
sco_codes.UEBSL,
]
def __init__(
self,
etud: Identite = None,
code: Union[str, list[str]] = None,
explanation="",
code_valide=None,
include_communs=True,
):
self.etud = etud
self.codes = []
"Les codes attribuables par ce jury"
if include_communs:
self.codes = self.codes_communs.copy()
if isinstance(code, list):
self.codes = code + self.codes
elif code is not None:
self.codes = [code] + self.codes
self.validation = None
"Validation enregistrée"
self.code_valide: str = code_valide
"Code décision actuel enregistré"
self.explanation: str = explanation
"Explication à afficher à côté de la décision"
self.recorded = False
"true si la décision vient d'être enregistrée"
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}"""
class DecisionsProposeesAnnee(DecisionsProposees):
"""Décisions de jury sur une année (ETP) du BUT
Le texte:
La poursuite d'études dans un semestre pair dune même année est de droit
pour tout étudiant. La poursuite détudes dans un semestre impair est
possible si et seulement si létudiant a obtenu :
- la moyenne à plus de la moitié des regroupements cohérents dUE;
- et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE.
La poursuite d'études dans le semestre 5 nécessite de plus la validation
de toutes les UE des semestres 1 et 2 dans les conditions de validation
des points 4.3 (moy_ue >= 10) et 4.4 (compensation rcue), ou par décision
de jury.
"""
# Codes toujours proposés sauf si include_communs est faux:
codes_communs = [
sco_codes.RAT,
sco_codes.ABAN,
sco_codes.ABL,
sco_codes.DEF,
sco_codes.DEM,
sco_codes.EXCLU,
]
def __init__(
self,
etud: Identite,
formsemestre: FormSemestre,
):
super().__init__(etud=etud)
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre)
assert (
(formsemestre_pair is None)
or (formsemestre_impair is None)
or (
((formsemestre_pair.semestre_id - formsemestre_impair.semestre_id) == 1)
and (
formsemestre_pair.formation.referentiel_competence_id
== formsemestre_impair.formation.referentiel_competence_id
)
)
)
self.formsemestre_impair = formsemestre_impair
"le 1er semestre de l'année scolaire considérée (S1, S3, S5)"
self.formsemestre_pair = formsemestre_pair
"le second formsemestre de la même année scolaire (S2, S4, S6)"
self.annee_but = (
formsemestre_impair.semestre_id // 2 + 1
if formsemestre_impair
else formsemestre_pair.semestre_id // 2
)
"le rang de l'année dans le BUT: 1, 2, 3"
assert self.annee_but in (1, 2, 3)
self.rcues_annee = []
"RCUEs de l'année"
if self.formsemestre_impair is not None:
self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
formsemestre_id=formsemestre_impair.id,
ordre=self.annee_but,
).first()
else:
self.validation = None
if self.validation is not None:
self.code_valide = self.validation.code
self.parcour = None
"Le parcours considéré (celui du semestre pair, ou à défaut impair)"
if self.formsemestre_pair is not None:
self.res_pair: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
self.formsemestre_pair
)
else:
self.res_pair = None
if self.formsemestre_impair is not None:
self.res_impair: ResultatsSemestreBUT = res_sem.load_formsemestre_results(
self.formsemestre_impair
)
else:
self.res_impair = None
self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all
self.decisions_ues = {
ue.id: DecisionsProposeesUE(etud, formsemestre_impair, ue)
for ue in self.ues_impair
}
"{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année"
self.decisions_ues.update(
{
ue.id: DecisionsProposeesUE(etud, formsemestre_pair, ue)
for ue in self.ues_pair
}
)
self.rcues_annee = self.compute_rcues_annee()
formation = (
self.formsemestre_impair.formation
if self.formsemestre_impair
else self.formsemestre_pair.formation
)
self.niveaux_competences = ApcNiveau.niveaux_annee_de_parcours(
self.parcour, self.annee_but, formation.referentiel_competence
).all() # non triés
"liste des niveaux de compétences associés à cette année"
self.decisions_rcue_by_niveau = self.compute_decisions_niveaux()
"les décisions rcue associées aux niveau_id"
self.dec_rcue_by_ue = self._dec_rcue_by_ue()
"{ ue_id : DecisionsProposeesRCUE } pour toutes les UE associées à un niveau"
self.nb_competences = len(self.niveaux_competences)
"le nombre de niveaux de compétences à valider cette année"
rcues_avec_niveau = [d.rcue for d in self.decisions_rcue_by_niveau.values()]
self.nb_validables = len(
[rcue for rcue in rcues_avec_niveau if rcue.est_validable()]
)
"le nombre de comp. validables (éventuellement par compensation)"
self.nb_rcues_under_8 = len(
[rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()]
)
"le nb de comp. sous la barre de 8/20"
# année ADM si toutes RCUE validées (sinon PASD)
self.admis = self.nb_validables == self.nb_competences
"vrai si l'année est réussie, tous niveaux validables"
self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2)
# Peut passer si plus de la moitié validables et tous > 8
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
# XXX TODO ajouter condition pour passage en S5
# Enfin calcule les codes des UE:
for dec_ue in self.decisions_ues.values():
dec_ue.compute_codes()
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
expl_rcues = (
f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}"
)
if self.admis:
self.codes = [sco_codes.ADM] + self.codes
self.explanation = expl_rcues
elif self.passage_de_droit:
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
self.explanation = expl_rcues
elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante
self.codes = [
sco_codes.RED,
sco_codes.NAR,
sco_codes.PAS1NCI,
sco_codes.ADJ,
] + self.codes
self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8"
else:
self.codes = [
sco_codes.RED,
sco_codes.NAR,
sco_codes.PAS1NCI,
sco_codes.ADJ,
] + self.codes
self.explanation = (
expl_rcues
+ f""" et {self.nb_rcues_under_8}
niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
)
#
def infos(self) -> str:
"informations, for debugging purpose"
return f"""<b>DecisionsProposeesAnnee</b>
<ul>
<li>Etudiant: <a href="{url_for("scolar.ficheEtud",
scodoc_dept=g.scodoc_dept, etudid=self.etud.id)
}">{self.etud.nomprenom}</a>
</li>
<li>formsemestre_impair: <a href="{url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre_impair.id)
}">{html.escape(str(self.formsemestre_impair))}</a>
<ul>
<li>Formation: <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
semestre_idx=self.formsemestre_impair.semestre_id,
formation_id=self.formsemestre_impair.formation.id)}">
{self.formsemestre_impair.formation.to_html()} ({self.formsemestre_impair.formation.id})</a>
</li>
</ul>
</li>
<li>formsemestre_pair: <a href="{url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre_pair.id)
}">{html.escape(str(self.formsemestre_pair))}</a>
<ul>
<li>Formation: <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
semestre_idx=self.formsemestre_pair.semestre_id,
formation_id=self.formsemestre_pair.formation.id)}">
{self.formsemestre_pair.formation.to_html()} ({self.formsemestre_pair.formation.id})</a>
</li>
</ul>
</li>
<li>RCUEs: {html.escape(str(self.rcues_annee))}</li>
<li>nb_competences: {getattr(self, "nb_competences", "-")}</li>
<li>nb_nb_validables: {getattr(self, "nb_validables", "-")}</li>
<li>codes: {self.codes}</li>
<li>explanation: {self.explanation}</li>
</ul>
"""
def annee_scolaire(self) -> int:
"L'année de début de l'année scolaire"
formsemestre = self.formsemestre_impair or self.formsemestre_pair
return formsemestre.annee_scolaire()
def annee_scolaire_str(self) -> str:
"L'année scolaire, eg '2021 - 2022'"
formsemestre = self.formsemestre_impair or self.formsemestre_pair
return formsemestre.annee_scolaire_str().replace(" ", "")
def comp_formsemestres(
self, formsemestre: FormSemestre
) -> tuple[FormSemestre, FormSemestre]:
"les deux formsemestres de l'année scolaire à laquelle appartient formsemestre"
if formsemestre.semestre_id % 2 == 0:
other_semestre_id = formsemestre.semestre_id - 1
else:
other_semestre_id = formsemestre.semestre_id + 1
annee_scolaire = formsemestre.annee_scolaire()
other_formsemestre = None
for inscr in self.etud.formsemestre_inscriptions:
if (
# Même spécialité BUT (tolère ainsi des variantes de formation)
(
inscr.formsemestre.formation.referentiel_competence
== formsemestre.formation.referentiel_competence
)
# L'autre semestre
and (inscr.formsemestre.semestre_id == other_semestre_id)
# de la même année scolaire:
and (inscr.formsemestre.annee_scolaire() == annee_scolaire)
):
other_formsemestre = inscr.formsemestre
if formsemestre.semestre_id % 2 == 0:
return other_formsemestre, formsemestre
return formsemestre, other_formsemestre
def compute_ues_annee(self) -> list[list[UniteEns], list[UniteEns]]:
"""UEs à valider cette année pour cet étudiant, selon son parcours.
Ramène [ listes des UE du semestre impair, liste des UE du semestre pair ].
"""
etudid = self.etud.id
ues_sems = []
for (formsemestre, res) in (
(self.formsemestre_impair, self.res_impair),
(self.formsemestre_pair, self.res_pair),
):
if formsemestre is None:
ues = []
else:
formation: Formation = formsemestre.formation
# Parcour dans lequel l'étudiant est inscrit, et liste des UEs
if res.etuds_parcour_id[etudid] is None:
# pas de parcour: prend toutes les UEs (non bonus)
ues = [ue for ue in res.etud_ues(etudid) if ue.type == UE_STANDARD]
ues.sort(key=lambda u: u.numero)
else:
parcour = ApcParcours.query.get(res.etuds_parcour_id[etudid])
if parcour is not None:
self.parcour = parcour
ues = (
formation.query_ues_parcour(parcour)
.filter_by(semestre_idx=formsemestre.semestre_id)
.order_by(UniteEns.numero)
.all()
)
ues_sems.append(ues)
return ues_sems
def check_ues_ready_jury(self) -> list[str]:
"""Vérifie que les toutes les UEs (hors bonus) de l'année sont
bien associées à des niveaux de compétences.
Renvoie liste vide si ok, sinon liste de message explicatifs
"""
messages = []
for ue in self.ues_impair + self.ues_pair:
if ue.niveau_competence is None:
messages.append(
f"UE {ue.acronyme} non associée à un niveau de compétence"
)
if ue.semestre_idx is None:
messages.append(
f"UE {ue.acronyme} n'a pas d'indice de semestre dans la formation"
)
return messages
def compute_rcues_annee(self) -> list[RegroupementCoherentUE]:
"""Liste des regroupements d'UE à considérer cette année.
Pour le moment on ne considère pas de RCUE à cheval sur plusieurs années (redoublants).
Si on n'a pas les deux semestres, aucun RCUE.
Raises ScoValueError s'il y a des UE sans RCUE.
"""
if self.formsemestre_pair is None or self.formsemestre_impair is None:
return []
rcues_annee = []
ues_impair_sans_rcue = {ue.id for ue in self.ues_impair}
for ue_pair in self.ues_pair:
rcue = None
for ue_impair in self.ues_impair:
if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
rcue = RegroupementCoherentUE(
self.etud,
self.formsemestre_impair,
ue_impair,
self.formsemestre_pair,
ue_pair,
)
ues_impair_sans_rcue.discard(ue_impair.id)
break
if rcue is None:
raise NoRCUEError(deca=self, ue=ue_pair)
rcues_annee.append(rcue)
if len(ues_impair_sans_rcue) > 0:
ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
raise NoRCUEError(deca=self, ue=ue)
return rcues_annee
def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
"""Pour chaque niveau de compétence de cette année, construit
le DecisionsProposeesRCUE,
ou None s'il n'y en a pas
(ne devrait pas arriver car compute_rcues_annee vérifie déjà cela).
Return: { niveau_id : DecisionsProposeesRCUE }
"""
# Retrouve le RCUE associé à chaque niveau
rc_niveaux = []
for niveau in self.niveaux_competences:
rcue = None
for rc in self.rcues_annee:
if rc.ue_1.niveau_competence_id == niveau.id:
rcue = rc
break
if rcue is not None:
dec_rcue = DecisionsProposeesRCUE(self, rcue)
rc_niveaux.append((dec_rcue, niveau.id))
# prévient les UE concernées :-)
self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue)
self.decisions_ues[dec_rcue.rcue.ue_2.id].set_rcue(dec_rcue.rcue)
# Ordonne par numéro d'UE
rc_niveaux.sort(key=lambda x: x[0].rcue.ue_1.numero)
decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux}
return decisions_rcue_by_niveau
def _dec_rcue_by_ue(self) -> dict[int, "DecisionsProposeesRCUE"]:
"""construit dict { ue_id : DecisionsProposeesRCUE }
à partir de self.decisions_rcue_by_niveau"""
d = {}
for dec_rcue in self.decisions_rcue_by_niveau.values():
d[dec_rcue.rcue.ue_1.id] = dec_rcue
d[dec_rcue.rcue.ue_2.id] = dec_rcue
return d
def next_annee_semestre_id(self, code: str) -> int:
"""L'indice du semestre dans lequel l'étudiant est autorisé à
poursuivre l'année suivante. None si aucun."""
if self.formsemestre_pair is None:
return None # seulement sur année
if code == RED:
return self.formsemestre_pair.semestre_id - 1
elif (
code in sco_codes.BUT_CODES_PASSAGE
and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM
):
return self.formsemestre_pair.semestre_id + 1
return None
def record_form(self, form: dict):
"""Enregistre les codes de jury en base
form dict:
- 'code_ue_1896' : 'AJ' code pour l'UE id 1896
- 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6
- 'code_annee' : 'ADM' code pour l'année
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.
"""
for key in form:
code = form[key]
# Codes d'UE
m = re.match(r"^code_ue_(\d+)$", key)
if m:
ue_id = int(m.group(1))
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:
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()
db.session.commit()
def record(self, code: str, no_overwrite=False):
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
Si no_overwrite, ne fait rien si un code est déjà enregistré.
"""
if code and not code in self.codes:
raise ScoValueError(
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}"
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True
return # no change
if self.validation:
db.session.delete(self.validation)
db.session.flush()
if code is None:
self.validation = None
else:
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
)
Scolog.logdb(
method="jury_but",
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}",
)
db.session.add(self.validation)
# --- Autorisation d'inscription dans semestre suivant ?
if self.formsemestre_pair is not None:
if code is None:
ScolarAutorisationInscription.delete_autorisation_etud(
etudid=self.etud.id,
origin_formsemestre_id=self.formsemestre_pair.id,
)
else:
next_semestre_id = self.next_annee_semestre_id(code)
if next_semestre_id is not None:
ScolarAutorisationInscription.autorise_etud(
self.etud.id,
self.formsemestre_pair.formation.formation_code,
self.formsemestre_pair.id,
next_semestre_id,
)
self.recorded = True
def record_all(self):
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire, et sont donc en mode "automatique" """
decisions = (
list(self.decisions_ues.values())
+ list(self.decisions_rcue_by_niveau.values())
+ [self]
)
for dec in decisions:
if not dec.recorded:
# rappel: le code par défaut est en tête
code = dec.codes[0] if dec.codes else None
# s'il n'y a pas de code, efface
dec.record(code, no_overwrite=True)
def erase(self):
"""Efface les décisions de jury de cet étudiant
pour cette année: décisions d'UE, de RCUE, d'année,
et autorisations d'inscription émises.
"""
for dec_ue in self.decisions_ues.values():
dec_ue.erase()
for dec_rcue in self.decisions_rcue_by_niveau.values():
dec_rcue.erase()
if self.formsemestre_impair:
ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_impair.id
)
if self.formsemestre_pair:
ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_pair.id
)
validations = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
formsemestre_id=self.formsemestre_impair.id,
ordre=self.annee_but,
)
for validation in validations:
db.session.delete(validation)
db.session.flush()
class DecisionsProposeesRCUE(DecisionsProposees):
"""Liste des codes de décisions que l'on peut proposer pour
le RCUE de cet étudiant dans cette année.
ADM, CMP, ADJ, AJ, RAT, DEF, ABAN
"""
codes_communs = [
sco_codes.ADJ,
sco_codes.RAT,
sco_codes.DEF,
sco_codes.ABAN,
]
def __init__(
self, dec_prop_annee: DecisionsProposeesAnnee, rcue: RegroupementCoherentUE
):
super().__init__(etud=dec_prop_annee.etud)
self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = []
return
self.parcour = dec_prop_annee.parcour
self.validation = rcue.query_validations().first()
if self.validation is not None:
self.code_valide = self.validation.code
if rcue.est_compensable():
self.codes.insert(0, sco_codes.CMP)
elif rcue.est_validable():
self.codes.insert(0, sco_codes.ADM)
else:
self.codes.insert(0, sco_codes.AJ)
def record(self, code: str, no_overwrite=False):
"""Enregistre le code"""
if code and not code in self.codes:
raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True
return # no change
parcours_id = self.parcour.id if self.parcour is not None else None
if self.validation:
db.session.delete(self.validation)
db.session.flush()
if code is None:
self.validation = None
else:
self.validation = ApcValidationRCUE(
etudid=self.etud.id,
formsemestre_id=self.rcue.formsemestre_2.id,
ue1_id=self.rcue.ue_1.id,
ue2_id=self.rcue.ue_2.id,
parcours_id=parcours_id,
code=code,
)
Scolog.logdb(
method="jury_but",
etudid=self.etud.id,
msg=f"Validation RCUE {repr(self.rcue)}",
)
db.session.add(self.validation)
self.recorded = True
def erase(self):
"""Efface la décision de jury de cet étudiant pour cet RCUE"""
# par prudence, on requete toutes les validations, en cas de doublons
validations = self.rcue.query_validations()
for validation in validations:
db.session.delete(validation)
db.session.flush()
class DecisionsProposeesUE(DecisionsProposees):
"""Décisions de jury sur une UE du BUT
Liste des codes de décisions que l'on peut proposer pour
cette UE d'un étudiant dans un semestre.
Si DEF ou DEM ou ABAN ou ABL sur année BUT: seulement DEF, DEM, ABAN, ABL
si moy_ue > 10, ADM
sinon si compensation dans RCUE: CMP
sinon: ADJ, AJ
et proposer toujours: RAT, DEF, ABAN, DEM, UEBSL (codes_communs)
"""
# Codes toujours proposés sauf si include_communs est faux:
codes_communs = [
sco_codes.RAT,
sco_codes.DEF,
sco_codes.ABAN,
sco_codes.DEM,
sco_codes.UEBSL,
]
def __init__(
self,
etud: Identite,
formsemestre: FormSemestre,
ue: UniteEns,
):
super().__init__(etud=etud)
self.formsemestre = formsemestre
self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None
"Le rcu auquel est rattaché cette UE, ou None"
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
# mais ici on a restreint au formsemestre donc une seule (prend la première)
self.validation = ScolarFormSemestreValidation.query.filter_by(
etudid=self.etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
).first()
if self.validation is not None:
self.code_valide = self.validation.code
if ue.type == sco_codes.UE_SPORT:
self.explanation = "UE bonus, pas de décision de jury"
self.codes = [] # aucun code proposé
return
# Moyenne de l'UE ?
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
if not ue.id in res.etud_moy_ue:
self.explanation = "UE sans résultat"
return
if not etud.id in res.etud_moy_ue[ue.id]:
self.explanation = "Étudiant sans résultat dans cette UE"
return
self.moy_ue = res.etud_moy_ue[ue.id][etud.id]
def set_rcue(self, rcue: RegroupementCoherentUE):
"""Rattache cette UE à un RCUE. Cela peut modifier les codes
proposés (si compensation)"""
self.rcue = rcue
def compute_codes(self):
"""Calcul des .codes attribuables et de l'explanation associée"""
if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE):
self.codes.insert(0, sco_codes.ADM)
self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",)
elif self.rcue and self.rcue.est_compensable():
self.codes.insert(0, sco_codes.CMP)
self.explanation = "compensable dans le RCUE"
else:
# Échec à valider cette UE
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
self.explanation = "notes insuffisantes"
def record(self, code: str, no_overwrite=False):
"""Enregistre le code"""
if code and not code in self.codes:
raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True
return # no change
if self.validation:
db.session.delete(self.validation)
db.session.flush()
if code is None:
self.validation = None
else:
self.validation = ScolarFormSemestreValidation(
etudid=self.etud.id,
formsemestre_id=self.formsemestre.id,
ue_id=self.ue.id,
code=code,
moy_ue=self.moy_ue,
)
Scolog.logdb(
method="jury_but",
etudid=self.etud.id,
msg=f"Validation UE {self.ue.id}",
)
db.session.add(self.validation)
self.recorded = True
def erase(self):
"""Efface la décision de jury de cet étudiant pour cette UE"""
# par prudence, on requete toutes les validations, en cas de doublons
validations = ScolarFormSemestreValidation.query.filter_by(
etudid=self.etud.id, formsemestre_id=self.formsemestre.id, ue_id=self.ue.id
)
for validation in validations:
db.session.delete(validation)
db.session.flush()
class BUTCursusEtud: # WIP TODO
"""Validation du cursus d'un étudiant"""
def __init__(self, formsemestre: FormSemestre, etud: Identite):
if formsemestre.formation.referentiel_competence is None:
raise ScoException("BUTCursusEtud: pas de référentiel de compétences")
assert len(etud.formsemestre_inscriptions) > 0
self.formsemestre = formsemestre
self.etud = etud
#
# La dernière inscription en date va donner le parcours (donc les compétences à valider)
self.last_inscription = sorted(
etud.formsemestre_inscriptions, key=attrgetter("formsemestre.date_debut")
)[-1]
def est_diplomable(self) -> bool:
"""Vrai si toutes les compétences sont validables"""
return all(
self.competence_validable(competence)
for competence in self.competences_du_parcours()
)
def est_diplome(self) -> bool:
"""Vrai si BUT déjà validé"""
# vrai si la troisième année est validée
# On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel
# de formation que nous.
return (
ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3)
.join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id)
.join(Formation, FormSemestre.formation_id == Formation.id)
.filter(
Formation.referentiel_competence_id
== self.formsemestre.formation.referentiel_competence_id
)
.count()
> 0
)
def competences_du_parcours(self) -> list[ApcCompetence]:
"""Construit liste des compétences du parcours, qui doivent être
validées pour obtenir le diplôme.
Le parcours est celui de la dernière inscription.
"""
parcour = self.last_inscription.parcour
query = self.formsemestre.formation.formation.query_competences_parcour(parcour)
if query is None:
return []
return query.all()
def competence_validee(self, competence: ApcCompetence) -> bool:
"""Vrai si la compétence est validée, c'est à dire que tous ses
niveaux sont validés (ApcValidationRCUE).
"""
# XXX A REVOIR
validations = (
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
.join(ApcNiveau, ApcNiveau.id == UniteEns.niveau_competence_id)
.join(ApcCompetence, ApcCompetence.id == ApcNiveau.competence_id)
)
def competence_validable(self, competence: ApcCompetence):
"""Vrai si la compétence est "validable" automatiquement, c'est à dire
que les conditions de notes sont satisfaites pour l'acquisition de
son niveau le plus élevé, qu'il ne manque que l'enregistrement de la décision.
En vertu de la règle "La validation des deux UE du niveau dune compétence
emporte la validation de l'ensemble des UE du niveau inférieur de cette
même compétence.",
il suffit de considérer le dernier niveau dans lequel l'étudiant est inscrit.
"""
pass
def ues_emportees(self, niveau: ApcNiveau) -> list[tuple[FormSemestre, UniteEns]]:
"""La liste des UE à valider si on valide ce niveau.
Ne liste que les UE qui ne sont pas déjà acquises.
Selon la règle donéne par l'arrêté BUT:
* La validation des deux UE du niveau dune compétence emporte la validation de
l'ensemble des UE du niveau inférieur de cette même compétence.
"""
pass

409
app/but/jury_but_recap.py Normal file
View File

@ -0,0 +1,409 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT: table recap annuelle et liens saisie
"""
import time
import numpy as np
from flask import g, url_for
from app.but import jury_but
from app.but.jury_but import (
DecisionsProposeesAnnee,
DecisionsProposeesRCUE,
DecisionsProposeesUE,
)
from app.comp.res_but import ResultatsSemestreBUT
from app.comp import res_sem
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc.sco_codes_parcours import (
BUT_BARRE_RCUE,
BUT_BARRE_UE,
BUT_BARRE_UE8,
BUT_RCUE_SUFFISANT,
)
from app.scodoc import sco_formsemestre_status
from app.scodoc import html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_saisie_jury_but(
formsemestre2: FormSemestre,
readonly: bool = False,
selected_etudid: int = None,
mode="jury",
) -> str:
"""formsemestre est un semestre PAIR
Si readonly, ne montre pas le lien "saisir la décision"
=> page html complète
Si mode == "recap", table recap des codes, sans liens de saisie.
"""
# Quick & Dirty
# pour chaque etud de res2 trié
# S1: UE1, ..., UEn
# S2: UE1, ..., UEn
#
# UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue
#
# Pour chaque etud de res2 trié
# DecisionsProposeesAnnee(etud, formsemestre2)
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
if formsemestre2.semestre_id % 2 != 0:
raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
if formsemestre2.formation.referentiel_competence is None:
raise ScoValueError(
"""
<p>Pas de référentiel de compétences associé à la formation !</p>
<p>Pour associer un référentiel, passer par le menu <b>Semestre /
Voir la formation... </b> et suivre le lien <em>"associer à un référentiel
de compétences"</em>
"""
)
rows, titles, column_ids = get_table_jury_but(
formsemestre2, readonly=readonly, mode=mode
)
if not rows:
return (
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
)
filename = scu.sanitize_filename(
f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
klass = "table_jury_but_bilan" if mode == "recap" else ""
table_html = build_table_jury_but_html(
filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass
)
H = [
html_sco_header.sco_header(
page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel",
no_side_bar=True,
init_qtip=True,
javascripts=["js/etud_info.js", "js/table_recap.js"],
),
sco_formsemestre_status.formsemestre_status_head(
formsemestre_id=formsemestre2.id
),
]
if mode == "recap":
H.append(
"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3>"""
)
H.append(
f"""
{table_html}
<div class="table_jury_but_links">
"""
)
if (mode == "recap") and not readonly:
H.append(
f"""
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_saisie_jury",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}">Saisie des décisions du jury</a>
</p>"""
)
else:
H.append(
f"""
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_validation_auto_but",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}">Calcul automatique des décisions du jury</a>
</p>
<p><a class="stdlink" href="{url_for(
"notes.formsemestre_jury_but_recap",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
}">Tableau récapitulatif des décisions du jury</a>
</p>
"""
)
H.append(
f"""
</div>
{html_sco_header.sco_footer()}
"""
)
return "\n".join(H)
def build_table_jury_but_html(
filename: str, rows, titles, column_ids, selected_etudid: int = None, klass=""
) -> str:
"""assemble la table html"""
footer_rows = [] # inutilisé pour l'instant
H = [
f"""<div class="table_recap"><table class="table_recap apc jury table_jury_but {klass}"
data-filename="{filename}">"""
]
# header
H.append(
f"""
<thead>
{scu.gen_row(column_ids, titles, "th")}
</thead>
"""
)
# body
H.append("<tbody>")
for row in rows:
H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n")
H.append("</tbody>\n")
# footer
H.append("<tfoot>")
idx_last = len(footer_rows) - 1
for i, row in enumerate(footer_rows):
H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n')
H.append(
"""
</tfoot>
</table>
</div>
"""
)
return "".join(H)
class RowCollector:
"""Une ligne de la table"""
def __init__(
self,
cells: dict = None,
titles: dict = None,
convert_values=True,
column_classes: dict = None,
):
self.titles = titles
self.row = cells or {} # col_id : str
self.column_classes = column_classes # col_id : str, css class
self.idx = 0
self.last_etud_cell_idx = 0
if convert_values:
self.fmt_note = scu.fmt_note
else:
self.fmt_note = lambda x: x
def __setitem__(self, key, value):
self.row[key] = value
def __getitem__(self, key):
return self.row[key]
def get_row_dict(self):
"La ligne, comme un dict"
# create empty cells
for col_id in self.titles:
if col_id not in self.row:
self.row[col_id] = ""
klass = self.column_classes.get(col_id)
if klass:
self.row[f"_{col_id}_class"] = klass
return self.row
def add_cell(
self,
col_id: str,
title: str,
content: str,
classes: str = "",
idx: int = None,
column_class="",
):
"""Add a row to our table. classes is a list of css class names"""
self.idx = idx if idx is not None else self.idx
self.row[col_id] = content
if classes:
self.row[f"_{col_id}_class"] = classes + f" c{self.idx}"
if not col_id in self.titles:
self.titles[col_id] = title
self.titles[f"_{col_id}_col_order"] = self.idx
if classes:
self.titles[f"_{col_id}_class"] = classes
self.column_classes[col_id] = column_class
self.idx += 1
def add_etud_cells(self, etud: Identite, formsemestre: FormSemestre):
"Les cells code, nom, prénom etc."
# --- Codes (seront cachés, mais exportés en excel)
self.add_cell("etudid", "etudid", etud.id, "codes")
self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
# --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO)
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
self["_nom_disp_order"] = etud.sort_key
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
self["_nom_short_order"] = etud.sort_key
self["_nom_short_target"] = url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etud.id,
)
self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"'
self["_nom_disp_target"] = self["_nom_short_target"]
self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"]
self.last_etud_cell_idx = self.idx
def add_ue_cells(self, dec_ue: DecisionsProposeesUE):
"cell de moyenne d'UE"
col_id = f"moy_ue_{dec_ue.ue.id}"
note_class = ""
val = dec_ue.moy_ue
if isinstance(val, float):
if val < BUT_BARRE_UE:
note_class = " moy_inf"
elif val >= BUT_BARRE_UE:
note_class = " moy_ue_valid"
if val < BUT_BARRE_UE8:
note_class = " moy_ue_warning" # notes très basses
self.add_cell(
col_id,
dec_ue.ue.acronyme,
self.fmt_note(val),
"col_ue" + note_class,
column_class="col_ue",
)
self.add_cell(
col_id + "_code",
dec_ue.ue.acronyme,
dec_ue.code_valide or "",
"col_ue_code recorded_code",
column_class="col_ue",
)
def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE):
"2 cells: moyenne du RCUE, code enregistré"
rcue = dec_rcue.rcue
col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id
note_class = ""
val = rcue.moy_rcue
if isinstance(val, float):
if val < BUT_BARRE_RCUE:
note_class = " moy_ue_inf"
elif val >= BUT_BARRE_RCUE:
note_class = " moy_ue_valid"
if val < BUT_RCUE_SUFFISANT:
note_class = " moy_ue_warning" # notes très basses
self.add_cell(
col_id,
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
self.fmt_note(val),
"col_rcue" + note_class,
column_class="col_rcue",
)
self.add_cell(
col_id + "_code",
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
dec_rcue.code_valide or "",
"col_rcue_code recorded_code",
column_class="col_rcue",
)
def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee):
"cell avec nb niveaux validables / total"
klass = " "
if deca.nb_rcues_under_8 > 0:
klass += "moy_ue_warning"
elif deca.nb_validables < deca.nb_competences:
klass += "moy_ue_inf"
else:
klass += "moy_ue_valid"
self.add_cell(
"rcues_validables",
"RCUEs",
f"""{deca.nb_validables}/{deca.nb_competences}"""
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
"col_rcue col_rcues_validables" + klass,
)
if len(deca.rcues_annee) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
moy = deca.res_pair.etud_moy_gen[deca.etud.id]
if np.isnan(moy):
moy_gen_d = "x"
else:
moy_gen_d = f"{int(moy*1000):05}"
else:
moy_gen_d = "x"
self["_rcues_validables_order"] = f"{deca.nb_validables:04d}-{moy_gen_d}"
else:
# etudiants sans RCUE: pas de semestre impair, ...
# les classe à la fin
self[
"_rcues_validables_order"
] = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}"
def get_table_jury_but(
formsemestre2: FormSemestre, readonly: bool = False, mode="jury"
) -> tuple[list[dict], list[str], list[str]]:
"""Construit la table des résultats annuels pour le jury BUT"""
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
titles = {} # column_id : title
column_classes = {}
rows = []
for etudid in formsemestre2.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2)
row = RowCollector(titles=titles, column_classes=column_classes)
row.add_etud_cells(etud, formsemestre2)
row.idx = 100 # laisse place pour les colonnes de groupes
# --- Nombre de niveaux
row.add_nb_rcues_cell(deca)
# --- Les RCUEs
for rcue in deca.rcues_annee:
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id])
row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id])
row.add_rcue_cells(dec_rcue)
# --- Le code annuel existant
row.add_cell(
"code_annee",
"Année",
f"""{deca.code_valide or ''}""",
"col_code_annee",
)
# --- Le lien de saisie
if not readonly and not mode == "recap":
row.add_cell(
"lien_saisie",
"",
f"""
<a href="{url_for(
'notes.formsemestre_validation_but',
scodoc_dept=g.scodoc_dept,
etudid=etud.id,
formsemestre_id=formsemestre2.id,
)}" class="stdlink">
{"modif." if deca.code_valide else "saisie"}
décision</a>
""",
"col_lien_saisie_but",
)
rows.append(row)
rows_dict = [row.get_row_dict() for row in rows]
if len(rows_dict) > 0:
res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1)
column_ids = [title for title in titles if not title.startswith("_")]
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
return rows_dict, titles, column_ids

View File

@ -0,0 +1,34 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Jury BUT: clacul des décisions de jury annuelles "automatiques"
"""
from flask import g, url_for
from app import db
from app.but import jury_but
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_validation_auto_but(formsemestre: FormSemestre) -> int:
"""Calcul automatique des décisions de jury sur une année BUT.
Returns: nombre d'étudiants "admis"
"""
if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT")
nb_admis = 0
for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.admis: # année réussie
deca.record_all()
nb_admis += 1
db.session.commit()
return nb_admis

View File

@ -767,6 +767,21 @@ class BonusStMalo(BonusIUTRennes1):
__doc__ = BonusIUTRennes1.__doc__ __doc__ = BonusIUTRennes1.__doc__
class BonusLaRocheSurYon(BonusSportAdditif):
"""Bonus IUT de La Roche-sur-Yon
Si une note de bonus est saisie, l'étudiant est gratifié de 0,2 points
sur sa moyenne générale ou, en BUT, sur la moyenne de chaque UE.
"""
name = "bonus_larochesuryon"
displayed_name = "IUT de La Roche-sur-Yon"
seuil_moy_gen = 0.0
seuil_comptage = 0.0
proportion_point = 1e10 # le moindre point sature le bonus
bonus_max = 0.2 # à 0.2
class BonusLaRochelle(BonusSportAdditif): class BonusLaRochelle(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle. """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
@ -1023,6 +1038,54 @@ class BonusNantes(BonusSportAdditif):
bonus_max = 0.5 # plafonnement à 0.5 points bonus_max = 0.5 # plafonnement à 0.5 points
class BonusOrleans(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT d'Orléans
<p><b>Cadre général :</b>
En reconnaissance de l'engagement des étudiants dans la vie associative,
sociale ou professionnelle, lIUT dOrléans accorde, sous conditions,
une bonification aux étudiants inscrits qui en font la demande en début
dannée universitaire.
</p>
<p>Cet engagement doit être régulier et correspondre à une activité réelle
et sérieuse qui bénéficie à toute la communauté étudiante de lIUT,
de lUniversité ou à lensemble de la collectivité.</p>
<p><b>Bonification :</b>
Pour les DUT et LP, cette bonification interviendra sur la moyenne générale
des semestres pairs :
<ul><li> du 2ème semestre pour les étudiants de 1ère année de DUT</li>
<li> du 4ème semestre pour les étudiants de 2nde année de DUT</li>
<li> du 6ème semestre pour les étudiants en LP</li>
</ul>
Pour le BUT, cette bonification interviendra sur la moyenne de chacune
des UE des semestre pairs :
<ul><li> du 2ème semestre pour les étudiants de 1ère année de BUT</li>
<li> du 4ème semestre pour les étudiants de 2ème année de BUT</li>
<li> du 6ème semestre pour les étudiants de 3ème année de BUT</li>
</ul>
<em>La bonification ne peut dépasser +0,5 points par année universitaire.</em>
</p>
<p><b> Avant février 2020 :</b>
Un bonus de 2,5% de la note de sport est accordé à la moyenne générale.
</p>
"""
name = "bonus_iutorleans"
displayed_name = "IUT d'Orléans"
bonus_max = 0.5
seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés
proportion_point = 1
classic_use_bonus_ues = False
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
if self.formsemestre.date_debut > datetime.date(2020, 2, 1):
self.proportion_point = 1.0
else:
self.proportion_point = 2.5 / 100.0
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
class BonusPoitiers(BonusSportAdditif): class BonusPoitiers(BonusSportAdditif):
"""Calcul bonus optionnels (sport, culture), règle IUT de Poitiers. """Calcul bonus optionnels (sport, culture), règle IUT de Poitiers.

View File

@ -161,8 +161,11 @@ class ModuleImplResults:
evals_notes = evals_notes.merge( evals_notes = evals_notes.merge(
eval_df, how="left", left_index=True, right_index=True eval_df, how="left", left_index=True, right_index=True
) )
# Notes en attente: (on prend dans evals_notes pour ne pas avoir les dem.) # Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
nb_att = sum(evals_notes[str(evaluation.id)] == scu.NOTES_ATTENTE) nb_att = sum(
evals_notes[str(evaluation.id)][list(inscrits_module)]
== scu.NOTES_ATTENTE
)
self.evaluations_etat[evaluation.id] = EvaluationEtat( self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
) )

View File

@ -6,6 +6,8 @@
"""Résultats semestres BUT """Résultats semestres BUT
""" """
from collections.abc import Generator
from re import U
import time import time
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -28,6 +30,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
"modimpl_coefs_df", "modimpl_coefs_df",
"modimpls_evals_poids", "modimpls_evals_poids",
"sem_cube", "sem_cube",
"etuds_parcour_id", # parcours de chaque étudiant
"ues_inscr_parcours_df", # inscriptions aux UE / parcours
) )
def __init__(self, formsemestre): def __init__(self, formsemestre):
@ -35,7 +39,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.sem_cube = None self.sem_cube = None
"""ndarray (etuds x modimpl x ue)""" """ndarray (etuds x modimpl x ue)"""
self.etuds_parcour_id = None
"""Parcours de chaque étudiant { etudid : parcour_id }"""
if not self.load_cached(): if not self.load_cached():
t0 = time.time() t0 = time.time()
self.compute() self.compute()
@ -55,6 +60,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.modimpls_results, self.modimpls_results,
) = moy_ue.notes_sem_load_cube(self.formsemestre) ) = moy_ue.notes_sem_load_cube(self.formsemestre)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
) )
@ -108,6 +114,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
# Clippe toutes les moyennes d'UE dans [0,20] # Clippe toutes les moyennes d'UE dans [0,20]
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Nanifie les moyennes d'UE hors parcours pour chaque étudiant
self.etud_moy_ue *= self.ues_inscr_parcours_df
# Moyenne générale indicative: # Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
# donc la moyenne indicative) # donc la moyenne indicative)
@ -149,16 +158,24 @@ class ResultatsSemestreBUT(NotesTableCompat):
""" """
return self.modimpl_coefs_df.loc[ue.id].sum() return self.modimpl_coefs_df.loc[ue.id].sum()
def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]: def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]:
"""Liste des modimpl ayant des coefs non nuls vers cette UE """Liste des modimpl ayant des coefs non nuls vers cette UE
et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant. et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant.
""" """
# sert pour l'affichage ou non de l'UE sur le bulletin et la table recap # sert pour l'affichage ou non de l'UE sur le bulletin et la table recap
coefs = self.modimpl_coefs_df # row UE, cols modimpl if ue.type == UE_SPORT:
return [
modimpl
for modimpl in self.formsemestre.modimpls_sorted
if modimpl.module.ue.id == ue.id
and self.modimpl_inscr_df[modimpl.id][etudid]
]
coefs = self.modimpl_coefs_df # row UE (sans bonus), cols modimpl
modimpls = [ modimpls = [
modimpl modimpl
for modimpl in self.formsemestre.modimpls_sorted for modimpl in self.formsemestre.modimpls_sorted
if (coefs[modimpl.id][ue_id] != 0) if modimpl.module.ue.type != UE_SPORT
and (coefs[modimpl.id][ue.id] != 0)
and self.modimpl_inscr_df[modimpl.id][etudid] and self.modimpl_inscr_df[modimpl.id][etudid]
] ]
if not with_bonus: if not with_bonus:
@ -175,3 +192,50 @@ class ResultatsSemestreBUT(NotesTableCompat):
i = self.modimpl_coefs_df.columns.get_loc(modimpl_id) i = self.modimpl_coefs_df.columns.get_loc(modimpl_id)
j = self.modimpl_coefs_df.index.get_loc(ue_id) j = self.modimpl_coefs_df.index.get_loc(ue_id)
return self.sem_cube[:, i, j] return self.sem_cube[:, i, j]
def load_ues_inscr_parcours(self) -> pd.DataFrame:
"""Chargement des inscriptions aux parcours et calcul de la
matrice d'inscriptions (etuds, ue).
S'il n'y pas de référentiel de compétence, donc pas de parcours,
on considère l'étudiant inscrit à toutes les ue.
La matrice avec ue ne comprend que les UE non bonus.
1.0 si étudiant inscrit à l'UE, NaN sinon.
"""
etuds_parcour_id = {
inscr.etudid: inscr.parcour_id for inscr in self.formsemestre.inscriptions
}
self.etuds_parcour_id = etuds_parcour_id
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
# matrice de 1, inscrits par défaut à toutes les UE:
ues_inscr_parcours_df = pd.DataFrame(
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
)
if self.formsemestre.formation.referentiel_competence is None:
return ues_inscr_parcours_df
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
for parcour in self.formsemestre.formation.referentiel_competence.parcours:
ue_by_parcours[parcour.id] = {
ue.id: 1.0
for ue in self.formsemestre.formation.query_ues_parcour(
parcour
).filter_by(semestre_idx=self.formsemestre.semestre_id)
}
for etudid in etuds_parcour_id:
parcour = etuds_parcour_id[etudid]
if parcour is not None:
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[
etuds_parcour_id[etudid]
]
return ues_inscr_parcours_df
def etud_ues_ids(self, etudid: int) -> list[int]:
"""Liste des id d'UE auxquelles l'étudiant est inscrit (sans bonus).
(surchargée ici pour prendre en compte les parcours)
"""
s = self.ues_inscr_parcours_df.loc[etudid]
return s.index[s.notna()]
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
"""Liste des UE auxquelles l'étudiant est inscrit."""
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))

View File

@ -112,6 +112,14 @@ class ResultatsSemestre(ResultatsCache):
"dict { etudid : indice dans les inscrits }" "dict { etudid : indice dans les inscrits }"
return {e.id: idx for idx, e in enumerate(self.etuds)} return {e.id: idx for idx, e in enumerate(self.etuds)}
def etud_ues_ids(self, etudid: int) -> list[int]:
"""Liste des UE auxquelles l'etudiant est inscrit, sans bonus
(surchargée en BUT pour prendre en compte les parcours)
"""
# Pour les formations classiques, etudid n'est pas utilisé
# car tous les étudiants sont inscrits à toutes les UE
return [ue.id for ue in self.ues if ue.type != UE_SPORT]
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray: def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue. """Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
Utile pour stats bottom tableau recap. Utile pour stats bottom tableau recap.
@ -179,7 +187,7 @@ class ResultatsSemestre(ResultatsCache):
ues = sorted(list(ues), key=lambda x: x.numero or 0) ues = sorted(list(ues), key=lambda x: x.numero or 0)
return ues return ues
def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]: def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]:
"""Liste des modimpl de cette UE auxquels l'étudiant est inscrit. """Liste des modimpl de cette UE auxquels l'étudiant est inscrit.
Utile en formations classiques, surchargée pour le BUT. Utile en formations classiques, surchargée pour le BUT.
Inclus modules bonus le cas échéant. Inclus modules bonus le cas échéant.
@ -189,7 +197,7 @@ class ResultatsSemestre(ResultatsCache):
modimpls = [ modimpls = [
modimpl modimpl
for modimpl in self.formsemestre.modimpls_sorted for modimpl in self.formsemestre.modimpls_sorted
if modimpl.module.ue.id == ue_id if modimpl.module.ue.id == ue.id
and self.modimpl_inscr_df[modimpl.id][etudid] and self.modimpl_inscr_df[modimpl.id][etudid]
] ]
if not with_bonus: if not with_bonus:
@ -564,7 +572,7 @@ class ResultatsSemestre(ResultatsCache):
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE # Les moyennes des modules (ou ressources et SAÉs) dans cette UE
idx_malus = idx # place pour colonne malus à gauche des modules idx_malus = idx # place pour colonne malus à gauche des modules
idx += 1 idx += 1
for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False): for modimpl in self.modimpls_in_ue(ue, etudid, with_bonus=False):
if ue_status["is_capitalized"]: if ue_status["is_capitalized"]:
val = "-c-" val = "-c-"
else: else:
@ -622,9 +630,10 @@ class ResultatsSemestre(ResultatsCache):
f"_{col_id}_target_attrs" f"_{col_id}_target_attrs"
] = f""" title="{modimpl.module.titre} ({nom_resp})" """ ] = f""" title="{modimpl.module.titre} ({nom_resp})" """
modimpl_ids.add(modimpl.id) modimpl_ids.add(modimpl.id)
nb_ues_etud_parcours = len(self.etud_ues_ids(etudid))
ue_valid_txt = ( ue_valid_txt = (
ue_valid_txt_html ue_valid_txt_html
) = f"{nb_ues_validables}/{len(ues_sans_bonus)}" ) = f"{nb_ues_validables}/{nb_ues_etud_parcours}"
if nb_ues_warning: if nb_ues_warning:
ue_valid_txt_html += " " + scu.EMO_WARNING ue_valid_txt_html += " " + scu.EMO_WARNING
add_cell( add_cell(
@ -655,7 +664,7 @@ class ResultatsSemestre(ResultatsCache):
) )
rows.append(row) rows.append(row)
self._recap_add_partitions(rows, titles) self.recap_add_partitions(rows, titles)
self._recap_add_admissions(rows, titles) self._recap_add_admissions(rows, titles)
# tri par rang croissant # tri par rang croissant
@ -762,7 +771,9 @@ class ResultatsSemestre(ResultatsCache):
"apo": row_apo, "apo": row_apo,
} }
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict): def _recap_etud_groups_infos(
self, etudid: int, row: dict, titles: dict
): # XXX non utilisé
"""Table recap: ajoute à row les colonnes sur les groupes pour cet etud""" """Table recap: ajoute à row les colonnes sur les groupes pour cet etud"""
# dec = self.get_etud_decision_sem(etudid) # dec = self.get_etud_decision_sem(etudid)
# if dec: # if dec:
@ -818,7 +829,7 @@ class ResultatsSemestre(ResultatsCache):
else: else:
row[f"_{cid}_class"] = "admission" row[f"_{cid}_class"] = "admission"
def _recap_add_partitions(self, rows: list[dict], titles: dict): def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None):
"""Ajoute les colonnes indiquant les groupes """Ajoute les colonnes indiquant les groupes
rows est une liste de dict avec une clé "etudid" rows est une liste de dict avec une clé "etudid"
Les colonnes ont la classe css "partition" Les colonnes ont la classe css "partition"
@ -827,7 +838,7 @@ class ResultatsSemestre(ResultatsCache):
self.formsemestre.id self.formsemestre.id
) )
first_partition = True first_partition = True
col_order = 10 col_order = 10 if col_idx is None else col_idx
for partition in partitions: for partition in partitions:
cid = f"part_{partition['partition_id']}" cid = f"part_{partition['partition_id']}"
rg_cid = cid + "_rg" # rang dans la partition rg_cid = cid + "_rg" # rang dans la partition

View File

@ -87,10 +87,10 @@ def permission_required(permission):
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
scodoc_dept = getattr(g, "scodoc_dept", None) scodoc_dept = getattr(g, "scodoc_dept", None)
if not current_user.has_permission(permission, scodoc_dept): if not current_user.has_permission(permission, scodoc_dept):
abort(403) return current_app.login_manager.unauthorized()
return f(*args, **kwargs) return f(*args, **kwargs)
return login_required(decorated_function) return decorated_function
return decorator return decorator

View File

@ -41,6 +41,7 @@ from app.scodoc import sco_codes_parcours
def _build_code_field(code): def _build_code_field(code):
return StringField( return StringField(
label=code, label=code,
default=code,
description=sco_codes_parcours.CODES_EXPL[code], description=sco_codes_parcours.CODES_EXPL[code],
validators=[ validators=[
validators.regexp( validators.regexp(
@ -58,6 +59,8 @@ def _build_code_field(code):
class CodesDecisionsForm(FlaskForm): class CodesDecisionsForm(FlaskForm):
"Formulaire code décisions Apogée" "Formulaire code décisions Apogée"
ABAN = _build_code_field("ABAN")
ABL = _build_code_field("ABL")
ADC = _build_code_field("ADC") ADC = _build_code_field("ADC")
ADJ = _build_code_field("ADJ") ADJ = _build_code_field("ADJ")
ADM = _build_code_field("ADM") ADM = _build_code_field("ADM")
@ -68,8 +71,13 @@ class CodesDecisionsForm(FlaskForm):
CMP = _build_code_field("CMP") CMP = _build_code_field("CMP")
DEF = _build_code_field("DEF") DEF = _build_code_field("DEF")
DEM = _build_code_field("DEM") DEM = _build_code_field("DEM")
EXCLU = _build_code_field("EXCLU")
NAR = _build_code_field("NAR") NAR = _build_code_field("NAR")
PASD = _build_code_field("PASD")
PAS1NCI = _build_code_field("PAS1NCI")
RAT = _build_code_field("RAT") RAT = _build_code_field("RAT")
RED = _build_code_field("RED")
NOTES_FMT = StringField( NOTES_FMT = StringField(
label="Format notes exportées", label="Format notes exportées",
description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""", description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",

View File

@ -1,14 +1,25 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
"""Modèles base de données ScoDoc """Modèles base de données ScoDoc
XXX version préliminaire ScoDoc8 #sco8 sans département
""" """
import sqlalchemy
CODE_STR_LEN = 16 # chaine pour les codes CODE_STR_LEN = 16 # chaine pour les codes
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs) APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
GROUPNAME_STR_LEN = 64 GROUPNAME_STR_LEN = 64
convention = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
"pk": "pk_%(table_name)s",
}
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
from app.models.raw_sql_init import create_database_functions from app.models.raw_sql_init import create_database_functions
from app.models.absences import Absence, AbsenceNotification, BilletAbsence from app.models.absences import Absence, AbsenceNotification, BilletAbsence
@ -65,5 +76,8 @@ from app.models.but_refcomp import (
ApcCompetence, ApcCompetence,
ApcSituationPro, ApcSituationPro,
ApcAppCritique, ApcAppCritique,
ApcParcours,
) )
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig

View File

@ -11,7 +11,9 @@ class Absence(db.Model):
__tablename__ = "absences" __tablename__ = "absences"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) etudid = db.Column(
db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True
)
jour = db.Column(db.Date) jour = db.Column(db.Date)
estabs = db.Column(db.Boolean()) estabs = db.Column(db.Boolean())
estjust = db.Column(db.Boolean()) estjust = db.Column(db.Boolean())
@ -59,7 +61,7 @@ class AbsenceNotification(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
) )
notification_date = db.Column( notification_date = db.Column(
db.DateTime(timezone=True), server_default=db.func.now() db.DateTime(timezone=True), server_default=db.func.now()

View File

@ -1,12 +1,9 @@
"""ScoDoc 9 models : Formation BUT 2021 """ScoDoc 9 models : Formation BUT 2021
XXX inutilisé
""" """
from enum import unique
from typing import Any
from app import db from app import db
from app.scodoc.sco_utils import ModuleType
class APCFormation(db.Model): class APCFormation(db.Model):
"""Formation par compétence""" """Formation par compétence"""

View File

@ -7,12 +7,14 @@
""" """
from datetime import datetime from datetime import datetime
import flask_sqlalchemy
from sqlalchemy.orm import class_mapper from sqlalchemy.orm import class_mapper
import sqlalchemy import sqlalchemy
from app import db from app import db
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import ScoValueError
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns # from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
@ -116,7 +118,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
# self.formations = formations # self.formations = formations
def __repr__(self): def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite}>" return f"<ApcReferentielCompetences {self.id} {self.specialite!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.
@ -139,6 +141,52 @@ class ApcReferentielCompetences(db.Model, XMLModel):
"parcours": {x.code: x.to_dict() for x in self.parcours}, "parcours": {x.code: x.to_dict() for x in self.parcours},
} }
def get_niveaux_by_parcours(self, annee) -> dict:
"""
Construit la liste des niveaux de compétences pour chaque parcours
de ce référentiel.
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut:
on cherche les niveaux qui sont présents dans tous les parcours et les range sous
la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun).
résultat:
{
"TC" : [ ApcNiveau ],
parcour.id : [ ApcNiveau ]
}
"""
parcours = self.parcours.order_by(ApcParcours.numero).all()
niveaux_by_parcours = {
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
for parcour in parcours
}
# Cherche tronc commun
niveaux_ids_tc = set.intersection(
*[
{n.id for n in niveaux_by_parcours[parcour_id]}
for parcour_id in niveaux_by_parcours
]
)
# Enleve les niveaux du tronc commun
niveaux_by_parcours_no_tc = {
parcour.id: [
niveau
for niveau in niveaux_by_parcours[parcour.id]
if niveau.id not in niveaux_ids_tc
]
for parcour in parcours
}
# Niveaux du TC
niveaux_tc = []
if len(parcours):
niveaux_parcours_1 = niveaux_by_parcours[parcours[0].id]
niveaux_tc = [
niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc
]
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
return niveaux_by_parcours_no_tc
class ApcCompetence(db.Model, XMLModel): class ApcCompetence(db.Model, XMLModel):
"Compétence" "Compétence"
@ -204,7 +252,7 @@ class ApcCompetence(db.Model, XMLModel):
self.niveaux = niveaux self.niveaux = niveaux
def __repr__(self): def __repr__(self):
return f"<ApcCompetence {self.id} {self.titre}>" return f"<ApcCompetence {self.id} {self.titre!r}>"
def to_dict(self): def to_dict(self):
return { return {
@ -257,13 +305,20 @@ class ApcComposanteEssentielle(db.Model, XMLModel):
class ApcNiveau(db.Model, XMLModel): class ApcNiveau(db.Model, XMLModel):
"""Niveau de compétence
Chaque niveau peut être associé à deux UE,
des semestres impair et pair de la même année.
"""
__tablename__ = "apc_niveau"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
competence_id = db.Column( competence_id = db.Column(
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
) )
libelle = db.Column(db.Text(), nullable=False) libelle = db.Column(db.Text(), nullable=False)
annee = db.Column(db.Text(), nullable=False) # "BUT2" annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3"
# L'ordre est l'année d'apparition de ce niveau # L'ordre est le niveau (1,2,3) ou (1,2) suivant la competence
ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3 ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3
app_critiques = db.relationship( app_critiques = db.relationship(
"ApcAppCritique", "ApcAppCritique",
@ -271,6 +326,7 @@ class ApcNiveau(db.Model, XMLModel):
lazy="dynamic", lazy="dynamic",
cascade="all, delete-orphan", cascade="all, delete-orphan",
) )
ues = db.relationship("UniteEns", back_populates="niveau_competence")
def __init__(self, id, competence_id, libelle, annee, ordre, app_critiques): def __init__(self, id, competence_id, libelle, annee, ordre, app_critiques):
self.id = id self.id = id
@ -281,7 +337,8 @@ class ApcNiveau(db.Model, XMLModel):
self.app_critiques = app_critiques self.app_critiques = app_critiques
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} ordre={self.ordre}>" return f"""<{self.__class__.__name__} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>"""
def to_dict(self): def to_dict(self):
return { return {
@ -291,6 +348,55 @@ class ApcNiveau(db.Model, XMLModel):
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}, "app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
} }
@classmethod
def niveaux_annee_de_parcours(
cls,
parcour: "ApcParcours",
annee: int,
referentiel_competence: ApcReferentielCompetences = None,
) -> flask_sqlalchemy.BaseQuery:
"""Les niveaux de l'année du parcours
Si le parcour est None, tous les niveaux de l'année
"""
if annee not in {1, 2, 3}:
raise ValueError("annee invalide pour un parcours BUT")
if referentiel_competence is None:
raise ScoValueError(
"Pas de référentiel de compétences associé à la formation !"
)
annee_formation = f"BUT{annee}"
if parcour is None:
return ApcNiveau.query.filter(
ApcNiveau.annee == annee_formation,
ApcCompetence.id == ApcNiveau.competence_id,
ApcCompetence.referentiel_id == referentiel_competence.id,
)
else:
return ApcNiveau.query.filter(
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
ApcParcours.id == ApcAnneeParcours.parcours_id,
ApcParcours.referentiel == parcour.referentiel,
ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id,
ApcCompetence.id == ApcNiveau.competence_id,
ApcAnneeParcours.parcours == parcour,
ApcNiveau.annee == annee_formation,
)
app_critiques_modules = db.Table(
"apc_modules_acs",
db.Column(
"module_id",
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
primary_key=True,
),
db.Column(
"app_crit_id",
db.ForeignKey("apc_app_critique.id"),
primary_key=True,
),
)
class ApcAppCritique(db.Model, XMLModel): class ApcAppCritique(db.Model, XMLModel):
"Apprentissage Critique BUT" "Apprentissage Critique BUT"
@ -299,12 +405,31 @@ class ApcAppCritique(db.Model, XMLModel):
code = db.Column(db.Text(), nullable=False, index=True) code = db.Column(db.Text(), nullable=False, index=True)
libelle = db.Column(db.Text()) libelle = db.Column(db.Text())
modules = db.relationship( # modules = db.relationship(
"Module", # "Module",
secondary="apc_modules_acs", # secondary="apc_modules_acs",
lazy="dynamic", # lazy="dynamic",
backref=db.backref("app_critiques", lazy="dynamic"), # backref=db.backref("app_critiques", lazy="dynamic"),
# )
@classmethod
def app_critiques_ref_comp(
cls,
ref_comp: ApcReferentielCompetences,
annee: str,
competence: ApcCompetence = None,
) -> flask_sqlalchemy.BaseQuery:
"Liste les AC de tous les parcours de ref_comp pour l'année indiquée"
assert annee in {"BUT1", "BUT2", "BUT3"}
query = cls.query.filter(
ApcAppCritique.niveau_id == ApcNiveau.id,
ApcNiveau.competence_id == ApcCompetence.id,
ApcNiveau.annee == annee,
ApcCompetence.referentiel_id == ref_comp.id,
) )
if competence is not None:
query = query.filter(ApcNiveau.competence == competence)
return query
def __init__(self, id, niveau_id, code, libelle, modules): def __init__(self, id, niveau_id, code, libelle, modules):
self.id = id self.id = id
@ -320,18 +445,40 @@ class ApcAppCritique(db.Model, XMLModel):
return self.code + " - " + self.titre return self.code + " - " + self.titre
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.code}>" return f"<{self.__class__.__name__} {self.code!r}>"
def get_saes(self): def get_saes(self):
"""Liste des SAE associées""" """Liste des SAE associées"""
return [m for m in self.modules if m.module_type == ModuleType.SAE] return [m for m in self.modules if m.module_type == ModuleType.SAE]
ApcAppCritiqueModules = db.Table( parcours_modules = db.Table(
"apc_modules_acs", "parcours_modules",
db.Column("module_id", db.ForeignKey("notes_modules.id")), db.Column(
db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")), "parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
),
db.Column(
"module_id",
db.Integer,
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
primary_key=True,
),
) )
"""Association parcours <-> modules (many-to-many)"""
parcours_formsemestre = db.Table(
"parcours_formsemestre",
db.Column(
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
),
db.Column(
"formsemestre_id",
db.Integer,
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
primary_key=True,
),
)
"""Association parcours <-> formsemestre (many-to-many)"""
class ApcParcours(db.Model, XMLModel): class ApcParcours(db.Model, XMLModel):
@ -358,7 +505,7 @@ class ApcParcours(db.Model, XMLModel):
self.annes = annes self.annes = annes
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.code}>" return f"<{self.__class__.__name__} {self.code!r}>"
def to_dict(self): def to_dict(self):
return { return {
@ -375,6 +522,7 @@ class ApcAnneeParcours(db.Model, XMLModel):
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
) )
ordre = db.Column(db.Integer) ordre = db.Column(db.Integer)
"numéro de l'année: 1, 2, 3"
def __init__(self, id, parcours_id, ordre): def __init__(self, id, parcours_id, ordre):
self.id = id self.id = id
@ -382,7 +530,7 @@ class ApcAnneeParcours(db.Model, XMLModel):
self.ordre = ordre self.ordre = ordre
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} ordre={self.ordre}>" return f"<{self.__class__.__name__} ordre={self.ordre!r} parcours={self.parcours.code!r}>"
def to_dict(self): def to_dict(self):
return { return {
@ -420,6 +568,7 @@ class ApcParcoursNiveauCompetence(db.Model):
"annee_parcours", "annee_parcours",
passive_deletes=True, passive_deletes=True,
cascade="save-update, merge, delete, delete-orphan", cascade="save-update, merge, delete, delete-orphan",
lazy="dynamic",
), ),
) )
annee_parcours = db.relationship( annee_parcours = db.relationship(
@ -432,4 +581,4 @@ class ApcParcoursNiveauCompetence(db.Model):
) )
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.competence} {self.annee_parcours}>" return f"<{self.__class__.__name__} {self.competence!r}<->{self.annee_parcours!r} niveau={self.niveau!r}>"

View File

@ -0,0 +1,282 @@
# -*- coding: UTF-8 -*
"""Décisions de jury (validations) des RCUE et années du BUT
"""
import flask_sqlalchemy
from sqlalchemy.sql import text
from typing import Union
from app import db
from app.models import CODE_STR_LEN
from app.models.but_refcomp import ApcNiveau
from app.models.etudiants import Identite
from app.models.ues import UniteEns
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours as sco_codes
class ApcValidationRCUE(db.Model):
"""Validation des niveaux de compétences
aka "regroupements cohérents d'UE" dans le jargon BUT.
le formsemestre est celui du semestre PAIR du niveau de compétence
"""
__tablename__ = "apc_validation_rcue"
# Assure unicité de la décision:
__table_args__ = (
db.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"),
)
id = db.Column(db.Integer, primary_key=True)
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id", ondelete="CASCADE"),
index=True,
nullable=False,
)
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
)
# Les deux UE associées à ce niveau:
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
# optionnel, le parcours dans lequel se trouve la compétence:
parcours_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), nullable=True)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
etud = db.relationship("Identite", backref="apc_validations_rcues")
formsemestre = db.relationship("FormSemestre", backref="apc_validations_rcues")
ue1 = db.relationship("UniteEns", foreign_keys=ue1_id)
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
parcour = db.relationship("ApcParcours")
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.etud} {self.ue1}/{self.ue2}:{self.code!r}>"
def niveau(self) -> ApcNiveau:
"""Le niveau de compétence associé à cet RCUE."""
# Par convention, il est donné par la seconde UE
return self.ue2.niveau_competence
# Attention: ce n'est pas un modèle mais une classe ordinaire:
class RegroupementCoherentUE:
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
La moyenne (10/20) au RCU déclenche la compensation des UE.
"""
def __init__(
self,
etud: Identite,
formsemestre_1: FormSemestre,
ue_1: UniteEns,
formsemestre_2: FormSemestre,
ue_2: UniteEns,
):
from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
(
ue_2,
formsemestre_2,
),
(ue_1, formsemestre_1),
)
assert formsemestre_1.semestre_id % 2 == 1
assert formsemestre_2.semestre_id % 2 == 0
assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1
assert ue_1.niveau_competence_id == ue_2.niveau_competence_id
self.etud = etud
self.formsemestre_1 = formsemestre_1
"semestre impair"
self.ue_1 = ue_1
self.formsemestre_2 = formsemestre_2
"semestre pair"
self.ue_2 = ue_2
# Stocke les moyennes d'UE
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN
else:
self.moy_ue_1 = None
self.moy_ue_1_val = 0.0
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2)
if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]:
self.moy_ue_2 = res.etud_moy_ue[ue_2.id][etud.id]
self.moy_ue_2_val = self.moy_ue_2
else:
self.moy_ue_2 = None
self.moy_ue_2_val = 0.0
# Calcul de la moyenne au RCUE
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
# Moyenne RCUE (les pondérations par défaut sont 1.)
self.moy_rcue = (
self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue
) / (ue_1.coef_rcue + ue_2.coef_rcue)
else:
self.moy_rcue = None
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>"
def query_validations(
self,
) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE]
"""Les validations de jury enregistrées pour ce RCUE"""
niveau = self.ue_2.niveau_competence
return (
ApcValidationRCUE.query.filter_by(
etudid=self.etud.id,
)
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
.filter(ApcNiveau.id == niveau.id)
)
def other_ue(self, ue: UniteEns) -> UniteEns:
"""L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
if ue.id == self.ue_1.id:
return self.ue_2
elif ue.id == self.ue_2.id:
return self.ue_1
raise ValueError(f"ue {ue} hors RCUE {self}")
def est_enregistre(self) -> bool:
"""Vrai si ce RCUE, donc le niveau de compétences correspondant
a une décision jury enregistrée
"""
return self.query_validations().count() > 0
def est_compensable(self):
"""Vrai si ce RCUE est validable par compensation
c'est à dire que sa moyenne est > 10 avec une UE < 10
"""
return (
(self.moy_rcue is not None)
and (self.moy_rcue > sco_codes.BUT_BARRE_RCUE)
and (
(self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN)
or (self.moy_ue_2_val < sco_codes.NOTES_BARRE_GEN)
)
)
def est_suffisant(self) -> bool:
"""Vrai si ce RCUE est > 8"""
return (self.moy_rcue is not None) and (
self.moy_rcue > sco_codes.BUT_RCUE_SUFFISANT
)
def est_validable(self) -> bool:
"""Vrai si ce RCU satisfait les conditions pour être validé
Pour cela, il suffit que la moyenne des UE qui le constitue soit > 10
"""
return (self.moy_rcue is not None) and (
self.moy_rcue > sco_codes.BUT_BARRE_RCUE
)
def code_valide(self) -> Union[ApcValidationRCUE, None]:
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
validation = self.query_validations().first()
if (validation is not None) and (
validation.code in {sco_codes.ADM, sco_codes.ADJ, sco_codes.CMP}
):
return validation
return None
def find_rcues(
formsemestre: FormSemestre, ue: UniteEns, etud: Identite
) -> list[RegroupementCoherentUE]:
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
ce semestre pour cette UE.
Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
Résultat: la liste peut être vide.
"""
if (ue.niveau_competence is None) or (ue.semestre_idx is None):
return []
if ue.semestre_idx % 2: # S1, S3, S5
other_semestre_idx = ue.semestre_idx + 1
else:
other_semestre_idx = ue.semestre_idx - 1
cursor = db.session.execute(
text(
"""SELECT
ue.id, formsemestre.id
FROM
notes_ue ue,
notes_formsemestre_inscription inscr,
notes_formsemestre formsemestre
WHERE
inscr.etudid = :etudid
AND inscr.formsemestre_id = formsemestre.id
AND formsemestre.semestre_id = :other_semestre_idx
AND ue.formation_id = formsemestre.formation_id
AND ue.niveau_competence_id = :ue_niveau_competence_id
AND ue.semestre_idx = :other_semestre_idx
"""
),
{
"etudid": etud.id,
"other_semestre_idx": other_semestre_idx,
"ue_niveau_competence_id": ue.niveau_competence_id,
},
)
rcues = []
for ue_id, formsemestre_id in cursor:
other_ue = UniteEns.query.get(ue_id)
other_formsemestre = FormSemestre.query.get(formsemestre_id)
rcues.append(
RegroupementCoherentUE(etud, formsemestre, ue, other_formsemestre, other_ue)
)
# safety check: 1 seul niveau de comp. concerné:
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
return rcues
class ApcValidationAnnee(db.Model):
"""Validation des années du BUT"""
__tablename__ = "apc_validation_annee"
# Assure unicité de la décision:
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire"),)
id = db.Column(db.Integer, primary_key=True)
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id", ondelete="CASCADE"),
index=True,
nullable=False,
)
ordre = db.Column(db.Integer, nullable=False)
"numéro de l'année: 1, 2, 3"
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
)
"le semestre IMPAIR (le 1er) de l'année"
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
etud = db.relationship("Identite", backref="apc_validations_annees")
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"

View File

@ -9,6 +9,8 @@ from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_codes_parcours import ( from app.scodoc.sco_codes_parcours import (
ABAN,
ABL,
ADC, ADC,
ADJ, ADJ,
ADM, ADM,
@ -19,11 +21,16 @@ from app.scodoc.sco_codes_parcours import (
CMP, CMP,
DEF, DEF,
DEM, DEM,
EXCLU,
NAR, NAR,
PASD,
PAS1NCI,
RAT, RAT,
) )
CODES_SCODOC_TO_APO = { CODES_SCODOC_TO_APO = {
ABAN: "ABAN",
ABL: "ABL",
ADC: "ADMC", ADC: "ADMC",
ADJ: "ADM", ADJ: "ADM",
ADM: "ADM", ADM: "ADM",
@ -34,7 +41,10 @@ CODES_SCODOC_TO_APO = {
CMP: "COMP", CMP: "COMP",
DEF: "NAR", DEF: "NAR",
DEM: "NAR", DEM: "NAR",
EXCLU: "EXC",
NAR: "NAR", NAR: "NAR",
PASD: "PASD",
PAS1NCI: "PAS1NCI",
RAT: "ATT", RAT: "ATT",
"NOTES_FMT": "%3.2f", "NOTES_FMT": "%3.2f",
} }
@ -161,9 +171,8 @@ class ScoDocSiteConfig(db.Model):
@classmethod @classmethod
def get_code_apo(cls, code: str) -> str: def get_code_apo(cls, code: str) -> str:
"""La représentation d'un code pour les exports Apogée. """La représentation d'un code pour les exports Apogée.
Par exemple, à l'iUT du H., le code ADM est réprésenté par VAL Par exemple, à l'IUT du H., le code ADM est réprésenté par VAL
Les codes par défaut sont donnés dans sco_apogee_csv. Les codes par défaut sont donnés dans sco_apogee_csv.
""" """
cfg = ScoDocSiteConfig.query.filter_by(name=code).first() cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
if not cfg: if not cfg:
@ -172,6 +181,11 @@ class ScoDocSiteConfig(db.Model):
code_apo = cfg.value code_apo = cfg.value
return code_apo return code_apo
@classmethod
def get_codes_apo_dict(cls) -> dict[str:str]:
"Un dict avec code jury : code exporté"
return {code: cls.get_code_apo(code) for code in CODES_SCODOC_TO_APO}
@classmethod @classmethod
def set_code_apo(cls, code: str, code_apo: str): def set_code_apo(cls, code: str, code_apo: str):
"""Enregistre nouvelle représentation du code""" """Enregistre nouvelle représentation du code"""

View File

@ -60,7 +60,9 @@ class Identite(db.Model):
admission = db.relationship("Admission", backref="identite", lazy="dynamic") admission = db.relationship("Admission", backref="identite", lazy="dynamic")
def __repr__(self): def __repr__(self):
return f"<Etud {self.id}/{self.departement.acronym} {self.nom} {self.prenom}>" return (
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
)
@classmethod @classmethod
def from_request(cls, etudid=None, code_nip=None): def from_request(cls, etudid=None, code_nip=None):
@ -133,8 +135,10 @@ class Identite(db.Model):
def sort_key(self) -> tuple: def sort_key(self) -> tuple:
"clé pour tris par ordre alphabétique" "clé pour tris par ordre alphabétique"
return ( return (
scu.suppress_accents(self.nom_usuel or self.nom or "").lower(), scu.sanitize_string(
scu.suppress_accents(self.prenom or "").lower(), scu.suppress_accents(self.nom_usuel or self.nom or "").lower()
),
scu.sanitize_string(scu.suppress_accents(self.prenom or "").lower()),
) )
def get_first_email(self, field="email") -> str: def get_first_email(self, field="email") -> str:
@ -434,7 +438,7 @@ class Adresse(db.Model):
adresse_id = db.synonym("id") adresse_id = db.synonym("id")
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
) )
email = db.Column(db.Text()) # mail institutionnel email = db.Column(db.Text()) # mail institutionnel
emailperso = db.Column(db.Text) # email personnel (exterieur) emailperso = db.Column(db.Text) # email personnel (exterieur)
@ -468,7 +472,7 @@ class Admission(db.Model):
adm_id = db.synonym("id") adm_id = db.synonym("id")
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
) )
# Anciens champs de ScoDoc7, à revoir pour être plus générique et souple # Anciens champs de ScoDoc7, à revoir pour être plus générique et souple
# notamment dans le cadre du bac 2021 # notamment dans le cadre du bac 2021
@ -513,21 +517,21 @@ class Admission(db.Model):
def to_dict(self, no_nulls=False): def to_dict(self, no_nulls=False):
"""Représentation dictionnaire,""" """Représentation dictionnaire,"""
e = dict(self.__dict__) d = dict(self.__dict__)
e.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
if no_nulls: if no_nulls:
for k in e: for k in d.keys():
if e[k] is None: if d[k] is None:
col_type = getattr( col_type = getattr(
sqlalchemy.inspect(models.Admission).columns, "apb_groupe" sqlalchemy.inspect(models.Admission).columns, "apb_groupe"
).expression.type ).expression.type
if isinstance(col_type, sqlalchemy.Text): if isinstance(col_type, sqlalchemy.Text):
e[k] = "" d[k] = ""
elif isinstance(col_type, sqlalchemy.Integer): elif isinstance(col_type, sqlalchemy.Integer):
e[k] = 0 d[k] = 0
elif isinstance(col_type, sqlalchemy.Boolean): elif isinstance(col_type, sqlalchemy.Boolean):
e[k] = False d[k] = False
return e return d
# Suivi scolarité / débouchés # Suivi scolarité / débouchés
@ -538,7 +542,7 @@ class ItemSuivi(db.Model):
itemsuivi_id = db.synonym("id") itemsuivi_id = db.synonym("id")
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
) )
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
situation = db.Column(db.Text) situation = db.Column(db.Text)

View File

@ -32,6 +32,21 @@ class Scolog(db.Model):
authenticated_user = db.Column(db.Text) # login, sans contrainte authenticated_user = db.Column(db.Text) # login, sans contrainte
# zope_remote_addr suppressed # zope_remote_addr suppressed
@classmethod
def logdb(
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
):
"""Add entry in student's log (replacement for old scolog.logdb)"""
entry = Scolog(
method=method,
msg=msg,
etudid=etudid,
authenticated_user=current_user.user_name,
)
db.session.add(entry)
if commit:
db.session.commit()
class ScolarNews(db.Model): class ScolarNews(db.Model):
"""Nouvelles pour page d'accueil""" """Nouvelles pour page d'accueil"""

View File

@ -1,15 +1,25 @@
"""ScoDoc 9 models : Formations """ScoDoc 9 models : Formations
""" """
import flask_sqlalchemy
import app import app
from app import db from app import db
from app.comp import df_cache from app.comp import df_cache
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcCompetence,
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
)
from app.models.modules import Module from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_cache 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_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_STANDARD
class Formation(db.Model): class Formation(db.Model):
@ -45,7 +55,11 @@ class Formation(db.Model):
modules = db.relationship("Module", lazy="dynamic", backref="formation") modules = db.relationship("Module", lazy="dynamic", backref="formation")
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>" return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>"
def to_html(self) -> str:
"titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
def to_dict(self): def to_dict(self):
e = dict(self.__dict__) e = dict(self.__dict__)
@ -55,7 +69,10 @@ class Formation(db.Model):
return e return e
def get_parcours(self): def get_parcours(self):
"""get l'instance de TypeParcours de cette formation""" """get l'instance de TypeParcours de cette formation
(le TypeParcours définit le genre de formation, à ne pas confondre
avec les parcours du BUT).
"""
return sco_codes_parcours.get_parcours_from_code(self.type_parcours) return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
def get_titre_version(self) -> str: def get_titre_version(self) -> str:
@ -97,6 +114,13 @@ class Formation(db.Model):
else: else:
keys = f"{self.id}.{semestre_idx}" keys = f"{self.id}.{semestre_idx}"
df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"}) df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"})
# Invalidate aussi les poids de toutes les évals de la formation
for modimpl in ModuleImpl.query.filter(
ModuleImpl.module_id == Module.id,
Module.formation_id == self.id,
):
modimpl.invalidate_evaluations_poids()
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
def invalidate_cached_sems(self): def invalidate_cached_sems(self):
@ -148,6 +172,40 @@ class Formation(db.Model):
if change: if change:
app.clear_scodoc_cache() app.clear_scodoc_cache()
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:
"""Les UEs d'un parcours de la formation.
Exemple: pour avoir les UE du semestre 3, faire
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
"""
return UniteEns.query.filter_by(formation=self).filter(
UniteEns.niveau_competence_id == ApcNiveau.id,
UniteEns.type == UE_STANDARD,
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
ApcAnneeParcours.parcours_id == parcour.id,
)
def query_competences_parcour(
self, parcour: ApcParcours
) -> flask_sqlalchemy.BaseQuery:
"""Les ApcCompetences d'un parcours de la formation.
None si pas de référentiel de compétences.
"""
if self.referentiel_competence_id is None:
return None
return (
ApcCompetence.query.filter_by(referentiel_id=self.referentiel_competence_id)
.join(
ApcParcoursNiveauCompetence,
ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id,
)
.join(
ApcAnneeParcours,
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
)
.filter(ApcAnneeParcours.parcours_id == parcour.id)
)
class Matiere(db.Model): class Matiere(db.Model):
"""Matières: regroupe les modules d'une UE """Matières: regroupe les modules d'une UE
@ -168,7 +226,7 @@ class Matiere(db.Model):
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={ return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
self.ue_id}, titre='{self.titre}')>""" self.ue_id}, titre='{self.titre!r}')>"""
def to_dict(self): def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7""" """as a dict, with the same conversions as in ScoDoc7"""

View File

@ -5,19 +5,31 @@
import datetime import datetime
from functools import cached_property from functools import cached_property
from flask import flash
import flask_sqlalchemy import flask_sqlalchemy
from sqlalchemy.sql import text
from app import db from app import db
from app import log from app import log
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
)
from app.models.groups import GroupDescr, Partition
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.models.ues import UniteEns from app.models.but_refcomp import ApcParcours
from app.models.but_refcomp import parcours_formsemestre
from app.models.etudiants import Identite
from app.models.modules import Module from app.models.modules import Module
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.etudiants import Identite from app.models.ues import UniteEns
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_vdi import ApoEtapeVDI
@ -113,6 +125,14 @@ class FormSemestre(db.Model):
# ne pas utiliser après migrate_scodoc7_dept_archives # ne pas utiliser après migrate_scodoc7_dept_archives
scodoc7_id = db.Column(db.Text(), nullable=True) scodoc7_id = db.Column(db.Text(), nullable=True)
# BUT
parcours = db.relationship(
"ApcParcours",
secondary=parcours_formsemestre,
lazy="subquery",
backref=db.backref("formsemestres", lazy=True),
)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(FormSemestre, self).__init__(**kwargs) super(FormSemestre, self).__init__(**kwargs)
if self.modalite is None: if self.modalite is None:
@ -219,6 +239,22 @@ class FormSemestre(db.Model):
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT) sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
return sem_ues.order_by(UniteEns.numero) return sem_ues.order_by(UniteEns.numero)
def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery:
"""UE que suit l'étudiant dans ce semestre BUT
en fonction du parcours dans lequel il est inscrit.
Si voulez les UE d'un parcours, il est plus efficace de passer par
`formation.query_ues_parcour(parcour)`.
"""
return self.query_ues().filter(
FormSemestreInscription.etudid == etudid,
FormSemestreInscription.formsemestre == self,
UniteEns.niveau_competence_id == ApcNiveau.id,
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
)
@cached_property @cached_property
def modimpls_sorted(self) -> list[ModuleImpl]: def modimpls_sorted(self) -> list[ModuleImpl]:
"""Liste des modimpls du semestre (y compris bonus) """Liste des modimpls du semestre (y compris bonus)
@ -245,6 +281,28 @@ class FormSemestre(db.Model):
) )
return modimpls return modimpls
def modimpls_parcours(self, parcours: ApcParcours) -> list[ModuleImpl]:
"""Liste des modimpls du semestre (sans les bonus (?)) dans le parcours donné.
- triée par type/numéro/code ??
"""
cursor = db.session.execute(
text(
"""
SELECT modimpl.id
FROM notes_moduleimpl modimpl, notes_modules mod,
parcours_modules pm, parcours_formsemestre pf
WHERE modimpl.formsemestre_id = :formsemestre_id
AND modimpl.module_id = mod.id
AND pm.module_id = mod.id
AND pm.parcours_id = pf.parcours_id
AND pf.parcours_id = :parcours_id
AND pf.formsemestre_id = :formsemestre_id
"""
),
{"formsemestre_id": self.id, "parcours_id": parcours.id},
)
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
def can_be_edited_by(self, user): def can_be_edited_by(self, user):
"""Vrai si user peut modifier ce semestre""" """Vrai si user peut modifier ce semestre"""
if not user.has_permission(Permission.ScoImplement): # pas chef if not user.has_permission(Permission.ScoImplement): # pas chef
@ -311,6 +369,25 @@ class FormSemestre(db.Model):
return "" return ""
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape])) return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
"""Calcule la liste des regroupements cohérents d'UE impliquant ce
formsemestre.
Pour une année donnée: l'étudiant est inscrit dans ScoDoc soit dans le semestre
impair, soit pair, soit les deux (il est rare mais pas impossible d'avoir une
inscription seulement en semestre pair, par exemple suite à un transfert ou un
arrêt temporaire du cursus).
1. Déterminer l'*autre* formsemestre: semestre précédent ou suivant de la même
année, formation compatible (même référentiel de compétence) dans lequel
l'étudiant est inscrit.
2. Construire les couples d'UE (regroupements cohérents): apparier les UE qui
ont le même `ApcParcoursNiveauCompetence`.
"""
if not self.formation.is_apc():
return []
raise NotImplementedError() # XXX
def responsables_str(self, abbrev_prenom=True) -> str: def responsables_str(self, abbrev_prenom=True) -> str:
"""chaîne "J. Dupond, X. Martin" """chaîne "J. Dupond, X. Martin"
ou "Jacques Dupond, Xavier Martin" ou "Jacques Dupond, Xavier Martin"
@ -327,6 +404,11 @@ class FormSemestre(db.Model):
"True si l'user est l'un des responsables du semestre" "True si l'user est l'un des responsables du semestre"
return user.id in [u.id for u in self.responsables] return user.id in [u.id for u in self.responsables]
def annee_scolaire(self) -> int:
"""L'année de début de l'année scolaire.
Par exemple, 2022 si le semestre va de septebre 2022 à février 2023."""
return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
def annee_scolaire_str(self): def annee_scolaire_str(self):
"2021 - 2022" "2021 - 2022"
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month) return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
@ -449,6 +531,85 @@ class FormSemestre(db.Model):
"""Map { etudid : inscription } (incluant DEM et DEF)""" """Map { etudid : inscription } (incluant DEM et DEF)"""
return {ins.etud.id: ins for ins in self.inscriptions} return {ins.etud.id: ins for ins in self.inscriptions}
def setup_parcours_groups(self) -> None:
"""Vérifie et créee si besoin la partition et les groupes de parcours BUT."""
if not self.formation.is_apc():
return
partition = Partition.query.filter_by(
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
).first()
if partition is None:
# Création de la partition de parcours
partition = Partition(
formsemestre_id=self.id,
partition_name=scu.PARTITION_PARCOURS,
numero=-1,
)
db.session.add(partition)
db.session.flush() # pour avoir un id
flash(f"Partition Parcours créée.")
for parcour in self.parcours:
if parcour.code:
group = GroupDescr.query.filter_by(
partition_id=partition.id, group_name=parcour.code
).first()
if not group:
partition.groups.append(GroupDescr(group_name=parcour.code))
db.session.commit()
def update_inscriptions_parcours_from_groups(self) -> None:
"""Met à jour les inscriptions dans les parcours du semestres en
fonction des groupes de parcours.
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
et leur nom est le code du parcours (eg "Cyber").
"""
partition = Partition.query.filter_by(
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
).first()
if partition is None: # pas de partition de parcours
return
# Efface les inscriptions aux parcours:
db.session.execute(
text(
"""UPDATE notes_formsemestre_inscription
SET parcour_id=NULL
WHERE formsemestre_id=:formsemestre_id
"""
),
{
"formsemestre_id": self.id,
},
)
# Inscrit les étudiants des groupes de parcours:
for group in partition.groups:
query = ApcParcours.query.filter_by(code=group.group_name)
if query.count() != 1:
log(
f"""update_inscriptions_parcours_from_groups: {
query.count()} parcours with code {group.group_name}"""
)
continue
parcour = query.first()
db.session.execute(
text(
"""UPDATE notes_formsemestre_inscription ins
SET parcour_id=:parcour_id
FROM group_membership gm
WHERE formsemestre_id=:formsemestre_id
AND gm.etudid = ins.etudid
AND gm.group_id = :group_id
"""
),
{
"formsemestre_id": self.id,
"parcour_id": parcour.id,
"group_id": group.id,
},
)
db.session.commit()
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
notes_formsemestre_responsables = db.Table( notes_formsemestre_responsables = db.Table(
@ -607,7 +768,9 @@ class FormSemestreInscription(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
formsemestre_inscription_id = db.synonym("id") formsemestre_inscription_id = db.synonym("id")
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) etudid = db.Column(
db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True
)
formsemestre_id = db.Column( formsemestre_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),
@ -627,11 +790,16 @@ class FormSemestreInscription(db.Model):
) )
# I inscrit, D demission en cours de semestre, DEF si "defaillant" # I inscrit, D demission en cours de semestre, DEF si "defaillant"
etat = db.Column(db.String(CODE_STR_LEN), index=True) etat = db.Column(db.String(CODE_STR_LEN), index=True)
# etape apogee d'inscription (experimental 2020) # Etape Apogée d'inscription (ajout 2020)
etape = db.Column(db.String(APO_CODE_STR_LEN)) etape = db.Column(db.String(APO_CODE_STR_LEN))
# Parcours (pour les BUT)
parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True)
parcour = db.relationship(ApcParcours)
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={self.formsemestre_id} etat={self.etat}>" return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
self.formsemestre_id} etat={self.etat} {
('parcours='+str(self.parcour)) if self.parcour else ''}>"""
class NotesSemSet(db.Model): class NotesSemSet(db.Model):

View File

@ -23,7 +23,7 @@ class Partition(db.Model):
) )
# "TD", "TP", ... (NULL for 'all') # "TD", "TP", ... (NULL for 'all')
partition_name = db.Column(db.String(SHORT_STR_LEN)) partition_name = db.Column(db.String(SHORT_STR_LEN))
# numero = ordre de presentation) # Numero = ordre de presentation)
numero = db.Column(db.Integer) numero = db.Column(db.Integer)
# Calculer le rang ? # Calculer le rang ?
bul_show_rank = db.Column( bul_show_rank = db.Column(
@ -33,6 +33,10 @@ class Partition(db.Model):
show_in_lists = db.Column( show_in_lists = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true" db.Boolean(), nullable=False, default=True, server_default="true"
) )
# Editable ? (faux pour les groupes de parcours)
groups_editable = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true"
)
groups = db.relationship( groups = db.relationship(
"GroupDescr", "GroupDescr",
backref=db.backref("partition", lazy=True), backref=db.backref("partition", lazy=True),
@ -106,7 +110,7 @@ class GroupDescr(db.Model):
group_membership = db.Table( group_membership = db.Table(
"group_membership", "group_membership",
db.Column("etudid", db.Integer, db.ForeignKey("identite.id")), db.Column("etudid", db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")),
db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")),
db.UniqueConstraint("etudid", "group_id"), db.UniqueConstraint("etudid", "group_id"),
) )
@ -116,5 +120,5 @@ group_membership = db.Table(
# __tablename__ = "group_membership" # __tablename__ = "group_membership"
# __table_args__ = (db.UniqueConstraint("etudid", "group_id"),) # __table_args__ = (db.UniqueConstraint("etudid", "group_id"),)
# id = db.Column(db.Integer, primary_key=True) # id = db.Column(db.Integer, primary_key=True)
# etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) # etudid = db.Column(db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"))
# group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id")) # group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id"))

View File

@ -3,6 +3,7 @@
from app import db from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models.but_refcomp import app_critiques_modules, parcours_modules
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -44,13 +45,27 @@ class Module(db.Model):
lazy=True, lazy=True,
backref=db.backref("modules", lazy=True), backref=db.backref("modules", lazy=True),
) )
# BUT
parcours = db.relationship(
"ApcParcours",
secondary=parcours_modules,
lazy="subquery",
backref=db.backref("modules", lazy=True),
)
app_critiques = db.relationship(
"ApcAppCritique",
secondary=app_critiques_modules,
lazy="subquery",
backref=db.backref("modules", lazy=True),
)
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.ue_coefs = [] self.ue_coefs = []
super(Module, self).__init__(**kwargs) super(Module, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code}>" return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code!r}>"
def to_dict(self): def to_dict(self):
e = dict(self.__dict__) e = dict(self.__dict__)

View File

@ -17,7 +17,7 @@ class BulAppreciations(db.Model):
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
index=True, index=True,
) )
formsemestre_id = db.Column( formsemestre_id = db.Column(
@ -36,7 +36,7 @@ class NotesNotes(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
) )
evaluation_id = db.Column( evaluation_id = db.Column(
db.Integer, db.ForeignKey("notes_evaluation.id"), index=True db.Integer, db.ForeignKey("notes_evaluation.id"), index=True
@ -75,7 +75,7 @@ class NotesNotesLog(db.Model):
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
) )
evaluation_id = db.Column( evaluation_id = db.Column(
db.Integer, db.Integer,

View File

@ -40,8 +40,15 @@ class UniteEns(db.Model):
# coef UE, utilise seulement si l'option use_ue_coefs est activée: # coef UE, utilise seulement si l'option use_ue_coefs est activée:
coefficient = db.Column(db.Float) coefficient = db.Column(db.Float)
# coef. pour le calcul de moyennes de RCUE. Par défaut, 1.
coef_rcue = db.Column(db.Float, nullable=False, default=1.0, server_default="1.0")
color = db.Column(db.Text()) color = db.Column(db.Text())
# BUT
niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"))
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
# relations # relations
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
modules = db.relationship("Module", lazy="dynamic", backref="ue") modules = db.relationship("Module", lazy="dynamic", backref="ue")

View File

@ -6,6 +6,7 @@
from app import db from app import db
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models.events import Scolog
class ScolarFormSemestreValidation(db.Model): class ScolarFormSemestreValidation(db.Model):
@ -19,7 +20,7 @@ class ScolarFormSemestreValidation(db.Model):
formsemestre_validation_id = db.synonym("id") formsemestre_validation_id = db.synonym("id")
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
index=True, index=True,
) )
formsemestre_id = db.Column( formsemestre_id = db.Column(
@ -36,7 +37,7 @@ class ScolarFormSemestreValidation(db.Model):
# NULL pour les UE, True|False pour les semestres: # NULL pour les UE, True|False pour les semestres:
assidu = db.Column(db.Boolean) assidu = db.Column(db.Boolean)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
# NULL sauf si compense un semestre: # NULL sauf si compense un semestre: (pas utilisé pour BUT)
compense_formsemestre_id = db.Column( compense_formsemestre_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),
@ -54,7 +55,7 @@ class ScolarFormSemestreValidation(db.Model):
ue = db.relationship("UniteEns", lazy="select", uselist=False) ue = db.relationship("UniteEns", lazy="select", uselist=False)
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue_id}, 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})"
class ScolarAutorisationInscription(db.Model): class ScolarAutorisationInscription(db.Model):
@ -66,10 +67,10 @@ class ScolarAutorisationInscription(db.Model):
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
) )
formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False)
# semestre ou on peut s'inscrire: # Indice du semestre où on peut s'inscrire:
semestre_id = db.Column(db.Integer) semestre_id = db.Column(db.Integer)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
origin_formsemestre_id = db.Column( origin_formsemestre_id = db.Column(
@ -77,6 +78,44 @@ class ScolarAutorisationInscription(db.Model):
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),
) )
@classmethod
def autorise_etud(
cls,
etudid: int,
formation_code: str,
origin_formsemestre_id: int,
semestre_id: int,
):
"""Enregistre une autorisation, remplace celle émanant du même semestre si elle existe."""
cls.delete_autorisation_etud(etudid, origin_formsemestre_id)
autorisation = cls(
etudid=etudid,
formation_code=formation_code,
origin_formsemestre_id=origin_formsemestre_id,
semestre_id=semestre_id,
)
db.session.add(autorisation)
Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}")
@classmethod
def delete_autorisation_etud(
cls,
etudid: int,
origin_formsemestre_id: int,
):
"""Efface les autorisations de cette étudiant venant du sem. origine"""
autorisations = cls.query.filter_by(
etudid=etudid, origin_formsemestre_id=origin_formsemestre_id
)
for autorisation in autorisations:
db.session.delete(autorisation)
Scolog.logdb(
"autorise_etud",
etudid=etudid,
msg=f"annule passage vers S{autorisation.semestre_id}",
)
db.session.flush()
class ScolarEvent(db.Model): class ScolarEvent(db.Model):
"""Evenement dans le parcours scolaire d'un étudiant""" """Evenement dans le parcours scolaire d'un étudiant"""
@ -86,7 +125,7 @@ class ScolarEvent(db.Model):
event_id = db.synonym("id") event_id = db.synonym("id")
etudid = db.Column( etudid = db.Column(
db.Integer, db.Integer,
db.ForeignKey("identite.id"), db.ForeignKey("identite.id", ondelete="CASCADE"),
) )
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
formsemestre_id = db.Column( formsemestre_id = db.Column(

View File

@ -207,11 +207,15 @@ class TF(object):
else: else:
self.values[field] = 1 self.values[field] = 1
if field not in self.values: if field not in self.values:
if (descr.get("input_type", None) == "checkbox") and self.submitted():
# aucune case cochée
self.values[field] = []
else:
if "default" in descr: # first: default in form description if "default" in descr: # first: default in form description
self.values[field] = descr["default"] self.values[field] = descr["default"]
else: # then: use initvalues dict else: # then: use initvalues dict
self.values[field] = self.initvalues.get(field, "") self.values[field] = self.initvalues.get(field, "")
if self.values[field] == None: if self.values[field] is None:
self.values[field] = "" self.values[field] = ""
# convert numbers, except ids # convert numbers, except ids
@ -392,9 +396,7 @@ class TF(object):
if self.top_buttons: if self.top_buttons:
R.append(buttons_markup + "<p></p>") R.append(buttons_markup + "<p></p>")
R.append('<table class="tf">') R.append('<table class="tf">')
idx = 0 for field, descr in self.formdescription:
for idx in range(len(self.formdescription)):
(field, descr) = self.formdescription[idx]
if descr.get("readonly", False): if descr.get("readonly", False):
R.append(self._ReadOnlyElement(field, descr)) R.append(self._ReadOnlyElement(field, descr))
continue continue
@ -408,7 +410,7 @@ class TF(object):
input_type = descr.get("input_type", "text") input_type = descr.get("input_type", "text")
item_dom_id = descr.get("dom_id", "") item_dom_id = descr.get("dom_id", "")
if item_dom_id: if item_dom_id:
item_dom_attr = ' id="%s"' % item_dom_id item_dom_attr = f' id="{item_dom_id}"'
else: else:
item_dom_attr = "" item_dom_attr = ""
# choix du template # choix du template
@ -523,7 +525,6 @@ class TF(object):
else: else:
checked = "" checked = ""
else: # boolcheckbox else: # boolcheckbox
# open('/tmp/toto','a').write('GenForm: values[%s] = %s (%s)\n' % (field, values[field], type(values[field])))
if values[field] == "True": if values[field] == "True":
v = True v = True
elif values[field] == "False": elif values[field] == "False":

View File

@ -59,35 +59,29 @@ BOOTSTRAP_MULTISELECT_CSS = [
def standard_html_header(): def standard_html_header():
"""Standard HTML header for pages outside depts""" """Standard HTML header for pages outside depts"""
# not used in ZScolar, see sco_header # not used in ZScolar, see sco_header
return """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> return f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head> <html><head>
<title>ScoDoc: accueil</title> <title>ScoDoc: accueil</title>
<META http-equiv="Content-Type" content="text/html; charset=%s"> <META http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}">
<META http-equiv="Content-Style-Type" content="text/css"> <META http-equiv="Content-Style-Type" content="text/css">
<META name="LANG" content="fr"> <META name="LANG" content="fr">
<META name="DESCRIPTION" content="ScoDoc: gestion scolarite"> <META name="DESCRIPTION" content="ScoDoc: gestion scolarite">
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css"/> <link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css"/>
</head><body>%s""" % ( </head><body>{scu.CUSTOM_HTML_HEADER_CNX}"""
scu.SCO_ENCODING,
scu.CUSTOM_HTML_HEADER_CNX,
)
def standard_html_footer(): def standard_html_footer():
"""Le pied de page HTML de la page d'accueil.""" """Le pied de page HTML de la page d'accueil."""
return """<p class="footer"> return f"""<p class="footer">
Problème de connexion (identifiant, mot de passe): <em>contacter votre responsable ou chef de département</em>.</p> Problème de connexion (identifiant, mot de passe): <em>contacter votre responsable ou chef de département</em>.</p>
<p>Probl&egrave;mes et suggestions sur le logiciel: <a href="mailto:%s">%s</a></p> <p>Probl&egrave;mes et suggestions sur le logiciel: <a href="mailto:{scu.SCO_USERS_LIST}">{scu.SCO_USERS_LIST}</a></p>
<p><em>ScoDoc est un logiciel libre développé par Emmanuel Viennet.</em></p> <p><em>ScoDoc est un logiciel libre développé par Emmanuel Viennet.</em></p>
</body></html>""" % ( </body></html>"""
scu.SCO_USERS_LIST,
scu.SCO_USERS_LIST,
)
_HTML_BEGIN = """<!DOCTYPE html> _HTML_BEGIN = f"""<!DOCTYPE html>
<html lang="fr"> <html lang="fr">
<head> <head>
@ -100,27 +94,27 @@ _HTML_BEGIN = """<!DOCTYPE html>
<meta name="DESCRIPTION" content="ScoDoc" /> <meta name="DESCRIPTION" content="ScoDoc" />
<title>%(page_title)s</title> <title>%(page_title)s</title>
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" /> <link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" /> <link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" /> <link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<script src="/ScoDoc/static/libjs/menu.js"></script> <script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="/ScoDoc/static/libjs/bubble.js"></script> <script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script> <script>
window.onload=function(){enableTooltips("gtrcontent")}; window.onload=function(){{enableTooltips("gtrcontent")}};
</script> </script>
<script src="/ScoDoc/static/jQuery/jquery.js"></script> <script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
<script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script> <script src="{scu.STATIC_DIR}/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script> <script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script> <script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script> <script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" /> <link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
<script src="/ScoDoc/static/js/scodoc.js"></script> <script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
<script src="/ScoDoc/static/js/etud_info.js"></script> <script src="{scu.STATIC_DIR}/js/etud_info.js"></script>
""" """
@ -138,9 +132,9 @@ def sco_header(
# optional args # optional args
page_title="", # page title page_title="", # page title
no_side_bar=False, # hide sidebar no_side_bar=False, # hide sidebar
cssstyles=[], # additionals CSS sheets cssstyles=(), # additionals CSS sheets
javascripts=[], # additionals JS filenames to load javascripts=(), # additionals JS filenames to load
scripts=[], # script to put in page header scripts=(), # script to put in page header
bodyOnLoad="", # JS bodyOnLoad="", # JS
init_qtip=False, # include qTip init_qtip=False, # include qTip
init_google_maps=False, # Google maps init_google_maps=False, # Google maps
@ -148,6 +142,8 @@ def sco_header(
titrebandeau="", # titre dans bandeau superieur titrebandeau="", # titre dans bandeau superieur
head_message="", # message action (petit cadre jaune en haut) head_message="", # message action (petit cadre jaune en haut)
user_check=True, # verifie passwords temporaires user_check=True, # verifie passwords temporaires
etudid=None,
formsemestre_id=None,
): ):
"Main HTML page header for ScoDoc" "Main HTML page header for ScoDoc"
from app.scodoc.sco_formsemestre_status import formsemestre_page_title from app.scodoc.sco_formsemestre_status import formsemestre_page_title
@ -191,7 +187,7 @@ def sco_header(
# jQuery UI # jQuery UI
# can modify loaded theme here # can modify loaded theme here
H.append( H.append(
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n' f'<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
) )
if init_google_maps: if init_google_maps:
# It may be necessary to add an API key: # It may be necessary to add an API key:
@ -200,72 +196,65 @@ def sco_header(
# Feuilles de style additionnelles: # Feuilles de style additionnelles:
for cssstyle in cssstyles: for cssstyle in cssstyles:
H.append( H.append(
"""<link type="text/css" rel="stylesheet" href="/ScoDoc/static/%s" />\n""" f"""<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/{cssstyle}" />\n"""
% cssstyle
) )
H.append( H.append(
""" f"""
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" /> <link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" /> <link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" /> <link href="{scu.STATIC_DIR}/css/gt_table.css" rel="stylesheet" type="text/css" />
<script src="/ScoDoc/static/libjs/menu.js"></script> <script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="/ScoDoc/static/libjs/bubble.js"></script> <script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script> <script>
window.onload=function(){enableTooltips("gtrcontent")}; window.onload=function(){{enableTooltips("gtrcontent")}};
var SCO_URL="%(ScoURL)s"; var SCO_URL="{scu.ScoURL()}";
</script>""" </script>"""
% params
) )
# jQuery # jQuery
H.append( H.append(
"""<script src="/ScoDoc/static/jQuery/jquery.js"></script> f"""<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
""" <script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>"""
) )
H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
# qTip # qTip
if init_qtip: if init_qtip:
H.append( H.append(
'<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>' f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
) <link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />"""
H.append(
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />'
) )
H.append( H.append(
'<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>' f"""<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>"""
) )
H.append('<script src="/ScoDoc/static/js/scodoc.js"></script>')
if init_google_maps: if init_google_maps:
H.append( H.append(
'<script src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>' f'<script src="{scu.STATIC_DIR}/libjs/jquery.ui.map.full.min.js"></script>'
) )
if init_datatables: if init_datatables:
H.append( H.append(
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>' f"""<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css"/>
<script src="{scu.STATIC_DIR}/DataTables/datatables.min.js"></script>"""
) )
H.append('<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>')
# H.append( # H.append(
# '<link href="/ScoDoc/static/css/tooltip.css" rel="stylesheet" type="text/css" />' # f'<link href="{scu.STATIC_DIR}/css/tooltip.css" rel="stylesheet" type="text/css" />'
# ) # )
# JS additionels # JS additionels
for js in javascripts: for js in javascripts:
H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js) H.append(f"""<script src="{scu.STATIC_DIR}/{js}"></script>\n""")
H.append( H.append(
"""<style> f"""<style>
#gtrcontent { #gtrcontent {{
margin-left: %(margin_left)s; margin-left: {params["margin_left"]};
height: 100%%; height: 100%%;
margin-bottom: 10px; margin-bottom: 10px;
} }}
</style> </style>
""" """
% params
) )
# Scripts de la page: # Scripts de la page:
if scripts: if scripts:
@ -281,25 +270,24 @@ def sco_header(
H.append(scu.CUSTOM_HTML_HEADER) H.append(scu.CUSTOM_HTML_HEADER)
# #
if not no_side_bar: if not no_side_bar:
H.append(html_sidebar.sidebar()) H.append(html_sidebar.sidebar(etudid))
H.append("""<div id="gtrcontent">""") H.append("""<div id="gtrcontent">""")
# En attendant le replacement complet de cette fonction, # En attendant le replacement complet de cette fonction,
# inclusion ici des messages flask # inclusion ici des messages flask
H.append(render_template("flashed_messages.html")) H.append(render_template("flashed_messages.html"))
# #
# Barre menu semestre: # Barre menu semestre:
H.append(formsemestre_page_title()) H.append(formsemestre_page_title(formsemestre_id))
# Avertissement si mot de passe à changer # Avertissement si mot de passe à changer
if user_check: if user_check:
if current_user.passwd_temp: if current_user.passwd_temp:
H.append( H.append(
"""<div class="passwd_warn"> f"""<div class="passwd_warn">
Attention !<br/> Attention !<br/>
Vous avez reçu un mot de passe temporaire.<br/> Vous avez reçu un mot de passe temporaire.<br/>
Vous devez le changer: <a href="%s/form_change_password?user_name=%s">cliquez ici</a> Vous devez le changer: <a href="{scu.UsersURL}/form_change_password?user_name={current_user.user_name}">cliquez ici</a>
</div>""" </div>"""
% (scu.UsersURL, current_user.user_name)
) )
# #
if head_message: if head_message:
@ -328,6 +316,6 @@ def html_sem_header(
else: else:
h = "" h = ""
if with_h2: if with_h2:
return h + """<h2 class="formsemestre">%s</h2>""" % (title) return h + f"""<h2 class="formsemestre">{title}</h2>"""
else: else:
return h return h

View File

@ -73,7 +73,7 @@ def sidebar_common():
return "".join(H) return "".join(H)
def sidebar(): def sidebar(etudid: int = None):
"Main HTML page sidebar" "Main HTML page sidebar"
# rewritten from legacy DTML code # rewritten from legacy DTML code
from app.scodoc import sco_abs from app.scodoc import sco_abs
@ -93,14 +93,14 @@ def sidebar():
""" """
] ]
# ---- Il y-a-t-il un etudiant selectionné ? # ---- Il y-a-t-il un etudiant selectionné ?
etudid = g.get("etudid", None) etudid = etudid if etudid is not None else g.get("etudid", None)
if not etudid: if etudid is None:
if request.method == "GET": if request.method == "GET":
etudid = request.args.get("etudid", None) etudid = request.args.get("etudid", None)
elif request.method == "POST": elif request.method == "POST":
etudid = request.form.get("etudid", None) etudid = request.form.get("etudid", None)
if etudid: if etudid is not None:
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
params.update(etud) params.update(etud)
params["fiche_url"] = url_for( params["fiche_url"] = url_for(

View File

@ -362,7 +362,11 @@ def do_formsemestre_archive(
# Decisions de jury, en XLS # Decisions de jury, en XLS
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False) data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
if data: if data:
PVArchive.store(archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data) PVArchive.store(
archive_id,
"Decisions_Jury" + scu.XLSX_SUFFIX,
data,
)
# Classeur bulletins (PDF) # Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=bulVersion formsemestre_id, version=bulVersion

View File

@ -158,9 +158,24 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["server_name"] = request.url_root I["server_name"] = request.url_root
# Formation et parcours # Formation et parcours
if I["sem"]["formation_id"]:
I["formation"] = sco_formations.formation_list( I["formation"] = sco_formations.formation_list(
args={"formation_id": I["sem"]["formation_id"]} args={"formation_id": I["sem"]["formation_id"]}
)[0] )[0]
else: # what's the fuck ?
I["formation"] = {
"acronyme": "?",
"code_specialite": "",
"dept_id": 1,
"formation_code": "?",
"formation_id": -1,
"id": -1,
"referentiel_competence_id": None,
"titre": "?",
"titre_officiel": "?",
"type_parcours": 0,
"version": 0,
}
I["parcours"] = sco_codes_parcours.get_parcours_from_code( I["parcours"] = sco_codes_parcours.get_parcours_from_code(
I["formation"]["type_parcours"] I["formation"]["type_parcours"]
) )

View File

@ -439,7 +439,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
ects_txt = "-" ects_txt = "-"
t = { t = {
"titre": ue["acronyme"] + " " + ue["titre"], "titre": ue["acronyme"] + " " + (ue["titre"] or ""),
"_titre_html": plusminus "_titre_html": plusminus
+ ue["acronyme"] + ue["acronyme"]
+ " " + " "

View File

@ -67,6 +67,7 @@ class ScoDocCache:
timeout = None # ttl, infinite by default timeout = None # ttl, infinite by default
prefix = "" prefix = ""
verbose = False # if true, verbose logging (debug)
@classmethod @classmethod
def _get_key(cls, oid): def _get_key(cls, oid):
@ -87,7 +88,10 @@ class ScoDocCache:
def set(cls, oid, value): def set(cls, oid, value):
"""Store value""" """Store value"""
key = cls._get_key(oid) key = cls._get_key(oid)
# log(f"CACHE key={key}, type={type(value)}, timeout={cls.timeout}") if cls.verbose:
log(
f"{cls.__name__}.set key={key}, type={type(value).__name__}, timeout={cls.timeout}"
)
try: try:
status = CACHE.set(key, value, timeout=cls.timeout) status = CACHE.set(key, value, timeout=cls.timeout)
if not status: if not status:
@ -101,11 +105,15 @@ class ScoDocCache:
@classmethod @classmethod
def delete(cls, oid): def delete(cls, oid):
"""Remove from cache""" """Remove from cache"""
# if cls.verbose:
# log(f"{cls.__name__}.delete({oid})")
CACHE.delete(cls._get_key(oid)) CACHE.delete(cls._get_key(oid))
@classmethod @classmethod
def delete_many(cls, oids): def delete_many(cls, oids):
"""Remove multiple keys at once""" """Remove multiple keys at once"""
if cls.verbose:
log(f"{cls.__name__}.delete_many({oids})")
# delete_many seems bugged: # delete_many seems bugged:
# CACHE.delete_many([cls._get_key(oid) for oid in oids]) # CACHE.delete_many([cls._get_key(oid) for oid in oids])
for oid in oids: for oid in oids:

View File

@ -35,7 +35,7 @@ from app import log
@enum.unique @enum.unique
class CodesParcours(enum.IntEnum): class CodesParcours(enum.IntEnum):
"""Codes numériques de sparcours, enregistrés en base """Codes numériques des parcours, enregistrés en base
dans notes_formations.type_parcours dans notes_formations.type_parcours
Ne pas modifier. Ne pas modifier.
""" """
@ -68,7 +68,8 @@ NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok
# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999) # (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
# Barre sur moyenne générale utilisée pour compensations semestres: # Barre sur moyenne générale utilisée pour compensations semestres:
NOTES_BARRE_GEN_COMPENSATION = 10.0 - NOTES_TOLERANCE NOTES_BARRE_GEN = 10.0
NOTES_BARRE_GEN_COMPENSATION = NOTES_BARRE_GEN - NOTES_TOLERANCE
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# Types d'UE: # Types d'UE:
@ -114,6 +115,8 @@ UE_SEM_DEFAULT = 1000000 # indice semestre des UE sans modules
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Codes proposés par ADIUT / Apogee # Codes proposés par ADIUT / Apogee
ABAN = "ABAN"
ABL = "ABL"
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10) ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
ADJ = "ADJ" # admis par le jury ADJ = "ADJ" # admis par le jury
@ -122,10 +125,16 @@ ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant
ATB = "ATB" ATB = "ATB"
AJ = "AJ" AJ = "AJ"
CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis) CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis)
NAR = "NAR"
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée
DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission) DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission)
DEM = "DEM" DEM = "DEM"
EXCLU = "EXCLU"
JSD = "JSD" # jury tenu mais pas de code (Jury Sans Décision)
NAR = "NAR"
PASD = "PASD"
PAS1NCI = "PAS1NCI"
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée
RED = "RED"
UEBSL = "UEBSL" # UE blanchie
# codes actions # codes actions
REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1) REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1)
@ -143,22 +152,34 @@ ALL = "ALL"
# Explication des codes (de semestre ou d'UE) # Explication des codes (de semestre ou d'UE)
CODES_EXPL = { CODES_EXPL = {
ABAN: "Non évalué pour manque dassiduité: non présentation des notes de l'étudiant au jury",
ABL: "Année blanche",
ADC: "Validé par compensation", ADC: "Validé par compensation",
ADJ: "Validé par le Jury", ADJ: "Validé par le Jury",
ADM: "Validé", ADM: "Validé",
AJ: "Ajourné", AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)", ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)", ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)", ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
CMP: "Code UE acquise car semestre acquis", CMP: """Code UE acquise car semestre acquis, ou, en BUT, acquise par
DEF: "Défaillant", compensation UE avec lUE de même compétence et de même année (ECTS acquis).
NAR: "Échec, non autorisé à redoubler", Utilisé aussi pour les blocs de compétences BUT (RCUE).
RAT: "En attente d'un rattrapage", """,
DEF: "Défaillant, pas ou peu de notes par arrêt de la formation. Non évalué par manque assiduité.",
DEM: "Démission", DEM: "Démission",
EXCLU: "Exclusion: décision réservée à des décisions disciplinaires",
NAR: "Non admis, réorientation, non autorisé à redoubler",
PASD: """Année BUT: non admis, mais passage de droit:
Passage en Année Supérieure de Droit (+ de 50% des UE VAL et RCUE Ajourné(s) >=8)
""",
PAS1NCI: """Année BUT: Non admis, mais passage par décision de jury:
Passage en Année Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8)
""",
RAT: "En attente d'un rattrapage",
RED: "Année: Ajourné, mais autorisé à redoubler",
UEBSL: "UE blanchie",
} }
# Nota: ces explications sont personnalisables via le fichier
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py
# variable: CONFIG.CODES_EXP
# Les codes de semestres: # Les codes de semestres:
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
@ -167,7 +188,20 @@ CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
CODES_SEM_REO = {NAR: 1} # reorientation CODES_SEM_REO = {NAR: 1} # reorientation
CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée
# Pour le BUT:
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
CODES_RCUE = {ADM, AJ, CMP}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
BUT_CODES_PASSAGE = {
ADM,
ADJ,
PASD,
PAS1NCI,
}
def code_semestre_validant(code: str) -> bool: def code_semestre_validant(code: str) -> bool:

View File

@ -76,7 +76,7 @@ def html_edit_formation_apc(
ues_by_sem[semestre_idx] = formation.ues.filter_by( ues_by_sem[semestre_idx] = formation.ues.filter_by(
semestre_idx=semestre_idx semestre_idx=semestre_idx
).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme) ).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
ects = [ue.ects for ue in ues_by_sem[semestre_idx]] ects = [ue.ects for ue in ues_by_sem[semestre_idx] if ue.type != UE_SPORT]
if None in ects: if None in ects:
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>' ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
else: else:
@ -127,27 +127,33 @@ def html_edit_formation_apc(
formation=formation, formation=formation,
titre=f"Ressources du S{semestre_idx}", titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource", create_element_msg="créer une nouvelle ressource",
matiere_parent=matiere_parent, # matiere_parent=matiere_parent,
modules=ressources_in_sem, modules=ressources_in_sem,
module_type=ModuleType.RESSOURCE, module_type=ModuleType.RESSOURCE,
editable=editable, editable=editable,
tag_editable=tag_editable, tag_editable=tag_editable,
icons=icons, icons=icons,
scu=scu, scu=scu,
), semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.html",
formation=formation, formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}", titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ", create_element_msg="créer une nouvelle SAÉ",
matiere_parent=matiere_parent, # matiere_parent=matiere_parent,
modules=saes_in_sem, modules=saes_in_sem,
module_type=ModuleType.SAE, module_type=ModuleType.SAE,
editable=editable, editable=editable,
tag_editable=tag_editable, tag_editable=tag_editable,
icons=icons, icons=icons,
scu=scu, scu=scu,
), semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.html",
formation=formation, formation=formation,
@ -159,7 +165,10 @@ def html_edit_formation_apc(
tag_editable=tag_editable, tag_editable=tag_editable,
icons=icons, icons=icons,
scu=scu, scu=scu,
), semestre_id=semestre_idx,
)
if ues_by_sem[semestre_idx].count() > 0
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>""",
] ]
return "\n".join(H) return "\n".join(H)

View File

@ -245,7 +245,11 @@ def formation_edit(formation_id=None, create=False):
return ( return (
"\n".join(H) "\n".join(H)
+ tf_error_message( + tf_error_message(
"Valeurs incorrectes: il existe déjà une formation avec même titre, acronyme et version." f"""Valeurs incorrectes: il existe déjà <a href="{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=others[0]["id"])
}">une formation</a> avec même titre,
acronyme et version.
"""
) )
+ tf[1] + tf[1]
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()

View File

@ -29,7 +29,7 @@
(portage from DTML) (portage from DTML)
""" """
import flask import flask
from flask import url_for, render_template from flask import flash, url_for, render_template
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
@ -39,6 +39,7 @@ from app.models import APO_CODE_STR_LEN
from app.models import Formation, Matiere, Module, UniteEns from app.models import Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl from app.models import FormSemestre, ModuleImpl
from app.models import ScolarNews from app.models import ScolarNews
from app.models.but_refcomp import ApcAppCritique, ApcParcours
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -121,234 +122,12 @@ def module_create(
Sinon, donne le choix de l'UE de rattachement et utilise la première Sinon, donne le choix de l'UE de rattachement et utilise la première
matière de cette UE (si elle n'existe pas, la crée). matière de cette UE (si elle n'existe pas, la crée).
""" """
if matiere_id: return module_edit(
matiere = Matiere.query.get_or_404(matiere_id) create=True,
ue = matiere.ue matiere_id=matiere_id,
formation = ue.formation module_type=module_type,
else:
formation = Formation.query.get_or_404(formation_id)
parcours = formation.get_parcours()
is_apc = parcours.APC_SAE
ues = formation.ues.order_by(
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
).all()
# cherche le numero adéquat (pour placer le module en fin de liste)
modules = formation.modules.all()
if modules:
default_num = max([m.numero or 0 for m in modules]) + 10
else:
default_num = 10
if is_apc and module_type is not None:
object_name = scu.MODULE_TYPE_NAMES[module_type]
else:
object_name = "Module"
H = [
html_sco_header.sco_header(page_title=f"Création {object_name}"),
]
if not matiere_id:
H += [
f"""<h2>Création {object_name} dans la formation {formation.acronyme}
</h2>
"""
]
else:
H += [
f"""<h2>Création {object_name} dans la matière {matiere.titre},
(UE {ue.acronyme}), semestre {ue.semestre_idx}</h2>
"""
]
H += [
render_template(
"scodoc/help/modules.html",
is_apc=is_apc,
semestre_id=semestre_id, semestre_id=semestre_id,
) formation_id=formation_id,
]
descr = [
(
"code",
{
"size": 10,
"explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.",
"allow_null": False,
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id
),
},
),
(
"titre",
{
"size": 30,
"explanation": "nom du module. Exemple: <em>Introduction à la démarche ergonomique</em>",
},
),
(
"abbrev",
{
"size": 20,
"explanation": "nom abrégé (pour les bulletins). Exemple: <em>Intro. à l'ergonomie</em>",
},
),
]
if is_apc:
module_types = scu.ModuleType # tous les types
else:
# ne propose pas SAE et Ressources:
module_types = set(scu.ModuleType) - {
scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE,
}
descr += [
(
"module_type",
{
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in module_types],
"allowed_values": [str(int(x)) for x in module_types],
},
),
(
"heures_cours",
{
"title": "Heures de cours",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de cours (optionnel)",
},
),
(
"heures_td",
{
"title": "Heures de TD",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Dirigés (optionnel)",
},
),
(
"heures_tp",
{
"title": "Heures de TP",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Pratiques (optionnel)",
},
),
]
if is_apc:
descr += [
(
"sep_ue_coefs",
{
"input_type": "separator",
"title": """
<div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
</div>""",
},
),
]
else:
descr += [
(
"coefficient",
{
"size": 4,
"type": "float",
"explanation": "coefficient dans la formation (PPN)",
"allow_null": False,
},
),
]
if matiere_id:
descr += [
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere_id, "input_type": "hidden"}),
]
else:
# choix de l'UE de rattachement
descr += [
(
"ue_id",
{
"input_type": "menu",
"type": "int",
"title": "UE de rattachement",
"explanation": "utilisée notamment pour les malus",
"labels": [
f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}"
for u in ues
],
"allowed_values": [u.id for u in ues],
},
),
]
descr += [
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
("formation_id", {"default": formation.id, "input_type": "hidden"}),
(
"code_apogee",
{
"title": "Code Apogée",
"size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
},
),
(
"numero",
{
"size": 2,
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage",
"type": "int",
"default": default_num,
},
),
]
args = scu.get_request_args()
tf = TrivialFormulator(
request.base_url,
args,
descr,
submitlabel="Créer ce module",
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
else:
if not matiere_id:
# formulaire avec choix UE de rattachement
ue = UniteEns.query.get(tf[2]["ue_id"])
if ue is None:
raise ValueError("UE invalide")
matiere = ue.matieres.first()
if matiere:
tf[2]["matiere_id"] = matiere.id
else:
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue.id, "titre": ue.titre, "numero": 1},
)
tf[2]["matiere_id"] = matiere_id
tf[2]["semestre_id"] = ue.semestre_idx
_ = do_module_create(tf[2])
return flask.redirect(
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=formation.id,
semestre_idx=tf[2]["semestre_id"],
)
) )
@ -359,7 +138,6 @@ def can_delete_module(module):
def do_module_delete(oid): def do_module_delete(oid):
"delete module" "delete module"
module = Module.query.get_or_404(oid) module = Module.query.get_or_404(oid)
mod = module_list({"module_id": oid})[0] # sco7 mod = module_list({"module_id": oid})[0] # sco7
if module_is_locked(module.id): if module_is_locked(module.id):
@ -379,9 +157,14 @@ def do_module_delete(oid):
# S'il y a des moduleimpls, on ne peut pas detruire le module ! # S'il y a des moduleimpls, on ne peut pas detruire le module !
mods = sco_moduleimpl.moduleimpl_list(module_id=oid) mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
if mods: if mods:
err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3> err_page = f"""
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de <h3>Destruction du module impossible car il est utilisé dans des
laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place. semestres existants !</h3>
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer
ce module).
Mais il est peut être préférable de laisser ce programme intact et
d'en créer une nouvelle version pour la modifier sans affecter
les semestres déjà en place.
</p> </p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a> formation_id=mod["formation_id"])}">reprendre</a>
@ -465,37 +248,65 @@ def do_module_edit(vals: dict) -> None:
def check_module_code_unicity(code, field, formation_id, module_id=None): def check_module_code_unicity(code, field, formation_id, module_id=None):
"true si code module unique dans la formation" "true si code module unique dans la formation"
Mods = module_list(args={"code": code, "formation_id": formation_id}) modules = module_list(args={"code": code, "formation_id": formation_id})
if module_id: # edition: supprime le module en cours if module_id: # edition: supprime le module en cours
Mods = [m for m in Mods if m["module_id"] != module_id] modules = [m for m in modules if m["module_id"] != module_id]
return len(Mods) == 0 return len(modules) == 0
def module_edit(module_id=None): def module_edit(
"""Edit a module""" module_id=None,
from app.scodoc import sco_formations create=False,
matiere_id=None,
module_type=None,
semestre_id=None,
formation_id=None,
):
"""Formulaire édition ou création module.
Si create, création nouveau module.
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
Sinon, donne le choix de l'UE de rattachement et utilise la première matière
de cette UE (si elle n'existe pas, la crée).
"""
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
# --- Détermination de la formation
orig_semestre_idx = semestre_id
ue = None
if create:
if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id)
ue = matiere.ue
formation = ue.formation
orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id
else:
formation = Formation.query.get_or_404(formation_id)
module = None
unlocked = True
else:
if not module_id: if not module_id:
raise ScoValueError("invalid module !") raise ValueError("missing module_id !")
modules = module_list(args={"module_id": module_id}) module = models.Module.query.get_or_404(module_id)
if not modules: ue = module.ue
raise ScoValueError("invalid module !") module_dict = module.to_dict()
module = modules[0] formation = module.formation
a_module = models.Module.query.get(module_id)
unlocked = not module_is_locked(module_id) unlocked = not module_is_locked(module_id)
formation_id = module["formation_id"]
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] parcours = sco_codes_parcours.get_parcours_from_code(formation.type_parcours)
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
is_apc = parcours.APC_SAE # BUT is_apc = parcours.APC_SAE # BUT
in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls if not create:
orig_semestre_idx = module.ue.semestre_idx if is_apc else module.semestre_id
if orig_semestre_idx is None:
orig_semestre_idx = 1
# il y a-t-il des modimpls ?
in_use = (module is not None) and (len(module.modimpls.all()) > 0)
matieres = Matiere.query.filter( matieres = Matiere.query.filter(
Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation_id Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation.id
).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero) ).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero)
if in_use: if in_use:
# restreint aux matières du même semestre # restreint aux matières du même semestre
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx) matieres = matieres.filter(UniteEns.semestre_idx == module.ue.semestre_idx)
if is_apc: if is_apc:
# ne conserve que la 1ere matière de chaque UE, # ne conserve que la 1ere matière de chaque UE,
@ -503,7 +314,8 @@ def module_edit(module_id=None):
matieres = [ matieres = [
mat mat
for mat in matieres for mat in matieres
if a_module.matiere.id == mat.id or mat.id == mat.ue.matieres.first().id if ((module is not None) and (module.matiere.id == mat.id))
or (mat.id == mat.ue.matieres.first().id)
] ]
mat_names = [ mat_names = [
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres "S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
@ -511,64 +323,131 @@ def module_edit(module_id=None):
else: else:
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
if module: # edition
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) module_dict["ue_matiere_id"] = "%s!%s" % (
module_dict["ue_id"],
module_dict["matiere_id"],
)
semestres_indices = list(range(1, parcours.NB_SEM + 1)) semestres_indices = list(range(1, parcours.NB_SEM + 1))
# Toutes les UEs de la formation (tout parcours):
ues = formation.ues.order_by(
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
).all()
# L'UE de rattachement par défaut: 1ere du semestre
ue_default = (
formation.ues.filter_by(semestre_idx=orig_semestre_idx)
.order_by(UniteEns.numero, UniteEns.acronyme)
.first()
)
# --- Titre de la page
if create:
if is_apc and module_type is not None:
object_name = scu.MODULE_TYPE_NAMES[module_type]
else:
object_name = "Module"
page_title = f"Création {object_name}"
if matiere_id:
title = f"""Création {object_name} dans la matière
{matiere.titre},
(UE {ue.acronyme}), semestre {ue.semestre_idx}
"""
else:
title = f"""Création {object_name} dans la formation
{formation.acronyme}"""
else:
page_title = f"Modification du module {module.code or module.titre or ''}"
title = f"""Modification du module {module.code or ''} {module.titre or ''}
(formation {formation.acronyme}, version {formation.version})
"""
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title=f"Modification du module {a_module.code or a_module.titre or ''}", page_title=page_title,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"], cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
javascripts=[ javascripts=[
"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",
"js/module_edit.js",
], ],
), ),
f"""<h2>Modification du module {a_module.code or ''} {a_module.titre or ''}""", f"""<h2>{title}</h2>""",
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
render_template( render_template(
"scodoc/help/modules.html", "scodoc/help/modules.html",
is_apc=is_apc, is_apc=is_apc,
semestre_id=semestre_id,
formsemestres=FormSemestre.query.filter( formsemestres=FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id, ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id, ModuleImpl.module_id == module_id,
) )
.order_by(FormSemestre.date_debut) .order_by(FormSemestre.date_debut)
.all(), .all()
if not create
else None,
), ),
] ]
if not unlocked: if not unlocked:
H.append( H.append(
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>""" """<div class="ue_warning"><span>Formation verrouillée, seuls
certains éléments peuvent être modifiés</span></div>"""
) )
if is_apc: if is_apc:
module_types = scu.ModuleType # tous les types module_types = scu.ModuleType # tous les types
else: else:
# ne propose pas SAE et Ressources, sauf si déjà de ce type... # ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = ( module_types = set(scu.ModuleType) - {
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} scu.ModuleType.RESSOURCE,
) | { scu.ModuleType.SAE,
scu.ModuleType(a_module.module_type) }
if a_module.module_type if module:
module_types |= {
scu.ModuleType(module.module_type)
if module.module_type
else scu.ModuleType.STANDARD else scu.ModuleType.STANDARD
} }
# Numéro du module
# cherche le numero adéquat (pour placer le module en fin de liste)
if module:
default_num = module.numero
else:
modules = formation.modules.all()
if modules:
default_num = max([m.numero or 0 for m in modules]) + 10
else:
default_num = 10
descr = [ descr = [
( (
"code", "code",
{ {
"size": 10, "size": 10,
"explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)", "explanation": """code du module (issu du programme, exemple M1203,
R2.01, ou SAÉ 3.4. Doit être unique dans la formation)""",
"allow_null": False, "allow_null": False,
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( "validator": lambda val, field, formation_id=formation.id: check_module_code_unicity(
val, field, formation_id, module_id=module_id val, field, formation_id, module_id=module.id if module else None
), ),
}, },
), ),
("titre", {"size": 30, "explanation": "nom du module"}), (
("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}), "titre",
{
"size": 30,
"explanation": """nom du module. Exemple:
<em>Introduction à la démarche ergonomique</em>""",
},
),
(
"abbrev",
{
"size": 20,
"explanation": """nom abrégé (pour bulletins).
Exemple: <em>Intro. à l'ergonomie</em>""",
},
),
( (
"module_type", "module_type",
{ {
@ -583,33 +462,34 @@ def module_edit(module_id=None):
( (
"heures_cours", "heures_cours",
{ {
"title": "Heures CM :", "title": "Heures cours :",
"size": 4, "size": 4,
"type": "float", "type": "float",
"explanation": "nombre d'heures de cours", "explanation": "nombre d'heures de cours (optionnel)",
}, },
), ),
( (
"heures_td", "heures_td",
{ {
"title": "Heures TD :", "title": "Heures de TD :",
"size": 4, "size": 4,
"type": "float", "type": "float",
"explanation": "nombre d'heures de Travaux Dirigés", "explanation": "nombre d'heures de Travaux Dirigés (optionnel)",
}, },
), ),
( (
"heures_tp", "heures_tp",
{ {
"title": "Heures TP :", "title": "Heures de TP :",
"size": 4, "size": 4,
"type": "float", "type": "float",
"explanation": "nombre d'heures de Travaux Pratiques", "explanation": "nombre d'heures de Travaux Pratiques (optionnel)",
}, },
), ),
] ]
if is_apc: if is_apc:
coefs_lst = a_module.ue_coefs_list() if module:
coefs_lst = module.ue_coefs_list()
if coefs_lst: if coefs_lst:
coefs_descr_txt = ", ".join( coefs_descr_txt = ", ".join(
[f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst] [f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst]
@ -623,10 +503,23 @@ def module_edit(module_id=None):
"readonly": True, "readonly": True,
"title": "Coefficients vers les UE ", "title": "Coefficients vers les UE ",
"default": coefs_descr_txt, "default": coefs_descr_txt,
"explanation": " <br>(passer par la page d'édition de la formation pour modifier les coefficients)", "explanation": """ <br>(passer par la page d'édition de la
formation pour modifier les coefficients)""",
}, },
) )
] ]
else:
descr += [
(
"sep_ue_coefs",
{
"input_type": "separator",
"title": """
<div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
</div>""",
},
),
]
else: # Module classique avec coef scalaire: else: # Module classique avec coef scalaire:
descr += [ descr += [
( (
@ -641,7 +534,23 @@ def module_edit(module_id=None):
), ),
] ]
descr += [ descr += [
("formation_id", {"input_type": "hidden"}), (
"formation_id",
{
"input_type": "hidden",
"default": formation.id,
},
),
(
"semestre_id",
{
"input_type": "hidden",
"default": orig_semestre_idx,
},
),
]
if module:
descr += [
("ue_id", {"input_type": "hidden"}), ("ue_id", {"input_type": "hidden"}),
("module_id", {"input_type": "hidden"}), ("module_id", {"input_type": "hidden"}),
( (
@ -650,9 +559,9 @@ def module_edit(module_id=None):
"input_type": "menu", "input_type": "menu",
"title": "Rattachement :" if is_apc else "Matière :", "title": "Rattachement :" if is_apc else "Matière :",
"explanation": ( "explanation": (
"UE de rattachement" "UE de rattachement, utilisée notamment pour les malus"
+ ( + (
" module utilisé, ne peut pas être changé de semestre" " (module utilisé, ne peut pas être changé de semestre)"
if in_use if in_use
else "" else ""
) )
@ -665,6 +574,32 @@ def module_edit(module_id=None):
}, },
), ),
] ]
else: # Création
if matiere_id:
descr += [
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere_id, "input_type": "hidden"}),
]
else:
# choix de l'UE de rattachement
descr += [
(
"ue_id",
{
"input_type": "menu",
"type": "int",
"title": "UE de rattachement",
"explanation": "utilisée notamment pour les malus",
"labels": [
f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}"
for u in ues
],
"allowed_values": [u.id for u in ues],
"default": ue_default.id if ue_default is not None else "",
},
),
]
if is_apc: if is_apc:
# le semestre du module est toujours celui de son UE # le semestre du module est toujours celui de son UE
descr += [ descr += [
@ -685,8 +620,7 @@ def module_edit(module_id=None):
"input_type": "menu", "input_type": "menu",
"type": "int", "type": "int",
"title": parcours.SESSION_NAME.capitalize(), "title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s de début du module dans la formation standard" "explanation": f"{parcours.SESSION_NAME} de début du module dans la formation standard",
% parcours.SESSION_NAME,
"labels": [str(x) for x in semestres_indices], "labels": [str(x) for x in semestres_indices],
"allowed_values": semestres_indices, "allowed_values": semestres_indices,
"enabled": unlocked, "enabled": unlocked,
@ -710,26 +644,123 @@ def module_edit(module_id=None):
"numero", "numero",
{ {
"size": 2, "size": 2,
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage", "explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
"type": "int", "type": "int",
"default": default_num,
}, },
), ),
] ]
if is_apc:
# Choix des parcours
ref_comp = formation.referentiel_competence
if ref_comp:
descr += [
(
"parcours",
{
"input_type": "checkbox",
"vertical": True,
"dom_id": "tf_module_parcours",
"labels": [parcour.libelle for parcour in ref_comp.parcours]
+ ["Tous (tronc commun)"],
"allowed_values": [
str(parcour.id) for parcour in ref_comp.parcours
]
+ ["-1"],
"explanation": """Parcours dans lesquels est utilisé ce module.<br>
Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours,
il faut en créer plusieurs versions, car dans ScoDoc chaque module a ses coefficients.""",
},
)
]
if module:
module_dict["parcours"] = [
str(parcour.id) for parcour in module.parcours
]
module_dict["app_critiques"] = [
str(app_crit.id) for app_crit in module.app_critiques
]
# Choix des Apprentissages Critiques
if ue is not None:
annee = f"BUT{orig_semestre_idx//2 + 1}"
app_critiques = ApcAppCritique.app_critiques_ref_comp(ref_comp, annee)
descr += (
[
(
"app_critiques",
{
"title": "Apprentissages Critiques",
"input_type": "checkbox",
"vertical": True,
"dom_id": "tf_module_app_critiques",
"labels": [
f"{app_crit.code}&nbsp; {app_crit.libelle}"
for app_crit in app_critiques
],
"allowed_values": [
str(app_crit.id) for app_crit in app_critiques
],
"html_data": [],
"explanation": """Apprentissages Critiques liés à ce module.
(si vous changez le semestre, revenez ensuite sur cette page
pour associer les AC.)
""",
},
)
]
if (ue.niveau_competence is not None)
else [
(
"app_critiques",
{
"input_type": "separator",
"title": f"""<span class="fontred">{scu.EMO_WARNING }
L'UE <a class="stdlink" href="{
url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}">{ue.acronyme} {ue.titre}</a>
n'est pas associée à un niveau de compétences
</span>""",
},
)
]
)
else:
descr += [
(
"parcours",
{
"input_type": "separator",
"title": f"""<span class="fontred">{scu.EMO_WARNING }
Pas de parcours:
<a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}">associer un référentiel de compétence</a>
</span>""",
},
)
]
# force module semestre_idx to its UE # force module semestre_idx to its UE
if a_module.ue.semestre_idx: if module:
module["semestre_id"] = a_module.ue.semestre_idx if module.ue.semestre_idx is None:
# Filet de sécurité si jamais l'UE n'a pas non plus de semestre: # Filet de sécurité si jamais l'UE n'a pas non plus de semestre:
if not module["semestre_id"]: module_dict["semestre_id"] = 1
module["semestre_id"] = 1 else:
module_dict["semestre_id"] = module.ue.semestre_idx
tf = TrivialFormulator( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
descr, descr,
html_foot_markup="""<div style="width: 90%;"><span class="sco_tag_edit"><textarea data-module_id="{}" class="module_tag_editor">{}</textarea></span></div>""".format( html_foot_markup=f"""<div class="sco_tag_module_edit"><span
module_id, ",".join(sco_tag_module.module_tag_list(module_id)) class="sco_tag_edit"><textarea data-module_id="{module_id}" class="module_tag_editor"
), >{','.join(sco_tag_module.module_tag_list(module_id))}</textarea></span></div>
initvalues=module, """
submitlabel="Modifier ce module", if not create
else "",
initvalues=module_dict if module else {},
submitlabel="Modifier ce module" if module else "Créer ce module",
cancelbutton="Annuler",
) )
# #
if tf[0] == 0: if tf[0] == 0:
@ -739,21 +770,40 @@ def module_edit(module_id=None):
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation.id,
semestre_idx=module["semestre_id"], semestre_idx=orig_semestre_idx,
) )
) )
else: else:
if create:
if not matiere_id:
# formulaire avec choix UE de rattachement
ue = UniteEns.query.get(tf[2]["ue_id"])
if ue is None:
raise ValueError("UE invalide")
matiere = ue.matieres.first()
if matiere:
tf[2]["matiere_id"] = matiere.id
else:
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue.id, "titre": ue.titre, "numero": 1},
)
tf[2]["matiere_id"] = matiere_id
tf[2]["semestre_id"] = ue.semestre_idx
module_id = do_module_create(tf[2])
module = Module.query.get(module_id)
else: # EDITION MODULE
# l'UE de rattachement peut changer # l'UE de rattachement peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
x, y = tf[2]["ue_matiere_id"].split("!") x, y = tf[2]["ue_matiere_id"].split("!")
tf[2]["ue_id"] = int(x) tf[2]["ue_id"] = int(x)
tf[2]["matiere_id"] = int(y) tf[2]["matiere_id"] = int(y)
old_ue_id = a_module.ue.id old_ue_id = module.ue.id
new_ue_id = tf[2]["ue_id"] new_ue_id = tf[2]["ue_id"]
if (old_ue_id != new_ue_id) and in_use: if (old_ue_id != new_ue_id) and in_use:
new_ue = UniteEns.query.get_or_404(new_ue_id) new_ue = UniteEns.query.get_or_404(new_ue_id)
if new_ue.semestre_idx != a_module.ue.semestre_idx: if new_ue.semestre_idx != module.ue.semestre_idx:
# pas changer de semestre un module utilisé ! # pas changer de semestre un module utilisé !
raise ScoValueError( raise ScoValueError(
"Module utilisé: il ne peut pas être changé de semestre !" "Module utilisé: il ne peut pas être changé de semestre !"
@ -765,13 +815,32 @@ def module_edit(module_id=None):
raise ValueError("UE invalide") raise ValueError("UE invalide")
tf[2]["semestre_id"] = selected_ue.semestre_idx tf[2]["semestre_id"] = selected_ue.semestre_idx
# Check unicité code module dans la formation # Check unicité code module dans la formation
# ??? TODO
#
do_module_edit(tf[2]) do_module_edit(tf[2])
# Modifie les parcours
if ("parcours" in tf[2]) and formation.referentiel_competence:
if "-1" in tf[2]["parcours"]: # "tous"
module.parcours = formation.referentiel_competence.parcours.all()
else:
module.parcours = [
ApcParcours.query.get(int(parcour_id_str))
for parcour_id_str in tf[2]["parcours"]
]
# Modifie les AC
if "app_critiques" in tf[2]:
module.app_critiques = [
ApcAppCritique.query.get(int(ac_id_str))
for ac_id_str in tf[2]["app_critiques"]
]
db.session.add(module)
db.session.commit()
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation.id,
semestre_idx=tf[2]["semestre_id"], semestre_idx=tf[2]["semestre_id"] if is_apc else 1,
) )
) )
@ -846,13 +915,23 @@ def module_count_moduleimpls(module_id):
return len(mods) return len(mods)
def formation_add_malus_modules(formation_id, titre=None, redirect=True): def formation_add_malus_modules(
formation_id: int, semestre_id: int = None, titre=None, redirect=True
):
"""Création d'un module de "malus" dans chaque UE d'une formation""" """Création d'un module de "malus" dans chaque UE d'une formation"""
formation = Formation.query.get_or_404(formation_id) formation = Formation.query.get_or_404(formation_id)
for ue in formation.ues: nb = 0
ue_add_malus_module(ue, titre=titre) ues = formation.ues
if semestre_id is not None:
ues = ues.filter_by(semestre_idx=semestre_id)
for ue in ues:
if ue.type == sco_codes_parcours.UE_STANDARD:
if ue_add_malus_module(ue, titre=titre) != None:
nb += 1
flash(f"Modules de malus ajoutés dans {nb} UEs du S{semestre_id}")
formation.invalidate_cached_sems() formation.invalidate_cached_sems()
@ -871,9 +950,9 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int:
""" """
modules_malus = [m for m in ue.modules if m.module_type == scu.ModuleType.MALUS] modules_malus = [m for m in ue.modules if m.module_type == scu.ModuleType.MALUS]
if len(modules_malus) > 0: if len(modules_malus) > 0:
return modules_malus[0].id # déjà existant return None # déjà existant
titre = titre or "" titre = titre or f"Malus {ue.acronyme}"
code = code or f"MALUS{ue.numero}" code = code or f"MALUS{ue.numero}"
# Tout module doit avoir un semestre_id (indice 1, 2, ...) # Tout module doit avoir un semestre_id (indice 1, 2, ...)
@ -885,7 +964,7 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int:
# c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
# le semestre ? ou affecter le malus au semestre 1 ??? # le semestre ? ou affecter le malus au semestre 1 ???
raise ScoValueError( raise ScoValueError(
"Impossible d'ajouter un malus s'il n'y a pas d'autres modules" "Impossible d'ajouter un malus si l'UE n'a pas de numéro de semestre et ne comporte pas d'autres modules"
) )
else: else:
semestre_id = ue.semestre_idx semestre_id = ue.semestre_idx
@ -899,7 +978,7 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int:
matiere = matieres_malus[0] matiere = matieres_malus[0]
else: else:
if ue.matieres.count() > 0: if ue.matieres.count() > 0:
numero = max([mat.numero for mat in ue.matieres]) + 10 numero = max([(mat.numero or 0) for mat in ue.matieres]) + 10
else: else:
numero = 0 numero = 0
matiere = Matiere(ue_id=ue.id, titre=titre_matiere_malus, numero=numero) matiere = Matiere(ue_id=ue.id, titre=titre_matiere_malus, numero=numero)

View File

@ -35,6 +35,7 @@ from flask_login import current_user
from app import db from app import db
from app import log from app import log
from app.but import apc_edit_ue
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import Formation, UniteEns, ModuleImpl, Module from app.models import Formation, UniteEns, ModuleImpl, Module
from app.models import ScolarNews from app.models import ScolarNews
@ -77,6 +78,7 @@ _ueEditor = ndb.EditableTable(
"is_external", "is_external",
"code_apogee", "code_apogee",
"coefficient", "coefficient",
"coef_rcue",
"color", "color",
), ),
sortkey="numero", sortkey="numero",
@ -121,12 +123,7 @@ def do_ue_create(args):
# create # create
ue_id = _ueEditor.create(cnx, args) ue_id = _ueEditor.create(cnx, args)
# Invalidate cache: vire les poids de toutes les évals de la formation formation: Formation = Formation.query.get(args["formation_id"])
for modimpl in ModuleImpl.query.filter(
ModuleImpl.module_id == Module.id, Module.formation_id == args["formation_id"]
):
modimpl.invalidate_evaluations_poids()
formation = Formation.query.get(args["formation_id"])
formation.invalidate_module_coefs() formation.invalidate_module_coefs()
# news # news
ue = UniteEns.query.get(ue_id) ue = UniteEns.query.get(ue_id)
@ -144,11 +141,10 @@ 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_formations
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
ue = UniteEns.query.get_or_404(ue_id) ue = UniteEns.query.get_or_404(ue_id)
formation_id = ue.formation_id formation = ue.formation
semestre_idx = ue.semestre_idx semestre_idx = ue.semestre_idx
if not ue.can_be_deleted(): if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject( raise ScoNonEmptyFormationObject(
@ -157,7 +153,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
dest_url=url_for( dest_url=url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation.id,
semestre_idx=semestre_idx, semestre_idx=semestre_idx,
), ),
) )
@ -181,7 +177,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
cancel_url=url_for( cancel_url=url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation.id,
semestre_idx=semestre_idx, semestre_idx=semestre_idx,
), ),
parameters={"ue_id": ue.id, "dialog_confirmed": 1}, parameters={"ue_id": ue.id, "dialog_confirmed": 1},
@ -207,13 +203,13 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
_ueEditor.delete(cnx, ue.id) _ueEditor.delete(cnx, ue.id)
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
# utilisé: acceptable de tout invalider): # utilisé: acceptable de tout invalider):
sco_cache.invalidate_formsemestre() formation.invalidate_module_coefs()
# -> invalide aussi .invalidate_formsemestre()
# news # news
F = sco_formations.formation_list(args={"formation_id": formation_id})[0]
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=formation_id, obj=formation.id,
text=f"Modification de la formation {F['acronyme']}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=10 * 60, max_frequency=10 * 60,
) )
# #
@ -222,7 +218,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation.id,
semestre_idx=semestre_idx, semestre_idx=semestre_idx,
) )
) )
@ -248,13 +244,16 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
title = f"Modification de l'UE {ue.acronyme} {ue.titre}" title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
initvalues = ue_dict initvalues = ue_dict
submitlabel = "Modifier les valeurs" submitlabel = "Modifier les valeurs"
can_change_semestre_id = (ue.modules.count() == 0) or (ue.semestre_idx is None) can_change_semestre_id = (
(ue.modules.count() == 0) or (ue.semestre_idx is None)
) and ue.niveau_competence is None
else: else:
ue = None ue = None
title = "Création d'une UE" title = "Création d'une UE"
initvalues = { initvalues = {
"semestre_idx": default_semestre_idx, "semestre_idx": default_semestre_idx,
"color": ue_guess_color_default(formation_id, default_semestre_idx), "color": ue_guess_color_default(formation_id, default_semestre_idx),
"coef_rcue": 1.0,
} }
submitlabel = "Créer cette UE" submitlabel = "Créer cette UE"
can_change_semestre_id = True can_change_semestre_id = True
@ -277,6 +276,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé. <p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
Seuls les <em>modules</em> ont des coefficients. Seuls les <em>modules</em> ont des coefficients.
</p>""", </p>""",
f"""
<h4>UE du semestre S{ue.semestre_idx}</h4>
"""
if is_apc and ue
else "",
] ]
ue_types = parcours.ALLOWED_UE_TYPES ue_types = parcours.ALLOWED_UE_TYPES
@ -308,8 +312,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "int", "type": "int",
"allow_null": False, "allow_null": False,
"title": parcours.SESSION_NAME.capitalize(), "title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s de l'UE dans la formation" "explanation": f"{parcours.SESSION_NAME} de l'UE dans la formation",
% parcours.SESSION_NAME,
"labels": ["non spécifié"] + [str(x) for x in semestres_indices], "labels": ["non spécifié"] + [str(x) for x in semestres_indices],
"allowed_values": [""] + semestres_indices, "allowed_values": [""] + semestres_indices,
}, },
@ -339,6 +342,25 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"allow_null": not is_apc, # ects requis en APC "allow_null": not is_apc, # ects requis en APC
}, },
), ),
]
if is_apc: # coef pour la moyenne RCUE
form_descr.append(
(
"coef_rcue",
{
"size": 4,
"type": "float",
"title": "Coef. RCUE",
"explanation": """pondération utilisée pour le calcul de la moyenne du RCUE. Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations.
""",
"defaut": 1.0,
"allow_null": False,
"enabled": is_apc,
},
)
)
else: # non APC, coef d'UE
form_descr.append(
( (
"coefficient", "coefficient",
{ {
@ -354,7 +376,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
""", """,
"enabled": not is_apc, "enabled": not is_apc,
}, },
), )
)
form_descr += [
( (
"ue_code", "ue_code",
{ {
@ -410,8 +434,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
form_descr, form_descr,
initvalues=initvalues, initvalues=initvalues,
submitlabel=submitlabel, submitlabel=submitlabel,
cancelbutton="Revenir à la formation",
) )
if tf[0] == 0: if tf[0] == 0:
niveau_competence_div = ""
if ue and is_apc:
niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(formation, ue)
if ue and ue.modules.count() and ue.semestre_idx is not None: if ue and ue.modules.count() and ue.semestre_idx is not None:
modules_div = f"""<div id="ue_list_modules"> modules_div = f"""<div id="ue_list_modules">
<div><b>{ue.modules.count()} modules sont rattachés <div><b>{ue.modules.count()} modules sont rattachés
@ -429,12 +457,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
return ( return (
"\n".join(H) "\n".join(H)
+ tf[1] + tf[1]
+ niveau_competence_div
+ modules_div + modules_div
+ bonus_div + bonus_div
+ ue_div + ue_div
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
else: elif tf[2]:
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"]
@ -467,12 +496,24 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
else: else:
do_ue_edit(tf[2]) do_ue_edit(tf[2])
flash("UE modifiée") flash("UE modifiée")
if tf[2]:
dest_semestre_idx = tf[2]["semestre_idx"]
elif ue:
dest_semestre_idx = ue.semestre_idx
elif default_semestre_idx:
dest_semestre_idx = default_semestre_idx
elif "semestre_idx" in request.form:
dest_semestre_idx = request.form["semestre_idx"]
else:
dest_semestre_idx = 1
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation_id,
semestre_idx=tf[2]["semestre_idx"], semestre_idx=dest_semestre_idx,
) )
) )
@ -646,9 +687,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
], ],
page_title=f"Programme {formation.acronyme}", page_title=f"Programme {formation.acronyme}",
), ),
f"""<h2>Formation {formation.titre} ({formation.acronyme}) f"""<h2>{formation.to_html()} {lockicon}
[version {formation.version}] code {formation.formation_code}
{lockicon}
</h2> </h2>
""", """,
] ]
@ -711,7 +750,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
else: else:
descr_refcomp = f"""Référentiel de compétences: descr_refcomp = f"""Référentiel de compétences:
<a href="{url_for('notes.refcomp_show', <a href="{url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"> scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
class="stdlink">
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long} {formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
</a>&nbsp;""" </a>&nbsp;"""
msg_refcomp = "changer" msg_refcomp = "changer"
@ -727,7 +767,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
f"""</li> f"""</li>
<li> <a class="stdlink" href="{ <li> <a class="stdlink" href="{
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx) url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
}">éditer les coefficients des ressources et SAÉs</a> }">Éditer les coefficients des ressources et SAÉs</a>
</li> </li>
</ul> </ul>
""" """
@ -816,6 +856,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
) )
}">Créer une nouvelle version (non verrouillée)</a> }">Créer une nouvelle version (non verrouillée)</a>
</li> </li>
""" """
) )
H.append( H.append(
@ -914,7 +955,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
def _ue_table_ues( def _ue_table_ues(
parcours, parcours,
ues, ues: list[dict],
editable, editable,
tag_editable, tag_editable,
has_perm_change, has_perm_change,
@ -923,7 +964,7 @@ def _ue_table_ues(
arrow_none, arrow_none,
delete_icon, delete_icon,
delete_disabled_icon, delete_disabled_icon,
): ) -> str:
"""Édition de programme: liste des UEs (avec leurs matières et modules). """Édition de programme: liste des UEs (avec leurs matières et modules).
Pour les formations classiques (non APC/BUT) Pour les formations classiques (non APC/BUT)
""" """
@ -951,9 +992,9 @@ def _ue_table_ues(
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:" lab = "Pas d'indication de semestre:"
else: else:
lab = "Semestre %s:" % ue["semestre_id"] lab = f"""Semestre {ue["semestre_id"]}:"""
H.append( H.append(
'<div class="ue_list_div"><div class="ue_list_tit_sem">%s</div>' % lab f'<div class="ue_list_div"><div class="ue_list_tit_sem">{lab}</div>'
) )
H.append('<ul class="notes_ue_list">') H.append('<ul class="notes_ue_list">')
H.append('<li class="notes_ue_list">') H.append('<li class="notes_ue_list">')
@ -1304,8 +1345,9 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
formation = Formation.query.get(ue["formation_id"]) formation = Formation.query.get(ue["formation_id"])
if not dont_invalidate_cache: if not dont_invalidate_cache:
# Invalide les semestres utilisant cette formation: # Invalide les semestres utilisant cette formation
formation.invalidate_cached_sems() # ainsi que les poids et coefs
formation.invalidate_module_coefs()
# essai edition en ligne: # essai edition en ligne:

View File

@ -141,11 +141,18 @@ def do_formsemestre_list(*a, **kw):
def _formsemestre_enrich(sem): def _formsemestre_enrich(sem):
"""Ajoute champs souvent utiles: titre + annee et dateord (pour tris)""" """Ajoute champs souvent utiles: titre + annee et dateord (pour tris).
XXX obsolete: préférer formsemestre.to_dict() ou, mieux, les méthodes de FormSemestre.
"""
# imports ici pour eviter refs circulaires # imports ici pour eviter refs circulaires
from app.scodoc import sco_formsemestre_edit from app.scodoc import sco_formsemestre_edit
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] formations = sco_formations.formation_list(
args={"formation_id": sem["formation_id"]}
)
if not formations:
raise ScoValueError("pas de formation pour ce semestre !")
F = formations[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
# 'S1', 'S2', ... ou '' pour les monosemestres # 'S1', 'S2', ... ou '' pour les monosemestres
if sem["semestre_id"] != NO_SEMESTRE_ID: if sem["semestre_id"] != NO_SEMESTRE_ID:

View File

@ -39,23 +39,21 @@ from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteE
from app.models import ScolarNews from app.models import ScolarNews
from app.models.formations import Formation from app.models.formations import Formation
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.models.but_refcomp import ApcParcours
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app import log from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, TF from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_vdi import ApoEtapeVDI
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_compute_moy from app.scodoc import sco_compute_moy
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_etud from app.scodoc import sco_etud
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_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -153,7 +151,7 @@ def do_formsemestre_createwithmodules(edit=False):
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not current_user.has_permission(Permission.ScoImplement): if not current_user.has_permission(Permission.ScoImplement):
if not edit: if not edit:
# il faut ScoImplement pour creer un semestre # il faut ScoImplement pour créer un semestre
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")
else: else:
if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]: if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
@ -175,6 +173,7 @@ def do_formsemestre_createwithmodules(edit=False):
formation = Formation.query.get(formation_id) formation = Formation.query.get(formation_id)
if formation is None: if formation is None:
raise ScoValueError("Formation inexistante !") raise ScoValueError("Formation inexistante !")
is_apc = formation.is_apc()
if not edit: if not edit:
initvalues = {"titre": _default_sem_title(formation)} initvalues = {"titre": _default_sem_title(formation)}
semestre_id = int(vals["semestre_id"]) semestre_id = int(vals["semestre_id"])
@ -210,12 +209,12 @@ def do_formsemestre_createwithmodules(edit=False):
if NB_SEM == 1: if NB_SEM == 1:
semestre_id_list = [-1] semestre_id_list = [-1]
else: else:
if edit and formation.is_apc(): if edit and is_apc:
# en APC, ne permet pas de changer de semestre # en APC, ne permet pas de changer de semestre
semestre_id_list = [formsemestre.semestre_id] semestre_id_list = [formsemestre.semestre_id]
else: else:
semestre_id_list = list(range(1, NB_SEM + 1)) semestre_id_list = list(range(1, NB_SEM + 1))
if not formation.is_apc(): if not is_apc:
# propose "pas de semestre" seulement en classique # propose "pas de semestre" seulement en classique
semestre_id_list.insert(0, -1) semestre_id_list.insert(0, -1)
@ -226,7 +225,7 @@ def do_formsemestre_createwithmodules(edit=False):
else: else:
semestre_id_labels.append(f"S{sid}") semestre_id_labels.append(f"S{sid}")
# Liste des modules dans cette formation # Liste des modules dans cette formation
if formation.is_apc(): if is_apc:
modules = formation.modules.order_by(Module.module_type, Module.numero) modules = formation.modules.order_by(Module.module_type, Module.numero)
else: else:
modules = ( modules = (
@ -318,10 +317,10 @@ def do_formsemestre_createwithmodules(edit=False):
{ {
"size": 40, "size": 40,
"title": "Nom de ce semestre", "title": "Nom de ce semestre",
"explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans "explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans
le titre: ils seront automatiquement ajoutés <input type="button" le titre: ils seront automatiquement ajoutés <input type="button"
value="remettre titre par défaut" onClick="document.tf.titre.value='%s';"/>""" value="remettre titre par défaut" onClick="document.tf.titre.value='{
% _default_sem_title(formation), _default_sem_title(formation)}';"/>""",
}, },
), ),
( (
@ -343,11 +342,9 @@ def do_formsemestre_createwithmodules(edit=False):
"allowed_values": semestre_id_list, "allowed_values": semestre_id_list,
"labels": semestre_id_labels, "labels": semestre_id_labels,
"explanation": "en BUT, on ne peut pas modifier le semestre après création" "explanation": "en BUT, on ne peut pas modifier le semestre après création"
if formation.is_apc() if is_apc
else "",
"attributes": ['onchange="change_semestre_id();"']
if formation.is_apc()
else "", else "",
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
}, },
), ),
) )
@ -386,7 +383,7 @@ def do_formsemestre_createwithmodules(edit=False):
mf = mf_manual mf = mf_manual
for n in range(1, scu.EDIT_NB_ETAPES + 1): for n in range(1, scu.EDIT_NB_ETAPES + 1):
mf["title"] = "Etape Apogée (%d)" % n mf["title"] = f"Etape Apogée ({n})"
modform.append(("etape_apo" + str(n), mf.copy())) modform.append(("etape_apo" + str(n), mf.copy()))
modform.append( modform.append(
( (
@ -443,15 +440,19 @@ def do_formsemestre_createwithmodules(edit=False):
) )
) )
if edit: if edit:
formtit = ( formtit = f"""
<p><a href="formsemestre_edit_uecoefs?formsemestre_id={formsemestre_id}"
>Modifier les coefficients des UE capitalisées</a></p>
<h3>Sélectionner les modules, leurs responsables et les étudiants
à inscrire:</h3>
""" """
<p><a href="formsemestre_edit_uecoefs?formsemestre_id=%s">Modifier les coefficients des UE capitalisées</a></p>
<h3>Sélectionner les modules, leurs responsables et les étudiants à inscrire:</h3>
"""
% formsemestre_id
)
else: else:
formtit = """<h3>Sélectionner les modules et leurs responsables</h3><p class="help">Si vous avez des parcours (options), ne sélectionnez que les modules du tronc commun.</p>""" formtit = """<h3>Sélectionner les modules et leurs responsables</h3>
<p class="help">Si vous avez des parcours (options), dans un premier
ne sélectionnez que les modules du tronc commun, puis après inscriptions,
revenez ajouter les modules de parcours en sélectionnant les groupes d'étudiants
à y inscrire.
</p>"""
modform += [ modform += [
( (
@ -531,12 +532,53 @@ def do_formsemestre_createwithmodules(edit=False):
"explanation": "empêcher le calcul des moyennes d'UE et générale.", "explanation": "empêcher le calcul des moyennes d'UE et générale.",
}, },
), ),
]
# Choix des parcours
if is_apc:
ref_comp = formation.referentiel_competence
if ref_comp:
modform += [
(
"parcours",
{
"input_type": "checkbox",
"vertical": True,
"dom_id": "tf_module_parcours",
"labels": [parcour.libelle for parcour in ref_comp.parcours],
"allowed_values": [
str(parcour.id) for parcour in ref_comp.parcours
],
"explanation": """Parcours proposés dans ce semestre.
S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours.""",
},
)
]
if edit:
sem["parcours"] = [str(parcour.id) for parcour in formsemestre.parcours]
else:
modform += [
(
"parcours",
{
"input_type": "separator",
"title": f"""<span class="fontred">{scu.EMO_WARNING }
Pas de parcours:
<a class="stdlink" href="{ url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}">vérifier la formation</a>
</span>""",
},
)
]
# Choix des modules
modform += [
( (
"sep", "sep",
{ {
"input_type": "separator", "input_type": "separator",
"title": "", "title": "",
"template": "</table>%s<table>" % formtit, "template": f"</table>{formtit}<table>",
}, },
), ),
] ]
@ -544,8 +586,8 @@ def do_formsemestre_createwithmodules(edit=False):
nbmod = 0 nbmod = 0
for semestre_id in semestre_ids: for semestre_id in semestre_ids:
if formation.is_apc(): if is_apc:
# pour restreindre l'édition aux module du semestre sélectionné # pour restreindre l'édition aux modules du semestre sélectionné
tr_class = f'class="sem{semestre_id}"' tr_class = f'class="sem{semestre_id}"'
else: else:
tr_class = "" tr_class = ""
@ -560,7 +602,7 @@ def do_formsemestre_createwithmodules(edit=False):
"sep", "sep",
{ {
"input_type": "separator", "input_type": "separator",
"title": "<b>Semestre %s</b>" % semestre_id, "title": f"<b>Semestre {semestre_id}</b>",
"template": templ_sep, "template": templ_sep,
}, },
) )
@ -568,13 +610,13 @@ def do_formsemestre_createwithmodules(edit=False):
for mod in mods: for mod in mods:
if mod["semestre_id"] == semestre_id and ( if mod["semestre_id"] == semestre_id and (
(not edit) # creation => tous modules (not edit) # creation => tous modules
or (not formation.is_apc()) # pas BUT, on peut mixer les semestres or (not is_apc) # pas BUT, on peut mixer les semestres
or (semestre_id == formsemestre.semestre_id) # module du semestre or (semestre_id == formsemestre.semestre_id) # module du semestre
or (mod["module_id"] in module_ids_set) # module déjà présent or (mod["module_id"] in module_ids_set) # module déjà présent
): ):
nbmod += 1 nbmod += 1
if edit: if edit:
select_name = "%s!group_id" % mod["module_id"] select_name = f"{mod['module_id']}!group_id"
def opt_selected(gid): def opt_selected(gid):
if gid == vals.get(select_name): if gid == vals.get(select_name):
@ -603,13 +645,16 @@ def do_formsemestre_createwithmodules(edit=False):
group["group_name"], group["group_name"],
) )
fcg += "</select>" fcg += "</select>"
itemtemplate = ( itemtemplate = f"""<tr {tr_class}>
f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>""" <td class="tf-fieldlabel">%(label)s</td>
+ fcg <td class="tf-field">%(elem)s</td>
+ "</td></tr>" <td>{fcg}</td>
) </tr>"""
else: else:
itemtemplate = f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>""" itemtemplate = f"""<tr {tr_class}>
<td class="tf-fieldlabel">%(label)s</td>
<td class="tf-field">%(elem)s</td>
</tr>"""
modform.append( modform.append(
( (
"MI" + str(mod["module_id"]), "MI" + str(mod["module_id"]),
@ -742,7 +787,8 @@ def do_formsemestre_createwithmodules(edit=False):
for module_id in tf[2]["tf-checked"]: for module_id in tf[2]["tf-checked"]:
mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id]) mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id])
if mod_resp_id is None: if mod_resp_id is None:
# Si un module n'a pas de responsable (ou inconnu), l'affecte au 1er directeur des etudes: # Si un module n'a pas de responsable (ou inconnu),
# l'affecte au 1er directeur des etudes:
mod_resp_id = tf[2]["responsable_id"] mod_resp_id = tf[2]["responsable_id"]
tf[2][module_id] = mod_resp_id tf[2][module_id] = mod_resp_id
@ -763,7 +809,7 @@ def do_formsemestre_createwithmodules(edit=False):
module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]] module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
_formsemestre_check_ue_bonus_unicity(module_ids_checked) _formsemestre_check_ue_bonus_unicity(module_ids_checked)
if not edit: if not edit:
if formation.is_apc(): if is_apc:
_formsemestre_check_module_list( _formsemestre_check_module_list(
module_ids_checked, tf[2]["semestre_id"] module_ids_checked, tf[2]["semestre_id"]
) )
@ -777,14 +823,6 @@ def do_formsemestre_createwithmodules(edit=False):
"responsable_id": tf[2][f"MI{module_id}"], "responsable_id": tf[2][f"MI{module_id}"],
} }
_ = sco_moduleimpl.do_moduleimpl_create(modargs) _ = sco_moduleimpl.do_moduleimpl_create(modargs)
flash("Nouveau semestre créé")
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
else: else:
# Modification du semestre: # Modification du semestre:
# on doit creer les modules nouvellement selectionnés # on doit creer les modules nouvellement selectionnés
@ -794,7 +832,7 @@ def do_formsemestre_createwithmodules(edit=False):
module_ids_tocreate = [ module_ids_tocreate = [
x for x in module_ids_checked if not x in module_ids_existing x for x in module_ids_checked if not x in module_ids_existing
] ]
if formation.is_apc(): if is_apc:
_formsemestre_check_module_list( _formsemestre_check_module_list(
module_ids_tocreate, tf[2]["semestre_id"] module_ids_tocreate, tf[2]["semestre_id"]
) )
@ -868,7 +906,19 @@ def do_formsemestre_createwithmodules(edit=False):
modargs, formsemestre_id=formsemestre_id modargs, formsemestre_id=formsemestre_id
) )
mod = sco_edit_module.module_list({"module_id": module_id})[0] mod = sco_edit_module.module_list({"module_id": module_id})[0]
# --- Association des parcours
formsemestre = FormSemestre.query.get(formsemestre_id)
if "parcours" in tf[2]:
formsemestre.parcours = [
ApcParcours.query.get(int(parcour_id_str))
for parcour_id_str in tf[2]["parcours"]
]
db.session.add(formsemestre)
db.session.commit()
# --- Crée ou met à jour les groupes de parcours BUT
formsemestre.setup_parcours_groups()
# --- Fin
if edit:
if msg: if msg:
msg_html = ( msg_html = (
'<div class="ue_warning"><span>Attention !<ul><li>' '<div class="ue_warning"><span>Attention !<ul><li>'
@ -889,6 +939,15 @@ def do_formsemestre_createwithmodules(edit=False):
"formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié" "formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
% formsemestre_id % formsemestre_id
) )
else:
flash("Nouveau semestre créé")
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
def _formsemestre_check_module_list(module_ids, semestre_idx): def _formsemestre_check_module_list(module_ids, semestre_idx):

View File

@ -35,6 +35,7 @@ from flask import url_for, 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 from app.models import FormSemestre
from app.models.groups import GroupDescr, Partition
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.scolog import logdb from app.scodoc.scolog import logdb
@ -257,14 +258,14 @@ def do_formsemestre_inscription_with_modules(
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS """Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
(donc sauf le sport) (donc sauf le sport)
""" """
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
# inscription au semestre # inscription au semestre
args = {"formsemestre_id": formsemestre_id, "etudid": etudid} args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
if etat is not None: if etat is not None:
args["etat"] = etat args["etat"] = etat
do_formsemestre_inscription_create(args, method=method) do_formsemestre_inscription_create(args, method=method)
log( log(
"do_formsemestre_inscription_with_modules: etudid=%s formsemestre_id=%s" f"do_formsemestre_inscription_with_modules: etudid={etudid} formsemestre_id={formsemestre_id}"
% (etudid, formsemestre_id)
) )
# inscriptions aux groupes # inscriptions aux groupes
# 1- inscrit au groupe 'tous' # 1- inscrit au groupe 'tous'
@ -275,10 +276,16 @@ def do_formsemestre_inscription_with_modules(
# 2- inscrit aux groupes # 2- inscrit aux groupes
for group_id in group_ids: for group_id in group_ids:
if group_id and not group_id in gdone: if group_id and not group_id in gdone:
group = GroupDescr.query.get_or_404(group_id)
if group.partition.groups_editable:
sco_groups.set_group(etudid, group_id) sco_groups.set_group(etudid, group_id)
gdone[group_id] = 1 gdone[group_id] = 1
else:
log(
f"do_formsemestre_inscription_with_modules: group {group:r} belongs to non editable partition"
)
# inscription a tous les modules de ce semestre # Inscription à tous les modules de ce semestre
modimpls = sco_moduleimpl.moduleimpl_withmodule_list( modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) )
@ -288,6 +295,8 @@ def do_formsemestre_inscription_with_modules(
{"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid}, {"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid},
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
# Mise à jour des inscriptions aux parcours:
formsemestre.update_inscriptions_parcours_from_groups()
def formsemestre_inscription_with_modules_etud( def formsemestre_inscription_with_modules_etud(

View File

@ -40,6 +40,7 @@ from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Module from app.models import Module
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -148,7 +149,10 @@ def formsemestre_status_menubar(sem):
{ {
"title": "Voir la formation %(acronyme)s (v%(version)s)" % F, "title": "Voir la formation %(acronyme)s (v%(version)s)" % F,
"endpoint": "notes.ue_table", "endpoint": "notes.ue_table",
"args": {"formation_id": sem["formation_id"]}, "args": {
"formation_id": sem["formation_id"],
"semestre_idx": sem["semestre_id"],
},
"enabled": True, "enabled": True,
"helpmsg": "Tableau de bord du semestre", "helpmsg": "Tableau de bord du semestre",
}, },
@ -325,7 +329,7 @@ def formsemestre_status_menubar(sem):
}, },
{ {
"title": "Créer/modifier les partitions...", "title": "Créer/modifier les partitions...",
"endpoint": "scolar.editPartitionForm", "endpoint": "scolar.edit_partition_form",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
"enabled": sco_groups.sco_permissions_check.can_change_groups( "enabled": sco_groups.sco_permissions_check.can_change_groups(
formsemestre_id formsemestre_id
@ -345,7 +349,7 @@ def formsemestre_status_menubar(sem):
"title": "%s" % partition["partition_name"], "title": "%s" % partition["partition_name"],
"endpoint": "scolar.affect_groups", "endpoint": "scolar.affect_groups",
"args": {"partition_id": partition["partition_id"]}, "args": {"partition_id": partition["partition_id"]},
"enabled": enabled, "enabled": enabled and partition["groups_editable"],
} }
) )
menuGroupes.append( menuGroupes.append(
@ -406,10 +410,9 @@ def formsemestre_status_menubar(sem):
}, },
{ {
"title": "Saisie des décisions du jury", "title": "Saisie des décisions du jury",
"endpoint": "notes.formsemestre_recapcomplet", "endpoint": "notes.formsemestre_saisie_jury",
"args": { "args": {
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"modejury": 1,
}, },
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id), "enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
}, },
@ -499,20 +502,24 @@ def retreive_formsemestre_from_request() -> int:
# Element HTML decrivant un semestre (barre de menu et infos) # Element HTML decrivant un semestre (barre de menu et infos)
def formsemestre_page_title(): def formsemestre_page_title(formsemestre_id=None):
"""Element HTML decrivant un semestre (barre de menu et infos) """Element HTML decrivant un semestre (barre de menu et infos)
Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group) Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group)
""" """
formsemestre_id = retreive_formsemestre_from_request() formsemestre_id = (
formsemestre_id
if formsemestre_id is not None
else retreive_formsemestre_from_request()
)
# #
if not formsemestre_id: if not formsemestre_id:
return "" return ""
try: try:
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.get(formsemestre_id) except ValueError:
except: log(f"formsemestre_id: invalid type {formsemestre_id:r}")
log("can't find formsemestre_id %s" % formsemestre_id)
return "" return ""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
h = render_template( h = render_template(
"formsemestre_page_title.html", "formsemestre_page_title.html",
@ -578,7 +585,9 @@ def fill_formsemestre(sem):
# Description du semestre sous forme de table exportable # Description du semestre sous forme de table exportable
def formsemestre_description_table(formsemestre_id, with_evals=False): def formsemestre_description_table(
formsemestre_id, with_evals=False, with_parcours=False
):
"""Description du semestre sous forme de table exportable """Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients Liste des modules et de leurs coefficients
""" """
@ -618,7 +627,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
ue_info["Coef._class"] = "ue_coef" ue_info["Coef._class"] = "ue_coef"
R.append(ue_info) R.append(ue_info)
ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list( mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=M["moduleimpl_id"] moduleimpl_id=M["moduleimpl_id"]
) )
enseignants = ", ".join( enseignants = ", ".join(
@ -629,7 +638,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
"Code": M["module"]["code"] or "", "Code": M["module"]["code"] or "",
"Module": M["module"]["abbrev"] or M["module"]["titre"], "Module": M["module"]["abbrev"] or M["module"]["titre"],
"_Module_class": "scotext", "_Module_class": "scotext",
"Inscrits": len(ModInscrits), "Inscrits": len(mod_inscrits),
"Responsable": sco_users.user_info(M["responsable_id"])["nomprenom"], "Responsable": sco_users.user_info(M["responsable_id"])["nomprenom"],
"_Responsable_class": "scotext", "_Responsable_class": "scotext",
"Enseignants": enseignants, "Enseignants": enseignants,
@ -648,10 +657,15 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
moduleimpl_id=M["moduleimpl_id"], moduleimpl_id=M["moduleimpl_id"],
), ),
} }
R.append(l)
if M["module"]["coefficient"]: if M["module"]["coefficient"]:
sum_coef += M["module"]["coefficient"] sum_coef += M["module"]["coefficient"]
if with_parcours:
module = Module.query.get(M["module_id"])
l["parcours"] = ", ".join(sorted([pa.code for pa in module.parcours]))
R.append(l)
if with_evals: if with_evals:
# Ajoute lignes pour evaluations # Ajoute lignes pour evaluations
evals = nt.get_mod_evaluation_etat_list(M["moduleimpl_id"]) evals = nt.get_mod_evaluation_etat_list(M["moduleimpl_id"])
@ -676,7 +690,10 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef} sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef}
R.append(sums) R.append(sums)
columns_ids = ["UE", "Code", "Module", "Coef."] columns_ids = ["UE", "Code", "Module"]
if with_parcours:
columns_ids += ["parcours"]
columns_ids += ["Coef."]
if sco_preferences.get_preference("bul_show_ects", formsemestre_id): if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
columns_ids += ["ects"] columns_ids += ["ects"]
columns_ids += ["Inscrits", "Responsable", "Enseignants"] columns_ids += ["Inscrits", "Responsable", "Enseignants"]
@ -696,6 +713,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
titles["description"] = "" titles["description"] = ""
titles["coefficient"] = "Coef. éval." titles["coefficient"] = "Coef. éval."
titles["evalcomplete_str"] = "Complète" titles["evalcomplete_str"] = "Complète"
titles["parcours"] = "Parcours"
titles["publish_incomplete_str"] = "Toujours Utilisée" titles["publish_incomplete_str"] = "Toujours Utilisée"
title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois()) title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois())
@ -720,21 +738,26 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
) )
def formsemestre_description(formsemestre_id, format="html", with_evals=False): def formsemestre_description(
formsemestre_id, format="html", with_evals=False, with_parcours=False
):
"""Description du semestre sous forme de table exportable """Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients Liste des modules et de leurs coefficients
""" """
with_evals = int(with_evals) with_evals = int(with_evals)
tab = formsemestre_description_table(formsemestre_id, with_evals=with_evals) tab = formsemestre_description_table(
tab.html_before_table = """<form name="f" method="get" action="%s"> formsemestre_id, with_evals=with_evals, with_parcours=with_parcours
<input type="hidden" name="formsemestre_id" value="%s"></input>
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()" """ % (
request.base_url,
formsemestre_id,
) )
if with_evals: tab.html_before_table = f"""
tab.html_before_table += "checked" <form name="f" method="get" action="{request.base_url}">
tab.html_before_table += ">indiquer les évaluations</input></form>" <input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()"
{ "checked" if with_evals else "" }
>indiquer les évaluations</input>
<input type="checkbox" name="with_parcours" value="1" onchange="document.f.submit()"
{ "checked" if with_parcours else "" }
>indiquer les parcours BUT</input>
"""
return tab.make_page(format=format) return tab.make_page(format=format)
@ -854,7 +877,7 @@ def _make_listes_sem(sem, with_absences=True):
H.append( H.append(
f"""<h4><a f"""<h4><a
href="{ href="{
url_for("scolar.editPartitionForm", url_for("scolar.edit_partition_form",
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
) )
@ -929,10 +952,18 @@ def formsemestre_status_head(formsemestre_id=None, page_title=None):
}</tt></b>)""" }</tt></b>)"""
) )
H.append("</td></tr>") H.append("</td></tr>")
if sem.parcours:
H.append(
f"""
<tr><td class="fichetitre2">Parcours: </td>
<td style="color: blue;">{', '.join(parcours.code for parcours in sem.parcours)}</td>
</tr>
"""
)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id)
H.append( H.append(
'<tr><td class="fichetitre2">Evaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides' '<tr><td class="fichetitre2">Évaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
% evals % evals
) )
if evals["last_modif"]: if evals["last_modif"]:

View File

@ -581,14 +581,20 @@ def formsemestre_recap_parcours_table(
else: else:
pm = plusminus % sem["formsemestre_id"] pm = plusminus % sem["formsemestre_id"]
H.append( inscr = formsemestre.etuds_inscriptions.get(etudid)
'<td class="rcp_type_sem" style="background-color:%s;">%s%s</td>' parcours_name = (
% (bgcolor, num_sem, pm) f' <span class="code_parcours">{inscr.parcour.code}</span>'
if (inscr and inscr.parcour)
else ""
) )
H.append('<td class="datedebut">%(mois_debut)s</td>' % sem)
H.append( H.append(
'<td class="rcp_titre_sem"><a class="formsemestre_status_link" href="%sformsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="Bulletin de notes">%s</a></td>' f"""
% (a_url, sem["formsemestre_id"], etudid, sem["titreannee"]) <td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
<td class="datedebut">{sem['mois_debut']}</td>
<td class="rcp_titre_sem"><a class="formsemestre_status_link"
href="{a_url}formsemestre_bulletinetud?formsemestre_id={formsemestre.id}&etudid={etudid}"
title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a></td>
"""
) )
if decision_sem: if decision_sem:
H.append('<td class="rcp_dec">%s</td>' % decision_sem["code"]) H.append('<td class="rcp_dec">%s</td>' % decision_sem["code"])

View File

@ -76,10 +76,12 @@ partitionEditor = ndb.EditableTable(
"numero", "numero",
"bul_show_rank", "bul_show_rank",
"show_in_lists", "show_in_lists",
"editable",
), ),
input_formators={ input_formators={
"bul_show_rank": bool, "bul_show_rank": bool,
"show_in_lists": bool, "show_in_lists": bool,
"editable": bool,
}, },
) )
@ -105,14 +107,19 @@ def get_group(group_id: int):
return r[0] return r[0]
def group_delete(group, force=False): def group_delete(group_id: int):
"""Delete a group.""" """Delete a group."""
# if not group['group_name'] and not force: # if not group['group_name'] and not force:
# raise ValueError('cannot suppress this group') # raise ValueError('cannot suppress this group')
# remove memberships: # remove memberships:
ndb.SimpleQuery("DELETE FROM group_membership WHERE group_id=%(group_id)s", group) ndb.SimpleQuery(
"DELETE FROM group_membership WHERE group_id=%(group_id)s",
{"group_id": group_id},
)
# delete group: # delete group:
ndb.SimpleQuery("DELETE FROM group_descr WHERE id=%(group_id)s", group) ndb.SimpleQuery(
"DELETE FROM group_descr WHERE id=%(group_id)s", {"group_id": group_id}
)
def get_partition(partition_id): def get_partition(partition_id):
@ -264,6 +271,17 @@ def get_group_members(group_id, etat=None):
return r return r
def check_group_name(group_name, partition, raiser=False):
"""If groupe name exists in partition : if raiser -> Raise ScoValueError else-> return true"""
exists = group_name in [g["group_name"] for g in get_partition_groups(partition)]
if exists:
if raiser:
raise ScoValueError("Le nom de groupe existe déjà dans la partition")
else:
return True
return False
# obsolete: sco_groups_view.DisplayedGroupsInfos # obsolete: sco_groups_view.DisplayedGroupsInfos
# def get_groups_members(group_ids, etat=None): # def get_groups_members(group_ids, etat=None):
# """Liste les étudiants d'une liste de groupes # """Liste les étudiants d'une liste de groupes
@ -621,10 +639,12 @@ def comp_origin(etud, cur_sem):
return "" # parcours normal, ne le signale pas return "" # parcours normal, ne le signale pas
def set_group(etudid, group_id): def set_group(etudid: int, group_id: int) -> bool:
"""Inscrit l'étudiant au groupe. """Inscrit l'étudiant au groupe.
Return True if ok, False si deja inscrit. Return True if ok, False si deja inscrit.
Warning: don't check if group_id exists (the caller should check). Warning:
- don't check if group_id exists (the caller should check).
- don't check if group's partition is editable
""" """
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
@ -686,7 +706,12 @@ def change_etud_group_in_partition(etudid, group_id, partition=None):
% (formsemestre_id, partition["partition_name"], group["group_name"]), % (formsemestre_id, partition["partition_name"], group["group_name"]),
) )
cnx.commit() cnx.commit()
# 4- invalidate cache
# 5- Update parcours
formsemestre = FormSemestre.query.get(formsemestre_id)
formsemestre.update_inscriptions_parcours_from_groups()
# 6- invalidate cache
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) # > change etud group ) # > change etud group
@ -698,14 +723,28 @@ def setGroups(
groupsToCreate="", # name and members of new groups groupsToCreate="", # name and members of new groups
groupsToDelete="", # groups to delete groupsToDelete="", # groups to delete
): ):
"""Affect groups (Ajax request) """Affect groups (Ajax request): renvoie du XML
groupsLists: lignes de la forme "group_id;etudid;...\n" groupsLists: lignes de la forme "group_id;etudid;...\n"
groupsToCreate: lignes "group_name;etudid;...\n" groupsToCreate: lignes "group_name;etudid;...\n"
groupsToDelete: group_id;group_id;... groupsToDelete: group_id;group_id;...
Ne peux pas modifier les groupes des partitions non éditables.
""" """
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
def xml_error(msg, code=404):
data = (
f'<?xml version="1.0" encoding="utf-8"?><response>Error: {msg}</response>'
)
response = make_response(data, code)
response.headers["Content-Type"] = scu.XML_MIMETYPE
return response
partition = get_partition(partition_id) partition = get_partition(partition_id)
if not partition["groups_editable"]:
msg = "setGroups: partition non editable"
log(msg)
return xml_error(msg, code=403)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
if not sco_permissions_check.can_change_groups(formsemestre_id): if not sco_permissions_check.can_change_groups(formsemestre_id):
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 !")
@ -727,8 +766,8 @@ def setGroups(
continue continue
try: try:
group_id = int(group_id) group_id = int(group_id)
except ValueError as exc: except ValueError:
log("setGroups: ignoring invalid group_id={group_id}") log(f"setGroups: ignoring invalid group_id={group_id}")
continue continue
group = get_group(group_id) group = get_group(group_id)
# Anciens membres du groupe: # Anciens membres du groupe:
@ -778,6 +817,10 @@ def setGroups(
for etudid in fs[1:-1]: for etudid in fs[1:-1]:
change_etud_group_in_partition(etudid, group_id, partition) change_etud_group_in_partition(etudid, group_id, partition)
# Update parcours
formsemestre = FormSemestre.query.get(formsemestre_id)
formsemestre.update_inscriptions_parcours_from_groups()
data = ( data = (
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>' '<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
) )
@ -798,15 +841,15 @@ def create_group(partition_id, group_name="", default=False) -> int:
if not group_name and not default: if not group_name and not default:
raise ValueError("invalid group name: ()") raise ValueError("invalid group name: ()")
# checkGroupName(group_name) # checkGroupName(group_name)
if group_name in [g["group_name"] for g in get_partition_groups(partition)]: if check_group_name(group_name, partition):
raise ValueError( raise ScoValueError(
"group_name %s already exists in partition" % group_name f"group_name {group_name} already exists in partition"
) # XXX FIX: incorrect error handling (in AJAX) ) # XXX FIX: incorrect error handling (in AJAX)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
group_id = groupEditor.create( group_id = groupEditor.create(
cnx, {"partition_id": partition_id, "group_name": group_name} cnx, {"partition_id": partition_id, "group_name": group_name}
) )
log("create_group: created group_id=%s" % group_id) log("create_group: created group_id={group_id}")
# #
return group_id return group_id
@ -817,21 +860,18 @@ def delete_group(group_id, partition_id=None):
affectation aux groupes) affectation aux groupes)
partition_id est optionnel et ne sert que pour verifier que le groupe partition_id est optionnel et ne sert que pour verifier que le groupe
est bien dans cette partition. est bien dans cette partition.
S'il s'agit d'un groupe de parcours, affecte l'inscription des étudiants aux parcours.
""" """
group = get_group(group_id) group = GroupDescr.query.get_or_404(group_id)
if partition_id: if partition_id:
if partition_id != group["partition_id"]: if partition_id != group.partition_id:
raise ValueError("inconsistent partition/group") raise ValueError("inconsistent partition/group")
else: if not sco_permissions_check.can_change_groups(group.partition.formsemestre_id):
partition_id = group["partition_id"]
partition = get_partition(partition_id)
if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]):
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 !")
log( log(f"delete_group: group={group} partition={group.partition}")
"delete_group: group_id=%s group_name=%s partition_name=%s" formsemestre = group.partition.formsemestre
% (group_id, group["group_name"], partition["partition_name"]) group_delete(group.id)
) formsemestre.update_inscriptions_parcours_from_groups()
group_delete(group)
def partition_create( def partition_create(
@ -881,7 +921,7 @@ def partition_create(
if redirect: if redirect:
return flask.redirect( return flask.redirect(
url_for( url_for(
"scolar.editPartitionForm", "scolar.edit_partition_form",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
@ -900,11 +940,12 @@ def get_arrow_icons_tags():
return arrow_up, arrow_down, arrow_none return arrow_up, arrow_down, arrow_none
def editPartitionForm(formsemestre_id=None): def edit_partition_form(formsemestre_id=None):
"""Form to create/suppress partitions""" """Form to create/suppress partitions"""
# ad-hoc form # ad-hoc form
if not sco_permissions_check.can_change_groups(formsemestre_id): if not sco_permissions_check.can_change_groups(formsemestre_id):
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 !")
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
partitions = get_partitions_list(formsemestre_id) partitions = get_partitions_list(formsemestre_id)
arrow_up, arrow_down, arrow_none = get_arrow_icons_tags() arrow_up, arrow_down, arrow_none = get_arrow_icons_tags()
suppricon = scu.icontag( suppricon = scu.icontag(
@ -914,7 +955,7 @@ def editPartitionForm(formsemestre_id=None):
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Partitions...", page_title="Partitions...",
javascripts=["js/editPartitionForm.js"], javascripts=["js/edit_partition_form.js"],
), ),
# limite à SHORT_STR_LEN # limite à SHORT_STR_LEN
r"""<script type="text/javascript"> r"""<script type="text/javascript">
@ -966,14 +1007,19 @@ def editPartitionForm(formsemestre_id=None):
for group in get_partition_groups(p) for group in get_partition_groups(p)
] ]
H.append(", ".join(lg)) H.append(", ".join(lg))
H.append("""</td><td>""")
if p["groups_editable"]:
H.append( H.append(
f"""</td><td><a class="stdlink" href="{ f"""<a class="stdlink" href="{
url_for("scolar.affect_groups", url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=p["partition_id"]) partition_id=p["partition_id"])
}">répartir</a></td> }">répartir</a></td>
""" """
) )
else:
H.append("""non éditable""")
H.append("""</td>""")
H.append( H.append(
'<td><a class="stdlink" href="partition_rename?partition_id=%s">renommer</a></td>' '<td><a class="stdlink" href="partition_rename?partition_id=%s">renommer</a></td>'
% p["partition_id"] % p["partition_id"]
@ -1000,16 +1046,32 @@ def editPartitionForm(formsemestre_id=None):
# #
H.append("</tr>") H.append("</tr>")
H.append("</table>") H.append("</table>")
H.append('<div class="form_rename_partition">')
H.append( H.append(
'<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id f"""<div class="form_rename_partition">
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
<input type="hidden" name="redirect" value="1"/>
<input type="text" name="partition_name" size="12" onkeyup="checkname();"/>
<input type="submit" name="ok" disabled="1" value="Nouvelle partition"/>
"""
) )
H.append('<input type="hidden" name="redirect" value="1"/>') if formsemestre.formation.is_apc() and scu.PARTITION_PARCOURS not in (
p["partition_name"] for p in partitions
):
# propose création partition "Parcours"
H.append( H.append(
'<input type="text" name="partition_name" size="12" onkeyup="checkname();"/>' f"""
<div style="margin-top: 10px"><a class="stdlink" href="{
url_for("scolar.create_partition_parcours", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Créer une partition avec un groupe par parcours (BUT)</a>
</div>
"""
)
H.append(
"""
</div>
</form>
"""
) )
H.append('<input type="submit" name="ok" disabled="1" value="Nouvelle partition"/>')
H.append("</div></form>")
H.append( H.append(
"""<div class="help"> """<div class="help">
<p>Les partitions sont des découpages de l'ensemble des étudiants. <p>Les partitions sont des découpages de l'ensemble des étudiants.
@ -1017,10 +1079,15 @@ def editPartitionForm(formsemestre_id=None):
On peut créer autant de partitions que nécessaire. On peut créer autant de partitions que nécessaire.
</p> </p>
<ul> <ul>
<li>Dans chaque partition, un nombre de groupes quelconque peuvent être créés (suivre le lien "répartir"). <li>Dans chaque partition, un nombre de groupes quelconque peuvent
<li>On peut faire afficher le classement de l'étudiant dans son groupe d'une partition en cochant "afficher rang sur bulletins" (ainsi, on peut afficher le classement en groupes de TD mais pas en groupe de TP, si ce sont deux partitions). être créés (suivre le lien "répartir").
<li>On peut faire afficher le classement de l'étudiant dans son
groupe d'une partition en cochant "afficher rang sur bulletins"
(ainsi, on peut afficher le classement en groupes de TD mais pas en
groupe de TP, si ce sont deux partitions).
</li> </li>
<li>Décocher "afficher sur noms groupes" pour ne pas que cette partition apparaisse dans les noms de groupes <li>Décocher "afficher sur noms groupes" pour ne pas que cette partition
apparaisse dans les noms de groupes
</li> </li>
</ul> </ul>
</div> </div>
@ -1052,11 +1119,14 @@ def partition_set_attr(partition_id, attr, value):
def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False): def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False):
"""Suppress a partition (and all groups within). """Suppress a partition (and all groups within).
default partition cannot be suppressed (unless force)""" The default partition cannot be suppressed (unless force).
Si la partition de parcours est supprimée, les étudiants sont désinscrits des parcours.
"""
partition = get_partition(partition_id) partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
if not sco_permissions_check.can_change_groups(formsemestre_id): if not sco_permissions_check.can_change_groups(formsemestre_id):
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 !")
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not partition["partition_name"] and not force: if not partition["partition_name"] and not force:
raise ValueError("cannot suppress this partition") raise ValueError("cannot suppress this partition")
@ -1075,21 +1145,23 @@ def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=Fal
""" """
% (partition["partition_name"], grnames), % (partition["partition_name"], grnames),
dest_url="", dest_url="",
cancel_url="editPartitionForm?formsemestre_id=%s" % formsemestre_id, cancel_url="edit_partition_form?formsemestre_id=%s" % formsemestre_id,
parameters={"redirect": redirect, "partition_id": partition_id}, parameters={"redirect": redirect, "partition_id": partition_id},
) )
log("partition_delete: partition_id=%s" % partition_id) log("partition_delete: partition_id=%s" % partition_id)
# 1- groups # 1- groups
for group in groups: for group in groups:
group_delete(group, force=force) group_delete(group["group_id"])
# 2- partition # 2- partition
partitionEditor.delete(cnx, partition_id) partitionEditor.delete(cnx, partition_id)
formsemestre.update_inscriptions_parcours_from_groups()
# redirect to partition edit page: # redirect to partition edit page:
if redirect: if redirect:
return flask.redirect( return flask.redirect(
"editPartitionForm?formsemestre_id=" + str(formsemestre_id) "edit_partition_form?formsemestre_id=" + str(formsemestre_id)
) )
@ -1146,7 +1218,7 @@ def partition_move(partition_id, after=0, redirect=1):
# redirect to partition edit page: # redirect to partition edit page:
if redirect: if redirect:
return flask.redirect( return flask.redirect(
"editPartitionForm?formsemestre_id=" + str(formsemestre_id) "edit_partition_form?formsemestre_id=" + str(formsemestre_id)
) )
@ -1169,7 +1241,8 @@ def partition_rename(partition_id):
"default": partition["partition_name"], "default": partition["partition_name"],
"allow_null": False, "allow_null": False,
"size": 12, "size": 12,
"validator": lambda val, _: len(val) < SHORT_STR_LEN, "validator": lambda val, _: (len(val) < SHORT_STR_LEN)
and (val != scu.PARTITION_PARCOURS),
}, },
), ),
), ),
@ -1186,7 +1259,7 @@ def partition_rename(partition_id):
) )
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
"editPartitionForm?formsemestre_id=" + str(formsemestre_id) "edit_partition_form?formsemestre_id=" + str(formsemestre_id)
) )
else: else:
# form submission # form submission
@ -1201,6 +1274,8 @@ def partition_set_name(partition_id, partition_name, redirect=1):
partition = get_partition(partition_id) partition = get_partition(partition_id)
if partition["partition_name"] is None: if partition["partition_name"] is None:
raise ValueError("can't set a name to default partition") raise ValueError("can't set a name to default partition")
if partition_name == scu.PARTITION_PARCOURS:
raise ScoValueError(f"nom de partition {scu.PARTITION_PARCOURS} réservé.")
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
# check unicity # check unicity
@ -1227,7 +1302,7 @@ def partition_set_name(partition_id, partition_name, redirect=1):
# redirect to partition edit page: # redirect to partition edit page:
if redirect: if redirect:
return flask.redirect( return flask.redirect(
"editPartitionForm?formsemestre_id=" + str(formsemestre_id) "edit_partition_form?formsemestre_id=" + str(formsemestre_id)
) )
@ -1246,7 +1321,7 @@ def group_set_name(group_id, group_name, redirect=True):
redirect = int(redirect) redirect = int(redirect)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
groupEditor.edit(cnx, {"group_id": group_id, "group_name": group_name}) groupEditor.edit(cnx, {"group_id": group_id, "group_name": group_name})
check_group_name(group_name, get_partition(group["partition_id"]), True)
# redirect to partition edit page: # redirect to partition edit page:
if redirect: if redirect:
return flask.redirect( return flask.redirect(
@ -1312,6 +1387,8 @@ def groups_auto_repartition(partition_id=None):
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
partition = get_partition(partition_id) partition = get_partition(partition_id)
if not partition["groups_editable"]:
raise AccessDenied("Partition non éditable")
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
formsemestre = FormSemestre.query.get(formsemestre_id) formsemestre = FormSemestre.query.get(formsemestre_id)
# renvoie sur page édition groupes # renvoie sur page édition groupes
@ -1368,7 +1445,7 @@ def groups_auto_repartition(partition_id=None):
group_names = sorted(set([x.strip() for x in groupNames.split(",")])) group_names = sorted(set([x.strip() for x in groupNames.split(",")]))
# Détruit les groupes existant de cette partition # Détruit les groupes existant de cette partition
for old_group in get_partition_groups(partition): for old_group in get_partition_groups(partition):
group_delete(old_group) group_delete(old_group["group_id"])
# Crée les nouveaux groupes # Crée les nouveaux groupes
group_ids = [] group_ids = []
for group_name in group_names: for group_name in group_names:

View File

@ -29,6 +29,7 @@
""" """
from flask import render_template from flask import render_template
from app.models import Partition
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_exceptions import AccessDenied
@ -39,10 +40,11 @@ def affect_groups(partition_id):
Permet aussi la creation et la suppression de groupes. Permet aussi la creation et la suppression de groupes.
""" """
# réécrit pour 9.0.47 avec un template # réécrit pour 9.0.47 avec un template
partition = sco_groups.get_partition(partition_id) partition = Partition.query.get_or_404(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition.formsemestre_id
if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("vous n'avez pas la permission de modifier les groupes") raise AccessDenied("vous n'avez pas la permission de modifier les groupes")
partition.formsemestre.setup_parcours_groups()
return render_template( return render_template(
"scolar/affect_groups.html", "scolar/affect_groups.html",
sco_header=html_sco_header.sco_header( sco_header=html_sco_header.sco_header(
@ -52,8 +54,9 @@ def affect_groups(partition_id):
), ),
sco_footer=html_sco_header.sco_footer(), sco_footer=html_sco_header.sco_footer(),
partition=partition, partition=partition,
partitions_list=sco_groups.get_partitions_list( # Liste des partitions sans celle par defaut:
formsemestre_id, with_default=False partitions_list=partition.formsemestre.partitions.filter(
Partition.partition_name != None
), ),
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )

View File

@ -33,14 +33,13 @@ import io
import os import os
import re import re
import time import time
from datetime import date
from flask import g, url_for from flask import g, url_for
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.models import ScolarNews from app.models import ScolarNews, GroupDescr
from app.scodoc.sco_excel import COLORS from app.scodoc.sco_excel import COLORS
from app.scodoc.sco_formsemestre_inscriptions import ( from app.scodoc.sco_formsemestre_inscriptions import (
@ -718,9 +717,17 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
) )
for group_id in group_ids: for group_id in group_ids:
group = GroupDescr.query.get(group_id)
if group.partition.groups_editable:
sco_groups.change_etud_group_in_partition( sco_groups.change_etud_group_in_partition(
args["etudid"], group_id args["etudid"], group_id
) )
else:
log("scolars_import_admission: partition non editable")
diag.append(
f"Attention: partition {group.partition} non editable (ignorée)"
)
# #
diag.append("import de %s" % (etud["nomprenom"])) diag.append("import de %s" % (etud["nomprenom"]))
n_import += 1 n_import += 1

View File

@ -219,6 +219,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
# inscrit aux groupes # inscrit aux groupes
for partition_group in partition_groups: for partition_group in partition_groups:
if partition_group["groups_editable"]:
sco_groups.change_etud_group_in_partition( sco_groups.change_etud_group_in_partition(
etudid, etudid,
partition_group["group_id"], partition_group["group_id"],

View File

@ -192,10 +192,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
"""Tableau de bord module (liste des evaluations etc)""" """Tableau de bord module (liste des evaluations etc)"""
if not isinstance(moduleimpl_id, int): if not isinstance(moduleimpl_id, int):
raise ScoInvalidIdType("moduleimpl_id must be an integer !") raise ScoInvalidIdType("moduleimpl_id must be an integer !")
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
M = modimpl.to_dict() M = modimpl.to_dict()
formsemestre_id = M["formsemestre_id"] formsemestre_id = modimpl.formsemestre_id
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(

View File

@ -52,7 +52,7 @@ from reportlab.platypus import Paragraph
from reportlab.lib import styles from reportlab.lib import styles
import flask import flask
from flask import url_for, g, request from flask import url_for, g, redirect, 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
@ -492,9 +492,18 @@ def pvjury_table(
def formsemestre_pvjury(formsemestre_id, format="html", publish=True): def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
"""Page récapitulant les décisions de jury """Page récapitulant les décisions de jury"""
dpv: result of dict_pvjury
""" # Bretelle provisoire pour BUT 9.3.0
# XXX TODO
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0:
from app.but import jury_but_recap
return jury_but_recap.formsemestre_saisie_jury_but(
formsemestre, readonly=True, mode="recap"
)
# /XXX
footer = html_sco_header.sco_footer() footer = html_sco_header.sco_footer()
dpv = dict_pvjury(formsemestre_id, with_prev=True) dpv = dict_pvjury(formsemestre_id, with_prev=True)

View File

@ -446,8 +446,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
else: else:
params["decisions_ue_descr_plural"] = "" params["decisions_ue_descr_plural"] = ""
params["INSTITUTION_CITY"] = sco_preferences.get_preference( params["INSTITUTION_CITY"] = (
"INSTITUTION_CITY", formsemestre_id 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"]
@ -528,8 +528,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
sco_preferences.get_preference( sco_preferences.get_preference(
"PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id "PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id
) )
% params or ""
) ) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">') sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.makeParas( objects += sco_pdf.makeParas(
( (
@ -545,8 +545,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
sco_preferences.get_preference( sco_preferences.get_preference(
"PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id "PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
) )
% params or ""
) ) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">') sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.makeParas( objects += sco_pdf.makeParas(
( (
@ -731,7 +731,7 @@ def _pvjury_pdf_type(
""" """
% ( % (
titre_jury, titre_jury,
sco_preferences.get_preference("DeptName", formsemestre_id), sco_preferences.get_preference("DeptName", formsemestre_id) or "(sans nom)",
sem["anneescolaire"], sem["anneescolaire"],
), ),
style, style,
@ -761,7 +761,7 @@ def _pvjury_pdf_type(
objects += sco_pdf.makeParas( objects += sco_pdf.makeParas(
"<para>" "<para>"
+ sco_preferences.get_preference("PV_INTRO", formsemestre_id) + (sco_preferences.get_preference("PV_INTRO", formsemestre_id) or "")
% { % {
"Decnum": numeroArrete, "Decnum": numeroArrete,
"VDICode": VDICode, "VDICode": VDICode,

View File

@ -235,7 +235,7 @@ def module_tag_list(module_id=""):
def module_tag_set(module_id="", taglist=None): def module_tag_set(module_id="", taglist=None):
"""taglist may either be: """taglist may either be:
a string with tag names separated by commas ("un;deux") a string with tag names separated by commas ("un,deux")
or a list of strings (["un", "deux"]) or a list of strings (["un", "deux"])
""" """
if not taglist: if not taglist:
@ -243,7 +243,7 @@ def module_tag_set(module_id="", taglist=None):
elif isinstance(taglist, str): elif isinstance(taglist, str):
taglist = taglist.split(",") taglist = taglist.split(",")
taglist = [t.strip() for t in taglist] taglist = [t.strip() for t in taglist]
# log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist)) log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist))
# Sanity check: # Sanity check:
Mod = sco_edit_module.module_list(args={"module_id": module_id}) Mod = sco_edit_module.module_list(args={"module_id": module_id})
if not Mod: if not Mod:

View File

@ -151,10 +151,8 @@ def trombino_html(groups_infos):
if sco_photos.etud_photo_is_local(t, size="small"): if sco_photos.etud_photo_is_local(t, size="small"):
foto = sco_photos.etud_photo_html(t, title="") foto = sco_photos.etud_photo_html(t, title="")
else: # la photo n'est pas immédiatement dispo else: # la photo n'est pas immédiatement dispo
foto = ( foto = f"""<span class="unloaded_img" id="{t["etudid"]
'<span class="unloaded_img" id="%s"><img border="0" height="90" alt="en cours" src="/ScoDoc/static/icons/loading.jpg"/></span>' }"><img border="0" height="90" alt="en cours" src="{scu.STATIC_DIR}/icons/loading.jpg"/></span>"""
% t["etudid"]
)
H.append( H.append(
'<a href="%s">%s</a>' '<a href="%s">%s</a>'
% ( % (

View File

@ -180,7 +180,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
les évaluations du semestre. les évaluations du semestre.
""" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
r = ndb.SimpleDictFetch( rows = ndb.SimpleDictFetch(
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name, e.id as evaluation_id """SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name, e.id as evaluation_id
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi, FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
notes_modules mod, identite i, "user" u notes_modules mod, identite i, "user" u
@ -194,6 +194,10 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
""", """,
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
) )
# Formatte les notes
keep_numeric = format in scu.FORMATS_NUMERIQUES
for row in rows:
row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric)
columns_ids = ( columns_ids = (
"date", "date",
"code_nip", "code_nip",
@ -223,7 +227,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
tab = GenTable( tab = GenTable(
titles=titles, titles=titles,
columns_ids=columns_ids, columns_ids=columns_ids,
rows=r, rows=rows,
html_title="<h2>Saisies de notes dans %s</h2>" % sem["titreannee"], html_title="<h2>Saisies de notes dans %s</h2>" % sem["titreannee"],
html_class="table_leftalign table_coldate gt_table_searchable", html_class="table_leftalign table_coldate gt_table_searchable",
html_class_ignore_default=True, html_class_ignore_default=True,

View File

@ -63,6 +63,8 @@ from app.scodoc import sco_exceptions
from app.scodoc import sco_xml from app.scodoc import sco_xml
import sco_version import sco_version
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
STATIC_DIR = "/ScoDoc/static/links/" + sco_version.SCOVERSION
# ----- CALCUL ET PRESENTATION DES NOTES # ----- CALCUL ET PRESENTATION DES NOTES
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis
@ -111,6 +113,8 @@ MODULE_TYPE_NAMES = {
None: "Module", None: "Module",
} }
PARTITION_PARCOURS = "Parcours"
MALUS_MAX = 20.0 MALUS_MAX = 20.0
MALUS_MIN = -20.0 MALUS_MIN = -20.0
@ -397,6 +401,9 @@ XLSX_SUFFIX = ".xlsx"
XML_MIMETYPE = "text/xml" XML_MIMETYPE = "text/xml"
XML_SUFFIX = ".xml" XML_SUFFIX = ".xml"
# Format pour lesquels on exporte sans formattage des nombres (pas de perte de précision)
FORMATS_NUMERIQUES = {"csv", "xls", "xlsx", "xml", "json"}
def get_mime_suffix(format_code: str) -> tuple[str, str]: def get_mime_suffix(format_code: str) -> tuple[str, str]:
"""Returns (MIME, SUFFIX) from format_code == "xls", "xml", ... """Returns (MIME, SUFFIX) from format_code == "xls", "xml", ...
@ -863,7 +870,7 @@ def annee_scolaire_repr(year, month):
return "%s - %s" % (year - 1, year) return "%s - %s" % (year - 1, year)
def annee_scolaire_debut(year, month): def annee_scolaire_debut(year, month) -> int:
"""Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord...""" """Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord..."""
if int(month) > 7: if int(month) > 7:
return int(year) return int(year)
@ -952,12 +959,7 @@ def icontag(name, file_format="png", no_size=False, **attrs):
if "alt" not in attrs: if "alt" not in attrs:
attrs["alt"] = "logo %s" % name attrs["alt"] = "logo %s" % name
s = " ".join(['%s="%s"' % (k, attrs[k]) for k in attrs]) s = " ".join(['%s="%s"' % (k, attrs[k]) for k in attrs])
return '<img class="%s" %s src="/ScoDoc/static/icons/%s.%s" />' % ( return f'<img class="{name}" {s} src="{STATIC_DIR}/icons/{name}.{file_format}" />'
name,
s,
name,
file_format,
)
ICON_PDF = icontag("pdficon16x20_img", title="Version PDF") ICON_PDF = icontag("pdficon16x20_img", title="Version PDF")

164
app/static/css/jury_but.css Normal file
View File

@ -0,0 +1,164 @@
/* Saisie décision de jury BUT */
.jury_but form {
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
.jury_but .titre_parcours {
font-size: 130%;
padding-bottom: 12px;
}
.jury_but .nom_etud {
font-size: 100%;
font-weight: bold;
padding-bottom: 12px;
}
.but_annee {
display: inline-grid;
grid-template-columns: repeat(4, auto);
gap: 4px;
}
.but_annee_caption {
grid-column: 4 / 5;
}
.but_annee_caption,
.but_niveau_titre {
background: #09c !important;
color: #FFF;
padding: 8px !important;
}
.but_annee>* {
display: flex;
align-items: center;
padding: 0px 16px;
background: #FFF;
border: 1px solid #aaa;
border-radius: 8px;
}
.but_annee>div.titre {
background: rgb(242, 242, 238);
border: none;
border-radius: 0px;
border-bottom: 1px solid gray;
}
.but_niveau_ue>div:nth-child(1),
.but_note {
border-right: 1px solid #aaa;
padding: 8px;
}
.but_annee select {
padding: 8px 8px;
border: none;
}
.but_niveau_rcue,
.but_niveau_rcue>* {
border-color: #09c;
font-weight: bold;
}
div.but_section_annee {
margin-bottom: 10px;
}
div.but_settings {
margin-top: 16px;
}
span.but_explanation {
color: blueviolet;
font-style: italic;
}
select:disabled {
font-weight: bold;
color: blue;
}
select:invalid {
background: red;
}
select.but_code option.recorded {
color: rgb(3, 157, 3);
font-weight: bold;
}
div.but_niveau_ue.recorded,
div.but_niveau_rcue.recorded {
border-color: rgb(136, 252, 136);
border-width: 2px;
}
div.but_niveau_ue.modified {
background-color: rgb(255, 214, 254);
}
div.but_buttons {
margin-top: 16px;
}
div.but_buttons span {
margin-right: 16px;
}
div.but_doc_codes {
margin: 16px;
background-color: rgb(227, 254, 254);
font-size: 75%;
border: 2px solid rgb(4, 4, 118);
border-radius: 4px;
padding-left: 16px;
padding-right: 16px;
padding-bottom: 16px;
}
div.but_doc_section {
margin-top: 16px;
font-size: 125%;
font-weight: bold;
margin-bottom: 8px;
}
div.but_doc table {
border-collapse: collapse;
font-family: Tahoma, Geneva, sans-serif;
}
div.but_doc table td {
padding: 7px;
}
div.but_doc table thead td {
background-color: #54585d;
color: #ffffff;
font-weight: bold;
font-size: 13px;
border: 1px solid #54585d;
}
div.but_doc table tbody td {
color: #636363;
border: 1px solid #dddfe1;
}
div.but_doc table tbody tr {
background-color: #f9fafb;
}
div.but_doc table tbody tr:nth-child(odd) {
background-color: #ffffff;
}
div.but_doc table tr td.amue {
color: rgb(127, 127, 206);
font-size: 90%;
}

View File

@ -2066,6 +2066,10 @@ span.notes_module_list_buts {
margin-right: 5px; margin-right: 5px;
} }
.formation_apc_infos ul li:not(:last-child) {
margin-bottom: 6px;
}
div.ue_list_tit { div.ue_list_tit {
font-weight: bold; font-weight: bold;
margin-top: 5px; margin-top: 5px;
@ -2206,7 +2210,7 @@ ul.notes_module_list {
list-style-type: none; list-style-type: none;
} }
div#ue_list_modules { div.ue_choix_niveau {
background-color: rgb(191, 242, 255); background-color: rgb(191, 242, 255);
border: 1px solid blue; border: 1px solid blue;
border-radius: 10px; border-radius: 10px;
@ -2215,6 +2219,15 @@ div#ue_list_modules {
margin-right: 15px; margin-right: 15px;
} }
div#ue_list_modules {
background-color: rgb(251, 225, 165);
border: 1px solid blue;
border-radius: 10px;
padding: 10px;
margin-top: 10px;
margin-right: 15px;
}
div#ue_list_etud_validations { div#ue_list_etud_validations {
background-color: rgb(220, 250, 220); background-color: rgb(220, 250, 220);
padding-left: 4px; padding-left: 4px;
@ -2258,6 +2271,22 @@ span.missing_value {
color: red; color: red;
} }
span.code_parcours {
color: white;
background-color: rgb(254, 95, 246);
padding-left: 4px;
padding-right: 4px;
border-radius: 2px;
}
tr#tf_module_parcours>td {
background-color: rgb(229, 229, 229);
}
tr#tf_module_app_critiques>td {
background-color: rgb(194, 209, 228);
}
/* tableau recap notes */ /* tableau recap notes */
table.notes_recapcomplet { table.notes_recapcomplet {
border: 2px solid blue; border: 2px solid blue;
@ -2346,7 +2375,6 @@ td.recap_col_ue_inf {
padding-right: 1.2em; padding-right: 1.2em;
padding-left: 4px; padding-left: 4px;
text-align: left; text-align: left;
font-weight: bold;
color: rgb(255, 0, 0); color: rgb(255, 0, 0);
border-left: 1px solid blue; border-left: 1px solid blue;
} }
@ -2355,7 +2383,6 @@ td.recap_col_ue_val {
padding-right: 1.2em; padding-right: 1.2em;
padding-left: 4px; padding-left: 4px;
text-align: left; text-align: left;
font-weight: bold;
color: rgb(0, 140, 0); color: rgb(0, 140, 0);
border-left: 1px solid blue; border-left: 1px solid blue;
} }
@ -3626,6 +3653,13 @@ span.sco_tag_edit .tag-editor {
margin-top: 2px; margin-top: 2px;
} }
div.sco_tag_module_edit span.sco_tag_edit .tag-editor {
background-color: rgb(210, 210, 210);
border: 0px;
margin-left: 0px;
margin-top: 2px;
}
span.sco_tag_edit .tag-editor-delete { span.sco_tag_edit .tag-editor-delete {
height: 20px; height: 20px;
} }
@ -3741,6 +3775,34 @@ table.table_recap .group {
border-left: 1px solid blue; border-left: 1px solid blue;
} }
table.table_recap .col_ue {
font-weight: bold;
}
table.table_recap.jury .col_ue {
font-weight: normal;
}
table.table_recap.jury .col_rcue {
font-weight: bold;
}
table.table_recap.jury tr.even td.col_rcue {
background-color: #b0d4f8;
}
table.table_recap.jury tr.odd td.col_rcue {
background-color: #abcdef;
}
table.table_recap.jury tr.odd td.col_rcues_validables {
background-color: #e1d3c5 !important;
}
table.table_recap.jury tr.even td.col_rcues_validables {
background-color: #fcebda !important;
}
table.table_recap .group { table.table_recap .group {
border-left: 1px dashed rgb(160, 160, 160); border-left: 1px dashed rgb(160, 160, 160);
white-space: nowrap; white-space: nowrap;
@ -3767,6 +3829,12 @@ table.table_recap a:visited {
color: black; color: black;
} }
table.table_recap a.stdlink:link,
table.table_recap a.stdlink:visited {
color: blue;
text-decoration: underline;
}
table.table_recap tfoot th, table.table_recap tfoot th,
table.table_recap thead th { table.table_recap thead th {
text-align: left; text-align: left;
@ -3779,18 +3847,15 @@ table.table_recap td.moy_inf {
} }
table.table_recap td.moy_ue_valid { table.table_recap td.moy_ue_valid {
font-weight: bold;
color: rgb(0, 140, 0); color: rgb(0, 140, 0);
} }
table.table_recap td.moy_ue_warning { table.table_recap td.moy_ue_warning {
font-weight: bold;
color: rgb(255, 0, 0); color: rgb(255, 0, 0);
} }
table.table_recap td.col_ues_validables { table.table_recap td.col_ues_validables {
white-space: nowrap; white-space: nowrap;
font-weight: bold;
font-style: normal !important; font-style: normal !important;
} }
@ -3917,6 +3982,11 @@ table.table_recap td.evaluation.non_inscrit {
color: rgb(101, 101, 101); color: rgb(101, 101, 101);
} }
div.table_jury_but_links {
margin-top: 16px;
margin-bottom: 16px;
}
/* ------------- Tableau etat evals ------------ */ /* ------------- Tableau etat evals ------------ */
div.evaluations_recap table.evaluations_recap { div.evaluations_recap table.evaluations_recap {

View File

@ -25,10 +25,28 @@ function update_bonus_description() {
} }
function update_ue_list() { function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value; let ue_id = $("#tf_ue_id")[0].value;
var ue_code = $("#tf_ue_code")[0].value; let ue_code = $("#tf_ue_code")[0].value;
var query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id; let query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id;
$.get(query, '', function (data) { $.get(query, '', function (data) {
$("#ue_list_code").html(data); $("#ue_list_code").html(data);
}); });
} }
function set_ue_niveau_competence() {
let ue_id = document.querySelector("#tf_ue_id").value;
let select = document.querySelector("#form_ue_choix_niveau select");
let niveau_id = select.value;
let set_ue_niveau_competence_url = select.dataset.setter;
$.post(set_ue_niveau_competence_url,
{
ue_id: ue_id,
niveau_id: niveau_id,
},
function (result) {
alert("niveau de compétence enregistré"); // XXX #frontend à améliorer
// obj.classList.remove("sco_wait");
// obj.classList.add("sco_modified");
}
);
}

14
app/static/js/jury_but.js Normal file
View File

@ -0,0 +1,14 @@
// active les menus des codes "manuels" (année, RCUEs)
function enable_manual_codes(elt) {
$(".jury_but select.manual").prop("disabled", !elt.checked);
}
// changement menu code:
function change_menu_code(elt) {
elt.parentElement.parentElement.classList.remove("recorded");
// TODO: comparer avec valeur enregistrée (à mettre en data-orig ?)
// et colorer en fonction
elt.parentElement.parentElement.classList.add("modified");
}

View File

@ -0,0 +1,8 @@
/* Page édition module */
$(document).ready(function () {
});

View File

@ -35,7 +35,7 @@ function build_table(data) {
${cellule.data} ${cellule.data}
</div>`; </div>`;
if (cellule.editable) { if (cellule.style.includes("champs")) {
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + (parseFloat(cellule.data) || 0); sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + (parseFloat(cellule.data) || 0);
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + (parseFloat(cellule.data) || 0); sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + (parseFloat(cellule.data) || 0);
} }

View File

@ -1,6 +1,30 @@
// Tableau recap notes // Tableau recap notes
$(function () { $(function () {
$(function () { $(function () {
let hidden_colums = ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"];
let mode_jury_but_bilan = $('table.table_recap').hasClass("table_jury_but_bilan");
if (mode_jury_but_bilan) {
// table bilan décisions: cache les notes
hidden_colums = hidden_colums.concat(["col_ue", "col_rcue", "col_lien_saisie_but"]);
} else {
hidden_colums = hidden_colums.concat(["recorded_code"]);
}
// Etat (tri des colonnes) de la table:
const url = new URL(document.URL);
const formsemestre_id = url.searchParams.get("formsemestre_id");
const order_info_key = JSON.stringify([url.pathname, formsemestre_id]);
let order_info;
if (formsemestre_id) {
const x = localStorage.getItem(order_info_key);
if (x) {
try {
order_info = JSON.parse(x);
} catch (error) {
console.error(error);
}
}
}
// Les boutons dépendent du mode BUT ou classique: // Les boutons dépendent du mode BUT ou classique:
let buttons = [ let buttons = [
{ {
@ -22,6 +46,10 @@ $(function () {
dt.buttons('toggle_partitions:name').text(visible ? "Montrer groupes" : "Cacher les groupes"); dt.buttons('toggle_partitions:name').text(visible ? "Montrer groupes" : "Cacher les groupes");
} }
}, },
];
// Bouton "rangs groupes", sauf pour table jury BUT
if (!$('table.table_recap').hasClass("table_jury_but")) {
buttons.push(
{ {
name: "toggle_partitions_rangs", name: "toggle_partitions_rangs",
text: "Rangs groupes", text: "Rangs groupes",
@ -30,8 +58,21 @@ $(function () {
dt.columns(".partition_rangs").visible(!rangs_visible); dt.columns(".partition_rangs").visible(!rangs_visible);
dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes"); dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes");
} }
}, });
]; } else {
// table jury BUT: avec ou sans codes enregistrés
buttons.push(
{
name: "toggle_recorded_code",
text: "Code jury enregistrés",
action: function (e, dt, node, config) {
let visible = dt.columns(".recorded_code").visible()[0];
dt.columns(".recorded_code").visible(!visible);
dt.buttons('toggle_recorded_code:name').text(visible ? "Code jury enregistrés" : "Cacher codes jury");
}
});
}
if (!$('table.table_recap').hasClass("jury")) { if (!$('table.table_recap').hasClass("jury")) {
buttons.push( buttons.push(
$('table.table_recap').hasClass("apc") ? $('table.table_recap').hasClass("apc") ?
@ -80,6 +121,8 @@ $(function () {
} }
}) })
} }
// Boutons admission, sauf pour table jury BUT
if (!$('table.table_recap').hasClass("table_jury_but")) {
buttons.push({ buttons.push({
name: "toggle_admission", name: "toggle_admission",
text: "Montrer infos admission", text: "Montrer infos admission",
@ -88,7 +131,8 @@ $(function () {
dt.columns(".admission").visible(!visible); dt.columns(".admission").visible(!visible);
dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission"); dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission");
} }
}) });
}
$('table.table_recap').DataTable( $('table.table_recap').DataTable(
{ {
paging: false, paging: false,
@ -105,7 +149,7 @@ $(function () {
"columnDefs": [ "columnDefs": [
{ {
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
targets: ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"], targets: hidden_colums,
visible: false, visible: false,
}, },
{ {
@ -141,7 +185,15 @@ $(function () {
autoClose: true, autoClose: true,
buttons: buttons, buttons: buttons,
}, },
] ],
"drawCallback": function (settings) {
// permet de conserver l'ordre de tri des colonnes
let order_info = JSON.stringify($('table.table_recap').DataTable().order());
if (formsemestre_id) {
localStorage.setItem(order_info_key, order_info);
}
},
"order": order_info,
} }
); );
@ -155,10 +207,13 @@ $(function () {
$(this).addClass('selected'); $(this).addClass('selected');
} }
}); });
// Pour montrer et highlihter l'étudiant sélectionné: // Pour montrer et surligner l'étudiant sélectionné:
$(function () { $(function () {
document.querySelector("#row_selected").scrollIntoView(); let row_selected = document.querySelector("#row_selected");
if (row_selected) {
row_selected.scrollIntoView();
window.scrollBy(0, -50); window.scrollBy(0, -50);
document.querySelector("#row_selected").classList.add("selected"); row_selected.classList.add("selected");
}
}); });
}); });

View File

@ -0,0 +1,3 @@
Liens symboliques utilises pour les URL vers les fichiers statiques.
Le lien est cree par scodoc.py au lancement de l'application.

View File

@ -10,7 +10,7 @@
{% include 'bul_head.html' %} {% include 'bul_head.html' %}
<releve-but></releve-but> <releve-but></releve-but>
<script src="/ScoDoc/static/js/releve-but.js"></script> <script src="{{sco.scu.STATIC_DIR}}/js/releve-but.js"></script>
{% include 'bul_foot.html' %} {% include 'bul_foot.html' %}

View File

@ -0,0 +1,215 @@
<div class="but_doc_codes">
<p><em>Ci-dessous la signification de chaque code est expliquée,
ainsi que la correspondance avec les codes préconisés par
l'AMUE pour Apogée dans un document informel qui a circulé début
2022 (les éventuelles erreurs n'engagent personne).
</em></p>
<div class="but_doc_section">Codes d'année</div>
<div class="but_doc">
<table>
<tr>
<th>ScoDoc</th>
<th>{{nom_univ}}</th>
<th>AMUE</th>
<th>Signification</th>
</tr>
<tr>
<td>ADM</td>
<td>{{codes["ADM"]}}</td>
<td class="amue"></td>
<td>Admis</td>
</tr>
<tr>
<td>ADJ</td>
<td>{{codes["ADJ"]}}</td>
<td class="amue"></td>
<td>Admis par décision jury</td>
</tr>
<tr>
<td>PASD</td>
<td>{{codes["PASD"]}}</td>
<td class="amue">PASD</td>
<td>Non admis, mais passage de droit</td>
</tr>
<tr>
<td>PAS1NCI</td>
<td>{{codes["PAS1NCI"]}}</td>
<td class="amue">PAS1NCI</td>
<td>Non admis, mais passage par décision de jury (Passage en Année
Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE&lt;8))
</td>
</tr>
<tr>
<td>RED</td>
<td>{{codes["RED"]}}</td>
<td class="amue">RED</td>
<td>Ajourné, mais autorisé à redoubler</td>
</tr>
<tr>
<td>NAR</td>
<td>{{codes["NAR"]}}</td>
<td class="amue">REO</td>
<td>Non admis, réorientation</td>
</tr>
<tr>
<td>DEM</td>
<td>{{codes["DEM"]}}</td>
<td class="amue"></td>
<td>Démission</td>
</tr>
<tr>
<td>ABAN</td>
<td>{{codes["ABAN"]}}</td>
<td class="amue">ABAN</td>
<td>ABANdon constaté (sans lettre de démission)</td>
</tr>
<tr>
<td>RAT</td>
<td>{{codes["RAT"]}}</td>
<td class="amue"></td>
<td>En attente dun rattrapage</td>
</tr>
<tr>
<td>EXCLU</td>
<td>{{codes["EXCLU"]}}</td>
<td class="amue">EXC</td>
<td>EXClusion, décision réservée à des décisions disciplinaires</td>
</tr>
<tr>
<td>DEF</td>
<td>{{codes["DEF"]}}</td>
<td class="amue"></td>
<td>(défaillance) Non évalué par manque assiduité</td>
</tr>
<tr>
<td>ABL</td>
<td>{{codes["ABL"]}}</td>
<td class="amue">ABL</td>
<td>Année Blanche</td>
</tr>
</table>
</div>
<div class="but_doc_section">Codes RCUE (niveaux de compétences annuels)</div>
<div class="but_doc">
<table>
<tr>
<th>ScoDoc</th>
<th>{{nom_univ}}</th>
<th>AMUE</th>
<th>Signification</th>
</tr>
<tr>
<th>ADM</td>
<td>{{codes["ADM"]}}</td>
<th class="amue">VAL</td>
<th>Acquis</td>
</tr>
<tr>
<td>CMP</td>
<td>{{codes["CMP"]}}</td>
<td class="amue"></td>
<td>Acquis par compensation annuelle</td>
</tr>
<tr>
<td>ADJ</td>
<td>{{codes["ADJ"]}}</td>
<td class="amue">CODJ</td>
<td>Acquis par décision du jury</td>
</tr>
<tr>
<td>AJ</td>
<td>{{codes["AJ"]}}</td>
<td class="amue">AJ</td>
<td>Attente pour problème de moyenne</td>
</tr>
<tr>
<td>RAT</td>
<td>{{codes["RAT"]}}</td>
<td></td>
<td>En attente dun rattrapage</td>
</tr>
<tr>
<td>DEF</td>
<td>{{codes["DEF"]}}</td>
<td class="amue"></td>
<td>Défaillant</td>
</tr>
<tr>
<td>ABAN</td>
<td>{{codes["ABAN"]}}</td>
<td class="amue"></td>
<td>Non évalué pour manque assiduité</td>
</tr>
</table>
</div>
<div class="but_doc_section">Codes des Unités d'Enseignement (UE)</div>
<div class="but_doc">
<table>
<tr>
<th>ScoDoc</th>
<th>{{nom_univ}}</th>
<th>AMUE</th>
<th>Signification</th>
</tr>
<tr>
<td>ADM</td>
<td>{{codes["ADM"]}}</td>
<td class="amue">VAL</td>
<td>Acquis (ECTS acquis)</td>
</tr>
<tr>
<td>CMP</td>
<td>{{codes["CMP"]}}</td>
<td class="amue">COMP</td>
<td>Acquis par compensation UE compensée avec lUE de même compétence et de même année (ECTS acquis)
</td>
</tr>
<tr>
<td>ADJ</td>
<td>{{codes["ADJ"]}}</td>
<td class="amue"></td>
<td>Acquis par décision de jury (ECTS acquis)</td>
</tr>
<tr>
<td>AJ</td>
<td>{{codes["AJ"]}}</td>
<td class="amue">AJ</td>
<td>Attente pour problème de moyenne</td>
</tr>
<tr>
<td>RAT</td>
<td>{{codes["RAT"]}}</td>
<td class="amue"></td>
<td>En attente dun rattrapage</td>
</tr>
<tr>
<td>DEF</td>
<td>{{codes["DEF"]}}</td>
<td class="amue">ABAN</td>
<td>Défaillant Pas ou peu de notes par arrêt de la formation</td>
</tr>
<tr>
<td>ABAN</td>
<td>{{codes["ABAN"]}}</td>
<td class="amue">ABAN</td>
<td>Non évalué pour manque dassiduité Non présentation des notes de létudiant au jury</td>
</tr>
<tr>
<td>DEM</td>
<td>{{codes["DEM"]}}</td>
<td class="amue"></td>
<td>Démission</td>
</tr>
<tr>
<td>UEBSL</td>
<td>{{codes["UEBSL"]}}</td>
<td class="amue">UEBSL</td>
<td>UE blanchie </td>
</tr>
</table>
</div>
</div>

View File

@ -0,0 +1,30 @@
{# -*- mode: jinja-html -*- #}
{% extends "sco_page.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block styles %}
{{super()}}
{% endblock %}
{% block app_content %}
<h2>Calcul automatique des décisions de jury annuelle BUT</h2>
<ul>
<li>Seuls les étudiants qui valident l'année seront affectés:
tous les niveaux de compétences (RCUE) validables
(moyenne annuelle au dessus de 10);
</li>
<li>l'assiduité n'est <b>pas</b> prise en compte;</li>
</ul>
<p class="warning">
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
</p>
<div class="row">
<div class="col-md-5">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}

View File

@ -6,12 +6,26 @@
<h1>Associer un référentiel de compétences</h1> <h1>Associer un référentiel de compétences</h1>
<div class="help"> <div class="help">
Association d'un référentiel de compétence à la formation Association d'un référentiel de compétence à la formation
{{formation.titre}} ({{formation.acronyme}}) <a href="{{
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}}">{{formation.titre}} ({{formation.acronyme}})</a>
</div> </div>
<div class="row"> <div style="margin-top: 20px; margin-bottom: 20px;">
Référentiel actuellement associé:
{% if formation.referentiel_competence is not none %}
<b>{{ formation.referentiel_competence.specialite_long }}</b>
<a href="{{
url_for('notes.refcomp_desassoc_formation', scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}}" class="stdlink">supprimer</a>
{% else %}
<b>aucun</b>
{% endif %}
<div class="row" style="margin-top: 20px;">
<div class="col-md-4"> <div class="col-md-4">
{{ wtf.quick_form(form) }} {{ wtf.quick_form(form) }}
</div> </div>
</div>
</div> </div>
<div class="help"> <div class="help">

View File

@ -10,7 +10,7 @@
<ref-competences></ref-competences> <ref-competences></ref-competences>
<script src="/ScoDoc/static/js/ref_competences.js"></script> <script src="{{sco.scu.STATIC_DIR}}/js/ref_competences.js"></script>
<div class="help"> <div class="help">
Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à

View File

@ -6,12 +6,12 @@
<div class="help"> <div class="help">
<p>Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury <p>Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury
et les validations de semestres ou d'UE. les valeurs indiquées ici sont utilisées et les validations de semestres ou d'UE.
dans les exports Apogée. Les valeurs indiquées ici sont utilisées dans les exports Apogée.
<p> <p>
<p>Ne les modifier que si vous savez ce que vous faites ! <p>Ne les modifier que si vous savez ce que vous faites !
</p> </p>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">

View File

@ -0,0 +1,22 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h2>{{ title }}</h2>
<div style="margin-top: 16px;">
{{ explanation }}
</div>
<div style="margin-top: 16px;">
<form method="post">
<input type="submit" value="OK" />
{% if cancel_url %}
<input type="button" value="Annuler" style="margin-left: 16px;"
onClick="document.location='{{ cancel_url }}';" />
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -84,17 +84,29 @@
url_for("notes.module_create", url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
module_type=module_type|int, module_type=module_type|int,
matiere_id=matiere_parent.id matiere_id=matiere_parent.id,
semestre_id=semestre_id,
)}}" )}}"
{% else %}"{{ {% else %}"{{
url_for("notes.module_create", url_for("notes.module_create",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
module_type=module_type|int, module_type=module_type|int,
formation_id=formation.id formation_id=formation.id,
semestre_id=semestre_id,
)}}" )}}"
{% endif %} {% endif %}
>{{create_element_msg}}</a> >{{create_element_msg}}</a>
</li> </li>
{% if module_type==scu.ModuleType.STANDARD %}
<li><a href="{{
url_for('notes.formation_add_malus_modules',
scodoc_dept=g.scodoc_dept,
formation_id=formation.id,
semestre_id=semestre_id)
}}" class="stdlink">ajouter un module de malus dans chaque UE du S{{semestre_id}}</a>
</li>
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</ul> </ul>

View File

@ -2,10 +2,12 @@
<h2>{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs</h2> <h2>{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs</h2>
<div class="help"> <div class="help">
{% if not read_only %} {% if not read_only %}
Double-cliquer pour changer une valeur. <p>Double-cliquer pour changer une valeur.
Les valeurs sont automatiquement enregistrées au fur et à mesure. Les valeurs sont automatiquement enregistrées au fur et à mesure.
</p>
{% endif %} {% endif %}
<p>Chaque ligne représente une ressource ou SAÉ, et chaque colonne une Unité d'Enseignement (UE).
</p>
</div> </div>
<form class="semestre_selector">Semestre: <form class="semestre_selector">Semestre:
<select onchange="this.form.submit()"" name="semestre_idx" id="semestre_idx"> <select onchange="this.form.submit()"" name="semestre_idx" id="semestre_idx">

View File

@ -32,14 +32,30 @@
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}}</b> <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}}: {{
('pas de compétence associée'
if ue.niveau_competence is none
else 'compétence ' + ue.niveau_competence.annee + ' ' + ue.niveau_competence.competence.titre_long)
if ue.type == 0
else ''
}}"
>{{ue.titre}}</a> >{{ue.titre}}</a>
{% 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 -%}
{{ virg() }}{{ue.ects if ue.ects is not none {{ virg() }}
else '<span class="missing_ue_ects">aucun</span>'|safe}} ECTS) {%- if ue.type == 0 -%}
{{ue.ects
if ue.ects is not none
else '<span class="missing_ue_ects">aucun</span>'|safe
}} ECTS
{%- endif -%}
)
</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',

View File

@ -15,7 +15,7 @@
<li>Code: <tt>{{ue.ue_code}}</tt></li> <li>Code: <tt>{{ue.ue_code}}</tt></li>
<li>Type: {{ue.type}}</li> <li>Type: {{ue.type}}</li>
<li>Externe: {{ "oui" if ue.is_external else "non" }}</li> <li>Externe: {{ "oui" if ue.is_external else "non" }}</li>
<li>Code Apogée: {{ue.code_apogee}}</li> <li>Code Apogée: {{ue.code_apogee or "aucun"}}</li>
</ul> </ul>
</li> </li>
<li>Formation: <li>Formation:

View File

@ -4,13 +4,14 @@
{% block styles %} {% block styles %}
{{super()}} {{super()}}
<link type="text/css" rel="stylesheet" <link type="text/css" rel="stylesheet"
href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" /> href="{{sco.scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
<link rel="stylesheet" href="/ScoDoc/static/css/scodoc.css"> <link rel="stylesheet" href="{{sco.scu.STATIC_DIR}}/css/scodoc.css">
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" /> <link href="{{sco.scu.STATIC_DIR}}/css/menu.css" rel="stylesheet" type="text/css" />
<link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" /> <link href="{{sco.scu.STATIC_DIR}}/css/gt_table.css" rel="stylesheet" type="text/css" />
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" /> <link type="text/css" rel="stylesheet" href="{{sco.scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
{# <link href="/ScoDoc/static/css/tooltip.css" rel="stylesheet" type="text/css" /> #} {#
<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css" /> <link href="{{sco.scu.STATIC_DIR}}/css/tooltip.css" rel="stylesheet" type="text/css" /> #}
<link rel="stylesheet" type="text/css" href="{{sco.scu.STATIC_DIR}}/DataTables/datatables.min.css" />
{% endblock %} {% endblock %}
{% block title %} {% block title %}
@ -46,16 +47,16 @@
{{ super() }} {{ super() }}
{{ moment.include_moment() }} {{ moment.include_moment() }}
{{ moment.lang(g.locale) }} {{ moment.lang(g.locale) }}
<script src="/ScoDoc/static/libjs/menu.js"></script> <script src="{{sco.scu.STATIC_DIR}}/libjs/menu.js"></script>
<script src="/ScoDoc/static/libjs/bubble.js"></script> <script src="{{sco.scu.STATIC_DIR}}/libjs/bubble.js"></script>
<script src="/ScoDoc/static/jQuery/jquery.js"></script> <script src="{{sco.scu.STATIC_DIR}}/jQuery/jquery.js"></script>
<script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script> <script src="{{sco.scu.STATIC_DIR}}/jQuery/jquery-migrate-1.2.0.min.js"></script>
<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script> <script src="{{sco.scu.STATIC_DIR}}/libjs/jquery.field.min.js"></script>
<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script> <script src="{{sco.scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script> <script src="{{sco.scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<script src="/ScoDoc/static/js/scodoc.js"></script> <script src="{{sco.scu.STATIC_DIR}}/js/scodoc.js"></script>
<script src="/ScoDoc/static/DataTables/datatables.min.js"></script> <script src="{{sco.scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
<script> <script>
window.onload = function () { enableTooltips("gtrcontent") }; window.onload = function () { enableTooltips("gtrcontent") };

View File

@ -1,13 +1,13 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{{ sco_header|safe }} {{ sco_header|safe }}
<h2 class="formsemestre">Affectation aux groupes de {{ partition["partition_name"] }}</h2> <h2 class="formsemestre">Affectation aux groupes de {{ partition.partition_name }}</h2>
<p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne <p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne
sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>". sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>".
Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien
"suppr." en haut à droite de sa boite. "suppr." en haut à droite de sa boite.
Vous pouvez aussi <a class="stdlink" Vous pouvez aussi <a class="stdlink"
href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, partition_id=partition['partition_id']) }}" href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, partition_id=partition.id) }}"
>répartir automatiquement les groupes</a>. >répartir automatiquement les groupes</a>.
</p> </p>
@ -15,24 +15,24 @@ href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, pa
<div id="ginfo"></div> <div id="ginfo"></div>
<div id="savedinfo"></div> <div id="savedinfo"></div>
<form name="formGroup" id="formGroup" onSubmit="return false;"> <form name="formGroup" id="formGroup" onSubmit="return false;">
<input type="hidden" name="partition_id" value="{{ partition['partition_id'] }}"/> <input type="hidden" name="partition_id" value="{{ partition.id }}"/>
<input name="groupName" size="6"/> <input name="groupName" size="6"/>
<input type="button" onClick="createGroup();" value="Créer groupe"/> <input type="button" onClick="createGroup();" value="Créer groupe"/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" /> <input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" <input type="button"
onClick="document.location = '{{ url_for( 'notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }}'" onClick="document.location = '{{ url_for( 'notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }}'"
value="Annuler" />&nbsp;&nbsp;&nbsp;&nbsp;Éditer groupes de value="Annuler" />&nbsp;&nbsp;&nbsp;&nbsp;Éditer groupes de
<select name="other_partition_id" onchange="GotoAnother();"> <select name="other_partition_id" onchange="GotoAnother();">
{% for p in partitions_list %} {% for p in partitions_list %}
<option value="{{ p['id'] }}" {{ <option value="{{ p.id }}" {{
"selected" if p['partition_id'] == partition['partition_id'] "selected" if p.id == partition.id
}}>{{ }}>{{
p["partition_name"] p.partition_name
}}</option> }}</option>
{% endfor %} {% endfor %}
</select> </select>
</form> </form>
<div id="groups"> <div id="groups">

View File

@ -31,6 +31,7 @@ Module notes: issu de ScoDoc7 / ZNotes.py
Emmanuel Viennet, 2021 Emmanuel Viennet, 2021
""" """
import html
from operator import itemgetter from operator import itemgetter
import time import time
from xml.etree import ElementTree from xml.etree import ElementTree
@ -39,10 +40,14 @@ import flask
from flask import abort, flash, jsonify, redirect, render_template, url_for from flask import abort, flash, jsonify, redirect, render_template, url_for
from flask import current_app, g, request from flask import current_app, g, request
from flask_login import current_user from flask_login import current_user
from werkzeug.utils import redirect
from app.but import jury_but, jury_but_validation_auto
from app.but.forms import jury_but_forms
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models.config import ScoDocSiteConfig
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.models.formsemestre import FormSemestreUEComputationExpr from app.models.formsemestre import FormSemestreUEComputationExpr
from app.models.modules import Module from app.models.modules import Module
@ -53,7 +58,7 @@ from app import db
from app import models from app import models
from app.models import ScolarNews from app.models import ScolarNews
from app.auth.models import User from app.auth.models import User
from app.but import bulletin_but from app.but import apc_edit_ue, bulletin_but, jury_but_recap
from app.decorators import ( from app.decorators import (
scodoc, scodoc,
scodoc7func, scodoc7func,
@ -416,6 +421,16 @@ sco_publish(
) )
@bp.route("/set_ue_niveau_competence", methods=["POST"])
@scodoc
@permission_required(Permission.ScoChangeFormation)
def set_ue_niveau_competence():
"associe UE et niveau"
ue_id = request.form.get("ue_id")
niveau_id = request.form.get("niveau_id")
return apc_edit_ue.set_ue_niveau_competence(ue_id, niveau_id)
@bp.route("/ue_list") # backward compat @bp.route("/ue_list") # backward compat
@bp.route("/ue_table") @bp.route("/ue_table")
@scodoc @scodoc
@ -534,12 +549,17 @@ sco_publish(
) )
sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView) sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView)
sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView)
sco_publish(
"/module_tag_set",
sco_tag_module.module_tag_set, @bp.route("/module_tag_set", methods=["POST"])
Permission.ScoEditFormationTags, @scodoc
methods=["GET", "POST"], @permission_required(Permission.ScoEditFormationTags)
) def module_tag_set():
"""Set tags on module"""
module_id = int(request.form.get("module_id"))
taglist = request.form.get("taglist")
return sco_tag_module.module_tag_set(module_id, taglist)
# #
@bp.route("/") @bp.route("/")
@ -2195,6 +2215,271 @@ def formsemestre_validation_etud_manu(
) )
# --- Jurys BUT
@bp.route(
"/formsemestre_validation_but/<int:formsemestre_id>/<int:etudid>",
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_validation_but(formsemestre_id: int, etudid: int):
"Form. saisie décision jury semestre BUT"
if not sco_permissions_check.can_validate_sem(formsemestre_id):
return scu.confirm_dialog(
message=f"<p>Opération non autorisée pour {current_user}</h2>",
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
)
H = [
html_sco_header.sco_header(
page_title="Validation BUT",
formsemestre_id=formsemestre_id,
etudid=etudid,
cssstyles=("css/jury_but.css",),
javascripts=("js/jury_but.js",),
),
f"""
<div class="jury_but">
""",
]
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
etud = Identite.query.get_or_404(etudid)
if formsemestre.etuds_inscriptions[etudid].etat != scu.INSCRIT:
return (
"\n".join(H)
+ f"""<div class="warning">Impossible de statuer sur cet étudiant:
il est démissionnaire ou défaillant (voir <a href="{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">sa fiche</a>)
</div>
<div><a href="{url_for(
"notes.formsemestre_saisie_jury", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, selected_etudid=etud.id
)}">retour à la liste</a></div>
</div>
"""
+ html_sco_header.sco_footer()
)
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if len(deca.rcues_annee) == 0:
raise ScoValueError("année incomplète: pas de jury BUT annuel possible")
if request.method == "POST":
deca.record_form(request.form)
flash("codes enregistrés")
return flask.redirect(
url_for(
"notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
)
if deca.code_valide:
erase_span = f"""<a href="{
url_for("notes.formsemestre_jury_but_erase",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
etudid=etudid)}" class="stdlink">effacer décisions</a>"""
else:
erase_span = ""
warning = ""
if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau):
warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)}
niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>"""
if deca.parcour is None:
warning += """<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>"""
H.append(
f"""
<div>
<div class="titre_parcours">Jury BUT{deca.annee_but}
- Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"}
- {deca.annee_scolaire_str()}</div>
<div class="nom_etud">{etud.nomprenom}</div>
{warning}
</div>
<form method="POST">
<div class="but_section_annee">
<div>
<b>Décision de jury pour l'année :</b> {
_gen_but_select("code_annee", deca.codes, deca.code_valide,
disabled=True, klass="manual")
}
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
<span>{erase_span}</span>
</div>
<span class="but_explanation">{deca.explanation}</span>
</div>
<div><b>Niveaux de compétences et unités d'enseignement :</b></div>
<div class="but_annee">
<div class="titre"></div>
<div class="titre">S{1}</div>
<div class="titre">S{2}</div>
<div class="titre">RCUE</div>
"""
)
for niveau in deca.niveaux_competences:
H.append(
f"""<div class="but_niveau_titre">
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
</div>"""
)
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id)
if dec_rcue is None:
break
# Semestre impair
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_1,
dec_rcue.rcue.moy_ue_1,
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
)
)
# Semestre pair
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_2,
dec_rcue.rcue.moy_ue_2,
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
)
)
# RCUE
H.append(
f"""<div class="but_niveau_rcue
{'recorded' if dec_rcue.code_valide is not None else ''}
">
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
<div class="but_code">{
_gen_but_select("code_rcue_"+str(niveau.id),
dec_rcue.codes,
dec_rcue.code_valide,
disabled=True, klass="manual"
)
}</div>
</div>"""
)
H.append("</div>") # but_annee
H.append(
f"""<div class="but_settings">
<input type="checkbox" onchange="enable_manual_codes(this)">
<em>permettre la saisie manuelles des codes d'année et de niveaux.
Dans ce cas, il vous revient de vous assurer de la cohérence entre
vos codes d'UE/RCUE/Année !</em>
</input>
</div>
<div class="but_buttons">
<span><input type="submit" value="Enregistrer ces décisions"></span>
<span><a href="{url_for(
"notes.formsemestre_saisie_jury", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, selected_etudid=etud.id
)}">retour à la liste</a></span>
</div>
"""
)
H.append("</form>") # but_annee
H.append(
render_template(
"but/documentation_codes_jury.html",
nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
or sco_preferences.get_preference("UnivName")
or "Apogée"}""",
codes=ScoDocSiteConfig.get_codes_apo_dict(),
)
)
return "\n".join(H) + html_sco_header.sco_footer()
def _gen_but_select(
name: str,
codes: list[str],
code_valide: str,
disabled: bool = False,
klass: str = "",
) -> str:
"Le menu html select avec les codes"
h = "\n".join(
[
f"""<option value="{code}"
{'selected' if code == code_valide else ''}
class="{'recorded' if code == code_valide else ''}"
>{code}</option>"""
for code in codes
]
)
return f"""<select required name="{name}"
class="but_code {klass}"
onchange="change_menu_code(this);"
{"disabled" if disabled else ""}
>{h}</select>
"""
def _gen_but_niveau_ue(
ue: UniteEns, moy_ue: float, dec_ue: jury_but.DecisionsProposeesUE
):
return f"""<div class="but_niveau_ue {
'recorded' if dec_ue.code_valide is not None else ''}
">
<div title="{ue.titre}">{ue.acronyme}</div>
<div class="but_note">{scu.fmt_note(moy_ue)}</div>
<div class="but_code">{
_gen_but_select("code_ue_"+str(ue.id),
dec_ue.codes,
dec_ue.code_valide
)
}</div>
</div>"""
@bp.route(
"/formsemestre_validation_auto_but/<int:formsemestre_id>", methods=["GET", "POST"]
)
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_validation_auto_but(formsemestre_id: int = None):
"Saisie automatique des décisions de jury BUT"
if not sco_permissions_check.can_validate_sem(formsemestre_id):
return scu.confirm_dialog(
message=f"<p>Opération non autorisée pour {current_user}</h2>",
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
),
)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
form = jury_but_forms.FormSemestreValidationAutoBUTForm()
if request.method == "POST":
if not form.cancel.data:
nb_admis = jury_but_validation_auto.formsemestre_validation_auto_but(
formsemestre
)
flash(f"Décisions enregistrées ({nb_admis} admis)")
return redirect(
url_for(
"notes.formsemestre_saisie_jury",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
return render_template(
"but/formsemestre_validation_auto_but.html",
form=form,
sco=ScoData(formsemestre=formsemestre),
title=f"Calcul automatique jury BUT",
)
@bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) @bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@ -2344,6 +2629,85 @@ def formsemestre_validation_suppress_etud(
# ------------- PV de JURY et archives # ------------- PV de JURY et archives
sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.ScoView) sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.ScoView)
@bp.route("/formsemestre_saisie_jury")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
"""Page de saisie: liste des étudiants et lien vers page jury
en semestres pairs de BUT, table spécifique avec l'année
sinon, redirect vers page recap en mode jury
"""
readonly = not sco_permissions_check.can_validate_sem(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0:
return jury_but_recap.formsemestre_saisie_jury_but(
formsemestre, readonly, selected_etudid=selected_etudid
)
return redirect(
url_for(
"notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
modejury=1,
)
)
@bp.route("/formsemestre_jury_but_recap")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_jury_but_recap(formsemestre_id: int, selected_etudid: int = None):
"""Tableau affichage des codes"""
readonly = not sco_permissions_check.can_validate_sem(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not (formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0):
raise ScoValueError(
"formsemestre_jury_but_recap: réservé aux semestres pairs de BUT"
)
return jury_but_recap.formsemestre_saisie_jury_but(
formsemestre, readonly=readonly, selected_etudid=selected_etudid, mode="recap"
)
@bp.route(
"/formsemestre_jury_but_erase/<int:formsemestre_id>/<int:etudid>",
methods=["GET", "POST"],
)
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
"""Supprime la décision de jury BUT pour cette année"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.formation.is_apc():
raise ScoValueError("semestre non BUT")
etud: Identite = Identite.query.get_or_404(etudid)
if not sco_permissions_check.can_validate_sem(formsemestre_id):
raise ScoValueError("opération non autorisée")
dest_url = url_for(
"notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
)
if request.method == "POST":
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
deca.erase()
db.session.commit()
flash("décisions de jury effacées")
return redirect(dest_url)
return render_template(
"confirm_dialog.html",
title=f"Effacer les validations de jury de {etud.nomprenom} ?",
explanation="""Les validations de toutes les UE, RCUE (compétences) et année seront effacées.""",
cancel_url=dest_url,
)
sco_publish( sco_publish(
"/formsemestre_lettres_individuelles", "/formsemestre_lettres_individuelles",
sco_pvjury.formsemestre_lettres_individuelles, sco_pvjury.formsemestre_lettres_individuelles,

View File

@ -160,6 +160,20 @@ def refcomp_assoc_formation(formation_id: int):
) )
@bp.route("/referentiel/comp/desassoc_formation/<int:formation_id>", methods=["GET"])
@scodoc
@permission_required(Permission.ScoChangeFormation)
def refcomp_desassoc_formation(formation_id: int):
"""Désassocie la formation de son ref. de compétence"""
formation = Formation.query.get_or_404(formation_id)
formation.referentiel_competence = None
db.session.add(formation)
db.session.commit()
return redirect(
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id)
)
@bp.route( @bp.route(
"/referentiel/comp/load", defaults={"formation_id": None}, methods=["GET", "POST"] "/referentiel/comp/load", defaults={"formation_id": None}, methods=["GET", "POST"]
) )
@ -183,10 +197,11 @@ def refcomp_load(formation_id=None):
refs_distrib_dict = [{"id": 0, "specialite": "Aucun", "created": "", "serial": ""}] refs_distrib_dict = [{"id": 0, "specialite": "Aucun", "created": "", "serial": ""}]
i = 1 i = 1
for filename in refs_distrib_files: for filename in refs_distrib_files:
m = re.match(r".*/but-([A-Za-z_]+)-([0-9]+)-([0-9]+).xml", str(filename)) m = re.match(r".*/but-([A-Za-z0-9_]+)-([0-9]+)-([0-9]+).xml", str(filename))
if ( if not m:
m log(f"refcomp_load: ignoring {filename} (invalid filename)")
and ApcReferentielCompetences.query.filter_by( elif (
ApcReferentielCompetences.query.filter_by(
scodoc_orig_filename=Path(filename).name, dept_id=g.scodoc_dept_id scodoc_orig_filename=Path(filename).name, dept_id=g.scodoc_dept_id
).count() ).count()
== 0 == 0
@ -202,7 +217,7 @@ def refcomp_load(formation_id=None):
) )
i += 1 i += 1
else: else:
log(f"refcomp_load: ignoring {filename} (invalid filename)") log(f"refcomp_load: ignoring {filename} (already loaded)")
form = RefCompLoadForm() form = RefCompLoadForm()
form.referentiel_standard.choices = [ form.referentiel_standard.choices = [

View File

@ -54,6 +54,7 @@ from app.decorators import (
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.etudiants import make_etud_args from app.models.etudiants import make_etud_args
from app.models.events import ScolarNews from app.models.events import ScolarNews
from app.models.formsemestre import FormSemestre
from app.views import scolar_bp as bp from app.views import scolar_bp as bp
from app.views import ScoData from app.views import ScoData
@ -860,8 +861,8 @@ sco_publish(
) )
sco_publish( sco_publish(
"/editPartitionForm", "/edit_partition_form",
sco_groups.editPartitionForm, sco_groups.edit_partition_form,
Permission.ScoView, Permission.ScoView,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
@ -904,21 +905,27 @@ sco_publish(
sco_publish( sco_publish(
"/partition_create", "/partition_create",
sco_groups.partition_create, sco_groups.partition_create,
Permission.ScoView, Permission.ScoView, # controle d'access ad-hoc
methods=["GET", "POST"], methods=["GET", "POST"],
) )
# @bp.route("/partition_create", methods=["GET", "POST"])
# @scodoc
# @permission_required(Permission.ScoView) @bp.route("/create_partition_parcours", methods=["GET", "POST"])
# @scodoc7func @scodoc
# def partition_create( @permission_required(Permission.ScoView)
# @scodoc7func
# formsemestre_id, def create_partition_parcours(formsemestre_id):
# partition_name="", """Création d'une partitions nommée "Parcours" (PARTITION_PARCOURS)
# default=False, avec un groupe par parcours."""
# numero=None, formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
# redirect=1): formsemestre.setup_parcours_groups()
# return sco_groups.partition_create( formsemestre_id, return flask.redirect(
url_for(
"scolar.edit_partition_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
sco_publish("/etud_info_html", sco_page_etud.etud_info_html, Permission.ScoView) sco_publish("/etud_info_html", sco_page_etud.etud_info_html, Permission.ScoView)

View File

@ -0,0 +1,76 @@
"""fix_calais
Revision ID: 3c31bb0b27c9
Revises: d5b3bdd1d622
Create Date: 2022-06-23 10:48:27.787550
Pour réparer les bases auxquelles il manquerait le ref. de comp.:
bug dit "de Calais"
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "3c31bb0b27c9"
down_revision = "d5b3bdd1d622"
branch_labels = None
depends_on = None
# Voir https://stackoverflow.com/questions/24082542/check-if-a-table-column-exists-in-the-database-using-sqlalchemy-and-alembic
from sqlalchemy import inspect
def column_exists(table_name, column_name):
bind = op.get_context().bind
insp = inspect(bind)
columns = insp.get_columns(table_name)
return any(c["name"] == column_name for c in columns)
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
if not column_exists("apc_competence", "id_orebut"):
op.add_column(
"apc_competence", sa.Column("id_orebut", sa.Text(), nullable=True)
)
op.create_index(
op.f("ix_apc_competence_id_orebut"),
"apc_competence",
["id_orebut"],
unique=False,
)
op.drop_constraint(
"apc_competence_referentiel_id_titre_key", "apc_competence", type_="unique"
)
if not column_exists("apc_referentiel_competences", "annexe"):
op.add_column(
"apc_referentiel_competences", sa.Column("annexe", sa.Text(), nullable=True)
)
if not column_exists("apc_referentiel_competences", "type_structure"):
op.add_column(
"apc_referentiel_competences",
sa.Column("type_structure", sa.Text(), nullable=True),
)
if not column_exists("apc_referentiel_competences", "type_departement"):
op.add_column(
"apc_referentiel_competences",
sa.Column("type_departement", sa.Text(), nullable=True),
)
if not column_exists("apc_referentiel_competences", "version_orebut"):
op.add_column(
"apc_referentiel_competences",
sa.Column("version_orebut", sa.Text(), nullable=True),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
# ne fait rien ! ce script est idempotent
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,128 @@
"""Validations BUT
Revision ID: 4311cc342dbd
Revises: a2771105c21c
Create Date: 2022-05-28 16:46:09.861248
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "4311cc342dbd"
down_revision = "a2771105c21c"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"apc_validation_annee",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("etudid", sa.Integer(), nullable=False),
sa.Column("ordre", sa.Integer(), nullable=False),
sa.Column("formsemestre_id", sa.Integer(), nullable=True),
sa.Column("annee_scolaire", sa.Integer(), nullable=False),
sa.Column(
"date",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("code", sa.String(length=16), nullable=False),
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["formsemestre_id"],
["notes_formsemestre.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("etudid", "annee_scolaire"),
)
op.create_index(
op.f("ix_apc_validation_annee_code"),
"apc_validation_annee",
["code"],
unique=False,
)
op.create_index(
op.f("ix_apc_validation_annee_etudid"),
"apc_validation_annee",
["etudid"],
unique=False,
)
op.create_table(
"apc_validation_rcue",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("etudid", sa.Integer(), nullable=False),
sa.Column("formsemestre_id", sa.Integer(), nullable=True),
sa.Column("ue1_id", sa.Integer(), nullable=False),
sa.Column("ue2_id", sa.Integer(), nullable=False),
sa.Column("parcours_id", sa.Integer(), nullable=True),
sa.Column(
"date",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("code", sa.String(length=16), nullable=False),
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["formsemestre_id"],
["notes_formsemestre.id"],
),
sa.ForeignKeyConstraint(
["parcours_id"],
["apc_parcours.id"],
),
sa.ForeignKeyConstraint(
["ue1_id"],
["notes_ue.id"],
),
sa.ForeignKeyConstraint(
["ue2_id"],
["notes_ue.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"),
)
op.create_index(
op.f("ix_apc_validation_rcue_code"),
"apc_validation_rcue",
["code"],
unique=False,
)
op.create_index(
op.f("ix_apc_validation_rcue_etudid"),
"apc_validation_rcue",
["etudid"],
unique=False,
)
op.create_index(
op.f("ix_apc_validation_rcue_formsemestre_id"),
"apc_validation_rcue",
["formsemestre_id"],
unique=False,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_apc_validation_rcue_formsemestre_id"), table_name="apc_validation_rcue"
)
op.drop_index(
op.f("ix_apc_validation_rcue_etudid"), table_name="apc_validation_rcue"
)
op.drop_index(op.f("ix_apc_validation_rcue_code"), table_name="apc_validation_rcue")
op.drop_table("apc_validation_rcue")
op.drop_index(
op.f("ix_apc_validation_annee_etudid"), table_name="apc_validation_annee"
)
op.drop_index(
op.f("ix_apc_validation_annee_code"), table_name="apc_validation_annee"
)
op.drop_table("apc_validation_annee")
# ### end Alembic commands ###

View File

@ -0,0 +1,93 @@
"""assoc UE - Niveau
Revision ID: 6002d7d366e5
Revises: af77ca6a89d0
Create Date: 2022-04-26 12:58:32.929910
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "6002d7d366e5"
down_revision = "af77ca6a89d0"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"notes_ue", sa.Column("niveau_competence_id", sa.Integer(), nullable=True)
)
op.create_foreign_key(
"notes_ue_niveau_competence_id_fkey",
"notes_ue",
"apc_niveau",
["niveau_competence_id"],
["id"],
)
op.create_table(
"parcours_modules",
sa.Column("parcours_id", sa.Integer(), nullable=False),
sa.Column("module_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["module_id"],
["notes_modules.id"],
# nom ajouté manuellement:
name="parcours_modules_module_id_fkey",
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["parcours_id"],
["apc_parcours.id"],
# nom ajouté manuellement:
name="parcours_modules_parcours_id_fkey",
),
sa.PrimaryKeyConstraint("parcours_id", "module_id"),
)
op.alter_column(
"apc_modules_acs", "module_id", existing_type=sa.INTEGER(), nullable=False
)
op.alter_column(
"apc_modules_acs", "app_crit_id", existing_type=sa.INTEGER(), nullable=False
)
op.drop_constraint(
"apc_modules_acs_module_id_fkey", "apc_modules_acs", type_="foreignkey"
)
op.create_foreign_key(
"apc_modules_acs_module_id_fkey",
"apc_modules_acs",
"notes_modules",
["module_id"],
["id"],
ondelete="CASCADE",
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
"notes_ue_niveau_competence_id_fkey", "notes_ue", type_="foreignkey"
)
op.drop_column("notes_ue", "niveau_competence_id")
op.drop_table("parcours_modules")
op.drop_constraint(
"apc_modules_acs_module_id_fkey", "apc_modules_acs", type_="foreignkey"
)
op.create_foreign_key(
"apc_modules_acs_module_id_fkey",
"apc_modules_acs",
"notes_modules",
["module_id"],
["id"],
)
op.alter_column(
"apc_modules_acs", "app_crit_id", existing_type=sa.INTEGER(), nullable=True
)
op.alter_column(
"apc_modules_acs", "module_id", existing_type=sa.INTEGER(), nullable=True
)
# ### end Alembic commands ###

View File

@ -0,0 +1,318 @@
"""Formsemestre / parcours, Inscriptions aux parcours + cascades sur Identite
Revision ID: a2771105c21c
Revises: 6002d7d366e5
Create Date: 2022-05-25 20:32:06.868296
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a2771105c21c"
down_revision = "6002d7d366e5"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"parcours_formsemestre",
sa.Column("parcours_id", sa.Integer(), nullable=False),
sa.Column("formsemestre_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["formsemestre_id"], ["notes_formsemestre.id"], ondelete="CASCADE"
),
sa.ForeignKeyConstraint(
["parcours_id"],
["apc_parcours.id"],
),
sa.PrimaryKeyConstraint("parcours_id", "formsemestre_id"),
)
op.drop_constraint("absences_etudid_fkey", "absences", type_="foreignkey")
op.create_foreign_key(
"absences_etudid_fkey",
"absences",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
"absences_notifications_etudid_fkey",
"absences_notifications",
type_="foreignkey",
)
op.create_foreign_key(
"absences_notifications_etudid_fkey",
"absences_notifications",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("admissions_etudid_fkey", "admissions", type_="foreignkey")
op.create_foreign_key(
"admissions_etudid_fkey",
"admissions",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("adresse_etudid_fkey", "adresse", type_="foreignkey")
op.create_foreign_key(
"adresse_etudid_fkey",
"adresse",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
"group_membership_etudid_fkey", "group_membership", type_="foreignkey"
)
op.create_foreign_key(
"group_membership_etudid_fkey",
"group_membership",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("itemsuivi_etudid_fkey", "itemsuivi", type_="foreignkey")
op.create_foreign_key(
"itemsuivi_etudid_fkey",
"itemsuivi",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
"notes_appreciations_etudid_fkey", "notes_appreciations", type_="foreignkey"
)
op.create_foreign_key(
"notes_appreciations_etudid_fkey",
"notes_appreciations",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
# GROUPES
op.add_column(
"partition",
sa.Column(
"groups_editable", sa.Boolean(), server_default="true", nullable=False
),
)
# INSCRIPTIONS
op.drop_constraint(
"notes_formsemestre_inscription_etudid_fkey",
"notes_formsemestre_inscription",
type_="foreignkey",
)
op.create_foreign_key(
"notes_formsemestre_inscription_etudid_fkey",
"notes_formsemestre_inscription",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.add_column(
"notes_formsemestre_inscription",
sa.Column("parcour_id", sa.Integer(), nullable=True),
)
op.create_index(
op.f("ix_notes_formsemestre_inscription_parcour_id"),
"notes_formsemestre_inscription",
["parcour_id"],
unique=False,
)
op.create_foreign_key(
"notes_formsemestre_inscription_parcour_id_fkey",
"notes_formsemestre_inscription",
"apc_parcours",
["parcour_id"],
["id"],
)
# ---
op.drop_constraint("notes_notes_etudid_fkey", "notes_notes", type_="foreignkey")
op.create_foreign_key(
"notes_notes_etudid_fkey",
"notes_notes",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
"notes_notes_log_etudid_fkey", "notes_notes_log", type_="foreignkey"
)
op.create_foreign_key(
"notes_notes_log_etudid_fkey",
"notes_notes_log",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
"scolar_autorisation_inscription_etudid_fkey",
"scolar_autorisation_inscription",
type_="foreignkey",
)
op.create_foreign_key(
"scolar_autorisation_inscription_etudid_fkey",
"scolar_autorisation_inscription",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint("scolar_events_etudid_fkey", "scolar_events", type_="foreignkey")
op.create_foreign_key(
"scolar_events_etudid_fkey",
"scolar_events",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
"scolar_formsemestre_validation_etudid_fkey",
"scolar_formsemestre_validation",
type_="foreignkey",
)
op.create_foreign_key(
"scolar_formsemestre_validation_etudid_fkey",
"scolar_formsemestre_validation",
"identite",
["etudid"],
["id"],
ondelete="CASCADE",
)
# ### end Alembic commands ###
# --------------------------------------------------------------
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
"scolar_formsemestre_validation_etudid_fkey",
"scolar_formsemestre_validation",
type_="foreignkey",
)
op.create_foreign_key(
"scolar_formsemestre_validation_etudid_fkey",
"scolar_formsemestre_validation",
"identite",
["etudid"],
["id"],
)
op.drop_constraint("scolar_events_etudid_fkey", "scolar_events", type_="foreignkey")
op.create_foreign_key(
"scolar_events_etudid_fkey", "scolar_events", "identite", ["etudid"], ["id"]
)
op.drop_constraint(
"scolar_autorisation_inscription_etudid_fkey",
"scolar_autorisation_inscription",
type_="foreignkey",
)
op.create_foreign_key(
"scolar_autorisation_inscription_etudid_fkey",
"scolar_autorisation_inscription",
"identite",
["etudid"],
["id"],
)
op.drop_constraint(
"notes_notes_log_etudid_fkey", "notes_notes_log", type_="foreignkey"
)
op.create_foreign_key(
"notes_notes_log_etudid_fkey", "notes_notes_log", "identite", ["etudid"], ["id"]
)
op.drop_constraint("notes_notes_etudid_fkey", "notes_notes", type_="foreignkey")
op.create_foreign_key(
"notes_notes_etudid_fkey", "notes_notes", "identite", ["etudid"], ["id"]
)
# GROUPES
op.drop_column("partition", "groups_editable")
# INSCRIPTIONS
op.drop_constraint(
"notes_formsemestre_inscription_etudid_fkey",
"notes_formsemestre_inscription",
type_="foreignkey",
)
op.create_foreign_key(
"notes_formsemestre_inscription_etudid_fkey",
"notes_formsemestre_inscription",
"identite",
["etudid"],
["id"],
)
op.drop_constraint(
"notes_formsemestre_inscription_parcour_id_fkey",
"notes_formsemestre_inscription",
type_="foreignkey",
)
op.drop_index(
op.f("ix_notes_formsemestre_inscription_parcour_id"),
table_name="notes_formsemestre_inscription",
)
op.drop_column("notes_formsemestre_inscription", "parcour_id")
# --
op.drop_constraint(
"notes_appreciations_etudid_fkey", "notes_appreciations", type_="foreignkey"
)
op.create_foreign_key(
"notes_appreciations_etudid_fkey",
"notes_appreciations",
"identite",
["etudid"],
["id"],
)
op.drop_constraint("itemsuivi_etudid_fkey", "itemsuivi", type_="foreignkey")
op.create_foreign_key(
"itemsuivi_etudid_fkey", "itemsuivi", "identite", ["etudid"], ["id"]
)
op.drop_constraint(
"group_membership_etudid_fkey", "group_membership", type_="foreignkey"
)
op.create_foreign_key(
"group_membership_etudid_fkey",
"group_membership",
"identite",
["etudid"],
["id"],
)
op.drop_constraint("adresse_etudid_fkey", "adresse", type_="foreignkey")
op.create_foreign_key(
"adresse_etudid_fkey", "adresse", "identite", ["etudid"], ["id"]
)
op.drop_constraint("admissions_etudid_fkey", "admissions", type_="foreignkey")
op.create_foreign_key(
"admissions_etudid_fkey", "admissions", "identite", ["etudid"], ["id"]
)
op.drop_constraint(
"absences_notifications_etudid_fkey",
"absences_notifications",
type_="foreignkey",
)
op.create_foreign_key(
"absences_notifications_etudid_fkey",
"absences_notifications",
"identite",
["etudid"],
["id"],
)
op.drop_constraint("absences_etudid_fkey", "absences", type_="foreignkey")
op.create_foreign_key(
"absences_etudid_fkey", "absences", "identite", ["etudid"], ["id"]
)
op.drop_table("parcours_formsemestre")
# ### end Alembic commands ###

View File

@ -0,0 +1,41 @@
"""news index
Revision ID: af77ca6a89d0
Revises: e97b2a10f86c
Create Date: 2022-04-26 12:56:22.862451
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "af77ca6a89d0"
down_revision = "3c31bb0b27c9"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(
op.f("ix_scolar_news_authenticated_user"),
"scolar_news",
["authenticated_user"],
unique=False,
)
op.create_index(op.f("ix_scolar_news_date"), "scolar_news", ["date"], unique=False)
op.create_index(
op.f("ix_scolar_news_object"), "scolar_news", ["object"], unique=False
)
op.create_index(op.f("ix_scolar_news_type"), "scolar_news", ["type"], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_scolar_news_type"), table_name="scolar_news")
op.drop_index(op.f("ix_scolar_news_object"), table_name="scolar_news")
op.drop_index(op.f("ix_scolar_news_date"), table_name="scolar_news")
op.drop_index(op.f("ix_scolar_news_authenticated_user"), table_name="scolar_news")
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""coef_rcue
Revision ID: c0c225192d61
Revises: 4311cc342dbd
Create Date: 2022-06-24 12:19:58.723862
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c0c225192d61'
down_revision = '4311cc342dbd'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('notes_ue', sa.Column('coef_rcue', sa.Float(), server_default='1.0', nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('notes_ue', 'coef_rcue')
# ### end Alembic commands ###

View File

@ -0,0 +1,37 @@
"""Corrige contrainte unicité référentiel compétences
Revision ID: ee21c76c8183
Revises: c0c225192d61
Create Date: 2022-06-27 20:18:24.822527
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "ee21c76c8183"
down_revision = "c0c225192d61"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index("ix_apc_competence_id_orebut", table_name="apc_competence")
op.create_index(
op.f("ix_apc_competence_id_orebut"),
"apc_competence",
["id_orebut"],
unique=False,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_apc_competence_id_orebut"), table_name="apc_competence")
op.create_index(
"ix_apc_competence_id_orebut", "apc_competence", ["id_orebut"], unique=False
)
# ### end Alembic commands ###

View File

@ -1,13 +1,19 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.2.24" SCOVERSION = "9.3.5"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"
SCONEWS = """ SCONEWS = """
<h4>Année 2021</h4> <h4>Année 2021</h4>
<ul> <ul>
<li>ScoDoc 9.3</li>
<ul>
<li>Prise en charge des parcours BUT</li>
<li>Association des UE aux compétences du référentiel</li>
<li>Jury BUT1</li>
</ul>
<li>ScoDoc 9.2: <li>ScoDoc 9.2:
<ul> <ul>
<li>Tableau récap. complet pour BUT et autres formations.</li> <li>Tableau récap. complet pour BUT et autres formations.</li>

View File

@ -4,35 +4,57 @@ Utiliser par exemple comme:
pytest tests/unit/test_refcomp.py pytest tests/unit/test_refcomp.py
""" """
import io
from flask import g from flask import g
import app
from app import db from app import db
from app import models from app import models
from app.but.import_refcomp import orebut_import_refcomp from app.but.import_refcomp import orebut_import_refcomp
from app.models import UniteEns
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcReferentielCompetences, ApcReferentielCompetences,
ApcCompetence, ApcCompetence,
ApcSituationPro, ApcSituationPro,
ApcNiveau,
) )
from tests.unit import setup
REF_RT_XML = open(
"ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml"
).read()
def test_but_refcomp(test_client): def test_but_refcomp(test_client):
"""modèles ref. comp.""" """modèles ref. comp."""
xml_data = open(
"ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml"
).read()
dept_id = models.Departement.query.first().id dept_id = models.Departement.query.first().id
ref = orebut_import_refcomp(xml_data, dept_id) ref_comp: ApcReferentielCompetences = orebut_import_refcomp(REF_RT_XML, dept_id)
assert ref.competences.count() == 13 assert ref_comp.competences.count() == 13
assert ref.competences[0].situations.count() == 3 assert ref_comp.competences[0].situations.count() == 3
assert ref.competences[0].situations[0].libelle.startswith("Conception ") assert ref_comp.competences[0].situations[0].libelle.startswith("Conception ")
assert ( assert (
ref.competences[-1].situations[-1].libelle ref_comp.competences[-1].situations[-1].libelle
== "Administration des services multimédia" == "Administration des services multimédia"
) )
# test cascades on delete # test cascades on delete
db.session.delete(ref) db.session.delete(ref_comp)
db.session.commit() db.session.commit()
assert ApcCompetence.query.count() == 0 assert ApcCompetence.query.count() == 0
assert ApcSituationPro.query.count() == 0 assert ApcSituationPro.query.count() == 0
def test_but_assoc_ue_parcours(test_client):
"""Association UE / Niveau compétence"""
dept_id = models.Departement.query.first().id
G, formation_id, (ue1_id, ue2_id, ue3_id), module_ids = setup.build_formation_test()
ref_comp: ApcReferentielCompetences = orebut_import_refcomp(REF_RT_XML, dept_id)
ue = UniteEns.query.get(ue1_id)
assert ue.niveau_competence is None
niveau = ApcNiveau.query.first()
ue.niveau_competence = niveau
db.session.add(ue)
db.session.commit()
ue = UniteEns.query.get(ue1_id)
assert ue.niveau_competence == niveau
assert len(niveau.ues) == 1
assert niveau.ues[0] == ue

View File

@ -14,6 +14,7 @@ Au besoin, créer un base de test neuve:
import random import random
from flask import g from flask import g
from app.models.formsemestre import FormSemestreInscription
from config import TestConfig from config import TestConfig
from tests.unit import sco_fake_gen from tests.unit import sco_fake_gen
@ -85,6 +86,15 @@ def run_sco_basic(verbose=False):
# --- Inscription des étudiants # --- Inscription des étudiants
for etud in etuds: for etud in etuds:
G.inscrit_etudiant(formsemestre_id, etud) G.inscrit_etudiant(formsemestre_id, etud)
# Vérification incription semestre:
q = FormSemestreInscription.query.filter_by(
etudid=etuds[0].id, formsemestre_id=formsemestre_id
)
assert q.count() == 1
ins = q.first()
assert ins.etape == None
assert ins.etat == "I"
assert ins.parcour == None
# --- Creation évaluation # --- Creation évaluation
e = G.create_evaluation( e = G.create_evaluation(
@ -217,3 +227,15 @@ def run_sco_basic(verbose=False):
dec_ues = nt.get_etud_decision_ues(etud["etudid"]) dec_ues = nt.get_etud_decision_ues(etud["etudid"])
for ue_id in dec_ues: for ue_id in dec_ues:
assert dec_ues[ue_id]["code"] in {"ADM", "CMP"} assert dec_ues[ue_id]["code"] in {"ADM", "CMP"}
# ---- Suppression étudiant, vérification inscription
# (permet de tester les cascades)
etud = etuds[0]
etudid = etud.id
db.session.delete(etud)
db.session.commit()
# Vérification incription semestre:
q = FormSemestreInscription.query.filter_by(
etudid=etudid, formsemestre_id=formsemestre_id
)
assert q.count() == 0