1
0
forked from ScoDoc/ScoDoc

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

This commit is contained in:
Emmanuel Viennet 2023-01-27 04:01:28 -03:00
commit 569df98e25
29 changed files with 536 additions and 243 deletions

View File

@ -630,19 +630,38 @@ class DecisionsProposeesAnnee(DecisionsProposees):
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
def next_semestre_ids(self, code: str) -> set[int]:
"""Les indices des semestres dans lequels l'étudiant est autorisé
à poursuivre après le semestre courant.
"""
ids = set()
# La poursuite d'études dans un semestre pair dune même année
# est de droit pour tout étudiant:
if (self.formsemestre.semestre_id % 2) and sco_codes.ParcoursBUT.NB_SEM:
ids.add(self.formsemestre.semestre_id + 1)
# 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 condition a paru trop stricte à de nombreux collègues.
# ScoDoc ne contraint donc pas à la respecter strictement.
# Si le code est dans BUT_CODES_PASSAGE (ADM, ADJ, PASD, PAS1NCI, ATJ),
# autorise à passer dans le semestre suivant
if (
self.jury_annuel
and 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
ids.add(self.formsemestre.semestre_id + 1)
if code == RED:
ids.add(
self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2
)
return ids
def record_form(self, form: dict):
"""Enregistre les codes de jury en base
@ -704,9 +723,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
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 False # no change
if code != self.code_valide and (self.code_valide is None or not no_overwrite):
# Enregistrement du code annuel BUT
if self.validation:
db.session.delete(self.validation)
db.session.commit()
@ -730,19 +749,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
# --- 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,
origin_formsemestre_id=self.formsemestre.id,
)
else:
next_semestre_id = self.next_annee_semestre_id(code)
if next_semestre_id is not None:
for next_semestre_id in self.next_semestre_ids(code):
ScolarAutorisationInscription.autorise_etud(
self.etud.id,
self.formsemestre_pair.formation.formation_code,
self.formsemestre_pair.id,
self.formsemestre.formation.formation_code,
self.formsemestre.id,
next_semestre_id,
)
@ -872,18 +887,18 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.invalidate_formsemestre_cache()
def get_autorisations_passage(self) -> list[int]:
"""Les liste des indices de semestres auxquels on est autorisé à
s'inscrire depuis cette année"""
formsemestre = self.formsemestre_pair or self.formsemestre_impair
if not formsemestre:
return []
return [
"""Liste des indices de semestres auxquels on est autorisé à
s'inscrire depuis le semestre courant.
"""
return sorted(
[
a.semestre_id
for a in ScolarAutorisationInscription.query.filter_by(
etudid=self.etud.id,
origin_formsemestre_id=formsemestre.id,
origin_formsemestre_id=self.formsemestre.id,
)
]
)
def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
"""Description textuelle des niveaux validés (enregistrés)

View File

@ -11,7 +11,7 @@ import re
import numpy as np
import flask
from flask import flash, url_for
from flask import flash, render_template, url_for
from flask import g, request
from app import db
@ -32,8 +32,10 @@ from app.models import (
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
)
from app.models.config import ScoDocSiteConfig
from app.scodoc import html_sco_header
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
@ -43,9 +45,10 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
"""
H = []
H.append("""<div class="but_section_annee">""")
if deca.jury_annuel:
H.append(
f"""
<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,
@ -53,11 +56,9 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
}
<span>({deca.code_valide or 'non'} enregistrée)</span>
</div>
</div>
"""
)
div_explanation = f"""<div class="but_explanation">{deca.explanation}</div>"""
H.append("""</div>""")
formsemestre_1 = deca.formsemestre_impair
formsemestre_2 = deca.formsemestre_pair
@ -74,7 +75,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
<div class="titre_niveaux">
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
</div>
{div_explanation}
<div class="but_explanation">{deca.explanation}</div>
<div class="but_annee">
<div class="titre"></div>
<div class="titre">{"S" +str(formsemestre_1.semestre_id)
@ -285,7 +286,7 @@ def jury_but_semestriel(
read_only: bool,
navigation_div: str = "",
) -> str:
"""Formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)"""
"""Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)."""
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
inscription_etat = etud.inscription_etat(formsemestre.id)
@ -450,11 +451,27 @@ def jury_but_semestriel(
)
H.append("</div>") # but_annee
div_autorisations_passage = (
f"""
<div class="but_autorisations_passage">
<span>Autorisé à passer en&nbsp;:</span>
{ ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )}
</div>
"""
if autorisations_passage
else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>"""
)
H.append(div_autorisations_passage)
if read_only:
H.append(
"""<div class="but_explanation">
Vous n'avez pas la permission de modifier ces décisions.
Les champs entourés en vert sont enregistrés.</div>"""
f"""<div class="but_explanation">
{"Vous n'avez pas la permission de modifier ces décisions."
if formsemestre.etat
else "Semestre verrouillé."}
Les champs entourés en vert sont enregistrés.
</div>
"""
)
else:
if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM:
@ -464,9 +481,6 @@ def jury_but_semestriel(
<input type="checkbox" name="autorisation_passage" value="1" {
"checked" if est_autorise_a_passer else ""}>
<em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em>
{("(autorisations enregistrées: " + ' '.join(
'S' + str(a.semestre_id or '') for a in autorisations_passage) + ")"
) if autorisations_passage else ""}
</input>
</div>
"""
@ -481,7 +495,19 @@ def jury_but_semestriel(
</div>
"""
)
H.append(navigation_div)
H.append("</div>")
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)

View File

@ -740,7 +740,8 @@ class ResultatsSemestre(ResultatsCache):
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
)
}">{"saisir" if not jury_code_sem else "modifier"} décision</a>""",
}">{("saisir" if not jury_code_sem else "modifier")
if self.formsemestre.etat else "voir"} décisions</a>""",
"col_jury_link",
)

View File

@ -55,7 +55,8 @@ class Formation(db.Model):
modules = db.relationship("Module", lazy="dynamic", backref="formation")
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>"
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
def to_html(self) -> str:
"titre complet pour affichage"

View File

@ -111,6 +111,7 @@ class UniteEns(db.Model):
e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None
e["parcour"] = self.parcour.to_dict() if self.parcour else None
if with_module_ue_coefs:
if convert_objects:
e["module_ue_coefs"] = [

View File

@ -99,7 +99,7 @@ def html_edit_formation_apc(
H = [
render_template(
"pn/form_ues.html",
"pn/form_ues.j2",
formation=formation,
semestre_ids=semestre_ids,
editable=editable,
@ -122,7 +122,7 @@ def html_edit_formation_apc(
).first()
H += [
render_template(
"pn/form_mods.html",
"pn/form_mods.j2",
formation=formation,
titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource",
@ -138,7 +138,7 @@ def html_edit_formation_apc(
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template(
"pn/form_mods.html",
"pn/form_mods.j2",
formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ",
@ -154,7 +154,7 @@ def html_edit_formation_apc(
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template(
"pn/form_mods.html",
"pn/form_mods.j2",
formation=formation,
titre=f"Autres modules (non BUT) du S{semestre_idx}",
create_element_msg="créer un nouveau module",
@ -196,7 +196,7 @@ def html_ue_infos(ue):
and ue.matieres.count() == 0
)
return render_template(
"pn/ue_infos.html",
"pn/ue_infos.j2",
titre=f"UE {ue.acronyme} {ue.titre}",
ue=ue,
formsemestres=formsemestres,

View File

@ -723,7 +723,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
],
page_title=f"Programme {formation.acronyme}",
page_title=f"Programme {formation.acronyme} v{formation.version}",
),
f"""<h2>{formation.to_html()} {lockicon}
</h2>
@ -765,7 +765,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
# Description de la formation
H.append(
render_template(
"pn/form_descr.html",
"pn/form_descr.j2",
formation=formation,
parcours=parcours,
editable=editable,
@ -913,8 +913,12 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<li><a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml')
}">Export XML de la formation</a>
(permet de la sauvegarder pour l'échanger avec un autre site)
}">Export XML de la formation</a> ou
<a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml', export_codes_apo=0)
}">sans codes Apogée</a>
(permet de l'enregistrer pour l'échanger avec un autre site)
</li>
<li><a class="stdlink" href="{

View File

@ -109,6 +109,7 @@ def formation_export(
export_ids=False,
export_tags=True,
export_external_ues=False,
export_codes_apo=True,
format=None,
):
"""Get a formation, with UE, matieres, modules
@ -116,30 +117,45 @@ def formation_export(
"""
formation: Formation = Formation.query.get_or_404(formation_id)
f_dict = formation.to_dict(with_refcomp_attrs=True)
selector = {"formation_id": formation_id}
if not export_ids:
del f_dict["formation_id"]
del f_dict["dept_id"]
ues = formation.ues
if not export_external_ues:
selector["is_external"] = False
ues = sco_edit_ue.ue_list(selector)
f_dict["ue"] = ues
for ue_dict in ues:
ue_id = ue_dict["ue_id"]
ues = ues.filter_by(is_external=False)
ues = ues.all()
ues.sort(key=lambda u: (u.semestre_idx or 0, u.numero or 0, u.acronyme))
f_dict["ue"] = []
for ue in ues:
ue_dict = ue.to_dict()
f_dict["ue"].append(ue_dict)
ue_dict.pop("module_ue_coefs", None)
if formation.is_apc():
# BUT: indique niveau de compétence associé à l'UE
ue = UniteEns.query.get(ue_id)
if ue.niveau_competence:
ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle
ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
ue_dict["reference"] = ue_id # pour les coefficients
# Et le parcour:
if ue.parcour:
ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)]
ue_dict["reference"] = ue.id # pour les coefficients
if not export_ids:
del ue_dict["id"]
del ue_dict["ue_id"]
del ue_dict["formation_id"]
if "niveau_competence_id" in ue_dict:
del ue_dict["niveau_competence_id"]
for id_id in (
"id",
"ue_id",
"formation_id",
"parcour_id",
"niveau_competence_id",
):
ue_dict.pop(id_id, None)
if not export_codes_apo:
ue_dict.pop("code_apogee", None)
if ue_dict["ects"] is None:
del ue_dict["ects"]
mats = sco_edit_matiere.matiere_list({"ue_id": ue_id})
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
mats.sort(key=lambda m: m["numero"] or 0)
ue_dict["matiere"] = mats
for mat in mats:
matiere_id = mat["matiere_id"]
@ -148,6 +164,7 @@ def formation_export(
del mat["matiere_id"]
del mat["ue_id"]
mods = sco_edit_module.module_list({"matiere_id": matiere_id})
mods.sort(key=lambda m: (m["numero"] or 0, m["code"]))
mat["module"] = mods
for mod in mods:
module_id = mod["module_id"]
@ -183,6 +200,8 @@ def formation_export(
del mod["matiere_id"]
del mod["module_id"]
del mod["formation_id"]
if not export_codes_apo:
del mod["code_apogee"]
if mod["ects"] is None:
del mod["ects"]
@ -323,14 +342,30 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
referentiel_competence_id, ue_info[1]
)
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
ue: UniteEns = UniteEns.query.get(ue_id)
assert ue
if xml_ue_id:
ues_old2new[xml_ue_id] = ue_id
# élément optionnel présent dans les exports BUT:
ue_reference = ue_info[1].get("reference")
if ue_reference:
ue_reference_to_id[int(ue_reference)] = ue_id
# -- create matieres
for mat_info in ue_info[2]:
if mat_info[0] == "parcour":
# Parcours (BUT)
code_parcours = mat_info[1]["code"]
parcour = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcour:
ue.parcour = parcour
db.session.add(ue)
else:
log(f"Warning: parcours {code_parcours} inexistant !")
continue
assert mat_info[0] == "matiere"
mat_info[1]["ue_id"] = ue_id
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
@ -382,12 +417,12 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
# associe les parcours de ce module (BUT)
if referentiel_competence_id is not None:
code_parcours = child[1]["code"]
parcours = ApcParcours.query.filter_by(
parcour = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcours:
module.parcours.append(parcours)
if parcour:
module.parcours.append(parcour)
db.session.add(module)
else:
log(

View File

@ -556,9 +556,10 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
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.
Attention, si aucun parcours n'est coché, toutes les UEs du
programme seront considérées, quel que soit leur parcours.
Cocher tous les parcours est exactement équivalent à n'en cocher aucun:
par exemple, pour un semestre de "tronc commun", on peut ne pas indiquer de parcours.
Si aucun parcours n'est coché, toutes les UEs du
programme seront donc considérées, quel que soit leur parcours.
""",
},
)

View File

@ -602,9 +602,21 @@ def formsemestre_recap_parcours_table(
<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>
title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a>
"""
)
if nt.is_apc:
H.append(
f"""<a class="stdlink jury_link" title="Validations du semestre BUT"
href="{ url_for("notes.formsemestre_validation_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etudid,
)
}">jury</a>"""
)
H.append("""</td>""")
if nt.is_apc:
H.append('<td class="rcp_but">BUT</td>')
elif decision_sem:

View File

@ -39,21 +39,22 @@ from app import log
from app.models import FormSemestre
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury
from app.scodoc import sco_etud
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formations
from app.scodoc import sco_groups
from app.scodoc import sco_etud
from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury
from app.scodoc.sco_exceptions import ScoValueError
def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
"""Liste des etudiants autorisés à s'inscrire dans sem.
delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible.
ignore_jury: si vrai, considère tous les étudiants comem autorisés, même
ignore_jury: si vrai, considère tous les étudiants comme autorisés, même
s'ils n'ont pas de décision de jury.
"""
src_sems = list_source_sems(sem, delai=delai)
@ -276,8 +277,8 @@ def formsemestre_inscr_passage(
submitted=False,
dialog_confirmed=False,
ignore_jury=False,
):
"""Form. pour inscription des etudiants d'un semestre dans un autre
) -> str:
"""Page Form. pour inscription des etudiants d'un semestre dans un autre
(donné par formsemestre_id).
Permet de selectionner parmi les etudiants autorisés à s'inscrire.
Principe:
@ -285,8 +286,8 @@ def formsemestre_inscr_passage(
- afficher chaque semestre "boites" avec cases à cocher
- si l'étudiant est déjà inscrit, le signaler (gras, nom de groupes): il peut être désinscrit
- on peut choisir les groupes TD, TP, TA
- seuls les etudiants non inscrits changent (de groupe)
- les etudiants inscrit qui se trouvent décochés sont désinscrits
- seuls les étudiants non inscrits changent (de groupe)
- les étudiants inscrit qui se trouvent décochés sont désinscrits
- Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant.
"""
@ -326,11 +327,9 @@ def formsemestre_inscr_passage(
a_desinscrire = inscrits_set - etuds_set
else:
a_inscrire = a_desinscrire = []
# log('formsemestre_inscr_passage: a_inscrire=%s' % str(a_inscrire) )
# log('formsemestre_inscr_passage: a_desinscrire=%s' % str(a_desinscrire) )
if not submitted:
H += build_page(
H += _build_page(
sem,
auth_etuds_by_sem,
inscrits,
@ -343,7 +342,7 @@ def formsemestre_inscr_passage(
if not dialog_confirmed:
# Confirmation
if a_inscrire:
H.append("<h3>Etudiants à inscrire</h3><ol>")
H.append("<h3>Étudiants à inscrire</h3><ol>")
for etud in set_to_sorted_etud_list(a_inscrire):
H.append("<li>%(nomprenom)s</li>" % etud)
H.append("</ol>")
@ -354,7 +353,7 @@ def formsemestre_inscr_passage(
H.append('<li class="inscrailleurs">%(nomprenom)s</li>' % etud)
H.append("</ul>")
if a_desinscrire:
H.append("<h3>Etudiants à désinscrire</h3><ol>")
H.append("<h3>Étudiants à désinscrire</h3><ol>")
for etudid in a_desinscrire:
H.append(
'<li class="desinscription">%(nomprenom)s</li>'
@ -384,21 +383,29 @@ def formsemestre_inscr_passage(
)
)
else:
with sco_cache.DeferredSemCacheManager():
# Inscription des étudiants au nouveau semestre:
do_inscrit(
sem,
a_inscrire,
inscrit_groupes=inscrit_groupes,
)
# Desincriptions:
# Désinscriptions:
do_desinscrit(sem, a_desinscrire)
H.append(
"""<h3>Opération effectuée</h3>
<ul><li><a class="stdlink" href="formsemestre_inscr_passage?formsemestre_id=%s">Continuer les inscriptions</a></li>
<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%s">Tableau de bord du semestre</a></li>"""
% (formsemestre_id, formsemestre_id)
f"""<h3>Opération effectuée</h3>
<ul>
<li><a class="stdlink" href="{
url_for("notes.formsemestre_inscr_passage",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Continuer les inscriptions</a>
</li>
<li><a class="stdlink" href="{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Tableau de bord du semestre</a>
</li>"""
)
partition = sco_groups.formsemestre_get_main_partition(formsemestre_id)
if (
@ -420,7 +427,7 @@ def formsemestre_inscr_passage(
return "\n".join(H)
def build_page(
def _build_page(
sem,
auth_etuds_by_sem,
inscrits,
@ -429,6 +436,7 @@ def build_page(
inscrit_groupes=False,
ignore_jury=False,
):
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
inscrit_groupes = int(inscrit_groupes)
ignore_jury = int(ignore_jury)
if inscrit_groupes:
@ -443,27 +451,36 @@ def build_page(
html_sco_header.html_sem_header(
"Passages dans le semestre", with_page_header=False
),
"""<form name="f" method="post" action="%s">""" % request.base_url,
"""<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
f"""<form name="f" method="post" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{sem['formsemestre_id']}"/>
<input type="submit" name="submitted" value="Appliquer les modifications"/>
&nbsp;<a href="#help">aide</a>
"""
% sem, # "
"""<input name="inscrit_groupes" type="checkbox" value="1" %s>inscrire aux mêmes groupes</input>"""
% inscrit_groupes_checked,
"""<input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()" %s>inclure tous les étudiants (même sans décision de jury)</input>"""
% ignore_jury_checked,
"""<div class="pas_recap">Actuellement <span id="nbinscrits">%s</span> inscrits
et %d candidats supplémentaires
</div>"""
% (len(inscrits), len(candidats_non_inscrits)),
etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs),
"""<p/><input type="submit" name="submitted" value="Appliquer les modifications"/>""",
formsemestre_inscr_passage_help(sem),
"""</form>""",
<input name="inscrit_groupes" type="checkbox" value="1"
{inscrit_groupes_checked}>inscrire aux mêmes groupes</input>
<input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()"
{ignore_jury_checked}>inclure tous les étudiants (même sans décision de jury)</input>
<div class="pas_recap">Actuellement <span id="nbinscrits">{len(inscrits)}</span> inscrits
et {len(candidats_non_inscrits)} candidats supplémentaires
</div>
<div>{scu.EMO_WARNING} <em>Seuls les semestres dont la date de fin est antérieure à la date de début
de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en compte.</em></div>
{etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)}
<input type="submit" name="submitted" value="Appliquer les modifications"/>
{formsemestre_inscr_passage_help(sem)}
</form>
""",
]
# Semestres sans etudiants autorisés
# Semestres sans étudiants autorisés
empty_sems = []
for formsemestre_id in auth_etuds_by_sem.keys():
if not auth_etuds_by_sem[formsemestre_id]["etuds"]:
@ -473,7 +490,10 @@ def build_page(
"""<div class="pas_empty_sems"><h3>Autres semestres sans candidats :</h3><ul>"""
)
for infos in empty_sems:
H.append("""<li><a href="%(title_target)s">%(title)s</a></li>""" % infos)
H.append(
"""<li><a class="stdlink" href="%(title_target)s">%(title)s</a></li>"""
% infos
)
H.append("""</ul></div>""")
return H

View File

@ -544,7 +544,9 @@ def _ligne_evaluation(
if not first_eval:
H.append("""<tr><td colspan="8">&nbsp;</td></tr>""")
tr_class_1 += " mievr_spaced"
H.append(f"""<tr class="{tr_class_1}"><td class="mievr_tit" colspan="8">""")
H.append(
f"""<tr class="{tr_class_1} mievr_tit"><td class="mievr_tit" colspan="8">"""
)
coef = evaluation.coefficient
if is_apc:
if not evaluation.get_ue_poids_dict():
@ -588,7 +590,9 @@ def _ligne_evaluation(
)
#
H.append(
f"""<div class="evaluation_order">
f"""
</td>
<td class="evaluation_order">
<span class="evalindex" title="Indice dans les vecteurs (formules)">{
eval_index:2}</span>
<span class="eval_arrows_chld">
@ -612,20 +616,6 @@ def _ligne_evaluation(
else:
H.append(arrow_none)
H.append(
f"""</span></span></td>
</div>
</tr>
<tr class="{tr_class}">
<th class="moduleimpl_evaluations" colspan="2">&nbsp;</th>
<th class="moduleimpl_evaluations">Durée</th>
<th class="moduleimpl_evaluations">Coef.</th>
<th class="moduleimpl_evaluations">Notes</th>
<th class="moduleimpl_evaluations">Abs</th>
<th class="moduleimpl_evaluations">N</th>
<th class="moduleimpl_evaluations">Moyenne """
)
if etat["evalcomplete"]:
etat_txt = """(prise en compte)"""
etat_descr = "notes utilisées dans les moyennes"
@ -648,9 +638,19 @@ def _ligne_evaluation(
}" title="{etat_descr}">{etat_txt}</a>"""
H.append(
f"""{etat_txt}</th>
f"""</span></span></td>
</tr>
<tr class="{tr_class}"><td class="mievr">"""
<tr class="{tr_class}">
<th class="moduleimpl_evaluations" colspan="2">&nbsp;</th>
<th class="moduleimpl_evaluations">Durée</th>
<th class="moduleimpl_evaluations">Coef.</th>
<th class="moduleimpl_evaluations">Notes</th>
<th class="moduleimpl_evaluations">Abs</th>
<th class="moduleimpl_evaluations">N</th>
<th class="moduleimpl_evaluations" colspan="2">Moyenne {etat_txt}</th>
</tr>
<tr class="{tr_class}">
<td class="mievr">"""
)
if can_edit_evals:
H.append(
@ -726,7 +726,7 @@ def _ligne_evaluation(
<td class="rightcell mievr_nbnotes">{etat["nb_notes"]} / {etat["nb_inscrits"]}</td>
<td class="rightcell mievr_coef">{etat["nb_abs"]}</td>
<td class="rightcell mievr_coef">{etat["nb_neutre"]}</td>
<td class="rightcell">"""
<td class="rightcell" colspan="2">"""
% etat
)
if etat["moy"]:
@ -750,11 +750,11 @@ def _ligne_evaluation(
H.append(f"""<tr class="{tr_class}"><td></td>""")
if modimpl.module.is_apc():
H.append(
f"""<td colspan="7" class="eval_poids">{
f"""<td colspan="8" class="eval_poids">{
evaluation.get_ue_poids_str()}</td>"""
)
else:
H.append('<td colspan="7"></td>')
H.append('<td colspan="8"></td>')
H.append("""</tr>""")
else: # il y a deja des notes saisies
gr_moyennes = etat["gr_moyennes"]
@ -773,7 +773,10 @@ def _ligne_evaluation(
name = "Tous" # tous
else:
name = f"""Groupe {gr_moyenne["group_name"]}"""
H.append(f"""<td colspan="2" class="mievr_grtit">{name} &nbsp;</td><td>""")
H.append(
f"""<td colspan="2" class="mievr_grtit">{name} &nbsp;</td>
<td colspan="2">"""
)
if gr_moyenne["gr_nb_notes"] > 0:
H.append(
f"""{gr_moyenne["gr_moy"]}&nbsp; (<a href="{

View File

@ -200,7 +200,7 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") ->
return abort(404, "etudiant inconnu")
etud = etuds[0]
else:
raise ValueError("etud_photo_html: either etud or etudid must be specified")
abort(404, "etud_photo_html: either etud or etudid must be specified")
photo_url = etud_photo_url(etud, size=size)
nom = etud.get("nomprenom", etud["nom_disp"])
if title is None:
@ -244,7 +244,7 @@ def photo_pathname(photo_filename: str, size="orig"):
elif size == "orig":
version = ""
else:
raise ValueError("invalid size parameter for photo")
abort(404, "invalid size parameter for photo")
if not photo_filename:
return False
path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT

View File

@ -1309,7 +1309,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
H = []
if nb_decisions > 0:
H.append(
"""<div class="saisie_warn">
f"""<div class="saisie_warn">
<ul class="tf-msg">
<li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour
{nb_decisions} étudiants. Après changement des notes, vérifiez la situation !</li>

View File

@ -1,5 +1,10 @@
/* Saisie décision de jury BUT */
:root {
--color-recorded: rgb(3, 157, 3);
--color-explanation: blueviolet;
}
.jury_but {
font-family: Verdana, Geneva, Tahoma, sans-serif;
}
@ -18,6 +23,19 @@
margin-top: 0px;
}
form#jury_but {
margin: 0px 16px 16px 16px;
background-color: rgb(253, 253, 231);
border: 2px solid rgb(4, 4, 118);
border-radius: 4px;
padding-top: 8px;
padding-left: 16px;
padding-right: 16px;
padding-bottom: 16px;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
}
.but_annee {
margin-left: 32px;
display: inline-grid;
@ -112,11 +130,10 @@ div.but_settings {
}
.but_explanation {
color: blueviolet;
color: var(--color-explanation);
font-style: italic;
padding-top: 4px;
padding-bottom: 12px;
;
}
select:disabled {
@ -130,7 +147,7 @@ select:invalid {
select.but_code option.recorded,
div.but_code recorded {
color: rgb(3, 157, 3);
color: var(--color-recorded);
font-weight: bold;
}
@ -143,14 +160,14 @@ div.but_code {
div.but_niveau_ue.recorded,
div.but_niveau_rcue.recorded {
border-color: rgb(0, 169, 0);
border-color: var(--color-recorded);
border-width: 3px;
}
div.but_niveau_ue.recorded_different,
div.but_niveau_rcue.recorded_different {
box-shadow: 0 0 0 3px red;
outline: dashed 3px rgb(0, 169, 0);
outline: dashed 3px var(--color-recorded);
}
div.but_niveau_ue.annee_prec {
@ -172,8 +189,16 @@ div.but_buttons span {
margin-right: 16px;
}
div.but_doc_codes.but_warning_rcue_cap {
padding-top: 8px;
font-size: 100%;
font-style: italic;
}
div.but_doc_codes {
margin: 16px;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
background-color: rgb(227, 254, 254);
font-size: 75%;
border: 2px solid rgb(4, 4, 118);
@ -228,3 +253,15 @@ div.but_doc table tr td.amue {
.but_niveau_rcue .scoplement {
font-weight: normal;
}
.but_autorisations_passage {
margin-top: 12px;
margin-left: 32px;
font-weight: bold;
color: var(--color-recorded);
}
.but_autorisations_passage.but_explanation {
font-weight: normal;
color: var(--color-explanation);
}

View File

@ -1,7 +1,11 @@
/* # -*- mode: css -*-
ScoDoc, (c) Emmanuel Viennet 1998 - 2021
/* ScoDoc, (c) Emmanuel Viennet 1998 - 2023
*/
:root {
--sco-content-min-width: 600px;
--sco-content-max-width: 1024px;
}
html,
body {
margin: 0;
@ -1657,6 +1661,11 @@ td.formsemestre_status_inscrits {
text-align: center;
}
td.rcp_titre_sem a.jury_link {
margin-left: 8px;
color: red;
}
td.formsemestre_status_cell {
white-space: nowrap;
}
@ -1728,6 +1737,8 @@ ul.ue_inscr_list li.etud {
div.moduleimpl_tableaubord {
padding: 7px;
border: 2px solid gray;
min-width: var(--sco-content-min-width);
max-width: var(--sco-content-max-width);
}
div.moduleimpl_type_sae {
@ -1855,11 +1866,19 @@ span.mievr_rattr {
padding: 1px 3px 1px 3px;
}
tr.mievr td.mievr_tit {
tr.mievr_tit td.mievr_tit {
font-weight: bold;
background-color: #cccccc;
border-top-left-radius: 8px;
}
tr.mievr.mievr_tit td {
background-color: #e1e1e1;
}
tr.mievr_tit td:last-child {
border-top-right-radius: 8px;
text-align: right;
padding-right: 8px;
}
tr.mievr td {
@ -1922,11 +1941,6 @@ span.eval_warning_coef {
background-color: rgb(255, 225, 0);
}
div.evaluation_order {
position: absolute;
right: 1em;
}
span.evalindex {
font-weight: normal;
font-size: 80%;
@ -2580,11 +2594,13 @@ div.bull_head {
display: grid;
justify-content: space-between;
grid-template-columns: auto auto;
min-width: 600px;
max-width: 1072px;
}
div.bull_photo {
display: inline-block;
margin-right: 10px;
margin-right: 8px;
}
span.bulletin_menubar_but {

View File

@ -78,7 +78,10 @@ $(function () {
} else {
document.querySelector("div.next").innerHTML = "";
}
} else {
// Supprime les liens de navigation
document.querySelector("div.prev").innerHTML = "";
document.querySelector("div.next").innerHTML = "";
}
});

View File

@ -262,10 +262,11 @@
<b>Textes de référence:</b>
<ul>
<li><a href="https://www.enseignementsup-recherche.gouv.fr/fr/bo/21/Special4/ESRS2114777A.htm">Bulletin
officiel spécial n°4 du 17 juin 2021</a></li>
officiel spécial n°4 du 26 mai 2022 </a></li>
<li><a
href="https://cache.media.enseignementsup-recherche.gouv.fr//file/SPE4-MESRI-17-6-2021/19/4/SP4_ESR_17_6_2021_1413194.pdf">Version
pdf complète</a></li>
href="https://cache.media.education.gouv.fr/file/SP4-MESRI-26-5-2022/10/0/spe617_annexe1_1426100.pdf">Version
pdf de l'arrêté de mai 2022 sur la licence professionnelle « bachelor universitaire de technologie
»</a></li>
</ul>
</div>

View File

@ -49,7 +49,8 @@
<span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>,
{% endif %}
parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun', true)|safe
- parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun',
true)|safe
}}</b>
{% if mod.heures_cours or mod.heures_td or mod.heures_tp %}
({{mod.heures_cours|default("&nbsp;",true)|safe}}/{{mod.heures_td|default("&nbsp;",true)|safe}}/{{mod.heures_tp|default("&nbsp;",true)|safe}},

View File

@ -704,10 +704,15 @@ def formation_list(format=None, formation_id=None, args={}):
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formation_export(formation_id, export_ids=False, format=None):
def formation_export(
formation_id, export_ids=False, format=None, export_codes_apo=True
):
"Export de la formation au format indiqué (xml ou json)"
return sco_formations.formation_export(
formation_id, export_ids=export_ids, format=format
formation_id,
export_ids=export_ids,
format=format,
export_codes_apo=export_codes_apo,
)
@ -2494,11 +2499,31 @@ def formsemestre_validation_but(
H.append(jury_but_view.show_etud(deca, read_only=read_only))
autorisations_idx = deca.get_autorisations_passage()
div_autorisations_passage = (
f"""
<div class="but_autorisations_passage">
<span>Autorisé à passer en&nbsp;:</span>
{ ", ".join( ["S" + str(i) for i in autorisations_idx ] )}
</div>
"""
if autorisations_idx
else """<div class="but_autorisations_passage but_explanation">
pas d'autorisations de passage enregistrées.
</div>
"""
)
H.append(div_autorisations_passage)
if read_only:
H.append(
"""<div class="but_explanation">
Vous n'avez pas la permission de modifier ces décisions.
Les champs entourés en vert sont enregistrés.</div>"""
f"""
<div class="but_explanation">
{"Vous n'avez pas la permission de modifier ces décisions."
if formsemestre.etat
else "Semestre verrouillé."}
Les champs entourés en vert sont enregistrés.
</div>"""
)
else:
erase_span = f"""<a href="{
@ -2508,9 +2533,11 @@ def formsemestre_validation_but(
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>
<em>permettre la saisie manuelles des codes
{"d'année et " if deca.jury_annuel else ""}
de niveaux.
Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année !
</em>
</input>
</div>
@ -2522,7 +2549,15 @@ def formsemestre_validation_but(
)
H.append(navigation_div)
H.append("</form>")
if deca.a_cheval:
H.append(
f"""<div class="but_doc_codes but_warning_rcue_cap">
{scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE <b>capitalisées</b> (note > 10)
lors d'une année précédente peuvent être prise en compte pour former
un RCUE (associé à un niveau de compétence du BUT).
</div>
"""
)
H.append(
render_template(
"but/documentation_codes_jury.html",

View File

@ -216,7 +216,7 @@ def edit_modules_ue_coefs():
</h2>
""",
render_template(
"pn/form_modules_ue_coefs.html",
"pn/form_modules_ue_coefs.j2",
formation=formation,
data_source=url_for(
"notes.table_modules_ue_coefs",

View File

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

View File

@ -32,7 +32,7 @@ from app.models import GroupDescr
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models import Partition
from app.models import ScolarFormSemestreValidation
from app.models import ScolarAutorisationInscription, ScolarFormSemestreValidation
from app.models.but_refcomp import (
ApcCompetence,
ApcNiveau,
@ -101,6 +101,7 @@ def make_shell_context():
"ResultatsSemestreBUT": ResultatsSemestreBUT,
"Role": Role,
"scolar": scolar,
"ScolarAutorisationInscription": ScolarAutorisationInscription,
"ScolarFormSemestreValidation": ScolarFormSemestreValidation,
"ScolarNews": models.ScolarNews,
"scu": scu,

View File

@ -1022,3 +1022,84 @@ Etudiants:
moy_ue: 9.5000
moy_ue_with_cap: 12.7600
decision_annee: AJ
geii1000:
prenom: etugeii1000
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 12.0000
"S1.2": 9.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "ADM", "..." ]
code_valide: ADM
moy_ue: 12.0000
"UE12":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 9.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 12.0000
"S2.2": 10.50 # capitalise mais ne compense pas
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
codes: [ "ADM", "..." ]
code_valide: ADM
moy_ue: 12.0000
"UE22":
code_valide: ADM
moy_ue: 10.5
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: ADM
decision_jury: ADM
rcue:
moy_rcue: 12.0000
est_compensable: False
"UE12":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.75
est_compensable: False
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 5.0000
"S1.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: AJ
moy_ue: 5. # LA MOYENNE COURANTE
moy_ue_with_cap: 12.0000
"UE12":
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
# RCUE inter-annuel
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE12":
code_valide: ADM
rcue:
moy_rcue: 11.25
est_compensable: False

View File

@ -33,7 +33,7 @@ DEPT = TestConfig.DEPT_TEST
@pytest.mark.but_gb
def test_but_jury_GB(test_client):
"""Tests sur un cursus GB
- construction des semestres et de leurs étudianst à partir du yaml
- construction des semestres et de leurs étudiants à partir du yaml
- vérification jury de S1
- vérification jury de S2
- vérification jury de S3
@ -96,7 +96,6 @@ def test_but_jury_GEII_lyon(test_client):
# Construit la base de test GB une seule fois
# puis lance les tests de jury
doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_geii_lyon.yaml")
formsemestres = FormSemestre.query.order_by(
FormSemestre.date_debut, FormSemestre.semestre_id
).all()