Compare commits

...

7 Commits

18 changed files with 411 additions and 305 deletions

View File

@ -10,7 +10,7 @@
from flask import g, url_for
from flask_json import as_json
from flask_login import login_required
from flask_login import current_user, login_required
import app
from app import db, log
@ -29,6 +29,7 @@ from app.models import (
)
from app.scodoc import sco_cache
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error
@bp.route("/formsemestre/<int:formsemestre_id>/decisions_jury")
@ -73,7 +74,7 @@ def _news_delete_jury_etud(etud: Identite):
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudInscrit)
@permission_required(Permission.ScoView)
@as_json
def validation_ue_delete(etudid: int, validation_id: int):
"Efface cette validation"
@ -90,7 +91,7 @@ def validation_ue_delete(etudid: int, validation_id: int):
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudInscrit)
@permission_required(Permission.ScoView)
@as_json
def validation_formsemestre_delete(etudid: int, validation_id: int):
"Efface cette validation"
@ -106,6 +107,24 @@ def _validation_ue_delete(etudid: int, validation_id: int):
validation = ScolarFormSemestreValidation.query.filter_by(
id=validation_id, etudid=etudid
).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}")
db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud)

View File

@ -200,6 +200,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Codes toujours proposés sauf si include_communs est faux:
codes_communs = [
sco_codes.RAT,
sco_codes.RED, # TODO temporaire 9.4.93: propose toujours RED
sco_codes.ABAN,
sco_codes.ABL,
sco_codes.ATJ,
@ -1334,7 +1335,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
ordre=annee_inferieure,
)
.join(Formation)
.filter_by(formation_code=self.rcue.formsemestre_1.formation.code)
.filter_by(formation_code=self.rcue.formsemestre_1.formation.formation_code)
.all()
)
if len(validations_annee) > 1:

View File

@ -9,12 +9,9 @@
Non spécifique au BUT.
"""
from flask import flash, render_template
from flask import g, request
from flask import render_template
import sqlalchemy as sa
from app import db
from app.models import (
ApcValidationAnnee,
ApcValidationRCUE,

View File

@ -65,6 +65,7 @@ class CodesDecisionsForm(FlaskForm):
ADJ = _build_code_field("ADJ")
ADJR = _build_code_field("ADJR")
ADM = _build_code_field("ADM")
ADSUP = _build_code_field("ADSUP")
AJ = _build_code_field("AJ")
ATB = _build_code_field("ATB")
ATJ = _build_code_field("ATJ")
@ -81,7 +82,8 @@ class CodesDecisionsForm(FlaskForm):
NOTES_FMT = StringField(
label="Format notes exportées",
description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
description="""Format des notes. Par défaut
<tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
validators=[
validators.Length(
max=SHORT_STR_LEN,

View File

@ -17,6 +17,7 @@ from app.scodoc.codes_cursus import (
ADJ,
ADJR,
ADM,
ADSUP,
AJ,
ATB,
ATJ,
@ -39,6 +40,7 @@ CODES_SCODOC_TO_APO = {
ADJ: "ADM",
ADJR: "ADM",
ADM: "ADM",
ADSUP: "ADM",
AJ: "AJ",
ATB: "AJAC",
ATJ: "AJAC",

View File

@ -8,6 +8,8 @@ from app import log
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
from app.models.events import Scolog
from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu
class ScolarFormSemestreValidation(db.Model):
@ -70,6 +72,14 @@ class ScolarFormSemestreValidation(db.Model):
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
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:
"as a dict"
d = dict(self.__dict__)
@ -79,15 +89,22 @@ class ScolarFormSemestreValidation(db.Model):
def html(self, detail=False) -> str:
"Affichage html"
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">'
+ ", ".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 ""}
: <b>{self.code}</b>
if self.formsemestre else "externe/antérieure"}
: <b>{self.code}</b>{moyenne}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
else:

View File

@ -1238,7 +1238,7 @@ def make_menu_autres_operations(
"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",
"args": {
"formsemestre_id": formsemestre.id,

View File

@ -972,7 +972,7 @@ def do_formsemestre_validate_ue(
moy_ue = ue_status["moy"] if ue_status else ""
args["moy_ue"] = moy_ue
log("formsemestre_validate_ue: create %s" % args)
if code != None:
if code is not None:
scolar_formsemestre_validation_create(cnx, args)
else:
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:
clone_form = ""
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 (
"\n".join(H)
+ tf[1]
@ -1375,13 +1375,12 @@ def _ue_table_modules(
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
Either ue_code or ue_id may be specified.
hide_ue_id spécifie un id à retirer de la liste.
"""
ue_code = str(ue_code)
if ue_id:
if ue_id is not None:
ue = UniteEns.query.get_or_404(ue_id)
if not 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)
)
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)
ues = q_ues.all()
msg = " dans les formations du département "
if not ues:
if ue_id:
return (
f"""<span class="ue_share">Seule UE avec code {ue_code or '-'}</span>"""
)
if ue_id is not None:
return f"""<span class="ue_share">Seule UE avec code {
ue_code if ue_code is not None else '-'}{msg}</span>"""
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 = []
if ue_id:
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:
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>")
for ue in ues:
H.append(
f"""<li>{ue.acronyme} ({ue.titre}) dans <a class="stdlink"
href="{url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)}"
f"""<li>{ue.acronyme} ({ue.titre}) dans
<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}
</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
sco_formsemestre_validation.do_formsemestre_validate_previous_ue(
formsemestre.id,
formsemestre,
etud.id,
ue.id,
note,

View File

@ -31,8 +31,9 @@ import time
import flask
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.sco_utils as scu
from app import db, log
@ -512,7 +513,7 @@ def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""):
def formsemestre_recap_parcours_table(
Se,
situation_etud_cursus: sco_cursus_dut.SituationEtudCursus,
etudid,
with_links=False,
with_all_columns=True,
@ -550,16 +551,18 @@ def formsemestre_recap_parcours_table(
"""
)
# titres des UE
H.append("<th></th>" * Se.nb_max_ue)
H.append("<th></th>" * situation_etud_cursus.nb_max_ue)
#
if with_links:
H.append("<th></th>")
H.append("<th></th></tr>")
num_sem = 0
for sem in Se.get_semestres():
is_prev = Se.prev and (Se.prev["formsemestre_id"] == sem["formsemestre_id"])
is_cur = Se.formsemestre_id == sem["formsemestre_id"]
for sem in situation_etud_cursus.get_semestres():
is_prev = situation_etud_cursus.prev and (
situation_etud_cursus.prev["formsemestre_id"] == sem["formsemestre_id"]
)
is_cur = situation_etud_cursus.formsemestre_id == sem["formsemestre_id"]
num_sem += 1
dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
@ -582,7 +585,7 @@ def formsemestre_recap_parcours_table(
else:
type_sem = ""
class_sem = "sem_autre"
if sem["formation_code"] != Se.formation.formation_code:
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
class_sem += " sem_autre_formation"
if sem["bul_bgcolor"]:
bgcolor = sem["bul_bgcolor"]
@ -646,7 +649,7 @@ def formsemestre_recap_parcours_table(
H.append("<td><em>en cours</em></td>")
H.append(f"""<td class="rcp_nonass">{ass}</td>""") # abs
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
ues = list(nt.etud_ues(etudid))
ues = list(nt.etud_ues(etudid)) # nb: en BUT, les UE "dispensées" sont incluses
cnx = ndb.GetDBConnexion()
etud_ue_status = {ue.id: nt.get_etud_ue_status(etudid, ue.id) for ue in ues}
if not nt.is_apc:
@ -660,8 +663,10 @@ def formsemestre_recap_parcours_table(
for ue in ues:
H.append(f"""<td class="ue_acro"><span>{ue.acronyme}</span></td>""")
if len(ues) < Se.nb_max_ue:
H.append(f"""<td colspan="{Se.nb_max_ue - len(ues)}"></td>""")
if len(ues) < situation_etud_cursus.nb_max_ue:
H.append(
f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
)
# indique le semestre compensé par celui ci:
if decision_sem and decision_sem["compense_formsemestre_id"]:
csem = sco_formsemestre.get_formsemestre(
@ -686,7 +691,7 @@ def formsemestre_recap_parcours_table(
if not sem["etat"]: # locked
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
default_sem_info += lockicon
if sem["formation_code"] != Se.formation.formation_code:
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
H.append(
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
@ -723,14 +728,21 @@ def formsemestre_recap_parcours_table(
explanation_ue.append(
f"""Capitalisée le {ue_status["event_date"] or "?"}."""
)
# Dispense BUT ?
if (etudid, ue.id) in nt.dispense_ues:
moy_ue_txt = "" if (ue_status and ue_status["is_capitalized"]) else ""
explanation_ue.append("non inscrit (dispense)")
else:
moy_ue_txt = scu.fmt_note(moy_ue)
H.append(
f"""<td class="{class_ue}" title="{
" ".join(explanation_ue)
}">{scu.fmt_note(moy_ue)}</td>"""
}">{moy_ue_txt}</td>"""
)
if len(ues) < situation_etud_cursus.nb_max_ue:
H.append(
f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
)
if len(ues) < Se.nb_max_ue:
H.append(f"""<td colspan="{Se.nb_max_ue - len(ues)}"></td>""")
H.append("<td></td>")
if with_links:
@ -1070,62 +1082,44 @@ def formsemestre_validation_suppress_etud(formsemestre_id, etudid):
) # > 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
(pour étudiants arrivant avec un UE antérieurement validée).
"""
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
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>
""",
]
formation: Formation = formsemestre.formation
# Toutes les UE de cette formation sont présentées (même celles des autres semestres)
ues = formation.ues.order_by(UniteEns.numero)
ue_names = ["Choisir..."] + [f"{ue.acronyme} {ue.titre}" for ue in ues]
# Toutes les UEs non bonus de cette formation sont présentées
# avec indice de semestre <= semestre courant ou NULL
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]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
form_descr = [
("etudid", {"input_type": "hidden"}),
("formsemestre_id", {"input_type": "hidden"}),
(
("etudid", {"input_type": "hidden"}),
("formsemestre_id", {"input_type": "hidden"}),
(
"ue_id",
{
"input_type": "menu",
"title": "Unité d'Enseignement (UE)",
"allow_null": False,
"allowed_values": ue_ids,
"labels": ue_names,
},
),
"ue_id",
{
"input_type": "menu",
"title": "Unité d'Enseignement (UE)",
"allow_null": False,
"allowed_values": ue_ids,
"labels": ue_names,
},
),
]
if not formation.is_apc():
form_descr.append(
(
"semestre_id",
{
@ -1136,69 +1130,160 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"allowed_values": [""] + [x for x in range(11)],
"labels": ["-"] + list(range(11)),
},
),
(
"date",
{
"input_type": "date",
"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:",
},
),
)
)
form_descr += [
(
"date",
{
"input_type": "date",
"size": 9,
"explanation": "j/m/a",
"default": time.strftime("%d/%m/%Y"),
},
),
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",
)
if tf[0] == 0:
X = """
<div id="ue_list_etud_validations"><!-- filled by get_etud_ue_cap_html --></div>
<div id="ue_list_code"><!-- filled by ue_sharing_code --></div>
"""
warn, ue_multiples = check_formation_ues(formation.id)
return "\n".join(H) + tf[1] + X + warn + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
scu.NotesURL()
+ "/formsemestre_status?formsemestre_id="
+ str(formsemestre_id)
)
else:
if tf[2]["semestre_id"]:
semestre_id = int(tf[2]["semestre_id"])
else:
semestre_id = None
do_formsemestre_validate_previous_ue(
formsemestre_id,
etudid,
tf[2]["ue_id"],
tf[2]["moy_ue"],
tf[2]["date"],
semestre_id=semestre_id,
)
flash("Validation d'UE enregistrée")
return f"""
{html_sco_header.sco_header(
page_title="Validation UE antérieure",
javascripts=["js/validate_previous_ue.js"],
cssstyles=["css/jury_delete_manual.css"],
etudid=etud.id,
formsemestre_id=formsemestre.id,
)}
<h2 class="formsemestre">Gestion des validations d'UEs antérieures
de {etud.html_link_fiche()}
</h2>
<p class="help">Utiliser cette page pour enregistrer une UE validée 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>
<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)}
<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(
url_for(
"notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etudid=etudid,
formsemestre_id=formsemestre.id,
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(
formsemestre_id,
formsemestre: FormSemestre,
etudid,
ue_id,
moy_ue,
@ -1211,21 +1296,20 @@ def do_formsemestre_validate_previous_ue(
Si le coefficient est spécifié, modifie le coefficient de
cette UE (utile seulement pour les semestres extérieurs).
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
cnx = ndb.GetDBConnexion()
if ue_coefficient != None:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre_id, ue_id, ue_coefficient
cnx, formsemestre.id, ue_id, ue_coefficient
)
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(
cnx,
nt,
formsemestre_id, # "importe" cette UE dans le semestre (new 3/2015)
formsemestre.id, # "importe" cette UE dans le semestre (new 3/2015)
etudid,
ue_id,
code,
@ -1263,62 +1347,6 @@ def _invalidate_etud_formation_caches(etudid, formation_id):
) # > 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):
"""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

View File

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

View File

@ -65,27 +65,34 @@ div#gtrcontent {
}
div.flashes {
transition: opacity 0.5s ease;
margin-top: 8px;
left: 50%;
margin-top: 8px;
max-width: 800px;
position: fixed;
text-align: center;
top: 8px;
transform: translateX(-50%);
width: auto;
transition: opacity 0.5s ease;
z-index: 1000;
}
div.alert {
/*
position: absolute;
top: 10px;
right: 10px; */
padding: 16px;
border-radius: 12px;
font-size: 200%;
opacity: 0.9;
}
div.alert-info {
color: #0019d7;
background-color: #68f36d;
border-color: #0a8d0c;
color: #208d3b;
background-color: #fffd97;
border-color: #208d3b;
}
div.alert-warning {
color: #ef5c00;
background-color: #fbfb00d4;
border-color: #767676;
}
div.alert-error {
@ -94,6 +101,9 @@ div.alert-error {
border-color: #8d0a17;
}
form.inline-form {
display: inline-block;
}
div.tab-content {
margin-top: 10px;
@ -1112,9 +1122,11 @@ a.discretelink:hover {
text-align: center;
}
.expl, .help {
max-width: var(--sco-content-max-width);
}
.help {
font-style: italic;
max-width: 800px;
}
.help_important {
@ -1122,13 +1134,28 @@ a.discretelink:hover {
color: red;
}
div.sco_help {
div.sco_box, div.sco_help {
margin-top: 12px;
margin-bottom: 4px;
margin-left: 0px;
padding: 8px;
border-radius: 4px;
border: 1px solid grey;
max-width: var(--sco-content-max-width);
}
div.sco_help {
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);
}
@ -2504,13 +2531,7 @@ input.sco_tag_checkbox {
}
div#ue_list_code {
background-color: rgb(155, 218, 155);
padding: 10px;
border: 1px solid blue;
border-radius: 10px;
padding: 10px;
margin-top: 10px;
margin-right: 15px;
}
ul.notes_module_list {
@ -2596,16 +2617,6 @@ div#ue_list_modules {
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 {
font-weight: bold;

View File

@ -1,31 +1,43 @@
// Affiche et met a jour la liste des UE partageant le meme code
$().ready(function () {
update_ue_validations();
update_ue_list();
$("#tf_ue_id").bind("change", update_ue_list);
$("#tf_ue_id").bind("change", update_ue_validations);
document.addEventListener("DOMContentLoaded", () => {
update_ue_list();
$("#tf_ue_id").bind("change", update_ue_list);
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() {
var ue_id = $("#tf_ue_id")[0].value;
if (ue_id) {
var query = "ue_sharing_code?ue_id=" + ue_id;
$.get(query, '', function (data) {
$("#ue_list_code").html(data);
});
}
var ue_id = $("#tf_ue_id")[0].value;
if (ue_id) {
var query = SCO_URL + "/Notes/ue_sharing_code?ue_id=" + ue_id;
$.get(query, "", function (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>
{% for v in sem_vals %}
<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>
{% endfor %}
</ul>
@ -39,7 +42,10 @@ pages de saisie de jury habituelles).
<ul>
{% for v in ue_vals %}
<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>
{% endfor %}
</ul>
@ -52,7 +58,10 @@ pages de saisie de jury habituelles).
<ul>
{% for v in rcue_vals %}
<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>
{% endfor %}
</ul>
@ -65,7 +74,10 @@ pages de saisie de jury habituelles).
<ul>
{% for v in annee_but_vals %}
<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>
{% endfor %}
</ul>
@ -78,7 +90,10 @@ pages de saisie de jury habituelles).
<ul>
{% for v in autorisations %}
<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>
{% endfor %}
</ul>
@ -113,10 +128,11 @@ document.addEventListener('DOMContentLoaded', () => {
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/{{etud.id}}/jury/${validation_type}/${v_id}/delete`,
fetch(`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`,
{
method: "POST",
}).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.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.config import ScoDocSiteConfig
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.moduleimpls import ModuleImpl
from app.models.modules import Module
from app.models.ues import DispenseUE, UniteEns
from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied
from app.tables import jury_recap
from app.views import notes_bp as bp
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(
"/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
@permission_required(Permission.ScoView)
@scodoc7func
def formsemestre_validate_previous_ue(formsemestre_id, etudid=None):
"Form. saisie UE validée hors ScoDoc"
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,
)
)
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(
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")
@scodoc
@permission_required(Permission.ScoView)

View File

@ -247,7 +247,7 @@ def config_codes_decisions():
if form.validate_on_submit():
for code in models.config.CODES_SCODOC_TO_APO:
ScoDocSiteConfig.set_code_apo(code, getattr(form, code).data)
flash("Codes décisions enregistrés.")
flash("Codes décisions enregistrés")
return redirect(url_for("scodoc.index"))
elif request.method == "GET":
for code in models.config.CODES_SCODOC_TO_APO:

View File

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