From 3325b41690888b7c2386bdc7b86b8e5a7d5968ad Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet <emmanuel.viennet@gmail.com>
Date: Fri, 30 Jun 2023 17:26:41 +0200
Subject: [PATCH] =?UTF-8?q?Interface=20pour=20UE=20externes=20et=20=C3=A9d?=
=?UTF-8?q?itions=20des=20validations?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/api/jury.py | 6 +-
app/models/but_validations.py | 10 +++-
app/models/formsemestre.py | 4 +-
app/models/validations.py | 24 ++++++--
app/scodoc/codes_cursus.py | 2 +
app/scodoc/sco_formsemestre_validation.py | 59 +++++++++++++------
app/scodoc/sco_page_etud.py | 28 +++++----
app/static/css/jury_delete_manual.css | 4 ++
app/static/js/validate_previous_ue.js | 31 +++++-----
.../jury/erase_decisions_annee_formation.j2 | 26 ++++++++
app/views/notes.py | 28 +++++----
11 files changed, 157 insertions(+), 65 deletions(-)
diff --git a/app/api/jury.py b/app/api/jury.py
index 6103d1164..6f0710770 100644
--- a/app/api/jury.py
+++ b/app/api/jury.py
@@ -114,16 +114,16 @@ def _validation_ue_delete(etudid: int, validation_id: int):
# rattachées à un formsemestre)
if not g.scodoc_dept: # accès API
if not current_user.has_permission(Permission.ScoEtudInscrit):
- return json_error(403, "validation_delete: non autorise")
+ return json_error(403, "opération non autorisée (117)")
else:
if validation.formsemestre:
if (
validation.formsemestre.dept_id != g.scodoc_dept_id
) or not validation.formsemestre.can_edit_jury():
- return json_error(403, "validation_delete: non autorise")
+ return json_error(403, "opération non autorisée (123)")
elif not current_user.has_permission(Permission.ScoEtudInscrit):
# Validation non rattachée à un semestre: on doit être chef
- return json_error(403, "validation_delete: non autorise")
+ return json_error(403, "opération non autorisée (126)")
log(f"validation_ue_delete: etuid={etudid} {validation}")
db.session.delete(validation)
diff --git a/app/models/but_validations.py b/app/models/but_validations.py
index aedf4cbaf..fcab132bd 100644
--- a/app/models/but_validations.py
+++ b/app/models/but_validations.py
@@ -158,8 +158,16 @@ class ApcValidationAnnee(db.Model):
if self.date
else "(sans date)"
)
+ link = (
+ self.formsemestre.html_link_status(
+ label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
+ title=self.formsemestre.titre_annee(),
+ )
+ if self.formsemestre
+ else "externe/antérieure"
+ )
return f"""Validation <b>année BUT{self.ordre}</b> émise par
- {self.formsemestre.html_link_status() if self.formsemestre else "-"}
+ {link}
: <b>{self.code}</b>
{date_str}
"""
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index cd296353f..64668fa16 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -163,12 +163,12 @@ class FormSemestre(db.Model):
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
- def html_link_status(self) -> str:
+ def html_link_status(self, label=None, title=None) -> str:
"html link to status page"
return f"""<a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
formsemestre_id=self.id,)
- }">{self.titre_mois()}</a>
+ }" title="{title or ''}">{label or self.titre_mois()}</a>
"""
@classmethod
diff --git a/app/models/validations.py b/app/models/validations.py
index 9e2cf5e27..91f17f605 100644
--- a/app/models/validations.py
+++ b/app/models/validations.py
@@ -94,6 +94,14 @@ class ScolarFormSemestreValidation(db.Model):
if self.moy_ue is not None
else ""
)
+ link = (
+ self.formsemestre.html_link_status(
+ label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
+ title=self.formsemestre.titre_annee(),
+ )
+ if self.formsemestre
+ else "externe/antérieure"
+ )
return f"""Validation
{'<span class="redboldtext">externe</span>' if self.is_external else ""}
de l'UE <b>{self.ue.acronyme}</b>
@@ -101,9 +109,7 @@ class ScolarFormSemestreValidation(db.Model):
+ ", ".join([p.code for p in self.ue.parcours]))
+ "</span>"
if self.ue.parcours else ""}
- de {self.ue.formation.acronyme}
- {("émise par " + self.formsemestre.html_link_status())
- if self.formsemestre else "externe/antérieure"}
+ {("émise par " + link)}
: <b>{self.code}</b>{moyenne}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
@@ -149,10 +155,16 @@ class ScolarAutorisationInscription(db.Model):
def html(self) -> str:
"Affichage html"
+ link = (
+ self.origin_formsemestre.html_link_status(
+ label=f"{self.origin_formsemestre.titre_formation(with_sem_idx=1)}",
+ title=self.origin_formsemestre.titre_annee(),
+ )
+ if self.origin_formsemestre
+ else "externe/antérieure"
+ )
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
- {self.origin_formsemestre.html_link_status()
- if self.origin_formsemestre
- else "-"}
+ {link}
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
"""
diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py
index 85d14b957..dff4ead7b 100644
--- a/app/scodoc/codes_cursus.py
+++ b/app/scodoc/codes_cursus.py
@@ -196,6 +196,8 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
CODES_SEM_REO = {NAR} # reorientation
+# Les codes d'UEs
+CODES_JURY_UE = {ADM, CMP, ADJ, ADJR, ADSUP, AJ, ATJ, RAT, DEF, ABAN, DEM, UEBSL}
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
"UE validée"
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index 6234e5f53..f8ce98471 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -398,7 +398,7 @@ def formsemestre_validation_etud(
selected_choice = choice
break
if not selected_choice:
- raise ValueError("code choix invalide ! (%s)" % codechoice)
+ raise ValueError(f"code choix invalide ! ({codechoice})")
#
Se.valide_decision(selected_choice) # enregistre
return _redirect_valid_choice(
@@ -1132,6 +1132,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
},
)
)
+ ue_codes = sorted(codes_cursus.CODES_JURY_UE)
form_descr += [
(
"date",
@@ -1152,6 +1153,18 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
"title": "Moyenne (/20) obtenue dans cette UE:",
},
),
+ (
+ "code_jury",
+ {
+ "input_type": "menu",
+ "title": "Code jury",
+ "explanation": " code donné par le jury (ADM si validée normalement)",
+ "allow_null": True,
+ "allowed_values": [""] + ue_codes,
+ "labels": ["-"] + ue_codes,
+ "default": ADM,
+ },
+ ),
]
tf = TrivialFormulator(
request.base_url,
@@ -1173,17 +1186,20 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
de {etud.html_link_fiche()}
</h2>
- <p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement,
+ <p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
<em>dans un semestre hors ScoDoc</em>.</p>
- <p class="expl"><b>Les UE validées dans ScoDoc sont déjà
- automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant
- suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré
- <b>sans ScoDoc</b> et qui <b>redouble</b> ce semestre
- (<em>pour les semestres précédents gérés avec ScoDoc,
- passer par la page jury normale)</em>).
+ <p class="expl"><b>Les UE validées dans ScoDoc sont
+ automatiquement prises en compte</b>.
+ </p>
+ <p>Cette page est surtout utile pour les étudiants ayant
+ suivi un début de cursus dans <b>un autre établissement</b>, ou qui
+ ont suivi une UE à l'étranger ou dans un semestre géré <b>sans ScoDoc</b>.
+ </p>
+ <p>Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.
+ </p>
+ <p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
+ l'attribution des ECTS si le code jury est validant (ADM).
</p>
- <p>Notez que l'UE est validée (ADM), avec enregistrement immédiat de la décision et
- l'attribution des ECTS.</p>
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
{_get_etud_ue_cap_html(etud, formsemestre)}
@@ -1221,12 +1237,16 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
else:
semestre_id = None
+ if tf[2]["code_jury"] not in CODES_JURY_UE:
+ flash("Code UE invalide")
+ return flask.redirect(dest_url)
do_formsemestre_validate_previous_ue(
formsemestre,
etud.id,
tf[2]["ue_id"],
tf[2]["moy_ue"],
tf[2]["date"],
+ code=tf[2]["code_jury"],
semestre_id=semestre_id,
)
flash("Validation d'UE enregistrée")
@@ -1258,7 +1278,7 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
<div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()},
sur des semestres ou déclarées comme "antérieures" (externes).
</div>
- <ul>"""
+ <ul class="liste_validations">"""
]
for validation in validations:
if validation.formsemestre_id is None:
@@ -1267,17 +1287,20 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
origine = f", du semestre {formsemestre.html_link_status()}"
if validation.semestre_id is not None:
origine += f" (<b>S{validation.semestre_id}</b>)"
- H.append(
- f"""
- <li>{validation.html()}
+ H.append(f"""<li>{validation.html()}""")
+ if validation.formsemestre.can_edit_jury():
+ H.append(
+ f"""
<form class="inline-form">
<button
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
>effacer</button>
</form>
- </li>
- """,
- )
+ """,
+ )
+ else:
+ H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
+ H.append("</li>")
H.append("</ul></div>")
return "\n".join(H)
@@ -1300,7 +1323,7 @@ def do_formsemestre_validate_previous_ue(
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
cnx = ndb.GetDBConnexion()
- if ue_coefficient != None:
+ if ue_coefficient is not None:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre.id, ue_id, ue_coefficient
)
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index e2dbd1cbc..cbb26f08f 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -59,11 +59,13 @@ from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_t
from app.scodoc.sco_permissions import Permission
-def _menu_scolarite(authuser, sem: dict, etudid: int):
+def _menu_scolarite(
+ authuser, formsemestre: FormSemestre, etudid: int, etat_inscription: str
+):
"""HTML pour menu "scolarite" pour un etudiant dans un semestre.
Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant.
"""
- locked = not sem["etat"]
+ locked = not formsemestre.etat
if locked:
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
return lockicon # no menu
@@ -71,10 +73,10 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
Permission.ScoEtudInscrit
) and not authuser.has_permission(Permission.ScoEtudChangeGroups):
return "" # no menu
- ins = sem["ins"]
- args = {"etudid": etudid, "formsemestre_id": ins["formsemestre_id"]}
- if ins["etat"] != "D":
+ args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
+
+ if etat_inscription != scu.DEMISSION:
dem_title = "Démission"
dem_url = "scolar.form_dem"
else:
@@ -82,14 +84,14 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
dem_url = "scolar.do_cancel_dem"
# Note: seul un etudiant inscrit (I) peut devenir défaillant.
- if ins["etat"] != codes_cursus.DEF:
+ if etat_inscription != codes_cursus.DEF:
def_title = "Déclarer défaillance"
def_url = "scolar.form_def"
- elif ins["etat"] == codes_cursus.DEF:
+ elif etat_inscription == codes_cursus.DEF:
def_title = "Annuler la défaillance"
def_url = "scolar.do_cancel_def"
def_enabled = (
- (ins["etat"] != "D")
+ (etat_inscription != scu.DEMISSION)
and authuser.has_permission(Permission.ScoEtudInscrit)
and not locked
)
@@ -128,6 +130,12 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
"enabled": authuser.has_permission(Permission.ScoEtudInscrit)
and not locked,
},
+ {
+ "title": "Gérer les validations d'UEs antérieures",
+ "endpoint": "notes.formsemestre_validate_previous_ue",
+ "args": args,
+ "enabled": formsemestre.can_edit_jury(),
+ },
{
"title": "Inscrire à un autre semestre",
"endpoint": "notes.formsemestre_inscription_with_modules_form",
@@ -250,8 +258,8 @@ def ficheEtud(etudid=None):
info["last_formsemestre_id"] = ""
sem_info = {}
for sem in info["sems"]:
+ formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
if sem["ins"]["etat"] != scu.INSCRIT:
- formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
descr, _ = etud_descr_situation_semestre(
etudid,
formsemestre,
@@ -283,7 +291,7 @@ def ficheEtud(etudid=None):
)
grlink = ", ".join(grlinks)
# infos ajoutées au semestre dans le parcours (groupe, menu)
- menu = _menu_scolarite(authuser, sem, etudid)
+ menu = _menu_scolarite(authuser, formsemestre, etudid, sem["ins"]["etat"])
if menu:
sem_info[sem["formsemestre_id"]] = (
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
diff --git a/app/static/css/jury_delete_manual.css b/app/static/css/jury_delete_manual.css
index d41e4f802..eda3bc633 100644
--- a/app/static/css/jury_delete_manual.css
+++ b/app/static/css/jury_delete_manual.css
@@ -7,3 +7,7 @@ div.jury_decisions_list div {
span.parcours {
color:blueviolet;
}
+
+div.ue_list_etud_validations ul.liste_validations li {
+ margin-bottom: 8px;
+}
\ No newline at end of file
diff --git a/app/static/js/validate_previous_ue.js b/app/static/js/validate_previous_ue.js
index 22655f498..c2fe3c888 100644
--- a/app/static/js/validate_previous_ue.js
+++ b/app/static/js/validate_previous_ue.js
@@ -11,27 +11,30 @@ document.addEventListener("DOMContentLoaded", () => {
// Handle button click event here
event.preventDefault();
const etudid = event.target.dataset.etudid;
- const v_id = event.target.dataset.v_id;
+ const validation_id = event.target.dataset.v_id;
const validation_type = event.target.dataset.type;
if (confirm("Supprimer cette validation ?")) {
- fetch(
- `${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`,
- {
- method: "POST",
- }
- ).then((response) => {
- // Handle the response
- if (response.ok) {
- location.reload();
- } else {
- throw new Error("Request failed");
- }
- });
+ delete_validation(etudid, validation_type, validation_id);
}
});
});
});
+async function delete_validation(etudid, validation_type, validation_id) {
+ const response = await fetch(
+ `${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`,
+ {
+ method: "POST",
+ }
+ );
+ if (response.ok) {
+ location.reload();
+ } else {
+ const data = await response.json();
+ sco_error_message("erreur: " + data.message);
+ }
+}
+
function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value;
if (ue_id) {
diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2
index 0c231b532..fc8fd0ce0 100644
--- a/app/templates/jury/erase_decisions_annee_formation.j2
+++ b/app/templates/jury/erase_decisions_annee_formation.j2
@@ -34,6 +34,32 @@ quelle que soit leur origine.</p>
{% endif %}
</form>
</div>
+
+<div class="sco_box">
+<div class="sco_box_title">Autres actions:</div>
+<ul>
+ <li><a class="stdlink" href="{{
+ url_for('notes.jury_delete_manual',
+ scodoc_dept=g.scodoc_dept,
+ etudid=etud.id
+ )
+ }}">effacer les décisions une à une</a>
+ </li>
+ {% if formsemestre_origine is not none %}
+ <li><a class="stdlink" href="{{
+ url_for('notes.formsemestre_jury_but_erase',
+ scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_origine.id,
+ etudid=etud.id, only_one_sem=1)
+ }}">
+ effacer seulement les décisions émises par le semestre
+ {{formsemestre_origine.titre_formation(with_sem_idx=1)|safe}}
+ (efface aussi la décision annuelle)
+ </a>
+ </li>
+ {% endif %}
+</ul>
+</div>
+
{% endif %}
diff --git a/app/views/notes.py b/app/views/notes.py
index d6a77c614..4bc79ebae 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -2534,21 +2534,20 @@ def formsemestre_validation_but(
</div>"""
)
else:
- erase_span = f"""<a href="{
- url_for("notes.formsemestre_jury_but_erase",
- scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre.id,
- etudid=deca.etud.id)}" class="stdlink"
- title="efface décisions issues des jurys de cette année"
- >effacer décisions de ce jury</a>
-
+ erase_span = f"""
<a style="margin-left: 16px;" class="stdlink"
href="{
url_for("notes.erase_decisions_annee_formation",
scodoc_dept=g.scodoc_dept, formation_id=deca.formsemestre.formation.id,
- etudid=deca.etud.id, annee=deca.annee_but)}"
- title="efface toutes décisions concernant le BUT{deca.annee_but}
- de cet étudiant (même extérieures ou issues d'un redoublement)"
- >effacer toutes ses décisions de BUT{deca.annee_but}</a>
+ etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)}"
+ >effacer des décisions de jury</a>
+
+ <a style="margin-left: 16px;" class="stdlink"
+ href="{
+ url_for("notes.formsemestre_validate_previous_ue",
+ scodoc_dept=g.scodoc_dept,
+ etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
+ >enregistrer des UEs antérieures</a>
"""
H.append(
f"""<div class="but_settings">
@@ -2966,6 +2965,12 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
)
)
validations = jury.erase_decisions_annee_formation(etud, formation, annee)
+ formsemestre_origine_id = request.args.get("formsemestre_id")
+ formsemestre_origine = (
+ FormSemestre.query.get_or_404(formsemestre_origine_id)
+ if formsemestre_origine_id
+ else None
+ )
return render_template(
"jury/erase_decisions_annee_formation.j2",
annee=annee,
@@ -2974,6 +2979,7 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
),
etud=etud,
formation=formation,
+ formsemestre_origine=formsemestre_origine,
validations=validations,
sco=ScoData(),
title=f"Effacer décisions de jury {etud.nom} - année {annee}",
--
GitLab