Améliore UI gestion des UE antérieures

This commit is contained in:
Emmanuel Viennet 2023-06-25 11:49:11 +02:00
parent 52db344926
commit 66983ff767
12 changed files with 365 additions and 280 deletions

View File

@ -10,7 +10,7 @@
from flask import g, url_for from flask import g, url_for
from flask_json import as_json from flask_json import as_json
from flask_login import login_required from flask_login import current_user, login_required
import app import app
from app import db, log from app import db, log
@ -29,6 +29,7 @@ from app.models import (
) )
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error
@bp.route("/formsemestre/<int:formsemestre_id>/decisions_jury") @bp.route("/formsemestre/<int:formsemestre_id>/decisions_jury")
@ -73,7 +74,7 @@ def _news_delete_jury_etud(etud: Identite):
) )
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoEtudInscrit) @permission_required(Permission.ScoView)
@as_json @as_json
def validation_ue_delete(etudid: int, validation_id: int): def validation_ue_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation"
@ -90,7 +91,7 @@ def validation_ue_delete(etudid: int, validation_id: int):
) )
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoEtudInscrit) @permission_required(Permission.ScoView)
@as_json @as_json
def validation_formsemestre_delete(etudid: int, validation_id: int): def validation_formsemestre_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation"
@ -106,6 +107,24 @@ def _validation_ue_delete(etudid: int, validation_id: int):
validation = ScolarFormSemestreValidation.query.filter_by( validation = ScolarFormSemestreValidation.query.filter_by(
id=validation_id, etudid=etudid id=validation_id, etudid=etudid
).first_or_404() ).first_or_404()
# Vérification de la permission:
# A le droit de supprimer cette validation: le chef de dept ou quelqu'un ayant
# le droit de saisir des décisions de jury dans le formsemestre concerné s'il y en a un
# (c'est le cas pour les validations de jury, mais pas pour les "antérieures" non
# 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")
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")
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")
log(f"validation_ue_delete: etuid={etudid} {validation}") log(f"validation_ue_delete: etuid={etudid} {validation}")
db.session.delete(validation) db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud) sco_cache.invalidate_formsemestre_etud(etud)

View File

@ -8,6 +8,8 @@ from app import log
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models.events import Scolog from app.models.events import Scolog
from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu
class ScolarFormSemestreValidation(db.Model): class ScolarFormSemestreValidation(db.Model):
@ -70,6 +72,14 @@ class ScolarFormSemestreValidation(db.Model):
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")}"""
def delete(self):
"Efface cette validation"
log(f"{self.__class__.__name__}.delete({self})")
etud = self.etud
db.session.delete(self)
db.session.commit()
sco_cache.invalidate_formsemestre_etud(etud)
def to_dict(self) -> dict: def to_dict(self) -> dict:
"as a dict" "as a dict"
d = dict(self.__dict__) d = dict(self.__dict__)
@ -79,15 +89,22 @@ class ScolarFormSemestreValidation(db.Model):
def html(self, detail=False) -> str: def html(self, detail=False) -> str:
"Affichage html" "Affichage html"
if self.ue_id is not None: if self.ue_id is not None:
return f"""Validation de l'UE <b>{self.ue.acronyme}</b> moyenne = (
f", moyenne {scu.fmt_note(self.moy_ue)}/20 "
if self.moy_ue is not None
else ""
)
return f"""Validation
{'<span class="redboldtext">externe</span>' if self.is_external else ""}
de l'UE <b>{self.ue.acronyme}</b>
{('parcours <span class="parcours">' {('parcours <span class="parcours">'
+ ", ".join([p.code for p in self.ue.parcours])) + ", ".join([p.code for p in self.ue.parcours]))
+ "</span>" + "</span>"
if self.ue.parcours else ""} if self.ue.parcours else ""}
de {self.ue.formation.acronyme} de {self.ue.formation.acronyme}
{("émise par " + self.formsemestre.html_link_status()) {("émise par " + self.formsemestre.html_link_status())
if self.formsemestre else ""} if self.formsemestre else "externe/antérieure"}
: <b>{self.code}</b> : <b>{self.code}</b>{moyenne}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")} le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
""" """
else: else:

View File

@ -1238,7 +1238,7 @@ def make_menu_autres_operations(
"enabled": current_user.has_permission(Permission.ScoImplement), "enabled": current_user.has_permission(Permission.ScoImplement),
}, },
{ {
"title": "Enregistrer une validation d'UE antérieure", "title": "Gérer les validations d'UEs antérieures",
"endpoint": "notes.formsemestre_validate_previous_ue", "endpoint": "notes.formsemestre_validate_previous_ue",
"args": { "args": {
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,

View File

@ -972,7 +972,7 @@ def do_formsemestre_validate_ue(
moy_ue = ue_status["moy"] if ue_status else "" moy_ue = ue_status["moy"] if ue_status else ""
args["moy_ue"] = moy_ue args["moy_ue"] = moy_ue
log("formsemestre_validate_ue: create %s" % args) log("formsemestre_validate_ue: create %s" % args)
if code != None: if code is not None:
scolar_formsemestre_validation_create(cnx, args) scolar_formsemestre_validation_create(cnx, args)
else: else:
log("formsemestre_validate_ue: code is None, not recording validation") log("formsemestre_validate_ue: code is None, not recording validation")

View File

@ -502,7 +502,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
else: else:
clone_form = "" clone_form = ""
bonus_div = """<div id="bonus_description"></div>""" bonus_div = """<div id="bonus_description"></div>"""
ue_div = """<div id="ue_list_code"></div>""" ue_div = """<div id="ue_list_code" class="sco_box sco_green_bg"></div>"""
return ( return (
"\n".join(H) "\n".join(H)
+ tf[1] + tf[1]
@ -1375,13 +1375,12 @@ def _ue_table_modules(
return "\n".join(H) return "\n".join(H)
def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None): def ue_sharing_code(ue_code: str = "", ue_id: int = None, hide_ue_id: int = None):
"""HTML list of UE sharing this code """HTML list of UE sharing this code
Either ue_code or ue_id may be specified. Either ue_code or ue_id may be specified.
hide_ue_id spécifie un id à retirer de la liste. hide_ue_id spécifie un id à retirer de la liste.
""" """
ue_code = str(ue_code) if ue_id is not None:
if ue_id:
ue = UniteEns.query.get_or_404(ue_id) ue = UniteEns.query.get_or_404(ue_id)
if not ue_code: if not ue_code:
ue_code = ue.ue_code ue_code = ue.ue_code
@ -1400,29 +1399,36 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
.filter_by(dept_id=g.scodoc_dept_id) .filter_by(dept_id=g.scodoc_dept_id)
) )
if hide_ue_id: # enlève l'ue de depart if hide_ue_id is not None: # enlève l'ue de depart
q_ues = q_ues.filter(UniteEns.id != hide_ue_id) q_ues = q_ues.filter(UniteEns.id != hide_ue_id)
ues = q_ues.all() ues = q_ues.all()
msg = " dans les formations du département "
if not ues: if not ues:
if ue_id: if ue_id is not None:
return ( return f"""<span class="ue_share">Seule UE avec code {
f"""<span class="ue_share">Seule UE avec code {ue_code or '-'}</span>""" ue_code if ue_code is not None else '-'}{msg}</span>"""
)
else: else:
return f"""<span class="ue_share">Aucune UE avec code {ue_code or '-'}</span>""" return f"""<span class="ue_share">Aucune UE avec code {
ue_code if ue_code is not None else '-'}{msg}</span>"""
H = [] H = []
if ue_id: if ue_id:
H.append( H.append(
f"""<span class="ue_share">Autres UE avec le code {ue_code or '-'}:</span>""" f"""<span class="ue_share">Pour information, autres UEs avec le code {
ue_code if ue_code is not None else '-'}{msg}:</span>"""
) )
else: else:
H.append(f"""<span class="ue_share">UE avec le code {ue_code or '-'}:</span>""") H.append(
f"""<span class="ue_share">UE avec le code {
ue_code if ue_code is not None else '-'}{msg}:</span>"""
)
H.append("<ul>") H.append("<ul>")
for ue in ues: for ue in ues:
H.append( H.append(
f"""<li>{ue.acronyme} ({ue.titre}) dans <a class="stdlink" f"""<li>{ue.acronyme} ({ue.titre}) dans
href="{url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}" <a class="stdlink" href="{
url_for("notes.ue_table",
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
>{ue.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version} >{ue.formation.acronyme} ({ue.formation.titre})</a>, version {ue.formation.version}
</li> </li>
""" """

View File

@ -517,7 +517,7 @@ def _record_ue_validations_and_coefs(
) )
assert code is None or (note) # si code validant, il faut une note assert code is None or (note) # si code validant, il faut une note
sco_formsemestre_validation.do_formsemestre_validate_previous_ue( sco_formsemestre_validation.do_formsemestre_validate_previous_ue(
formsemestre.id, formsemestre,
etud.id, etud.id,
ue.id, ue.id,
note, note,

View File

@ -31,8 +31,9 @@ import time
import flask import flask
from flask import url_for, flash, g, request from flask import url_for, flash, g, request
from app.models.etudiants import Identite import sqlalchemy as sa
from app.models.etudiants import Identite
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import db, log from app import db, log
@ -1081,62 +1082,44 @@ def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
) # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée) ) # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée)
def formsemestre_validate_previous_ue(formsemestre_id, etudid): def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite):
"""Form. saisie UE validée hors ScoDoc """Form. saisie UE validée hors ScoDoc
(pour étudiants arrivant avec un UE antérieurement validée). (pour étudiants arrivant avec un UE antérieurement validée).
""" """
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] formation: Formation = formsemestre.formation
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
H = [
html_sco_header.sco_header(
page_title="Validation UE",
javascripts=["js/validate_previous_ue.js"],
),
'<table style="width: 100%"><tr><td>',
"""<h2 class="formsemestre">%s: validation d'une UE antérieure</h2>"""
% etud["nomprenom"],
(
'</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>'
% (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
)
),
f"""<p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement,
<em>dans un semestre hors ScoDoc</em>.</p>
<p><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>ne pas utiliser pour les semestres précédents !</em>).
</p>
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
l'attribution des ECTS.</p>
<p>On ne peut prendre en compte ici que les UE du cursus <b>{formation.titre}</b></p>
""",
]
# Toutes les UE de cette formation sont présentées (même celles des autres semestres) # Toutes les UEs non bonus de cette formation sont présentées
ues = formation.ues.order_by(UniteEns.numero) # avec indice de semestre <= semestre courant ou NULL
ue_names = ["Choisir..."] + [f"{ue.acronyme} {ue.titre}" for ue in ues] ues = formation.ues.filter(
UniteEns.type != UE_SPORT,
db.or_(
UniteEns.semestre_idx == None,
UniteEns.semestre_idx <= formsemestre.semestre_id,
),
).order_by(UniteEns.semestre_idx, UniteEns.numero)
ue_names = ["Choisir..."] + [
f"""{('S'+str(ue.semestre_idx)+' : ') if ue.semestre_idx is not None else ''
}{ue.acronyme} {ue.titre} ({ue.ue_code or ""})"""
for ue in ues
]
ue_ids = [""] + [ue.id for ue in ues] ue_ids = [""] + [ue.id for ue in ues]
tf = TrivialFormulator( form_descr = [
request.base_url, ("etudid", {"input_type": "hidden"}),
scu.get_request_args(), ("formsemestre_id", {"input_type": "hidden"}),
( (
("etudid", {"input_type": "hidden"}), "ue_id",
("formsemestre_id", {"input_type": "hidden"}), {
( "input_type": "menu",
"ue_id", "title": "Unité d'Enseignement (UE)",
{ "allow_null": False,
"input_type": "menu", "allowed_values": ue_ids,
"title": "Unité d'Enseignement (UE)", "labels": ue_names,
"allow_null": False, },
"allowed_values": ue_ids, ),
"labels": ue_names, ]
}, if not formation.is_apc():
), form_descr.append(
( (
"semestre_id", "semestre_id",
{ {
@ -1147,69 +1130,159 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"allowed_values": [""] + [x for x in range(11)], "allowed_values": [""] + [x for x in range(11)],
"labels": ["-"] + list(range(11)), "labels": ["-"] + list(range(11)),
}, },
), )
( )
"date", form_descr += [
{ (
"input_type": "date", "date",
"size": 9, {
"explanation": "j/m/a", "input_type": "date",
"default": time.strftime("%d/%m/%Y"), "size": 9,
}, "explanation": "j/m/a",
), "default": time.strftime("%d/%m/%Y"),
( },
"moy_ue",
{
"type": "float",
"allow_null": False,
"min_value": 0,
"max_value": 20,
"title": "Moyenne (/20) obtenue dans cette UE:",
},
),
), ),
cancelbutton="Annuler", (
"moy_ue",
{
"type": "float",
"allow_null": False,
"min_value": 0,
"max_value": 20,
"title": "Moyenne (/20) obtenue dans cette UE:",
},
),
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
form_descr,
cancelbutton="Revenir au bulletin",
submitlabel="Enregistrer validation d'UE", submitlabel="Enregistrer validation d'UE",
) )
if tf[0] == 0: if tf[0] == 0:
X = """ return f"""
<div id="ue_list_etud_validations"><!-- filled by get_etud_ue_cap_html --></div> {html_sco_header.sco_header(
<div id="ue_list_code"><!-- filled by ue_sharing_code --></div> page_title="Validation UE antérieure",
""" javascripts=["js/validate_previous_ue.js"],
warn, ue_multiples = check_formation_ues(formation.id) cssstyles=["css/jury_delete_manual.css"],
return "\n".join(H) + tf[1] + X + warn + html_sco_header.sco_footer() etudid=etud.id,
elif tf[0] == -1: )}
return flask.redirect( <h2 class="formsemestre">Gestion des validations d'UEs antérieures
scu.NotesURL() de {etud.html_link_fiche()}
+ "/formsemestre_status?formsemestre_id=" </h2>
+ str(formsemestre_id)
) <p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement,
else: <em>dans un semestre hors ScoDoc</em>.</p>
if tf[2]["semestre_id"]: <p class="expl"><b>Les UE validées dans ScoDoc sont déjà
semestre_id = int(tf[2]["semestre_id"]) automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant
else: suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré
semestre_id = None <b>sans ScoDoc</b> et qui <b>redouble</b> ce semestre
do_formsemestre_validate_previous_ue( (<em>pour les semestres précédents gérés avec ScoDoc,
formsemestre_id, passer par la page jury normale)</em>).
etudid, </p>
tf[2]["ue_id"], <p>Notez que l'UE est validée (ADM), avec enregistrement immédiat de la décision et
tf[2]["moy_ue"], l'attribution des ECTS.</p>
tf[2]["date"], <p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
semestre_id=semestre_id,
) {_get_etud_ue_cap_html(etud, formsemestre)}
flash("Validation d'UE enregistrée")
<div class="sco_box">
<div class="sco_box_title">
Enregistrer une UE antérieure
</div>
{tf[1]}
</div>
<div id="ue_list_code" class="sco_box sco_green_bg">
<!-- filled by ue_sharing_code -->
</div>
{check_formation_ues(formation.id)[0]}
{html_sco_header.sco_footer()}
"""
dest_url = url_for(
"notes.formsemestre_validate_previous_ue",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etud.id,
)
if tf[0] == -1:
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.formsemestre_bulletinetud", "notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre.id,
etudid=etudid, etudid=etud.id,
) )
) )
if tf[2].get("semestre_id"):
semestre_id = int(tf[2]["semestre_id"])
else:
semestre_id = None
do_formsemestre_validate_previous_ue(
formsemestre,
etud.id,
tf[2]["ue_id"],
tf[2]["moy_ue"],
tf[2]["date"],
semestre_id=semestre_id,
)
flash("Validation d'UE enregistrée")
return flask.redirect(dest_url)
def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
"""HTML listant les validations d'UEs pour cet étudiant dans des formations de même
code que celle du formsemestre indiqué.
"""
validations: list[ScolarFormSemestreValidation] = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.join(UniteEns)
.join(Formation)
.filter_by(formation_code=formsemestre.formation.formation_code)
.order_by(
sa.desc(UniteEns.semestre_idx),
UniteEns.acronyme,
sa.desc(ScolarFormSemestreValidation.event_date),
)
.all()
)
if not validations:
return ""
H = [
f"""<div class="sco_box sco_lightgreen_bg ue_list_etud_validations">
<div class="sco_box_title">Validations d'UEs dans cette formation</div>
<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>"""
]
for validation in validations:
if validation.formsemestre_id is None:
origine = " enregistrée d'un parcours antérieur (hors ScoDoc)"
else:
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()}
<form class="inline-form">
<button
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
>effacer</button>
</form>
</li>
""",
)
H.append("</ul></div>")
return "\n".join(H)
def do_formsemestre_validate_previous_ue( def do_formsemestre_validate_previous_ue(
formsemestre_id, formsemestre: FormSemestre,
etudid, etudid,
ue_id, ue_id,
moy_ue, moy_ue,
@ -1222,21 +1295,20 @@ def do_formsemestre_validate_previous_ue(
Si le coefficient est spécifié, modifie le coefficient de Si le coefficient est spécifié, modifie le coefficient de
cette UE (utile seulement pour les semestres extérieurs). cette UE (utile seulement pour les semestres extérieurs).
""" """
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ue: UniteEns = UniteEns.query.get_or_404(ue_id) ue: UniteEns = UniteEns.query.get_or_404(ue_id)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
if ue_coefficient != None: if ue_coefficient != None:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create( sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre_id, ue_id, ue_coefficient cnx, formsemestre.id, ue_id, ue_coefficient
) )
else: else:
sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id) sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre.id, ue_id)
sco_cursus_dut.do_formsemestre_validate_ue( sco_cursus_dut.do_formsemestre_validate_ue(
cnx, cnx,
nt, nt,
formsemestre_id, # "importe" cette UE dans le semestre (new 3/2015) formsemestre.id, # "importe" cette UE dans le semestre (new 3/2015)
etudid, etudid,
ue_id, ue_id,
code, code,
@ -1274,62 +1346,6 @@ def _invalidate_etud_formation_caches(etudid, formation_id):
) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif) ) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif)
def get_etud_ue_cap_html(etudid, formsemestre_id, ue_id):
"""Ramene bout de HTML pour pouvoir supprimer une validation de cette UE"""
valids = ndb.SimpleDictFetch(
"""SELECT SFV.*
FROM scolar_formsemestre_validation SFV
WHERE ue_id=%(ue_id)s
AND etudid=%(etudid)s""",
{"etudid": etudid, "ue_id": ue_id},
)
if not valids:
return ""
H = [
'<div class="existing_valids"><span>Validations existantes pour cette UE:</span><ul>'
]
for valid in valids:
valid["event_date"] = ndb.DateISOtoDMY(valid["event_date"])
if valid["moy_ue"] != None:
valid["m"] = ", moyenne %(moy_ue)g/20" % valid
else:
valid["m"] = ""
if valid["formsemestre_id"]:
sem = sco_formsemestre.get_formsemestre(valid["formsemestre_id"])
valid["s"] = ", du semestre %s" % sem["titreannee"]
else:
valid["s"] = " enregistrée d'un parcours antérieur (hors ScoDoc)"
if valid["semestre_id"]:
valid["s"] += " (<b>S%d</b>)" % valid["semestre_id"]
valid["ds"] = formsemestre_id
H.append(
'<li>%(code)s%(m)s%(s)s, le %(event_date)s <a class="stdlink" href="etud_ue_suppress_validation?etudid=%(etudid)s&ue_id=%(ue_id)s&formsemestre_id=%(ds)s" title="supprime cette validation">effacer</a></li>'
% valid
)
H.append("</ul></div>")
return "\n".join(H)
def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id):
"""Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
log("etud_ue_suppress_validation( %s, %s, %s)" % (etudid, formsemestre_id, ue_id))
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"DELETE FROM scolar_formsemestre_validation WHERE etudid=%(etudid)s and ue_id=%(ue_id)s",
{"etudid": etudid, "ue_id": ue_id},
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
_invalidate_etud_formation_caches(etudid, sem["formation_id"])
return flask.redirect(
scu.NotesURL()
+ "/formsemestre_validate_previous_ue?etudid=%s&formsemestre_id=%s"
% (etudid, formsemestre_id)
)
def check_formation_ues(formation_id): def check_formation_ues(formation_id):
"""Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id """Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id
Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de

View File

@ -4,10 +4,6 @@ div.jury_decisions_list div {
font-weight: bold; font-weight: bold;
} }
div.jury_decisions_list form {
display: inline-block;
}
span.parcours { span.parcours {
color:blueviolet; color:blueviolet;
} }

View File

@ -76,16 +76,16 @@ div.flashes {
} }
div.alert { div.alert {
/* padding: 16px;
position: absolute; border-radius: 12px;
top: 10px; font-size: 200%;
right: 10px; */ opacity: 0.8;
} }
div.alert-info { div.alert-info {
color: #0019d7; color: #208d3b;
background-color: #68f36d; background-color: #fffd97;
border-color: #0a8d0c; border-color: #208d3b;
} }
div.alert-error { div.alert-error {
@ -94,6 +94,9 @@ div.alert-error {
border-color: #8d0a17; border-color: #8d0a17;
} }
form.inline-form {
display: inline-block;
}
div.tab-content { div.tab-content {
margin-top: 10px; margin-top: 10px;
@ -1112,9 +1115,11 @@ a.discretelink:hover {
text-align: center; text-align: center;
} }
.expl, .help {
max-width: var(--sco-content-max-width);
}
.help { .help {
font-style: italic; font-style: italic;
max-width: 800px;
} }
.help_important { .help_important {
@ -1122,13 +1127,28 @@ a.discretelink:hover {
color: red; color: red;
} }
div.sco_help { div.sco_box, div.sco_help {
margin-top: 12px; margin-top: 12px;
margin-bottom: 4px; margin-bottom: 4px;
margin-left: 0px;
padding: 8px; padding: 8px;
border-radius: 4px; border-radius: 4px;
border: 1px solid grey;
max-width: var(--sco-content-max-width);
}
div.sco_help {
font-style: italic; font-style: italic;
max-width: 800px; background-color: rgb(209, 255, 214);
}
div.sco_box_title {
font-size: 120%;
font-weight: bold;
margin-bottom: 8px;
}
.sco_green_bg {
background-color: rgb(155, 218, 155);
}
.sco_lightgreen_bg {
background-color: rgb(209, 255, 214); background-color: rgb(209, 255, 214);
} }
@ -2504,13 +2524,7 @@ input.sco_tag_checkbox {
} }
div#ue_list_code { div#ue_list_code {
background-color: rgb(155, 218, 155);
padding: 10px;
border: 1px solid blue; border: 1px solid blue;
border-radius: 10px;
padding: 10px;
margin-top: 10px;
margin-right: 15px;
} }
ul.notes_module_list { ul.notes_module_list {
@ -2596,16 +2610,6 @@ div#ue_list_modules {
margin-right: 15px; margin-right: 15px;
} }
div#ue_list_etud_validations {
background-color: rgb(220, 250, 220);
padding-left: 4px;
padding-bottom: 1px;
margin: 3ex;
}
div#ue_list_etud_validations span {
font-weight: bold;
}
span.ue_share { span.ue_share {
font-weight: bold; font-weight: bold;

View File

@ -1,31 +1,43 @@
// Affiche et met a jour la liste des UE partageant le meme code // Affiche et met a jour la liste des UE partageant le meme code
$().ready(function () { document.addEventListener("DOMContentLoaded", () => {
update_ue_validations(); update_ue_list();
update_ue_list(); $("#tf_ue_id").bind("change", update_ue_list);
$("#tf_ue_id").bind("change", update_ue_list);
$("#tf_ue_id").bind("change", update_ue_validations); const buttons = document.querySelectorAll(".ue_list_etud_validations button");
buttons.forEach((button) => {
button.addEventListener("click", (event) => {
// Handle button click event here
event.preventDefault();
const etudid = event.target.dataset.etudid;
const v_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");
}
});
}
});
});
}); });
function update_ue_list() { function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value; var ue_id = $("#tf_ue_id")[0].value;
if (ue_id) { if (ue_id) {
var query = "ue_sharing_code?ue_id=" + ue_id; var query = SCO_URL + "/Notes/ue_sharing_code?ue_id=" + ue_id;
$.get(query, '', function (data) { $.get(query, "", function (data) {
$("#ue_list_code").html(data); $("#ue_list_code").html(data);
}); });
} }
} }
function update_ue_validations() {
var etudid = $("#tf_etudid")[0].value;
var ue_id = $("#tf_ue_id")[0].value;
var formsemestre_id = $("#tf_formsemestre_id")[0].value;
if (ue_id) {
var query = SCO_URL + "/Notes/get_etud_ue_cap_html?ue_id=" + ue_id + "&etudid=" + etudid + "&formsemestre_id=" + formsemestre_id;
$.get(query, '', function (data) {
$("#ue_list_etud_validations").html(data);
});
}
}

View File

@ -26,7 +26,10 @@ pages de saisie de jury habituelles).
<ul> <ul>
{% for v in sem_vals %} {% for v in sem_vals %}
<li>{{v.html()|safe}} <li>{{v.html()|safe}}
<form><button data-v_id="{{v.id}}" data-type="validation_formsemestre">effacer</button></form> <form>
<button
data-v_id="{{v.id}}" data-type="validation_formsemestre" data-etudid="{{etud.id}}"
>effacer</button></form>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -39,7 +42,10 @@ pages de saisie de jury habituelles).
<ul> <ul>
{% for v in ue_vals %} {% for v in ue_vals %}
<li>{{v.html(detail=True)|safe}} <li>{{v.html(detail=True)|safe}}
<form><button data-v_id="{{v.id}}" data-type="validation_ue">effacer</button></form> <form class="inline-form">
<button data-v_id="{{v.id}}" data-type="validation_ue" data-etudid="{{etud.id"}}
>effacer</button>
</form>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -52,7 +58,10 @@ pages de saisie de jury habituelles).
<ul> <ul>
{% for v in rcue_vals %} {% for v in rcue_vals %}
<li>{{v.html()|safe}} <li>{{v.html()|safe}}
<form><button data-v_id="{{v.id}}" data-type="validation_rcue">effacer</button></form> <form>
<button data-v_id="{{v.id}}" data-type="validation_rcue" data-etudid="{{etud.id}}"
>effacer</button>
</form>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -65,7 +74,10 @@ pages de saisie de jury habituelles).
<ul> <ul>
{% for v in annee_but_vals %} {% for v in annee_but_vals %}
<li>{{v.html()|safe}} <li>{{v.html()|safe}}
<form><button data-v_id="{{v.id}}" data-type="validation_annee_but">effacer</button></form> <form>
<button data-v_id="{{v.id}}" data-type="validation_annee_but" data-etudid="{{etud.id}}"
>effacer</button>
</form>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -78,7 +90,10 @@ pages de saisie de jury habituelles).
<ul> <ul>
{% for v in autorisations %} {% for v in autorisations %}
<li>{{v.html()|safe}} <li>{{v.html()|safe}}
<form><button data-v_id="{{v.id}}" data-type="autorisation_inscription">effacer</button></form> <form>
<button data-v_id="{{v.id}}" data-type="autorisation_inscription" data-etudid="{{etud.id}}"
>effacer</button>
</form>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -113,10 +128,11 @@ document.addEventListener('DOMContentLoaded', () => {
button.addEventListener('click', (event) => { button.addEventListener('click', (event) => {
// Handle button click event here // Handle button click event here
event.preventDefault(); event.preventDefault();
const etudid = event.target.dataset.etudid;
const v_id = event.target.dataset.v_id; const v_id = event.target.dataset.v_id;
const validation_type = event.target.dataset.type; const validation_type = event.target.dataset.type;
if (confirm("Supprimer cette validation ?")) { if (confirm("Supprimer cette validation ?")) {
fetch(`${SCO_URL}/../api/etudiant/{{etud.id}}/jury/${validation_type}/${v_id}/delete`, fetch(`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`,
{ {
method: "POST", method: "POST",
}).then(response => { }).then(response => {

View File

@ -56,17 +56,22 @@ from app.but.forms import jury_but_forms
from app.comp import jury, res_sem from app.comp import jury, res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Formation, ScolarAutorisationInscription, ScolarNews, Scolog from app.models import (
Formation,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
ScolarNews,
Scolog,
)
from app.models.but_refcomp import ApcNiveau from app.models.but_refcomp import ApcNiveau
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.formsemestre import FormSemestreUEComputationExpr from app.models.formsemestre import FormSemestreUEComputationExpr
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.modules import Module from app.models.modules import Module
from app.models.ues import DispenseUE, UniteEns from app.models.ues import DispenseUE, UniteEns
from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied
from app.tables import jury_recap
from app.views import notes_bp as bp from app.views import notes_bp as bp
from app.decorators import ( from app.decorators import (
@ -483,7 +488,21 @@ def ue_set_internal(ue_id):
) )
sco_publish("/ue_sharing_code", sco_edit_ue.ue_sharing_code, Permission.ScoView) @bp.route("/ue_sharing_code")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def ue_sharing_code():
ue_code = request.args.get("ue_code")
ue_id = request.args.get("ue_id")
hide_ue_id = request.args.get("hide_ue_id")
return sco_edit_ue.ue_sharing_code(
ue_code=ue_code,
ue_id=None if ue_id is None else int(ue_id),
hide_ue_id=None if hide_ue_id is None else int(hide_ue_id),
)
sco_publish( sco_publish(
"/edit_ue_set_code_apogee", "/edit_ue_set_code_apogee",
sco_edit_ue.edit_ue_set_code_apogee, sco_edit_ue.edit_ue_set_code_apogee,
@ -2621,10 +2640,12 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
) )
@bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) @bp.route(
"/formsemestre_validate_previous_ue/<int:formsemestre_id>/<int:etudid>",
methods=["GET", "POST"],
)
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_validate_previous_ue(formsemestre_id, etudid=None): def formsemestre_validate_previous_ue(formsemestre_id, etudid=None):
"Form. saisie UE validée hors ScoDoc" "Form. saisie UE validée hors ScoDoc"
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
@ -2636,9 +2657,15 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid=None):
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
) )
etud: Identite = (
Identite.query.filter_by(id=etudid)
.join(FormSemestreInscription)
.filter_by(formsemestre_id=formsemestre_id)
.first_or_404()
)
return sco_formsemestre_validation.formsemestre_validate_previous_ue( return sco_formsemestre_validation.formsemestre_validate_previous_ue(
formsemestre_id, etudid formsemestre, etud
) )
@ -2671,34 +2698,6 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None):
) )
sco_publish(
"/get_etud_ue_cap_html",
sco_formsemestre_validation.get_etud_ue_cap_html,
Permission.ScoView,
)
@bp.route("/etud_ue_suppress_validation")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def etud_ue_suppress_validation(etudid, formsemestre_id, ue_id):
"""Suppress a validation (ue_id, etudid) and redirect to formsemestre"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_jury():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
return sco_formsemestre_validation.etud_ue_suppress_validation(
etudid, formsemestre_id, ue_id
)
@bp.route("/formsemestre_validation_auto") @bp.route("/formsemestre_validation_auto")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)