WIP: nouvelles gestion jury BUT.

This commit is contained in:
Emmanuel Viennet 2023-06-28 21:25:38 +02:00
parent 4b49fd5ed9
commit 5d30b9233b
16 changed files with 749 additions and 922 deletions

View File

@ -36,7 +36,6 @@ from app.models import Scolog, ScolarAutorisationInscription
from app.models.but_validations import ( from app.models.but_validations import (
ApcValidationAnnee, ApcValidationAnnee,
ApcValidationRCUE, ApcValidationRCUE,
RegroupementCoherentUE,
) )
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formations import Formation from app.models.formations import Formation

File diff suppressed because it is too large Load Diff

View File

@ -153,7 +153,7 @@ def pvjury_table_but(
etudid=etud.id, etudid=etud.id,
), ),
"cursus": _descr_cursus_but(etud), "cursus": _descr_cursus_but(etud),
"ects": f"{deca.formsemestre_ects():g}", "ects": f"{deca.ects_annee():g}",
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-", "ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep) "niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
if deca if deca

View File

@ -48,9 +48,9 @@ def _get_jury_but_etud_result(
# --- Les RCUEs # --- Les RCUEs
rcue_list = [] rcue_list = []
if deca: if deca:
for rcue in deca.rcues_annee: for dec_rcue in deca.get_decisions_rcues_annee():
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) rcue = dec_rcue.rcue
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau if rcue.complete: # n'exporte que les RCUEs complets
dec_ue1 = deca.decisions_ues[rcue.ue_1.id] dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
dec_ue2 = deca.decisions_ues[rcue.ue_2.id] dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
rcue_dict = { rcue_dict = {

View File

@ -93,35 +93,25 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
<div class="titre">RCUE</div> <div class="titre">RCUE</div>
""" """
) )
for niveau in deca.niveaux_competences: for dec_rcue in deca.get_decisions_rcues_annee():
rcue = dec_rcue.rcue
niveau = rcue.niveau
H.append( H.append(
f"""<div class="but_niveau_titre"> f"""<div class="but_niveau_titre">
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div> <div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
</div>""" </div>"""
) )
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
ues = [
ue
for ue in deca.ues_impair
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
]
ue_impair = ues[0] if ues else None
ues = [
ue
for ue in deca.ues_pair
if ue.niveau_competence and ue.niveau_competence.id == niveau.id
]
ue_pair = ues[0] if ues else None
# Les UEs à afficher, # Les UEs à afficher,
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant # qui
ues_ro = [ ues_ro = [
( (
ue_impair, ue_impair,
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id), rcue.ue_cur_impair is None,
), ),
( (
ue_pair, ue_pair,
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id, rcue.ue_cur_pair is None,
), ),
] ]
# Ordonne selon les dates des 2 semestres considérés: # Ordonne selon les dates des 2 semestres considérés:
@ -155,12 +145,13 @@ def _gen_but_select(
code_valide: str, code_valide: str,
disabled: bool = False, disabled: bool = False,
klass: str = "", klass: str = "",
data: dict = {}, data: dict = None,
code_valide_label: str = "", code_valide_label: str = "",
) -> str: ) -> str:
"Le menu html select avec les codes" "Le menu html select avec les codes"
# if disabled: # mauvaise idée car le disabled est traité en JS # if disabled: # mauvaise idée car le disabled est traité en JS
# return f"""<div class="but_code {klass}">{code_valide}</div>""" # return f"""<div class="but_code {klass}">{code_valide}</div>"""
data = data or {}
options_htm = "\n".join( options_htm = "\n".join(
[ [
f"""<option value="{code}" f"""<option value="{code}"
@ -220,8 +211,14 @@ def _gen_but_niveau_ue(
else: else:
scoplement = "" scoplement = ""
return f"""<div class="but_niveau_ue { ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
'recorded' if dec_ue.code_valide is not None else ''} if dec_ue.code_valide is not None and dec_ue.codes:
if dec_ue.code_valide == dec_ue.codes[0]:
ue_class = "recorded"
else:
ue_class = "recorded_different"
return f"""<div class="but_niveau_ue {ue_class}
{'annee_prec' if annee_prec else ''} {'annee_prec' if annee_prec else ''}
"> ">
<div title="{ue.titre}">{ue.acronyme}</div> <div title="{ue.titre}">{ue.acronyme}</div>
@ -242,7 +239,7 @@ def _gen_but_niveau_ue(
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str: def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
if dec_rcue is None: if dec_rcue is None or not dec_rcue.rcue.complete:
return """ return """
<div class="but_niveau_rcue niveau_vide with_scoplement"> <div class="but_niveau_rcue niveau_vide with_scoplement">
<div></div> <div></div>

View File

@ -4,7 +4,6 @@
""" """
from typing import Union from typing import Union
from flask_sqlalchemy.query import Query
from app import db from app import db
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
@ -13,8 +12,6 @@ from app.models.etudiants import Identite
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.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import codes_cursus as sco_codes
from app.scodoc import sco_utils as scu
class ApcValidationRCUE(db.Model): class ApcValidationRCUE(db.Model):
@ -22,7 +19,7 @@ class ApcValidationRCUE(db.Model):
aka "regroupements cohérents d'UE" dans le jargon BUT. aka "regroupements cohérents d'UE" dans le jargon BUT.
Le formsemestre est celui du semestre PAIR du niveau de compétence Le formsemestre est l'origine, utilisé pour effacer
""" """
__tablename__ = "apc_validation_rcue" __tablename__ = "apc_validation_rcue"
@ -109,139 +106,6 @@ class ApcValidationRCUE(db.Model):
} }
# 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 RCUE déclenche la compensation des UE.
"""
def __init__(
self,
etud: Identite,
formsemestre_1: FormSemestre,
dec_ue_1: "DecisionsProposeesUE",
formsemestre_2: FormSemestre,
dec_ue_2: "DecisionsProposeesUE",
inscription_etat: str,
):
ue_1 = dec_ue_1.ue
ue_2 = dec_ue_2.ue
# 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
if inscription_etat != scu.INSCRIT:
self.moy_rcue = None
self.moy_ue_1 = self.moy_ue_2 = "-"
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
return
self.moy_ue_1 = dec_ue_1.moy_ue_with_cap
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
self.moy_ue_2 = dec_ue_2.moy_ue_with_cap
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées)
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 __str__(self) -> str:
return f"""RCUE {
self.ue_1.acronyme}({self.moy_ue_1}) + {
self.ue_2.acronyme}({self.moy_ue_2})"""
def query_validations(
self,
) -> Query: # 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 (uniquement) par compensation
c'est à dire que sa moyenne est > 10 avec une UE < 10.
Note: si ADM, est_compensable est faux.
"""
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 RCUE satisfait les conditions pour être validé,
c'est à dire que la moyenne des UE qui le constituent 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.CODES_RCUE_VALIDES
):
return validation
return None
# unused # unused
# def find_rcues( # def find_rcues(
# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str # formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
@ -319,7 +183,7 @@ class ApcValidationAnnee(db.Model):
formsemestre_id = db.Column( formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
) )
"le semestre IMPAIR (le 1er) de l'année" "le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
formation_id = db.Column( formation_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_formations.id"), db.ForeignKey("notes_formations.id"),

View File

@ -265,11 +265,8 @@ class ScolarNews(db.Model):
# Informations générales # Informations générales
H.append( H.append(
f"""<div> f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
Pour être informé des évolutions de ScoDoc, Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
vous pouvez vous
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">
abonner à la liste de diffusion</a>.
</div> </div>
""" """
) )

View File

@ -68,7 +68,7 @@ class ScolarFormSemestreValidation(db.Model):
if self.ue_id: if self.ue_id:
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue ! # Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
}: {self.code}""" } ({self.ue_id}): {self.code}"""
return f"""décision sur semestre {self.formsemestre.titre_mois()} du { return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
self.event_date.strftime("%d/%m/%Y")}""" self.event_date.strftime("%d/%m/%Y")}"""

View File

@ -793,7 +793,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{tf[1]} {tf[1]}
""" """
elif tf[0] == -1: elif tf[0] == -1:
return "<h4>annulation</h4>" return redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
)
else: else:
if tf[2]["gestion_compensation_lst"]: if tf[2]["gestion_compensation_lst"]:
tf[2]["gestion_compensation"] = True tf[2]["gestion_compensation"] = True

View File

@ -349,7 +349,7 @@ SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT
# Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés: # Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés:
SCO_WEBSITE = "https://scodoc.org" SCO_WEBSITE = "https://scodoc.org"
SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur" SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur"
SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces" SCO_ANNONCES_WEBSITE = "https://scodoc.org/Contact"
SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr" SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr"
SCO_USERS_LIST = "notes@listes.univ-paris13.fr" SCO_USERS_LIST = "notes@listes.univ-paris13.fr"
SCO_LISTS_URL = "https://scodoc.org/Contact" SCO_LISTS_URL = "https://scodoc.org/Contact"

View File

@ -1,138 +1,152 @@
// JS for all ScoDoc pages (using jQuery UI) // JS for all ScoDoc pages (using jQuery UI)
$(function () { $(function () {
// Autocomplete recherche etudiants par nom // Autocomplete recherche etudiants par nom
$(".in-expnom").autocomplete( $(".in-expnom").autocomplete({
{ delay: 300, // wait 300ms before suggestions
delay: 300, // wait 300ms before suggestions minLength: 2, // min nb of chars before suggest
minLength: 2, // min nb of chars before suggest position: { collision: "flip" }, // automatic menu position up/down
position: { collision: 'flip' }, // automatic menu position up/down source: SCO_URL + "/search_etud_by_name",
source: SCO_URL + "/search_etud_by_name", select: function (event, ui) {
select: function (event, ui) { $(".in-expnom").val(ui.item.value);
$(".in-expnom").val(ui.item.value); $("#form-chercheetud").submit();
$("#form-chercheetud").submit(); },
} });
});
// Date picker // Date picker
$(".datepicker").datepicker({ $(".datepicker").datepicker({
showOn: 'button', showOn: "button",
buttonImage: '/ScoDoc/static/icons/calendar_img.png', buttonImage: "/ScoDoc/static/icons/calendar_img.png",
buttonImageOnly: true, buttonImageOnly: true,
dateFormat: 'dd/mm/yy', dateFormat: "dd/mm/yy",
duration: 'fast', duration: "fast",
}); });
$('.datepicker').datepicker('option', $.extend({ showMonthAfterYear: false }, $(".datepicker").datepicker(
$.datepicker.regional['fr'])); "option",
$.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"])
);
/* Barre menu */ /* Barre menu */
var sco_menu_position = { my: "left top", at: "left bottom" }; var sco_menu_position = { my: "left top", at: "left bottom" };
$("#sco_menu").menu({ $("#sco_menu")
position: sco_menu_position, .menu({
blur: function () { position: sco_menu_position,
$(this).menu("option", "position", sco_menu_position); blur: function () {
}, $(this).menu("option", "position", sco_menu_position);
focus: function (e, ui) { },
if ($("#sco_menu").get(0) !== $(ui).get(0).item.parent().get(0)) { focus: function (e, ui) {
$(this).menu("option", "position", { my: "left top", at: "right top" }); if ($("#sco_menu").get(0) !== $(ui).get(0).item.parent().get(0)) {
} $(this).menu("option", "position", {
my: "left top",
at: "right top",
});
} }
}).mouseleave(function (x, y) { },
$("#sco_menu").menu('collapseAll'); })
.mouseleave(function (x, y) {
$("#sco_menu").menu("collapseAll");
}); });
$("#sco_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s"); $("#sco_menu > li > a > span").switchClass(
"ui-icon-carat-1-e",
"ui-icon-carat-1-s"
);
/* Les menus isoles dropdown */ /* Les menus isoles dropdown */
$(".sco_dropdown_menu").menu({ $(".sco_dropdown_menu")
position: sco_menu_position .menu({
}).mouseleave(function (x, y) { position: sco_menu_position,
$(".sco_dropdown_menu").menu('collapseAll'); })
} .mouseleave(function (x, y) {
); $(".sco_dropdown_menu").menu("collapseAll");
$(".sco_dropdown_menu > li > a > span").switchClass("ui-icon-carat-1-e", "ui-icon-carat-1-s"); });
$(".sco_dropdown_menu > li > a > span").switchClass(
"ui-icon-carat-1-e",
"ui-icon-carat-1-s"
);
/* up-to-date status */ /* up-to-date status */
var update_div = document.getElementById("update_warning"); var update_div = document.getElementById("update_warning");
if (update_div) { if (update_div) {
fetch('install_info').then( fetch("install_info")
response => response.text() .then((response) => response.text())
).then(text => { .then((text) => {
update_div.innerHTML = text; update_div.innerHTML = text;
if (text) { if (text) {
update_div.style.display = "block"; update_div.style.display = "block";
} }
}); });
} }
}); });
function sco_capitalize(string) { function sco_capitalize(string) {
return string[0].toUpperCase() + string.slice(1).toLowerCase(); return string[0].toUpperCase() + string.slice(1).toLowerCase();
} }
// Affiche un message transitoire (duration milliseconds, 0 means infinity) // Affiche un message transitoire (duration milliseconds, 0 means infinity)
function sco_message(msg, className = "message_custom", duration = 0) { function sco_message(msg, className = "message_custom", duration = 0) {
var div = document.createElement("div"); var div = document.createElement("div");
div.className = className; div.className = className;
div.innerHTML = msg; div.innerHTML = msg;
document.querySelector("body").appendChild(div); document.querySelector("body").appendChild(div);
if (duration) { if (duration) {
setTimeout(() => { setTimeout(() => {
div.remove(); div.remove();
}, 3000); }, 8000);
} }
} }
function sco_error_message(msg) { function sco_error_message(msg) {
sco_message(msg, className = "message_error", duration = 0); sco_message(msg, (className = "message_error"), (duration = 0));
} }
function get_query_args() { function get_query_args() {
var s = window.location.search; // eg "?x=1&y=2" var s = window.location.search; // eg "?x=1&y=2"
var vars = {}; var vars = {};
s.replace( s.replace(
/[?&]+([^=&]+)=?([^&]*)?/gi, // regexp /[?&]+([^=&]+)=?([^&]*)?/gi, // regexp
function (m, key, value) { // callback function (m, key, value) {
vars[key] = value !== undefined ? value : ''; // callback
} vars[key] = value !== undefined ? value : "";
); }
return vars; );
return vars;
} }
// Tables (gen_tables) // Tables (gen_tables)
$(function () { $(function () {
if ($('table.gt_table').length > 0) { if ($("table.gt_table").length > 0) {
var table_options = { var table_options = {
"paging": false, paging: false,
"searching": false, searching: false,
"info": false, info: false,
/* "autoWidth" : false, */ /* "autoWidth" : false, */
"fixedHeader": { fixedHeader: {
"header": true, header: true,
"footer": true footer: true,
}, },
"orderCellsTop": true, // cellules ligne 1 pour tri orderCellsTop: true, // cellules ligne 1 pour tri
"aaSorting": [], // Prevent initial sorting aaSorting: [], // Prevent initial sorting
}; };
$('table.gt_table').DataTable(table_options); $("table.gt_table").DataTable(table_options);
table_options["searching"] = true; table_options["searching"] = true;
$('table.gt_table_searchable').DataTable(table_options); $("table.gt_table_searchable").DataTable(table_options);
} }
}); });
// Show tags (readonly) // Show tags (readonly)
function readOnlyTags(nodes) { function readOnlyTags(nodes) {
// nodes are textareas, hide them and create a span showing tags // nodes are textareas, hide them and create a span showing tags
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
var node = $(nodes[i]); var node = $(nodes[i]);
node.hide(); node.hide();
var tags = nodes[i].value.split(','); var tags = nodes[i].value.split(",");
node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>'); node.after(
} '<span class="ro_tags"><span class="ro_tag">' +
tags.join('</span><span class="ro_tag">') +
"</span></span>"
);
}
} }
/* Editeur pour champs /* Editeur pour champs
@ -142,150 +156,161 @@ function readOnlyTags(nodes) {
* Inspiré par les codes et conseils de Seb. L. * Inspiré par les codes et conseils de Seb. L.
*/ */
class ScoFieldEditor { class ScoFieldEditor {
constructor(selector, save_url, read_only) { constructor(selector, save_url, read_only) {
this.save_url = save_url; this.save_url = save_url;
this.read_only = read_only; this.read_only = read_only;
this.selector = selector; this.selector = selector;
this.installListeners(); this.installListeners();
}
// Enregistre l'élément obj
save(obj) {
var value = obj.innerText.trim();
if (value.length == 0) {
value = "";
} }
// Enregistre l'élément obj if (value == obj.dataset.value) {
save(obj) { return true; // Aucune modification, pas d'enregistrement mais on continue normalement
var value = obj.innerText.trim(); }
if (value.length == 0) { obj.classList.add("sco_wait");
value = ""; // DEBUG
} // console.log(`
if (value == obj.dataset.value) { // data : ${value},
return true; // Aucune modification, pas d'enregistrement mais on continue normalement // id: ${obj.dataset.oid}
} // `);
obj.classList.add("sco_wait");
// DEBUG
// console.log(`
// data : ${value},
// id: ${obj.dataset.oid}
// `);
$.post(this.save_url, $.post(
{ this.save_url,
oid: obj.dataset.oid, {
value: value, oid: obj.dataset.oid,
}, value: value,
function (result) { },
obj.classList.remove("sco_wait"); function (result) {
obj.classList.add("sco_modified"); obj.classList.remove("sco_wait");
} obj.classList.add("sco_modified");
); }
return true; );
return true;
}
/*****************************/
/* Gestion des évènements */
/*****************************/
installListeners() {
if (this.read_only) {
return;
} }
/*****************************/ document.body.addEventListener("keydown", this.key);
/* Gestion des évènements */ let editor = this;
/*****************************/ this.handleSelectCell = (event) => {
installListeners() { editor.selectCell(event);
if (this.read_only) { };
return; this.handleModifCell = (event) => {
editor.modifCell(event);
};
this.handleBlur = (event) => {
editor.blurCell(event);
};
this.handleKeyCell = (event) => {
editor.keyCell(event);
};
document.querySelectorAll(this.selector).forEach((cellule) => {
cellule.addEventListener("click", this.handleSelectCell);
cellule.addEventListener("dblclick", this.handleModifCell);
cellule.addEventListener("blur", this.handleBlur);
});
}
/*********************************/
/* Interaction avec les cellules */
/*********************************/
blurCell(event) {
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
}
selectCell(event) {
let obj = event.currentTarget;
if (obj) {
if (obj.classList.contains("sco_modifying")) {
return; // Cellule en cours de modification, ne pas sélectionner.
}
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
} }
document.body.addEventListener("keydown", this.key); }
let editor = this;
this.handleSelectCell = (event) => { editor.selectCell(event) };
this.handleModifCell = (event) => { editor.modifCell(event) };
this.handleBlur = (event) => { editor.blurCell(event) };
this.handleKeyCell = (event) => { editor.keyCell(event) };
document.querySelectorAll(this.selector).forEach(cellule => {
cellule.addEventListener("click", this.handleSelectCell);
cellule.addEventListener("dblclick", this.handleModifCell);
cellule.addEventListener("blur", this.handleBlur);
});
}
/*********************************/
/* Interaction avec les cellules */
/*********************************/
blurCell(event) {
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
}
selectCell(event) {
let obj = event.currentTarget;
if (obj) {
if (obj.classList.contains("sco_modifying")) {
return; // Cellule en cours de modification, ne pas sélectionner.
}
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
this.unselectCell(); this.unselectCell();
obj.classList.add("sco_selected"); obj.classList.add("sco_selected");
} }
} }
unselectCell() { unselectCell() {
document.querySelectorAll(".sco_selected, .sco_modifying").forEach(cellule => { document
cellule.classList.remove("sco_selected", "sco_modifying"); .querySelectorAll(".sco_selected, .sco_modifying")
cellule.removeAttribute("contentEditable"); .forEach((cellule) => {
cellule.removeEventListener("keydown", this.handleKeyCell); cellule.classList.remove("sco_selected", "sco_modifying");
}); cellule.removeAttribute("contentEditable");
} cellule.removeEventListener("keydown", this.handleKeyCell);
modifCell(event) { });
let obj = event.currentTarget; }
if (obj) { modifCell(event) {
obj.classList.add("sco_modifying"); let obj = event.currentTarget;
obj.contentEditable = true; if (obj) {
obj.addEventListener("keydown", this.handleKeyCell); obj.classList.add("sco_modifying");
obj.focus(); obj.contentEditable = true;
} obj.addEventListener("keydown", this.handleKeyCell);
} obj.focus();
key(event) { }
switch (event.key) { }
case "Enter": key(event) {
this.modifCell(document.querySelector(".sco_selected")); switch (event.key) {
event.preventDefault(); case "Enter":
break; this.modifCell(document.querySelector(".sco_selected"));
} event.preventDefault();
} break;
keyCell(event) { }
let obj = event.currentTarget; }
if (obj) { keyCell(event) {
if (event.key == "Enter") { let obj = event.currentTarget;
event.preventDefault(); if (obj) {
event.stopPropagation(); if (event.key == "Enter") {
if (!this.save(obj)) { event.preventDefault();
return event.stopPropagation();
} if (!this.save(obj)) {
obj.classList.remove("sco_modifying"); return;
// ArrowMove(0, 1);
// modifCell(document.querySelector(".sco_selected"));
this.unselectCell();
}
} }
obj.classList.remove("sco_modifying");
// ArrowMove(0, 1);
// modifCell(document.querySelector(".sco_selected"));
this.unselectCell();
}
} }
}
} }
function getCurrentScriptPath() { function getCurrentScriptPath() {
// Get all the script elements on the page // Get all the script elements on the page
var scripts = document.getElementsByTagName('script'); var scripts = document.getElementsByTagName("script");
// Find the last script element (which is the currently executing script) // Find the last script element (which is the currently executing script)
var currentScript = scripts[scripts.length - 1]; var currentScript = scripts[scripts.length - 1];
// Retrieve the src attribute of the script element // Retrieve the src attribute of the script element
var scriptPath = currentScript.src; var scriptPath = currentScript.src;
return scriptPath; return scriptPath;
} }
function removeLastTwoComponents(path) { function removeLastTwoComponents(path) {
// Split the path into individual components // Split the path into individual components
var components = path.split('/'); var components = path.split("/");
// Remove the last two components (filename and enclosing directory) // Remove the last two components (filename and enclosing directory)
components.splice(-2); components.splice(-2);
// Join the remaining components back into a path // Join the remaining components back into a path
var newPath = components.join('/'); var newPath = components.join("/");
return newPath; return newPath;
} }

View File

@ -74,9 +74,8 @@ class TableJury(TableRecap):
self.freq_codes_annuels[deca.code_valide] += 1 self.freq_codes_annuels[deca.code_valide] += 1
row.add_nb_rcues_cell() row.add_nb_rcues_cell()
# --- Les RCUEs # --- Les RCUEs
for rcue in deca.rcues_annee: for dec_rcue in deca.get_decisions_rcues_annee():
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id) if dec_rcue.rcue.complete:
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
row.add_rcue_cols(dec_rcue) row.add_rcue_cols(dec_rcue)
self.freq_codes_annuels["total"] = len(self.rows) self.freq_codes_annuels["total"] = len(self.rows)
@ -205,7 +204,7 @@ class RowJury(RowRecap):
else: else:
classes.append("moy_ue_valid") classes.append("moy_ue_valid")
if len(deca.rcues_annee) > 0: if len(deca.get_decisions_rcues_annee()) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair # 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: 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] moy = deca.res_pair.etud_moy_gen[deca.etud.id]
@ -260,9 +259,11 @@ class RowJury(RowRecap):
def add_rcue_cols(self, dec_rcue: DecisionsProposeesRCUE): def add_rcue_cols(self, dec_rcue: DecisionsProposeesRCUE):
"2 cells: moyenne du RCUE, code enregistré" "2 cells: moyenne du RCUE, code enregistré"
self.table.group_titles["rcue"] = "RCUEs en cours"
rcue = dec_rcue.rcue rcue = dec_rcue.rcue
col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id if not rcue.complete:
return
col_id = f"moy_rcue_{rcue.niveau.id}" # le niveau_id
self.table.group_titles["rcue"] = "RCUEs en cours"
note_class = "" note_class = ""
val = rcue.moy_rcue val = rcue.moy_rcue
if isinstance(val, float): if isinstance(val, float):

View File

@ -130,6 +130,12 @@
<td class="amue">CODJ</td> <td class="amue">CODJ</td>
<td>Acquis par décision du jury</td> <td>Acquis par décision du jury</td>
</tr> </tr>
<tr>
<td>ADSUP</td>
<td>{{codes["ADSUP"]}}</td>
<td class="amue"></td>
<td>Acquis parce que le niveau de compétence supérieur est acquis</td>
</tr>
<tr> <tr>
<td>AJ</td> <td>AJ</td>
<td>{{codes["AJ"]}}</td> <td>{{codes["AJ"]}}</td>
@ -200,6 +206,12 @@
<td class="amue"></td> <td class="amue"></td>
<td>Acquis par décision de jury sur le RCUE (ECTS acquis)</td> <td>Acquis par décision de jury sur le RCUE (ECTS acquis)</td>
</tr> </tr>
<tr>
<td>ADSUP</td>
<td>{{codes["ADSUP"]}}</td>
<td class="amue"></td>
<td>Acquis parce que le niveau de compétence supérieur est acquis</td>
</tr>
<tr> <tr>
<td>AJ</td> <td>AJ</td>
<td>{{codes["AJ"]}}</td> <td>{{codes["AJ"]}}</td>

View File

@ -13,6 +13,7 @@
<ul> <ul>
<li>Semestre: {{ue.semestre_idx}}</li> <li>Semestre: {{ue.semestre_idx}}</li>
<li>Code: <tt>{{ue.ue_code}}</tt></li> <li>Code: <tt>{{ue.ue_code}}</tt></li>
<li>ECTS: <b>{{ue.ects or 0}}</b></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 or "aucun"}}</li> <li>Code Apogée: {{ue.code_apogee or "aucun"}}</li>

View File

@ -2430,7 +2430,7 @@ def formsemestre_validation_but(
) )
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if len(deca.rcues_annee) == 0: if len(deca.get_decisions_rcues_annee()) == 0:
return jury_but_view.jury_but_semestriel( return jury_but_view.jury_but_semestriel(
formsemestre, etud, read_only, navigation_div=navigation_div formsemestre, etud, read_only, navigation_div=navigation_div
) )
@ -2459,22 +2459,25 @@ def formsemestre_validation_but(
warning = "" warning = ""
if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau):
if deca.a_cheval: warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)}
warning += """<div class="warning">Attention: regroupements RCUE
entre années (redoublement).</div>"""
else:
warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)}
niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>""" niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.</div>"""
if (deca.parcour is None) and len(formsemestre.parcours) > 0: if (deca.parcour is None) and len(formsemestre.parcours) > 0:
warning += ( warning += (
"""<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>""" """<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>"""
) )
if deca.formsemestre_impair and deca.inscription_etat_impair != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(deca.inscription_etat_impair, "inconnu?") if deca.formsemestre_impair:
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_impair.semestre_id}</div>""" inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id)
if deca.formsemestre_pair and deca.inscription_etat_pair != scu.INSCRIT: if (not inscription) or inscription.etat != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(deca.inscription_etat_pair, "inconnu?") etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>""" warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_impair.semestre_id}</div>"""
if deca.formsemestre_pair:
inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id)
if (not inscription) or inscription.etat != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>"""
if deca.has_notes_en_attente(): if deca.has_notes_en_attente():
warning += f"""<div class="warning">{etud.nomprenom} a des notes en ATTente. warning += f"""<div class="warning">{etud.nomprenom} a des notes en ATTente.
Vous devriez régler cela avant de statuer en jury !</div>""" Vous devriez régler cela avant de statuer en jury !</div>"""
@ -2531,7 +2534,7 @@ def formsemestre_validation_but(
else: else:
erase_span = f"""<a href="{ erase_span = f"""<a href="{
url_for("notes.formsemestre_jury_but_erase", url_for("notes.formsemestre_jury_but_erase",
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id, scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre.id,
etudid=deca.etud.id)}" class="stdlink" etudid=deca.etud.id)}" class="stdlink"
title="efface décisions issues des jurys de cette année" title="efface décisions issues des jurys de cette année"
>effacer décisions de ce jury</a> >effacer décisions de ce jury</a>
@ -2564,15 +2567,7 @@ def formsemestre_validation_but(
) )
H.append(navigation_div) H.append(navigation_div)
H.append("</form>") 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>
"""
)
# Affichage cursus BUT # Affichage cursus BUT
but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation) but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation)
H += [ H += [
@ -2595,7 +2590,14 @@ def formsemestre_validation_but(
codes=ScoDocSiteConfig.get_codes_apo_dict(), codes=ScoDocSiteConfig.get_codes_apo_dict(),
) )
) )
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>
"""
)
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()

View File

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