forked from ScoDoc/ScoDoc
Compare commits
140 Commits
bac_a_sabl
...
master
Author | SHA1 | Date | |
---|---|---|---|
632e285d26 | |||
96531f839c | |||
1dfe754793 | |||
e0188ebc2d | |||
c79eb6410a | |||
92e75e11f2 | |||
0dfc27e072 | |||
c3cc316777 | |||
b04812f812 | |||
febc15c4c8 | |||
a58919d8b4 | |||
8be0ab0678 | |||
b913119b58 | |||
0573081711 | |||
f414ec1c0d | |||
1b297580c9 | |||
538a5427ba | |||
30666fc822 | |||
8614a29f9b | |||
5cdfb360fa | |||
f08a4130dd | |||
24bb78bdfd | |||
ea9c6a6ef2 | |||
8ee95cc2e5 | |||
c933f010d4 | |||
2c93c35aa7 | |||
417b3b8383 | |||
814a3802e9 | |||
1de265536e | |||
ea1e5cfb89 | |||
5c78c1cf9f | |||
9bfebfc8a2 | |||
2e5add1c48 | |||
52282c98cb | |||
dba48f32eb | |||
8b37f661d6 | |||
d88e41b83e | |||
41a791282a | |||
6c56b921e8 | |||
428a34e6ba | |||
c5a702e6d1 | |||
0824598aa4 | |||
90c1454b21 | |||
9b3df5febf | |||
a2c5be22cb | |||
cf0d3c06c4 | |||
e963ca52f5 | |||
2cc911eb0d | |||
84d1ed6c85 | |||
5f06b190a2 | |||
937a96d086 | |||
10de8c4cc2 | |||
da7f9a334f | |||
0dda9157eb | |||
90fd45a572 | |||
ad4e4e33ec | |||
87316f057e | |||
7a1dfcbb63 | |||
3325b41690 | |||
35fb269a41 | |||
b4c68cea10 | |||
77e4c4f726 | |||
61b46db4dd | |||
75d0170b4a | |||
ee95a6178a | |||
ebe7dd8f73 | |||
5d30b9233b | |||
4b49fd5ed9 | |||
7ed521e4f5 | |||
71ffb33175 | |||
0c9d202e09 | |||
e190756b98 | |||
37845750a6 | |||
70049da38f | |||
b2bd659c47 | |||
658fb3595d | |||
f9961498bf | |||
66983ff767 | |||
52db344926 | |||
87cc4c06d6 | |||
c9babcd8c2 | |||
d57b6638ea | |||
438caf1052 | |||
c45abc33cc | |||
cc39e4a862 | |||
b501233ba4 | |||
9067424f8f | |||
83218e39b6 | |||
544abba758 | |||
ff73ba8a5b | |||
d2fbbad84b | |||
7c9f07c36e | |||
c8a974d460 | |||
ccc589f4d5 | |||
f4277a1336 | |||
8156cce4be | |||
47cf5962f9 | |||
d1d83e0327 | |||
027224a7b3 | |||
91b86f30a5 | |||
b026349e74 | |||
fdfffb70be | |||
de23302b3e | |||
756c46df0b | |||
84d40091a8 | |||
f9b4539231 | |||
021b4ec5f8 | |||
e3b979fc10 | |||
44dffea8d2 | |||
008dd9b50e | |||
e46ae76399 | |||
73023b7806 | |||
c73581c52f | |||
c547990eef | |||
c6f3ad448a | |||
f4c776e9f3 | |||
cf876ca0d3 | |||
f979eb137c | |||
fda11298b4 | |||
0322603a22 | |||
fb4cabee3b | |||
bfa8cf1683 | |||
42b232dd59 | |||
a8ab0cb48c | |||
feb799cc20 | |||
bf738d1706 | |||
656bf4d25e | |||
8e1cb055f6 | |||
111c400333 | |||
9579cd73c2 | |||
c5f5cb7daa | |||
5f0ac236d7 | |||
63cf7dfc42 | |||
9a4f7abfa8 | |||
79d92dc9ac | |||
1693bb6c6c | |||
753578813e | |||
cf72686ce4 | |||
4a3910adcf | |||
e5a620a9ea |
@ -8,6 +8,7 @@
|
||||
|
||||
from flask_json import as_json
|
||||
|
||||
from app import db
|
||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
@ -51,7 +52,7 @@ def absences(etudid: int = None):
|
||||
}
|
||||
]
|
||||
"""
|
||||
etud = Identite.query.get(etudid)
|
||||
etud = db.session.get(Identite, etudid)
|
||||
if etud is None:
|
||||
return json_error(404, message="etudiant inexistant")
|
||||
# Absences de l'étudiant
|
||||
@ -96,7 +97,7 @@ def absences_just(etudid: int = None):
|
||||
}
|
||||
]
|
||||
"""
|
||||
etud = Identite.query.get(etudid)
|
||||
etud = db.session.get(Identite, etudid)
|
||||
if etud is None:
|
||||
return json_error(404, message="etudiant inexistant")
|
||||
|
||||
|
@ -8,16 +8,17 @@
|
||||
API : accès aux étudiants
|
||||
"""
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from sqlalchemy import desc, or_
|
||||
from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.api import tools
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
@ -31,6 +32,8 @@ from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||
|
||||
|
||||
# Un exemple:
|
||||
# @bp.route("/api_function/<int:arg>")
|
||||
@ -164,12 +167,39 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
)
|
||||
if not None in allowed_depts:
|
||||
# restreint aux départements autorisés:
|
||||
etuds = etuds.join(Departement).filter(
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
return [etud.to_dict_api() for etud in query]
|
||||
|
||||
|
||||
@bp.route("/etudiants/name/<string:start>")
|
||||
@api_web_bp.route("/etudiants/name/<string:start>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
"""Liste des étudiants dont le nom débute par start.
|
||||
Si start fait moins de min_len=3 caractères, liste vide.
|
||||
La casse et les accents sont ignorés.
|
||||
"""
|
||||
if len(start) < min_len:
|
||||
return []
|
||||
start = suppress_accents(start).lower()
|
||||
query = Identite.query.filter(
|
||||
func.lower(func.unaccent(Identite.nom, type_=VARCHAR)).ilike(start + "%")
|
||||
)
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
if not None in allowed_depts:
|
||||
# restreint aux départements autorisés:
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
||||
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
||||
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||
@bp.route("/etudiant/nip/<string:nip>/formsemestres")
|
||||
@bp.route("/etudiant/ine/<string:ine>/formsemestres")
|
||||
|
@ -8,7 +8,7 @@
|
||||
ScoDoc 9 API : accès aux évaluations
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
@ -17,7 +17,7 @@ import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def the_eval(evaluation_id: int):
|
||||
def evaluation(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
{
|
||||
@ -93,24 +93,22 @@ def evaluations(moduleimpl_id: int):
|
||||
@as_json
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
Retourne la liste des notes à partir de l'id d'une évaluation donnée
|
||||
Retourne la liste des notes de l'évaluation
|
||||
|
||||
evaluation_id : l'id d'une évaluation
|
||||
evaluation_id : l'id de l'évaluation
|
||||
|
||||
Exemple de résultat :
|
||||
{
|
||||
"1": {
|
||||
"id": 1,
|
||||
"etudid": 10,
|
||||
"11": {
|
||||
"etudid": 11,
|
||||
"evaluation_id": 1,
|
||||
"value": 15.0,
|
||||
"comment": "",
|
||||
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
"2": {
|
||||
"id": 2,
|
||||
"etudid": 1,
|
||||
"12": {
|
||||
"etudid": 12,
|
||||
"evaluation_id": 1,
|
||||
"value": 12.0,
|
||||
"comment": "",
|
||||
@ -128,8 +126,8 @@ def evaluation_notes(evaluation_id: int):
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
the_eval = query.first_or_404()
|
||||
dept = the_eval.moduleimpl.formsemestre.departement
|
||||
evaluation = query.first_or_404()
|
||||
dept = evaluation.moduleimpl.formsemestre.departement
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
@ -137,7 +135,49 @@ def evaluation_notes(evaluation_id: int):
|
||||
# "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval.
|
||||
note = notes[etudid]
|
||||
note["value"] = scu.fmt_note(note["value"], keep_numeric=True)
|
||||
note["note_max"] = the_eval.note_max
|
||||
note["note_max"] = evaluation.note_max
|
||||
del note["id"]
|
||||
|
||||
return notes
|
||||
# in JS, keys must be string, not integers
|
||||
return {str(etudid): note for etudid, note in notes.items()}
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
|
||||
@api_web_bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEnsView)
|
||||
@as_json
|
||||
def evaluation_set_notes(evaluation_id: int):
|
||||
"""Écriture de notes dans une évaluation.
|
||||
The request content type should be "application/json",
|
||||
and contains:
|
||||
{
|
||||
'notes' : [ [etudid, value], ... ],
|
||||
'comment' : optional string
|
||||
}
|
||||
Result:
|
||||
- nb_changed: nombre de notes changées
|
||||
- nb_suppress: nombre de notes effacées
|
||||
- etudids_with_decision: liste des etudiants dont la note a changé
|
||||
alors qu'ils ont une décision de jury enregistrée.
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
query.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
evaluation = query.first_or_404()
|
||||
dept = evaluation.moduleimpl.formsemestre.departement
|
||||
app.set_sco_dept(dept.acronym)
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
notes = data.get("notes")
|
||||
if notes is None:
|
||||
return scu.json_error(404, "no notes")
|
||||
if not isinstance(notes, list):
|
||||
return scu.json_error(404, "invalid notes argument (must be a list)")
|
||||
return sco_saisie_notes.save_notes(
|
||||
evaluation, notes, comment=data.get("comment", "")
|
||||
)
|
||||
|
323
app/api/jury.py
323
app/api/jury.py
@ -5,19 +5,38 @@
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : jury WIP
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from flask import flash, g, request, 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.api import api_bp as bp, api_web_bp
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR, tools
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.but import jury_but_results
|
||||
from app.models import FormSemestre
|
||||
from app.models import (
|
||||
ApcParcours,
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
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")
|
||||
@ -29,10 +48,304 @@ from app.scodoc.sco_permissions import Permission
|
||||
def decisions_jury(formsemestre_id: int):
|
||||
"""Décisions du jury des étudiants du formsemestre."""
|
||||
# APC, pair:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if formsemestre.formation.is_apc():
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
rows = jury_but_results.get_jury_but_results(formsemestre)
|
||||
return rows
|
||||
else:
|
||||
raise ScoException("non implemente")
|
||||
|
||||
|
||||
def _news_delete_jury_etud(etud: Identite):
|
||||
"génère news sur effacement décision"
|
||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||
url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=etud.id,
|
||||
text=f"""Suppression décision jury pour <a href="{url}">{etud.nomprenom}</a>""",
|
||||
url=url,
|
||||
)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_formsemestre_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
# c'est la même chose (formations classiques)
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
def _validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation (semestres classiques ou UEs)"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
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, "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, "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, "opération non autorisée (126)")
|
||||
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ScolarAutorisationInscription.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"autorisation_inscription_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/record",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/record",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_record(etudid: int):
|
||||
"""Enregistre une validation de RCUE.
|
||||
Si une validation existe déjà pour ce RCUE, la remplace.
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"code" : str,
|
||||
"ue1_id" : int,
|
||||
"ue2_id" : int,
|
||||
// Optionnel:
|
||||
"formsemestre_id" : int,
|
||||
"date" : date_iso, // si non spécifié, now()
|
||||
"parcours_id" :int,
|
||||
}
|
||||
"""
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return json_error(404, "étudiant inconnu")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
code = data.get("code")
|
||||
if code is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: code")
|
||||
if code not in codes_cursus.CODES_JURY_RCUE:
|
||||
return json_error(API_CLIENT_ERROR, "invalid code value")
|
||||
ue1_id = data.get("ue1_id")
|
||||
if ue1_id is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: ue1_id")
|
||||
try:
|
||||
ue1_id = int(ue1_id)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid value for ue1_id")
|
||||
ue2_id = data.get("ue2_id")
|
||||
if ue2_id is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: ue2_id")
|
||||
try:
|
||||
ue2_id = int(ue2_id)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid value for ue2_id")
|
||||
formsemestre_id = data.get("formsemestre_id")
|
||||
date_validation_str = data.get("date", datetime.datetime.now().isoformat())
|
||||
parcours_id = data.get("parcours_id")
|
||||
#
|
||||
query = UniteEns.query.filter_by(id=ue1_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue1: UniteEns = query.first_or_404()
|
||||
query = UniteEns.query.filter_by(id=ue2_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue2: UniteEns = query.first_or_404()
|
||||
if ue1.niveau_competence_id != ue2.niveau_competence_id:
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, "UEs non associees au meme niveau de competence"
|
||||
)
|
||||
if formsemestre_id is not None:
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404()
|
||||
if (formsemestre.formation_id != ue1.formation_id) or (
|
||||
formsemestre.formation_id != ue2.formation_id
|
||||
):
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, "ues et semestre ne sont pas de la meme formation"
|
||||
)
|
||||
else:
|
||||
formsemestre = None
|
||||
try:
|
||||
date_validation = datetime.datetime.fromisoformat(date_validation_str)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid date string")
|
||||
if parcours_id is not None:
|
||||
parcours: ApcParcours = ApcParcours.query.get_or_404(parcours_id)
|
||||
if parcours.referentiel_id != ue1.niveau_competence.competence.referentiel_id:
|
||||
return json_error(API_CLIENT_ERROR, "niveau et parcours incompatibles")
|
||||
|
||||
# Une validation pour ce niveau de compétence existe-elle ?
|
||||
validation = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etudid)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.filter_by(niveau_competence_id=ue2.niveau_competence_id)
|
||||
.first()
|
||||
)
|
||||
if validation:
|
||||
validation.code = code
|
||||
validation.date = date_validation
|
||||
validation.formsemestre_id = formsemestre_id
|
||||
validation.parcours_id = parcours_id
|
||||
validation.ue1_id = ue1_id
|
||||
validation.ue2_id = ue2_id
|
||||
operation = "update"
|
||||
else:
|
||||
validation = ApcValidationRCUE(
|
||||
code=code,
|
||||
date=date_validation,
|
||||
etudid=etudid,
|
||||
formsemestre_id=formsemestre_id,
|
||||
parcours_id=parcours_id,
|
||||
ue1_id=ue1_id,
|
||||
ue2_id=ue2_id,
|
||||
)
|
||||
operation = "record"
|
||||
db.session.add(validation)
|
||||
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="validation_rcue_record",
|
||||
etudid=etudid,
|
||||
msg=f"Enregistrement {validation}",
|
||||
commit=True,
|
||||
)
|
||||
log(f"{operation} {validation}")
|
||||
return validation.to_dict()
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationRCUE.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_annee_but_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"validation_annee_but: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
@ -12,6 +12,8 @@ from operator import attrgetter
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
@ -23,6 +25,7 @@ from app.models import GroupDescr, Partition, Scolog
|
||||
from app.models.groups import group_membership
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -182,10 +185,12 @@ def set_etud_group(etudid: int, group_id: int):
|
||||
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
|
||||
return json_error(404, "etud non inscrit au formsemestre du groupe")
|
||||
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid, group_id, group.partition.to_dict()
|
||||
)
|
||||
|
||||
try:
|
||||
sco_groups.change_etud_group_in_partition(etudid, group)
|
||||
except ScoValueError as exc:
|
||||
return json_error(404, exc.args[0])
|
||||
except IntegrityError:
|
||||
return json_error(404, "échec de l'enregistrement")
|
||||
return {"group_id": group_id, "etudid": etudid}
|
||||
|
||||
|
||||
@ -244,19 +249,25 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
groups = (
|
||||
GroupDescr.query.filter_by(partition_id=partition_id)
|
||||
.join(group_membership)
|
||||
.filter_by(etudid=etudid)
|
||||
|
||||
db.session.execute(
|
||||
sa.text(
|
||||
"""DELETE FROM group_membership
|
||||
WHERE etudid=:etudid
|
||||
and group_id IN (
|
||||
SELECT id FROM group_descr WHERE partition_id = :partition_id
|
||||
);
|
||||
"""
|
||||
),
|
||||
{"etudid": etudid, "partition_id": partition_id},
|
||||
)
|
||||
|
||||
Scolog.logdb(
|
||||
method="partition_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait de la partition {partition.partition_name}",
|
||||
commit=False,
|
||||
)
|
||||
for group in groups:
|
||||
group.etuds.remove(etud)
|
||||
Scolog.logdb(
|
||||
method="partition_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait du groupe {group.group_name} de {group.partition.partition_name}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
# Update parcours
|
||||
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||
@ -271,7 +282,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def group_create(partition_id: int):
|
||||
def group_create(partition_id: int): # partition-group-create
|
||||
"""Création d'un groupe dans une partition
|
||||
|
||||
The request content type should be "application/json":
|
||||
|
@ -35,7 +35,7 @@ def user_info(uid: int):
|
||||
"""
|
||||
Info sur un compte utilisateur scodoc
|
||||
"""
|
||||
user: User = User.query.get(uid)
|
||||
user: User = db.session.get(User, uid)
|
||||
if user is None:
|
||||
return json_error(404, "user not found")
|
||||
if g.scodoc_dept:
|
||||
|
@ -9,7 +9,7 @@ from flask import current_app, g, redirect, request, url_for
|
||||
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
|
||||
import flask_login
|
||||
|
||||
from app import login
|
||||
from app import db, login
|
||||
from app.auth.models import User
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc.sco_utils import json_error
|
||||
@ -39,7 +39,7 @@ def basic_auth_error(status):
|
||||
@login.user_loader
|
||||
def load_user(uid: str) -> User:
|
||||
"flask-login: accès à un utilisateur"
|
||||
return User.query.get(int(uid))
|
||||
return db.session.get(User, int(uid))
|
||||
|
||||
|
||||
@token_auth.verify_token
|
||||
|
@ -225,7 +225,7 @@ class User(UserMixin, db.Model):
|
||||
return None
|
||||
except (TypeError, KeyError):
|
||||
return None
|
||||
return User.query.get(user_id)
|
||||
return db.session.get(User, user_id)
|
||||
|
||||
def to_dict(self, include_email=True):
|
||||
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
|
||||
@ -376,7 +376,9 @@ class User(UserMixin, db.Model):
|
||||
"""
|
||||
if not isinstance(role, Role):
|
||||
raise ScoValueError("add_role: rôle invalide")
|
||||
self.user_roles.append(UserRole(user=self, role=role, dept=dept))
|
||||
user_role = UserRole(user=self, role=role, dept=dept)
|
||||
db.session.add(user_role)
|
||||
self.user_roles.append(user_role)
|
||||
|
||||
def add_roles(self, roles: "list[Role]", dept: str):
|
||||
"""Add roles to this user.
|
||||
|
@ -12,6 +12,7 @@ import datetime
|
||||
import numpy as np
|
||||
from flask import g, has_request_context, url_for
|
||||
|
||||
from app import db
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import Evaluation, FormSemestre, Identite
|
||||
from app.models.groups import GroupDescr
|
||||
@ -158,7 +159,7 @@ class BulletinBUT:
|
||||
[etud.id]
|
||||
].iterrows():
|
||||
if codes_cursus.code_ue_validant(ue_capitalisee.code):
|
||||
ue = UniteEns.query.get(ue_capitalisee.ue_id) # XXX cacher ?
|
||||
ue = db.session.get(UniteEns, ue_capitalisee.ue_id) # XXX cacher ?
|
||||
# déjà capitalisé ? montre la meilleure
|
||||
if ue.acronyme in d:
|
||||
moy_cap = d[ue.acronyme]["moyenne_num"] or 0.0
|
||||
|
@ -189,7 +189,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
moy_ue = moy_ue.get("value", "-") if moy_ue is not None else "-"
|
||||
t = {
|
||||
"titre": f"{ue_acronym} - {ue['titre']}",
|
||||
"moyenne": Paragraph(f"""<para align=right><b>{moy_ue}</b></para>"""),
|
||||
"moyenne": Paragraph(
|
||||
f"""<para align=right><b>{moy_ue or "-"}</b></para>"""
|
||||
),
|
||||
"_css_row_class": "note_bold",
|
||||
"_pdf_row_markup": ["b"],
|
||||
"_pdf_style": [
|
||||
|
@ -14,17 +14,14 @@ Classe raccordant avec ScoDoc 7:
|
||||
|
||||
"""
|
||||
import collections
|
||||
from typing import Union
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
|
||||
from app.comp import res_sem
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
@ -37,7 +34,6 @@ from app.models import Scolog, ScolarAutorisationInscription
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
RegroupementCoherentUE,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
@ -45,7 +41,8 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import RED, UE_STANDARD
|
||||
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
@ -72,6 +69,7 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
class EtudCursusBUT:
|
||||
"""L'état de l'étudiant dans son cursus BUT
|
||||
Liste des niveaux validés/à valider
|
||||
(utilisé pour le résumé sur la fiche étudiant)
|
||||
"""
|
||||
|
||||
def __init__(self, etud: Identite, formation: Formation):
|
||||
@ -103,8 +101,8 @@ class EtudCursusBUT:
|
||||
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
|
||||
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
||||
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
self.niveaux_by_annee = {}
|
||||
"{ annee : liste des niveaux à valider }"
|
||||
self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
|
||||
"{ annee:int : liste des niveaux à valider }"
|
||||
self.niveaux: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
@ -118,21 +116,6 @@ class EtudCursusBUT:
|
||||
self.niveaux.update(
|
||||
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
)
|
||||
# Probablement inutile:
|
||||
# # Cherche les validations de jury enregistrées pour chaque niveau
|
||||
# self.validations_by_niveau = collections.defaultdict(lambda: [])
|
||||
# " { niveau_id : [ ApcValidationRCUE ] }"
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# self.validations_by_niveau[validation_rcue.niveau().id].append(
|
||||
# validation_rcue
|
||||
# )
|
||||
# self.validation_by_niveau = {
|
||||
# niveau_id: sorted(
|
||||
# validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
|
||||
# )[0]
|
||||
# for niveau_id, validations in self.validations_by_niveau.items()
|
||||
# }
|
||||
# "{ niveau_id : meilleure validation pour ce niveau }"
|
||||
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
@ -145,8 +128,8 @@ class EtudCursusBUT:
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation.code]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
@ -206,6 +189,28 @@ class EtudCursusBUT:
|
||||
)
|
||||
return d
|
||||
|
||||
def competence_annee_has_niveau(self, competence_id: int, annee: str) -> bool:
|
||||
"vrai si la compétence à un niveau dans cette annee ('BUT1') pour le parcour de cet etud"
|
||||
# slow, utile pour affichage fiche
|
||||
return annee in [n.annee for n in self.competences[competence_id].niveaux]
|
||||
|
||||
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
||||
"""Cherche les validations de jury enregistrées pour chaque niveau
|
||||
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
||||
meilleure validation pour ce niveau
|
||||
"""
|
||||
validations_by_niveau = collections.defaultdict(lambda: [])
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=self.etud):
|
||||
validations_by_niveau[validation_rcue.niveau().id].append(validation_rcue)
|
||||
validation_by_niveau = {
|
||||
niveau_id: sorted(
|
||||
validations, key=lambda v: sco_codes.BUT_CODES_ORDER[v.code]
|
||||
)[0]
|
||||
for niveau_id, validations in validations_by_niveau.items()
|
||||
if validations
|
||||
}
|
||||
return validation_by_niveau
|
||||
|
||||
|
||||
class FormSemestreCursusBUT:
|
||||
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
|
||||
@ -246,7 +251,9 @@ class FormSemestreCursusBUT:
|
||||
parcour = None
|
||||
else:
|
||||
if parcour_id not in self.parcours_by_id:
|
||||
self.parcours_by_id[parcour_id] = ApcParcours.query.get(parcour_id)
|
||||
self.parcours_by_id[parcour_id] = db.session.get(
|
||||
ApcParcours, parcour_id
|
||||
)
|
||||
parcour = self.parcours_by_id[parcour_id]
|
||||
|
||||
return self.get_niveaux_parcours_by_annee(parcour)
|
||||
@ -303,8 +310,8 @@ class FormSemestreCursusBUT:
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
@ -340,8 +347,8 @@ class FormSemestreCursusBUT:
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
@ -358,6 +365,66 @@ class FormSemestreCursusBUT:
|
||||
"cache { competence_id : competence }"
|
||||
|
||||
|
||||
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
||||
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
|
||||
Ne prend que les UE associées à des niveaux de compétences,
|
||||
et ne les compte qu'une fois même en cas de redoublement avec re-validation.
|
||||
"""
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.join(UniteEns)
|
||||
.join(ApcNiveau)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=referentiel_competence_id)
|
||||
)
|
||||
|
||||
ects_dict = {}
|
||||
for v in validations:
|
||||
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
||||
if v.code in CODES_UE_VALIDES:
|
||||
ects_dict[key] = v.ue.ects
|
||||
|
||||
return sum(ects_dict.values()) if ects_dict else 0.0
|
||||
|
||||
|
||||
def etud_ues_de_but1_non_validees(
|
||||
etud: Identite, formation: Formation, parcour: ApcParcours
|
||||
) -> list[UniteEns]:
|
||||
"""Liste des UEs de S1 et S2 non validées, dans son parcours"""
|
||||
# Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.join(UniteEns)
|
||||
.filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2))
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
)
|
||||
codes_validations_by_ue_code = collections.defaultdict(list)
|
||||
for v in validations:
|
||||
codes_validations_by_ue_code[v.ue.ue_code].append(v.code)
|
||||
|
||||
# Les UEs du parcours en S1 et S2:
|
||||
ues = formation.query_ues_parcour(parcour).filter(
|
||||
db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)
|
||||
)
|
||||
# Liste triée des ues non validées
|
||||
return sorted(
|
||||
[
|
||||
ue
|
||||
for ue in ues
|
||||
if not any(
|
||||
(
|
||||
code_ue_validant(code)
|
||||
for code in codes_validations_by_ue_code[ue.ue_code]
|
||||
)
|
||||
)
|
||||
],
|
||||
key=attrgetter("numero", "acronyme"),
|
||||
)
|
||||
|
||||
|
||||
def formsemestre_warning_apc_setup(
|
||||
formsemestre: FormSemestre, res: ResultatsSemestreBUT
|
||||
) -> str:
|
||||
@ -413,3 +480,122 @@ def formsemestre_warning_apc_setup(
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def ue_associee_au_niveau_du_parcours(
|
||||
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
||||
) -> UniteEns:
|
||||
"L'UE associée à ce niveau, ou None"
|
||||
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id]
|
||||
if len(ues) > 1:
|
||||
# plusieurs UEs associées à ce niveau: élimine celles sans parcours
|
||||
ues_pair_avec_parcours = [ue for ue in ues if ue.parcours]
|
||||
if ues_pair_avec_parcours:
|
||||
ues = ues_pair_avec_parcours
|
||||
if len(ues) > 1:
|
||||
log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}")
|
||||
return ues[0] if ues else None
|
||||
|
||||
|
||||
def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list:
|
||||
"""
|
||||
[
|
||||
{
|
||||
'competence' : ApcCompetence,
|
||||
'niveaux' : {
|
||||
1 : { ... },
|
||||
2 : { ... },
|
||||
3 : {
|
||||
'niveau' : ApcNiveau,
|
||||
'ue_impair' : UniteEns, # actuellement associée
|
||||
'ues_impair' : list[UniteEns], # choix possibles
|
||||
'ue_pair' : UniteEns,
|
||||
'ues_pair' : list[UniteEns],
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"""
|
||||
refcomp: ApcReferentielCompetences = formation.referentiel_competence
|
||||
|
||||
def _niveau_ues(competence: ApcCompetence, annee: int) -> dict:
|
||||
"""niveau et ues pour cette compétence de cette année du parcours.
|
||||
Si parcour est None, les niveaux du tronc commun
|
||||
"""
|
||||
if parcour is not None:
|
||||
# L'étudiant est inscrit à un parcours: cherche les niveaux
|
||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(
|
||||
parcour, annee, competence=competence
|
||||
)
|
||||
else:
|
||||
# sans parcours, on cherche les niveaux du Tronc Commun de cette année
|
||||
niveaux = [
|
||||
niveau
|
||||
for niveau in refcomp.get_niveaux_by_parcours(annee)[1]["TC"]
|
||||
if niveau.competence_id == competence.id
|
||||
]
|
||||
|
||||
if len(niveaux) > 0:
|
||||
if len(niveaux) > 1:
|
||||
log(
|
||||
f"""_niveau_ues: plus d'un niveau pour {competence}
|
||||
annee {annee} {("parcours " + parcour.code) if parcour else ""}"""
|
||||
)
|
||||
niveau = niveaux[0]
|
||||
elif len(niveaux) == 0:
|
||||
return {
|
||||
"niveau": None,
|
||||
"ue_pair": None,
|
||||
"ue_impair": None,
|
||||
"ues_pair": [],
|
||||
"ues_impair": [],
|
||||
}
|
||||
# Toutes les UEs de la formation dans ce parcours ou tronc commun
|
||||
ues = [
|
||||
ue
|
||||
for ue in formation.ues
|
||||
if (
|
||||
(not ue.parcours)
|
||||
or (parcour is not None and (parcour.id in (p.id for p in ue.parcours)))
|
||||
)
|
||||
and ue.type == UE_STANDARD
|
||||
]
|
||||
ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)]
|
||||
ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)]
|
||||
|
||||
# UE associée au niveau dans ce parcours
|
||||
ue_pair = ue_associee_au_niveau_du_parcours(
|
||||
ues_pair_possibles, niveau, f"S{2*annee}"
|
||||
)
|
||||
ue_impair = ue_associee_au_niveau_du_parcours(
|
||||
ues_impair_possibles, niveau, f"S{2*annee-1}"
|
||||
)
|
||||
|
||||
return {
|
||||
"niveau": niveau,
|
||||
"ue_pair": ue_pair,
|
||||
"ues_pair": [
|
||||
ue
|
||||
for ue in ues_pair_possibles
|
||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
||||
],
|
||||
"ue_impair": ue_impair,
|
||||
"ues_impair": [
|
||||
ue
|
||||
for ue in ues_impair_possibles
|
||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
||||
],
|
||||
}
|
||||
|
||||
competences = [
|
||||
{
|
||||
"competence": competence,
|
||||
"niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)},
|
||||
}
|
||||
for competence in (
|
||||
parcour.query_competences()
|
||||
if parcour
|
||||
else refcomp.competences.order_by(ApcCompetence.numero)
|
||||
)
|
||||
]
|
||||
return competences
|
||||
|
1146
app/but/jury_but.py
1146
app/but/jury_but.py
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
||||
|
||||
from app import log
|
||||
from app.but import jury_but
|
||||
from app.but.cursus_but import but_ects_valides
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -109,6 +110,11 @@ def pvjury_table_but(
|
||||
"""
|
||||
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
|
||||
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||
referentiel_competence_id = formsemestre.formation.referentiel_competence_id
|
||||
if referentiel_competence_id is None:
|
||||
raise ScoValueError(
|
||||
"pas de référentiel de compétences associé à la formation de ce semestre !"
|
||||
)
|
||||
titles = {
|
||||
"nom": "Code" if anonymous else "Nom",
|
||||
"cursus": "Cursus",
|
||||
@ -153,7 +159,7 @@ def pvjury_table_but(
|
||||
etudid=etud.id,
|
||||
),
|
||||
"cursus": _descr_cursus_but(etud),
|
||||
"ects": f"{deca.formsemestre_ects():g}",
|
||||
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
|
||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||
if deca
|
||||
|
@ -48,9 +48,9 @@ def _get_jury_but_etud_result(
|
||||
# --- Les RCUEs
|
||||
rcue_list = []
|
||||
if deca:
|
||||
for rcue in deca.rcues_annee:
|
||||
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
|
||||
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
|
||||
for dec_rcue in deca.get_decisions_rcues_annee():
|
||||
rcue = dec_rcue.rcue
|
||||
if rcue.complete: # n'exporte que les RCUEs complets
|
||||
dec_ue1 = deca.decisions_ues[rcue.ue_1.id]
|
||||
dec_ue2 = deca.decisions_ues[rcue.ue_2.id]
|
||||
rcue_dict = {
|
||||
|
@ -6,24 +6,22 @@
|
||||
|
||||
"""Jury BUT: calcul des décisions de jury annuelles "automatiques"
|
||||
"""
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models import Identite, FormSemestre, ScolarNews
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
|
||||
formsemestre: FormSemestre, only_adm: bool = True
|
||||
) -> int:
|
||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||
|
||||
- N'enregistre jamais de décisions de l'année scolaire précédente, même
|
||||
si on a des RCUE "à cheval".
|
||||
- Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
|
||||
ce qui est utilisé pour certains tests unitaires).
|
||||
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
|
||||
de droit: ADM ou CMP.
|
||||
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||
@ -38,9 +36,17 @@ def formsemestre_validation_auto_but(
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
etud = Identite.get_etud(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
nb_etud_modif += deca.record_all(
|
||||
no_overwrite=no_overwrite, only_validantes=only_adm
|
||||
)
|
||||
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
||||
|
||||
db.session.commit()
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=formsemestre.id,
|
||||
text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()}""",
|
||||
url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
)
|
||||
return nb_etud_modif
|
||||
|
@ -31,9 +31,11 @@ from app.models import (
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarNews,
|
||||
)
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
@ -91,35 +93,25 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
<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(
|
||||
f"""<div class="but_niveau_titre">
|
||||
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
||||
</div>"""
|
||||
)
|
||||
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None
|
||||
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
|
||||
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
|
||||
# Les UEs à afficher,
|
||||
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
|
||||
# qui
|
||||
ues_ro = [
|
||||
(
|
||||
ue_impair,
|
||||
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
|
||||
rcue.ue_cur_impair is None,
|
||||
),
|
||||
(
|
||||
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:
|
||||
@ -153,17 +145,22 @@ def _gen_but_select(
|
||||
code_valide: str,
|
||||
disabled: bool = False,
|
||||
klass: str = "",
|
||||
data: dict = {},
|
||||
data: dict = None,
|
||||
code_valide_label: str = "",
|
||||
) -> str:
|
||||
"Le menu html select avec les codes"
|
||||
# if disabled: # mauvaise idée car le disabled est traité en JS
|
||||
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
|
||||
data = data or {}
|
||||
options_htm = "\n".join(
|
||||
[
|
||||
f"""<option value="{code}"
|
||||
{'selected' if code == code_valide else ''}
|
||||
class="{'recorded' if code == code_valide else ''}"
|
||||
>{code}</option>"""
|
||||
>{code
|
||||
if ((code != code_valide) or not code_valide_label)
|
||||
else code_valide_label
|
||||
}</option>"""
|
||||
for code in codes
|
||||
]
|
||||
)
|
||||
@ -202,20 +199,54 @@ def _gen_but_niveau_ue(
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
elif dec_ue.formsemestre is None:
|
||||
# Validation d'UE antérieure (semestre hors année scolaire courante)
|
||||
if dec_ue.validation:
|
||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.validation.moy_ue)}</span>"""
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>
|
||||
<b>UE {ue.acronyme} antérieure </b>
|
||||
<span>validée {dec_ue.validation.code}
|
||||
le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||
</span>
|
||||
</div>
|
||||
<div>Non reprise dans l'année en cours</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
moy_ue_str = """<span>-</span>"""
|
||||
scoplement = """<div class="scoplement">
|
||||
<div>
|
||||
<b>Pas d'UE en cours ou validée dans cette compétence de ce côté.</b>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
|
||||
if dec_ue.code_valide:
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||
date_str = (
|
||||
f"""enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
|
||||
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
if dec_ue.validation and dec_ue.validation.event_date
|
||||
else ""
|
||||
)
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>Code {dec_ue.code_valide} {date_str}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
scoplement = ""
|
||||
|
||||
return f"""<div class="but_niveau_ue {
|
||||
'recorded' if dec_ue.code_valide is not None else ''}
|
||||
ue_class = "" # '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 ''}
|
||||
">
|
||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||
@ -236,7 +267,7 @@ def _gen_but_niveau_ue(
|
||||
|
||||
|
||||
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 """
|
||||
<div class="but_niveau_rcue niveau_vide with_scoplement">
|
||||
<div></div>
|
||||
@ -244,13 +275,25 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||
</div>
|
||||
"""
|
||||
|
||||
scoplement = (
|
||||
f"""<div class="scoplement">{
|
||||
dec_rcue.validation.to_html()
|
||||
}</div>"""
|
||||
if dec_rcue.validation
|
||||
else ""
|
||||
)
|
||||
code_propose_menu = dec_rcue.code_valide # le code enregistré
|
||||
code_valide_label = code_propose_menu
|
||||
if dec_rcue.validation:
|
||||
if dec_rcue.code_valide == dec_rcue.codes[0]:
|
||||
descr_validation = dec_rcue.validation.html()
|
||||
else: # on une validation enregistrée différence de celle proposée
|
||||
descr_validation = f"""Décision recommandée: <b>{dec_rcue.codes[0]}.</b>
|
||||
Il y avait {dec_rcue.validation.html()}"""
|
||||
if (
|
||||
sco_codes.BUT_CODES_ORDER[dec_rcue.codes[0]]
|
||||
> sco_codes.BUT_CODES_ORDER[dec_rcue.code_valide]
|
||||
):
|
||||
code_propose_menu = dec_rcue.codes[0]
|
||||
code_valide_label = (
|
||||
f"{dec_rcue.codes[0]} (actuel {dec_rcue.code_valide})"
|
||||
)
|
||||
scoplement = f"""<div class="scoplement">{descr_validation}</div>"""
|
||||
else:
|
||||
scoplement = "" # "pas de validation"
|
||||
|
||||
# Déjà enregistré ?
|
||||
niveau_rcue_class = ""
|
||||
@ -270,10 +313,11 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||
<div class="but_code">
|
||||
{_gen_but_select("code_rcue_"+str(niveau.id),
|
||||
dec_rcue.codes,
|
||||
dec_rcue.code_valide,
|
||||
code_propose_menu,
|
||||
disabled=True,
|
||||
klass="manual code_rcue",
|
||||
data = { "niveau_id" : str(niveau.id)}
|
||||
data = { "niveau_id" : str(niveau.id)},
|
||||
code_valide_label = code_valide_label,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -351,6 +395,16 @@ def jury_but_semestriel(
|
||||
flash(
|
||||
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=formsemestre.id,
|
||||
text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
|
||||
url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_validation_but",
|
||||
@ -394,7 +448,7 @@ def jury_but_semestriel(
|
||||
{warning}
|
||||
</div>
|
||||
|
||||
<form method="post" id="jury_but">
|
||||
<form method="post" class="jury_but_box" id="jury_but">
|
||||
""",
|
||||
]
|
||||
|
||||
|
67
app/but/jury_edit_manual.py
Normal file
67
app/but/jury_edit_manual.py
Normal file
@ -0,0 +1,67 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
||||
|
||||
Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def jury_delete_manual(etud: Identite):
|
||||
"""Vue (réservée au chef de dept.)
|
||||
présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant de les supprimer une à une.
|
||||
"""
|
||||
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, ue_id=None
|
||||
).order_by(ScolarFormSemestreValidation.event_date)
|
||||
ue_vals = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.order_by(
|
||||
sa.extract("year", ScolarFormSemestreValidation.event_date),
|
||||
UniteEns.semestre_idx,
|
||||
UniteEns.numero,
|
||||
UniteEns.acronyme,
|
||||
)
|
||||
)
|
||||
autorisations = ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id
|
||||
).order_by(
|
||||
ScolarAutorisationInscription.semestre_id, ScolarAutorisationInscription.date
|
||||
)
|
||||
rcue_vals = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.order_by(UniteEns.semestre_idx, UniteEns.numero, ApcValidationRCUE.date)
|
||||
)
|
||||
annee_but_vals = ApcValidationAnnee.query.filter_by(etudid=etud.id).order_by(
|
||||
ApcValidationAnnee.ordre, ApcValidationAnnee.date
|
||||
)
|
||||
return render_template(
|
||||
"jury/jury_delete_manual.j2",
|
||||
etud=etud,
|
||||
sem_vals=sem_vals,
|
||||
ue_vals=ue_vals,
|
||||
autorisations=autorisations,
|
||||
rcue_vals=rcue_vals,
|
||||
annee_but_vals=annee_but_vals,
|
||||
sco=ScoData(),
|
||||
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
||||
)
|
253
app/but/rcue.py
Normal file
253
app/but/rcue.py
Normal file
@ -0,0 +1,253 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs
|
||||
"""
|
||||
from typing import Union
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
ApcValidationRCUE,
|
||||
Identite,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc.codes_cursus import BUT_CODES_ORDER
|
||||
|
||||
|
||||
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 UEs.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
etud: Identite,
|
||||
niveau: ApcNiveau,
|
||||
res_pair: ResultatsSemestreBUT,
|
||||
res_impair: ResultatsSemestreBUT,
|
||||
semestre_id_impair: int,
|
||||
cur_ues_pair: list[UniteEns],
|
||||
cur_ues_impair: list[UniteEns],
|
||||
):
|
||||
"""
|
||||
res_pair, res_impair: résultats des formsemestre de l'année en cours, ou None
|
||||
cur_ues_pair, cur_ues_impair: ues auxquelles l'étudiant est inscrit cette année
|
||||
"""
|
||||
self.semestre_id_impair = semestre_id_impair
|
||||
self.semestre_id_pair = semestre_id_impair + 1
|
||||
self.etud: Identite = etud
|
||||
self.niveau: ApcNiveau = niveau
|
||||
"Le niveau de compétences de ce RCUE"
|
||||
# Chercher l'UE en cours pour pair, impair
|
||||
# une UE à laquelle l'étudiant est inscrit (non dispensé)
|
||||
# dans l'un des formsemestre en cours
|
||||
ues = [ue for ue in cur_ues_pair if ue.niveau_competence_id == niveau.id]
|
||||
self.ue_cur_pair = ues[0] if ues else None
|
||||
"UE paire en cours"
|
||||
ues = [ue for ue in cur_ues_impair if ue.niveau_competence_id == niveau.id]
|
||||
self.ue_cur_impair = ues[0] if ues else None
|
||||
"UE impaire en cours"
|
||||
|
||||
self.validation_ue_cur_pair = (
|
||||
ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id,
|
||||
formsemestre_id=res_pair.formsemestre.id,
|
||||
ue_id=self.ue_cur_pair.id,
|
||||
).first()
|
||||
if self.ue_cur_pair
|
||||
else None
|
||||
)
|
||||
self.validation_ue_cur_impair = (
|
||||
ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id,
|
||||
formsemestre_id=res_impair.formsemestre.id,
|
||||
ue_id=self.ue_cur_impair.id,
|
||||
).first()
|
||||
if self.ue_cur_impair
|
||||
else None
|
||||
)
|
||||
|
||||
# Autres validations pour l'UE paire
|
||||
self.validation_ue_best_pair = best_autre_ue_validation(
|
||||
etud.id,
|
||||
niveau.id,
|
||||
semestre_id_impair + 1,
|
||||
res_pair.formsemestre.id if (res_pair and self.ue_cur_pair) else None,
|
||||
)
|
||||
self.validation_ue_best_impair = best_autre_ue_validation(
|
||||
etud.id,
|
||||
niveau.id,
|
||||
semestre_id_impair,
|
||||
res_impair.formsemestre.id if (res_impair and self.ue_cur_impair) else None,
|
||||
)
|
||||
|
||||
# Suis-je complet ? (= en cours ou validé sur les deux moitiés)
|
||||
self.complete = (self.ue_cur_pair or self.validation_ue_best_pair) and (
|
||||
self.ue_cur_impair or self.validation_ue_best_impair
|
||||
)
|
||||
if not self.complete:
|
||||
self.moy_rcue = None
|
||||
|
||||
# Stocke les moyennes d'UE
|
||||
self.res_impair = None
|
||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
||||
self.ue_status_impair = None
|
||||
if self.ue_cur_impair:
|
||||
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
|
||||
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
|
||||
self.ue_1 = self.ue_cur_impair
|
||||
self.res_impair = res_impair
|
||||
self.ue_status_impair = ue_status
|
||||
elif self.validation_ue_best_impair:
|
||||
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
|
||||
self.ue_1 = self.validation_ue_best_impair.ue
|
||||
else:
|
||||
self.moy_ue_1, self.ue_1 = None, None
|
||||
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
|
||||
|
||||
self.res_pair = None
|
||||
"résultats formsemestre de l'UE si elle est courante, None sinon"
|
||||
self.ue_status_pair = None
|
||||
if self.ue_cur_pair:
|
||||
ue_status = res_pair.get_etud_ue_status(etud.id, self.ue_cur_pair.id)
|
||||
self.moy_ue_2 = ue_status["moy"] if ue_status else None # avec capitalisée
|
||||
self.ue_2 = self.ue_cur_pair
|
||||
self.res_pair = res_pair
|
||||
self.ue_status_pair = ue_status
|
||||
elif self.validation_ue_best_pair:
|
||||
self.moy_ue_2 = self.validation_ue_best_pair.moy_ue
|
||||
self.ue_2 = self.validation_ue_best_pair.ue
|
||||
else:
|
||||
self.moy_ue_2, self.ue_2 = None, None
|
||||
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 ou antérieures)
|
||||
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 * self.ue_1.coef_rcue
|
||||
+ self.moy_ue_2 * self.ue_2.coef_rcue
|
||||
) / (self.ue_1.coef_rcue + self.ue_2.coef_rcue)
|
||||
else:
|
||||
self.moy_rcue = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} {
|
||||
self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) {
|
||||
self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})>"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""RCUE {
|
||||
self.ue_1.acronyme if self.ue_1 else "?"}({self.moy_ue_1}) + {
|
||||
self.ue_2.acronyme if self.ue_2 else "?"}({self.moy_ue_2})"""
|
||||
|
||||
def query_validations(
|
||||
self,
|
||||
) -> Query: # list[ApcValidationRCUE]
|
||||
"""Les validations de jury enregistrées pour ce RCUE"""
|
||||
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 == self.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 > codes_cursus.BUT_BARRE_RCUE)
|
||||
and (
|
||||
(self.moy_ue_1_val < codes_cursus.NOTES_BARRE_GEN)
|
||||
or (self.moy_ue_2_val < codes_cursus.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 > codes_cursus.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 > codes_cursus.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 codes_cursus.CODES_RCUE_VALIDES
|
||||
):
|
||||
return validation
|
||||
return None
|
||||
|
||||
|
||||
def best_autre_ue_validation(
|
||||
etudid: int, niveau_id: int, semestre_id: int, formsemestre_id: int
|
||||
) -> ScolarFormSemestreValidation:
|
||||
"""La "meilleure" validation validante d'UE pour ce niveau/semestre"""
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
|
||||
.join(UniteEns)
|
||||
.filter_by(semestre_idx=semestre_id)
|
||||
.join(ApcNiveau)
|
||||
.filter(ApcNiveau.id == niveau_id)
|
||||
)
|
||||
validations = [v for v in validations if codes_cursus.code_ue_validant(v.code)]
|
||||
# Elimine l'UE en cours si elle existe
|
||||
if formsemestre_id is not None:
|
||||
validations = [v for v in validations if v.formsemestre_id != formsemestre_id]
|
||||
validations = sorted(validations, key=lambda v: BUT_CODES_ORDER.get(v.code, 0))
|
||||
return validations[-1] if validations else None
|
||||
|
||||
|
||||
# def compute_ues_by_niveau(
|
||||
# niveaux: list[ApcNiveau],
|
||||
# ) -> dict[int, tuple[list[UniteEns], list[UniteEns]]]:
|
||||
# """UEs à valider cette année pour cet étudiant, selon son parcours.
|
||||
# Considérer les UEs associées aux niveaux et non celles des formsemestres
|
||||
# en cours. Notez que même si l'étudiant n'est pas inscrit ("dispensé") à une UE
|
||||
# dans le formsemestre origine, elle doit apparaitre sur la page jury.
|
||||
# Return: { niveau_id : ( [ues impair], [ues pair]) }
|
||||
# """
|
||||
# # Les UEs associées à ce niveau, toutes formations confondues
|
||||
# return {
|
||||
# niveau.id: (
|
||||
# [ue for ue in niveau.ues if ue.semestre_idx % 2],
|
||||
# [ue for ue in niveau.ues if not (ue.semestre_idx % 2)],
|
||||
# )
|
||||
# for niveau in niveaux
|
||||
# }
|
117
app/but/validations_view.py
Normal file
117
app/but/validations_view.py
Normal file
@ -0,0 +1,117 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
||||
|
||||
Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import log
|
||||
from app.but import cursus_but
|
||||
from app.models import (
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcReferentielCompetences,
|
||||
# ApcValidationAnnee, # TODO
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
UniteEns,
|
||||
# ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = False):
|
||||
"""Page de saisie des décisions de RCUEs "antérieures"
|
||||
On peut l'utiliser pour saisir la validation de n'importe quel RCUE
|
||||
d'une année antérieure et de la formation du formsemestre indiqué.
|
||||
"""
|
||||
formation: Formation = formsemestre.formation
|
||||
refcomp = formation.referentiel_competence
|
||||
if refcomp is None:
|
||||
raise ScoNoReferentielCompetences(formation=formation)
|
||||
parcour = formsemestre.etuds_inscriptions[etud.id].parcour
|
||||
# Si non inscrit à un parcours, prend toutes les compétences
|
||||
competences_parcour = cursus_but.parcour_formation_competences(parcour, formation)
|
||||
|
||||
ue_validation_by_niveau = get_ue_validation_by_niveau(refcomp, etud)
|
||||
rcue_validation_by_niveau = get_rcue_validation_by_niveau(refcomp, etud)
|
||||
ects_total = sum((v.ects() for v in ue_validation_by_niveau.values()))
|
||||
return render_template(
|
||||
"but/validation_rcues.j2",
|
||||
competences_parcour=competences_parcour,
|
||||
edit=edit,
|
||||
ects_total=ects_total,
|
||||
formation=formation,
|
||||
parcour=parcour,
|
||||
rcue_validation_by_niveau=rcue_validation_by_niveau,
|
||||
rcue_codes=sorted(codes_cursus.CODES_JURY_RCUE),
|
||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||
title=f"{formation.acronyme} - Niveaux et UEs",
|
||||
ue_validation_by_niveau=ue_validation_by_niveau,
|
||||
)
|
||||
|
||||
|
||||
def get_ue_validation_by_niveau(
|
||||
refcomp: ApcReferentielCompetences, etud: Identite
|
||||
) -> dict[tuple[int, str], ScolarFormSemestreValidation]:
|
||||
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
|
||||
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
|
||||
"""
|
||||
validations: list[ScolarFormSemestreValidation] = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.join(ApcNiveau)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=refcomp.id)
|
||||
.all()
|
||||
)
|
||||
# La meilleure validation pour chaque UE
|
||||
ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation }
|
||||
for validation in validations:
|
||||
if validation.ue.niveau_competence is None:
|
||||
log(
|
||||
f"""validation_rcues: ignore validation d'UE {
|
||||
validation.ue.id} pas de niveau de competence"""
|
||||
)
|
||||
key = (
|
||||
validation.ue.niveau_competence.id,
|
||||
"impair" if validation.ue.semestre_idx % 2 else "pair",
|
||||
)
|
||||
existing = ue_validation_by_niveau.get(key, None)
|
||||
if (not existing) or (
|
||||
codes_cursus.BUT_CODES_ORDER[existing.code]
|
||||
< codes_cursus.BUT_CODES_ORDER[validation.code]
|
||||
):
|
||||
ue_validation_by_niveau[key] = validation
|
||||
return ue_validation_by_niveau
|
||||
|
||||
|
||||
def get_rcue_validation_by_niveau(
|
||||
refcomp: ApcReferentielCompetences, etud: Identite
|
||||
) -> dict[int, ApcValidationRCUE]:
|
||||
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
|
||||
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
|
||||
"""
|
||||
validations: list[ApcValidationRCUE] = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=refcomp.id)
|
||||
.all()
|
||||
)
|
||||
return {
|
||||
validation.ue2.niveau_competence.id: validation for validation in validations
|
||||
}
|
@ -18,7 +18,7 @@ import pandas as pd
|
||||
|
||||
from flask import g
|
||||
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.codes_cursus import UE_SPORT, UE_STANDARD
|
||||
from app.scodoc.codes_cursus import CursusDUT, CursusDUTMono
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
@ -194,7 +194,7 @@ class BonusSportAdditif(BonusSport):
|
||||
# les points au dessus du seuil sont comptés (defaut: seuil_moy_gen):
|
||||
seuil_comptage = None
|
||||
proportion_point = 0.05 # multiplie les points au dessus du seuil
|
||||
bonux_max = 20.0 # le bonus ne peut dépasser 20 points
|
||||
bonus_max = 20.0 # le bonus ne peut dépasser 20 points
|
||||
bonus_min = 0.0 # et ne peut pas être négatif
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
@ -435,8 +435,11 @@ class BonusAmiens(BonusSportAdditif):
|
||||
class BonusBesanconVesoul(BonusSportAdditif):
|
||||
"""Bonus IUT Besançon - Vesoul pour les UE libres
|
||||
|
||||
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
|
||||
sur toutes les moyennes d'UE.
|
||||
<p>Le bonus est compris entre 0 et 0,2 points.
|
||||
et est reporté sur les moyennes d'UE.
|
||||
</p>
|
||||
<p>La valeur saisie doit être entre 0 et 0,2: toute valeur
|
||||
supérieure à 0,2 entraine un bonus de 0,2.
|
||||
</p>
|
||||
"""
|
||||
|
||||
@ -444,7 +447,7 @@ class BonusBesanconVesoul(BonusSportAdditif):
|
||||
displayed_name = "IUT de Besançon - Vesoul"
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
seuil_moy_gen = 0.0 # tous les points sont comptés
|
||||
proportion_point = 1e10 # infini
|
||||
proportion_point = 1
|
||||
bonus_max = 0.2
|
||||
|
||||
|
||||
@ -740,6 +743,7 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
|
||||
|
||||
name = "bonus_iut1grenoble_2017"
|
||||
displayed_name = "IUT de Grenoble 1"
|
||||
|
||||
# C'est un bonus "multiplicatif": on l'exprime en additif,
|
||||
# sur chaque moyenne d'UE m_0
|
||||
# Augmenter de 5% correspond à multiplier par a=1.05
|
||||
@ -782,6 +786,7 @@ class BonusIUTRennes1(BonusSportAdditif):
|
||||
seuil_moy_gen = 10.0
|
||||
proportion_point = 1 / 20.0
|
||||
classic_use_bonus_ues = False
|
||||
|
||||
# S'applique aussi en classic, sur la moy. gen.
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus"""
|
||||
@ -822,16 +827,32 @@ class BonusStMalo(BonusIUTRennes1):
|
||||
class BonusLaRocheSurYon(BonusSportAdditif):
|
||||
"""Bonus IUT de La Roche-sur-Yon
|
||||
|
||||
Si une note de bonus est saisie, l'étudiant est gratifié de 0,2 points
|
||||
sur sa moyenne générale ou, en BUT, sur la moyenne de chaque UE.
|
||||
<p>
|
||||
<b>La note saisie s'applique directement</b>: si on saisit 0,2, un bonus de 0,2 points est appliqué
|
||||
aux moyennes.
|
||||
La valeur maximale du bonus est 1 point. Il est appliqué sur les moyennes d'UEs en BUT,
|
||||
ou sur la moyenne générale dans les autres formations.
|
||||
</p>
|
||||
<p>Pour les <b>semestres antérieurs à janvier 2023</b>: si une note de bonus est saisie,
|
||||
l'étudiant est gratifié de 0,2 points sur sa moyenne générale ou, en BUT, sur la
|
||||
moyenne de chaque UE.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_larochesuryon"
|
||||
displayed_name = "IUT de La Roche-sur-Yon"
|
||||
seuil_moy_gen = 0.0
|
||||
seuil_comptage = 0.0
|
||||
proportion_point = 1e10 # le moindre point sature le bonus
|
||||
bonus_max = 0.2 # à 0.2
|
||||
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
"""calcul du bonus, avec réglage différent suivant la date"""
|
||||
if self.formsemestre.date_debut > datetime.date(2022, 12, 31):
|
||||
self.proportion_point = 1.0
|
||||
self.bonus_max = 1
|
||||
else: # ancienne règle
|
||||
self.proportion_point = 1e10 # le moindre point sature le bonus
|
||||
self.bonus_max = 0.2 # à 0.2
|
||||
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
|
||||
|
||||
|
||||
class BonusLaRochelle(BonusSportAdditif):
|
||||
@ -1055,6 +1076,36 @@ class BonusLyon(BonusSportAdditif):
|
||||
)
|
||||
|
||||
|
||||
class BonusLyon3(BonusSportAdditif):
|
||||
"""IUT de Lyon 3 (septembre 2022)
|
||||
|
||||
<p>Nous avons deux types de bonifications : sport et/ou culture
|
||||
</p>
|
||||
<p>
|
||||
Pour chaque point au-dessus de 10 obtenu en sport ou en culture nous
|
||||
ajoutons 0,03 points à toutes les moyennes d’UE du semestre. Exemple : 16 en
|
||||
sport ajoute 6*0,03 = 0,18 points à toutes les moyennes d’UE du semestre.
|
||||
</p>
|
||||
<p>
|
||||
Les bonifications sport et culture peuvent se cumuler dans la limite de 0,3
|
||||
points ajoutés aux moyennes des UE. Exemple : 17 en sport et 16 en culture
|
||||
conduisent au calcul (7 + 6)*0,03 = 0,39 qui dépasse 0,3. La bonification
|
||||
dans ce cas ne sera que de 0,3 points ajoutés à toutes les moyennes d’UE du
|
||||
semestre.
|
||||
</p>
|
||||
<p>
|
||||
Dans Scodoc on déclarera une UE Sport&Culture dans laquelle on aura un
|
||||
module pour le Sport et un autre pour la Culture avec pour chaque module la
|
||||
note sur 20 obtenue en sport ou en culture par l’étudiant.
|
||||
</p>
|
||||
"""
|
||||
|
||||
name = "bonus_lyon3"
|
||||
displayed_name = "IUT de Lyon 3"
|
||||
proportion_point = 0.03
|
||||
bonus_max = 0.3
|
||||
|
||||
|
||||
class BonusMantes(BonusSportAdditif):
|
||||
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.
|
||||
|
||||
@ -1336,6 +1387,7 @@ class BonusStNazaire(BonusSport):
|
||||
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
|
||||
amplitude = 0.01 / 4 # 4pt => 1%
|
||||
factor_max = 0.1 # 10% max
|
||||
|
||||
# Modifié 2022-11-29: calculer chaque bonus
|
||||
# (de 1 à 3 modules) séparément.
|
||||
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||
@ -1533,6 +1585,63 @@ class BonusIUTV(BonusSportAdditif):
|
||||
# c'est le bonus par défaut: aucune méthode à surcharger
|
||||
|
||||
|
||||
# Finalement inutile: un bonus direct est mieux adapté à leurs besoins.
|
||||
# # class BonusMastersUSPNIG(BonusSportAdditif):
|
||||
# """Calcul bonus modules optionnels (sport, culture), règle Masters de l'Institut Galilée (USPN)
|
||||
|
||||
# Les étudiants peuvent suivre des enseignements optionnels
|
||||
# de l'USPN (sports, musique, deuxième langue, culture, etc) dans une
|
||||
# UE libre. Les points au-dessus de 10 sur 20 obtenus dans cette UE
|
||||
# libre sont ajoutés au total des points obtenus pour les UE obligatoires
|
||||
# du semestre concerné.
|
||||
# """
|
||||
|
||||
# name = "bonus_masters__uspn_ig"
|
||||
# displayed_name = "Masters de l'Institut Galilée (USPN)"
|
||||
# proportion_point = 1.0
|
||||
# seuil_moy_gen = 10.0
|
||||
|
||||
# def __init__(
|
||||
# self,
|
||||
# formsemestre: "FormSemestre",
|
||||
# sem_modimpl_moys: np.array,
|
||||
# ues: list,
|
||||
# modimpl_inscr_df: pd.DataFrame,
|
||||
# modimpl_coefs: np.array,
|
||||
# etud_moy_gen,
|
||||
# etud_moy_ue,
|
||||
# ):
|
||||
# # Pour ce bonus, il nous faut la somme des coefs des modules non bonus
|
||||
# # du formsemestre (et non auxquels les étudiants sont inscrits !)
|
||||
# self.sum_coefs = sum(
|
||||
# [
|
||||
# m.module.coefficient
|
||||
# for m in formsemestre.modimpls_sorted
|
||||
# if (m.module.module_type == ModuleType.STANDARD)
|
||||
# and (m.module.ue.type == UE_STANDARD)
|
||||
# ]
|
||||
# )
|
||||
# super().__init__(
|
||||
# formsemestre,
|
||||
# sem_modimpl_moys,
|
||||
# ues,
|
||||
# modimpl_inscr_df,
|
||||
# modimpl_coefs,
|
||||
# etud_moy_gen,
|
||||
# etud_moy_ue,
|
||||
# )
|
||||
# # Bonus sur la moyenne générale seulement
|
||||
# # On a dans bonus_moy_arr le bonus additif classique
|
||||
# # Sa valeur sera appliquée comme moy_gen += bonus_moy_gen
|
||||
# # or ici on veut
|
||||
# # moy_gen = (somme des notes + bonus_moy_arr) / somme des coefs
|
||||
# # moy_gen += bonus_moy_arr / somme des coefs
|
||||
|
||||
# self.bonus_moy_gen = (
|
||||
# None if self.bonus_moy_gen is None else self.bonus_moy_gen / self.sum_coefs
|
||||
# )
|
||||
|
||||
|
||||
def get_bonus_class_dict(start=BonusSport, d=None):
|
||||
"""Dictionnaire des classes de bonus
|
||||
(liste les sous-classes de BonusSport ayant un nom)
|
||||
|
@ -10,8 +10,17 @@ import pandas as pd
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
|
||||
@ -81,7 +90,7 @@ class ValidationsSemestre(ResultatsCache):
|
||||
|
||||
# UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }}
|
||||
decisions_jury_ues = {}
|
||||
# Parcours les décisions d'UE:
|
||||
# Parcoure les décisions d'UE:
|
||||
for decision in (
|
||||
decisions_jury_q.filter(db.text("ue_id is not NULL"))
|
||||
.join(UniteEns)
|
||||
@ -172,3 +181,79 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
||||
with db.engine.begin() as connection:
|
||||
df = pd.read_sql_query(query, connection, params=params, index_col="etudid")
|
||||
return df
|
||||
|
||||
|
||||
def erase_decisions_annee_formation(
|
||||
etud: Identite, formation: Formation, annee: int, delete=False
|
||||
) -> list:
|
||||
"""Efface toutes les décisions de jury de l'étudiant dans les formations de même code
|
||||
que celle donnée pour cette année de la formation:
|
||||
UEs, RCUEs de l'année BUT, année BUT, passage vers l'année suivante.
|
||||
Ne considère pas l'origine de la décision.
|
||||
annee: entier, 1, 2, 3, ...
|
||||
Si delete est faux, renvoie la liste des validations qu'il faudrait effacer, sans y toucher.
|
||||
"""
|
||||
sem1, sem2 = annee * 2 - 1, annee * 2
|
||||
# UEs
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.filter(db.or_(UniteEns.semestre_idx == sem1, UniteEns.semestre_idx == sem2))
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.order_by(
|
||||
UniteEns.acronyme, UniteEns.numero
|
||||
) # acronyme d'abord car 2 semestres
|
||||
.all()
|
||||
)
|
||||
# RCUEs (a priori inutile de matcher sur l'ue2_id)
|
||||
validations += (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.filter_by(semestre_idx=sem1)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.order_by(UniteEns.acronyme, UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
# Validation de semestres classiques
|
||||
validations += (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id, ue_id=None)
|
||||
.join(
|
||||
FormSemestre,
|
||||
FormSemestre.id == ScolarFormSemestreValidation.formsemestre_id,
|
||||
)
|
||||
.filter(
|
||||
db.or_(FormSemestre.semestre_id == sem1, FormSemestre.semestre_id == sem2)
|
||||
)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.all()
|
||||
)
|
||||
# Année BUT
|
||||
validations += ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
ordre=annee,
|
||||
referentiel_competence_id=formation.referentiel_competence_id,
|
||||
).all()
|
||||
# Autorisations vers les semestres suivants ceux de l'année:
|
||||
validations += (
|
||||
ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id, formation_code=formation.formation_code
|
||||
)
|
||||
.filter(
|
||||
db.or_(
|
||||
ScolarAutorisationInscription.semestre_id == sem1 + 1,
|
||||
ScolarAutorisationInscription.semestre_id == sem2 + 1,
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if delete:
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
return []
|
||||
return validations
|
||||
|
@ -134,7 +134,7 @@ class ModuleImplResults:
|
||||
manque des notes) ssi il y a des étudiants inscrits au semestre et au module
|
||||
qui ont des notes ATT.
|
||||
"""
|
||||
moduleimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
||||
self.etudids = self._etudids()
|
||||
|
||||
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
|
||||
@ -225,8 +225,8 @@ class ModuleImplResults:
|
||||
"""
|
||||
return [
|
||||
inscr.etudid
|
||||
for inscr in ModuleImpl.query.get(
|
||||
self.moduleimpl_id
|
||||
for inscr in db.session.get(
|
||||
ModuleImpl, self.moduleimpl_id
|
||||
).formsemestre.inscriptions
|
||||
]
|
||||
|
||||
@ -319,10 +319,16 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||
ne donnent pas de coef vers cette UE.
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
||||
nb_etuds, nb_evals = self.evals_notes.shape
|
||||
nb_ues = evals_poids_df.shape[1]
|
||||
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
|
||||
if evals_poids_df.shape[0] != nb_evals:
|
||||
# compat notes/poids: race condition ?
|
||||
app.critical_error(
|
||||
f"""compute_module_moy: evals_poids_df.shape[0] != nb_evals ({
|
||||
evals_poids_df.shape[0]} != {nb_evals})
|
||||
"""
|
||||
)
|
||||
if nb_etuds == 0:
|
||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
if nb_ues == 0:
|
||||
@ -413,7 +419,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||
|
||||
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
|
||||
"""
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
|
||||
ues = modimpl.formsemestre.get_ues(with_sport=False)
|
||||
ue_ids = [ue.id for ue in ues]
|
||||
@ -492,7 +498,7 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||
ne donnent pas de coef.
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
|
||||
nb_etuds, nb_evals = self.evals_notes.shape
|
||||
if nb_etuds == 0:
|
||||
return pd.Series()
|
||||
|
@ -30,7 +30,10 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from flask import flash, g, Markup, url_for
|
||||
from flask import flash, g, url_for
|
||||
from markupsafe import Markup
|
||||
|
||||
from app import db
|
||||
from app.models.formations import Formation
|
||||
|
||||
|
||||
@ -78,7 +81,7 @@ def compute_sem_moys_apc_using_ects(
|
||||
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1)
|
||||
except TypeError:
|
||||
if None in ects:
|
||||
formation = Formation.query.get(formation_id)
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
flash(
|
||||
Markup(
|
||||
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||
@ -92,7 +95,7 @@ def compute_sem_moys_apc_using_ects(
|
||||
return moy_gen
|
||||
|
||||
|
||||
def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
|
||||
def comp_ranks_series(notes: pd.Series) -> tuple[pd.Series, pd.Series]:
|
||||
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
|
||||
numérique) en tenant compte des ex-aequos.
|
||||
|
||||
|
@ -30,6 +30,7 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import (
|
||||
@ -167,8 +168,14 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
||||
"""
|
||||
assert len(modimpls_notes)
|
||||
modimpls_notes_arr = [df.values for df in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud x ue) à (etud x mod x ue)
|
||||
try:
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud x ue) à (etud x mod x ue)
|
||||
except ValueError:
|
||||
app.critical_error(
|
||||
f"""notes_sem_assemble_cube: shapes {
|
||||
", ".join([x.shape for x in modimpls_notes_arr])}"""
|
||||
)
|
||||
return modimpls_notes.swapaxes(0, 1)
|
||||
|
||||
|
||||
|
@ -10,17 +10,17 @@ import time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import FormSemestreInscription, ScoDocSiteConfig
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.but_refcomp import ApcParcours, ApcNiveau
|
||||
from app.models.ues import DispenseUE, UniteEns
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.codes_cursus import BUT_CODES_ORDER, UE_SPORT
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
@ -44,7 +44,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||
self.ues_ids_by_parcour: dict[set[int]] = {}
|
||||
"""{ parcour_id : set }, ue_id de chaque parcours"""
|
||||
|
||||
self.validations_annee: dict[int, ApcValidationAnnee] = {}
|
||||
"""chargé par get_validations_annee: jury annuel BUT"""
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
self.compute()
|
||||
@ -288,9 +289,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
if ref_comp is None:
|
||||
return set()
|
||||
if parcour_id is None:
|
||||
ues_ids = {ue.id for ue in self.ues}
|
||||
ues_ids = {ue.id for ue in self.ues if ue.type != UE_SPORT}
|
||||
else:
|
||||
parcour: ApcParcours = ApcParcours.query.get(parcour_id)
|
||||
parcour: ApcParcours = db.session.get(ApcParcours, parcour_id)
|
||||
annee = (self.formsemestre.semestre_id + 1) // 2
|
||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(parcour, annee, ref_comp)
|
||||
# Les UEs du formsemestre associées à ces niveaux:
|
||||
@ -306,12 +307,13 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
||||
return ues_ids
|
||||
|
||||
def etud_has_decision(self, etudid):
|
||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||
def etud_has_decision(self, etudid) -> bool:
|
||||
"""True s'il y a une décision (quelconque) de jury
|
||||
émanant de ce formsemestre pour cet étudiant.
|
||||
prend aussi en compte les autorisations de passage.
|
||||
Sous-classée en BUT pour les RCUEs et années.
|
||||
Ici sous-classée (BUT) pour les RCUEs et années.
|
||||
"""
|
||||
return (
|
||||
return bool(
|
||||
super().etud_has_decision(etudid)
|
||||
or ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
@ -320,3 +322,40 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
).count()
|
||||
)
|
||||
|
||||
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
|
||||
"""Les validations des étudiants de ce semestre
|
||||
pour l'année BUT d'une formation compatible avec celle de ce semestre.
|
||||
Attention:
|
||||
1) la validation ne provient pas nécessairement de ce semestre
|
||||
(redoublants, pair/impair, extérieurs).
|
||||
2) l'étudiant a pu démissionner ou défaillir.
|
||||
3) S'il y a plusieurs validations pour le même étudiant, prend la "meilleure".
|
||||
|
||||
Mémorise le résultat (dans l'instance, pas en cache: TODO voir au profiler)
|
||||
"""
|
||||
if self.validations_annee:
|
||||
return self.validations_annee
|
||||
annee_but = (self.formsemestre.semestre_id + 1) // 2
|
||||
validations = ApcValidationAnnee.query.filter_by(
|
||||
ordre=annee_but,
|
||||
referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
|
||||
).join(
|
||||
FormSemestreInscription,
|
||||
db.and_(
|
||||
FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
|
||||
FormSemestreInscription.formsemestre_id == self.formsemestre.id,
|
||||
),
|
||||
)
|
||||
validation_by_etud = {}
|
||||
for validation in validations:
|
||||
if validation.etudid in validation_by_etud:
|
||||
# keep the "best"
|
||||
if BUT_CODES_ORDER.get(validation.code, 0) > BUT_CODES_ORDER.get(
|
||||
validation_by_etud[validation.etudid].code, 0
|
||||
):
|
||||
validation_by_etud[validation.etudid] = validation
|
||||
else:
|
||||
validation_by_etud[validation.etudid] = validation
|
||||
self.validations_annee = validation_by_etud
|
||||
return self.validations_annee
|
||||
|
@ -17,6 +17,7 @@ import pandas as pd
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.comp.jury import ValidationsSemestre
|
||||
@ -31,6 +32,7 @@ from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
# Il faut bien distinguer
|
||||
# - ce qui est caché de façon persistente (via redis):
|
||||
# ce sont les attributs listés dans `_cached_attrs`
|
||||
@ -137,7 +139,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
||||
"""Liste des UE auxquelles l'étudiant est inscrit
|
||||
(sans bonus, en BUT prend en compte le parcours de l'étudiant)."""
|
||||
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))
|
||||
return (db.session.get(UniteEns, ue_id) for ue_id in self.etud_ues_ids(etudid))
|
||||
|
||||
def etud_ects_tot_sem(self, etudid: int) -> float:
|
||||
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
||||
@ -351,7 +353,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
"""L'état de l'UE pour cet étudiant.
|
||||
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
||||
"""
|
||||
ue: UniteEns = UniteEns.query.get(ue_id)
|
||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||
ue_dict = ue.to_dict()
|
||||
|
||||
if ue.type == UE_SPORT:
|
||||
@ -381,7 +383,11 @@ class ResultatsSemestre(ResultatsCache):
|
||||
was_capitalized = False
|
||||
if etudid in self.validations.ue_capitalisees.index:
|
||||
ue_cap = self._get_etud_ue_cap(etudid, ue)
|
||||
if ue_cap and not np.isnan(ue_cap["moy_ue"]):
|
||||
if (
|
||||
ue_cap
|
||||
and (ue_cap["moy_ue"] is not None)
|
||||
and not np.isnan(ue_cap["moy_ue"])
|
||||
):
|
||||
was_capitalized = True
|
||||
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
|
||||
moy_ue = ue_cap["moy_ue"]
|
||||
@ -397,7 +403,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
|
||||
if self.is_apc:
|
||||
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
|
||||
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
|
||||
ue_capitalized = db.session.get(UniteEns, ue_cap["ue_id"])
|
||||
coef_ue = ue_capitalized.ects
|
||||
if coef_ue is None:
|
||||
orig_sem = FormSemestre.get_formsemestre(ue_cap["formsemestre_id"])
|
||||
|
@ -9,9 +9,10 @@
|
||||
from functools import cached_property
|
||||
import pandas as pd
|
||||
|
||||
from flask import flash, g, Markup, url_for
|
||||
from flask import flash, g, url_for
|
||||
from markupsafe import Markup
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp import moy_sem
|
||||
from app.comp.aux_stats import StatsMoyenne
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
@ -283,12 +284,12 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
]
|
||||
return etudids
|
||||
|
||||
def etud_has_decision(self, etudid):
|
||||
def etud_has_decision(self, etudid) -> bool:
|
||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||
prend aussi en compte les autorisations de passage.
|
||||
Sous-classée en BUT pour les RCUEs et années.
|
||||
"""
|
||||
return (
|
||||
return bool(
|
||||
self.get_etud_decisions_ue(etudid)
|
||||
or self.get_etud_decision_sem(etudid)
|
||||
or ScolarAutorisationInscription.query.filter_by(
|
||||
@ -393,7 +394,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
de ce module.
|
||||
Évaluation "complete" ssi toutes notes saisies ou en attente.
|
||||
"""
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
modimpl_results = self.modimpls_results.get(moduleimpl_id)
|
||||
if not modimpl_results:
|
||||
return [] # safeguard
|
||||
|
@ -55,6 +55,9 @@ from wtforms.validators import (
|
||||
)
|
||||
from wtforms.widgets import ListWidget, CheckboxInput
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.entreprises import SIRET_PROVISOIRE_START
|
||||
from app.entreprises.models import (
|
||||
Entreprise,
|
||||
EntrepriseCorrespondant,
|
||||
@ -62,9 +65,6 @@ from app.entreprises.models import (
|
||||
EntrepriseSite,
|
||||
EntrepriseTaxeApprentissage,
|
||||
)
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.entreprises import SIRET_PROVISOIRE_START
|
||||
from app.models import Identite, Departement
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -122,13 +122,13 @@ class EntrepriseCreationForm(FlaskForm):
|
||||
origine = _build_string_field("Origine du correspondant", required=False)
|
||||
notes = _build_string_field("Notes sur le correspondant", required=False)
|
||||
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
if EntreprisePreferences.get_check_siret() and self.siret.data != "":
|
||||
siret_data = self.siret.data.strip().replace(" ", "")
|
||||
@ -248,13 +248,13 @@ class SiteCreationForm(FlaskForm):
|
||||
codepostal = _build_string_field("Code postal (*)")
|
||||
ville = _build_string_field("Ville (*)")
|
||||
pays = _build_string_field("Pays", required=False)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
site = EntrepriseSite.query.filter_by(
|
||||
entreprise_id=self.hidden_entreprise_id.data, nom=self.nom.data
|
||||
@ -278,10 +278,10 @@ class SiteModificationForm(FlaskForm):
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
site = EntrepriseSite.query.filter(
|
||||
EntrepriseSite.entreprise_id == self.hidden_entreprise_id.data,
|
||||
@ -326,7 +326,7 @@ class OffreCreationForm(FlaskForm):
|
||||
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
|
||||
],
|
||||
)
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -344,10 +344,10 @@ class OffreCreationForm(FlaskForm):
|
||||
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
if len(self.depts.data) < 1:
|
||||
self.depts.errors.append("Choisir au moins un département")
|
||||
@ -392,10 +392,10 @@ class OffreModificationForm(FlaskForm):
|
||||
(dept.id, dept.acronym) for dept in Departement.query.all()
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
if len(self.depts.data) < 1:
|
||||
self.depts.errors.append("Choisir au moins un département")
|
||||
@ -442,10 +442,10 @@ class CorrespondantCreationForm(FlaskForm):
|
||||
"Notes", required=False, render_kw={"class": "form-control"}
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
if not self.telephone.data and not self.mail.data:
|
||||
msg = "Saisir un moyen de contact (mail ou téléphone)"
|
||||
@ -458,13 +458,13 @@ class CorrespondantCreationForm(FlaskForm):
|
||||
class CorrespondantsCreationForm(FlaskForm):
|
||||
hidden_site_id = HiddenField()
|
||||
correspondants = FieldList(FormField(CorrespondantCreationForm), min_entries=1)
|
||||
submit = SubmitField("Envoyer")
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler")
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
correspondant_list = []
|
||||
for entry in self.correspondants.entries:
|
||||
@ -531,10 +531,10 @@ class CorrespondantModificationForm(FlaskForm):
|
||||
.all()
|
||||
]
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
correspondant = EntrepriseCorrespondant.query.filter(
|
||||
EntrepriseCorrespondant.id != self.hidden_correspondant_id.data,
|
||||
@ -566,7 +566,7 @@ class ContactCreationForm(FlaskForm):
|
||||
render_kw={"placeholder": "Tapez le nom de l'utilisateur"},
|
||||
)
|
||||
notes = TextAreaField("Notes (*)", validators=[DataRequired(message=CHAMP_REQUIS)])
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate_utilisateur(self, utilisateur):
|
||||
@ -613,8 +613,9 @@ class ContactModificationForm(FlaskForm):
|
||||
class StageApprentissageCreationForm(FlaskForm):
|
||||
etudiant = _build_string_field(
|
||||
"Étudiant (*)",
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant", "autocomplete": "off"},
|
||||
)
|
||||
etudid = HiddenField()
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
@ -627,12 +628,12 @@ class StageApprentissageCreationForm(FlaskForm):
|
||||
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
if not super().validate(extra_validators):
|
||||
validate = False
|
||||
|
||||
if (
|
||||
@ -646,64 +647,27 @@ class StageApprentissageCreationForm(FlaskForm):
|
||||
|
||||
return validate
|
||||
|
||||
def validate_etudiant(self, etudiant):
|
||||
etudiant_data = etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
|
||||
)
|
||||
def validate_etudid(self, field):
|
||||
"L'etudid doit avoit été placé par le JS"
|
||||
etudid = int(field.data) if field.data else None
|
||||
etudiant = db.session.get(Identite, etudid) if etudid is not None else None
|
||||
if etudiant is None:
|
||||
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
|
||||
raise ValidationError("Étudiant introuvable (sélectionnez dans la liste)")
|
||||
|
||||
|
||||
class StageApprentissageModificationForm(FlaskForm):
|
||||
etudiant = _build_string_field(
|
||||
"Étudiant (*)",
|
||||
render_kw={"placeholder": "Tapez le nom de l'étudiant"},
|
||||
)
|
||||
type_offre = SelectField(
|
||||
"Type de l'offre (*)",
|
||||
choices=[("Stage"), ("Alternance")],
|
||||
validators=[DataRequired(message=CHAMP_REQUIS)],
|
||||
)
|
||||
date_debut = DateField(
|
||||
"Date début (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
date_fin = DateField(
|
||||
"Date fin (*)", validators=[DataRequired(message=CHAMP_REQUIS)]
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Modifier", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
class FrenchFloatField(StringField):
|
||||
"A field allowing to enter . or ,"
|
||||
|
||||
def validate(self):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
|
||||
if (
|
||||
self.date_debut.data
|
||||
and self.date_fin.data
|
||||
and self.date_debut.data > self.date_fin.data
|
||||
):
|
||||
self.date_debut.errors.append("Les dates sont incompatibles")
|
||||
self.date_fin.errors.append("Les dates sont incompatibles")
|
||||
validate = False
|
||||
|
||||
return validate
|
||||
|
||||
def validate_etudiant(self, etudiant):
|
||||
etudiant_data = etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
|
||||
)
|
||||
if etudiant is None:
|
||||
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
|
||||
def process_formdata(self, valuelist):
|
||||
"catch incoming data"
|
||||
if not valuelist:
|
||||
return
|
||||
try:
|
||||
value = valuelist[0].replace(",", ".")
|
||||
self.data = float(value)
|
||||
except ValueError as exc:
|
||||
self.data = None
|
||||
raise ValueError(self.gettext("Not a valid decimal value.")) from exc
|
||||
|
||||
|
||||
class TaxeApprentissageForm(FlaskForm):
|
||||
@ -720,25 +684,26 @@ class TaxeApprentissageForm(FlaskForm):
|
||||
],
|
||||
default=int(datetime.now().strftime("%Y")),
|
||||
)
|
||||
montant = IntegerField(
|
||||
montant = FrenchFloatField(
|
||||
"Montant (*)",
|
||||
validators=[
|
||||
DataRequired(message=CHAMP_REQUIS),
|
||||
NumberRange(
|
||||
min=1,
|
||||
message="Le montant doit être supérieur à 0",
|
||||
),
|
||||
# NumberRange(
|
||||
# min=0.1,
|
||||
# max=1e8,
|
||||
# message="Le montant doit être supérieur à 0",
|
||||
# ),
|
||||
],
|
||||
default=1,
|
||||
)
|
||||
notes = TextAreaField("Notes")
|
||||
submit = SubmitField("Envoyer", render_kw=SUBMIT_MARGE)
|
||||
submit = SubmitField("Enregistrer", render_kw=SUBMIT_MARGE)
|
||||
cancel = SubmitField("Annuler", render_kw=SUBMIT_MARGE)
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
taxe = EntrepriseTaxeApprentissage.query.filter_by(
|
||||
entreprise_id=self.hidden_entreprise_id.data, annee=self.annee.data
|
||||
@ -788,12 +753,12 @@ class EnvoiOffreForm(FlaskForm):
|
||||
submit = SubmitField("Envoyer")
|
||||
cancel = SubmitField("Annuler")
|
||||
|
||||
def validate(self):
|
||||
def validate(self, extra_validators=None):
|
||||
validate = True
|
||||
list_select = True
|
||||
|
||||
if not FlaskForm.validate(self):
|
||||
validate = False
|
||||
if not super().validate(extra_validators):
|
||||
return False
|
||||
|
||||
for entry in self.responsables.entries:
|
||||
if entry.data:
|
||||
|
@ -164,7 +164,10 @@ class EntrepriseStageApprentissage(db.Model):
|
||||
entreprise_id = db.Column(
|
||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||
)
|
||||
etudid = db.Column(db.Integer)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
)
|
||||
type_offre = db.Column(db.Text)
|
||||
date_debut = db.Column(db.Date)
|
||||
date_fin = db.Column(db.Date)
|
||||
@ -180,7 +183,7 @@ class EntrepriseTaxeApprentissage(db.Model):
|
||||
db.Integer, db.ForeignKey("are_entreprises.id", ondelete="cascade")
|
||||
)
|
||||
annee = db.Column(db.Integer)
|
||||
montant = db.Column(db.Integer)
|
||||
montant = db.Column(db.Float)
|
||||
notes = db.Column(db.Text)
|
||||
|
||||
|
||||
|
@ -28,7 +28,6 @@ from app.entreprises.forms import (
|
||||
ContactCreationForm,
|
||||
ContactModificationForm,
|
||||
StageApprentissageCreationForm,
|
||||
StageApprentissageModificationForm,
|
||||
EnvoiOffreForm,
|
||||
AjoutFichierForm,
|
||||
TaxeApprentissageForm,
|
||||
@ -239,7 +238,7 @@ def delete_validation_entreprise(entreprise_id):
|
||||
text=f"Non validation de la fiche entreprise ({entreprise.nom})",
|
||||
)
|
||||
db.session.add(log)
|
||||
flash("L'entreprise a été supprimé de la liste des entreprise à valider.")
|
||||
flash("L'entreprise a été supprimée de la liste des entreprises à valider.")
|
||||
return redirect(url_for("entreprises.validation"))
|
||||
return render_template(
|
||||
"entreprises/form_confirmation.j2",
|
||||
@ -770,7 +769,7 @@ def delete_taxe_apprentissage(entreprise_id, taxe_id):
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
flash("La taxe d'apprentissage a été supprimé de la liste.")
|
||||
flash("La taxe d'apprentissage a été supprimée de la liste.")
|
||||
return redirect(
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=taxe.entreprise_id)
|
||||
)
|
||||
@ -966,7 +965,7 @@ def delete_offre(entreprise_id, offre_id):
|
||||
)
|
||||
db.session.add(log)
|
||||
db.session.commit()
|
||||
flash("L'offre a été supprimé de la fiche entreprise.")
|
||||
flash("L'offre a été supprimée de la fiche entreprise.")
|
||||
return redirect(
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=offre.entreprise_id)
|
||||
)
|
||||
@ -1473,7 +1472,8 @@ def delete_contact(entreprise_id, contact_id):
|
||||
@permission_required(Permission.RelationsEntreprisesChange)
|
||||
def add_stage_apprentissage(entreprise_id):
|
||||
"""
|
||||
Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
|
||||
Permet d'ajouter un étudiant ayant réalisé un stage ou alternance
|
||||
sur la fiche de l'entreprise
|
||||
"""
|
||||
entreprise = Entreprise.query.filter_by(
|
||||
id=entreprise_id, visible=True
|
||||
@ -1484,15 +1484,8 @@ def add_stage_apprentissage(entreprise_id):
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
etudiant_nomcomplet = form.etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm)
|
||||
.params(nom_prenom=etudiant_nomcomplet)
|
||||
.first()
|
||||
)
|
||||
etudid = form.etudid.data
|
||||
etudiant = Identite.query.get_or_404(etudid)
|
||||
formation = etudiant.inscription_courante_date(
|
||||
form.date_debut.data, form.date_fin.data
|
||||
)
|
||||
@ -1538,7 +1531,7 @@ def add_stage_apprentissage(entreprise_id):
|
||||
@permission_required(Permission.RelationsEntreprisesChange)
|
||||
def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
"""
|
||||
Permet de modifier un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
|
||||
Permet de modifier un étudiant ayant réalisé un stage ou alternance sur la fiche de l'entreprise
|
||||
"""
|
||||
stage_apprentissage = EntrepriseStageApprentissage.query.filter_by(
|
||||
id=stage_apprentissage_id, entreprise_id=entreprise_id
|
||||
@ -1548,21 +1541,14 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
etudiant = Identite.query.filter_by(id=stage_apprentissage.etudid).first_or_404(
|
||||
description=f"etudiant {stage_apprentissage.etudid} inconnue"
|
||||
)
|
||||
form = StageApprentissageModificationForm()
|
||||
form = StageApprentissageCreationForm()
|
||||
if request.method == "POST" and form.cancel.data:
|
||||
return redirect(
|
||||
url_for("entreprises.fiche_entreprise", entreprise_id=entreprise_id)
|
||||
)
|
||||
if form.validate_on_submit():
|
||||
etudiant_nomcomplet = form.etudiant.data.upper().strip()
|
||||
stm = text(
|
||||
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
|
||||
)
|
||||
etudiant = (
|
||||
Identite.query.from_statement(stm)
|
||||
.params(nom_prenom=etudiant_nomcomplet)
|
||||
.first()
|
||||
)
|
||||
etudid = form.etudid.data
|
||||
etudiant = Identite.query.get_or_404(etudid)
|
||||
formation = etudiant.inscription_courante_date(
|
||||
form.date_debut.data, form.date_fin.data
|
||||
)
|
||||
@ -1577,6 +1563,7 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
formation.formsemestre.formsemestre_id if formation else None,
|
||||
)
|
||||
stage_apprentissage.notes = form.notes.data.strip()
|
||||
db.session.add(stage_apprentissage)
|
||||
log = EntrepriseHistorique(
|
||||
authenticated_user=current_user.user_name,
|
||||
entreprise_id=stage_apprentissage.entreprise_id,
|
||||
@ -1593,7 +1580,9 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
)
|
||||
)
|
||||
elif request.method == "GET":
|
||||
form.etudiant.data = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
|
||||
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
||||
sco_etud.format_prenom(etudiant.prenom)}"""
|
||||
form.etudid.data = etudiant.id
|
||||
form.type_offre.data = stage_apprentissage.type_offre
|
||||
form.date_debut.data = stage_apprentissage.date_debut
|
||||
form.date_fin.data = stage_apprentissage.date_fin
|
||||
|
@ -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,
|
||||
|
@ -9,6 +9,7 @@ from datetime import datetime
|
||||
import functools
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm import class_mapper
|
||||
import sqlalchemy
|
||||
@ -93,6 +94,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
backref="referentiel_competence",
|
||||
order_by="Formation.acronyme, Formation.version",
|
||||
)
|
||||
validations_annee = db.relationship(
|
||||
"ApcValidationAnnee",
|
||||
backref="referentiel_competence",
|
||||
lazy="dynamic",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
|
||||
@ -358,6 +364,9 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
||||
self.annee!r} {self.competence!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
return f"""{self.competence.titre} niveau {self.ordre}"""
|
||||
|
||||
def to_dict(self, with_app_critiques=True):
|
||||
"as a dict, recursif (ou non) sur les AC"
|
||||
return {
|
||||
@ -388,7 +397,9 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
return (
|
||||
ApcParcours.query.join(ApcAnneeParcours)
|
||||
.filter_by(ordre=annee)
|
||||
.join(ApcParcoursNiveauCompetence, ApcCompetence, ApcNiveau)
|
||||
.join(ApcParcoursNiveauCompetence)
|
||||
.join(ApcCompetence)
|
||||
.join(ApcNiveau)
|
||||
.filter_by(id=self.id)
|
||||
.order_by(ApcParcours.numero, ApcParcours.code)
|
||||
.all()
|
||||
@ -412,6 +423,20 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
(dans ce cas, spécifier referentiel_competence)
|
||||
Si competence est indiquée, filtre les niveaux de cette compétence.
|
||||
"""
|
||||
key = (
|
||||
parcour.id if parcour else None,
|
||||
annee,
|
||||
referentiel_competence.id if referentiel_competence else None,
|
||||
competence.id if competence else None,
|
||||
)
|
||||
_cache = getattr(g, "_niveaux_annee_de_parcours_cache", None)
|
||||
if _cache:
|
||||
result = g._niveaux_annee_de_parcours_cache.get(key, False)
|
||||
if result is not False:
|
||||
return result
|
||||
else:
|
||||
g._niveaux_annee_de_parcours_cache = {}
|
||||
_cache = g._niveaux_annee_de_parcours_cache
|
||||
if annee not in {1, 2, 3}:
|
||||
raise ValueError("annee invalide pour un parcours BUT")
|
||||
referentiel_competence = (
|
||||
@ -428,10 +453,13 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
)
|
||||
if competence is not None:
|
||||
query = query.filter(ApcCompetence.id == competence.id)
|
||||
return query.all()
|
||||
result = query.all()
|
||||
_cache[key] = result
|
||||
return result
|
||||
|
||||
annee_parcour: ApcAnneeParcours = parcour.annees.filter_by(ordre=annee).first()
|
||||
if not annee_parcour:
|
||||
_cache[key] = []
|
||||
return []
|
||||
|
||||
if competence is None:
|
||||
@ -443,9 +471,17 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
for pn in parcour_niveaux
|
||||
]
|
||||
else:
|
||||
niveaux: list[ApcNiveau] = competence.niveaux.filter_by(
|
||||
annee=f"BUT{int(annee)}"
|
||||
).all()
|
||||
niveaux: list[ApcNiveau] = (
|
||||
ApcNiveau.query.filter_by(annee=f"BUT{int(annee)}")
|
||||
.join(ApcCompetence)
|
||||
.filter_by(id=competence.id)
|
||||
.join(ApcParcoursNiveauCompetence)
|
||||
.filter(ApcParcoursNiveauCompetence.niveau == ApcNiveau.ordre)
|
||||
.join(ApcAnneeParcours)
|
||||
.filter_by(parcours_id=parcour.id)
|
||||
.all()
|
||||
)
|
||||
_cache[key] = niveaux
|
||||
return niveaux
|
||||
|
||||
|
||||
@ -587,7 +623,8 @@ class ApcParcours(db.Model, XMLModel):
|
||||
def query_competences(self) -> Query:
|
||||
"Les compétences associées à ce parcours"
|
||||
return (
|
||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence)
|
||||
.join(ApcAnneeParcours)
|
||||
.filter_by(parcours_id=self.id)
|
||||
.order_by(ApcCompetence.numero)
|
||||
)
|
||||
@ -596,7 +633,8 @@ class ApcParcours(db.Model, XMLModel):
|
||||
"La compétence de titre donné dans ce parcours, ou None"
|
||||
return (
|
||||
ApcCompetence.query.filter_by(titre=titre)
|
||||
.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
.join(ApcParcoursNiveauCompetence)
|
||||
.join(ApcAnneeParcours)
|
||||
.filter_by(parcours_id=self.id)
|
||||
.order_by(ApcCompetence.numero)
|
||||
.first()
|
||||
|
@ -2,9 +2,6 @@
|
||||
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
from typing import Union
|
||||
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN
|
||||
@ -13,8 +10,6 @@ from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
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):
|
||||
@ -22,7 +17,7 @@ class ApcValidationRCUE(db.Model):
|
||||
|
||||
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"
|
||||
@ -41,7 +36,7 @@ class ApcValidationRCUE(db.Model):
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||
)
|
||||
"formsemestre pair du RCUE"
|
||||
"formsemestre origine du RCUE (celui d'où a été émis la validation)"
|
||||
# Les deux UE associées à ce niveau:
|
||||
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||
@ -66,7 +61,7 @@ class ApcValidationRCUE(db.Model):
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
||||
|
||||
def to_html(self) -> str:
|
||||
def html(self) -> str:
|
||||
"description en HTML"
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||
<b>{self.code}</b>
|
||||
@ -87,6 +82,10 @@ class ApcValidationRCUE(db.Model):
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
d["etud"] = self.etud.to_dict_short()
|
||||
d["ue1"] = self.ue1.to_dict()
|
||||
d["ue2"] = self.ue2.to_dict()
|
||||
|
||||
return d
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
@ -109,204 +108,14 @@ 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
|
||||
# def find_rcues(
|
||||
# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||
# ) -> list[RegroupementCoherentUE]:
|
||||
# """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||
# ce semestre pour cette UE.
|
||||
|
||||
# Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||
# En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||
|
||||
# Résultat: la liste peut être vide.
|
||||
# """
|
||||
# if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||
# return []
|
||||
|
||||
# if ue.semestre_idx % 2: # S1, S3, S5
|
||||
# other_semestre_idx = ue.semestre_idx + 1
|
||||
# else:
|
||||
# other_semestre_idx = ue.semestre_idx - 1
|
||||
|
||||
# cursor = db.session.execute(
|
||||
# text(
|
||||
# """SELECT
|
||||
# ue.id, formsemestre.id
|
||||
# FROM
|
||||
# notes_ue ue,
|
||||
# notes_formsemestre_inscription inscr,
|
||||
# notes_formsemestre formsemestre
|
||||
|
||||
# WHERE
|
||||
# inscr.etudid = :etudid
|
||||
# AND inscr.formsemestre_id = formsemestre.id
|
||||
|
||||
# AND formsemestre.semestre_id = :other_semestre_idx
|
||||
# AND ue.formation_id = formsemestre.formation_id
|
||||
# AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||
# AND ue.semestre_idx = :other_semestre_idx
|
||||
# """
|
||||
# ),
|
||||
# {
|
||||
# "etudid": etud.id,
|
||||
# "other_semestre_idx": other_semestre_idx,
|
||||
# "ue_niveau_competence_id": ue.niveau_competence_id,
|
||||
# },
|
||||
# )
|
||||
# rcues = []
|
||||
# for ue_id, formsemestre_id in cursor:
|
||||
# other_ue = UniteEns.query.get(ue_id)
|
||||
# other_formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# rcues.append(
|
||||
# RegroupementCoherentUE(
|
||||
# etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
||||
# )
|
||||
# )
|
||||
# # safety check: 1 seul niveau de comp. concerné:
|
||||
# assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||
# return rcues
|
||||
|
||||
|
||||
class ApcValidationAnnee(db.Model):
|
||||
"""Validation des années du BUT"""
|
||||
|
||||
__tablename__ = "apc_validation_annee"
|
||||
# Assure unicité de la décision:
|
||||
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire", "ordre"),)
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint("etudid", "ordre", "referentiel_competence_id"),
|
||||
)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
@ -319,8 +128,11 @@ class ApcValidationAnnee(db.Model):
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
|
||||
)
|
||||
"le semestre IMPAIR (le 1er) de l'année"
|
||||
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
|
||||
"le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
|
||||
referentiel_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
)
|
||||
annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||
|
||||
@ -338,25 +150,50 @@ class ApcValidationAnnee(db.Model):
|
||||
"dict pour bulletins"
|
||||
return {
|
||||
"annee_scolaire": self.annee_scolaire,
|
||||
"date": self.date.isoformat(),
|
||||
"date": self.date.isoformat() if self.date else "",
|
||||
"code": self.code,
|
||||
"ordre": self.ordre,
|
||||
}
|
||||
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
date_str = (
|
||||
f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
|
||||
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
|
||||
{link}
|
||||
: <b>{self.code}</b>
|
||||
{date_str}
|
||||
"""
|
||||
|
||||
|
||||
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
"""
|
||||
Un dict avec les décisions de jury BUT enregistrées:
|
||||
- decision_rcue : list[dict]
|
||||
- decision_annee : dict
|
||||
- decision_annee : dict (décision issue de ce semestre seulement (à confirmer ?))
|
||||
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
|
||||
"""
|
||||
decisions = {}
|
||||
# --- RCUEs: seulement sur semestres pairs XXX à améliorer
|
||||
if formsemestre.semestre_id % 2 == 0:
|
||||
# validations émises depuis ce formsemestre:
|
||||
validations_rcues = ApcValidationRCUE.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||
validations_rcues = (
|
||||
ApcValidationRCUE.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||
)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.order_by(UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
||||
titres_rcues = []
|
||||
@ -378,16 +215,11 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
decisions["descr_decisions_rcue"] = ""
|
||||
decisions["descr_decisions_niveaux"] = ""
|
||||
# --- Année: prend la validation pour l'année scolaire de ce semestre
|
||||
validation = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
)
|
||||
.join(ApcValidationAnnee.formsemestre)
|
||||
.join(FormSemestre.formation)
|
||||
.filter(Formation.formation_code == formsemestre.formation.formation_code)
|
||||
.first()
|
||||
)
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
if validation:
|
||||
decisions["decision_annee"] = validation.to_dict_bul()
|
||||
else:
|
||||
|
@ -15,6 +15,7 @@ from app.scodoc.codes_cursus import (
|
||||
ADJ,
|
||||
ADJR,
|
||||
ADM,
|
||||
ADSUP,
|
||||
AJ,
|
||||
ATB,
|
||||
ATJ,
|
||||
@ -37,6 +38,7 @@ CODES_SCODOC_TO_APO = {
|
||||
ADJ: "ADM",
|
||||
ADJR: "ADM",
|
||||
ADM: "ADM",
|
||||
ADSUP: "ADM",
|
||||
AJ: "AJ",
|
||||
ATB: "AJAC",
|
||||
ATJ: "AJAC",
|
||||
|
@ -43,8 +43,8 @@ class Identite(db.Model):
|
||||
"optionnel (si present, affiché à la place du nom)"
|
||||
civilite = db.Column(db.String(1), nullable=False)
|
||||
|
||||
# données d'état-civil. Si présent remplace les données d'usage dans les documents officiels (bulletins, PV)
|
||||
# cf nomprenom_etat_civil()
|
||||
# données d'état-civil. Si présent remplace les données d'usage dans les documents
|
||||
# officiels (bulletins, PV): voir nomprenom_etat_civil()
|
||||
civilite_etat_civil = db.Column(db.String(1), nullable=False, server_default="X")
|
||||
prenom_etat_civil = db.Column(db.Text(), nullable=False, server_default="")
|
||||
|
||||
@ -78,6 +78,12 @@ class Identite(db.Model):
|
||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||
)
|
||||
|
||||
def html_link_fiche(self) -> str:
|
||||
"lien vers la fiche"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id)
|
||||
}">{self.nomprenom}</a>"""
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
||||
"""Étudiant à partir de l'etudid ou du code_nip, soit
|
||||
@ -220,7 +226,7 @@ class Identite(db.Model):
|
||||
}
|
||||
args_dict = {}
|
||||
for key, value in args.items():
|
||||
if hasattr(cls, key):
|
||||
if hasattr(cls, key) and not isinstance(getattr(cls, key, None), property):
|
||||
# compat scodoc7 (mauvaise idée de l'époque)
|
||||
if key in fs_empty_stored_as_nulls and value == "":
|
||||
value = None
|
||||
|
@ -145,6 +145,18 @@ class Evaluation(db.Model):
|
||||
db.session.add(copy)
|
||||
return copy
|
||||
|
||||
def is_matin(self) -> bool:
|
||||
"Evaluation ayant lieu le matin (faux si pas de date)"
|
||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
||||
# 8:00 au cas ou pas d'heure (note externe?)
|
||||
return bool(self.jour) and heure_debut_dt < datetime.time(12, 00)
|
||||
|
||||
def is_apresmidi(self) -> bool:
|
||||
"Evaluation ayant lieu l'après midi (faux si pas de date)"
|
||||
heure_debut_dt = self.heure_debut or datetime.time(8, 00)
|
||||
# 8:00 au cas ou pas d'heure (note externe?)
|
||||
return bool(self.jour) and heure_debut_dt >= datetime.time(12, 00)
|
||||
|
||||
def set_default_poids(self) -> bool:
|
||||
"""Initialize les poids bvers les UE à leurs valeurs par défaut
|
||||
C'est à dire à 1 si le coef. module/UE est non nul, 0 sinon.
|
||||
@ -178,8 +190,10 @@ class Evaluation(db.Model):
|
||||
"""
|
||||
L = []
|
||||
for ue_id, poids in ue_poids_dict.items():
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids))
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||
L.append(ue_poids)
|
||||
db.session.add(ue_poids)
|
||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
@ -326,7 +340,7 @@ def check_evaluation_args(args):
|
||||
jour = args.get("jour", None)
|
||||
args["jour"] = jour
|
||||
if jour:
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
formsemestre = modimpl.formsemestre
|
||||
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
|
||||
jour = datetime.date(y, m, d)
|
||||
|
@ -54,14 +54,17 @@ class ScolarNews(db.Model):
|
||||
NEWS_APO = "APO" # changements de codes APO
|
||||
NEWS_FORM = "FORM" # modification formation (object=formation_id)
|
||||
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
|
||||
NEWS_JURY = "JURY" # saisie jury
|
||||
NEWS_MISC = "MISC" # unused
|
||||
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
|
||||
NEWS_SEM = "SEM" # creation semestre (object=None)
|
||||
|
||||
NEWS_MAP = {
|
||||
NEWS_ABS: "saisie absence",
|
||||
NEWS_APO: "modif. code Apogée",
|
||||
NEWS_FORM: "modification formation",
|
||||
NEWS_INSCR: "inscription d'étudiants",
|
||||
NEWS_JURY: "saisie jury",
|
||||
NEWS_MISC: "opération", # unused
|
||||
NEWS_NOTE: "saisie note",
|
||||
NEWS_SEM: "création semestre",
|
||||
@ -130,10 +133,10 @@ class ScolarNews(db.Model):
|
||||
return query.order_by(cls.date.desc()).limit(n).all()
|
||||
|
||||
@classmethod
|
||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=0):
|
||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=600):
|
||||
"""Enregistre une nouvelle
|
||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||
à moins de max_frequency secondes d'intervalle.
|
||||
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
||||
Deux nouvelles sont considérées comme "identiques" si elles ont
|
||||
même (obj, typ, user).
|
||||
La nouvelle enregistrée est aussi envoyée par mail.
|
||||
@ -153,7 +156,10 @@ class ScolarNews(db.Model):
|
||||
if last_news:
|
||||
now = datetime.datetime.now(tz=last_news.date.tzinfo)
|
||||
if (now - last_news.date) < datetime.timedelta(seconds=max_frequency):
|
||||
# on n'enregistre pas
|
||||
# pas de nouvel event, mais met à jour l'heure
|
||||
last_news.date = datetime.datetime.now()
|
||||
db.session.add(last_news)
|
||||
db.session.commit()
|
||||
return
|
||||
|
||||
news = ScolarNews(
|
||||
@ -181,14 +187,14 @@ class ScolarNews(db.Model):
|
||||
elif self.type == self.NEWS_NOTE:
|
||||
moduleimpl_id = self.object
|
||||
if moduleimpl_id:
|
||||
modimpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
if modimpl is None:
|
||||
return None # module does not exists anymore
|
||||
formsemestre_id = modimpl.formsemestre_id
|
||||
|
||||
if not formsemestre_id:
|
||||
return None
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
return formsemestre
|
||||
|
||||
def notify_by_mail(self):
|
||||
@ -259,11 +265,8 @@ class ScolarNews(db.Model):
|
||||
|
||||
# Informations générales
|
||||
H.append(
|
||||
f"""<div>
|
||||
Pour être informé des évolutions de ScoDoc,
|
||||
vous pouvez vous
|
||||
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
||||
abonner à la liste de diffusion</a>.
|
||||
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
|
||||
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
@ -60,7 +60,7 @@ class Formation(db.Model):
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
|
||||
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
|
||||
|
||||
def to_html(self) -> str:
|
||||
def html(self) -> str:
|
||||
"titre complet pour affichage"
|
||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||
|
||||
|
@ -16,7 +16,7 @@ from operator import attrgetter
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from flask import flash, g
|
||||
from flask import flash, g, url_for
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -163,6 +163,14 @@ class FormSemestre(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
||||
|
||||
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,)
|
||||
}" title="{title or ''}">{label or self.titre_mois()}</a>
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
|
||||
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
|
||||
@ -297,6 +305,17 @@ class FormSemestre(db.Model):
|
||||
- et sont associées à l'un des parcours de ce formsemestre
|
||||
(ou à aucun, donc tronc commun).
|
||||
"""
|
||||
# per-request caching
|
||||
key = (self.id, with_sport)
|
||||
_cache = getattr(g, "_formsemestre_get_ues_cache", None)
|
||||
if _cache:
|
||||
result = _cache.get(key, False)
|
||||
if result is not False:
|
||||
return result
|
||||
else:
|
||||
g._formsemestre_get_ues_cache = {}
|
||||
_cache = g._formsemestre_get_ues_cache
|
||||
|
||||
formation: Formation = self.formation
|
||||
if formation.is_apc():
|
||||
# UEs de tronc commun (sans parcours indiqué)
|
||||
@ -316,8 +335,7 @@ class FormSemestre(db.Model):
|
||||
).filter(UniteEns.semestre_idx == self.semestre_id)
|
||||
}
|
||||
)
|
||||
ues = sem_ues.values()
|
||||
return sorted(ues, key=attrgetter("numero"))
|
||||
ues = sorted(sem_ues.values(), key=attrgetter("numero", "acronyme"))
|
||||
else:
|
||||
sem_ues = db.session.query(UniteEns).filter(
|
||||
ModuleImpl.formsemestre_id == self.id,
|
||||
@ -326,7 +344,9 @@ class FormSemestre(db.Model):
|
||||
)
|
||||
if not with_sport:
|
||||
sem_ues = sem_ues.filter(UniteEns.type != codes_cursus.UE_SPORT)
|
||||
return sem_ues.order_by(UniteEns.numero).all()
|
||||
ues = sem_ues.order_by(UniteEns.numero).all()
|
||||
_cache[key] = ues
|
||||
return ues
|
||||
|
||||
@cached_property
|
||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||
@ -374,7 +394,7 @@ class FormSemestre(db.Model):
|
||||
),
|
||||
{"formsemestre_id": self.id, "parcours_id": parcours.id},
|
||||
)
|
||||
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
|
||||
return [db.session.get(ModuleImpl, modimpl_id) for modimpl_id in cursor]
|
||||
|
||||
def can_be_edited_by(self, user):
|
||||
"""Vrai si user peut modifier ce semestre (est chef ou l'un des responsables)"""
|
||||
@ -518,6 +538,11 @@ class FormSemestre(db.Model):
|
||||
return ""
|
||||
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
||||
|
||||
def add_etape(self, etape_apo: str):
|
||||
"Ajoute une étape"
|
||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
|
||||
db.session.add(etape)
|
||||
|
||||
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
||||
"""Calcule la liste des regroupements cohérents d'UE impliquant ce
|
||||
formsemestre.
|
||||
@ -560,6 +585,17 @@ class FormSemestre(db.Model):
|
||||
user
|
||||
)
|
||||
|
||||
def can_change_groups(self, user: User = None) -> bool:
|
||||
"""Vrai si l'utilisateur (par def. current) peut changer les groupes dans
|
||||
ce semestre: vérifie permission et verrouillage.
|
||||
"""
|
||||
if not self.etat:
|
||||
return False # semestre verrouillé
|
||||
user = user or current_user
|
||||
if user.has_permission(Permission.ScoEtudChangeGroups):
|
||||
return True # typiquement admin, chef dept
|
||||
return self.est_responsable(user)
|
||||
|
||||
def can_edit_jury(self, user: User = None):
|
||||
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||
dans ce semestre: vérifie permission et verrouillage.
|
||||
@ -782,6 +818,8 @@ class FormSemestre(db.Model):
|
||||
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
|
||||
et leur nom est le code du parcours (eg "Cyber").
|
||||
"""
|
||||
if self.formation.referentiel_competence_id is None:
|
||||
return # safety net
|
||||
partition = Partition.query.filter_by(
|
||||
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
|
||||
).first()
|
||||
@ -805,7 +843,10 @@ class FormSemestre(db.Model):
|
||||
query = (
|
||||
ApcParcours.query.filter_by(code=group.group_name)
|
||||
.join(ApcReferentielCompetences)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
.filter_by(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
id=self.formation.referentiel_competence_id,
|
||||
)
|
||||
)
|
||||
if query.count() != 1:
|
||||
log(
|
||||
@ -854,15 +895,12 @@ class FormSemestre(db.Model):
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
vals_annee = (
|
||||
vals_annee = ( # issues de cette année scolaire seulement
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
etudid=etudid,
|
||||
annee_scolaire=self.annee_scolaire(),
|
||||
)
|
||||
.join(ApcValidationAnnee.formsemestre)
|
||||
.join(FormSemestre.formation)
|
||||
.filter(Formation.formation_code == self.formation.formation_code)
|
||||
.all()
|
||||
referentiel_competence_id=self.formation.referentiel_competence_id,
|
||||
).all()
|
||||
)
|
||||
H = []
|
||||
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):
|
||||
|
@ -8,11 +8,13 @@
|
||||
"""ScoDoc models: Groups & partitions
|
||||
"""
|
||||
from operator import attrgetter
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import GROUPNAME_STR_LEN
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
|
||||
|
||||
class Partition(db.Model):
|
||||
@ -117,6 +119,81 @@ class Partition(db.Model):
|
||||
.first()
|
||||
)
|
||||
|
||||
def set_etud_group(self, etud: "Identite", group: "GroupDescr") -> bool:
|
||||
"""Affect etudid to group_id in given partition.
|
||||
Raises IntegrityError si conflit,
|
||||
or ValueError si ce group_id n'est pas dans cette partition
|
||||
ou que l'étudiant n'est pas inscrit au semestre.
|
||||
Return True si changement, False s'il était déjà dans ce groupe.
|
||||
"""
|
||||
if not group.id in (g.id for g in self.groups):
|
||||
raise ScoValueError(
|
||||
f"""Le groupe {group.id} n'est pas dans la partition {
|
||||
self.partition_name or "tous"}"""
|
||||
)
|
||||
if etud.id not in (e.id for e in self.formsemestre.etuds):
|
||||
raise ScoValueError(
|
||||
f"""étudiant {etud.nomprenom} non inscrit au formsemestre du groupe {
|
||||
group.group_name}"""
|
||||
)
|
||||
try:
|
||||
existing_row = (
|
||||
db.session.query(group_membership)
|
||||
.filter_by(etudid=etud.id)
|
||||
.join(GroupDescr)
|
||||
.filter_by(partition_id=self.id)
|
||||
.first()
|
||||
)
|
||||
if existing_row:
|
||||
existing_group_id = existing_row[1]
|
||||
if group.id == existing_group_id:
|
||||
return False
|
||||
# Fait le changement avec l'ORM sinon risque élevé de blocage
|
||||
existing_group = db.session.get(GroupDescr, existing_group_id)
|
||||
db.session.commit()
|
||||
group.etuds.append(etud)
|
||||
existing_group.etuds.remove(etud)
|
||||
db.session.add(etud)
|
||||
db.session.add(existing_group)
|
||||
db.session.add(group)
|
||||
else:
|
||||
new_row = group_membership.insert().values(
|
||||
etudid=etud.id, group_id=group.id
|
||||
)
|
||||
db.session.execute(new_row)
|
||||
db.session.commit()
|
||||
except IntegrityError:
|
||||
db.session.rollback()
|
||||
raise
|
||||
return True
|
||||
|
||||
def create_group(self, group_name="", default=False) -> "GroupDescr":
|
||||
"Crée un groupe dans cette partition"
|
||||
if not self.formsemestre.can_change_groups():
|
||||
raise AccessDenied(
|
||||
"""Vous n'avez pas le droit d'effectuer cette opération,
|
||||
ou bien le semestre est verrouillé !"""
|
||||
)
|
||||
if group_name:
|
||||
group_name = group_name.strip()
|
||||
if not group_name and not default:
|
||||
raise ValueError("invalid group name: ()")
|
||||
if not GroupDescr.check_name(self, group_name, default=default):
|
||||
raise ScoValueError(
|
||||
f"Le groupe {group_name} existe déjà dans cette partition"
|
||||
)
|
||||
numeros = [g.numero if g.numero is not None else 0 for g in self.groups]
|
||||
if len(numeros) > 0:
|
||||
new_numero = max(numeros) + 1
|
||||
else:
|
||||
new_numero = 0
|
||||
group = GroupDescr(partition=self, group_name=group_name, numero=new_numero)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"create_group: created group_id={group.id}")
|
||||
#
|
||||
return group
|
||||
|
||||
|
||||
class GroupDescr(db.Model):
|
||||
"""Description d'un groupe d'une partition"""
|
||||
|
@ -55,7 +55,7 @@ class Module(db.Model):
|
||||
secondary=parcours_modules,
|
||||
lazy="subquery",
|
||||
backref=db.backref("modules", lazy=True),
|
||||
order_by="ApcParcours.numero",
|
||||
order_by="ApcParcours.numero, ApcParcours.code",
|
||||
)
|
||||
|
||||
app_critiques = db.relationship(
|
||||
@ -198,7 +198,7 @@ class Module(db.Model):
|
||||
else:
|
||||
# crée nouveau coef:
|
||||
if coef != 0.0:
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
ue_coef = ModuleUECoef(module=self, ue=ue, coef=coef)
|
||||
db.session.add(ue_coef)
|
||||
self.ue_coefs.append(ue_coef)
|
||||
@ -229,19 +229,19 @@ class Module(db.Model):
|
||||
"""delete coef"""
|
||||
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
||||
current_app.logguer.info(
|
||||
f"delete_ue_coef: locked formation, ignoring request"
|
||||
"delete_ue_coef: locked formation, ignoring request"
|
||||
)
|
||||
raise ScoValueError("Formation verrouillée")
|
||||
ue_coef = ModuleUECoef.query.get((self.id, ue.id))
|
||||
ue_coef = db.session.get(ModuleUECoef, (self.id, ue.id))
|
||||
if ue_coef:
|
||||
db.session.delete(ue_coef)
|
||||
self.formation.invalidate_module_coefs()
|
||||
|
||||
def get_ue_coefs_sorted(self):
|
||||
"les coefs d'UE, trié par numéro d'UE"
|
||||
"les coefs d'UE, trié par numéro et acronyme d'UE"
|
||||
# je n'ai pas su mettre un order_by sur le backref sans avoir
|
||||
# à redéfinir les relationships...
|
||||
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
|
||||
return sorted(self.ue_coefs, key=lambda uc: (uc.ue.numero, uc.ue.acronyme))
|
||||
|
||||
def ue_coefs_list(
|
||||
self, include_zeros=True, ues: list["UniteEns"] = None
|
||||
|
@ -56,8 +56,8 @@ class NotesNotes(db.Model):
|
||||
"pour debug"
|
||||
from app.models.evaluations import Evaluation
|
||||
|
||||
return f"""<{self.__class__.__name__} {self.id} v={self.value} {self.date.isoformat()
|
||||
} {Evaluation.query.get(self.evaluation_id) if self.evaluation_id else "X" }>"""
|
||||
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} v={self.value} {self.date.isoformat()
|
||||
} {db.session.get(Evaluation, self.evaluation_id) if self.evaluation_id else "X" }>"""
|
||||
|
||||
|
||||
class NotesNotesLog(db.Model):
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
import pandas as pd
|
||||
|
||||
from app import db, log
|
||||
@ -8,7 +9,6 @@ from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau, ApcParcours
|
||||
from app.models.modules import Module
|
||||
from app.scodoc.sco_exceptions import ScoFormationConflict
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -58,7 +58,10 @@ class UniteEns(db.Model):
|
||||
|
||||
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
|
||||
parcours = db.relationship(
|
||||
ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
|
||||
ApcParcours,
|
||||
secondary="ue_parcours",
|
||||
backref=db.backref("ues", lazy=True),
|
||||
order_by="ApcParcours.numero, ApcParcours.code",
|
||||
)
|
||||
|
||||
# relations
|
||||
@ -104,6 +107,17 @@ class UniteEns(db.Model):
|
||||
If convert_objects, convert all attributes to native types
|
||||
(suitable for json encoding).
|
||||
"""
|
||||
# cache car très utilisé par anciens codes
|
||||
key = (self.id, convert_objects, with_module_ue_coefs)
|
||||
_cache = getattr(g, "_ue_to_dict_cache", None)
|
||||
if _cache:
|
||||
result = g._ue_to_dict_cache.get(key, False)
|
||||
if result is not False:
|
||||
return result
|
||||
else:
|
||||
g._ue_to_dict_cache = {}
|
||||
_cache = g._ue_to_dict_cache
|
||||
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
e.pop("evaluation_ue_poids", None)
|
||||
@ -130,6 +144,7 @@ class UniteEns(db.Model):
|
||||
]
|
||||
else:
|
||||
e.pop("module_ue_coefs", None)
|
||||
_cache[key] = e
|
||||
return e
|
||||
|
||||
def annee(self) -> int:
|
||||
@ -177,12 +192,23 @@ class UniteEns(db.Model):
|
||||
le parcours indiqué.
|
||||
"""
|
||||
if parcour is not None:
|
||||
key = (parcour.id, self.id, only_parcours)
|
||||
ue_ects_cache = getattr(g, "_ue_ects_cache", None)
|
||||
if ue_ects_cache:
|
||||
ects = g._ue_ects_cache.get(key, False)
|
||||
if ects is not False:
|
||||
return ects
|
||||
else:
|
||||
g._ue_ects_cache = {}
|
||||
ue_ects_cache = g._ue_ects_cache
|
||||
ue_parcour = UEParcours.query.filter_by(
|
||||
ue_id=self.id, parcours_id=parcour.id
|
||||
).first()
|
||||
if ue_parcour is not None and ue_parcour.ects is not None:
|
||||
ue_ects_cache[key] = ue_parcour.ects
|
||||
return ue_parcour.ects
|
||||
if only_parcours:
|
||||
ue_ects_cache[key] = None
|
||||
return None
|
||||
return self.ects
|
||||
|
||||
|
@ -8,10 +8,13 @@ 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
|
||||
from app.scodoc.codes_cursus import CODES_UE_VALIDES
|
||||
|
||||
|
||||
class ScolarFormSemestreValidation(db.Model):
|
||||
"""Décisions de jury"""
|
||||
"""Décisions de jury (sur semestre ou UEs)"""
|
||||
|
||||
__tablename__ = "scolar_formsemestre_validation"
|
||||
# Assure unicité de la décision:
|
||||
@ -54,18 +57,30 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
)
|
||||
|
||||
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
||||
etud = db.relationship("Identite", backref="validations")
|
||||
formsemestre = db.relationship(
|
||||
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||
return f"""{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={
|
||||
self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"""
|
||||
|
||||
def __str__(self):
|
||||
if self.ue_id:
|
||||
# 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}: {self.code}"""
|
||||
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {self.event_date.strftime("%d/%m/%Y")}"""
|
||||
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
|
||||
} ({self.ue_id}): {self.code}"""
|
||||
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"
|
||||
@ -73,6 +88,49 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def html(self, detail=False) -> str:
|
||||
"Affichage html"
|
||||
if self.ue_id is not None:
|
||||
moyenne = (
|
||||
f", moyenne {scu.fmt_note(self.moy_ue)}/20 "
|
||||
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>
|
||||
{('parcours <span class="parcours">'
|
||||
+ ", ".join([p.code for p in self.ue.parcours]))
|
||||
+ "</span>"
|
||||
if self.ue.parcours else ""}
|
||||
{("émise par " + link)}
|
||||
: <b>{self.code}</b>{moyenne}
|
||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
else:
|
||||
return f"""Validation du semestre S{
|
||||
self.formsemestre.semestre_id if self.formsemestre else "?"}
|
||||
{self.formsemestre.html_link_status() if self.formsemestre else ""}
|
||||
: <b>{self.code}</b>
|
||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
|
||||
def ects(self) -> float:
|
||||
"Les ECTS acquis par cette validation. (0 si ce n'est pas une validation d'UE)"
|
||||
return (
|
||||
self.ue.ects
|
||||
if (self.ue is not None) and (self.code in CODES_UE_VALIDES)
|
||||
else 0.0
|
||||
)
|
||||
|
||||
|
||||
class ScolarAutorisationInscription(db.Model):
|
||||
"""Autorisation d'inscription dans un semestre"""
|
||||
@ -93,6 +151,7 @@ class ScolarAutorisationInscription(db.Model):
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
origin_formsemestre = db.relationship("FormSemestre", lazy="select", uselist=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
||||
@ -104,6 +163,21 @@ class ScolarAutorisationInscription(db.Model):
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
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
|
||||
{link}
|
||||
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def autorise_etud(
|
||||
cls,
|
||||
|
@ -36,7 +36,7 @@ Created on Fri Sep 9 09:15:05 2016
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
@ -487,7 +487,7 @@ def comp_coeff_pond(coeffs, ponderations):
|
||||
# -----------------------------------------------------------------------------
|
||||
def get_moduleimpl(modimpl_id) -> dict:
|
||||
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
|
||||
modimpl = ModuleImpl.query.get(modimpl_id)
|
||||
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
||||
if modimpl:
|
||||
return modimpl
|
||||
if SemestreTag.DEBUG:
|
||||
|
@ -122,6 +122,7 @@ ABAN = "ABAN"
|
||||
ABL = "ABL"
|
||||
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
||||
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
||||
ADSUP = "ADSUP" # BUT: UE ou RCUE validé par niveau supérieur
|
||||
ADJ = "ADJ" # admis par le jury
|
||||
ADJR = "ADJR" # UE admise car son RCUE est ADJ
|
||||
ATT = "ATT" #
|
||||
@ -162,6 +163,7 @@ CODES_EXPL = {
|
||||
ADJ: "Validé par le Jury",
|
||||
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
|
||||
ADM: "Validé",
|
||||
ADSUP: "UE ou RCUE validé car le niveau supérieur est validé",
|
||||
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
|
||||
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
||||
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
|
||||
@ -194,18 +196,23 @@ 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}
|
||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
|
||||
"UE validée"
|
||||
CODES_UE_CAPITALISANTS = {ADM}
|
||||
"UE capitalisée"
|
||||
|
||||
CODES_JURY_RCUE = {ADM, ADJ, ADSUP, CMP, AJ, ATJ, RAT, DEF, ABAN}
|
||||
"codes de jury utilisables sur les RCUEs"
|
||||
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
|
||||
"Niveau RCUE validé"
|
||||
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} # PASD pour enregistrement auto
|
||||
CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
@ -219,17 +226,25 @@ BUT_CODES_PASSAGE = {
|
||||
}
|
||||
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
|
||||
# (valeur par défaut 0)
|
||||
BUT_CODES_ORDERED = {
|
||||
NAR: 0,
|
||||
BUT_CODES_ORDER = {
|
||||
ABAN: 0,
|
||||
ABL: 0,
|
||||
DEM: 0,
|
||||
DEF: 0,
|
||||
EXCLU: 0,
|
||||
NAR: 0,
|
||||
UEBSL: 0,
|
||||
RAT: 5,
|
||||
RED: 6,
|
||||
AJ: 10,
|
||||
ATJ: 20,
|
||||
CMP: 50,
|
||||
ADC: 50,
|
||||
PASD: 50,
|
||||
PAS1NCI: 60,
|
||||
PAS1NCI: 50,
|
||||
PASD: 60,
|
||||
ADJR: 90,
|
||||
ADJ: 100,
|
||||
ADSUP: 90,
|
||||
ADJ: 90,
|
||||
ADM: 100,
|
||||
}
|
||||
|
||||
@ -249,6 +264,16 @@ def code_ue_validant(code: str) -> bool:
|
||||
return code in CODES_UE_VALIDES
|
||||
|
||||
|
||||
def code_rcue_validant(code: str) -> bool:
|
||||
"Vrai si ce code d'RCUE est validant"
|
||||
return code in CODES_RCUE_VALIDES
|
||||
|
||||
|
||||
def code_annee_validant(code: str) -> bool:
|
||||
"Vrai si code d'année BUT validant"
|
||||
return code in CODES_ANNEE_BUT_VALIDES
|
||||
|
||||
|
||||
DEVENIR_EXPL = {
|
||||
NEXT: "Passage au semestre suivant",
|
||||
REDOANNEE: "Redoublement année",
|
||||
|
@ -88,7 +88,7 @@ class DEFAULT_TABLE_PREFERENCES(object):
|
||||
return self.values[k]
|
||||
|
||||
|
||||
class GenTable(object):
|
||||
class GenTable:
|
||||
"""Simple 2D tables with export to HTML, PDF, Excel, CSV.
|
||||
Can be sub-classed to generate fancy formats.
|
||||
"""
|
||||
@ -197,6 +197,9 @@ class GenTable(object):
|
||||
def __repr__(self):
|
||||
return f"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
|
||||
|
||||
def __len__(self):
|
||||
return len(self.rows)
|
||||
|
||||
def get_nb_cols(self):
|
||||
return len(self.columns_ids)
|
||||
|
||||
|
@ -51,7 +51,14 @@ from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre, Identite, ApcValidationAnnee
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc.sco_apogee_reader import (
|
||||
APO_DECIMAL_SEP,
|
||||
@ -64,6 +71,7 @@ from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.scodoc.codes_cursus import code_semestre_validant
|
||||
from app.scodoc.codes_cursus import (
|
||||
ADSUP,
|
||||
DEF,
|
||||
DEM,
|
||||
NAR,
|
||||
@ -216,7 +224,12 @@ class ApoEtud(dict):
|
||||
break
|
||||
self.col_elts[code] = elt
|
||||
if elt is None:
|
||||
self.new_cols[col_id] = self.cols[col_id]
|
||||
try:
|
||||
self.new_cols[col_id] = self.cols[col_id]
|
||||
except KeyError as exc:
|
||||
raise ScoFormatError(
|
||||
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{col_id}</tt> non déclarée ?"""
|
||||
) from exc
|
||||
else:
|
||||
try:
|
||||
self.new_cols[col_id] = sco_elts[code][
|
||||
@ -323,14 +336,22 @@ class ApoEtud(dict):
|
||||
x.strip() for x in ue["code_apogee"].split(",")
|
||||
}:
|
||||
if self.export_res_ues:
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
if (
|
||||
decisions_ue and ue["ue_id"] in decisions_ue
|
||||
) or self.export_res_sdj:
|
||||
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
|
||||
code_decision_ue
|
||||
)
|
||||
else:
|
||||
code_decision_ue_apo = ""
|
||||
return dict(
|
||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
B=20,
|
||||
J="",
|
||||
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
|
||||
R=code_decision_ue_apo,
|
||||
M="",
|
||||
)
|
||||
else:
|
||||
@ -343,14 +364,17 @@ class ApoEtud(dict):
|
||||
module_code_found = False
|
||||
for modimpl in modimpls:
|
||||
module = modimpl["module"]
|
||||
if module["code_apogee"] and code in {
|
||||
x.strip() for x in module["code_apogee"].split(",")
|
||||
}:
|
||||
if (
|
||||
res.modimpl_inscr_df[modimpl["moduleimpl_id"]][etudid]
|
||||
and module["code_apogee"]
|
||||
and code in {x.strip() for x in module["code_apogee"].split(",")}
|
||||
):
|
||||
n = res.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
||||
if n != "NI" and self.export_res_modules:
|
||||
return dict(N=self.fmt_note(n), B=20, J="", R="")
|
||||
else:
|
||||
module_code_found = True
|
||||
|
||||
if module_code_found:
|
||||
return VOID_APO_RES
|
||||
#
|
||||
@ -473,7 +497,10 @@ class ApoEtud(dict):
|
||||
)
|
||||
|
||||
def _but_load_validation_annuelle(self):
|
||||
"charge la validation de jury BUT annuelle"
|
||||
"""charge la validation de jury BUT annuelle.
|
||||
Ici impose qu'elle soit issue d'un semestre de l'année en cours
|
||||
(pas forcément nécessaire, voir selon les retours des équipes ?)
|
||||
"""
|
||||
# le semestre impair de l'année scolaire
|
||||
if self.cur_res.formsemestre.semestre_id % 2:
|
||||
formsemestre = self.cur_res.formsemestre
|
||||
@ -488,11 +515,11 @@ class ApoEtud(dict):
|
||||
# ne trouve pas de semestre impair
|
||||
self.validation_annee_but = None
|
||||
return
|
||||
self.validation_annee_but: ApcValidationAnnee = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id, etudid=self.etud["etudid"]
|
||||
).first()
|
||||
)
|
||||
self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=self.etud["etudid"],
|
||||
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
self.is_nar = (
|
||||
self.validation_annee_but and self.validation_annee_but.code == NAR
|
||||
)
|
||||
@ -892,6 +919,75 @@ class ApoData:
|
||||
)
|
||||
return T
|
||||
|
||||
def build_adsup_table(self):
|
||||
"""Construit une table listant les ADSUP émis depuis les formsemestres
|
||||
NIP nom prenom nom_formsemestre etape UE
|
||||
"""
|
||||
validations_ues, validations_rcue = self.list_adsup()
|
||||
rows = [
|
||||
{
|
||||
"code_nip": v.etud.code_nip,
|
||||
"nom": v.etud.nom,
|
||||
"prenom": v.etud.prenom,
|
||||
"formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
|
||||
"etape": v.formsemestre.etapes_apo_str(),
|
||||
"ue": v.ue.acronyme,
|
||||
}
|
||||
for v in validations_ues
|
||||
]
|
||||
rows += [
|
||||
{
|
||||
"code_nip": v.etud.code_nip,
|
||||
"nom": v.etud.nom,
|
||||
"prenom": v.etud.prenom,
|
||||
"formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
|
||||
"etape": "", # on ne sait pas à quel étape rattacher le RCUE
|
||||
"rcue": f"{v.ue1.acronyme}/{v.ue2.acronyme}",
|
||||
}
|
||||
for v in validations_rcue
|
||||
]
|
||||
|
||||
return GenTable(
|
||||
columns_ids=(
|
||||
"code_nip",
|
||||
"nom",
|
||||
"prenom",
|
||||
"formsemestre",
|
||||
"etape",
|
||||
"ue",
|
||||
"rcue",
|
||||
),
|
||||
titles={
|
||||
"code_nip": "NIP",
|
||||
"nom": "Nom",
|
||||
"prenom": "Prénom",
|
||||
"formsemestre": "Semestre",
|
||||
"etape": "Etape",
|
||||
"ue": "UE",
|
||||
"rcue": "RCUE",
|
||||
},
|
||||
rows=rows,
|
||||
xls_sheet_name="ADSUPs",
|
||||
)
|
||||
|
||||
def list_adsup(
|
||||
self,
|
||||
) -> tuple[list[ScolarFormSemestreValidation], list[ApcValidationRCUE]]:
|
||||
"""Liste les validations ADSUP émises par des formsemestres de cet ensemble"""
|
||||
validations_ues = (
|
||||
ScolarFormSemestreValidation.query.filter_by(code=ADSUP)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.filter(
|
||||
ScolarFormSemestreValidation.formsemestre_id.in_(
|
||||
self.etape_formsemestre_ids
|
||||
)
|
||||
)
|
||||
)
|
||||
validations_rcue = ApcValidationRCUE.query.filter_by(code=ADSUP).filter(
|
||||
ApcValidationRCUE.formsemestre_id.in_(self.etape_formsemestre_ids)
|
||||
)
|
||||
return validations_ues, validations_rcue
|
||||
|
||||
|
||||
def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
|
||||
"""
|
||||
@ -1018,6 +1114,10 @@ def export_csv_to_apogee(
|
||||
cr_table = apo_data.build_cr_table()
|
||||
cr_xls = cr_table.excel()
|
||||
|
||||
# ADSUPs
|
||||
adsup_table = apo_data.build_adsup_table()
|
||||
adsup_xls = adsup_table.excel() if len(adsup_table) else None
|
||||
|
||||
# Create ZIP
|
||||
if not dest_zip:
|
||||
data = io.BytesIO()
|
||||
@ -1043,6 +1143,7 @@ def export_csv_to_apogee(
|
||||
log_filename = "scodoc-" + basename + ".log.txt"
|
||||
nar_filename = basename + "-nar" + scu.XLSX_SUFFIX
|
||||
cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX
|
||||
adsup_filename = f"{basename}-adsups{scu.XLSX_SUFFIX}"
|
||||
|
||||
logf = io.StringIO()
|
||||
logf.write(f"export_to_apogee du {time.ctime()}\n\n")
|
||||
@ -1079,6 +1180,8 @@ def export_csv_to_apogee(
|
||||
"\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n"
|
||||
+ "\n".join(apo_data.list_unknown_elements())
|
||||
)
|
||||
if adsup_xls:
|
||||
logf.write(f"\n\nADSUP générés: {len(adsup_table)}\n")
|
||||
log(logf.getvalue()) # sortie aussi sur le log ScoDoc
|
||||
|
||||
# Write data to ZIP
|
||||
@ -1087,6 +1190,8 @@ def export_csv_to_apogee(
|
||||
if nar_xls:
|
||||
dest_zip.writestr(nar_filename, nar_xls)
|
||||
dest_zip.writestr(cr_filename, cr_xls)
|
||||
if adsup_xls:
|
||||
dest_zip.writestr(adsup_filename, adsup_xls)
|
||||
|
||||
if my_zip:
|
||||
dest_zip.close()
|
||||
|
@ -295,8 +295,15 @@ class ApoCSVReadWrite:
|
||||
filename=self.get_filename(),
|
||||
)
|
||||
cols = {} # { col_id : value }
|
||||
for i, field in enumerate(fields):
|
||||
cols[self.col_ids[i]] = field
|
||||
try:
|
||||
for i, field in enumerate(fields):
|
||||
cols[self.col_ids[i]] = field
|
||||
except IndexError as exc:
|
||||
raise
|
||||
raise ScoFormatError(
|
||||
f"Fichier Apogee incorrect (colonnes excédentaires ? (<tt>{i}/{field}</tt>))",
|
||||
filename=self.get_filename(),
|
||||
) from exc
|
||||
etud_tuples.append(
|
||||
ApoEtudTuple(
|
||||
nip=fields[0], # id etudiant
|
||||
|
@ -70,7 +70,7 @@ from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Departement, FormSemestre
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_exceptions import ScoPermissionDenied
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoPermissionDenied
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc import sco_groups
|
||||
@ -125,6 +125,12 @@ class BaseArchiver(object):
|
||||
if not os.path.isdir(obj_dir):
|
||||
log(f"creating directory {obj_dir}")
|
||||
os.mkdir(obj_dir)
|
||||
except FileExistsError as exc:
|
||||
raise ScoException(
|
||||
f"""BaseArchiver error: obj_dir={obj_dir} exists={
|
||||
os.path.exists(obj_dir)
|
||||
} isdir={os.path.isdir(obj_dir)}"""
|
||||
) from exc
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
return obj_dir
|
||||
@ -338,7 +344,7 @@ def do_formsemestre_archive(
|
||||
if data:
|
||||
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data)
|
||||
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
|
||||
table_html, _ = gen_formsemestre_recapcomplet_html_table(
|
||||
table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
|
||||
formsemestre, res, include_evaluations=True
|
||||
)
|
||||
if table_html:
|
||||
|
@ -38,7 +38,7 @@ from flask import flash, render_template, url_for
|
||||
from flask_json import json_response
|
||||
from flask_login import current_user
|
||||
|
||||
from app import email
|
||||
from app import db, email
|
||||
from app import log
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.but import bulletin_but
|
||||
@ -354,7 +354,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
"modules_capitalized"
|
||||
] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée)
|
||||
if ue_status["is_capitalized"] and ue_status["formsemestre_id"] is not None:
|
||||
sem_origin = FormSemestre.query.get(ue_status["formsemestre_id"])
|
||||
sem_origin = db.session.get(FormSemestre, ue_status["formsemestre_id"])
|
||||
u[
|
||||
"ue_descr_txt"
|
||||
] = f'capitalisée le {ndb.DateISOtoDMY(ue_status["event_date"])}'
|
||||
@ -369,7 +369,9 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
)
|
||||
if ue_status["moy"] != "NA":
|
||||
# détail des modules de l'UE capitalisée
|
||||
formsemestre_cap = FormSemestre.query.get(ue_status["formsemestre_id"])
|
||||
formsemestre_cap = db.session.get(
|
||||
FormSemestre, ue_status["formsemestre_id"]
|
||||
)
|
||||
nt_cap: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||
formsemestre_cap
|
||||
)
|
||||
@ -749,7 +751,7 @@ def etud_descr_situation_semestre(
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
parcour_id = res.etuds_parcour_id[etudid]
|
||||
parcour: ApcParcours = (
|
||||
ApcParcours.query.get(parcour_id) if parcour_id is not None else None
|
||||
db.session.get(ApcParcours, parcour_id) if parcour_id is not None else None
|
||||
)
|
||||
if parcour:
|
||||
infos["parcours_titre"] = parcour.libelle or ""
|
||||
@ -928,7 +930,7 @@ def formsemestre_bulletinetud(
|
||||
|
||||
"""
|
||||
format = format or "html"
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if not formsemestre:
|
||||
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
|
||||
|
||||
@ -943,7 +945,7 @@ def formsemestre_bulletinetud(
|
||||
)[0]
|
||||
|
||||
if format not in {"html", "pdfmail"}:
|
||||
filename = scu.bul_filename(formsemestre, etud, format)
|
||||
filename = scu.bul_filename(formsemestre, etud)
|
||||
mime, suffix = scu.get_mime_suffix(format)
|
||||
return scu.send_file(bulletin, filename, mime=mime, suffix=suffix)
|
||||
elif format == "pdfmail":
|
||||
@ -1238,7 +1240,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,
|
||||
|
@ -33,7 +33,7 @@ import json
|
||||
|
||||
from flask import abort
|
||||
|
||||
from app import ScoDocJSONEncoder
|
||||
from app import db, ScoDocJSONEncoder
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import but_validations
|
||||
@ -245,7 +245,7 @@ def formsemestre_bulletinetud_published_dict(
|
||||
u["module"] = []
|
||||
# Structure UE/Matière/Module
|
||||
# Recodé en 2022
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
u["matiere"] = [
|
||||
{
|
||||
"matiere_id": mat.id,
|
||||
|
@ -54,7 +54,7 @@ import traceback
|
||||
from flask import g
|
||||
|
||||
import app
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
@ -266,7 +266,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
# appel via API ou tests sans dept:
|
||||
formsemestre = None
|
||||
if formsemestre_id:
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if formsemestre is None:
|
||||
raise ScoException("invalidate_formsemestre: departement must be set")
|
||||
app.set_sco_dept(formsemestre.departement.acronym, open_cnx=False)
|
||||
@ -315,6 +315,19 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||
|
||||
|
||||
def invalidate_formsemestre_etud(etud: "Identite"):
|
||||
"""Invalide tous les formsemestres auxquels l'étudiant est inscrit"""
|
||||
from app.models import FormSemestre, FormSemestreInscription
|
||||
|
||||
inscriptions = (
|
||||
FormSemestreInscription.query.filter_by(etudid=etud.id)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
for inscription in inscriptions:
|
||||
invalidate_formsemestre(inscription.formsemestre_id)
|
||||
|
||||
|
||||
class DeferredSemCacheManager:
|
||||
"""Contexte pour effectuer des opérations indépendantes dans la
|
||||
même requete qui invalident le cache. Par exemple, quand on inscrit
|
||||
|
@ -949,6 +949,7 @@ def do_formsemestre_validate_ue(
|
||||
"ue_id": ue_id,
|
||||
"semestre_id": semestre_id,
|
||||
"is_external": is_external,
|
||||
"moy_ue": moy_ue,
|
||||
}
|
||||
if date:
|
||||
args["event_date"] = date
|
||||
@ -965,14 +966,13 @@ def do_formsemestre_validate_ue(
|
||||
cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
|
||||
# insert
|
||||
args["code"] = code
|
||||
if code == ADM:
|
||||
if moy_ue is None:
|
||||
# stocke la moyenne d'UE capitalisée:
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
moy_ue = ue_status["moy"] if ue_status else ""
|
||||
args["moy_ue"] = moy_ue
|
||||
if (code == ADM) and (moy_ue is None):
|
||||
# stocke la moyenne d'UE capitalisée:
|
||||
ue_status = nt.get_etud_ue_status(etudid, ue_id)
|
||||
args["moy_ue"] = ue_status["moy"] if ue_status else ""
|
||||
|
||||
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")
|
||||
|
@ -82,7 +82,7 @@ def html_edit_formation_apc(
|
||||
if None in ects:
|
||||
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
|
||||
else:
|
||||
ects_by_sem[semestre_idx] = sum(ects)
|
||||
ects_by_sem[semestre_idx] = f"{sum(ects):g}"
|
||||
|
||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||
|
||||
|
@ -103,7 +103,7 @@ def do_formation_delete(formation_id):
|
||||
"""delete a formation (and all its UE, matieres, modules)
|
||||
Warning: delete all ues, will ask if there are validations !
|
||||
"""
|
||||
formation: Formation = Formation.query.get(formation_id)
|
||||
formation: Formation = db.session.get(Formation, formation_id)
|
||||
if formation is None:
|
||||
return
|
||||
acronyme = formation.acronyme
|
||||
@ -132,6 +132,7 @@ def do_formation_delete(formation_id):
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation_id,
|
||||
text=f"Suppression de la formation {acronyme}",
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
|
||||
@ -329,6 +330,7 @@ def do_formation_create(args: dict) -> Formation:
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
text=f"""Création de la formation {
|
||||
formation.titre} ({formation.acronyme}) version {formation.version}""",
|
||||
max_frequency=0,
|
||||
)
|
||||
return formation
|
||||
|
||||
|
@ -30,13 +30,13 @@
|
||||
"""
|
||||
import flask
|
||||
from flask import g, url_for, request
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.formations import Matiere
|
||||
|
||||
from app import db, log
|
||||
from app.models import Formation, Matiere, UniteEns, ScolarNews
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import Formation
|
||||
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
@ -73,7 +73,7 @@ def do_matiere_edit(*args, **kw):
|
||||
# edit
|
||||
_matiereEditor.edit(cnx, *args, **kw)
|
||||
formation_id = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]["formation_id"]
|
||||
Formation.query.get(formation_id).invalidate_cached_sems()
|
||||
db.session.get(Formation, formation_id).invalidate_cached_sems()
|
||||
|
||||
|
||||
def do_matiere_create(args):
|
||||
@ -88,12 +88,11 @@ def do_matiere_create(args):
|
||||
r = _matiereEditor.create(cnx, args)
|
||||
|
||||
# news
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return r
|
||||
@ -101,13 +100,12 @@ def do_matiere_create(args):
|
||||
|
||||
def matiere_create(ue_id=None):
|
||||
"""Creation d'une matiere"""
|
||||
from app.scodoc import sco_edit_ue
|
||||
|
||||
UE = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
||||
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
|
||||
default_numero = max([mat.numero for mat in ue.matieres] or [9]) + 1
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Création d'une matière"),
|
||||
"""<h2>Création d'une matière dans l'UE %(titre)s (%(acronyme)s)</h2>""" % UE,
|
||||
"""<p class="help">Les matières sont des groupes de modules dans une UE
|
||||
f"""<h2>Création d'une matière dans l'UE {ue.titre} ({ue.acronyme})</h2>
|
||||
<p class="help">Les matières sont des groupes de modules dans une UE
|
||||
d'une formation donnée. Les matières servent surtout pour la
|
||||
présentation (bulletins, etc) mais <em>n'ont pas de rôle dans le calcul
|
||||
des notes.</em>
|
||||
@ -127,13 +125,21 @@ associé.
|
||||
scu.get_request_args(),
|
||||
(
|
||||
("ue_id", {"input_type": "hidden", "default": ue_id}),
|
||||
("titre", {"size": 30, "explanation": "nom de la matière."}),
|
||||
(
|
||||
"titre",
|
||||
{
|
||||
"size": 30,
|
||||
"explanation": "nom de la matière.",
|
||||
},
|
||||
),
|
||||
(
|
||||
"numero",
|
||||
{
|
||||
"size": 2,
|
||||
"explanation": "numéro (1,2,3,4...) pour affichage",
|
||||
"type": "int",
|
||||
"default": default_numero,
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -141,7 +147,7 @@ associé.
|
||||
)
|
||||
|
||||
dest_url = url_for(
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=UE["formation_id"]
|
||||
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue.formation_id
|
||||
)
|
||||
|
||||
if tf[0] == 0:
|
||||
@ -194,12 +200,11 @@ def do_matiere_delete(oid):
|
||||
_matiereEditor.delete(cnx, oid)
|
||||
|
||||
# news
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=ue["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
|
@ -98,10 +98,10 @@ def module_list(*args, **kw):
|
||||
|
||||
def do_module_create(args) -> int:
|
||||
"Create a module. Returns id of new object."
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
formation = db.session.get(Formation, args["formation_id"])
|
||||
# refuse de créer un module APC avec semestres incohérents:
|
||||
if formation.is_apc():
|
||||
ue = UniteEns.query.get(args["ue_id"])
|
||||
ue = db.session.get(UniteEns, args["ue_id"])
|
||||
if int(args.get("semestre_id", 0)) != ue.semestre_idx:
|
||||
raise ScoValueError("Formation incompatible: contacter le support ScoDoc")
|
||||
# create
|
||||
@ -114,7 +114,6 @@ def do_module_create(args) -> int:
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return module_id
|
||||
@ -186,7 +185,6 @@ def do_module_delete(oid):
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=mod["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
|
||||
@ -250,7 +248,7 @@ def do_module_edit(vals: dict) -> None:
|
||||
# edit
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_moduleEditor.edit(cnx, vals)
|
||||
Formation.query.get(mod["formation_id"]).invalidate_cached_sems()
|
||||
db.session.get(Formation, mod["formation_id"]).invalidate_cached_sems()
|
||||
|
||||
|
||||
def check_module_code_unicity(code, field, formation_id, module_id=None):
|
||||
@ -661,6 +659,7 @@ def module_edit(
|
||||
"explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage",
|
||||
"type": "int",
|
||||
"default": default_num,
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -806,7 +805,7 @@ def module_edit(
|
||||
if create:
|
||||
if not matiere_id:
|
||||
# formulaire avec choix UE de rattachement
|
||||
ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
ue = db.session.get(UniteEns, tf[2]["ue_id"])
|
||||
if ue is None:
|
||||
raise ValueError("UE invalide")
|
||||
matiere = ue.matieres.first()
|
||||
@ -820,7 +819,7 @@ def module_edit(
|
||||
|
||||
tf[2]["semestre_id"] = ue.semestre_idx
|
||||
module_id = do_module_create(tf[2])
|
||||
module = Module.query.get(module_id)
|
||||
module = db.session.get(Module, module_id)
|
||||
else: # EDITION MODULE
|
||||
# l'UE de rattachement peut changer
|
||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||
@ -838,7 +837,7 @@ def module_edit(
|
||||
)
|
||||
# En APC, force le semestre égal à celui de l'UE
|
||||
if is_apc:
|
||||
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
selected_ue = db.session.get(UniteEns, tf[2]["ue_id"])
|
||||
if selected_ue is None:
|
||||
raise ValueError("UE invalide")
|
||||
tf[2]["semestre_id"] = selected_ue.semestre_idx
|
||||
@ -854,13 +853,13 @@ def module_edit(
|
||||
module.parcours = formation.referentiel_competence.parcours.all()
|
||||
else:
|
||||
module.parcours = [
|
||||
ApcParcours.query.get(int(parcour_id_str))
|
||||
db.session.get(ApcParcours, int(parcour_id_str))
|
||||
for parcour_id_str in tf[2]["parcours"]
|
||||
]
|
||||
# Modifie les AC
|
||||
if "app_critiques" in tf[2]:
|
||||
module.app_critiques = [
|
||||
ApcAppCritique.query.get(int(ac_id_str))
|
||||
db.session.get(ApcAppCritique, int(ac_id_str))
|
||||
for ac_id_str in tf[2]["app_critiques"]
|
||||
]
|
||||
db.session.add(module)
|
||||
|
@ -36,8 +36,7 @@ from flask import flash, render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.but import apc_edit_ue
|
||||
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models import (
|
||||
@ -137,15 +136,14 @@ def do_ue_create(args):
|
||||
ue_id = _ueEditor.create(cnx, args)
|
||||
log(f"do_ue_create: created {ue_id} with {args}")
|
||||
|
||||
formation: Formation = Formation.query.get(args["formation_id"])
|
||||
formation: Formation = db.session.get(Formation, args["formation_id"])
|
||||
formation.invalidate_module_coefs()
|
||||
# news
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
formation = db.session.get(Formation, args["formation_id"])
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=args["formation_id"],
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
formation.invalidate_cached_sems()
|
||||
return ue_id
|
||||
@ -230,7 +228,6 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=formation.id,
|
||||
text=f"Modification de la formation {formation.acronyme}",
|
||||
max_frequency=10 * 60,
|
||||
)
|
||||
#
|
||||
if not force:
|
||||
@ -286,7 +283,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
}
|
||||
submitlabel = "Créer cette UE"
|
||||
can_change_semestre_id = True
|
||||
formation = Formation.query.get(formation_id)
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
if not formation:
|
||||
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
|
||||
cursus = formation.get_cursus()
|
||||
@ -443,6 +440,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
{
|
||||
"input_type": "boolcheckbox",
|
||||
"title": "UE externe",
|
||||
"readonly": not create, # ne permet pas de transformer une UE existante en externe
|
||||
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
|
||||
},
|
||||
),
|
||||
@ -503,7 +501,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]
|
||||
@ -544,9 +542,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"semestre_id": tf[2]["semestre_idx"],
|
||||
},
|
||||
)
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
flash(f"UE créée (code {ue.ue_code})")
|
||||
else:
|
||||
if not tf[2]["numero"]:
|
||||
tf[2]["numero"] = 0
|
||||
do_ue_edit(tf[2])
|
||||
flash("UE modifiée")
|
||||
|
||||
@ -596,7 +596,7 @@ def next_ue_numero(formation_id, semestre_id=None):
|
||||
"""Numero d'une nouvelle UE dans cette formation.
|
||||
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre
|
||||
"""
|
||||
formation = Formation.query.get(formation_id)
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
ues = ue_list(args={"formation_id": formation_id})
|
||||
if not ues:
|
||||
return 0
|
||||
@ -660,7 +660,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
"""
|
||||
from app.scodoc import sco_formsemestre_validation
|
||||
|
||||
formation: Formation = Formation.query.get(formation_id)
|
||||
formation: Formation = db.session.get(Formation, formation_id)
|
||||
if not formation:
|
||||
raise ScoValueError("invalid formation_id")
|
||||
parcours = formation.get_cursus()
|
||||
@ -756,7 +756,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
||||
],
|
||||
page_title=f"Programme {formation.acronyme} v{formation.version}",
|
||||
),
|
||||
f"""<h2>{formation.to_html()} {lockicon}
|
||||
f"""<h2>{formation.html()} {lockicon}
|
||||
</h2>
|
||||
""",
|
||||
]
|
||||
@ -1009,12 +1009,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
<p><ul>"""
|
||||
)
|
||||
for formsemestre in formsemestres:
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id
|
||||
)}">{formsemestre.titre_mois()}</a>"""
|
||||
)
|
||||
H.append(f"""<li>{formsemestre.html_link_status()}""")
|
||||
if not formsemestre.etat:
|
||||
H.append(" [verrouillé]")
|
||||
else:
|
||||
@ -1381,13 +1376,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
|
||||
@ -1406,29 +1400,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>
|
||||
"""
|
||||
@ -1460,7 +1461,7 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
||||
cnx = ndb.GetDBConnexion()
|
||||
_ueEditor.edit(cnx, args)
|
||||
|
||||
formation = Formation.query.get(ue["formation_id"])
|
||||
formation = db.session.get(Formation, ue["formation_id"])
|
||||
if not dont_invalidate_cache:
|
||||
# Invalide les semestres utilisant cette formation
|
||||
# ainsi que les poids et coefs
|
||||
|
@ -62,7 +62,9 @@ def format_etud_ident(etud):
|
||||
else:
|
||||
etud["prenom_etat_civil"] = ""
|
||||
etud["civilite_str"] = format_civilite(etud["civilite"])
|
||||
etud["civilite_etat_civil_str"] = format_civilite(etud["civilite_etat_civil"])
|
||||
etud["civilite_etat_civil_str"] = format_civilite(
|
||||
etud.get("civilite_etat_civil", "X")
|
||||
)
|
||||
# Nom à afficher:
|
||||
if etud["nom_usuel"]:
|
||||
etud["nom_disp"] = etud["nom_usuel"]
|
||||
@ -145,7 +147,7 @@ def format_civilite(civilite):
|
||||
|
||||
def format_etat_civil(etud: dict):
|
||||
if etud["prenom_etat_civil"]:
|
||||
civ = {"M": "M.", "F": "Mme", "X": ""}[etud["civilite_etat_civil"]]
|
||||
civ = {"M": "M.", "F": "Mme", "X": ""}[etud.get("civilite_etat_civil", "X")]
|
||||
return f'{civ} {etud["prenom_etat_civil"]} {etud["nom"]}'
|
||||
else:
|
||||
return etud["nomprenom"]
|
||||
@ -260,7 +262,7 @@ def identite_list(cnx, *a, **kw):
|
||||
|
||||
def identite_edit_nocheck(cnx, args):
|
||||
"""Modifie les champs mentionnes dans args, sans verification ni notification."""
|
||||
etud = Identite.query.get(args["etudid"])
|
||||
etud = db.session.get(Identite, args["etudid"])
|
||||
etud.from_dict(args)
|
||||
db.session.commit()
|
||||
|
||||
@ -669,6 +671,7 @@ def create_etud(cnx, args: dict = None):
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
|
||||
url=etud["url"],
|
||||
max_frequency=0,
|
||||
)
|
||||
return etud
|
||||
|
||||
|
@ -129,7 +129,7 @@ def do_evaluation_create(
|
||||
)
|
||||
args = locals()
|
||||
log("do_evaluation_create: args=" + str(args))
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
|
||||
if modimpl is None:
|
||||
raise ValueError("module not found")
|
||||
check_evaluation_args(args)
|
||||
@ -252,12 +252,11 @@ def do_evaluation_delete(evaluation_id):
|
||||
def do_evaluation_get_all_notes(
|
||||
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
|
||||
):
|
||||
"""Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||||
"""Toutes les notes pour une évaluation: { etudid : { 'value' : value, 'date' : date ... }}
|
||||
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
|
||||
"""
|
||||
do_cache = (
|
||||
filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||||
) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||||
# pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant
|
||||
do_cache = filter_suppressed and table == "notes_notes" and (by_uid is None)
|
||||
if do_cache:
|
||||
r = sco_cache.EvaluationCache.get(evaluation_id)
|
||||
if r is not None:
|
||||
|
@ -37,11 +37,8 @@ from flask_login import current_user
|
||||
from flask import request
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models import Evaluation, FormSemestre, ModuleImpl
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
@ -62,7 +59,7 @@ def evaluation_create_form(
|
||||
):
|
||||
"Formulaire création/édition d'une évaluation (pas de ses notes)"
|
||||
if evaluation_id is not None:
|
||||
evaluation: Evaluation = models.Evaluation.query.get(evaluation_id)
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if evaluation is None:
|
||||
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
|
||||
moduleimpl_id = evaluation.moduleimpl_id
|
||||
@ -363,7 +360,7 @@ def evaluation_create_form(
|
||||
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
|
||||
if is_apc:
|
||||
# Set poids
|
||||
evaluation = models.Evaluation.query.get(evaluation_id)
|
||||
evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
for ue in sem_ues:
|
||||
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
|
||||
db.session.add(evaluation)
|
||||
|
@ -12,6 +12,7 @@ Sur une idée de Pascal Bouron, de Lyon.
|
||||
import time
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.models import Evaluation, FormSemestre
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
@ -113,7 +114,7 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||
rows.append(row)
|
||||
line_idx += 1
|
||||
for evaluation_id in modimpl_results.evals_notes:
|
||||
e = Evaluation.query.get(evaluation_id)
|
||||
e = db.session.get(Evaluation, evaluation_id)
|
||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||
row = {
|
||||
"type": "",
|
||||
|
@ -433,7 +433,7 @@ def excel_simple_table(
|
||||
return ws.generate()
|
||||
|
||||
|
||||
def excel_feuille_saisie(e, titreannee, description, lines):
|
||||
def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines):
|
||||
"""Genere feuille excel pour saisie des notes.
|
||||
E: evaluation (dict)
|
||||
lines: liste de tuples
|
||||
@ -512,18 +512,20 @@ def excel_feuille_saisie(e, titreannee, description, lines):
|
||||
# description evaluation
|
||||
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
|
||||
ws.append_single_cell_row(
|
||||
"Evaluation du %s (coef. %g)" % (e["jour"], e["coefficient"]), style
|
||||
"Evaluation du %s (coef. %g)"
|
||||
% (evaluation.jour or "sans date", evaluation.coefficient or 0.0),
|
||||
style,
|
||||
)
|
||||
# ligne blanche
|
||||
ws.append_blank_row()
|
||||
# code et titres colonnes
|
||||
ws.append_row(
|
||||
[
|
||||
ws.make_cell("!%s" % e["evaluation_id"], style_ro),
|
||||
ws.make_cell("!%s" % evaluation.id, style_ro),
|
||||
ws.make_cell("Nom", style_titres),
|
||||
ws.make_cell("Prénom", style_titres),
|
||||
ws.make_cell("Groupe", style_titres),
|
||||
ws.make_cell("Note sur %g" % e["note_max"], style_titres),
|
||||
ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres),
|
||||
ws.make_cell("Remarque", style_titres),
|
||||
]
|
||||
)
|
||||
|
@ -28,15 +28,15 @@
|
||||
"""Table recap formation (avec champs éditables)
|
||||
"""
|
||||
import io
|
||||
from zipfile import ZipFile, BadZipfile
|
||||
from zipfile import ZipFile
|
||||
|
||||
from flask import Response
|
||||
from flask import send_file, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app.models import Formation, FormSemestre, UniteEns, Module
|
||||
from app.models.formations import Matiere
|
||||
from app import db
|
||||
from app.models import Formation, FormSemestre, Matiere, Module, UniteEns
|
||||
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -178,7 +178,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire):
|
||||
)
|
||||
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
|
||||
for formation_id in formation_ids:
|
||||
formation = Formation.query.get(formation_id)
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
xls = formation_table_recap(formation_id, format="xlsx").data
|
||||
filename = (
|
||||
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
|
||||
|
@ -200,31 +200,31 @@ def do_formsemestres_associate_new_version(
|
||||
|
||||
# New formation:
|
||||
(
|
||||
formation_id,
|
||||
new_formation_id,
|
||||
modules_old2new,
|
||||
ues_old2new,
|
||||
) = sco_formations.formation_create_new_version(formation_id, redirect=False)
|
||||
# Log new ues:
|
||||
for ue_id in ues_old2new:
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
new_ue = UniteEns.query.get(ues_old2new[ue_id])
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
new_ue = db.session.get(UniteEns, ues_old2new[ue_id])
|
||||
assert ue.semestre_idx == new_ue.semestre_idx
|
||||
log(f"{ue} -> {new_ue}")
|
||||
# Log new modules
|
||||
for module_id in modules_old2new:
|
||||
mod = Module.query.get(module_id)
|
||||
new_mod = Module.query.get(modules_old2new[module_id])
|
||||
mod = db.session.get(Module, module_id)
|
||||
new_mod = db.session.get(Module, modules_old2new[module_id])
|
||||
assert mod.semestre_id == new_mod.semestre_id
|
||||
log(f"{mod} -> {new_mod}")
|
||||
# re-associate
|
||||
for formsemestre_id in formsemestre_ids:
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
formsemestre.formation_id = formation_id
|
||||
formsemestre.formation_id = new_formation_id
|
||||
db.session.add(formsemestre)
|
||||
_reassociate_moduleimpls(formsemestre, ues_old2new, modules_old2new)
|
||||
|
||||
db.session.commit()
|
||||
return formation_id
|
||||
return new_formation_id
|
||||
|
||||
|
||||
def _reassociate_moduleimpls(
|
||||
@ -246,8 +246,12 @@ def _reassociate_moduleimpls(
|
||||
Evaluation.moduleimpl_id == ModuleImpl.id,
|
||||
ModuleImpl.formsemestre_id == formsemestre.id,
|
||||
):
|
||||
poids.ue_id = ues_old2new[poids.ue_id]
|
||||
db.session.add(poids)
|
||||
if poids.ue_id in ues_old2new:
|
||||
poids.ue_id = ues_old2new[poids.ue_id]
|
||||
db.session.add(poids)
|
||||
else:
|
||||
# poids vers une UE qui n'est pas ou plus dans notre formation
|
||||
db.session.delete(poids)
|
||||
|
||||
# update decisions:
|
||||
for event in ScolarEvent.query.filter_by(formsemestre_id=formsemestre.id):
|
||||
@ -258,8 +262,9 @@ def _reassociate_moduleimpls(
|
||||
for validation in ScolarFormSemestreValidation.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
):
|
||||
if validation.ue_id is not None:
|
||||
if (validation.ue_id is not None) and validation.ue_id in ues_old2new:
|
||||
validation.ue_id = ues_old2new[validation.ue_id]
|
||||
# si l'UE n'est pas ou plus dans notre formation, laisse.
|
||||
db.session.add(validation)
|
||||
|
||||
db.session.commit()
|
||||
|
@ -163,7 +163,7 @@ def formation_export_dict(
|
||||
if tags:
|
||||
mod["tags"] = [{"name": x} for x in tags]
|
||||
#
|
||||
module: Module = Module.query.get(module_id)
|
||||
module: Module = db.session.get(Module, module_id)
|
||||
if module.is_apc():
|
||||
# Exporte les coefficients
|
||||
if ue_reference_style == "id":
|
||||
@ -359,7 +359,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
referentiel_competence_id, ue_info[1]
|
||||
)
|
||||
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
|
||||
ue: UniteEns = UniteEns.query.get(ue_id)
|
||||
ue: UniteEns = db.session.get(UniteEns, ue_id)
|
||||
assert ue
|
||||
if xml_ue_id:
|
||||
ues_old2new[xml_ue_id] = ue_id
|
||||
@ -424,7 +424,7 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
if xml_module_id:
|
||||
modules_old2new[int(xml_module_id)] = mod_id
|
||||
if len(mod_info) > 2:
|
||||
module: Module = Module.query.get(mod_id)
|
||||
module: Module = db.session.get(Module, mod_id)
|
||||
tag_names = []
|
||||
ue_coef_dict = {}
|
||||
for child in mod_info[2]:
|
||||
@ -626,7 +626,9 @@ def formation_list_table() -> GenTable:
|
||||
def formation_create_new_version(formation_id, redirect=True):
|
||||
"duplicate formation, with new version number"
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
resp = formation_export(formation_id, export_ids=True, format="xml")
|
||||
resp = formation_export(
|
||||
formation_id, export_ids=True, export_external_ues=True, format="xml"
|
||||
)
|
||||
xml_data = resp.get_data(as_text=True)
|
||||
new_id, modules_old2new, ues_old2new = formation_import_xml(
|
||||
xml_data, use_local_refcomp=True
|
||||
@ -636,6 +638,7 @@ def formation_create_new_version(formation_id, redirect=True):
|
||||
typ=ScolarNews.NEWS_FORM,
|
||||
obj=new_id,
|
||||
text=f"Nouvelle version de la formation {formation.acronyme}",
|
||||
max_frequency=0,
|
||||
)
|
||||
if redirect:
|
||||
flash("Nouvelle version !")
|
||||
|
@ -261,6 +261,7 @@ def do_formsemestre_create(args, silent=False):
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
|
||||
url=args["url"],
|
||||
max_frequency=0,
|
||||
)
|
||||
return formsemestre_id
|
||||
|
||||
|
@ -793,7 +793,16 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
{tf[1]}
|
||||
"""
|
||||
elif tf[0] == -1:
|
||||
return "<h4>annulation</h4>"
|
||||
if formsemestre:
|
||||
return redirect(
|
||||
url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
|
||||
else:
|
||||
if tf[2]["gestion_compensation_lst"]:
|
||||
tf[2]["gestion_compensation"] = True
|
||||
@ -941,7 +950,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if "parcours" in tf[2]:
|
||||
formsemestre.parcours = [
|
||||
ApcParcours.query.get(int(parcour_id_str))
|
||||
db.session.get(ApcParcours, int(parcour_id_str))
|
||||
for parcour_id_str in tf[2]["parcours"]
|
||||
]
|
||||
db.session.add(formsemestre)
|
||||
@ -1035,7 +1044,7 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
|
||||
ok = True
|
||||
msg = []
|
||||
for module_id in module_ids_to_del:
|
||||
module = Module.query.get(module_id)
|
||||
module = db.session.get(Module, module_id)
|
||||
if module is None:
|
||||
continue # ignore invalid ids
|
||||
modimpls = ModuleImpl.query.filter_by(
|
||||
@ -1215,7 +1224,7 @@ def do_formsemestre_clone(
|
||||
args["etat"] = 1 # non verrouillé
|
||||
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
|
||||
log(f"created formsemestre {formsemestre_id}")
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
# 2- create moduleimpls
|
||||
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
|
||||
for mod_orig in mods_orig:
|
||||
@ -1333,11 +1342,18 @@ Ceci n'est possible que si :
|
||||
cancelbutton="Annuler",
|
||||
)
|
||||
if tf[0] == 0:
|
||||
if formsemestre_has_decisions_or_compensations(formsemestre):
|
||||
has_decisions, message = formsemestre_has_decisions_or_compensations(
|
||||
formsemestre
|
||||
)
|
||||
if has_decisions:
|
||||
H.append(
|
||||
"""<p><b>Ce semestre ne peut pas être supprimé !
|
||||
(il y a des décisions de jury ou des compensations par d'autres semestres)</b>
|
||||
</p>"""
|
||||
f"""<p><b>Ce semestre ne peut pas être supprimé !</b></p>
|
||||
<p>il y a des décisions de jury ou des compensations par d'autres semestres:
|
||||
</p>
|
||||
<ul>
|
||||
<li>{message}</li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
H.append(tf[1])
|
||||
@ -1372,32 +1388,46 @@ def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
|
||||
return flask.redirect(scu.ScoURL())
|
||||
|
||||
|
||||
def formsemestre_has_decisions_or_compensations(formsemestre: FormSemestre):
|
||||
def formsemestre_has_decisions_or_compensations(
|
||||
formsemestre: FormSemestre,
|
||||
) -> tuple[bool, str]:
|
||||
"""True if decision de jury (sem. UE, RCUE, année) émanant de ce semestre
|
||||
ou compensation de ce semestre par d'autres semestres
|
||||
ou autorisations de passage.
|
||||
"""
|
||||
# Validations de semestre ou d'UEs
|
||||
if ScolarFormSemestreValidation.query.filter_by(
|
||||
nb_validations = ScolarFormSemestreValidation.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
).count():
|
||||
return True
|
||||
if ScolarFormSemestreValidation.query.filter_by(
|
||||
).count()
|
||||
if nb_validations:
|
||||
return True, f"{nb_validations} validations de semestre ou d'UE"
|
||||
nb_validations = ScolarFormSemestreValidation.query.filter_by(
|
||||
compense_formsemestre_id=formsemestre.id
|
||||
).count():
|
||||
return True
|
||||
).count()
|
||||
if nb_validations:
|
||||
return True, f"{nb_validations} compensations utilisées dans d'autres semestres"
|
||||
# Autorisations d'inscription:
|
||||
if ScolarAutorisationInscription.query.filter_by(
|
||||
nb_validations = ScolarAutorisationInscription.query.filter_by(
|
||||
origin_formsemestre_id=formsemestre.id
|
||||
).count():
|
||||
return True
|
||||
).count()
|
||||
if nb_validations:
|
||||
return (
|
||||
True,
|
||||
f"{nb_validations} autorisations d'inscriptions émanant de ce semestre",
|
||||
)
|
||||
# Validations d'années BUT
|
||||
if ApcValidationAnnee.query.filter_by(formsemestre_id=formsemestre.id).count():
|
||||
return True
|
||||
nb_validations = ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
).count()
|
||||
if nb_validations:
|
||||
return True, f"{nb_validations} validations d'année BUT utilisant ce semestre"
|
||||
# Validations de RCUEs
|
||||
if ApcValidationRCUE.query.filter_by(formsemestre_id=formsemestre.id).count():
|
||||
return True
|
||||
return False
|
||||
nb_validations = ApcValidationRCUE.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
).count()
|
||||
if nb_validations:
|
||||
return True, f"{nb_validations} validations de RCUE utilisant ce semestre"
|
||||
return False, ""
|
||||
|
||||
|
||||
def do_formsemestre_delete(formsemestre_id):
|
||||
@ -1500,6 +1530,7 @@ def do_formsemestre_delete(formsemestre_id):
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
obj=formsemestre_id,
|
||||
text="Suppression du semestre %(titre)s" % sem,
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -175,9 +175,7 @@ def do_formsemestre_demission(
|
||||
)
|
||||
db.session.add(event)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=formsemestre_id
|
||||
) # > démission ou défaillance
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
|
||||
if etat_new == scu.DEMISSION:
|
||||
flash("Démission enregistrée")
|
||||
elif etat_new == scu.DEF:
|
||||
@ -210,7 +208,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
|
||||
|
||||
if nt.etud_has_decision(etudid):
|
||||
raise ScoValueError(
|
||||
"""désinscription impossible: l'étudiant {etud.nomprenom} a
|
||||
f"""désinscription impossible: l'étudiant {etud.nomprenom} a
|
||||
une décision de jury (la supprimer avant si nécessaire)"""
|
||||
)
|
||||
|
||||
|
@ -36,14 +36,20 @@ from flask import request
|
||||
from flask import flash, redirect, render_template, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.but.cursus_but import formsemestre_warning_apc_setup
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_common import ResultatsSemestre
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Evaluation, Formation, Module, ModuleImpl, NotesNotes
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models import (
|
||||
Evaluation,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
NotesNotes,
|
||||
)
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -254,7 +260,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
},
|
||||
]
|
||||
# debug :
|
||||
if current_app.config["ENV"] == "development":
|
||||
if current_app.config["DEBUG"]:
|
||||
menu_semestre.append(
|
||||
{
|
||||
"title": "Vérifier l'intégrité",
|
||||
@ -594,6 +600,7 @@ def formsemestre_description_table(
|
||||
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
||||
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
is_apc = formsemestre.formation.is_apc()
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
||||
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours)
|
||||
@ -607,7 +614,7 @@ def formsemestre_description_table(
|
||||
else:
|
||||
ues = formsemestre.get_ues()
|
||||
columns_ids += [f"ue_{ue.id}" for ue in ues]
|
||||
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
|
||||
if sco_preferences.get_preference("bul_show_ects", formsemestre_id) and not is_apc:
|
||||
columns_ids += ["ects"]
|
||||
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
||||
if with_evals:
|
||||
@ -634,6 +641,7 @@ def formsemestre_description_table(
|
||||
sum_coef = 0
|
||||
sum_ects = 0
|
||||
last_ue_id = None
|
||||
formsemestre_parcours_ids = {p.id for p in formsemestre.parcours}
|
||||
for modimpl in formsemestre.modimpls_sorted:
|
||||
# Ligne UE avec ECTS:
|
||||
ue = modimpl.module.ue
|
||||
@ -660,7 +668,7 @@ def formsemestre_description_table(
|
||||
ue_info[
|
||||
f"_{k}_td_attrs"
|
||||
] = f'style="background-color: {ue.color} !important;"'
|
||||
if not formsemestre.formation.is_apc():
|
||||
if not is_apc:
|
||||
# n'affiche la ligne UE qu'en formation classique
|
||||
# car l'UE de rattachement n'a pas d'intérêt en BUT
|
||||
rows.append(ue_info)
|
||||
@ -701,8 +709,17 @@ def formsemestre_description_table(
|
||||
for ue in ues:
|
||||
row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
|
||||
if with_parcours:
|
||||
# Intersection des parcours du module avec ceux du formsemestre
|
||||
row["parcours"] = ", ".join(
|
||||
sorted([pa.code for pa in modimpl.module.parcours])
|
||||
[
|
||||
pa.code
|
||||
for pa in (
|
||||
modimpl.module.parcours
|
||||
if modimpl.module.parcours
|
||||
else modimpl.formsemestre.parcours
|
||||
)
|
||||
if pa.id in formsemestre_parcours_ids
|
||||
]
|
||||
)
|
||||
|
||||
rows.append(row)
|
||||
@ -742,7 +759,7 @@ def formsemestre_description_table(
|
||||
e["publish_incomplete_str"] = "Non"
|
||||
e["_publish_incomplete_str_td_attrs"] = 'style="color: red;"'
|
||||
# Poids vers UEs (en APC)
|
||||
evaluation: Evaluation = Evaluation.query.get(e["evaluation_id"])
|
||||
evaluation: Evaluation = db.session.get(Evaluation, e["evaluation_id"])
|
||||
for ue_id, poids in evaluation.get_ue_poids_dict().items():
|
||||
e[f"ue_{ue_id}"] = poids or ""
|
||||
e[f"_ue_{ue_id}_class"] = "poids"
|
||||
@ -864,11 +881,15 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
H.append("<h4>Tous les étudiants</h4>")
|
||||
else:
|
||||
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
|
||||
partition_is_empty = True
|
||||
groups = sco_groups.get_partition_groups(partition)
|
||||
if groups:
|
||||
H.append("<table>")
|
||||
for group in groups:
|
||||
n_members = len(sco_groups.get_group_members(group["group_id"]))
|
||||
if n_members == 0:
|
||||
continue # skip empty groups
|
||||
partition_is_empty = False
|
||||
group["url_etat"] = url_for(
|
||||
"absences.EtatAbsencesGr",
|
||||
group_ids=group["group_id"],
|
||||
@ -901,13 +922,14 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
|
||||
|
||||
H.append("</tr>")
|
||||
H.append("</table>")
|
||||
else:
|
||||
H.append('<p class="help indent">Aucun groupe dans cette partition')
|
||||
if partition_is_empty:
|
||||
H.append('<p class="help indent">Aucun groupe peuplé dans cette partition')
|
||||
if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id):
|
||||
H.append(
|
||||
f""" (<a href="{url_for("scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
partition_id=partition["partition_id"])
|
||||
f""" (<a href="{url_for("scolar.partition_editor",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
edit_partition=1)
|
||||
}" class="stdlink">créer</a>)"""
|
||||
)
|
||||
H.append("</p>")
|
||||
@ -959,7 +981,7 @@ def html_expr_diagnostic(diagnostics):
|
||||
|
||||
def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None):
|
||||
"""En-tête HTML des pages "semestre" """
|
||||
sem: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
sem: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
if not sem:
|
||||
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
|
||||
formation: Formation = sem.formation
|
||||
@ -1210,7 +1232,7 @@ def formsemestre_tableau_modules(
|
||||
H = []
|
||||
prev_ue_id = None
|
||||
for modimpl in modimpls:
|
||||
mod: Module = Module.query.get(modimpl["module_id"])
|
||||
mod: Module = db.session.get(Module, modimpl["module_id"])
|
||||
moduleimpl_status_url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -31,15 +31,17 @@ import time
|
||||
|
||||
import flask
|
||||
from flask import url_for, flash, g, request
|
||||
from app.models.etudiants import Identite
|
||||
from flask_login import current_user
|
||||
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
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Formation, FormSemestre, UniteEns
|
||||
from app.models import Formation, FormSemestre, UniteEns, ScolarNews
|
||||
from app.models.notes import etud_has_notes_attente
|
||||
from app.models.validations import (
|
||||
ScolarAutorisationInscription,
|
||||
@ -65,6 +67,8 @@ from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_pv_dict
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------
|
||||
def formsemestre_validation_etud_form(
|
||||
@ -396,7 +400,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(
|
||||
@ -511,7 +515,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,
|
||||
@ -549,16 +553,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])
|
||||
@ -570,7 +576,7 @@ def formsemestre_recap_parcours_table(
|
||||
else:
|
||||
ass = ""
|
||||
|
||||
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||
formsemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if is_cur:
|
||||
type_sem = "*" # now unused
|
||||
@ -581,7 +587,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"]
|
||||
@ -645,7 +651,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:
|
||||
@ -659,8 +665,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(
|
||||
@ -685,7 +693,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>'
|
||||
@ -722,14 +730,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:
|
||||
@ -991,16 +1006,26 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
||||
)
|
||||
nb_valid += 1
|
||||
log(
|
||||
"do_formsemestre_validation_auto: %d validations, %d conflicts"
|
||||
% (nb_valid, len(conflicts))
|
||||
f"do_formsemestre_validation_auto: {nb_valid} validations, {len(conflicts)} conflicts"
|
||||
)
|
||||
H = [html_sco_header.sco_header(page_title="Saisie automatique")]
|
||||
H.append(
|
||||
"""<h2>Saisie automatique des décisions du semestre %s</h2>
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=formsemestre.id,
|
||||
text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()
|
||||
} ({nb_valid} décisions)""",
|
||||
url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
),
|
||||
)
|
||||
H = [
|
||||
f"""{html_sco_header.sco_header(page_title="Saisie automatique")}
|
||||
<h2>Saisie automatique des décisions du semestre {formsemestre.titre_annee()}</h2>
|
||||
<p>Opération effectuée.</p>
|
||||
<p>%d étudiants validés (sur %s)</p>"""
|
||||
% (sem["titreannee"], nb_valid, len(etudids))
|
||||
)
|
||||
<p>{nb_valid} étudiants validés sur {len(etudids)}</p>
|
||||
"""
|
||||
]
|
||||
if conflicts:
|
||||
H.append(
|
||||
f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
|
||||
@ -1059,64 +1084,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).
|
||||
"""
|
||||
from app.scodoc import sco_formations
|
||||
formation: Formation = formsemestre.formation
|
||||
|
||||
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"],
|
||||
# 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,
|
||||
),
|
||||
'<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>
|
||||
""",
|
||||
).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
|
||||
]
|
||||
|
||||
# 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]
|
||||
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",
|
||||
{
|
||||
@ -1127,69 +1132,185 @@ 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:",
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
ue_codes = sorted(codes_cursus.CODES_JURY_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:",
|
||||
},
|
||||
),
|
||||
(
|
||||
"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,
|
||||
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 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
|
||||
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>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
|
||||
|
||||
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")
|
||||
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 class="liste_validations">"""
|
||||
]
|
||||
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()}""")
|
||||
if (validation.formsemestre and validation.formsemestre.can_edit_jury()) or (
|
||||
current_user and current_user.has_permission(Permission.ScoEtudInscrit)
|
||||
):
|
||||
H.append(
|
||||
f"""
|
||||
<form class="inline-form">
|
||||
<button
|
||||
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
|
||||
>effacer</button>
|
||||
</form>
|
||||
""",
|
||||
)
|
||||
else:
|
||||
H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
|
||||
H.append("</li>")
|
||||
H.append("</ul></div>")
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def do_formsemestre_validate_previous_ue(
|
||||
formsemestre_id,
|
||||
formsemestre: FormSemestre,
|
||||
etudid,
|
||||
ue_id,
|
||||
moy_ue,
|
||||
@ -1202,21 +1323,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:
|
||||
if ue_coefficient is not 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,
|
||||
@ -1254,62 +1374,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
|
||||
|
@ -34,7 +34,6 @@ Optimisation possible:
|
||||
|
||||
"""
|
||||
import collections
|
||||
import operator
|
||||
import time
|
||||
|
||||
from xml.etree import ElementTree
|
||||
@ -45,15 +44,14 @@ from flask import g, request
|
||||
from flask import url_for, make_response
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
from app import db
|
||||
from app import cache, db, log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models import FormSemestre, Identite, Scolog
|
||||
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.models.groups import GroupDescr, Partition, group_membership
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log, cache
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
@ -94,7 +92,7 @@ groupEditor = ndb.EditableTable(
|
||||
group_list = groupEditor.list
|
||||
|
||||
|
||||
def get_group(group_id: int) -> dict:
|
||||
def get_group(group_id: int) -> dict: # OBSOLETE !
|
||||
"""Returns group object, with partition"""
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
|
||||
@ -124,7 +122,7 @@ def group_delete(group_id: int):
|
||||
)
|
||||
|
||||
|
||||
def get_partition(partition_id):
|
||||
def get_partition(partition_id): # OBSOLETE
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT p.id AS partition_id, p.*
|
||||
FROM partition p
|
||||
@ -200,7 +198,7 @@ def get_formsemestre_etuds_groups(formsemestre_id: int) -> dict:
|
||||
return d
|
||||
|
||||
|
||||
def get_partition_groups(partition):
|
||||
def get_partition_groups(partition): # OBSOLETE !
|
||||
"""List of groups in this partition (list of dicts).
|
||||
Some groups may be empty."""
|
||||
return ndb.SimpleDictFetch(
|
||||
@ -243,7 +241,7 @@ def get_default_group(formsemestre_id, fix_if_missing=False):
|
||||
return group.id
|
||||
# debug check
|
||||
if len(r) != 1:
|
||||
raise ScoException(f"invalid group structure for {formsemestre_id}")
|
||||
log(f"invalid group structure for {formsemestre_id}: {len(r)}")
|
||||
group_id = r[0]["group_id"]
|
||||
return group_id
|
||||
|
||||
@ -452,7 +450,7 @@ def get_etud_formsemestre_groups(
|
||||
),
|
||||
{"etudid": etud.id, "formsemestre_id": formsemestre.id},
|
||||
)
|
||||
return [GroupDescr.query.get(group_id) for group_id in cursor]
|
||||
return [db.session.get(GroupDescr, group_id) for group_id in cursor]
|
||||
|
||||
|
||||
# Ancienne fonction:
|
||||
@ -562,10 +560,10 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
x_group = Element(
|
||||
"group",
|
||||
partition_id=str(partition_id),
|
||||
partition_name=partition["partition_name"],
|
||||
partition_name=partition["partition_name"] or "",
|
||||
groups_editable=str(int(partition["groups_editable"])),
|
||||
group_id=str(group["group_id"]),
|
||||
group_name=group["group_name"],
|
||||
group_name=group["group_name"] or "",
|
||||
)
|
||||
x_response.append(x_group)
|
||||
for e in get_group_members(group["group_id"]):
|
||||
@ -574,10 +572,10 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
Element(
|
||||
"etud",
|
||||
etudid=str(e["etudid"]),
|
||||
civilite=etud["civilite_str"],
|
||||
sexe=etud["civilite_str"], # compat
|
||||
nom=sco_etud.format_nom(etud["nom"]),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"]),
|
||||
civilite=etud["civilite_str"] or "",
|
||||
sexe=etud["civilite_str"] or "", # compat
|
||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
||||
origin=_comp_etud_origin(etud, formsemestre),
|
||||
)
|
||||
)
|
||||
@ -589,7 +587,7 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
x_group = Element(
|
||||
"group",
|
||||
partition_id=str(partition_id),
|
||||
partition_name=partition["partition_name"],
|
||||
partition_name=partition["partition_name"] or "",
|
||||
groups_editable=str(int(partition["groups_editable"])),
|
||||
group_id="_none_",
|
||||
group_name="",
|
||||
@ -601,9 +599,9 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
Element(
|
||||
"etud",
|
||||
etudid=str(etud["etudid"]),
|
||||
sexe=etud["civilite_str"],
|
||||
nom=sco_etud.format_nom(etud["nom"]),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"]),
|
||||
sexe=etud["civilite_str"] or "",
|
||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
||||
origin=_comp_etud_origin(etud, formsemestre),
|
||||
)
|
||||
)
|
||||
@ -637,7 +635,7 @@ def _comp_etud_origin(etud: dict, cur_formsemestre: FormSemestre):
|
||||
return "" # parcours normal, ne le signale pas
|
||||
|
||||
|
||||
def set_group(etudid: int, group_id: int) -> bool:
|
||||
def set_group(etudid: int, group_id: int) -> bool: # OBSOLETE !
|
||||
"""Inscrit l'étudiant au groupe.
|
||||
Return True if ok, False si deja inscrit.
|
||||
Warning:
|
||||
@ -664,55 +662,33 @@ def set_group(etudid: int, group_id: int) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def change_etud_group_in_partition(etudid: int, group_id: int, partition: dict = None):
|
||||
"""Inscrit etud au groupe de cette partition,
|
||||
et le desinscrit d'autres groupes de cette partition.
|
||||
def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> bool:
|
||||
"""Inscrit etud au groupe
|
||||
(et le désinscrit d'autres groupes de cette partition)
|
||||
Return True si changement, False s'il était déjà dans ce groupe.
|
||||
"""
|
||||
log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id))
|
||||
# 0- La partition
|
||||
group = get_group(group_id)
|
||||
if partition:
|
||||
# verifie que le groupe est bien dans cette partition:
|
||||
if group["partition_id"] != partition["partition_id"]:
|
||||
raise ValueError(
|
||||
"inconsistent group/partition (group_id=%s, partition_id=%s)"
|
||||
% (group_id, partition["partition_id"])
|
||||
)
|
||||
else:
|
||||
partition = get_partition(group["partition_id"])
|
||||
# 1- Supprime membership dans cette partition
|
||||
ndb.SimpleQuery(
|
||||
"""DELETE FROM group_membership gm
|
||||
WHERE EXISTS
|
||||
(SELECT 1 FROM group_descr gd
|
||||
WHERE gm.etudid = %(etudid)s
|
||||
AND gm.group_id = gd.id
|
||||
AND gd.partition_id = %(partition_id)s)
|
||||
""",
|
||||
{"etudid": etudid, "partition_id": partition["partition_id"]},
|
||||
)
|
||||
# 2- associe au nouveau groupe
|
||||
set_group(etudid, group_id)
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
if not group.partition.set_etud_group(etud, group):
|
||||
return # pas de changement
|
||||
|
||||
# 3- log
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
cnx = ndb.GetDBConnexion()
|
||||
logdb(
|
||||
cnx,
|
||||
# - log
|
||||
formsemestre: FormSemestre = group.partition.formsemestre
|
||||
log(f"change_etud_group_in_partition: etudid={etudid} group={group}")
|
||||
Scolog.logdb(
|
||||
method="changeGroup",
|
||||
etudid=etudid,
|
||||
msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
|
||||
% (formsemestre_id, partition["partition_name"], group["group_name"]),
|
||||
msg=f"""formsemestre_id={formsemestre.id}, partition_name={
|
||||
group.partition.partition_name or ""}, group_name={group.group_name or ""}""",
|
||||
commit=True,
|
||||
)
|
||||
cnx.commit()
|
||||
|
||||
# 5- Update parcours
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre.update_inscriptions_parcours_from_groups()
|
||||
# - Update parcours
|
||||
if group.partition.partition_name == scu.PARTITION_PARCOURS:
|
||||
formsemestre.update_inscriptions_parcours_from_groups()
|
||||
|
||||
# 6- invalidate cache
|
||||
# - invalidate cache
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=formsemestre_id
|
||||
formsemestre_id=formsemestre.id
|
||||
) # > change etud group
|
||||
|
||||
|
||||
@ -729,7 +705,6 @@ def setGroups(
|
||||
|
||||
Ne peux pas modifier les groupes des partitions non éditables.
|
||||
"""
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
||||
def xml_error(msg, code=404):
|
||||
data = (
|
||||
@ -739,26 +714,27 @@ def setGroups(
|
||||
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
||||
return response
|
||||
|
||||
partition = get_partition(partition_id)
|
||||
if not partition["groups_editable"] and (groupsToCreate or groupsToDelete):
|
||||
partition: Partition = db.session.get(Partition, partition_id)
|
||||
if not partition.groups_editable and (groupsToCreate or groupsToDelete):
|
||||
msg = "setGroups: partition non editable"
|
||||
log(msg)
|
||||
return xml_error(msg, code=403)
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||
|
||||
if not sco_permissions_check.can_change_groups(partition.formsemestre.id):
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
log("***setGroups: partition_id=%s" % partition_id)
|
||||
log("groupsLists=%s" % groupsLists)
|
||||
log("groupsToCreate=%s" % groupsToCreate)
|
||||
log("groupsToDelete=%s" % groupsToDelete)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
if not sem["etat"]:
|
||||
|
||||
if not partition.formsemestre.etat:
|
||||
raise AccessDenied("Modification impossible: semestre verrouillé")
|
||||
|
||||
groupsToDelete = [g for g in groupsToDelete.split(";") if g]
|
||||
|
||||
etud_groups = formsemestre_get_etud_groupnames(formsemestre_id, attr="group_id")
|
||||
etud_groups = formsemestre_get_etud_groupnames(
|
||||
partition.formsemestre.id, attr="group_id"
|
||||
)
|
||||
for line in groupsLists.split("\n"): # for each group_id (one per line)
|
||||
fs = line.split(";")
|
||||
group_id = fs[0].strip()
|
||||
@ -769,26 +745,23 @@ def setGroups(
|
||||
except ValueError:
|
||||
log(f"setGroups: ignoring invalid group_id={group_id}")
|
||||
continue
|
||||
group = get_group(group_id)
|
||||
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
|
||||
# Anciens membres du groupe:
|
||||
old_members = get_group_members(group_id)
|
||||
old_members_set = set([x["etudid"] for x in old_members])
|
||||
old_members_set = {etud.id for etud in group.etuds}
|
||||
# Place dans ce groupe les etudiants indiqués:
|
||||
for etudid_str in fs[1:-1]:
|
||||
etudid = int(etudid_str)
|
||||
if etudid in old_members_set:
|
||||
old_members_set.remove(
|
||||
etudid
|
||||
) # a nouveau dans ce groupe, pas besoin de l'enlever
|
||||
# était dans ce groupe, l'enlever
|
||||
old_members_set.remove(etudid)
|
||||
if (etudid not in etud_groups) or (
|
||||
group_id != etud_groups[etudid].get(partition_id, "")
|
||||
): # pas le meme groupe qu'actuel
|
||||
change_etud_group_in_partition(etudid, group_id, partition)
|
||||
change_etud_group_in_partition(etudid, group)
|
||||
# Retire les anciens membres:
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
for etudid in old_members_set:
|
||||
log("removing %s from group %s" % (etudid, group_id))
|
||||
ndb.SimpleQuery(
|
||||
"DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s",
|
||||
{"etudid": etudid, "group_id": group_id},
|
||||
@ -798,8 +771,8 @@ def setGroups(
|
||||
cnx,
|
||||
method="removeFromGroup",
|
||||
etudid=etudid,
|
||||
msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
|
||||
% (formsemestre_id, partition["partition_name"], group["group_name"]),
|
||||
msg=f"""formsemestre_id={partition.formsemestre.id},partition_name={
|
||||
partition.partition_name}, group_name={group.group_name}""",
|
||||
)
|
||||
|
||||
# Supprime les groupes indiqués comme supprimés:
|
||||
@ -819,10 +792,10 @@ def setGroups(
|
||||
return xml_error(msg, code=404)
|
||||
# Place dans ce groupe les etudiants indiqués:
|
||||
for etudid in fs[1:-1]:
|
||||
change_etud_group_in_partition(etudid, group.id, partition)
|
||||
change_etud_group_in_partition(etudid, group)
|
||||
|
||||
# Update parcours
|
||||
formsemestre.update_inscriptions_parcours_from_groups()
|
||||
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
||||
|
||||
data = (
|
||||
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
|
||||
@ -835,6 +808,7 @@ def setGroups(
|
||||
def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
||||
"""Create a new group in this partition.
|
||||
If default, create default partition (with no name)
|
||||
Obsolete: utiliser Partition.create_group
|
||||
"""
|
||||
partition = Partition.query.get_or_404(partition_id)
|
||||
if not sco_permissions_check.can_change_groups(partition.formsemestre_id):
|
||||
@ -856,7 +830,7 @@ def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
||||
group = GroupDescr(partition=partition, group_name=group_name, numero=new_numero)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log("create_group: created group_id={group.id}")
|
||||
log(f"create_group: created group_id={group.id}")
|
||||
#
|
||||
return group
|
||||
|
||||
@ -976,10 +950,20 @@ def edit_partition_form(formsemestre_id=None):
|
||||
}
|
||||
</script>
|
||||
""",
|
||||
r"""<h2>Partitions du semestre</h2>
|
||||
f"""<h2>Partitions du semestre</h2>
|
||||
<p class="help">
|
||||
👉💡 vous pourriez essayer <a href="{
|
||||
url_for("scolar.partition_editor",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||
}" class="stdlink">le nouvel éditeur</a>
|
||||
</p>
|
||||
|
||||
<form name="editpart" id="editpart" method="POST" action="partition_create">
|
||||
<div id="epmsg"></div>
|
||||
<table><tr class="eptit"><th></th><th></th><th></th><th>Partition</th><th>Groupes</th><th></th><th></th><th></th></tr>
|
||||
<table>
|
||||
<tr class="eptit">
|
||||
<th></th><th></th><th></th><th>Partition</th><th>Groupes</th><th></th><th></th><th></th>
|
||||
</tr>
|
||||
""",
|
||||
]
|
||||
i = 0
|
||||
@ -1400,14 +1384,16 @@ def groups_auto_repartition(partition_id=None):
|
||||
"""Reparti les etudiants dans des groupes dans une partition, en respectant le niveau
|
||||
et la mixité.
|
||||
"""
|
||||
partition = get_partition(partition_id)
|
||||
if not partition["groups_editable"]:
|
||||
partition: Partition = Partition.query.get_or_404(partition_id)
|
||||
if not partition.groups_editable:
|
||||
raise AccessDenied("Partition non éditable")
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# renvoie sur page édition groupes
|
||||
formsemestre_id = partition.formsemestre_id
|
||||
formsemestre = partition.formsemestre
|
||||
# renvoie sur page édition partitions et groupes
|
||||
dest_url = url_for(
|
||||
"scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id
|
||||
"scolar.partition_editor",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
@ -1427,12 +1413,14 @@ def groups_auto_repartition(partition_id=None):
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Répartition des groupes"),
|
||||
"<h2>Répartition des groupes de %s</h2>" % partition["partition_name"],
|
||||
f"<p>Semestre {formsemestre.titre_annee()}</p>",
|
||||
"""<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
|
||||
f"""<h2>Répartition des groupes de {partition.partition_name}</h2>
|
||||
<p>Semestre {formsemestre.titre_annee()}</p>
|
||||
<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
|
||||
ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau
|
||||
des groupes (en utilisant la dernière moyenne générale disponible pour
|
||||
chaque étudiant) et de maximiser la mixité de chaque groupe.</p>""",
|
||||
chaque étudiant) et de maximiser la mixité de chaque groupe.
|
||||
</p>
|
||||
""",
|
||||
]
|
||||
|
||||
tf = TrivialFormulator(
|
||||
@ -1450,25 +1438,23 @@ def groups_auto_repartition(partition_id=None):
|
||||
return flask.redirect(dest_url)
|
||||
else:
|
||||
# form submission
|
||||
log(
|
||||
"groups_auto_repartition( partition_id=%s partition_name=%s"
|
||||
% (partition_id, partition["partition_name"])
|
||||
)
|
||||
groupNames = tf[2]["groupNames"]
|
||||
group_names = sorted(set([x.strip() for x in groupNames.split(",")]))
|
||||
log(f"groups_auto_repartition({partition})")
|
||||
group_names = tf[2]["groupNames"]
|
||||
group_names = sorted({x.strip() for x in group_names.split(",")})
|
||||
# Détruit les groupes existant de cette partition
|
||||
for old_group in get_partition_groups(partition):
|
||||
group_delete(old_group["group_id"])
|
||||
for group in partition.groups:
|
||||
db.session.delete(group)
|
||||
db.session.commit()
|
||||
# Crée les nouveaux groupes
|
||||
group_ids = []
|
||||
groups = []
|
||||
for group_name in group_names:
|
||||
if group_name.strip():
|
||||
group_ids.append(create_group(partition_id, group_name).id)
|
||||
groups.append(partition.create_group(group_name))
|
||||
#
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
identdict = nt.identdict
|
||||
# build: { civilite : liste etudids trie par niveau croissant }
|
||||
civilites = set([x["civilite"] for x in identdict.values()])
|
||||
civilites = {x["civilite"] for x in identdict.values()}
|
||||
listes = {}
|
||||
for civilite in civilites:
|
||||
listes[civilite] = [
|
||||
@ -1481,16 +1467,19 @@ def groups_auto_repartition(partition_id=None):
|
||||
# affect aux groupes:
|
||||
n = len(identdict)
|
||||
igroup = 0
|
||||
nbgroups = len(group_ids)
|
||||
nbgroups = len(groups)
|
||||
while n > 0:
|
||||
log(f"n={n}")
|
||||
for civilite in civilites:
|
||||
log(f"civilite={civilite}")
|
||||
if len(listes[civilite]):
|
||||
n -= 1
|
||||
etudid = listes[civilite].pop()[1]
|
||||
group_id = group_ids[igroup]
|
||||
group = groups[igroup]
|
||||
igroup = (igroup + 1) % nbgroups
|
||||
change_etud_group_in_partition(etudid, group_id, partition)
|
||||
log("%s in group %s" % (etudid, group_id))
|
||||
log(f"in {etudid} in group {group.id}")
|
||||
change_etud_group_in_partition(etudid, group)
|
||||
log(f"{etudid} in group {group.id}")
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
@ -1498,15 +1487,13 @@ def _get_prev_moy(etudid, formsemestre_id):
|
||||
"""Donne la derniere moyenne generale calculee pour cette étudiant,
|
||||
ou 0 si on n'en trouve pas (nouvel inscrit,...).
|
||||
"""
|
||||
from app.scodoc import sco_cursus_dut
|
||||
|
||||
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||
if not info:
|
||||
raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
|
||||
etud = info[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
if Se.prev:
|
||||
prev_sem = FormSemestre.query.get(Se.prev["formsemestre_id"])
|
||||
prev_sem = db.session.get(FormSemestre, Se.prev["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
|
||||
return nt.get_etud_moy_gen(etudid)
|
||||
else:
|
||||
@ -1520,10 +1507,11 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
||||
Si la partition existe déjà, ses groupes sont mis à jour (les groupes devenant
|
||||
vides ne sont pas supprimés).
|
||||
"""
|
||||
# A RE-ECRIRE pour utiliser les modèles.
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
|
||||
partition_name = str(partition_name)
|
||||
log("create_etapes_partition(%s)" % formsemestre_id)
|
||||
log(f"create_etapes_partition({formsemestre_id})")
|
||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
args={"formsemestre_id": formsemestre_id}
|
||||
)
|
||||
@ -1542,20 +1530,17 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
||||
pid = partition_create(
|
||||
formsemestre_id, partition_name=partition_name, redirect=False
|
||||
)
|
||||
partition = get_partition(pid)
|
||||
groups = get_partition_groups(partition)
|
||||
groups_by_names = {g["group_name"]: g for g in groups}
|
||||
partition: Partition = db.session.get(Partition, pid)
|
||||
groups = partition.groups
|
||||
groups_by_names = {g.group_name: g for g in groups}
|
||||
for etape in etapes:
|
||||
if not (etape in groups_by_names):
|
||||
if etape not in groups_by_names:
|
||||
new_group = create_group(pid, etape)
|
||||
g = get_group(new_group.id) # XXX transition: recupere old style dict
|
||||
groups_by_names[etape] = g
|
||||
groups_by_names[etape] = new_group
|
||||
# Place les etudiants dans les groupes
|
||||
for i in ins:
|
||||
if i["etape"]:
|
||||
change_etud_group_in_partition(
|
||||
i["etudid"], groups_by_names[i["etape"]]["group_id"], partition
|
||||
)
|
||||
change_etud_group_in_partition(i["etudid"], groups_by_names[i["etape"]])
|
||||
|
||||
|
||||
def do_evaluation_listeetuds_groups(
|
||||
|
@ -36,16 +36,12 @@ import time
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.models import ScolarNews, GroupDescr
|
||||
from app.models.etudiants import input_civilite
|
||||
from app.scodoc.sco_excel import COLORS
|
||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||
do_formsemestre_inscription_with_modules,
|
||||
)
|
||||
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_excel import COLORS
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
ScoFormatError,
|
||||
@ -55,7 +51,6 @@ from app.scodoc.sco_exceptions import (
|
||||
ScoLockedFormError,
|
||||
ScoGenError,
|
||||
)
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
@ -63,6 +58,11 @@ from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_preferences
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||
do_formsemestre_inscription_with_modules,
|
||||
)
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# format description (in tools/)
|
||||
FORMAT_FILE = "format_import_etudiants.txt"
|
||||
@ -480,6 +480,7 @@ def scolars_import_excel_file(
|
||||
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
|
||||
% len(created_etudids),
|
||||
obj=formsemestre_id,
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
log("scolars_import_excel_file: completing transaction")
|
||||
@ -638,10 +639,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
||||
fields = adm_get_fields(titles, formsemestre_id)
|
||||
idx_nom = None
|
||||
idx_prenom = None
|
||||
for idx in fields:
|
||||
if fields[idx][0] == "nom":
|
||||
for idx, field in fields.items():
|
||||
if field[0] == "nom":
|
||||
idx_nom = idx
|
||||
if fields[idx][0] == "prenom":
|
||||
if field[0] == "prenom":
|
||||
idx_prenom = idx
|
||||
if (idx_nom is None) or (idx_prenom is None):
|
||||
log("fields indices=" + ", ".join([str(x) for x in fields]))
|
||||
@ -663,21 +664,20 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
||||
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
|
||||
nom = adm_normalize_string(line[idx_nom])
|
||||
prenom = adm_normalize_string(line[idx_prenom])
|
||||
if not (nom, prenom) in etuds_by_nomprenom:
|
||||
log(
|
||||
"unable to find %s %s among members" % (line[idx_nom], line[idx_prenom])
|
||||
)
|
||||
if (nom, prenom) not in etuds_by_nomprenom:
|
||||
msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>"""
|
||||
diag.append(msg)
|
||||
else:
|
||||
etud = etuds_by_nomprenom[(nom, prenom)]
|
||||
cur_adm = sco_etud.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
|
||||
# peuple les champs presents dans le tableau
|
||||
args = {}
|
||||
for idx in fields:
|
||||
field_name, convertor = fields[idx]
|
||||
for idx, field in fields.items():
|
||||
field_name, convertor = field
|
||||
if field_name in modifiable_fields:
|
||||
try:
|
||||
val = convertor(line[idx])
|
||||
except ValueError:
|
||||
except ValueError as exc:
|
||||
raise ScoFormatError(
|
||||
'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
|
||||
% (nline, field_name, line[idx]),
|
||||
@ -686,7 +686,7 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
),
|
||||
)
|
||||
) from exc
|
||||
if val is not None: # note: ne peut jamais supprimer une valeur
|
||||
args[field_name] = val
|
||||
if args:
|
||||
@ -719,10 +719,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
||||
)
|
||||
|
||||
for group_id in group_ids:
|
||||
group = GroupDescr.query.get(group_id)
|
||||
group = db.session.get(GroupDescr, group_id)
|
||||
if group.partition.groups_editable:
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
args["etudid"], group_id
|
||||
args["etudid"], group
|
||||
)
|
||||
else:
|
||||
log("scolars_import_admission: partition non editable")
|
||||
|
@ -35,14 +35,13 @@ from flask import url_for, g, request
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import Formation, FormSemestre
|
||||
from app import db, log
|
||||
from app.models import Formation, FormSemestre, GroupDescr
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
@ -177,7 +176,8 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
(la liste doit avoir été vérifiée au préalable)
|
||||
En option: inscrit aux mêmes groupes que dans le semestre origine
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||
# TODO à ré-écrire pour utiliser le smodèle, notamment GroupDescr
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
formsemestre.setup_parcours_groups()
|
||||
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
|
||||
for etudid in etudids:
|
||||
@ -220,11 +220,10 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
||||
|
||||
# Inscrit aux groupes
|
||||
for partition_group in partition_groups:
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid,
|
||||
partition_group["group_id"],
|
||||
partition_group,
|
||||
group: GroupDescr = db.session.get(
|
||||
GroupDescr, partition_group["group_id"]
|
||||
)
|
||||
sco_groups.change_etud_group_in_partition(etudid, group)
|
||||
|
||||
|
||||
def do_desinscrit(sem, etudids):
|
||||
@ -416,10 +415,10 @@ def formsemestre_inscr_passage(
|
||||
): # il y a au moins une vraie partition
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"])
|
||||
}">Répartir les groupes de {partition["partition_name"]}</a></li>
|
||||
"""
|
||||
url_for("scolar.partition_editor", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id)
|
||||
}">Répartir les groupes de {partition["partition_name"]}</a></li>
|
||||
"""
|
||||
)
|
||||
|
||||
#
|
||||
@ -436,7 +435,7 @@ def _build_page(
|
||||
inscrit_groupes=False,
|
||||
ignore_jury=False,
|
||||
):
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
inscrit_groupes = int(inscrit_groupes)
|
||||
ignore_jury = int(ignore_jury)
|
||||
if inscrit_groupes:
|
||||
|
@ -33,7 +33,7 @@ import numpy as np
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app import models
|
||||
from app.comp import res_sem
|
||||
from app.comp import moy_mod
|
||||
@ -79,7 +79,7 @@ def do_evaluation_listenotes(
|
||||
return "<p>Aucune évaluation !</p>", "ScoDoc"
|
||||
|
||||
E = evals[0] # il y a au moins une evaluation
|
||||
modimpl = ModuleImpl.query.get(E["moduleimpl_id"])
|
||||
modimpl = db.session.get(ModuleImpl, E["moduleimpl_id"])
|
||||
# description de l'evaluation
|
||||
if mode == "eval":
|
||||
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
|
||||
@ -624,7 +624,7 @@ def _make_table_notes(
|
||||
]
|
||||
commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
|
||||
commentkeys.sort(key=lambda x: int(x[1]))
|
||||
for (comment, key) in commentkeys:
|
||||
for comment, key in commentkeys:
|
||||
C.append(
|
||||
'<span class="colcomment">(%s)</span> <em>%s</em><br>' % (key, comment)
|
||||
)
|
||||
@ -673,7 +673,7 @@ def _add_eval_columns(
|
||||
sum_notes = 0
|
||||
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
||||
evaluation_id = e["evaluation_id"]
|
||||
e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture
|
||||
e_o = db.session.get(Evaluation, evaluation_id) # XXX en attendant ré-écriture
|
||||
inscrits = e_o.moduleimpl.formsemestre.etudids_actifs # set d'etudids
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
|
||||
|
@ -31,15 +31,16 @@
|
||||
from flask_login import current_user
|
||||
import psycopg2
|
||||
|
||||
from app import db
|
||||
|
||||
from app.models import Formation
|
||||
from app.scodoc import scolog
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_cache
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
|
||||
from app import log
|
||||
from app import models
|
||||
from app.scodoc import scolog
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
# --- Gestion des "Implémentations de Modules"
|
||||
# Un "moduleimpl" correspond a la mise en oeuvre d'un module
|
||||
@ -170,7 +171,7 @@ def moduleimpl_withmodule_list(
|
||||
mi["matiere"] = matieres[matiere_id]
|
||||
|
||||
mod = modimpls[0]["module"]
|
||||
formation = models.Formation.query.get(mod["formation_id"])
|
||||
formation = db.session.get(Formation, mod["formation_id"])
|
||||
|
||||
if formation.is_apc():
|
||||
# tri par numero_module
|
||||
|
@ -28,12 +28,13 @@
|
||||
"""Opérations d'inscriptions aux modules (interface pour gérer options ou parcours)
|
||||
"""
|
||||
import collections
|
||||
from operator import itemgetter
|
||||
from operator import attrgetter
|
||||
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db, log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
@ -43,9 +44,6 @@ from app.models import (
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
)
|
||||
|
||||
from app import log
|
||||
from app.tables import list_etuds
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
@ -62,6 +60,7 @@ import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.tables import list_etuds
|
||||
|
||||
|
||||
def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
@ -520,7 +519,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
else set()
|
||||
)
|
||||
ues = sorted(
|
||||
(UniteEns.query.get(ue_id) for ue_id in ue_ids),
|
||||
(db.session.get(UniteEns, ue_id) for ue_id in ue_ids),
|
||||
key=lambda u: (u.numero or 0, u.acronyme),
|
||||
)
|
||||
H.append(
|
||||
@ -553,8 +552,11 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
>{etud.nomprenom}</a></td>"""
|
||||
)
|
||||
# Parcours:
|
||||
group = partition_parcours.get_etud_group(etud.id)
|
||||
parcours_name = group.group_name if group else ""
|
||||
if partition_parcours:
|
||||
group = partition_parcours.get_etud_group(etud.id)
|
||||
parcours_name = group.group_name if group else ""
|
||||
else:
|
||||
parcours_name = ""
|
||||
H.append(f"""<td class="parcours">{parcours_name}</td>""")
|
||||
# UEs:
|
||||
for ue in ues:
|
||||
@ -578,7 +580,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
.all()
|
||||
)
|
||||
validations_ue.sort(
|
||||
key=lambda v: codes_cursus.BUT_CODES_ORDERED.get(v.code, 0)
|
||||
key=lambda v: codes_cursus.BUT_CODES_ORDER.get(v.code, 0)
|
||||
)
|
||||
validation = validations_ue[-1] if validations_ue else None
|
||||
expl_validation = (
|
||||
@ -668,7 +670,7 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
|
||||
gr.append((partition["partition_name"], grp))
|
||||
#
|
||||
d = []
|
||||
for (partition_name, grp) in gr:
|
||||
for partition_name, grp in gr:
|
||||
if grp:
|
||||
d.append("groupes de %s: %s" % (partition_name, ", ".join(grp)))
|
||||
r = []
|
||||
@ -680,25 +682,25 @@ def descr_inscrs_module(moduleimpl_id, set_all, partitions):
|
||||
return False, len(ins), " et ".join(r)
|
||||
|
||||
|
||||
def _fmt_etud_set(ins, max_list_size=7):
|
||||
def _fmt_etud_set(etudids, max_list_size=7) -> str:
|
||||
# max_list_size est le nombre max de noms d'etudiants listés
|
||||
# au delà, on indique juste le nombre, sans les noms.
|
||||
if len(ins) > max_list_size:
|
||||
return "%d étudiants" % len(ins)
|
||||
if len(etudids) > max_list_size:
|
||||
return f"{len(etudids)} étudiants"
|
||||
etuds = []
|
||||
for etudid in ins:
|
||||
etuds.append(sco_etud.get_etud_info(etudid=etudid, filled=True)[0])
|
||||
etuds.sort(key=itemgetter("nom"))
|
||||
for etudid in etudids:
|
||||
etud = db.session.get(Identite, etudid)
|
||||
if etud:
|
||||
etuds.append(etud)
|
||||
|
||||
return ", ".join(
|
||||
[
|
||||
'<a class="discretelink" href="%s">%s</a>'
|
||||
% (
|
||||
f"""<a class="discretelink" href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
),
|
||||
etud["nomprenom"],
|
||||
)
|
||||
for etud in etuds
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
|
||||
)
|
||||
}">{etud.nomprenom}</a>"""
|
||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -57,6 +57,7 @@ from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.tables import list_etuds
|
||||
|
||||
|
||||
# menu evaluation dans moduleimpl
|
||||
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
||||
"Menu avec actions sur une evaluation"
|
||||
@ -226,7 +227,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
)
|
||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||
#
|
||||
module_resp = User.query.get(modimpl.responsable_id)
|
||||
module_resp = db.session.get(User, modimpl.responsable_id)
|
||||
mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type]
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
@ -528,7 +529,7 @@ def _ligne_evaluation(
|
||||
) -> str:
|
||||
"""Ligne <tr> décrivant une évaluation dans le tableau de bord moduleimpl."""
|
||||
H = []
|
||||
# evaluation: Evaluation = Evaluation.query.get(eval_dict["evaluation_id"])
|
||||
# evaluation: Evaluation = db.session.get(Evaluation, eval_dict["evaluation_id"])
|
||||
etat = sco_evaluations.do_evaluation_etat(
|
||||
evaluation.id,
|
||||
partition_id=partition_id,
|
||||
@ -732,7 +733,7 @@ def _ligne_evaluation(
|
||||
)
|
||||
if etat["moy"]:
|
||||
H.append(
|
||||
f"""<b>{etat["moy"]} / {evaluation.note_max:g}</b>
|
||||
f"""<b>{etat["moy"]} / 20</b>
|
||||
(<a class="stdlink" href="{
|
||||
url_for('notes.evaluation_listenotes',
|
||||
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id)
|
||||
@ -837,7 +838,7 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
|
||||
"></div>
|
||||
</div>"""
|
||||
for ue, poids in (
|
||||
(UniteEns.query.get(ue_id), poids)
|
||||
(db.session.get(UniteEns, ue_id), poids)
|
||||
for ue_id, poids in ue_poids.items()
|
||||
)
|
||||
]
|
||||
|
@ -33,10 +33,8 @@
|
||||
from flask import abort, url_for, g, render_template, request
|
||||
from flask_login import current_user
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.but import cursus_but, jury_but_view
|
||||
from app import db, log
|
||||
from app.but import cursus_but
|
||||
from app.models.etudiants import Identite, make_etud_args
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import html_sco_header
|
||||
@ -57,13 +55,17 @@ from app.scodoc.sco_bulletins import etud_descr_situation_semestre
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
|
||||
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,10 @@ def ficheEtud(etudid=None):
|
||||
info["last_formsemestre_id"] = ""
|
||||
sem_info = {}
|
||||
for sem in info["sems"]:
|
||||
formsemestre: FormSemestre = db.session.get(
|
||||
FormSemestre, 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 +293,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>"
|
||||
@ -303,16 +313,39 @@ def ficheEtud(etudid=None):
|
||||
)
|
||||
info[
|
||||
"link_bul_pdf"
|
||||
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">tous les bulletins</a></span>"""
|
||||
] = f"""
|
||||
<span class="link_bul_pdf">
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Tous les bulletins</a>
|
||||
</span>
|
||||
"""
|
||||
last_formsemestre: FormSemestre = db.session.get(
|
||||
FormSemestre, info["sems"][0]["formsemestre_id"]
|
||||
)
|
||||
if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2:
|
||||
info[
|
||||
"link_bul_pdf"
|
||||
] += f"""
|
||||
<span class="link_bul_pdf">
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
|
||||
}">Visualiser les compétences BUT</a>
|
||||
</span>
|
||||
"""
|
||||
if authuser.has_permission(Permission.ScoEtudInscrit):
|
||||
info[
|
||||
"link_inscrire_ailleurs"
|
||||
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_inscription_with_modules_form",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">inscrire à un autre semestre</a></span>"""
|
||||
}">Inscrire à un autre semestre</a></span>
|
||||
<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.jury_delete_manual",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Éditer toutes décisions de jury</a></span>
|
||||
"""
|
||||
|
||||
else:
|
||||
info["link_inscrire_ailleurs"] = ""
|
||||
else:
|
||||
@ -337,17 +370,18 @@ def ficheEtud(etudid=None):
|
||||
if not sco_permissions_check.can_suppress_annotation(a["id"]):
|
||||
a["dellink"] = ""
|
||||
else:
|
||||
a[
|
||||
"dellink"
|
||||
] = '<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>' % (
|
||||
etudid,
|
||||
a["id"],
|
||||
scu.icontag(
|
||||
"delete_img",
|
||||
border="0",
|
||||
alt="suppress",
|
||||
title="Supprimer cette annotation",
|
||||
),
|
||||
a["dellink"] = (
|
||||
'<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>'
|
||||
% (
|
||||
etudid,
|
||||
a["id"],
|
||||
scu.icontag(
|
||||
"delete_img",
|
||||
border="0",
|
||||
alt="suppress",
|
||||
title="Supprimer cette annotation",
|
||||
),
|
||||
)
|
||||
)
|
||||
author = sco_users.user_info(a["author"])
|
||||
alist.append(
|
||||
@ -446,7 +480,7 @@ def ficheEtud(etudid=None):
|
||||
info[
|
||||
"inscriptions_mkup"
|
||||
] = f"""<div class="ficheinscriptions" id="ficheinscriptions">
|
||||
<div class="fichetitre">Parcours</div>{info["liste_inscriptions"]}
|
||||
<div class="fichetitre">Cursus</div>{info["liste_inscriptions"]}
|
||||
{info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]}
|
||||
</div>"""
|
||||
|
||||
@ -474,11 +508,26 @@ def ficheEtud(etudid=None):
|
||||
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"])
|
||||
if last_sem.formation.is_apc():
|
||||
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
|
||||
info["but_cursus_mkup"] = render_template(
|
||||
"but/cursus_etud.j2",
|
||||
cursus=but_cursus,
|
||||
scu=scu,
|
||||
)
|
||||
info[
|
||||
"but_cursus_mkup"
|
||||
] = f"""
|
||||
<div class="section_but">
|
||||
{render_template(
|
||||
"but/cursus_etud.j2",
|
||||
cursus=but_cursus,
|
||||
scu=scu,
|
||||
)}
|
||||
<div class="link_validation_rcues">
|
||||
<a href="{url_for("notes.validation_rcues",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||
formsemestre_id=last_formsemestre.id)}"
|
||||
title="Visualiser les compétences BUT"
|
||||
>
|
||||
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
|
||||
<div class="ficheEtud" id="ficheEtud"><table>
|
||||
|
@ -84,6 +84,8 @@ def SU(s: str) -> str:
|
||||
s = html.unescape(s)
|
||||
# Remplace les <br> par des <br/>
|
||||
s = re.sub(r"<br\s*>", "<br/>", s)
|
||||
# And substitute unicode characters not supported by ReportLab
|
||||
s = s.replace("‐", "-")
|
||||
return s
|
||||
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
from flask import g
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -131,7 +132,10 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl
|
||||
"<h2>Opération non autorisée pour %s</h2>" % current_user,
|
||||
"<p>Responsable de ce semestre : <b>%s</b></p>"
|
||||
% ", ".join(
|
||||
[User.query.get(i).get_prenomnom() for i in sem["responsables"]]
|
||||
[
|
||||
db.session.get(User, i).get_prenomnom()
|
||||
for i in sem["responsables"]
|
||||
]
|
||||
),
|
||||
footer,
|
||||
]
|
||||
@ -142,7 +146,9 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl
|
||||
|
||||
|
||||
def can_change_groups(formsemestre_id: int) -> bool:
|
||||
"Vrai si l'utilisateur peut changer les groupes dans ce semestre"
|
||||
"""Vrai si l'utilisateur peut changer les groupes dans ce semestre
|
||||
Obsolete: utiliser FormSemestre.can_change_groups
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if not formsemestre.etat:
|
||||
return False # semestre verrouillé
|
||||
|
@ -489,6 +489,7 @@ def _normalize_apo_fields(infolist):
|
||||
recode les champs: paiementinscription (-> booleen), datefinalisationinscription (date)
|
||||
ajoute le champs 'paiementinscription_str' : 'ok', 'Non' ou '?'
|
||||
ajoute les champs 'etape' (= None) et 'prenom' ('') s'ils ne sont pas présents.
|
||||
ajoute le champ 'civilite_etat_civil' (='X'), et 'prenom_etat_civil' (='') si non présent.
|
||||
"""
|
||||
for infos in infolist:
|
||||
if "paiementinscription" in infos:
|
||||
@ -520,6 +521,15 @@ def _normalize_apo_fields(infolist):
|
||||
if "prenom" not in infos:
|
||||
infos["prenom"] = ""
|
||||
|
||||
if "civilite_etat_civil" not in infos:
|
||||
infos["civilite_etat_civil"] = "X"
|
||||
|
||||
if "civilite_etat_civil" not in infos:
|
||||
infos["civilite_etat_civil"] = "X"
|
||||
|
||||
if "prenom_etat_civil" not in infos:
|
||||
infos["prenom_etat_civil"] = ""
|
||||
|
||||
return infolist
|
||||
|
||||
|
||||
|
@ -113,9 +113,9 @@ get_base_preferences(formsemestre_id)
|
||||
import flask
|
||||
from flask import current_app, flash, g, request, url_for
|
||||
|
||||
from app import db, log
|
||||
from app.models import Departement
|
||||
from app.scodoc import sco_cache
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoException
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -272,7 +272,7 @@ class BasePreferences(object):
|
||||
)
|
||||
|
||||
def __init__(self, dept_id: int):
|
||||
dept = Departement.query.get(dept_id)
|
||||
dept = db.session.get(Departement, dept_id)
|
||||
if not dept:
|
||||
raise ScoValueError(f"BasePreferences: Invalid departement: {dept_id}")
|
||||
self.dept_id = dept.id
|
||||
|
@ -30,7 +30,7 @@
|
||||
"""
|
||||
from operator import itemgetter
|
||||
|
||||
from app import log
|
||||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
@ -63,25 +63,32 @@ def dict_pvjury(
|
||||
Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours
|
||||
Résultat:
|
||||
{
|
||||
'date' : date de la decision la plus recente,
|
||||
'formsemestre' : sem,
|
||||
'is_apc' : bool,
|
||||
'formation' : { 'acronyme' :, 'titre': ... }
|
||||
'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,},
|
||||
'etat' : I ou D ou DEF
|
||||
'decision_sem' : {'code':, 'code_prev': },
|
||||
'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :,
|
||||
'acronyme', 'numero': } },
|
||||
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
||||
'validation_parcours' : True si parcours validé (diplome obtenu)
|
||||
'prev_code' : code (calculé slt si with_prev),
|
||||
'mention' : mention (en fct moy gen),
|
||||
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
||||
'sum_ects_capitalises' : somme des ECTS des UE capitalisees
|
||||
}
|
||||
]
|
||||
},
|
||||
'decisions_dict' : { etudid : decision (comme ci-dessus) },
|
||||
'date' : str = date de la decision la plus recente, format dd/mm/yyyy,
|
||||
'formsemestre' : dict = formsemestre,
|
||||
'is_apc' : bool,
|
||||
'formation' : { 'acronyme' :, 'titre': ... }
|
||||
'decisions' : [
|
||||
{
|
||||
'identite' : {'nom' :, 'prenom':, ...,},
|
||||
'etat' : I ou D ou DEF
|
||||
'decision_sem' : {'code':, 'code_prev': },
|
||||
'decisions_ue' : {
|
||||
ue_id : {
|
||||
'code' : ADM|CMP|AJ,
|
||||
'ects' : float,
|
||||
'event_date' :str = "dd/mm/yyyy",
|
||||
},
|
||||
},
|
||||
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
||||
'validation_parcours' : True si parcours validé (diplome obtenu)
|
||||
'prev_code' : code (calculé slt si with_prev),
|
||||
'mention' : mention (en fct moy gen),
|
||||
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
||||
'sum_ects_capitalises' : somme des ECTS des UE capitalisees
|
||||
},
|
||||
...
|
||||
],
|
||||
'decisions_dict' : { etudid : decision (comme ci-dessus) },
|
||||
}
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
@ -253,7 +260,7 @@ def _comp_ects_by_ue_code(nt, decisions_ue):
|
||||
ects_by_ue_code = {}
|
||||
for ue_id in decisions_ue:
|
||||
d = decisions_ue[ue_id]
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
ects_by_ue_code[ue.ue_code] = d["ects"]
|
||||
|
||||
return ects_by_ue_code
|
||||
|
@ -42,6 +42,7 @@ from reportlab.platypus import PageBreak, Table, Image
|
||||
from reportlab.platypus.doctemplate import BaseDocTemplate
|
||||
from reportlab.lib import styles
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre, Identite
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -70,7 +71,7 @@ def pdf_lettres_individuelles(
|
||||
if not dpv:
|
||||
return ""
|
||||
#
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||
params = {
|
||||
"date_jury": date_jury,
|
||||
|
@ -27,6 +27,7 @@
|
||||
|
||||
"""Tableau récapitulatif des notes d'un semestre
|
||||
"""
|
||||
import collections
|
||||
import datetime
|
||||
import time
|
||||
from xml.etree import ElementTree
|
||||
@ -109,7 +110,7 @@ def formsemestre_recapcomplet(
|
||||
force_publishing=force_publishing,
|
||||
)
|
||||
|
||||
table_html, table = _formsemestre_recapcomplet_to_html(
|
||||
table_html, table, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
|
||||
formsemestre,
|
||||
filename=filename,
|
||||
mode_jury=mode_jury,
|
||||
@ -142,7 +143,7 @@ def formsemestre_recapcomplet(
|
||||
H.append(
|
||||
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
||||
)
|
||||
for (fmt, label) in (
|
||||
for fmt, label in (
|
||||
("html", "Tableau"),
|
||||
("evals", "Avec toutes les évaluations"),
|
||||
("xlsx", "Excel (non formaté)"),
|
||||
@ -186,7 +187,7 @@ def formsemestre_recapcomplet(
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
|
||||
}">Effacer <em>toutes</em> les décisions de jury (BUT) du semestre</a>
|
||||
}">Effacer <em>toutes</em> les décisions de jury BUT issues de ce semestre</a>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
@ -215,33 +216,37 @@ def formsemestre_recapcomplet(
|
||||
"""
|
||||
)
|
||||
|
||||
if mode_jury and table and sum(table.freq_codes_annuels.values()) > 0:
|
||||
if mode_jury and freq_codes_annuels and sum(freq_codes_annuels.values()) > 0:
|
||||
nb_etud_avec_decision_annuelle = (
|
||||
sum(freq_codes_annuels.values()) - freq_codes_annuels["total"]
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
<div class="jury_stats">
|
||||
<div>Nb d'étudiants avec décision annuelle:
|
||||
{sum(table.freq_codes_annuels.values())} / {len(table)}
|
||||
<div><b>Nb d'étudiants avec décision annuelle:</b>
|
||||
{nb_etud_avec_decision_annuelle} / {freq_codes_annuels["total"]}
|
||||
</div>
|
||||
<div><b>Codes annuels octroyés:</b></div>
|
||||
<table class="jury_stats_codes">
|
||||
"""
|
||||
)
|
||||
for code in sorted(table.freq_codes_annuels.keys()):
|
||||
if nb_etud_avec_decision_annuelle > 0:
|
||||
H.append(
|
||||
f"""<tr>
|
||||
<td>{code}</td>
|
||||
<td style="text-align:right">{table.freq_codes_annuels[code]}</td>
|
||||
<td style="text-align:right">{
|
||||
(100*table.freq_codes_annuels[code] / len(table)):2.1f}%
|
||||
</td>
|
||||
</tr>"""
|
||||
"""<div><b>Codes annuels octroyés:</b></div>
|
||||
<table class="jury_stats_codes">
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
"""
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
for code in sorted(freq_codes_annuels.keys()):
|
||||
if code != "total":
|
||||
H.append(
|
||||
f"""<tr>
|
||||
<td>{code}</td>
|
||||
<td style="text-align:right">{freq_codes_annuels[code]}</td>
|
||||
<td style="text-align:right">{
|
||||
(100*freq_codes_annuels[code] / freq_codes_annuels["total"]):2.1f}%
|
||||
</td>
|
||||
</tr>"""
|
||||
)
|
||||
H.append("""</table>""")
|
||||
H.append("""</div>""")
|
||||
# Légende
|
||||
H.append(
|
||||
"""
|
||||
@ -251,6 +256,7 @@ def formsemestre_recapcomplet(
|
||||
<div><tt>~</tt></div><div>valeur manquante</div>
|
||||
<div><tt>=</tt></div><div>UE dispensée</div>
|
||||
<div><tt>nan</tt></div><div>valeur non disponible</div>
|
||||
<div>📍</div><div>code jury non enregistré</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
@ -271,12 +277,12 @@ def _formsemestre_recapcomplet_to_html(
|
||||
filename: str = "",
|
||||
mode_jury=False, # saisie décisions jury
|
||||
selected_etudid=None,
|
||||
) -> tuple[str, TableRecap]:
|
||||
) -> tuple[str, TableRecap, collections.Counter]:
|
||||
"""Le tableau recap en html"""
|
||||
if tabformat not in ("html", "evals"):
|
||||
raise ScoValueError("invalid table format")
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
table_html, table = gen_formsemestre_recapcomplet_html_table(
|
||||
table_html, table, freq_codes_annuels = gen_formsemestre_recapcomplet_html_table(
|
||||
formsemestre,
|
||||
res,
|
||||
include_evaluations=(tabformat == "evals"),
|
||||
@ -284,7 +290,7 @@ def _formsemestre_recapcomplet_to_html(
|
||||
filename=filename,
|
||||
selected_etudid=selected_etudid,
|
||||
)
|
||||
return table_html, table
|
||||
return table_html, table, freq_codes_annuels
|
||||
|
||||
|
||||
def _formsemestre_recapcomplet_to_file(
|
||||
@ -446,9 +452,9 @@ def gen_formsemestre_recapcomplet_html_table(
|
||||
mode_jury=False,
|
||||
filename="",
|
||||
selected_etudid=None,
|
||||
) -> tuple[str, TableRecap]:
|
||||
) -> tuple[str, TableRecap, collections.Counter]:
|
||||
"""Construit table recap pour le BUT
|
||||
Cache le résultat pour le semestre (sauf en mode jury).
|
||||
Cache le résultat pour le semestre.
|
||||
Note: on cache le HTML et non l'objet Table.
|
||||
|
||||
Si mode_jury, occultera colonnes modules (en js)
|
||||
@ -460,6 +466,7 @@ def gen_formsemestre_recapcomplet_html_table(
|
||||
"""
|
||||
table = None
|
||||
table_html = None
|
||||
table_html_cached = None
|
||||
cache_class = {
|
||||
(True, True): sco_cache.TableJuryWithEvalsCache,
|
||||
(True, False): sco_cache.TableJuryCache,
|
||||
@ -467,8 +474,8 @@ def gen_formsemestre_recapcomplet_html_table(
|
||||
(False, False): sco_cache.TableRecapCache,
|
||||
}[(bool(mode_jury), bool(include_evaluations))]
|
||||
if not selected_etudid:
|
||||
table_html = cache_class.get(formsemestre.id)
|
||||
if table_html is None:
|
||||
table_html_cached = cache_class.get(formsemestre.id)
|
||||
if table_html_cached is None:
|
||||
table = _gen_formsemestre_recapcomplet_table(
|
||||
res,
|
||||
include_evaluations,
|
||||
@ -477,9 +484,14 @@ def gen_formsemestre_recapcomplet_html_table(
|
||||
selected_etudid=selected_etudid,
|
||||
)
|
||||
table_html = table.html()
|
||||
cache_class.set(formsemestre.id, table_html)
|
||||
freq_codes_annuels = (
|
||||
table.freq_codes_annuels if hasattr(table, "freq_codes_annuels") else None
|
||||
)
|
||||
cache_class.set(formsemestre.id, (table_html, freq_codes_annuels))
|
||||
else:
|
||||
table_html, freq_codes_annuels = table_html_cached
|
||||
|
||||
return table_html, table
|
||||
return table_html, table, freq_codes_annuels
|
||||
|
||||
|
||||
def _gen_formsemestre_recapcomplet_table(
|
||||
|
@ -33,11 +33,9 @@ from collections import defaultdict
|
||||
|
||||
from flask import request
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestreInscription
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -170,7 +168,7 @@ def but_indicateurs_by_bac(formsemestre: FormSemestre) -> dict[str:dict]:
|
||||
if deca and deca.formsemestre_impair
|
||||
}
|
||||
for formsemestre_id_precedent in formsemestre_id_precedents:
|
||||
formsemestre_impair = FormSemestre.query.get(formsemestre_id_precedent)
|
||||
formsemestre_impair = db.session.get(FormSemestre, formsemestre_id_precedent)
|
||||
suffix = (
|
||||
f"S{formsemestre_impair.semestre_id}"
|
||||
if len(formsemestre_id_precedents) == 1
|
||||
|
@ -36,41 +36,49 @@ import flask
|
||||
from flask import g, url_for, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db, log
|
||||
from app.auth.models import User
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Evaluation, FormSemestre
|
||||
from app.models import ModuleImpl, ScolarNews
|
||||
from app.models import (
|
||||
Evaluation,
|
||||
FormSemestre,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
NotesNotes,
|
||||
ScolarNews,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
AccessDenied,
|
||||
InvalidNoteValue,
|
||||
NoteProcessError,
|
||||
ScoGenError,
|
||||
ScoBugCatcher,
|
||||
ScoException,
|
||||
ScoInvalidParamError,
|
||||
ScoValueError,
|
||||
)
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
|
||||
from app.scodoc import html_sco_header, sco_users
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_undo_notes
|
||||
from app.scodoc import sco_etud
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
def convert_note_from_string(
|
||||
@ -128,29 +136,30 @@ def _displayNote(val):
|
||||
return val
|
||||
|
||||
|
||||
def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict):
|
||||
def _check_notes(notes: list[(int, float)], evaluation: Evaluation):
|
||||
# XXX typehint : float or str
|
||||
"""notes is a list of tuples (etudid, value)
|
||||
mod is the module (used to ckeck type, for malus)
|
||||
returns list of valid notes (etudid, float value)
|
||||
and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
|
||||
and 4 lists of etudid: etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress
|
||||
"""
|
||||
note_max = evaluation["note_max"]
|
||||
if mod["module_type"] in (
|
||||
note_max = evaluation.note_max or 0.0
|
||||
module: Module = evaluation.moduleimpl.module
|
||||
if module.module_type in (
|
||||
scu.ModuleType.STANDARD,
|
||||
scu.ModuleType.RESSOURCE,
|
||||
scu.ModuleType.SAE,
|
||||
):
|
||||
note_min = scu.NOTES_MIN
|
||||
elif mod["module_type"] == ModuleType.MALUS:
|
||||
elif module.module_type == ModuleType.MALUS:
|
||||
note_min = -20.0
|
||||
else:
|
||||
raise ValueError("Invalid module type") # bug
|
||||
L = [] # liste (etudid, note) des notes ok (ou absent)
|
||||
invalids = [] # etudid avec notes invalides
|
||||
withoutnotes = [] # etudid sans notes (champs vides)
|
||||
absents = [] # etudid absents
|
||||
tosuppress = [] # etudids avec ancienne note à supprimer
|
||||
valid_notes = [] # liste (etudid, note) des notes ok (ou absent)
|
||||
etudids_invalids = [] # etudid avec notes invalides
|
||||
etudids_without_notes = [] # etudid sans notes (champs vides)
|
||||
etudids_absents = [] # etudid absents
|
||||
etudid_to_suppress = [] # etudids avec ancienne note à supprimer
|
||||
|
||||
for etudid, note in notes:
|
||||
note = str(note).strip().upper()
|
||||
@ -166,31 +175,34 @@ def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict):
|
||||
note_max,
|
||||
note_min=note_min,
|
||||
etudid=etudid,
|
||||
absents=absents,
|
||||
tosuppress=tosuppress,
|
||||
invalids=invalids,
|
||||
absents=etudids_absents,
|
||||
tosuppress=etudid_to_suppress,
|
||||
invalids=etudids_invalids,
|
||||
)
|
||||
if not invalid:
|
||||
L.append((etudid, value))
|
||||
valid_notes.append((etudid, value))
|
||||
else:
|
||||
withoutnotes.append(etudid)
|
||||
return L, invalids, withoutnotes, absents, tosuppress
|
||||
etudids_without_notes.append(etudid)
|
||||
return (
|
||||
valid_notes,
|
||||
etudids_invalids,
|
||||
etudids_without_notes,
|
||||
etudids_absents,
|
||||
etudid_to_suppress,
|
||||
)
|
||||
|
||||
|
||||
def do_evaluation_upload_xls():
|
||||
"""
|
||||
Soumission d'un fichier XLS (evaluation_id, notefile)
|
||||
"""
|
||||
authuser = current_user
|
||||
vals = scu.get_request_args()
|
||||
evaluation_id = int(vals["evaluation_id"])
|
||||
comment = vals["comment"]
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
# Check access
|
||||
# (admin, respformation, and responsable_id)
|
||||
if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]):
|
||||
raise AccessDenied("Modification des notes impossible pour %s" % authuser)
|
||||
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
# Check access (admin, respformation, and responsable_id)
|
||||
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
|
||||
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
||||
#
|
||||
diag, lines = sco_excel.excel_file_to_list(vals["notefile"])
|
||||
try:
|
||||
@ -239,14 +251,16 @@ def do_evaluation_upload_xls():
|
||||
if etudid:
|
||||
notes.append((etudid, val))
|
||||
ni += 1
|
||||
except:
|
||||
except Exception as exc:
|
||||
diag.append(
|
||||
f"""Erreur: Ligne invalide ! (erreur ligne {ni})<br>{lines[ni]}"""
|
||||
)
|
||||
raise InvalidNoteValue()
|
||||
raise InvalidNoteValue() from exc
|
||||
# -- check values
|
||||
L, invalids, withoutnotes, absents, _ = _check_notes(notes, E, M["module"])
|
||||
if len(invalids):
|
||||
valid_notes, invalids, withoutnotes, absents, _ = _check_notes(
|
||||
notes, evaluation
|
||||
)
|
||||
if invalids:
|
||||
diag.append(
|
||||
f"Erreur: la feuille contient {len(invalids)} notes invalides</p>"
|
||||
)
|
||||
@ -258,37 +272,33 @@ def do_evaluation_upload_xls():
|
||||
diag.append("Notes invalides pour: " + ", ".join(etudsnames))
|
||||
raise InvalidNoteValue()
|
||||
else:
|
||||
nb_changed, nb_suppress, existing_decisions = notes_add(
|
||||
authuser, evaluation_id, L, comment
|
||||
etudids_changed, nb_suppress, etudids_with_decisions = notes_add(
|
||||
current_user, evaluation_id, valid_notes, comment
|
||||
)
|
||||
# news
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[
|
||||
0
|
||||
]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
mod["moduleimpl_id"] = M["moduleimpl_id"]
|
||||
mod["url"] = url_for(
|
||||
module: Module = evaluation.moduleimpl.module
|
||||
status_url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=mod["moduleimpl_id"],
|
||||
moduleimpl_id=evaluation.moduleimpl_id,
|
||||
_external=True,
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=M["moduleimpl_id"],
|
||||
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
|
||||
url=mod["url"],
|
||||
obj=evaluation.moduleimpl_id,
|
||||
text=f"""Chargement notes dans <a href="{status_url}">{
|
||||
module.titre or module.code}</a>""",
|
||||
url=status_url,
|
||||
max_frequency=30 * 60, # 30 minutes
|
||||
)
|
||||
|
||||
msg = (
|
||||
"<p>%d notes changées (%d sans notes, %d absents, %d note supprimées)</p>"
|
||||
% (nb_changed, len(withoutnotes), len(absents), nb_suppress)
|
||||
)
|
||||
if existing_decisions:
|
||||
msg += """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
|
||||
# msg += '<p>' + str(notes) # debug
|
||||
msg = f"""<p>{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, {
|
||||
len(absents)} absents, {nb_suppress} note supprimées)
|
||||
</p>"""
|
||||
if etudids_with_decisions:
|
||||
msg += """<p class="warning">Important: il y avait déjà des décisions de jury
|
||||
enregistrées, qui sont peut-être à revoir suite à cette modification !</p>
|
||||
"""
|
||||
return 1, msg
|
||||
|
||||
except InvalidNoteValue:
|
||||
@ -310,14 +320,12 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -
|
||||
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id):
|
||||
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
|
||||
# Convert and check value
|
||||
L, invalids, _, _, _ = _check_notes(
|
||||
[(etud.id, value)], evaluation.to_dict(), evaluation.moduleimpl.module.to_dict()
|
||||
)
|
||||
L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation)
|
||||
if len(invalids) == 0:
|
||||
nb_changed, _, _ = notes_add(
|
||||
etudids_changed, _, _ = notes_add(
|
||||
current_user, evaluation.id, L, "Initialisation notes"
|
||||
)
|
||||
if nb_changed == 1:
|
||||
if len(etudids_changed) == 1:
|
||||
return True
|
||||
return False # error
|
||||
|
||||
@ -352,9 +360,7 @@ def do_evaluation_set_missing(
|
||||
if etudid not in notes_db: # pas de note
|
||||
notes.append((etudid, value))
|
||||
# Convert and check values
|
||||
L, invalids, _, _, _ = _check_notes(
|
||||
notes, evaluation.to_dict(), modimpl.module.to_dict()
|
||||
)
|
||||
valid_notes, invalids, _, _, _ = _check_notes(notes, evaluation)
|
||||
dest_url = url_for(
|
||||
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id
|
||||
)
|
||||
@ -372,13 +378,13 @@ def do_evaluation_set_missing(
|
||||
"""
|
||||
# Confirm action
|
||||
if not dialog_confirmed:
|
||||
plural = len(L) > 1
|
||||
plural = len(valid_notes) > 1
|
||||
return scu.confirm_dialog(
|
||||
f"""<h2>Mettre toutes les notes manquantes de l'évaluation
|
||||
à la valeur {value} ?</h2>
|
||||
<p>Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
|
||||
n'a été rentrée seront affectés.</p>
|
||||
<p><b>{len(L)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
|
||||
<p><b>{len(valid_notes)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
|
||||
par ce changement de note.</b>
|
||||
</p>
|
||||
""",
|
||||
@ -392,7 +398,7 @@ def do_evaluation_set_missing(
|
||||
)
|
||||
# ok
|
||||
comment = "Initialisation notes manquantes"
|
||||
nb_changed, _, _ = notes_add(current_user, evaluation_id, L, comment)
|
||||
etudids_changed, _, _ = notes_add(current_user, evaluation_id, valid_notes, comment)
|
||||
# news
|
||||
url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
@ -408,7 +414,7 @@ def do_evaluation_set_missing(
|
||||
)
|
||||
return f"""
|
||||
{ html_sco_header.sco_header() }
|
||||
<h2>{nb_changed} notes changées</h2>
|
||||
<h2>{len(etudids_changed)} notes changées</h2>
|
||||
<ul>
|
||||
<li><a class="stdlink" href="{dest_url}">
|
||||
Revenir au formulaire de saisie des notes</a>
|
||||
@ -454,7 +460,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
||||
)
|
||||
|
||||
if not dialog_confirmed:
|
||||
nb_changed, nb_suppress, existing_decisions = notes_add(
|
||||
etudids_changed, nb_suppress, existing_decisions = notes_add(
|
||||
current_user, evaluation_id, notes, do_it=False, check_inscription=False
|
||||
)
|
||||
msg = f"""<p>Confirmer la suppression des {nb_suppress} notes ?
|
||||
@ -475,14 +481,14 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
||||
)
|
||||
|
||||
# modif
|
||||
nb_changed, nb_suppress, existing_decisions = notes_add(
|
||||
etudids_changed, nb_suppress, existing_decisions = notes_add(
|
||||
current_user,
|
||||
evaluation_id,
|
||||
notes,
|
||||
comment="effacer tout",
|
||||
check_inscription=False,
|
||||
)
|
||||
assert nb_changed == nb_suppress
|
||||
assert len(etudids_changed) == nb_suppress
|
||||
H = [f"""<p>{nb_suppress} notes supprimées</p>"""]
|
||||
if existing_decisions:
|
||||
H.append(
|
||||
@ -516,7 +522,7 @@ def notes_add(
|
||||
comment=None,
|
||||
do_it=True,
|
||||
check_inscription=True,
|
||||
) -> tuple:
|
||||
) -> tuple[list[int], int, list[int]]:
|
||||
"""
|
||||
Insert or update notes
|
||||
notes is a list of tuples (etudid,value)
|
||||
@ -524,12 +530,12 @@ def notes_add(
|
||||
WOULD be changed or suppressed.
|
||||
Nota:
|
||||
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
|
||||
Return tuple (nb_changed, nb_suppress, existing_decisions)
|
||||
|
||||
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
|
||||
"""
|
||||
now = psycopg2.Timestamp(
|
||||
*time.localtime()[:6]
|
||||
) # datetime.datetime.now().isoformat()
|
||||
# Verifie inscription et valeur note
|
||||
now = psycopg2.Timestamp(*time.localtime()[:6])
|
||||
|
||||
# Vérifie inscription et valeur note
|
||||
inscrits = {
|
||||
x[0]
|
||||
for x in sco_groups.do_evaluation_listeetuds_groups(
|
||||
@ -548,13 +554,13 @@ def notes_add(
|
||||
# Met a jour la base
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
nb_changed = 0
|
||||
etudids_changed = []
|
||||
nb_suppress = 0
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
existing_decisions = (
|
||||
[]
|
||||
) # etudids pour lesquels il y a une decision de jury et que la note change
|
||||
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
|
||||
formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
# etudids pour lesquels il y a une decision de jury et que la note change:
|
||||
etudids_with_decision = []
|
||||
try:
|
||||
for etudid, value in notes:
|
||||
changed = False
|
||||
@ -562,7 +568,7 @@ def notes_add(
|
||||
# nouvelle note
|
||||
if value != scu.NOTES_SUPPRESS:
|
||||
if do_it:
|
||||
aa = {
|
||||
args = {
|
||||
"etudid": etudid,
|
||||
"evaluation_id": evaluation_id,
|
||||
"value": value,
|
||||
@ -570,13 +576,20 @@ def notes_add(
|
||||
"uid": user.id,
|
||||
"date": now,
|
||||
}
|
||||
ndb.quote_dict(aa)
|
||||
ndb.quote_dict(args)
|
||||
# Note: le conflit ci-dessous peut arriver si un autre thread
|
||||
# a modifié la base après qu'on ait lu notes_db
|
||||
cursor.execute(
|
||||
"""INSERT INTO notes_notes
|
||||
(etudid, evaluation_id, value, comment, date, uid)
|
||||
VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)
|
||||
VALUES
|
||||
(%(etudid)s,%(evaluation_id)s,%(value)s,
|
||||
%(comment)s,%(date)s,%(uid)s)
|
||||
ON CONFLICT ON CONSTRAINT notes_notes_etudid_evaluation_id_key
|
||||
DO UPDATE SET etudid=%(etudid)s, evaluation_id=%(evaluation_id)s,
|
||||
value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s
|
||||
""",
|
||||
aa,
|
||||
args,
|
||||
)
|
||||
changed = True
|
||||
else:
|
||||
@ -584,7 +597,7 @@ def notes_add(
|
||||
oldval = notes_db[etudid]["value"]
|
||||
if type(value) != type(oldval):
|
||||
changed = True
|
||||
elif type(value) == float and (
|
||||
elif isinstance(value, float) and (
|
||||
abs(value - oldval) > scu.NOTES_PRECISION
|
||||
):
|
||||
changed = True
|
||||
@ -603,7 +616,7 @@ def notes_add(
|
||||
""",
|
||||
{"etudid": etudid, "evaluation_id": evaluation_id},
|
||||
)
|
||||
aa = {
|
||||
args = {
|
||||
"etudid": etudid,
|
||||
"evaluation_id": evaluation_id,
|
||||
"value": value,
|
||||
@ -611,7 +624,7 @@ def notes_add(
|
||||
"comment": comment,
|
||||
"uid": user.id,
|
||||
}
|
||||
ndb.quote_dict(aa)
|
||||
ndb.quote_dict(args)
|
||||
if value != scu.NOTES_SUPPRESS:
|
||||
if do_it:
|
||||
cursor.execute(
|
||||
@ -620,52 +633,49 @@ def notes_add(
|
||||
WHERE etudid = %(etudid)s
|
||||
and evaluation_id = %(evaluation_id)s
|
||||
""",
|
||||
aa,
|
||||
args,
|
||||
)
|
||||
else: # suppression ancienne note
|
||||
if do_it:
|
||||
log(
|
||||
"notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
|
||||
% (evaluation_id, etudid, oldval)
|
||||
f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={
|
||||
etudid}, oldval={oldval}"""
|
||||
)
|
||||
cursor.execute(
|
||||
"""DELETE FROM notes_notes
|
||||
WHERE etudid = %(etudid)s
|
||||
AND evaluation_id = %(evaluation_id)s
|
||||
""",
|
||||
aa,
|
||||
args,
|
||||
)
|
||||
# garde trace de la suppression dans l'historique:
|
||||
aa["value"] = scu.NOTES_SUPPRESS
|
||||
args["value"] = scu.NOTES_SUPPRESS
|
||||
cursor.execute(
|
||||
"""INSERT INTO notes_notes_log (etudid,evaluation_id,value,comment,date,uid)
|
||||
VALUES (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
|
||||
"""INSERT INTO notes_notes_log
|
||||
(etudid,evaluation_id,value,comment,date,uid)
|
||||
VALUES
|
||||
(%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
|
||||
""",
|
||||
aa,
|
||||
args,
|
||||
)
|
||||
nb_suppress += 1
|
||||
if changed:
|
||||
nb_changed += 1
|
||||
if has_existing_decision(M, E, etudid):
|
||||
existing_decisions.append(etudid)
|
||||
etudids_changed.append(etudid)
|
||||
if res.etud_has_decision(etudid):
|
||||
etudids_with_decision.append(etudid)
|
||||
except Exception as exc:
|
||||
log("*** exception in notes_add")
|
||||
if do_it:
|
||||
cnx.rollback() # abort
|
||||
# inval cache
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=M["formsemestre_id"]
|
||||
) # > modif notes (exception)
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
|
||||
sco_cache.EvaluationCache.delete(evaluation_id)
|
||||
raise # XXX
|
||||
raise ScoGenError("Erreur enregistrement note: merci de ré-essayer") from exc
|
||||
raise ScoException from exc
|
||||
if do_it:
|
||||
cnx.commit()
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=M["formsemestre_id"]
|
||||
) # > modif notes
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
|
||||
sco_cache.EvaluationCache.delete(evaluation_id)
|
||||
return nb_changed, nb_suppress, existing_decisions
|
||||
return etudids_changed, nb_suppress, etudids_with_decision
|
||||
|
||||
|
||||
def saisie_notes_tableur(evaluation_id, group_ids=()):
|
||||
@ -868,44 +878,39 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
|
||||
|
||||
def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
||||
"""Document Excel pour saisie notes dans l'évaluation et les groupes indiqués"""
|
||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not evals:
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if not evaluation:
|
||||
raise ScoValueError("invalid evaluation_id")
|
||||
eval_dict = evals[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=eval_dict["moduleimpl_id"])[0]
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
|
||||
mod_responsable = sco_users.user_info(M["responsable_id"])
|
||||
if eval_dict["jour"]:
|
||||
indication_date = ndb.DateDMYtoISO(eval_dict["jour"])
|
||||
modimpl = evaluation.moduleimpl
|
||||
formsemestre = modimpl.formsemestre
|
||||
mod_responsable = sco_users.user_info(modimpl.responsable_id)
|
||||
if evaluation.jour:
|
||||
indication_date = evaluation.jour.isoformat()
|
||||
else:
|
||||
indication_date = scu.sanitize_filename(eval_dict["description"])[:12]
|
||||
eval_name = "%s-%s" % (Mod["code"], indication_date)
|
||||
indication_date = scu.sanitize_filename(evaluation.description or "")[:12]
|
||||
eval_name = f"{evaluation.moduleimpl.module.code}-{indication_date}"
|
||||
|
||||
if eval_dict["description"]:
|
||||
evaltitre = "%s du %s" % (eval_dict["description"], eval_dict["jour"])
|
||||
else:
|
||||
evaltitre = "évaluation du %s" % eval_dict["jour"]
|
||||
description = "%s en %s (%s) resp. %s" % (
|
||||
evaltitre,
|
||||
Mod["abbrev"] or "",
|
||||
Mod["code"] or "",
|
||||
mod_responsable["prenomnom"],
|
||||
date_str = (
|
||||
f"""du {evaluation.jour.strftime("%d/%m/%Y")}"""
|
||||
if evaluation.jour
|
||||
else "(sans date)"
|
||||
)
|
||||
eval_titre = f"""{evaluation.description if evaluation.description else "évaluation"} {date_str}"""
|
||||
|
||||
description = f"""{eval_titre} en {evaluation.moduleimpl.module.abbrev or ""} ({
|
||||
evaluation.moduleimpl.module.code
|
||||
}) resp. {mod_responsable["prenomnom"]}"""
|
||||
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids=group_ids,
|
||||
formsemestre_id=formsemestre_id,
|
||||
formsemestre_id=formsemestre.id,
|
||||
select_all_when_unspecified=True,
|
||||
etat=None,
|
||||
)
|
||||
groups = sco_groups.listgroups(groups_infos.group_ids)
|
||||
gr_title_filename = sco_groups.listgroups_filename(groups)
|
||||
# gr_title = sco_groups.listgroups_abbrev(groups)
|
||||
if None in [g["group_name"] for g in groups]: # tous les etudiants
|
||||
getallstudents = True
|
||||
# gr_title = "tous"
|
||||
gr_title_filename = "tous"
|
||||
else:
|
||||
getallstudents = False
|
||||
@ -917,17 +922,17 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
||||
]
|
||||
|
||||
# une liste de liste de chaines: lignes de la feuille de calcul
|
||||
L = []
|
||||
rows = []
|
||||
|
||||
etuds = _get_sorted_etuds(eval_dict, etudids, formsemestre_id)
|
||||
etuds = _get_sorted_etuds(evaluation, etudids, formsemestre.id)
|
||||
for e in etuds:
|
||||
etudid = e["etudid"]
|
||||
groups = sco_groups.get_etud_groups(etudid, formsemestre_id)
|
||||
groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
|
||||
grc = sco_groups.listgroups_abbrev(groups)
|
||||
|
||||
L.append(
|
||||
rows.append(
|
||||
[
|
||||
"%s" % etudid,
|
||||
str(etudid),
|
||||
e["nom"].upper(),
|
||||
e["prenom"].lower().capitalize(),
|
||||
e["inscr"]["etat"],
|
||||
@ -937,31 +942,11 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
|
||||
]
|
||||
)
|
||||
|
||||
filename = "notes_%s_%s" % (eval_name, gr_title_filename)
|
||||
filename = f"notes_{eval_name}_{gr_title_filename}"
|
||||
xls = sco_excel.excel_feuille_saisie(
|
||||
eval_dict, sem["titreannee"], description, lines=L
|
||||
evaluation, formsemestre.titre_annee(), description, lines=rows
|
||||
)
|
||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
||||
# return sco_excel.send_excel_file(xls, filename)
|
||||
|
||||
|
||||
def has_existing_decision(M, E, etudid):
|
||||
"""Verifie s'il y a une validation pour cet etudiant dans ce semestre ou UE
|
||||
Si oui, return True
|
||||
"""
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if nt.get_etud_decision_sem(etudid):
|
||||
return True
|
||||
dec_ues = nt.get_etud_decisions_ue(etudid)
|
||||
if dec_ues:
|
||||
mod = sco_edit_module.module_list({"module_id": M["module_id"]})[0]
|
||||
ue_id = mod["ue_id"]
|
||||
if ue_id in dec_ues:
|
||||
return True # decision pour l'UE a laquelle appartient cette evaluation
|
||||
|
||||
return False # pas de decision de jury affectee par cette note
|
||||
|
||||
|
||||
# -----------------------------
|
||||
@ -973,20 +958,18 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||
if not isinstance(evaluation_id, int):
|
||||
raise ScoInvalidParamError()
|
||||
group_ids = [int(group_id) for group_id in (group_ids or [])]
|
||||
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
|
||||
if not evals:
|
||||
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if evaluation is None:
|
||||
raise ScoValueError("évaluation inexistante")
|
||||
E = evals[0]
|
||||
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
modimpl = evaluation.moduleimpl
|
||||
moduleimpl_status_url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=E["moduleimpl_id"],
|
||||
moduleimpl_id=evaluation.moduleimpl_id,
|
||||
)
|
||||
# Check access
|
||||
# (admin, respformation, and responsable_id)
|
||||
if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
|
||||
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
|
||||
return f"""
|
||||
{html_sco_header.sco_header()}
|
||||
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
|
||||
@ -1001,16 +984,16 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||
# Informations sur les groupes à afficher:
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids=group_ids,
|
||||
formsemestre_id=formsemestre_id,
|
||||
formsemestre_id=modimpl.formsemestre_id,
|
||||
select_all_when_unspecified=True,
|
||||
etat=None,
|
||||
)
|
||||
|
||||
if E["description"]:
|
||||
page_title = 'Saisie "%s"' % E["description"]
|
||||
else:
|
||||
page_title = "Saisie des notes"
|
||||
|
||||
page_title = (
|
||||
f'Saisie "{evaluation.description}"'
|
||||
if evaluation.description
|
||||
else "Saisie des notes"
|
||||
)
|
||||
# HTML page:
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
@ -1036,19 +1019,19 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||
"id": "menu_saisie_tableur",
|
||||
"endpoint": "notes.saisie_notes_tableur",
|
||||
"args": {
|
||||
"evaluation_id": E["evaluation_id"],
|
||||
"evaluation_id": evaluation.id,
|
||||
"group_ids": groups_infos.group_ids,
|
||||
},
|
||||
},
|
||||
{
|
||||
"title": "Voir toutes les notes du module",
|
||||
"endpoint": "notes.evaluation_listenotes",
|
||||
"args": {"moduleimpl_id": E["moduleimpl_id"]},
|
||||
"args": {"moduleimpl_id": evaluation.moduleimpl_id},
|
||||
},
|
||||
{
|
||||
"title": "Effacer toutes les notes de cette évaluation",
|
||||
"endpoint": "notes.evaluation_suppress_alln",
|
||||
"args": {"evaluation_id": E["evaluation_id"]},
|
||||
"args": {"evaluation_id": evaluation.id},
|
||||
},
|
||||
],
|
||||
alone=True,
|
||||
@ -1077,7 +1060,9 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||
)
|
||||
|
||||
# Le formulaire de saisie des notes:
|
||||
form = _form_saisie_notes(E, M, groups_infos, destination=moduleimpl_status_url)
|
||||
form = _form_saisie_notes(
|
||||
evaluation, modimpl, groups_infos, destination=moduleimpl_status_url
|
||||
)
|
||||
if form is None:
|
||||
return flask.redirect(moduleimpl_status_url)
|
||||
H.append(form)
|
||||
@ -1101,10 +1086,9 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
eval_dict["evaluation_id"]
|
||||
) # Notes existantes
|
||||
def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: int):
|
||||
# Notes existantes
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
etuds = []
|
||||
for etudid in etudids:
|
||||
@ -1123,17 +1107,17 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
|
||||
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
|
||||
|
||||
# Information sur absence (tenant compte de la demi-journée)
|
||||
jour_iso = ndb.DateDMYtoISO(eval_dict["jour"])
|
||||
jour_iso = evaluation.jour.isoformat() if evaluation.jour else ""
|
||||
warn_abs_lst = []
|
||||
if eval_dict["matin"]:
|
||||
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=1)
|
||||
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=1)
|
||||
if evaluation.is_matin():
|
||||
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True)
|
||||
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True)
|
||||
if nbabs:
|
||||
if nbabsjust:
|
||||
warn_abs_lst.append("absent justifié le matin !")
|
||||
else:
|
||||
warn_abs_lst.append("absent le matin !")
|
||||
if eval_dict["apresmidi"]:
|
||||
if evaluation.is_apresmidi():
|
||||
nbabs = sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0)
|
||||
nbabsjust = sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
|
||||
if nbabs:
|
||||
@ -1169,35 +1153,38 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
|
||||
return etuds
|
||||
|
||||
|
||||
def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
def _form_saisie_notes(
|
||||
evaluation: Evaluation, modimpl: ModuleImpl, groups_infos, destination=""
|
||||
):
|
||||
"""Formulaire HTML saisie des notes dans l'évaluation E du moduleimpl M
|
||||
pour les groupes indiqués.
|
||||
|
||||
On charge tous les étudiants, ne seront montrés que ceux
|
||||
des groupes sélectionnés grace a un filtre en javascript.
|
||||
"""
|
||||
evaluation_id = E["evaluation_id"]
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
|
||||
formsemestre_id = modimpl.formsemestre_id
|
||||
formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
etudids = [
|
||||
x[0]
|
||||
for x in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True, include_demdef=True
|
||||
evaluation.id, getallstudents=True, include_demdef=True
|
||||
)
|
||||
]
|
||||
if not etudids:
|
||||
return '<div class="ue_warning"><span>Aucun étudiant sélectionné !</span></div>'
|
||||
|
||||
# Decisions de jury existantes ?
|
||||
decisions_jury = {etudid: has_existing_decision(M, E, etudid) for etudid in etudids}
|
||||
# Nb de decisions de jury (pour les inscrits à l'évaluation):
|
||||
# Décisions de jury existantes ?
|
||||
decisions_jury = {etudid: res.etud_has_decision(etudid) for etudid in etudids}
|
||||
|
||||
# Nb de décisions de jury (pour les inscrits à l'évaluation):
|
||||
nb_decisions = sum(decisions_jury.values())
|
||||
|
||||
etuds = _get_sorted_etuds(E, etudids, formsemestre_id)
|
||||
etuds = _get_sorted_etuds(evaluation, etudids, formsemestre_id)
|
||||
|
||||
# Build form:
|
||||
descr = [
|
||||
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
|
||||
("evaluation_id", {"default": evaluation.id, "input_type": "hidden"}),
|
||||
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
|
||||
(
|
||||
"group_ids",
|
||||
@ -1207,7 +1194,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}),
|
||||
("changed", {"default": "0", "input_type": "hidden"}), # changed in JS
|
||||
]
|
||||
if M["module"]["module_type"] in (
|
||||
if modimpl.module.module_type in (
|
||||
ModuleType.STANDARD,
|
||||
ModuleType.RESSOURCE,
|
||||
ModuleType.SAE,
|
||||
@ -1220,11 +1207,11 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
"title": "Notes ",
|
||||
"cssclass": "formnote_bareme",
|
||||
"readonly": True,
|
||||
"default": " / %g" % E["note_max"],
|
||||
"default": " / %g" % evaluation.note_max,
|
||||
},
|
||||
)
|
||||
)
|
||||
elif M["module"]["module_type"] == ModuleType.MALUS:
|
||||
elif modimpl.module.module_type == ModuleType.MALUS:
|
||||
descr.append(
|
||||
(
|
||||
"s3",
|
||||
@ -1238,7 +1225,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise ValueError("invalid module type (%s)" % M["module"]["module_type"]) # bug
|
||||
raise ValueError(f"invalid module type ({modimpl.module.module_type})") # bug
|
||||
|
||||
initvalues = {}
|
||||
for e in etuds:
|
||||
@ -1248,7 +1235,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
if disabled:
|
||||
classdem = " etud_dem"
|
||||
etud_classes.append("etud_dem")
|
||||
disabled_attr = 'disabled="%d"' % disabled
|
||||
disabled_attr = f'disabled="{disabled}"'
|
||||
else:
|
||||
classdem = ""
|
||||
disabled_attr = ""
|
||||
@ -1265,18 +1252,17 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
)
|
||||
|
||||
# Historique des saisies de notes:
|
||||
if not disabled:
|
||||
explanation = (
|
||||
'<span id="hist_%s">' % etudid
|
||||
+ get_note_history_menu(evaluation_id, etudid)
|
||||
+ "</span>"
|
||||
)
|
||||
else:
|
||||
explanation = ""
|
||||
explanation = (
|
||||
""
|
||||
if disabled
|
||||
else f"""<span id="hist_{etudid}">{
|
||||
get_note_history_menu(evaluation.id, etudid)
|
||||
}</span>"""
|
||||
)
|
||||
explanation = e["absinfo"] + explanation
|
||||
|
||||
# Lien modif decision de jury:
|
||||
explanation += '<span id="jurylink_%s" class="jurylink"></span>' % etudid
|
||||
explanation += f'<span id="jurylink_{etudid}" class="jurylink"></span>'
|
||||
|
||||
# Valeur actuelle du champ:
|
||||
initvalues["note_" + str(etudid)] = e["val"]
|
||||
@ -1330,7 +1316,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
H.append(tf.getform()) # check and init
|
||||
H.append(
|
||||
f"""<a href="{url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=M["moduleimpl_id"])
|
||||
moduleimpl_id=modimpl.id)
|
||||
}" class="btn btn-primary">Terminer</a>
|
||||
"""
|
||||
)
|
||||
@ -1345,7 +1331,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
Mettre les notes manquantes à
|
||||
<input type="text" size="5" name="value"/>
|
||||
<input type="submit" value="OK"/>
|
||||
<input type="hidden" name="evaluation_id" value="{evaluation_id}"/>
|
||||
<input type="hidden" name="evaluation_id" value="{evaluation.id}"/>
|
||||
<input class="group_ids_str" type="hidden" name="group_ids_str" value="{
|
||||
",".join([str(x) for x in groups_infos.group_ids])
|
||||
}"/>
|
||||
@ -1362,50 +1348,56 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
|
||||
return None
|
||||
|
||||
|
||||
def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
|
||||
"""Enregistre une note (ajax)"""
|
||||
authuser = current_user
|
||||
log(
|
||||
"save_note: evaluation_id=%s etudid=%s uid=%s value=%s"
|
||||
% (evaluation_id, etudid, authuser, value)
|
||||
)
|
||||
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
Mod["url"] = url_for(
|
||||
def save_notes(
|
||||
evaluation: Evaluation, notes: list[tuple[(int, float)]], comment: str = ""
|
||||
) -> dict:
|
||||
"""Enregistre une liste de notes.
|
||||
Vérifie que les étudiants sont bien inscrits à ce module, et que l'on a le droit.
|
||||
Result: dict avec
|
||||
"""
|
||||
log(f"save_note: evaluation_id={evaluation.id} uid={current_user} notes={notes}")
|
||||
status_url = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=M["moduleimpl_id"],
|
||||
moduleimpl_id=evaluation.moduleimpl_id,
|
||||
_external=True,
|
||||
)
|
||||
result = {"nbchanged": 0} # JSON
|
||||
# Check access: admin, respformation, or responsable_id
|
||||
if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]):
|
||||
result["status"] = "unauthorized"
|
||||
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
|
||||
return json_error(403, "modification notes non autorisee pour cet utilisateur")
|
||||
#
|
||||
valid_notes, _, _, _, _ = _check_notes(notes, evaluation)
|
||||
if valid_notes:
|
||||
etudids_changed, _, etudids_with_decision = notes_add(
|
||||
current_user, evaluation.id, valid_notes, comment=comment, do_it=True
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=evaluation.moduleimpl_id,
|
||||
text=f"""Chargement notes dans <a href="{status_url}">{
|
||||
evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}</a>""",
|
||||
url=status_url,
|
||||
max_frequency=30 * 60, # 30 minutes
|
||||
)
|
||||
result = {
|
||||
"etudids_with_decision": etudids_with_decision,
|
||||
"etudids_changed": etudids_changed,
|
||||
"history_menu": {
|
||||
etudid: get_note_history_menu(evaluation.id, etudid)
|
||||
for etudid in etudids_changed
|
||||
},
|
||||
}
|
||||
else:
|
||||
L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
|
||||
if L:
|
||||
nbchanged, _, existing_decisions = notes_add(
|
||||
authuser, evaluation_id, L, comment=comment, do_it=True
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_NOTE,
|
||||
obj=M["moduleimpl_id"],
|
||||
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
|
||||
url=Mod["url"],
|
||||
max_frequency=30 * 60, # 30 minutes
|
||||
)
|
||||
result["nbchanged"] = nbchanged
|
||||
result["existing_decisions"] = existing_decisions
|
||||
if nbchanged > 0:
|
||||
result["history_menu"] = get_note_history_menu(evaluation_id, etudid)
|
||||
else:
|
||||
result["history_menu"] = "" # no update needed
|
||||
result["status"] = "ok"
|
||||
return scu.sendJSON(result)
|
||||
result = {
|
||||
"etudids_changed": [],
|
||||
"etudids_with_decision": [],
|
||||
"history_menu": [],
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_note_history_menu(evaluation_id, etudid):
|
||||
def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
|
||||
"""Menu HTML historique de la note"""
|
||||
history = sco_undo_notes.get_note_history(evaluation_id, etudid)
|
||||
if not history:
|
||||
|
@ -42,6 +42,7 @@ sem_set_list()
|
||||
import flask
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db, log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
@ -52,7 +53,6 @@ from app.scodoc import sco_formsemestre_status
|
||||
from app.scodoc import sco_portal_apogee
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app import log
|
||||
from app.scodoc.sco_etape_bilan import EtapeBilan
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
@ -127,7 +127,7 @@ class SemSet(dict):
|
||||
self.sems = []
|
||||
self.formsemestres = []
|
||||
for formsemestre_id in self.formsemestre_ids:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
self.formsemestres.append(formsemestre)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
self.sems.append(sem)
|
||||
@ -145,12 +145,7 @@ class SemSet(dict):
|
||||
|
||||
# Construction du ou des lien(s) vers le semestre
|
||||
self["semlinks"] = [
|
||||
f"""<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id)
|
||||
}">{formsemestre.titre_annee()}</a>
|
||||
"""
|
||||
for formsemestre in self.formsemestres
|
||||
formsemestre.html_link_status() for formsemestre in self.formsemestres
|
||||
]
|
||||
|
||||
self["semtitles_str"] = "<br>".join(self["semlinks"])
|
||||
@ -383,7 +378,7 @@ class SemSet(dict):
|
||||
|
||||
def html_diagnostic(self):
|
||||
"""Affichage de la partie Effectifs et Liste des étudiants
|
||||
(actif seulement si un portail est configuré)
|
||||
(actif seulement si un portail est configuré) XXX pourquoi ??
|
||||
"""
|
||||
if sco_portal_apogee.has_portal():
|
||||
return self.bilan.html_diagnostic()
|
||||
|
@ -55,6 +55,7 @@ EKEY_APO = "nip"
|
||||
EKEY_SCO = "code_nip"
|
||||
EKEY_NAME = "code NIP"
|
||||
|
||||
|
||||
# view:
|
||||
def formsemestre_synchro_etuds(
|
||||
formsemestre_id,
|
||||
@ -270,11 +271,10 @@ def formsemestre_synchro_etuds(
|
||||
if partitions: # il y a au moins une vraie partition
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
partition_id=partitions[0]["partition_id"]
|
||||
)}">Répartir les groupes de {partitions[0]["partition_name"]}</a></li>
|
||||
"""
|
||||
url_for("scolar.partition_editor",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||
}">Répartir les groupes de {partitions[0]["partition_name"]}</a></li>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append(footer)
|
||||
@ -407,6 +407,7 @@ def list_synch(sem, anneeapogee=None):
|
||||
)
|
||||
#
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
||||
# Tri listes
|
||||
def set_to_sorted_list(etudset, etud_apo=False, is_inscrit=False):
|
||||
def key2etud(key, etud_apo=False):
|
||||
@ -704,7 +705,6 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text=f"Import Apogée de {len(created_etudids)} étudiants en ",
|
||||
obj=sem["formsemestre_id"],
|
||||
max_frequency=10 * 60, # 10'
|
||||
)
|
||||
|
||||
|
||||
|
@ -58,9 +58,8 @@ from flask import flash, g, request, url_for
|
||||
from flask_login import current_user
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
|
||||
from app import db, log
|
||||
from app.models import UniteEns
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import codes_cursus
|
||||
@ -74,6 +73,8 @@ from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
def external_ue_create(
|
||||
@ -114,7 +115,7 @@ def external_ue_create(
|
||||
"is_external": True,
|
||||
},
|
||||
)
|
||||
ue = UniteEns.query.get(ue_id)
|
||||
ue = db.session.get(UniteEns, ue_id)
|
||||
flash(f"UE créée (code {ue.ue_code})")
|
||||
matiere_id = sco_edit_matiere.do_matiere_create(
|
||||
{"ue_id": ue_id, "titre": titre or acronyme, "numero": 1}
|
||||
|
@ -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:
|
||||
SCO_WEBSITE = "https://scodoc.org"
|
||||
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_USERS_LIST = "notes@listes.univ-paris13.fr"
|
||||
SCO_LISTS_URL = "https://scodoc.org/Contact"
|
||||
@ -660,10 +660,10 @@ def bul_filename_old(sem: dict, etud: dict, format):
|
||||
return filename
|
||||
|
||||
|
||||
def bul_filename(formsemestre, etud, format):
|
||||
"""Build a filename for this bulletin"""
|
||||
def bul_filename(formsemestre, etud):
|
||||
"""Build a filename for this bulletin (without suffix)"""
|
||||
dt = time.strftime("%Y-%m-%d")
|
||||
filename = f"bul-{formsemestre.titre_num()}-{dt}-{etud.nom}.{format}"
|
||||
filename = f"bul-{formsemestre.titre_num()}-{dt}-{etud.nom}"
|
||||
filename = make_filename(filename)
|
||||
return filename
|
||||
|
||||
|
@ -15,7 +15,6 @@
|
||||
padding-bottom: 0px;
|
||||
padding-left: 16px;
|
||||
padding-right: 0px;
|
||||
|
||||
background: #FFF;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 8px;
|
||||
@ -40,3 +39,13 @@ div.code_rcue {
|
||||
padding-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.no_niveau {
|
||||
background-color: rgb(245, 237, 200);
|
||||
}
|
||||
|
||||
div.code_jury {
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
width: 64px;
|
||||
}
|
@ -23,7 +23,7 @@
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
form#jury_but {
|
||||
.jury_but_box {
|
||||
margin: 0px 16px 16px 16px;
|
||||
background-color: rgb(253, 253, 231);
|
||||
border: 2px solid rgb(4, 4, 118);
|
||||
@ -35,7 +35,9 @@ form#jury_but {
|
||||
min-width: var(--sco-content-min-width);
|
||||
max-width: var(--sco-content-max-width);
|
||||
}
|
||||
|
||||
div.jury_but_box_title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.but_annee {
|
||||
margin-left: 32px;
|
||||
display: inline-grid;
|
||||
@ -168,6 +170,7 @@ div.but_niveau_ue.recorded_different,
|
||||
div.but_niveau_rcue.recorded_different {
|
||||
box-shadow: 0 0 0 3px red;
|
||||
outline: dashed 3px var(--color-recorded);
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
div.but_niveau_ue.annee_prec {
|
||||
|
13
app/static/css/jury_delete_manual.css
Normal file
13
app/static/css/jury_delete_manual.css
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
div.jury_decisions_list div {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.parcours {
|
||||
color:blueviolet;
|
||||
}
|
||||
|
||||
div.ue_list_etud_validations ul.liste_validations li {
|
||||
margin-bottom: 8px;
|
||||
}
|
@ -144,6 +144,10 @@ div.ue.pair {
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.rcue {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
/* ne fonctionne pas
|
||||
option.non_associe {
|
||||
background-color: yellow;
|
||||
@ -154,3 +158,23 @@ option.non_associe {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
div.ue_validation_code {
|
||||
display: inline-block;
|
||||
}
|
||||
div.ue_validation_code div.code {
|
||||
margin-left: 12px;
|
||||
}
|
||||
select.validation_rcue {
|
||||
color: black;
|
||||
display: inline-block;
|
||||
margin-left: 32px;
|
||||
}
|
||||
div.recap_ects, div.link_edit {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.link_edit a {
|
||||
padding-right: 48px;
|
||||
}
|
@ -106,7 +106,7 @@ body:not(.editionActivated) .editing {
|
||||
|
||||
@keyframes boing {
|
||||
100% {
|
||||
transform: translateY(-20px)
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,6 +152,7 @@ body.editionActivated .filtres>div>div>div>div {
|
||||
color: #000;
|
||||
border-radius: 4px;
|
||||
outline: 4px solid #FFF;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Suppression */
|
||||
@ -354,6 +355,10 @@ body.editionActivated .filtres .nonEditable .move {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.groupe:has(.etudiants:empty) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* .filtres .unselect {
|
||||
background: rgba(0, 153, 204, 0.5) !important;
|
||||
} */
|
||||
@ -361,7 +366,29 @@ body.editionActivated .filtres .nonEditable .move {
|
||||
/*****************************/
|
||||
/* Zone Etudiants */
|
||||
/*****************************/
|
||||
#zoneChoix>.autoAffectation {
|
||||
#zoneChoix summary{
|
||||
margin: 0 0 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#zoneChoix .autoAffectation>a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#zoneChoix .dropZone {
|
||||
background: #FFF;
|
||||
border-radius: 8px;
|
||||
border: 2px dashed #09C;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.fileOver {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
#zoneChoix .autoAffectation {
|
||||
background: #c9c9c9;
|
||||
color: #141414;
|
||||
padding: 4px 8px;
|
||||
@ -369,13 +396,13 @@ body.editionActivated .filtres .nonEditable .move {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#zoneChoix>.autoAffectation>select {
|
||||
#zoneChoix .autoAffectation>select {
|
||||
border: none;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#zoneChoix>.autoAffectation>.affectationGo {
|
||||
#zoneChoix .autoAffectation>.affectationGo {
|
||||
display: inline-block;
|
||||
background: #0c9;
|
||||
padding: 8px 16px;
|
||||
|
@ -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;
|
||||
@ -629,7 +639,7 @@ div.news {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
div.news a {
|
||||
div.news a, div.news a.stdlink {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
@ -905,6 +915,17 @@ td.fichetitre2 .fl {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div.section_but {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
div.section_but > div.link_validation_rcues {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.ficheannotations {
|
||||
background-color: #f7d892;
|
||||
width: 910px;
|
||||
@ -1112,9 +1133,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 +1145,29 @@ 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;
|
||||
background-color: rgb(200, 200, 220);
|
||||
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);
|
||||
}
|
||||
|
||||
div.vertical_spacing_but {
|
||||
@ -2503,13 +2542,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 {
|
||||
@ -2595,16 +2628,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;
|
||||
@ -2773,6 +2796,8 @@ table.notes_recapcomplet a:hover {
|
||||
|
||||
div.table_recap_caption {
|
||||
width: fit-content;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(202, 255, 180);
|
||||
@ -3173,6 +3198,19 @@ li.tf-msg {
|
||||
/* EMO_WARNING, "⚠️" */
|
||||
}
|
||||
|
||||
p.error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
}
|
||||
|
||||
p.error::before {
|
||||
content: "\2049 \fe0f";
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.infop {
|
||||
font-weight: normal;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user