Merge branch 'modif' of https://scodoc.org/git/iziram/ScoDoc into iziram-rev
This commit is contained in:
commit
efa8f617bb
@ -502,12 +502,10 @@ def clear_scodoc_cache():
|
||||
|
||||
|
||||
# --------- Logging
|
||||
def log(msg: str, silent_test=True):
|
||||
def log(msg: str):
|
||||
"""log a message.
|
||||
If Flask app, use configured logger, else stderr.
|
||||
"""
|
||||
if silent_test and current_app and current_app.config["TESTING"]:
|
||||
return
|
||||
try:
|
||||
dept = getattr(g, "scodoc_dept", "")
|
||||
msg = f" ({dept}) {msg}"
|
||||
@ -552,3 +550,22 @@ def scodoc_flash_status_messages():
|
||||
f"Mode test: mails redirigés vers {email_test_mode_address}",
|
||||
category="warning",
|
||||
)
|
||||
|
||||
|
||||
def critical_error(msg):
|
||||
"""Handle a critical error: flush all caches, display message to the user"""
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||
clear_scodoc_cache()
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Une erreur est survenue.
|
||||
|
||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||
{scu.SCO_DISCORD_ASSISTANCE}
|
||||
|
||||
{msg}
|
||||
"""
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Absences
|
||||
|
@ -19,39 +19,34 @@ from app.scodoc.sco_permissions import Permission
|
||||
from flask_login import login_required
|
||||
|
||||
|
||||
from app.models import Identite, Assiduite, FormSemestreInscription, FormSemestre
|
||||
from app.models import Identite, Assiduite, FormSemestre, ModuleImpl
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:assiduiteid>")
|
||||
@api_web_bp.route("/assiduite/<int:assiduiteid>")
|
||||
@bp.route("/assiduite/<int:assiduite_id>")
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# XEV à revoir pour les droits d'accès par département
|
||||
|
||||
|
||||
def assiduite(
|
||||
assiduite_id: int = None,
|
||||
): # XEV xxx_id (sauf pour etudid qui est l'exception qui confirme la règle)
|
||||
def assiduite(assiduite_id: int = None):
|
||||
"""Retourne un objet assiduité à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
{
|
||||
"assiduiteid": 1,
|
||||
"assiduite_id": 1,
|
||||
"etudid": 2,
|
||||
"moduleimpl_id": 3,
|
||||
"date_debut": "2022-10-31T08:00+01:00",
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "retard"
|
||||
"etat": "retard",
|
||||
"desc": "une description",
|
||||
}
|
||||
"""
|
||||
# XEV je pense qu'il faut requeter ainsi pour vérifier qu'on est dans le bon département
|
||||
# afin que quelqu'un avec la paermission ScoView dans son département n'ait pas
|
||||
# accès aux infos des autres départements: à tester
|
||||
|
||||
query = Assiduite.query.filter_by(id=assiduite_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
# if g.scodoc_dept:
|
||||
# query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
assiduite = query.first_or_404()
|
||||
|
||||
@ -264,21 +259,13 @@ def count_assiduites_formsemestre(
|
||||
return jsonify(scass.get_assiduites_stats(assiduites, metric, filter))
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"], defaults={"batch": False})
|
||||
@api_web_bp.route(
|
||||
"/assiduite/<int:etudid>/create", methods=["POST"], defaults={"batch": False}
|
||||
)
|
||||
@bp.route(
|
||||
"/assiduite/<int:etudid>/create/batch", methods=["POST"], defaults={"batch": True}
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduite/<int:etudid>/create/batch", methods=["POST"], defaults={"batch": True}
|
||||
)
|
||||
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
|
||||
@scodoc
|
||||
@login_required
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def create(etudid: int = None, batch: bool = False):
|
||||
def create(etudid: int = None):
|
||||
"""
|
||||
Création d'une assiduité pour l'étudiant (etudid)
|
||||
La requête doit avoir un content type "application/json":
|
||||
@ -298,12 +285,10 @@ def create(etudid: int = None, batch: bool = False):
|
||||
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
|
||||
if batch:
|
||||
|
||||
errors: dict[int, str] = {}
|
||||
success: dict[
|
||||
int,
|
||||
] = {}
|
||||
for i, data in enumerate(request.get_json(force=True).get("batch")):
|
||||
success: dict[int, object] = {}
|
||||
for i, data in enumerate(request.get_json(force=True)):
|
||||
code, obj = create_singular(data, etud)
|
||||
if code == 404:
|
||||
errors[i] = obj
|
||||
@ -312,13 +297,6 @@ def create(etudid: int = None, batch: bool = False):
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
else:
|
||||
code, obj = create_singular(request.get_json(force=True), etud)
|
||||
if code == 404:
|
||||
return json_error(code, obj)
|
||||
else:
|
||||
return jsonify(obj)
|
||||
|
||||
|
||||
def create_singular(
|
||||
data: dict,
|
||||
@ -355,64 +333,54 @@ def create_singular(
|
||||
|
||||
# cas 4 : moduleimpl_id
|
||||
|
||||
moduleimpl_id = data.get("moduleimpl_id", None)
|
||||
if moduleimpl_id is not None:
|
||||
try:
|
||||
moduleimpl_id: int = int(moduleimpl_id)
|
||||
if moduleimpl_id < 0:
|
||||
raise Exception
|
||||
except:
|
||||
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||
moduleimpl: ModuleImpl = None
|
||||
|
||||
if moduleimpl_id is not False:
|
||||
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||
if moduleimpl is None:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
|
||||
# cas 5 : desc
|
||||
|
||||
desc:str = data.get("desc", None)
|
||||
|
||||
if errors != []:
|
||||
err: str = ", ".join(errors)
|
||||
return (404, err)
|
||||
|
||||
# TOUT EST OK
|
||||
nouv_assiduite: Assiduite or int = Assiduite.create_assiduite(
|
||||
|
||||
try:
|
||||
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
|
||||
date_debut=deb,
|
||||
date_fin=fin,
|
||||
etat=etat,
|
||||
etud=etud,
|
||||
module=moduleimpl_id,
|
||||
moduleimpl=moduleimpl,
|
||||
)
|
||||
|
||||
if type(nouv_assiduite) is Assiduite:
|
||||
db.session.add(nouv_assiduite)
|
||||
db.session.commit()
|
||||
return (200, {"assiduiteid": nouv_assiduite.assiduiteid})
|
||||
|
||||
return (200, {"assiduite_id": nouv_assiduite.assiduite_id})
|
||||
except ScoValueError as excp:
|
||||
return (
|
||||
404,
|
||||
{
|
||||
1: "La période sélectionnée est déjà couverte par une autre assiduite",
|
||||
2: "L'étudiant ne participe pas au moduleimpl sélectionné",
|
||||
}.get(nouv_assiduite),
|
||||
excp.args[0],
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/assiduite/delete", methods=["POST"], defaults={"batch": False})
|
||||
@api_web_bp.route("/assiduite/delete", methods=["POST"], defaults={"batch": False})
|
||||
@bp.route(
|
||||
"/assiduite/delete/batch",
|
||||
methods=["POST"],
|
||||
defaults={"batch": True},
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/assiduite/delete/batch",
|
||||
methods=["POST"],
|
||||
defaults={"batch": True},
|
||||
)
|
||||
@bp.route("/assiduite/delete", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/delete", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def delete(batch: bool = False):
|
||||
def delete():
|
||||
"""
|
||||
Suppression d'une assiduité à partir de son id
|
||||
"""
|
||||
if batch:
|
||||
assiduites: list[int] = request.get_json(force=True).get("batch", [])
|
||||
assiduites: list[int] = request.get_json(force=True)
|
||||
output = {"errors": {}, "success": {}}
|
||||
|
||||
for i, ass in enumerate(assiduites):
|
||||
@ -424,16 +392,6 @@ def delete(batch: bool = False):
|
||||
db.session.commit()
|
||||
return jsonify(output)
|
||||
|
||||
else:
|
||||
code, msg = delete_singular(
|
||||
request.get_json(force=True).get("assiduiteid", -1), db
|
||||
)
|
||||
if code == 404:
|
||||
return json_error(code, msg)
|
||||
if code == 200:
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
|
||||
|
||||
def delete_singular(assiduite_id: int, db):
|
||||
assiduite: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
|
||||
@ -443,13 +401,13 @@ def delete_singular(assiduite_id: int, db):
|
||||
return (200, "OK")
|
||||
|
||||
|
||||
@bp.route("/assiduite/<int:assiduiteid>/edit", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:assiduiteid>/edit", methods=["POST"])
|
||||
@bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||
@api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
# @permission_required(Permission.ScoAssiduiteChange)
|
||||
def edit(assiduiteid: int):
|
||||
def edit(assiduite_id: int):
|
||||
"""
|
||||
Edition d'une assiduité à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
@ -458,7 +416,7 @@ def edit(assiduiteid: int):
|
||||
"moduleimpl_id": int
|
||||
}
|
||||
"""
|
||||
assiduite: Assiduite = Assiduite.query.filter_by(id=assiduiteid).first_or_404()
|
||||
assiduite: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first_or_404()
|
||||
errors: List[str] = []
|
||||
data = request.get_json(force=True)
|
||||
|
||||
@ -474,19 +432,22 @@ def edit(assiduiteid: int):
|
||||
|
||||
# Cas 2 : Moduleimpl_id
|
||||
moduleimpl_id = data.get("moduleimpl_id", False)
|
||||
moduleimpl: ModuleImpl = None
|
||||
|
||||
if moduleimpl_id is not False:
|
||||
try:
|
||||
if moduleimpl_id is not None:
|
||||
moduleimpl_id: int = int(moduleimpl_id)
|
||||
if moduleimpl_id < 0 or not Assiduite.verif_moduleimpl(
|
||||
moduleimpl_id, assiduite.etudid
|
||||
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
|
||||
if moduleimpl is None:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
else:
|
||||
if not moduleimpl.est_inscrit(
|
||||
Identite.query.filter_by(id=assiduite.etudid).first()
|
||||
):
|
||||
errors.append("param 'moduleimpl_id': etud non inscrit")
|
||||
|
||||
else:
|
||||
assiduite.moduleimpl_id = moduleimpl_id
|
||||
else:
|
||||
assiduite.moduleimpl_id = moduleimpl_id
|
||||
except:
|
||||
errors.append("param 'moduleimpl_id': invalide")
|
||||
|
||||
if errors != []:
|
||||
err: str = ", ".join(errors)
|
||||
return json_error(404, err)
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -19,6 +19,7 @@ from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.models import GroupDescr, Partition
|
||||
from app.models.groups import group_membership
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -170,24 +171,15 @@ def set_etud_group(etudid: int, group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
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")
|
||||
groups = (
|
||||
GroupDescr.query.filter_by(partition_id=group.partition.id)
|
||||
.join(group_membership)
|
||||
.filter_by(etudid=etudid)
|
||||
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
etudid, group_id, group.partition.to_dict()
|
||||
)
|
||||
ok = False
|
||||
for other_group in groups:
|
||||
if other_group.id == group_id:
|
||||
ok = True
|
||||
else:
|
||||
other_group.etuds.remove(etud)
|
||||
if not ok:
|
||||
group.etuds.append(etud)
|
||||
log(f"set_etud_group({etud}, {group})")
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
|
||||
return jsonify({"group_id": group_id, "etudid": etudid})
|
||||
|
||||
|
||||
@ -207,6 +199,8 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if etud in group.etuds:
|
||||
group.etuds.remove(etud)
|
||||
db.session.commit()
|
||||
@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
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)
|
||||
@ -262,8 +258,10 @@ def group_create(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is None:
|
||||
@ -294,8 +292,10 @@ def group_delete(group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not group.partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
log(f"deleting {group}")
|
||||
db.session.delete(group)
|
||||
@ -318,8 +318,10 @@ def group_edit(group_id: int):
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not group.partition.groups_editable:
|
||||
return json_error(404, "partition non editable")
|
||||
return json_error(403, "partition non editable")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is not None:
|
||||
@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
if not formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
partition_name = data.get("partition_name")
|
||||
if partition_name is None:
|
||||
@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
if not formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(partition_ids, int) and not all(
|
||||
isinstance(x, int) for x in partition_ids
|
||||
@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(group_ids, int) and not all(
|
||||
isinstance(x, int) for x in group_ids
|
||||
@ -484,6 +492,8 @@ def partition_edit(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
modified = False
|
||||
partition_name = data.get("partition_name")
|
||||
@ -542,6 +552,8 @@ def partition_delete(partition_id: int):
|
||||
if g.scodoc_dept:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
partition: Partition = query.first_or_404()
|
||||
if not partition.formsemestre.etat:
|
||||
return json_error(403, "formsemestre verrouillé")
|
||||
if not partition.partition_name:
|
||||
return json_error(404, "ne peut pas supprimer la partition par défaut")
|
||||
is_parcours = partition.is_parcours()
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : outils
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -361,7 +361,7 @@ class BulletinBUT:
|
||||
"formsemestre_id": formsemestre.id,
|
||||
"etat_inscription": etat_inscription,
|
||||
"options": sco_preferences.bulletin_option_affichage(
|
||||
formsemestre.id, self.prefs
|
||||
formsemestre, self.prefs
|
||||
),
|
||||
}
|
||||
if not published:
|
||||
@ -465,6 +465,7 @@ class BulletinBUT:
|
||||
"ressources": {},
|
||||
"saes": {},
|
||||
"ues": {},
|
||||
"ues_capitalisees": {},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7:
|
||||
avec la même interface.
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
from typing import Union
|
||||
|
||||
from flask import g, url_for
|
||||
@ -47,12 +47,14 @@ from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
from app.scodoc import sco_cursus_dut
|
||||
|
||||
|
||||
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
|
||||
|
||||
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
super().__init__(etud, formsemestre_id, res)
|
||||
# Ajustements pour le BUT
|
||||
@ -65,3 +67,117 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
def parcours_validated(self):
|
||||
"True si le parcours est validé"
|
||||
return False # XXX TODO
|
||||
|
||||
|
||||
class EtudCursusBUT:
|
||||
"""L'état de l'étudiant dans son cursus BUT
|
||||
Liste des niveaux validés/à valider
|
||||
"""
|
||||
|
||||
def __init__(self, etud: Identite, formation: Formation):
|
||||
"""formation indique la spécialité préparée"""
|
||||
# Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
|
||||
if formation.id not in (
|
||||
ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
|
||||
):
|
||||
raise ScoValueError(
|
||||
f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}"
|
||||
)
|
||||
if not formation.referentiel_competence:
|
||||
raise ScoNoReferentielCompetences(formation=formation)
|
||||
#
|
||||
self.etud = etud
|
||||
self.formation = formation
|
||||
self.inscriptions = sorted(
|
||||
[
|
||||
ins
|
||||
for ins in etud.formsemestre_inscriptions
|
||||
if ins.formsemestre.formation.referentiel_competence
|
||||
and (
|
||||
ins.formsemestre.formation.referentiel_competence.id
|
||||
== formation.referentiel_competence.id
|
||||
)
|
||||
],
|
||||
key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut),
|
||||
)
|
||||
"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: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
annee, self.parcour
|
||||
)[1]
|
||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
niveaux_d[self.parcour.id] if self.parcour else []
|
||||
)
|
||||
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, ... } }"
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).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]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
|
||||
self.competences = {
|
||||
competence.id: competence
|
||||
for competence in (
|
||||
self.parcour.query_competences()
|
||||
if self.parcour
|
||||
else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
)
|
||||
}
|
||||
"cache { competence_id : competence }"
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
{
|
||||
competence_id : {
|
||||
annee : meilleure_validation
|
||||
}
|
||||
}
|
||||
"""
|
||||
return {
|
||||
competence.id: {
|
||||
annee: {
|
||||
self.validation_par_competence_et_annee.get(competence.id, {}).get(
|
||||
annee
|
||||
)
|
||||
}
|
||||
for annee in ("BUT1", "BUT2", "BUT3")
|
||||
}
|
||||
for competence in self.competences.values()
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
from xml.etree import ElementTree
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -64,7 +64,7 @@ import re
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
from flask import g, url_for
|
||||
from flask import flash, g, url_for
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
@ -91,9 +91,15 @@ from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
BUT_CODES_ORDERED,
|
||||
CODES_RCUE_VALIDES,
|
||||
CODES_UE_VALIDES,
|
||||
RED,
|
||||
UE_STANDARD,
|
||||
)
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoException, ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
|
||||
class NoRCUEError(ScoValueError):
|
||||
@ -170,7 +176,7 @@ class DecisionsProposees:
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} valid={self.code_valide
|
||||
} codes={self.codes} explanation={self.explanation}"""
|
||||
} codes={self.codes} explanation={self.explanation}>"""
|
||||
|
||||
|
||||
class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
@ -204,7 +210,12 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
etud: Identite,
|
||||
formsemestre: FormSemestre,
|
||||
):
|
||||
assert formsemestre.formation.is_apc()
|
||||
if formsemestre.formation.referentiel_competence is None:
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
||||
super().__init__(etud=etud)
|
||||
self.formsemestre = formsemestre
|
||||
"le formsemestre utilisé pour construire ce deca"
|
||||
self.formsemestre_id = formsemestre.id
|
||||
"l'id du formsemestre utilisé pour construire ce deca"
|
||||
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre)
|
||||
@ -219,23 +230,34 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
)
|
||||
)
|
||||
)
|
||||
# Si les années scolaires sont distinctes, on est "à cheval"
|
||||
self.a_cheval = (
|
||||
formsemestre_impair
|
||||
and formsemestre_pair
|
||||
and formsemestre_impair.annee_scolaire()
|
||||
!= formsemestre_pair.annee_scolaire()
|
||||
)
|
||||
"vrai si on groupe deux semestres d'années scolaires différentes"
|
||||
# Si on part d'un semestre IMPAIR, il n'y aura pas de décision année proposée
|
||||
# (mais on pourra évidemment valider des UE et même des RCUE)
|
||||
self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6)
|
||||
"vrai si jury de fin d'année scolaire (propose code annuel)"
|
||||
|
||||
self.formsemestre_impair = formsemestre_impair
|
||||
"le 1er semestre de l'année scolaire considérée (S1, S3, S5)"
|
||||
"le 1er semestre du groupement (S1, S3, S5)"
|
||||
self.formsemestre_pair = formsemestre_pair
|
||||
"le second formsemestre de la même année scolaire (S2, S4, S6)"
|
||||
"le second formsemestre (S2, S4, S6), de la même année scolaire ou d'une précédente"
|
||||
formsemestre_last = formsemestre_pair or formsemestre_impair
|
||||
"le formsemestre le plus avancé dans cette année"
|
||||
"le formsemestre le plus avancé (en indice de semestre) dans le groupement"
|
||||
|
||||
self.annee_but = (formsemestre_last.semestre_id + 1) // 2
|
||||
"le rang de l'année dans le BUT: 1, 2, 3"
|
||||
assert self.annee_but in (1, 2, 3)
|
||||
self.rcues_annee = []
|
||||
"RCUEs de l'année"
|
||||
"""RCUEs de l'année
|
||||
(peuvent concerner l'année scolaire antérieur pour les redoublants
|
||||
avec UE capitalisées)
|
||||
"""
|
||||
self.inscription_etat = etud.inscription_etat(formsemestre_last.id)
|
||||
"état de l'inscription dans le semestre le plus avancé (pair si année complète)"
|
||||
self.inscription_etat_pair = (
|
||||
@ -334,8 +356,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
)
|
||||
"vrai si l'année est réussie, tous niveaux validables ou validés par le jury"
|
||||
self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2)
|
||||
"Peut passer si plus de la moitié validables et tous > 8"
|
||||
"Vrai si plus de la moitié des RCUE validables"
|
||||
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
|
||||
"Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8"
|
||||
# XXX TODO ajouter condition pour passage en S5
|
||||
|
||||
# Enfin calcule les codes des UE:
|
||||
@ -343,12 +366,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
dec_ue.compute_codes()
|
||||
|
||||
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
|
||||
expl_rcues = (
|
||||
f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}"
|
||||
)
|
||||
plural = self.nb_validables > 1
|
||||
expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
|
||||
"s" if plural else ""} sur {self.nb_competences}"""
|
||||
if self.admis:
|
||||
self.codes = [sco_codes.ADM] + self.codes
|
||||
self.explanation = expl_rcues
|
||||
# elif not self.jury_annuel:
|
||||
# self.codes = [] # pas de décision annuelle sur semestres impairs
|
||||
elif self.inscription_etat != scu.INSCRIT:
|
||||
@ -364,9 +386,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.ABL,
|
||||
sco_codes.EXCLU,
|
||||
]
|
||||
expl_rcues = ""
|
||||
elif self.passage_de_droit:
|
||||
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
|
||||
self.explanation = expl_rcues
|
||||
elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante
|
||||
self.codes = [
|
||||
sco_codes.RED,
|
||||
@ -374,7 +396,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.PAS1NCI,
|
||||
sco_codes.ADJ,
|
||||
] + self.codes
|
||||
self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8"
|
||||
expl_rcues += f" et {self.nb_rcues_under_8} < 8"
|
||||
else:
|
||||
self.codes = [
|
||||
sco_codes.RED,
|
||||
@ -383,17 +405,21 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.ADJ,
|
||||
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
||||
] + self.codes
|
||||
self.explanation = (
|
||||
expl_rcues
|
||||
+ f""" et {self.nb_rcues_under_8}
|
||||
niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||
)
|
||||
expl_rcues += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||
|
||||
# Si l'un des semestres est extérieur, propose ADM
|
||||
if (
|
||||
self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT"
|
||||
) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"):
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
|
||||
self.explanation = f"<div>{expl_rcues}</div>"
|
||||
messages = self.descr_pb_coherence()
|
||||
if messages:
|
||||
self.explanation += (
|
||||
'<div class="warning">'
|
||||
+ '</div><div class="warning">'.join(messages)
|
||||
+ "</div>"
|
||||
)
|
||||
#
|
||||
|
||||
def infos(self) -> str:
|
||||
@ -526,9 +552,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
|
||||
def compute_rcues_annee(self) -> list[RegroupementCoherentUE]:
|
||||
"""Liste des regroupements d'UE à considérer cette année.
|
||||
Pour le moment on ne considère pas de RCUE à cheval sur plusieurs années (redoublants).
|
||||
On peut avoir un RCUE à cheval sur plusieurs années (redoublants avec UE capitalisées).
|
||||
Si on n'a pas les deux semestres, aucun RCUE.
|
||||
Raises ScoValueError s'il y a des UE sans RCUE.
|
||||
"""
|
||||
if self.formsemestre_pair is None or self.formsemestre_impair is None:
|
||||
return []
|
||||
@ -537,6 +562,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
for ue_pair in self.ues_pair:
|
||||
rcue = None
|
||||
for ue_impair in self.ues_impair:
|
||||
if self.a_cheval:
|
||||
# l'UE paire DOIT être capitalisée pour être utilisée
|
||||
if (
|
||||
self.decisions_ues[ue_pair.id].code_valide
|
||||
not in CODES_UE_VALIDES
|
||||
):
|
||||
continue # ignore cette UE antérieure non capitalisée
|
||||
# et l'UE impaire doit être actuellement meilleure que
|
||||
# celle éventuellement capitalisée
|
||||
if (
|
||||
self.decisions_ues[ue_impair.id].ue_status
|
||||
and self.decisions_ues[ue_impair.id].ue_status["is_capitalized"]
|
||||
):
|
||||
continue # ignore cette UE car capitalisée et actuelle moins bonne
|
||||
if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
|
||||
rcue = RegroupementCoherentUE(
|
||||
self.etud,
|
||||
@ -548,19 +587,22 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
)
|
||||
ues_impair_sans_rcue.discard(ue_impair.id)
|
||||
break
|
||||
if rcue is None:
|
||||
raise NoRCUEError(deca=self, ue=ue_pair)
|
||||
# if rcue is None and not self.a_cheval:
|
||||
# raise NoRCUEError(deca=self, ue=ue_pair)
|
||||
if rcue is not None:
|
||||
rcues_annee.append(rcue)
|
||||
if len(ues_impair_sans_rcue) > 0:
|
||||
ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
|
||||
raise NoRCUEError(deca=self, ue=ue)
|
||||
# Si jury annuel (pas à cheval), on doit avoir tous les RCUEs:
|
||||
# if len(ues_impair_sans_rcue) > 0 and not self.a_cheval:
|
||||
# ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
|
||||
# raise NoRCUEError(deca=self, ue=ue)
|
||||
return rcues_annee
|
||||
|
||||
def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
|
||||
"""Pour chaque niveau de compétence de cette année, construit
|
||||
le DecisionsProposeesRCUE,
|
||||
ou None s'il n'y en a pas
|
||||
le DecisionsProposeesRCUE, ou None s'il n'y en a pas
|
||||
(ne devrait pas arriver car compute_rcues_annee vérifie déjà cela).
|
||||
|
||||
Appelé à la construction du deca, donc avant décisions manuelles.
|
||||
Return: { niveau_id : DecisionsProposeesRCUE }
|
||||
"""
|
||||
# Retrouve le RCUE associé à chaque niveau
|
||||
@ -591,22 +633,42 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
d[dec_rcue.rcue.ue_2.id] = dec_rcue
|
||||
return d
|
||||
|
||||
def next_annee_semestre_id(self, code: str) -> int:
|
||||
"""L'indice du semestre dans lequel l'étudiant est autorisé à
|
||||
poursuivre l'année suivante. None si aucun."""
|
||||
if self.formsemestre_pair is None:
|
||||
return None # seulement sur année
|
||||
if code == RED:
|
||||
return self.formsemestre_pair.semestre_id - 1
|
||||
elif (
|
||||
code in sco_codes.BUT_CODES_PASSAGE
|
||||
def next_semestre_ids(self, code: str) -> set[int]:
|
||||
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
||||
à poursuivre après le semestre courant.
|
||||
"""
|
||||
ids = set()
|
||||
# La poursuite d'études dans un semestre pair d’une même année
|
||||
# est de droit pour tout étudiant:
|
||||
if (self.formsemestre.semestre_id % 2) and sco_codes.ParcoursBUT.NB_SEM:
|
||||
ids.add(self.formsemestre.semestre_id + 1)
|
||||
|
||||
# La poursuite d’études dans un semestre impair est possible si
|
||||
# et seulement si l’étudiant a obtenu :
|
||||
# - la moyenne à plus de la moitié des regroupements cohérents d’UE ;
|
||||
# - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE.
|
||||
#
|
||||
# La condition a paru trop stricte à de nombreux collègues.
|
||||
# ScoDoc ne contraint donc pas à la respecter strictement.
|
||||
# Si le code est dans BUT_CODES_PASSAGE (ADM, ADJ, PASD, PAS1NCI, ATJ),
|
||||
# autorise à passer dans le semestre suivant
|
||||
if (
|
||||
self.jury_annuel
|
||||
and code in sco_codes.BUT_CODES_PASSAGE
|
||||
and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM
|
||||
):
|
||||
return self.formsemestre_pair.semestre_id + 1
|
||||
return None
|
||||
ids.add(self.formsemestre.semestre_id + 1)
|
||||
|
||||
if code == RED:
|
||||
ids.add(
|
||||
self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2
|
||||
)
|
||||
|
||||
return ids
|
||||
|
||||
def record_form(self, form: dict):
|
||||
"""Enregistre les codes de jury en base
|
||||
à partir d'un dict représentant le formulaire jury BUT:
|
||||
form dict:
|
||||
- 'code_ue_1896' : 'AJ' code pour l'UE id 1896
|
||||
- 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6
|
||||
@ -616,7 +678,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
|
||||
"""
|
||||
log("jury_but.DecisionsProposeesAnnee.record_form")
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
code_annee = None
|
||||
codes_rcues = [] # [ (dec_rcue, code), ... ]
|
||||
codes_ues = [] # [ (dec_ue, code), ... ]
|
||||
for key in form:
|
||||
code = form[key]
|
||||
# Codes d'UE
|
||||
@ -626,7 +690,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
dec_ue = self.decisions_ues.get(ue_id)
|
||||
if not dec_ue:
|
||||
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
||||
dec_ue.record(code)
|
||||
codes_ues.append((dec_ue, code))
|
||||
else:
|
||||
# Codes de RCUE
|
||||
m = re.match(r"^code_rcue_(\d+)$", key)
|
||||
@ -635,31 +699,39 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id)
|
||||
if not dec_rcue:
|
||||
raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}")
|
||||
dec_rcue.record(code)
|
||||
codes_rcues.append((dec_rcue, code))
|
||||
elif key == "code_annee":
|
||||
# Code annuel
|
||||
self.record(code)
|
||||
code_annee = code
|
||||
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
# Enregistre les codes, dans l'ordre UE, RCUE, Année
|
||||
for dec_ue, code in codes_ues:
|
||||
dec_ue.record(code)
|
||||
for dec_rcue, code in codes_rcues:
|
||||
dec_rcue.record(code)
|
||||
self.record(code_annee)
|
||||
self.record_all()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
||||
Si no_overwrite, ne fait rien si un code est déjà enregistré.
|
||||
Si l'étudiant est DEM ou DEF, ne fait rien.
|
||||
"""
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
return
|
||||
return False
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}"
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
|
||||
if code != self.code_valide and (self.code_valide is None or not no_overwrite):
|
||||
# Enregistrement du code annuel BUT
|
||||
if self.validation:
|
||||
db.session.delete(self.validation)
|
||||
db.session.flush()
|
||||
db.session.commit()
|
||||
if code is None:
|
||||
self.validation = None
|
||||
else:
|
||||
@ -670,32 +742,32 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
annee_scolaire=self.annee_scolaire(),
|
||||
code=code,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
log(f"Recording {self}: {code}")
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: {code}",
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
|
||||
# --- Autorisation d'inscription dans semestre suivant ?
|
||||
if self.formsemestre_pair is not None:
|
||||
if code is None:
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
etudid=self.etud.id,
|
||||
origin_formsemestre_id=self.formsemestre_pair.id,
|
||||
origin_formsemestre_id=self.formsemestre.id,
|
||||
)
|
||||
else:
|
||||
next_semestre_id = self.next_annee_semestre_id(code)
|
||||
if next_semestre_id is not None:
|
||||
for next_semestre_id in self.next_semestre_ids(code):
|
||||
ScolarAutorisationInscription.autorise_etud(
|
||||
self.etud.id,
|
||||
self.formsemestre_pair.formation.formation_code,
|
||||
self.formsemestre_pair.id,
|
||||
self.formsemestre.formation.formation_code,
|
||||
self.formsemestre.id,
|
||||
next_semestre_id,
|
||||
)
|
||||
|
||||
self.recorded = True
|
||||
db.session.commit()
|
||||
self.recorded = True
|
||||
self.invalidate_formsemestre_cache()
|
||||
return True
|
||||
|
||||
def invalidate_formsemestre_cache(self):
|
||||
"invalide le résultats des deux formsemestres"
|
||||
@ -706,29 +778,71 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if self.formsemestre_pair is not None:
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
|
||||
|
||||
def record_all(self):
|
||||
def record_all(
|
||||
self, no_overwrite: bool = True, only_validantes: bool = False
|
||||
) -> bool:
|
||||
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
|
||||
et sont donc en mode "automatique"
|
||||
et sont donc en mode "automatique".
|
||||
- Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente.
|
||||
- Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne.
|
||||
|
||||
Si only_validantes, n'enregistre que des décisions "validantes" de droit: ADM ou CMP.
|
||||
|
||||
Return: True si au moins un code modifié et enregistré.
|
||||
"""
|
||||
decisions = (
|
||||
list(self.decisions_ues.values())
|
||||
+ list(self.decisions_rcue_by_niveau.values())
|
||||
+ [self]
|
||||
)
|
||||
for dec in decisions:
|
||||
if not dec.recorded:
|
||||
modif = False
|
||||
# Toujours valider dans l'ordre UE, RCUE, Année
|
||||
annee_scolaire = self.formsemestre.annee_scolaire()
|
||||
# UEs
|
||||
for dec_ue in self.decisions_ues.values():
|
||||
if (
|
||||
not dec_ue.recorded
|
||||
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
|
||||
# rappel: le code par défaut est en tête
|
||||
code = dec.codes[0] if dec.codes else None
|
||||
code = dec_ue.codes[0] if dec_ue.codes else None
|
||||
if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
|
||||
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
||||
dec.record(code, no_overwrite=True)
|
||||
# (no_overwrite=True) sauf en mode test yaml
|
||||
modif |= dec_ue.record(code, no_overwrite=no_overwrite)
|
||||
# RCUE :
|
||||
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
||||
code = dec_rcue.codes[0] if dec_rcue.codes else None
|
||||
if (
|
||||
(not dec_rcue.recorded)
|
||||
and ( # enregistre seulement si pas déjà validé "mieux"
|
||||
(not dec_rcue.validation)
|
||||
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
|
||||
< BUT_CODES_ORDERED.get(code, 0)
|
||||
)
|
||||
and ( # décision validante de droit ?
|
||||
(
|
||||
(not only_validantes)
|
||||
or code in sco_codes.CODES_RCUE_VALIDES_DE_DROIT
|
||||
)
|
||||
)
|
||||
):
|
||||
modif |= dec_rcue.record(code, no_overwrite=no_overwrite)
|
||||
# Année:
|
||||
if not self.recorded:
|
||||
# rappel: le code par défaut est en tête
|
||||
code = self.codes[0] if self.codes else None
|
||||
# enregistre le code jury seulement s'il n'y a pas déjà de code
|
||||
# (no_overwrite=True) sauf en mode test yaml
|
||||
if (
|
||||
not only_validantes
|
||||
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
||||
modif |= self.record(code, no_overwrite=no_overwrite)
|
||||
|
||||
return modif
|
||||
|
||||
def erase(self, only_one_sem=False):
|
||||
"""Efface les décisions de jury de cet étudiant
|
||||
pour cette année: décisions d'UE, de RCUE, d'année,
|
||||
et autorisations d'inscription émises.
|
||||
Efface même si étudiant DEM ou DEF.
|
||||
Si à cheval, n'efface que pour le semestre d'origine du deca.
|
||||
"""
|
||||
if only_one_sem:
|
||||
if only_one_sem or self.a_cheval:
|
||||
# N'efface que les autorisations venant de ce semestre,
|
||||
# et les validations de ses UEs
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
@ -757,22 +871,37 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
)
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
db.session.flush()
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
||||
)
|
||||
|
||||
# Efface éventuelles validations de semestre
|
||||
# (en principe inutilisées en BUT)
|
||||
# et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
|
||||
#
|
||||
for validation in ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=self.formsemestre_id
|
||||
):
|
||||
db.session.delete(validation)
|
||||
|
||||
db.session.commit()
|
||||
self.invalidate_formsemestre_cache()
|
||||
|
||||
def get_autorisations_passage(self) -> list[int]:
|
||||
"""Les liste des indices de semestres auxquels on est autorisé à
|
||||
s'inscrire depuis cette année"""
|
||||
formsemestre = self.formsemestre_pair or self.formsemestre_impair
|
||||
if not formsemestre:
|
||||
return []
|
||||
return [
|
||||
"""Liste des indices de semestres auxquels on est autorisé à
|
||||
s'inscrire depuis le semestre courant.
|
||||
"""
|
||||
return sorted(
|
||||
[
|
||||
a.semestre_id
|
||||
for a in ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
origin_formsemestre_id=formsemestre.id,
|
||||
origin_formsemestre_id=self.formsemestre.id,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
|
||||
"""Description textuelle des niveaux validés (enregistrés)
|
||||
@ -800,12 +929,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
validations.append(", ".join(v for v in valids if v))
|
||||
return line_sep.join(validations)
|
||||
|
||||
def descr_pb_coherence(self) -> list[str]:
|
||||
"""Description d'éventuels problèmes de cohérence entre
|
||||
les décisions *enregistrées* d'UE et de RCUE.
|
||||
Note: en principe, la cohérence RCUE/UE est assurée au moment de
|
||||
l'enregistrement (record).
|
||||
Mais la base peut avoir été modifiée par d'autres voies.
|
||||
"""
|
||||
messages = []
|
||||
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
||||
if dec_rcue.code_valide in CODES_RCUE_VALIDES:
|
||||
for ue in (dec_rcue.rcue.ue_1, dec_rcue.rcue.ue_2):
|
||||
dec_ue = self.decisions_ues.get(ue.id)
|
||||
if dec_ue:
|
||||
if dec_ue.code_valide not in CODES_UE_VALIDES:
|
||||
messages.append(
|
||||
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
|
||||
)
|
||||
else:
|
||||
messages.append(f"L'UE {ue.acronyme} n'a pas décision (???)")
|
||||
return messages
|
||||
|
||||
|
||||
def list_ue_parcour_etud(
|
||||
formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT
|
||||
) -> tuple[ApcParcours, list[UniteEns]]:
|
||||
"""Parcour dans lequel l'étudiant est inscrit,
|
||||
et liste des UEs à valider pour ce semestre
|
||||
et liste des UEs à valider pour ce semestre (sans les UE "dispensées")
|
||||
"""
|
||||
if res.etuds_parcour_id[etud.id] is None:
|
||||
parcour = None
|
||||
@ -820,6 +970,7 @@ def list_ue_parcour_etud(
|
||||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
ues = [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues]
|
||||
return parcour, ues
|
||||
|
||||
|
||||
@ -845,6 +996,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
inscription_etat: str = scu.INSCRIT,
|
||||
):
|
||||
super().__init__(etud=dec_prop_annee.etud)
|
||||
self.deca = dec_prop_annee
|
||||
self.rcue = rcue
|
||||
if rcue is None: # RCUE non dispo, eg un seul semestre
|
||||
self.codes = []
|
||||
@ -876,30 +1028,48 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
or dec_prop_annee.formsemestre_pair.modalite == "EXT"
|
||||
):
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
# S'il y a une décision enregistrée: si elle est plus favorable que celle que l'on
|
||||
# proposerait, la place en tête.
|
||||
# Sinon, la place en seconde place
|
||||
if self.code_valide and self.code_valide != self.codes[0]:
|
||||
code_default = self.codes[0]
|
||||
if self.code_valide in self.codes:
|
||||
self.codes.remove(self.code_valide)
|
||||
if sco_codes.BUT_CODES_ORDERED.get(
|
||||
self.code_valide, 0
|
||||
) > sco_codes.BUT_CODES_ORDERED.get(code_default, 0):
|
||||
self.codes.insert(0, self.code_valide)
|
||||
else:
|
||||
self.codes.insert(1, self.code_valide)
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
"""Enregistre le code"""
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
|
||||
} codes={self.codes} explanation={self.explanation}"""
|
||||
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code RCUE.
|
||||
Note:
|
||||
- si le RCUE est ADJ, les UE non validées sont passées à ADJ
|
||||
XXX on pourra imposer ici d'autres règles de cohérence
|
||||
"""
|
||||
if self.rcue is None:
|
||||
return # pas de RCUE a enregistrer
|
||||
return False # pas de RCUE a enregistrer
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
return
|
||||
return False
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
return False # no change
|
||||
parcours_id = self.parcour.id if self.parcour is not None else None
|
||||
if self.validation:
|
||||
db.session.delete(self.validation)
|
||||
db.session.flush()
|
||||
db.session.commit()
|
||||
if code is None:
|
||||
self.validation = None
|
||||
else:
|
||||
# log(
|
||||
# f"RCUE.record(etudid={self.etud.id}, ue1_id={self.rcue.ue_1.id}, ue2_id={self.rcue.ue_2.id}, code={code} )"
|
||||
# )
|
||||
self.validation = ApcValidationRCUE(
|
||||
etudid=self.etud.id,
|
||||
formsemestre_id=self.rcue.formsemestre_2.id,
|
||||
@ -908,12 +1078,31 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
parcours_id=parcours_id,
|
||||
code=code,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation RCUE {repr(self.rcue)}",
|
||||
msg=f"Validation {self.rcue}: {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
log(f"rcue.record {self}: {code}")
|
||||
|
||||
# Modifie au besoin les codes d'UE
|
||||
if code == "ADJ":
|
||||
deca = self.deca
|
||||
for ue_id in (self.rcue.ue_1.id, self.rcue.ue_2.id):
|
||||
dec_ue = deca.decisions_ues.get(ue_id)
|
||||
if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES:
|
||||
log(f"rcue.record: force ADJR sur {dec_ue}")
|
||||
flash(
|
||||
f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR"""
|
||||
)
|
||||
dec_ue.record(sco_codes.ADJR)
|
||||
|
||||
# Valide les niveaux inférieurs de la compétence (code ADSUP)
|
||||
# TODO
|
||||
|
||||
if self.rcue.formsemestre_1 is not None:
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=self.rcue.formsemestre_1.id
|
||||
@ -922,13 +1111,16 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=self.rcue.formsemestre_2.id
|
||||
)
|
||||
self.code_valide = code # mise à jour état
|
||||
self.recorded = True
|
||||
return True
|
||||
|
||||
def erase(self):
|
||||
"""Efface la décision de jury de cet étudiant pour cet RCUE"""
|
||||
# par prudence, on requete toutes les validations, en cas de doublons
|
||||
validations = self.rcue.query_validations()
|
||||
for validation in validations:
|
||||
log(f"DecisionsProposeesRCUE: deleting {validation}")
|
||||
db.session.delete(validation)
|
||||
db.session.flush()
|
||||
|
||||
@ -960,7 +1152,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
sinon si compensation dans RCUE: CMP
|
||||
sinon: ADJ, AJ
|
||||
|
||||
et proposer toujours: RAT, DEF, ABAN, DEM, UEBSL (codes_communs)
|
||||
et proposer toujours: RAT, DEF, ABAN, ADJR, DEM, UEBSL (codes_communs)
|
||||
"""
|
||||
|
||||
# Codes toujours proposés sauf si include_communs est faux:
|
||||
@ -968,6 +1160,7 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
sco_codes.RAT,
|
||||
sco_codes.DEF,
|
||||
sco_codes.ABAN,
|
||||
sco_codes.ADJR,
|
||||
sco_codes.ATJ,
|
||||
sco_codes.DEM,
|
||||
sco_codes.UEBSL,
|
||||
@ -982,14 +1175,14 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
):
|
||||
# Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non)
|
||||
# mais ici on a restreint au formsemestre donc une seule (prend la première)
|
||||
self.validation = ScolarFormSemestreValidation.query.filter_by(
|
||||
validation = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
|
||||
).first()
|
||||
super().__init__(
|
||||
etud=etud,
|
||||
code_valide=self.validation.code if self.validation is not None else None,
|
||||
code_valide=validation.code if validation is not None else None,
|
||||
)
|
||||
# log(f"built {self}")
|
||||
self.validation = validation
|
||||
self.formsemestre = formsemestre
|
||||
self.ue: UniteEns = ue
|
||||
self.rcue: RegroupementCoherentUE = None
|
||||
@ -1026,9 +1219,13 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
self.moy_ue_with_cap = ue_status["moy"]
|
||||
self.ue_status = ue_status
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
|
||||
} codes={self.codes} explanation={self.explanation}>"""
|
||||
|
||||
def set_rcue(self, rcue: RegroupementCoherentUE):
|
||||
"""Rattache cette UE à un RCUE. Cela peut modifier les codes
|
||||
proposés (si compensation)"""
|
||||
proposés par compute_codes() (si compensation)"""
|
||||
self.rcue = rcue
|
||||
|
||||
def compute_codes(self):
|
||||
@ -1048,9 +1245,10 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
|
||||
self.explanation = "notes insuffisantes"
|
||||
|
||||
def record(self, code: str, no_overwrite=False):
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
"""Enregistre le code jury pour cette UE.
|
||||
Si no_overwrite, n'enregistre pas s'il y a déjà un code.
|
||||
Return: True si code enregistré (modifié)
|
||||
"""
|
||||
if code and not code in self.codes:
|
||||
raise ScoValueError(
|
||||
@ -1058,10 +1256,16 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
)
|
||||
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
|
||||
self.recorded = True
|
||||
return # no change
|
||||
return False # no change
|
||||
self.erase()
|
||||
if code is None:
|
||||
self.validation = None
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}: effacée",
|
||||
commit=True,
|
||||
)
|
||||
else:
|
||||
self.validation = ScolarFormSemestreValidation(
|
||||
etudid=self.etud.id,
|
||||
@ -1070,16 +1274,20 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
code=code,
|
||||
moy_ue=self.moy_ue,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation UE {self.ue.id}",
|
||||
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}({self.moy_ue}): {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
log(f"DecisionsProposeesUE: recording {self.validation}")
|
||||
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
||||
self.code_valide = code # mise à jour
|
||||
self.recorded = True
|
||||
return True
|
||||
|
||||
def erase(self):
|
||||
"""Efface la décision de jury de cet étudiant pour cette UE"""
|
||||
@ -1090,7 +1298,13 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
for validation in validations:
|
||||
log(f"DecisionsProposeesUE: deleting {validation}")
|
||||
db.session.delete(validation)
|
||||
db.session.flush()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation UE {validation.ue.id} {validation.ue.acronyme}: effacée",
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def descr_validation(self) -> str:
|
||||
"""Description validation niveau enregistrée, pour PV jury.
|
||||
@ -1106,7 +1320,7 @@ class BUTCursusEtud: # WIP TODO
|
||||
|
||||
def __init__(self, formsemestre: FormSemestre, etud: Identite):
|
||||
if formsemestre.formation.referentiel_competence is None:
|
||||
raise ScoException("BUTCursusEtud: pas de référentiel de compétences")
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
|
||||
assert len(etud.formsemestre_inscriptions) > 0
|
||||
self.formsemestre = formsemestre
|
||||
self.etud = etud
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury BUT: table recap annuelle et liens saisie
|
||||
"""
|
||||
|
||||
import collections
|
||||
import time
|
||||
import numpy as np
|
||||
from flask import g, url_for
|
||||
@ -31,7 +32,7 @@ from app.scodoc.sco_codes_parcours import (
|
||||
from app.scodoc import sco_formsemestre_status
|
||||
from app.scodoc import sco_pvjury
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
|
||||
|
||||
def formsemestre_saisie_jury_but(
|
||||
@ -62,16 +63,9 @@ def formsemestre_saisie_jury_but(
|
||||
# raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
|
||||
|
||||
if formsemestre2.formation.referentiel_competence is None:
|
||||
raise ScoValueError(
|
||||
"""
|
||||
<p>Pas de référentiel de compétences associé à la formation !</p>
|
||||
<p>Pour associer un référentiel, passer par le menu <b>Semestre /
|
||||
Voir la formation... </b> et suivre le lien <em>"associer à un référentiel
|
||||
de compétences"</em>
|
||||
"""
|
||||
)
|
||||
raise ScoNoReferentielCompetences(formation=formsemestre2.formation)
|
||||
|
||||
rows, titles, column_ids = get_jury_but_table(
|
||||
rows, titles, column_ids, jury_stats = get_jury_but_table(
|
||||
formsemestre2, read_only=read_only, mode=mode
|
||||
)
|
||||
if not rows:
|
||||
@ -153,6 +147,28 @@ def formsemestre_saisie_jury_but(
|
||||
f"""
|
||||
</div>
|
||||
|
||||
<div class="jury_stats">
|
||||
<div>Nb d'étudiants avec décision annuelle:
|
||||
{sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]}
|
||||
</div>
|
||||
<div><b>Codes annuels octroyés:</b></div>
|
||||
<table class="jury_stats_codes">
|
||||
"""
|
||||
)
|
||||
for code in sorted(jury_stats["codes_annuels"].keys()):
|
||||
H.append(
|
||||
f"""<tr>
|
||||
<td>{code}</td>
|
||||
<td style="text-align:right">{jury_stats["codes_annuels"][code]}</td>
|
||||
<td style="text-align:right">{
|
||||
(100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}%
|
||||
</td>
|
||||
</tr>"""
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
</table>
|
||||
</div>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
)
|
||||
@ -268,6 +284,10 @@ class RowCollector:
|
||||
self["_nom_disp_order"] = etud.sort_key
|
||||
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
||||
self["_nom_short_data"] = {
|
||||
"etudid": etud.id,
|
||||
"nomprenom": etud.nomprenom,
|
||||
}
|
||||
if with_links:
|
||||
self["_nom_short_order"] = etud.sort_key
|
||||
self["_nom_short_target"] = url_for(
|
||||
@ -352,10 +372,6 @@ class RowCollector:
|
||||
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
||||
"col_rcue col_rcues_validables" + klass,
|
||||
)
|
||||
self["_rcues_validables_data"] = {
|
||||
"etudid": deca.etud.id,
|
||||
"nomprenom": deca.etud.nomprenom,
|
||||
}
|
||||
if len(deca.rcues_annee) > 0:
|
||||
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
|
||||
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
|
||||
@ -377,10 +393,17 @@ class RowCollector:
|
||||
|
||||
def get_jury_but_table(
|
||||
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
|
||||
) -> tuple[list[dict], list[str], list[str]]:
|
||||
"""Construit la table des résultats annuels pour le jury BUT"""
|
||||
) -> tuple[list[dict], list[str], list[str], dict]:
|
||||
"""Construit la table des résultats annuels pour le jury BUT
|
||||
=> rows_dict, titles, column_ids, jury_stats
|
||||
où jury_stats est un dict donnant des comptages sur le jury.
|
||||
"""
|
||||
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
||||
titles = {} # column_id : title
|
||||
jury_stats = {
|
||||
"nb_etuds": len(formsemestre2.etuds_inscriptions),
|
||||
"codes_annuels": collections.Counter(),
|
||||
}
|
||||
column_classes = {}
|
||||
rows = []
|
||||
for etudid in formsemestre2.etuds_inscriptions:
|
||||
@ -417,6 +440,8 @@ def get_jury_but_table(
|
||||
f"""{deca.code_valide or ''}""",
|
||||
"col_code_annee",
|
||||
)
|
||||
if deca.code_valide:
|
||||
jury_stats["codes_annuels"][deca.code_valide] += 1
|
||||
# --- Le lien de saisie
|
||||
if mode != "recap" and with_links:
|
||||
row.add_cell(
|
||||
@ -439,11 +464,14 @@ def get_jury_but_table(
|
||||
rows.append(row)
|
||||
rows_dict = [row.get_row_dict() for row in rows]
|
||||
if len(rows_dict) > 0:
|
||||
res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1)
|
||||
col_idx = res2.recap_add_partitions(
|
||||
rows_dict, titles, col_idx=row.last_etud_cell_idx + 1
|
||||
)
|
||||
res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1)
|
||||
column_ids = [title for title in titles if not title.startswith("_")]
|
||||
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
|
||||
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
|
||||
return rows_dict, titles, column_ids
|
||||
return rows_dict, titles, column_ids, jury_stats
|
||||
|
||||
|
||||
def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -15,25 +15,32 @@ from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def formsemestre_validation_auto_but(formsemestre: FormSemestre, only_adm=True) -> int:
|
||||
"""Calcul automatique des décisions de jury sur une année BUT.
|
||||
Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit).
|
||||
Si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||
(mode à n'utiliser que pour les tests)
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
|
||||
) -> int:
|
||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||
|
||||
Returns: nombre d'étudiants "admis"
|
||||
- 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
|
||||
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||
|
||||
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
|
||||
"""
|
||||
if not formsemestre.formation.is_apc():
|
||||
raise ScoValueError("fonction réservée aux formations BUT")
|
||||
nb_admis = 0
|
||||
nb_etud_modif = 0
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etudid in formsemestre.etuds_inscriptions:
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if deca.admis: # année réussie
|
||||
nb_admis += 1
|
||||
if deca.admis or not only_adm:
|
||||
deca.record_all()
|
||||
nb_etud_modif += deca.record_all(
|
||||
no_overwrite=no_overwrite, only_validantes=only_adm
|
||||
)
|
||||
|
||||
db.session.commit()
|
||||
return nb_admis
|
||||
return nb_etud_modif
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -8,25 +8,34 @@
|
||||
"""
|
||||
|
||||
import re
|
||||
import numpy as np
|
||||
|
||||
import flask
|
||||
from flask import flash, url_for
|
||||
from flask import flash, render_template, url_for
|
||||
from flask import g, request
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but
|
||||
from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE
|
||||
from app.but.jury_but import (
|
||||
DecisionsProposeesAnnee,
|
||||
DecisionsProposeesRCUE,
|
||||
DecisionsProposeesUE,
|
||||
)
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -35,53 +44,50 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
Si pas read_only, menus sélection codes jury.
|
||||
"""
|
||||
H = []
|
||||
if deca.code_valide and not read_only:
|
||||
erase_span = f"""<a href="{
|
||||
url_for("notes.formsemestre_jury_but_erase",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id,
|
||||
etudid=deca.etud.id)}" class="stdlink">effacer décisions</a>"""
|
||||
else:
|
||||
erase_span = ""
|
||||
|
||||
H.append("""<div class="but_section_annee">""")
|
||||
if deca.jury_annuel:
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_section_annee">
|
||||
<div>
|
||||
<b>Décision de jury pour l'année :</b> {
|
||||
_gen_but_select("code_annee", deca.codes, deca.code_valide,
|
||||
disabled=True, klass="manual")
|
||||
}
|
||||
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
|
||||
<span>{erase_span}</span>
|
||||
<span>({deca.code_valide or 'non'} enregistrée)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="but_explanation">{deca.explanation}</div>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
H.append("""<div><em>Pas de décision annuelle (sem. impair)</em></div>""")
|
||||
H.append("""</div>""")
|
||||
|
||||
if deca.formsemestre_pair is not None:
|
||||
annee_sco_pair = deca.formsemestre_pair.annee_scolaire()
|
||||
avertissement_redoublement = (
|
||||
f"année {annee_sco_pair}-{annee_sco_pair+1}"
|
||||
if annee_sco_pair != deca.annee_scolaire()
|
||||
else ""
|
||||
formsemestre_1 = deca.formsemestre_impair
|
||||
formsemestre_2 = deca.formsemestre_pair
|
||||
# Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval):
|
||||
reverse_semestre = (
|
||||
deca.formsemestre_pair
|
||||
and deca.formsemestre_impair
|
||||
and deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut
|
||||
)
|
||||
else:
|
||||
avertissement_redoublement = ""
|
||||
|
||||
if reverse_semestre:
|
||||
formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1
|
||||
H.append(
|
||||
f"""
|
||||
<div class="titre_niveaux"><b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b></div>
|
||||
<div class="titre_niveaux">
|
||||
<b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b>
|
||||
</div>
|
||||
<div class="but_explanation">{deca.explanation}</div>
|
||||
<div class="but_annee">
|
||||
<div class="titre"></div>
|
||||
<div class="titre">S{deca.formsemestre_impair.semestre_id
|
||||
if deca.formsemestre_impair else "-"}</div>
|
||||
<div class="titre">S{deca.formsemestre_pair.semestre_id
|
||||
if deca.formsemestre_pair else "-"}
|
||||
<span class="avertissement_redoublement">{avertissement_redoublement}</span></div>
|
||||
<div class="titre">{"S" +str(formsemestre_1.semestre_id)
|
||||
if formsemestre_1 else "-"}
|
||||
<span class="avertissement_redoublement">{formsemestre_1.annee_scolaire_str()
|
||||
if formsemestre_1 else ""}</span>
|
||||
</div>
|
||||
<div class="titre">{"S"+str(formsemestre_2.semestre_id)
|
||||
if formsemestre_2 else "-"}
|
||||
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
|
||||
if formsemestre_2 else ""}</span>
|
||||
</div>
|
||||
<div class="titre">RCUE</div>
|
||||
"""
|
||||
)
|
||||
@ -91,43 +97,52 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
|
||||
<div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
|
||||
</div>"""
|
||||
)
|
||||
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id)
|
||||
if dec_rcue is None:
|
||||
H.append(
|
||||
"""<div class="niveau_vide"></div><div class="niveau_vide"></div><div class="niveau_vide"></div>"""
|
||||
)
|
||||
continue
|
||||
# Semestre impair
|
||||
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
|
||||
# Les UEs à afficher,
|
||||
# qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant
|
||||
ues_ro = [
|
||||
(
|
||||
ue_impair,
|
||||
(deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id),
|
||||
),
|
||||
(
|
||||
ue_pair,
|
||||
deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id,
|
||||
),
|
||||
]
|
||||
# Ordonne selon les dates des 2 semestres considérés:
|
||||
if reverse_semestre:
|
||||
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
|
||||
# Colonnes d'UE:
|
||||
for ue, ue_read_only in ues_ro:
|
||||
if ue:
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
dec_rcue.rcue.ue_1,
|
||||
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
|
||||
disabled=read_only,
|
||||
ue,
|
||||
deca.decisions_ues[ue.id],
|
||||
disabled=read_only or ue_read_only,
|
||||
annee_prec=ue_read_only,
|
||||
niveau_id=ue.niveau_competence.id,
|
||||
)
|
||||
)
|
||||
# Semestre pair
|
||||
H.append(
|
||||
_gen_but_niveau_ue(
|
||||
dec_rcue.rcue.ue_2,
|
||||
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
|
||||
disabled=read_only,
|
||||
)
|
||||
)
|
||||
# RCUE
|
||||
H.append(
|
||||
f"""<div class="but_niveau_rcue
|
||||
{'recorded' if dec_rcue.code_valide is not None else ''}
|
||||
">
|
||||
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
||||
<div class="but_code">{
|
||||
_gen_but_select("code_rcue_"+str(niveau.id),
|
||||
dec_rcue.codes,
|
||||
dec_rcue.code_valide,
|
||||
disabled=True, klass="manual"
|
||||
)
|
||||
}</div>
|
||||
</div>"""
|
||||
)
|
||||
else:
|
||||
H.append("""<div class="niveau_vide"></div>""")
|
||||
|
||||
# Colonne RCUE
|
||||
H.append(_gen_but_rcue(dec_rcue, niveau))
|
||||
|
||||
H.append("</div>") # but_annee
|
||||
return "\n".join(H)
|
||||
|
||||
@ -138,9 +153,12 @@ def _gen_but_select(
|
||||
code_valide: str,
|
||||
disabled: bool = False,
|
||||
klass: str = "",
|
||||
data: dict = {},
|
||||
) -> str:
|
||||
"Le menu html select avec les codes"
|
||||
h = "\n".join(
|
||||
# if disabled: # mauvaise idée car le disabled est traité en JS
|
||||
# return f"""<div class="but_code {klass}">{code_valide}</div>"""
|
||||
options_htm = "\n".join(
|
||||
[
|
||||
f"""<option value="{code}"
|
||||
{'selected' if code == code_valide else ''}
|
||||
@ -151,33 +169,54 @@ def _gen_but_select(
|
||||
)
|
||||
return f"""<select required name="{name}"
|
||||
class="but_code {klass}"
|
||||
data-orig_code="{code_valide or (codes[0] if codes else '')}"
|
||||
data-orig_recorded="{code_valide or ''}"
|
||||
onchange="change_menu_code(this);"
|
||||
{"disabled" if disabled else ""}
|
||||
>{h}</select>
|
||||
{" ".join( f'data-{k}="{v}"' for (k,v) in data.items() )}
|
||||
>{options_htm}</select>
|
||||
"""
|
||||
|
||||
|
||||
def _gen_but_niveau_ue(ue: UniteEns, dec_ue: DecisionsProposeesUE, disabled=False):
|
||||
def _gen_but_niveau_ue(
|
||||
ue: UniteEns,
|
||||
dec_ue: DecisionsProposeesUE,
|
||||
disabled: bool = False,
|
||||
annee_prec: bool = False,
|
||||
niveau_id: int = None,
|
||||
) -> str:
|
||||
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
|
||||
moy_ue_str = f"""<span class="ue_cap">{
|
||||
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
|
||||
scoplement = f"""<div class="scoplement">
|
||||
<div>
|
||||
<b>UE {ue.acronyme} capitalisée le
|
||||
{dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
|
||||
</b>
|
||||
<b>UE {ue.acronyme} capitalisée </b>
|
||||
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
|
||||
</span>
|
||||
</div>
|
||||
<div>UE en cours avec moyenne
|
||||
{scu.fmt_note(dec_ue.moy_ue)}
|
||||
<div>UE en cours
|
||||
{ "sans notes" if np.isnan(dec_ue.moy_ue)
|
||||
else
|
||||
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</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")}
|
||||
à {dec_ue.validation.event_date.strftime("%Hh%M")}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
scoplement = ""
|
||||
|
||||
return f"""<div class="but_niveau_ue {
|
||||
'recorded' if dec_ue.code_valide is not None else ''}
|
||||
{'annee_prec' if annee_prec else ''}
|
||||
">
|
||||
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||
<div class="but_note with_scoplement">
|
||||
@ -187,37 +226,82 @@ def _gen_but_niveau_ue(ue: UniteEns, dec_ue: DecisionsProposeesUE, disabled=Fals
|
||||
<div class="but_code">{
|
||||
_gen_but_select("code_ue_"+str(ue.id),
|
||||
dec_ue.codes,
|
||||
dec_ue.code_valide, disabled=disabled
|
||||
dec_ue.code_valide,
|
||||
disabled=disabled,
|
||||
klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else ""
|
||||
)
|
||||
}</div>
|
||||
|
||||
</div>"""
|
||||
|
||||
|
||||
def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
||||
if dec_rcue is None:
|
||||
return """
|
||||
<div class="but_niveau_rcue niveau_vide with_scoplement">
|
||||
<div></div>
|
||||
<div class="scoplement">Pas de RCUE (UE non capitalisée ?)</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
scoplement = (
|
||||
f"""<div class="scoplement">{
|
||||
dec_rcue.validation.to_html()
|
||||
}</div>"""
|
||||
if dec_rcue.validation
|
||||
else ""
|
||||
)
|
||||
|
||||
# Déjà enregistré ?
|
||||
niveau_rcue_class = ""
|
||||
if dec_rcue.code_valide is not None and dec_rcue.codes:
|
||||
if dec_rcue.code_valide == dec_rcue.codes[0]:
|
||||
niveau_rcue_class = "recorded"
|
||||
else:
|
||||
niveau_rcue_class = "recorded_different"
|
||||
|
||||
return f"""
|
||||
<div class="but_niveau_rcue {niveau_rcue_class}
|
||||
">
|
||||
<div class="but_note with_scoplement">
|
||||
<div>{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
||||
{scoplement}
|
||||
</div>
|
||||
<div class="but_code">
|
||||
{_gen_but_select("code_rcue_"+str(niveau.id),
|
||||
dec_rcue.codes,
|
||||
dec_rcue.code_valide,
|
||||
disabled=True,
|
||||
klass="manual code_rcue",
|
||||
data = { "niveau_id" : str(niveau.id)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def jury_but_semestriel(
|
||||
formsemestre: FormSemestre,
|
||||
etud: Identite,
|
||||
read_only: bool,
|
||||
navigation_div: str = "",
|
||||
) -> str:
|
||||
"""Formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)"""
|
||||
"""Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)."""
|
||||
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
||||
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
|
||||
inscription_etat = etud.inscription_etat(formsemestre.id)
|
||||
semestre_terminal = (
|
||||
formsemestre.semestre_id >= formsemestre.formation.get_parcours().NB_SEM
|
||||
)
|
||||
autorisations_passage = ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id,
|
||||
origin_formsemestre_id=formsemestre.id,
|
||||
).all()
|
||||
# Par défaut: autorisé à passer dans le semestre suivant si sem. impair,
|
||||
# ou si décision déjà enregistrée:
|
||||
est_autorise_a_passer = (formsemestre.semestre_id % 2) or (
|
||||
formsemestre.semestre_id + 1
|
||||
) in (
|
||||
a.semestre_id
|
||||
for a in ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id,
|
||||
origin_formsemestre_id=formsemestre.id,
|
||||
)
|
||||
)
|
||||
) in (a.semestre_id for a in autorisations_passage)
|
||||
decisions_ues = {
|
||||
ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
|
||||
for ue in ues
|
||||
@ -230,9 +314,9 @@ def jury_but_semestriel(
|
||||
for key in request.form:
|
||||
code = request.form[key]
|
||||
# Codes d'UE
|
||||
m = re.match(r"^code_ue_(\d+)$", key)
|
||||
if m:
|
||||
ue_id = int(m.group(1))
|
||||
code_match = re.match(r"^code_ue_(\d+)$", key)
|
||||
if code_match:
|
||||
ue_id = int(code_match.group(1))
|
||||
dec_ue = decisions_ues.get(ue_id)
|
||||
if not dec_ue:
|
||||
raise ScoValueError(f"UE invalide ue_id={ue_id}")
|
||||
@ -241,7 +325,9 @@ def jury_but_semestriel(
|
||||
flash("codes enregistrés")
|
||||
if not semestre_terminal:
|
||||
if request.form.get("autorisation_passage"):
|
||||
if not est_autorise_a_passer:
|
||||
if not formsemestre.semestre_id + 1 in (
|
||||
a.semestre_id for a in autorisations_passage
|
||||
):
|
||||
ScolarAutorisationInscription.autorise_etud(
|
||||
etud.id,
|
||||
formsemestre.formation.formation_code,
|
||||
@ -250,7 +336,8 @@ def jury_but_semestriel(
|
||||
)
|
||||
db.session.commit()
|
||||
flash(
|
||||
f"autorisation de passage en S{formsemestre.semestre_id + 1} enregistrée"
|
||||
f"""autorisation de passage en S{formsemestre.semestre_id + 1
|
||||
} enregistrée"""
|
||||
)
|
||||
else:
|
||||
if est_autorise_a_passer:
|
||||
@ -279,7 +366,7 @@ def jury_but_semestriel(
|
||||
warning = ""
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title="Validation BUT",
|
||||
page_title=f"Validation BUT S{formsemestre.semestre_id}",
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id,
|
||||
cssstyles=("css/jury_but.css",),
|
||||
@ -300,25 +387,35 @@ def jury_but_semestriel(
|
||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Jury sur un semestre BUT isolé</h3>
|
||||
<h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
|
||||
{warning}
|
||||
</div>
|
||||
|
||||
<form method="POST">
|
||||
<form method="post" id="jury_but">
|
||||
""",
|
||||
]
|
||||
if (not read_only) and any([dec.code_valide for dec in decisions_ues.values()]):
|
||||
|
||||
erase_span = ""
|
||||
if not read_only:
|
||||
# Requête toutes les validations (pas seulement celles du deca courant),
|
||||
# au cas où: changement d'architecture, saisie en mode classique, ...
|
||||
validations = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||
).all()
|
||||
if validations:
|
||||
erase_span = f"""<a href="{
|
||||
url_for("notes.formsemestre_jury_but_erase",
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id,
|
||||
etudid=etud.id, only_one_sem=1)}" class="stdlink">effacer décisions</a>"""
|
||||
etudid=etud.id, only_one_sem=1)
|
||||
}" class="stdlink">effacer les décisions enregistrées</a>"""
|
||||
else:
|
||||
erase_span = "aucune décision enregistrée pour ce semestre"
|
||||
erase_span = (
|
||||
"Cet étudiant n'a aucune décision enregistrée pour ce semestre."
|
||||
)
|
||||
|
||||
H.append(
|
||||
f"""
|
||||
<div class="but_section_annee">
|
||||
<span>{erase_span}</span>
|
||||
</div>
|
||||
<div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div>
|
||||
"""
|
||||
@ -354,11 +451,27 @@ def jury_but_semestriel(
|
||||
)
|
||||
H.append("</div>") # but_annee
|
||||
|
||||
div_autorisations_passage = (
|
||||
f"""
|
||||
<div class="but_autorisations_passage">
|
||||
<span>Autorisé à passer en :</span>
|
||||
{ ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )}
|
||||
</div>
|
||||
"""
|
||||
if autorisations_passage
|
||||
else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>"""
|
||||
)
|
||||
H.append(div_autorisations_passage)
|
||||
|
||||
if read_only:
|
||||
H.append(
|
||||
"""<div class="but_explanation">
|
||||
Vous n'avez pas la permission de modifier ces décisions.
|
||||
Les champs entourés en vert sont enregistrés.</div>"""
|
||||
f"""<div class="but_explanation">
|
||||
{"Vous n'avez pas la permission de modifier ces décisions."
|
||||
if formsemestre.etat
|
||||
else "Semestre verrouillé."}
|
||||
Les champs entourés en vert sont enregistrés.
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
else:
|
||||
if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM:
|
||||
@ -375,13 +488,26 @@ def jury_but_semestriel(
|
||||
else:
|
||||
H.append("""<div class="help">dernier semestre de la formation.</div>""")
|
||||
H.append(
|
||||
"""
|
||||
f"""
|
||||
<div class="but_buttons">
|
||||
<input type="submit" value="Enregistrer ces décisions">
|
||||
<span><input type="submit" value="Enregistrer ces décisions"></span>
|
||||
<span>{erase_span}</span>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
H.append(navigation_div)
|
||||
H.append("</div>")
|
||||
H.append(
|
||||
render_template(
|
||||
"but/documentation_codes_jury.html",
|
||||
nom_univ=f"""Export {sco_preferences.get_preference("InstituteName")
|
||||
or sco_preferences.get_preference("UnivName")
|
||||
or "Apogée"}""",
|
||||
codes=ScoDocSiteConfig.get_codes_apo_dict(),
|
||||
)
|
||||
)
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
@ -407,7 +533,6 @@ def infos_fiche_etud_html(etudid: int) -> str:
|
||||
# temporaire quick & dirty: affiche le dernier
|
||||
try:
|
||||
deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1])
|
||||
if True: # len(deca.rcues_annee) > 0:
|
||||
return f"""<div class="infos_but">
|
||||
{show_etud(deca, read_only=True)}
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -430,6 +430,22 @@ 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>
|
||||
"""
|
||||
|
||||
name = "bonus_besancon_vesoul"
|
||||
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
|
||||
bonus_max = 0.2
|
||||
|
||||
|
||||
class BonusBethune(BonusSportMultiplicatif):
|
||||
"""
|
||||
Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
|
||||
@ -647,7 +663,10 @@ class BonusCalais(BonusSportAdditif):
|
||||
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
|
||||
<ul>
|
||||
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
|
||||
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b> (ex : UE2.1BS, UE32BS)
|
||||
</li>
|
||||
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b>
|
||||
(ex : UE2.1BS, UE32BS)
|
||||
</li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -122,6 +122,10 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
||||
event_date :
|
||||
} ]
|
||||
"""
|
||||
|
||||
# Note: pour récupérer aussi les UE validées en CMp ou ADJ, changer une ligne
|
||||
# and ( SFV.code = 'ADM' or SFV.code = 'ADJ' or SFV.code = 'CMP' )
|
||||
|
||||
query = """
|
||||
SELECT DISTINCT SFV.*, ue.ue_code
|
||||
FROM
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -39,6 +39,7 @@ from dataclasses import dataclass
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
||||
from app.scodoc import sco_cache
|
||||
@ -484,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||
if nb_etuds == 0:
|
||||
return pd.Series()
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
|
||||
assert evals_coefs.shape == (nb_evals,)
|
||||
if evals_coefs.shape != (nb_evals,):
|
||||
app.critical_error("compute_module_moy: vals_coefs.shape != nb_evals")
|
||||
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
|
||||
# Les coefs des évals pour chaque étudiant: là où il a des notes
|
||||
# non neutralisées
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -33,10 +33,7 @@ import pandas as pd
|
||||
from app import db
|
||||
from app import models
|
||||
from app.models import (
|
||||
DispenseUE,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
Identite,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
ModuleUECoef,
|
||||
@ -218,31 +215,6 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||
)
|
||||
|
||||
|
||||
def load_dispense_ues(
|
||||
formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns]
|
||||
) -> set[tuple[int, int]]:
|
||||
"""Construit l'ensemble des
|
||||
etudids = modimpl_inscr_df.index, # les etudids
|
||||
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
|
||||
|
||||
Résultat: set de (etudid, ue_id).
|
||||
"""
|
||||
dispense_ues = set()
|
||||
ue_sem_by_code = {ue.ue_code: ue for ue in ues}
|
||||
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
|
||||
# puis filtre sur inscrits et code d'UE UE
|
||||
for dispense_ue in DispenseUE.query.join(
|
||||
Identite, FormSemestreInscription
|
||||
).filter_by(formsemestre_id=formsemestre.id):
|
||||
if dispense_ue.etudid in etudids:
|
||||
# UE dans le semestre avec même code ?
|
||||
ue = ue_sem_by_code.get(dispense_ue.ue.ue_code)
|
||||
if ue is not None:
|
||||
dispense_ues.add((dispense_ue.etudid, ue.id))
|
||||
|
||||
return dispense_ues
|
||||
|
||||
|
||||
def compute_ue_moys_apc(
|
||||
sem_cube: np.array,
|
||||
etuds: list,
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -16,7 +16,7 @@ from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.ues import DispenseUE, UniteEns
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
@ -72,7 +72,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
modimpl.module.ue.type != UE_SPORT
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
]
|
||||
self.dispense_ues = moy_ue.load_dispense_ues(
|
||||
self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set(
|
||||
self.formsemestre, self.modimpl_inscr_df.index, self.ues
|
||||
)
|
||||
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -494,7 +494,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
classes: str = "",
|
||||
idx: int = 100,
|
||||
):
|
||||
"Add a row to our table. classes is a list of css class names"
|
||||
"Add a cell to our table. classes is a list of css class names"
|
||||
row[col_id] = content
|
||||
if classes:
|
||||
row[f"_{col_id}_class"] = classes + f" c{idx}"
|
||||
@ -519,6 +519,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
|
||||
)
|
||||
# --- Rang
|
||||
if not self.formsemestre.block_moyenne_generale:
|
||||
idx = add_cell(
|
||||
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
|
||||
)
|
||||
@ -542,12 +543,17 @@ class ResultatsSemestre(ResultatsCache):
|
||||
formsemestre_id=self.formsemestre.id,
|
||||
etudid=etudid,
|
||||
)
|
||||
row["_nom_short_data"] = {
|
||||
"etudid": etud.id,
|
||||
"nomprenom": etud.nomprenom,
|
||||
}
|
||||
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
|
||||
row["_nom_disp_target"] = row["_nom_short_target"]
|
||||
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
|
||||
|
||||
idx = 30 # début des colonnes de notes
|
||||
# --- Moyenne générale
|
||||
if not self.formsemestre.block_moyenne_generale:
|
||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||
note_class = ""
|
||||
if moy_gen is False:
|
||||
@ -567,7 +573,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||
)
|
||||
# --- Moyenne d'UE
|
||||
nb_ues_validables, nb_ues_warning = 0, 0
|
||||
for ue in ues_sans_bonus:
|
||||
idx_ue_start = idx
|
||||
for idx_ue, ue in enumerate(ues_sans_bonus):
|
||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||
if ue_status is not None:
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
@ -588,7 +595,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
ue.acronyme,
|
||||
fmt_note(val),
|
||||
"col_ue" + note_class,
|
||||
idx,
|
||||
idx_ue * 10000 + idx_ue_start,
|
||||
)
|
||||
titles_bot[
|
||||
f"_{col_id}_target_attrs"
|
||||
@ -609,7 +616,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
f"Bonus {ue.acronyme}",
|
||||
val_fmt_html if allow_html else val_fmt,
|
||||
"col_ue_bonus",
|
||||
idx,
|
||||
idx_ue * 10000 + idx_ue_start + 1,
|
||||
)
|
||||
row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
|
||||
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
||||
@ -654,7 +661,11 @@ class ResultatsSemestre(ResultatsCache):
|
||||
val_fmt_html,
|
||||
# class col_res mod_ue_123
|
||||
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
|
||||
idx,
|
||||
idx_ue * 10000
|
||||
+ idx_ue_start
|
||||
+ 1
|
||||
+ (modimpl.module.module_type or 0) * 1000
|
||||
+ (modimpl.module.numero or 0),
|
||||
)
|
||||
row[f"_{col_id}_xls"] = val_fmt
|
||||
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
||||
@ -704,7 +715,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
else:
|
||||
jury_code_sem = ""
|
||||
else:
|
||||
# formations classiqes: code semestre
|
||||
# formations classiques: code semestre
|
||||
dec_sem = self.validations.decisions_jury.get(etudid)
|
||||
jury_code_sem = dec_sem["code"] if dec_sem else ""
|
||||
idx = add_cell(
|
||||
@ -722,17 +733,22 @@ class ResultatsSemestre(ResultatsCache):
|
||||
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
)
|
||||
}">{"saisir" if not jury_code_sem else "modifier"} décision</a>""",
|
||||
}">{("saisir" if not jury_code_sem else "modifier")
|
||||
if self.formsemestre.etat else "voir"} décisions</a>""",
|
||||
"col_jury_link",
|
||||
idx,
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
self.recap_add_partitions(rows, titles)
|
||||
col_idx = self.recap_add_partitions(rows, titles)
|
||||
self.recap_add_cursus(rows, titles, col_idx=col_idx + 1)
|
||||
self._recap_add_admissions(rows, titles)
|
||||
|
||||
# tri par rang croissant
|
||||
if not self.formsemestre.block_moyenne_generale:
|
||||
rows.sort(key=lambda e: e["_rang_order"])
|
||||
else:
|
||||
rows.sort(key=lambda e: e["_ues_validables_order"], reverse=True)
|
||||
|
||||
# INFOS POUR FOOTER
|
||||
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
|
||||
@ -749,6 +765,20 @@ class ResultatsSemestre(ResultatsCache):
|
||||
for row in bottom_infos.values():
|
||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
||||
|
||||
# Ligne avec la classe de chaque colonne
|
||||
# récupère le type à partir des classes css (hack...)
|
||||
row_class = {}
|
||||
for col_id in titles:
|
||||
klass = titles.get(f"_{col_id}_class")
|
||||
if klass:
|
||||
row_class[col_id] = " ".join(
|
||||
cls[4:] for cls in klass.split() if cls.startswith("col_")
|
||||
)
|
||||
# cette case (nb d'UE validables) a deux classes col_xxx, on en garde une seule:
|
||||
if "ues_validables" in row_class[col_id]:
|
||||
row_class[col_id] = "ues_validables"
|
||||
bottom_infos["type_col"] = row_class
|
||||
|
||||
# --- TABLE FOOTER: ECTS, moyennes, min, max...
|
||||
footer_rows = []
|
||||
for (bottom_line, row) in bottom_infos.items():
|
||||
@ -772,7 +802,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
return (rows, footer_rows, titles, column_ids)
|
||||
|
||||
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
|
||||
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
|
||||
"""Les informations à mettre en bas de la table: min, max, moy, ECTS, Apo"""
|
||||
row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
|
||||
{"_tr_class": "bottom_info", "_title": "Min."},
|
||||
{"_tr_class": "bottom_info"},
|
||||
@ -832,7 +862,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
row_moy[f"_{colid}_class"] = "col_empty"
|
||||
row_apo[colid] = modimpl.module.code_apogee or ""
|
||||
|
||||
return { # { key : row } avec key = min, max, moy, coef
|
||||
return { # { key : row } avec key = min, max, moy, coef, ...
|
||||
"min": row_min,
|
||||
"max": row_max,
|
||||
"moy": row_moy,
|
||||
@ -880,7 +910,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
}
|
||||
first = True
|
||||
for i, cid in enumerate(fields):
|
||||
titles[f"_{cid}_col_order"] = 10000 + i # tout à droite
|
||||
titles[f"_{cid}_col_order"] = 100000 + i # tout à droite
|
||||
if first:
|
||||
titles[f"_{cid}_class"] = "admission admission_first"
|
||||
first = False
|
||||
@ -899,10 +929,29 @@ class ResultatsSemestre(ResultatsCache):
|
||||
else:
|
||||
row[f"_{cid}_class"] = "admission"
|
||||
|
||||
def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None):
|
||||
def recap_add_cursus(self, rows: list[dict], titles: dict, col_idx: int = None):
|
||||
"""Ajoute colonne avec code cursus, eg 'S1 S2 S1'"""
|
||||
cid = "code_cursus"
|
||||
titles[cid] = "Cursus"
|
||||
titles[f"_{cid}_col_order"] = col_idx
|
||||
formation_code = self.formsemestre.formation.formation_code
|
||||
for row in rows:
|
||||
etud = Identite.query.get(row["etudid"])
|
||||
row[cid] = " ".join(
|
||||
[
|
||||
f"S{ins.formsemestre.semestre_id}"
|
||||
for ins in reversed(etud.inscriptions())
|
||||
if ins.formsemestre.formation.formation_code == formation_code
|
||||
]
|
||||
)
|
||||
|
||||
def recap_add_partitions(
|
||||
self, rows: list[dict], titles: dict, col_idx: int = None
|
||||
) -> int:
|
||||
"""Ajoute les colonnes indiquant les groupes
|
||||
rows est une liste de dict avec une clé "etudid"
|
||||
Les colonnes ont la classe css "partition"
|
||||
Renvoie l'indice de la dernière colonne utilisée
|
||||
"""
|
||||
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
||||
self.formsemestre.id
|
||||
@ -951,6 +1000,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
row[rg_cid] = rang.get(row["etudid"], "")
|
||||
|
||||
first_partition = False
|
||||
return col_order
|
||||
|
||||
def _recap_add_evaluations(
|
||||
self, rows: list[dict], titles: dict, bottom_infos: dict
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -16,6 +16,7 @@ import flask_login
|
||||
import app
|
||||
from app.auth.models import User
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class ZUser(object):
|
||||
@ -180,18 +181,23 @@ def scodoc7func(func):
|
||||
else:
|
||||
arg_names = argspec.args
|
||||
for arg_name in arg_names: # pour chaque arg de la fonction vue
|
||||
if arg_name == "REQUEST": # ne devrait plus arriver !
|
||||
# debug check, TODO remove after tests
|
||||
raise ValueError("invalid REQUEST parameter !")
|
||||
else:
|
||||
# peut produire une KeyError s'il manque un argument attendu:
|
||||
v = req_args[arg_name]
|
||||
# try to convert all arguments to INTEGERS
|
||||
# necessary for db ids and boolean values
|
||||
try:
|
||||
v = int(v)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
v = int(v) if v else v
|
||||
except (ValueError, TypeError) as exc:
|
||||
if arg_name in {
|
||||
"etudid",
|
||||
"formation_id",
|
||||
"formsemestre_id",
|
||||
"module_id",
|
||||
"moduleimpl_id",
|
||||
"partition_id",
|
||||
"ue_id",
|
||||
}:
|
||||
raise ScoValueError("page introuvable (id invalide)") from exc
|
||||
pos_arg_values.append(v)
|
||||
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
|
||||
# current_app.logger.info("req_args=%s" % req_args)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -63,6 +63,7 @@ class CodesDecisionsForm(FlaskForm):
|
||||
ABL = _build_code_field("ABL")
|
||||
ADC = _build_code_field("ADC")
|
||||
ADJ = _build_code_field("ADJ")
|
||||
ADJR = _build_code_field("ADJR")
|
||||
ADM = _build_code_field("ADM")
|
||||
AJ = _build_code_field("AJ")
|
||||
ATB = _build_code_field("ATB")
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -4,8 +4,8 @@
|
||||
from app import db
|
||||
from app.models import ModuleImpl, ModuleImplInscription
|
||||
from app.models.etudiants import Identite
|
||||
from app.scodoc.sco_utils import EtatAssiduite, localize_datetime, verif_interval
|
||||
|
||||
from app.scodoc.sco_utils import EtatAssiduite, localize_datetime, is_period_overlapping
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ class Assiduite(db.Model):
|
||||
Représente une assiduité:
|
||||
- une plage horaire lié à un état et un étudiant
|
||||
- un module si spécifiée
|
||||
- une description si spécifiée
|
||||
"""
|
||||
|
||||
__tablename__ = "assiduites"
|
||||
@ -40,14 +41,17 @@ class Assiduite(db.Model):
|
||||
)
|
||||
etat = db.Column(db.Integer, nullable=False)
|
||||
|
||||
desc = db.Column(db.Text)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
data = {
|
||||
"assiduiteid": self.assiduiteid,
|
||||
"assiduite_id": self.assiduite_id,
|
||||
"etudid": self.etudid,
|
||||
"moduleimpl_id": self.moduleimpl_id,
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
"etat": self.etat,
|
||||
"desc": self.desc,
|
||||
}
|
||||
return data
|
||||
|
||||
@ -58,15 +62,10 @@ class Assiduite(db.Model):
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatAssiduite,
|
||||
module: int
|
||||
or None = None, # XEV est-ce un id (alors module_id ou modimpl_id), ou un objet (ModuleImpl ??) => cela simplifiera le check d'erreur
|
||||
moduleimpl: ModuleImpl = None,
|
||||
description: str = None,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant
|
||||
Documentation des codes d'erreurs renvoyés:
|
||||
1: Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)
|
||||
2: l'ID du module_impl n'existe pas.
|
||||
#XEV => utiliser plutôt des exceptions.
|
||||
"""
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
assiduites: list[Assiduite] = etud.assiduites.all()
|
||||
|
||||
@ -75,67 +74,40 @@ class Assiduite(db.Model):
|
||||
assiduites = [
|
||||
ass
|
||||
for ass in assiduites
|
||||
if verif_interval( # XEV
|
||||
if is_period_overlapping(
|
||||
(date_debut, date_fin),
|
||||
(ass.date_debut, ass.date_fin),
|
||||
)
|
||||
]
|
||||
if len(assiduites) != 0:
|
||||
return 1 # XEV raise une exception
|
||||
raise ScoValueError(
|
||||
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
|
||||
)
|
||||
|
||||
if module is not None:
|
||||
if moduleimpl is not None:
|
||||
# Vérification de l'existence du module pour l'étudiant
|
||||
if cls.verif_moduleimpl(module, etud):
|
||||
if moduleimpl.est_inscrit(etud):
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
moduleimpl_id=module,
|
||||
moduleimpl_id=moduleimpl.id,
|
||||
desc=description,
|
||||
)
|
||||
else:
|
||||
return 2
|
||||
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
|
||||
else:
|
||||
nouv_assiduite = Assiduite(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
etat=etat,
|
||||
etudiant=etud,
|
||||
desc=description,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
||||
@staticmethod
|
||||
def verif_moduleimpl(moduleimpl_id: int, etud: Identite or int) -> bool:
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||
|
||||
Retourne Vrai si c'est le cas, faux sinon
|
||||
"""
|
||||
# XEV: cette méthode n'a pas de raison d'être dans la classe Assiduite
|
||||
# et pourrait etre ModuleImpl.est_inscrit(etud)
|
||||
# + éviter les "Identite or int" : cela complique les tests, mieux vaut avoir un type unique bien défini.
|
||||
output = True
|
||||
|
||||
# XEV: "module" est un "modimpl": changer nom sinon on pense que c'est un Module
|
||||
module: ModuleImpl = ModuleImpl.query.filter_by(
|
||||
moduleimpl_id=moduleimpl_id
|
||||
).first()
|
||||
if module is None:
|
||||
output = False
|
||||
|
||||
if output:
|
||||
search_etudid: int = etud.id if type(etud) == Identite else etud
|
||||
|
||||
# XEV: is_xxx indique un booléen, or ici is_module est un comptage
|
||||
is_module: int = ModuleImplInscription.query.filter_by(
|
||||
etudid=search_etudid, moduleimpl_id=moduleimpl_id
|
||||
).count()
|
||||
|
||||
output = is_module > 0
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class Justificatif(db.Model):
|
||||
"""
|
||||
@ -147,7 +119,7 @@ class Justificatif(db.Model):
|
||||
|
||||
__tablename__ = "justificatifs"
|
||||
|
||||
justifid = db.Column(db.Integer, primary_key=True)
|
||||
justif_id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
date_debut = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
@ -167,12 +139,18 @@ class Justificatif(db.Model):
|
||||
)
|
||||
|
||||
raison = db.Column(db.Text())
|
||||
fichier = db.Column(db.Integer()) # XEV qu'est-ce que cet entier ?
|
||||
# XEV pour les fichiers stockés, on va utiliser sco_archives.py
|
||||
|
||||
"""
|
||||
Les justificatifs sont enregistrés dans
|
||||
<archivedir>/justificatifs/<dept_id>/<etudid>/<nom_fichier.extension>
|
||||
|
||||
d'après sco_archives.py#JustificatifArchiver
|
||||
"""
|
||||
fichier = db.Column(db.Text())
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
data = {
|
||||
"justifid": self.assiduiteid,
|
||||
"justif_id": self.assiduite_id,
|
||||
"etudid": self.etudid,
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
|
@ -1,6 +1,6 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 models : Référentiel Compétence BUT 2021
|
||||
@ -14,7 +14,7 @@ import sqlalchemy
|
||||
from app import db
|
||||
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
|
||||
|
||||
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
||||
@ -54,13 +54,15 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
"Référentiel de compétence d'une spécialité"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
annexe = db.Column(db.Text())
|
||||
specialite = db.Column(db.Text())
|
||||
specialite_long = db.Column(db.Text())
|
||||
type_titre = db.Column(db.Text())
|
||||
type_structure = db.Column(db.Text())
|
||||
annexe = db.Column(db.Text()) # '1', '22', ...
|
||||
specialite = db.Column(db.Text()) # 'CJ', 'RT', 'INFO', ...
|
||||
specialite_long = db.Column(
|
||||
db.Text()
|
||||
) # 'Carrière Juridique', 'Réseaux et télécommunications', ...
|
||||
type_titre = db.Column(db.Text()) # 'B.U.T.'
|
||||
type_structure = db.Column(db.Text()) # 'type1', 'type2', ...
|
||||
type_departement = db.Column(db.Text()) # "secondaire", "tertiaire"
|
||||
version_orebut = db.Column(db.Text())
|
||||
version_orebut = db.Column(db.Text()) # '2021-12-11 00:00:00'
|
||||
_xml_attribs = { # Orébut xml attrib : attribute
|
||||
"type": "type_titre",
|
||||
"version": "version_orebut",
|
||||
@ -92,9 +94,10 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
return ""
|
||||
return self.version_orebut.split()[0]
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, parcours: list["ApcParcours"] = None, with_app_critiques=True):
|
||||
"""Représentation complète du ref. de comp.
|
||||
comme un dict.
|
||||
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
|
||||
"""
|
||||
return {
|
||||
"dept_id": self.dept_id,
|
||||
@ -109,8 +112,14 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
if self.scodoc_date_loaded
|
||||
else "",
|
||||
"scodoc_orig_filename": self.scodoc_orig_filename,
|
||||
"competences": {x.titre: x.to_dict() for x in self.competences},
|
||||
"parcours": {x.code: x.to_dict() for x in self.parcours},
|
||||
"competences": {
|
||||
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
|
||||
for x in self.competences
|
||||
},
|
||||
"parcours": {
|
||||
x.code: x.to_dict()
|
||||
for x in (self.parcours if parcours is None else parcours)
|
||||
},
|
||||
}
|
||||
|
||||
def get_niveaux_by_parcours(
|
||||
@ -172,6 +181,27 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
|
||||
return parcours, niveaux_by_parcours_no_tc
|
||||
|
||||
def get_competences_tronc_commun(self) -> list["ApcCompetence"]:
|
||||
"""Liste des compétences communes à tous les parcours du référentiel."""
|
||||
parcours = self.parcours.all()
|
||||
if not parcours:
|
||||
return []
|
||||
|
||||
ids = set.intersection(
|
||||
*[
|
||||
{competence.id for competence in parcour.query_competences()}
|
||||
for parcour in parcours
|
||||
]
|
||||
)
|
||||
return sorted(
|
||||
[
|
||||
competence
|
||||
for competence in parcours[0].query_competences()
|
||||
if competence.id in ids
|
||||
],
|
||||
key=lambda c: c.numero or 0,
|
||||
)
|
||||
|
||||
|
||||
class ApcCompetence(db.Model, XMLModel):
|
||||
"Compétence"
|
||||
@ -213,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel):
|
||||
def __repr__(self):
|
||||
return f"<ApcCompetence {self.id} {self.titre!r}>"
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, with_app_critiques=True):
|
||||
"repr dict recursive sur situations, composantes, niveaux"
|
||||
return {
|
||||
"id_orebut": self.id_orebut,
|
||||
@ -225,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel):
|
||||
"composantes_essentielles": [
|
||||
x.to_dict() for x in self.composantes_essentielles
|
||||
],
|
||||
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
|
||||
"niveaux": {
|
||||
x.annee: x.to_dict(with_app_critiques=with_app_critiques)
|
||||
for x in self.niveaux
|
||||
},
|
||||
}
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
@ -291,13 +324,15 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
|
||||
self.annee!r} {self.competence!r}>"""
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict, recursif sur les AC"
|
||||
def to_dict(self, with_app_critiques=True):
|
||||
"as a dict, recursif (ou non) sur les AC"
|
||||
return {
|
||||
"libelle": self.libelle,
|
||||
"annee": self.annee,
|
||||
"ordre": self.ordre,
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
|
||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
|
||||
if with_app_critiques
|
||||
else {},
|
||||
}
|
||||
|
||||
def to_dict_bul(self):
|
||||
@ -322,9 +357,8 @@ class ApcNiveau(db.Model, XMLModel):
|
||||
if annee not in {1, 2, 3}:
|
||||
raise ValueError("annee invalide pour un parcours BUT")
|
||||
if referentiel_competence is None:
|
||||
raise ScoValueError(
|
||||
"Pas de référentiel de compétences associé à la formation !"
|
||||
)
|
||||
raise ScoNoReferentielCompetences()
|
||||
|
||||
annee_formation = f"BUT{annee}"
|
||||
if parcour is None:
|
||||
return ApcNiveau.query.filter(
|
||||
@ -470,6 +504,14 @@ class ApcParcours(db.Model, XMLModel):
|
||||
d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
|
||||
return d
|
||||
|
||||
def query_competences(self) -> flask_sqlalchemy.BaseQuery:
|
||||
"Les compétences associées à ce parcours"
|
||||
return (
|
||||
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
|
||||
.filter_by(parcours_id=self.id)
|
||||
.order_by(ApcCompetence.numero)
|
||||
)
|
||||
|
||||
|
||||
class ApcAnneeParcours(db.Model, XMLModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -2,19 +2,17 @@
|
||||
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
|
||||
import flask_sqlalchemy
|
||||
from sqlalchemy.sql import text
|
||||
from typing import Union
|
||||
|
||||
from app import db
|
||||
import flask_sqlalchemy
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_codes_parcours as sco_codes
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
@ -63,13 +61,32 @@ class ApcValidationRCUE(db.Model):
|
||||
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
return f"""décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {self.code}"""
|
||||
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:
|
||||
"description en HTML"
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||
<b>{self.code}</b>
|
||||
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
|
||||
à {self.date.strftime("%Hh%M")}</em>"""
|
||||
|
||||
def annee(self) -> str:
|
||||
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
|
||||
niveau = self.niveau()
|
||||
return niveau.annee if niveau else None
|
||||
|
||||
def niveau(self) -> ApcNiveau:
|
||||
"""Le niveau de compétence associé à cet RCUE."""
|
||||
# Par convention, il est donné par la seconde UE
|
||||
return self.ue2.niveau_competence
|
||||
|
||||
def to_dict(self):
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def to_dict_bul(self) -> dict:
|
||||
"Export dict pour bulletins: le code et le niveau de compétence"
|
||||
niveau = self.niveau()
|
||||
@ -96,10 +113,6 @@ class RegroupementCoherentUE:
|
||||
dec_ue_2: "DecisionsProposeesUE",
|
||||
inscription_etat: str,
|
||||
):
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
|
||||
# from app.but.jury_but import DecisionsProposeesUE
|
||||
ue_1 = dec_ue_1.ue
|
||||
ue_2 = dec_ue_2.ue
|
||||
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
||||
@ -144,6 +157,11 @@ class RegroupementCoherentUE:
|
||||
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,
|
||||
) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE]
|
||||
@ -174,8 +192,9 @@ class RegroupementCoherentUE:
|
||||
return self.query_validations().count() > 0
|
||||
|
||||
def est_compensable(self):
|
||||
"""Vrai si ce RCUE est validable par compensation
|
||||
c'est à dire que sa moyenne est > 10 avec une UE < 10
|
||||
"""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)
|
||||
@ -296,7 +315,8 @@ class ApcValidationAnnee(db.Model):
|
||||
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
||||
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
||||
|
||||
def __str__(self):
|
||||
return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}"""
|
||||
@ -333,7 +353,8 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
|
||||
else:
|
||||
titres_rcues.append(
|
||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {dec_rcue["code"]}"""
|
||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {
|
||||
dec_rcue["code"]}"""
|
||||
)
|
||||
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
||||
decisions["descr_decisions_niveaux"] = (
|
||||
|
@ -13,6 +13,7 @@ from app.scodoc.sco_codes_parcours import (
|
||||
ABL,
|
||||
ADC,
|
||||
ADJ,
|
||||
ADJR,
|
||||
ADM,
|
||||
AJ,
|
||||
ATB,
|
||||
@ -34,6 +35,7 @@ CODES_SCODOC_TO_APO = {
|
||||
ABL: "ABL",
|
||||
ADC: "ADMC",
|
||||
ADJ: "ADM",
|
||||
ADJR: "ADM",
|
||||
ADM: "ADM",
|
||||
AJ: "AJ",
|
||||
ATB: "AJAC",
|
||||
|
@ -55,7 +55,8 @@ class Formation(db.Model):
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>"
|
||||
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:
|
||||
"titre complet pour affichage"
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -56,55 +56,58 @@ class FormSemestre(db.Model):
|
||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
|
||||
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
|
||||
titre = db.Column(db.Text())
|
||||
date_debut = db.Column(db.Date())
|
||||
date_fin = db.Column(db.Date())
|
||||
etat = db.Column(
|
||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||
) # False si verrouillé
|
||||
titre = db.Column(db.Text(), nullable=False)
|
||||
date_debut = db.Column(db.Date(), nullable=False)
|
||||
date_fin = db.Column(db.Date(), nullable=False)
|
||||
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
|
||||
"False si verrouillé"
|
||||
modalite = db.Column(
|
||||
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
|
||||
) # "FI", "FAP", "FC", ...
|
||||
# gestion compensation sem DUT:
|
||||
)
|
||||
"Modalité de formation: 'FI', 'FAP', 'FC', ..."
|
||||
gestion_compensation = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# ne publie pas le bulletin XML ou JSON:
|
||||
"gestion compensation sem DUT (inutilisé en APC)"
|
||||
bul_hide_xml = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# Bloque le calcul des moyennes (générale et d'UE)
|
||||
"ne publie pas le bulletin XML ou JSON"
|
||||
block_moyennes = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# Bloque le calcul de la moyenne générale (utile pour BUT)
|
||||
"Bloque le calcul des moyennes (générale et d'UE)"
|
||||
block_moyenne_generale = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# semestres decales (pour gestion jurys):
|
||||
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
|
||||
gestion_semestrielle = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# couleur fond bulletins HTML:
|
||||
"Semestres décalés (pour gestion jurys DUT, pas implémenté ou utile en BUT)"
|
||||
bul_bgcolor = db.Column(
|
||||
db.String(SHORT_STR_LEN), default="white", server_default="white"
|
||||
db.String(SHORT_STR_LEN),
|
||||
default="white",
|
||||
server_default="white",
|
||||
nullable=False,
|
||||
)
|
||||
# autorise resp. a modifier semestre:
|
||||
"couleur fond bulletins HTML"
|
||||
resp_can_edit = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="false"
|
||||
)
|
||||
# autorise resp. a modifier slt les enseignants:
|
||||
"autorise resp. à modifier le formsemestre"
|
||||
resp_can_change_ens = db.Column(
|
||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||
)
|
||||
# autorise les ens a creer des evals:
|
||||
"autorise resp. a modifier slt les enseignants"
|
||||
ens_can_edit_eval = db.Column(
|
||||
db.Boolean(), nullable=False, default=False, server_default="False"
|
||||
)
|
||||
# code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'
|
||||
"autorise les enseignants à créer des évals dans leurs modimpls"
|
||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
||||
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
|
||||
"code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
elt_annee_apo = db.Column(db.Text())
|
||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||
|
||||
# Relations:
|
||||
etapes = db.relationship(
|
||||
@ -114,6 +117,7 @@ class FormSemestre(db.Model):
|
||||
"ModuleImpl",
|
||||
backref="formsemestre",
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
@ -153,6 +157,11 @@ class FormSemestre(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
||||
|
||||
def sort_key(self) -> tuple:
|
||||
"""clé pour tris par ordre alphabétique
|
||||
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
||||
return (self.date_debut, self.semestre_id)
|
||||
|
||||
def to_dict(self, convert_objects=False) -> dict:
|
||||
"""dict (compatible ScoDoc7).
|
||||
If convert_objects, convert all attributes to native types
|
||||
@ -321,7 +330,7 @@ class FormSemestre(db.Model):
|
||||
if self.formation.is_apc():
|
||||
modimpls.sort(
|
||||
key=lambda m: (
|
||||
m.module.module_type or 0,
|
||||
m.module.module_type or 0, # ressources (2) avant SAEs (3)
|
||||
m.module.numero or 0,
|
||||
m.module.code or 0,
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
@ -87,6 +87,7 @@ class Partition(db.Model):
|
||||
def to_dict(self, with_groups=False) -> dict:
|
||||
"""as a dict, with or without groups"""
|
||||
d = dict(self.__dict__)
|
||||
d["partition_id"] = self.id
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("formsemestre", None)
|
||||
|
||||
|
@ -20,14 +20,12 @@ class ModuleImpl(db.Model):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_id = db.synonym("id")
|
||||
module_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_modules.id"),
|
||||
)
|
||||
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
|
||||
# formule de calcul moyenne:
|
||||
@ -101,6 +99,22 @@ class ModuleImpl(db.Model):
|
||||
d.pop("module", None)
|
||||
return d
|
||||
|
||||
def est_inscrit(self, etud: Identite) -> bool:
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl
|
||||
|
||||
Retourne Vrai si c'est le cas, faux sinon
|
||||
"""
|
||||
|
||||
is_module: int = (
|
||||
ModuleImplInscription.query.filter_by(
|
||||
etudid=etud.id, moduleimpl_id=self.id
|
||||
).count()
|
||||
> 0
|
||||
)
|
||||
|
||||
return is_module
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
notes_modules_enseignants = db.Table(
|
||||
|
@ -37,7 +37,9 @@ class Module(db.Model):
|
||||
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
|
||||
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
|
||||
# Relations:
|
||||
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
|
||||
modimpls = db.relationship(
|
||||
"ModuleImpl", backref="module", lazy="dynamic", cascade="all, delete-orphan"
|
||||
)
|
||||
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
|
||||
tags = db.relationship(
|
||||
"NotesTag",
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from app import db, log
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
@ -109,6 +111,7 @@ class UniteEns(db.Model):
|
||||
e["ects"] = e["ects"]
|
||||
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
|
||||
e["code_apogee"] = e["code_apogee"] or "" # pas de None
|
||||
e["parcour"] = self.parcour.to_dict() if self.parcour else None
|
||||
if with_module_ue_coefs:
|
||||
if convert_objects:
|
||||
e["module_ue_coefs"] = [
|
||||
@ -217,6 +220,8 @@ class UniteEns(db.Model):
|
||||
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_niveau_competence( {self}, {niveau} )")
|
||||
|
||||
def set_parcour(self, parcour: ApcParcours):
|
||||
@ -244,17 +249,30 @@ class UniteEns(db.Model):
|
||||
self.niveau_competence = None
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
self.formation.invalidate_cached_sems()
|
||||
log(f"ue.set_parcour( {self}, {parcour} )")
|
||||
|
||||
|
||||
class DispenseUE(db.Model):
|
||||
"""Dispense d'UE
|
||||
Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
|
||||
Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
|
||||
qu'ils ne refont pas.
|
||||
La dispense d'UE n'est PAS une validation:
|
||||
- elle n'est pas affectée par les décisions de jury (pas effacée)
|
||||
- elle est associée à un formsemestre
|
||||
- elle ne permet pas la délivrance d'ECTS ou du diplôme.
|
||||
|
||||
On utilise cette dispense et non une "inscription" par souci d'efficacité:
|
||||
en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours,
|
||||
la dispense étant une exception.
|
||||
"""
|
||||
|
||||
__table_args__ = (db.UniqueConstraint("ue_id", "etudid"),)
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),)
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
formsemestre_id = formsemestre_id = db.Column(
|
||||
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
|
||||
@ -273,3 +291,25 @@ class DispenseUE(db.Model):
|
||||
def __repr__(self) -> str:
|
||||
return f"""<{self.__class__.__name__} {self.id} etud={
|
||||
repr(self.etud)} ue={repr(self.ue)}>"""
|
||||
|
||||
@classmethod
|
||||
def load_formsemestre_dispense_ues_set(
|
||||
cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns]
|
||||
) -> set[tuple[int, int]]:
|
||||
"""Construit l'ensemble des
|
||||
etudids = modimpl_inscr_df.index, # les etudids
|
||||
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
|
||||
|
||||
Résultat: set de (etudid, ue_id).
|
||||
"""
|
||||
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
|
||||
# puis filtre sur inscrits et ues
|
||||
ue_ids = {ue.id for ue in ues}
|
||||
dispense_ues = {
|
||||
(dispense_ue.etudid, dispense_ue.ue_id)
|
||||
for dispense_ue in DispenseUE.query.filter_by(
|
||||
formsemestre_id=formsemestre.id
|
||||
)
|
||||
if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids
|
||||
}
|
||||
return dispense_ues
|
||||
|
@ -4,6 +4,7 @@
|
||||
"""
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.events import Scolog
|
||||
@ -58,7 +59,7 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.formsemestre_id}, {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:
|
||||
@ -93,6 +94,10 @@ class ScolarAutorisationInscription(db.Model):
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
||||
self.etudid}, semestre_id={self.semestre_id})"""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
@ -116,7 +121,10 @@ class ScolarAutorisationInscription(db.Model):
|
||||
semestre_id=semestre_id,
|
||||
)
|
||||
db.session.add(autorisation)
|
||||
Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}")
|
||||
Scolog.logdb(
|
||||
"autorise_etud", etudid=etudid, msg=f"Passage vers S{semestre_id}: autorisé"
|
||||
)
|
||||
log(f"ScolarAutorisationInscription: recording {autorisation}")
|
||||
|
||||
@classmethod
|
||||
def delete_autorisation_etud(
|
||||
@ -130,10 +138,11 @@ class ScolarAutorisationInscription(db.Model):
|
||||
)
|
||||
for autorisation in autorisations:
|
||||
db.session.delete(autorisation)
|
||||
log(f"ScolarAutorisationInscription: deleting {autorisation}")
|
||||
Scolog.logdb(
|
||||
"autorise_etud",
|
||||
etudid=etudid,
|
||||
msg=f"annule passage vers S{autorisation.semestre_id}",
|
||||
msg=f"Passage vers S{autorisation.semestre_id}: effacé",
|
||||
)
|
||||
db.session.flush()
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -459,8 +459,7 @@ class JuryPE(object):
|
||||
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
|
||||
(_, parcours) = sco_report.get_codeparcoursetud(etud)
|
||||
if (
|
||||
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values()))
|
||||
> 0
|
||||
len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0
|
||||
): # Eliminé car NAR apparait dans le parcours
|
||||
reponse = True
|
||||
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
|
||||
@ -563,9 +562,8 @@ class JuryPE(object):
|
||||
dec = nt.get_etud_decision_sem(
|
||||
etudid
|
||||
) # quelle est la décision du jury ?
|
||||
if dec and dec["code"] in list(
|
||||
sco_codes_parcours.CODES_SEM_VALIDES.keys()
|
||||
): # isinstance( sesMoyennes[i+1], float) and
|
||||
if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
|
||||
# isinstance( sesMoyennes[i+1], float) and
|
||||
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
|
||||
leFid = sem["formsemestre_id"]
|
||||
else:
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -105,13 +105,13 @@ class BaseArchiver(object):
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
if not os.path.isdir(path):
|
||||
log("creating directory %s" % path)
|
||||
log(f"creating directory {path}")
|
||||
os.mkdir(path)
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
self.initialized = True
|
||||
|
||||
def get_obj_dir(self, oid):
|
||||
def get_obj_dir(self, oid: int):
|
||||
"""
|
||||
:return: path to directory of archives for this object (eg formsemestre_id or etudid).
|
||||
If directory does not yet exist, create it.
|
||||
@ -142,7 +142,7 @@ class BaseArchiver(object):
|
||||
dirs = glob.glob(base + "*")
|
||||
return [os.path.split(x)[1] for x in dirs]
|
||||
|
||||
def list_obj_archives(self, oid):
|
||||
def list_obj_archives(self, oid: int):
|
||||
"""Returns
|
||||
:return: list of archive identifiers for this object (paths to non empty dirs)
|
||||
"""
|
||||
@ -157,7 +157,7 @@ class BaseArchiver(object):
|
||||
dirs.sort()
|
||||
return dirs
|
||||
|
||||
def delete_archive(self, archive_id):
|
||||
def delete_archive(self, archive_id: str):
|
||||
"""Delete (forever) this archive"""
|
||||
self.initialize()
|
||||
try:
|
||||
@ -166,7 +166,7 @@ class BaseArchiver(object):
|
||||
finally:
|
||||
scu.GSL.release()
|
||||
|
||||
def get_archive_date(self, archive_id):
|
||||
def get_archive_date(self, archive_id: str):
|
||||
"""Returns date (as a DateTime object) of an archive"""
|
||||
return datetime.datetime(
|
||||
*[int(x) for x in os.path.split(archive_id)[1].split("-")]
|
||||
@ -183,17 +183,17 @@ class BaseArchiver(object):
|
||||
files.sort()
|
||||
return [f for f in files if f and f[0] != "_"]
|
||||
|
||||
def get_archive_name(self, archive_id):
|
||||
def get_archive_name(self, archive_id: str):
|
||||
"""name identifying archive, to be used in web URLs"""
|
||||
return os.path.split(archive_id)[1]
|
||||
|
||||
def is_valid_archive_name(self, archive_name):
|
||||
def is_valid_archive_name(self, archive_name: str):
|
||||
"""check if name is valid."""
|
||||
return re.match(
|
||||
"^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name
|
||||
)
|
||||
|
||||
def get_id_from_name(self, oid, archive_name):
|
||||
def get_id_from_name(self, oid, archive_name: str):
|
||||
"""returns archive id (check that name is valid)"""
|
||||
self.initialize()
|
||||
if not self.is_valid_archive_name(archive_name):
|
||||
@ -206,7 +206,7 @@ class BaseArchiver(object):
|
||||
raise ScoValueError(f"Archive {archive_name} introuvable")
|
||||
return archive_id
|
||||
|
||||
def get_archive_description(self, archive_id):
|
||||
def get_archive_description(self, archive_id: str) -> str:
|
||||
"""Return description of archive"""
|
||||
self.initialize()
|
||||
filename = os.path.join(archive_id, "_description.txt")
|
||||
@ -247,7 +247,7 @@ class BaseArchiver(object):
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
self.initialize()
|
||||
filename = scu.sanitize_filename(filename)
|
||||
log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id))
|
||||
log(f"storing {filename} ({len(data)} bytes) in {archive_id}")
|
||||
try:
|
||||
scu.GSL.acquire()
|
||||
fname = os.path.join(archive_id, filename)
|
||||
@ -261,16 +261,18 @@ class BaseArchiver(object):
|
||||
"""Retreive data"""
|
||||
self.initialize()
|
||||
if not scu.is_valid_filename(filename):
|
||||
log('Archiver.get: invalid filename "%s"' % filename)
|
||||
log(f"""Archiver.get: invalid filename '{filename}'""")
|
||||
raise ScoValueError("archive introuvable (déjà supprimée ?)")
|
||||
fname = os.path.join(archive_id, filename)
|
||||
log("reading archive file %s" % fname)
|
||||
log(f"reading archive file {fname}")
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
return data
|
||||
|
||||
def get_archived_file(self, oid, archive_name, filename):
|
||||
"""Recupere donnees du fichier indiqué et envoie au client"""
|
||||
"""Recupère les donnees du fichier indiqué et envoie au client.
|
||||
Returns: Response
|
||||
"""
|
||||
archive_id = self.get_id_from_name(oid, archive_name)
|
||||
data = self.get(archive_id, filename)
|
||||
mime = mimetypes.guess_type(filename)[0]
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -1084,7 +1084,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
||||
recipients = [recipient_addr]
|
||||
sender = sco_preferences.get_preference("email_from_addr", formsemestre_id)
|
||||
if copy_addr:
|
||||
bcc = copy_addr.strip()
|
||||
bcc = copy_addr.strip().split(",")
|
||||
else:
|
||||
bcc = ""
|
||||
|
||||
@ -1094,7 +1094,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
||||
subject,
|
||||
sender,
|
||||
recipients,
|
||||
bcc=[bcc],
|
||||
bcc=bcc,
|
||||
text_body=hea,
|
||||
attachments=[
|
||||
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -435,7 +435,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
plusminus = pluslink
|
||||
try:
|
||||
ects_txt = str(int(ue["ects"]))
|
||||
except (ValueError, KeyError):
|
||||
except (ValueError, KeyError, TypeError):
|
||||
ects_txt = "-"
|
||||
|
||||
t = {
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@ -122,6 +122,7 @@ ABL = "ABL"
|
||||
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
||||
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
||||
ADJ = "ADJ" # admis par le jury
|
||||
ADJR = "ADJR" # UE admise car son RCUE est ADJ
|
||||
ATT = "ATT" #
|
||||
ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant
|
||||
ATB = "ATB"
|
||||
@ -158,6 +159,7 @@ CODES_EXPL = {
|
||||
ABL: "Année blanche",
|
||||
ADC: "Validé par compensation",
|
||||
ADJ: "Validé par le Jury",
|
||||
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
|
||||
ADM: "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)",
|
||||
@ -185,16 +187,23 @@ CODES_EXPL = {
|
||||
|
||||
# Les codes de semestres:
|
||||
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
|
||||
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
|
||||
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
|
||||
CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC}
|
||||
CODES_SEM_VALIDES = CODES_SEM_VALIDES_DE_DROIT | {ADJ} # semestre validé
|
||||
CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
|
||||
|
||||
CODES_SEM_REO = {NAR: 1} # reorientation
|
||||
CODES_SEM_REO = {NAR} # reorientation
|
||||
|
||||
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
|
||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
|
||||
"UE validée"
|
||||
|
||||
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
|
||||
"Niveau RCUE validé"
|
||||
|
||||
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée
|
||||
CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
CODES_RCUE = {ADM, AJ, CMP}
|
||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
|
||||
@ -205,21 +214,36 @@ BUT_CODES_PASSAGE = {
|
||||
PAS1NCI,
|
||||
ATJ,
|
||||
}
|
||||
# les codes, du plus "défavorable" à l'étudiant au plus favorable:
|
||||
# (valeur par défaut 0)
|
||||
BUT_CODES_ORDERED = {
|
||||
NAR: 0,
|
||||
DEF: 0,
|
||||
AJ: 10,
|
||||
ATJ: 20,
|
||||
CMP: 50,
|
||||
ADC: 50,
|
||||
PASD: 50,
|
||||
PAS1NCI: 60,
|
||||
ADJR: 90,
|
||||
ADJ: 100,
|
||||
ADM: 100,
|
||||
}
|
||||
|
||||
|
||||
def code_semestre_validant(code: str) -> bool:
|
||||
"Vrai si ce CODE entraine la validation du semestre"
|
||||
return CODES_SEM_VALIDES.get(code, False)
|
||||
return code in CODES_SEM_VALIDES
|
||||
|
||||
|
||||
def code_semestre_attente(code: str) -> bool:
|
||||
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
|
||||
return CODES_SEM_ATTENTES.get(code, False)
|
||||
return code in CODES_SEM_ATTENTES
|
||||
|
||||
|
||||
def code_ue_validant(code: str) -> bool:
|
||||
"Vrai si ce code d'UE est validant (ie attribue les ECTS)"
|
||||
return CODES_UE_VALIDES.get(code, False)
|
||||
return code in CODES_UE_VALIDES
|
||||
|
||||
|
||||
DEVENIR_EXPL = {
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -5,7 +5,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -4,7 +4,7 @@
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user