1
0
forked from ScoDoc/ScoDoc

Merge branch 'modif' of https://scodoc.org/git/iziram/ScoDoc into iziram-rev

This commit is contained in:
Emmanuel Viennet 2023-01-31 07:19:21 -03:00
commit efa8f617bb
229 changed files with 5891 additions and 1902 deletions

View File

@ -502,12 +502,10 @@ def clear_scodoc_cache():
# --------- Logging # --------- Logging
def log(msg: str, silent_test=True): def log(msg: str):
"""log a message. """log a message.
If Flask app, use configured logger, else stderr. If Flask app, use configured logger, else stderr.
""" """
if silent_test and current_app and current_app.config["TESTING"]:
return
try: try:
dept = getattr(g, "scodoc_dept", "") dept = getattr(g, "scodoc_dept", "")
msg = f" ({dept}) {msg}" msg = f" ({dept}) {msg}"
@ -552,3 +550,22 @@ def scodoc_flash_status_messages():
f"Mode test: mails redirigés vers {email_test_mode_address}", f"Mode test: mails redirigés vers {email_test_mode_address}",
category="warning", 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}
"""
)

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""ScoDoc 9 API : Absences """ScoDoc 9 API : Absences

View File

@ -19,39 +19,34 @@ from app.scodoc.sco_permissions import Permission
from flask_login import login_required 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_utils as scu
import app.scodoc.sco_assiduites as scass import app.scodoc.sco_assiduites as scass
@bp.route("/assiduite/<int:assiduiteid>") @bp.route("/assiduite/<int:assiduite_id>")
@api_web_bp.route("/assiduite/<int:assiduiteid>") @api_web_bp.route("/assiduite/<int:assiduite_id>")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# XEV à revoir pour les droits d'accès par département def assiduite(assiduite_id: int = None):
def assiduite(
assiduite_id: int = None,
): # XEV xxx_id (sauf pour etudid qui est l'exception qui confirme la règle)
"""Retourne un objet assiduité à partir de son id """Retourne un objet assiduité à partir de son id
Exemple de résultat: Exemple de résultat:
{ {
"assiduiteid": 1, "assiduite_id": 1,
"etudid": 2, "etudid": 2,
"moduleimpl_id": 3, "moduleimpl_id": 3,
"date_debut": "2022-10-31T08:00+01:00", "date_debut": "2022-10-31T08:00+01:00",
"date_fin": "2022-10-31T10: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) query = Assiduite.query.filter_by(id=assiduite_id)
if g.scodoc_dept: # if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) # query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
assiduite = query.first_or_404() assiduite = query.first_or_404()
@ -264,21 +259,13 @@ def count_assiduites_formsemestre(
return jsonify(scass.get_assiduites_stats(assiduites, metric, filter)) return jsonify(scass.get_assiduites_stats(assiduites, metric, filter))
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"], defaults={"batch": False}) @bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
@api_web_bp.route( @api_web_bp.route("/assiduite/<int:etudid>/create", methods=["POST"])
"/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}
)
@scodoc @scodoc
@login_required @login_required
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @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) Création d'une assiduité pour l'étudiant (etudid)
La requête doit avoir un content type "application/json": 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() etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
if batch:
errors: dict[int, str] = {} errors: dict[int, str] = {}
success: dict[ success: dict[int, object] = {}
int, for i, data in enumerate(request.get_json(force=True)):
] = {}
for i, data in enumerate(request.get_json(force=True).get("batch")):
code, obj = create_singular(data, etud) code, obj = create_singular(data, etud)
if code == 404: if code == 404:
errors[i] = obj errors[i] = obj
@ -312,13 +297,6 @@ def create(etudid: int = None, batch: bool = False):
return jsonify({"errors": errors, "success": success}) 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( def create_singular(
data: dict, data: dict,
@ -355,64 +333,54 @@ def create_singular(
# cas 4 : moduleimpl_id # cas 4 : moduleimpl_id
moduleimpl_id = data.get("moduleimpl_id", None) moduleimpl_id = data.get("moduleimpl_id", False)
if moduleimpl_id is not None: moduleimpl: ModuleImpl = None
try:
moduleimpl_id: int = int(moduleimpl_id) if moduleimpl_id is not False:
if moduleimpl_id < 0: moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
raise Exception if moduleimpl is None:
except:
errors.append("param 'moduleimpl_id': invalide") errors.append("param 'moduleimpl_id': invalide")
# cas 5 : desc
desc:str = data.get("desc", None)
if errors != []: if errors != []:
err: str = ", ".join(errors) err: str = ", ".join(errors)
return (404, err) return (404, err)
# TOUT EST OK # TOUT EST OK
nouv_assiduite: Assiduite or int = Assiduite.create_assiduite(
try:
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
date_debut=deb, date_debut=deb,
date_fin=fin, date_fin=fin,
etat=etat, etat=etat,
etud=etud, etud=etud,
module=moduleimpl_id, moduleimpl=moduleimpl,
) )
if type(nouv_assiduite) is Assiduite:
db.session.add(nouv_assiduite) db.session.add(nouv_assiduite)
db.session.commit() db.session.commit()
return (200, {"assiduiteid": nouv_assiduite.assiduiteid}) return (200, {"assiduite_id": nouv_assiduite.assiduite_id})
except ScoValueError as excp:
return ( return (
404, 404,
{ excp.args[0],
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),
) )
@bp.route("/assiduite/delete", methods=["POST"], defaults={"batch": False}) @bp.route("/assiduite/delete", methods=["POST"])
@api_web_bp.route("/assiduite/delete", methods=["POST"], defaults={"batch": False}) @api_web_bp.route("/assiduite/delete", methods=["POST"])
@bp.route(
"/assiduite/delete/batch",
methods=["POST"],
defaults={"batch": True},
)
@api_web_bp.route(
"/assiduite/delete/batch",
methods=["POST"],
defaults={"batch": True},
)
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def delete(batch: bool = False): def delete():
""" """
Suppression d'une assiduité à partir de son id Suppression d'une assiduité à partir de son id
""" """
if batch: assiduites: list[int] = request.get_json(force=True)
assiduites: list[int] = request.get_json(force=True).get("batch", [])
output = {"errors": {}, "success": {}} output = {"errors": {}, "success": {}}
for i, ass in enumerate(assiduites): for i, ass in enumerate(assiduites):
@ -424,16 +392,6 @@ def delete(batch: bool = False):
db.session.commit() db.session.commit()
return jsonify(output) 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): def delete_singular(assiduite_id: int, db):
assiduite: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() assiduite: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
@ -443,13 +401,13 @@ def delete_singular(assiduite_id: int, db):
return (200, "OK") return (200, "OK")
@bp.route("/assiduite/<int:assiduiteid>/edit", methods=["POST"]) @bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
@api_web_bp.route("/assiduite/<int:assiduiteid>/edit", methods=["POST"]) @api_web_bp.route("/assiduite/<int:assiduite_id>/edit", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange) # @permission_required(Permission.ScoAssiduiteChange)
def edit(assiduiteid: int): def edit(assiduite_id: int):
""" """
Edition d'une assiduité à partir de son id Edition d'une assiduité à partir de son id
La requête doit avoir un content type "application/json": La requête doit avoir un content type "application/json":
@ -458,7 +416,7 @@ def edit(assiduiteid: int):
"moduleimpl_id": 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] = [] errors: List[str] = []
data = request.get_json(force=True) data = request.get_json(force=True)
@ -474,19 +432,22 @@ def edit(assiduiteid: int):
# Cas 2 : Moduleimpl_id # Cas 2 : Moduleimpl_id
moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl_id = data.get("moduleimpl_id", False)
moduleimpl: ModuleImpl = None
if moduleimpl_id is not False: if moduleimpl_id is not False:
try:
if moduleimpl_id is not None: if moduleimpl_id is not None:
moduleimpl_id: int = int(moduleimpl_id) moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
if moduleimpl_id < 0 or not Assiduite.verif_moduleimpl( if moduleimpl is None:
moduleimpl_id, assiduite.etudid 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") errors.append("param 'moduleimpl_id': etud non inscrit")
else:
assiduite.moduleimpl_id = moduleimpl_id
else:
assiduite.moduleimpl_id = moduleimpl_id assiduite.moduleimpl_id = moduleimpl_id
except:
errors.append("param 'moduleimpl_id': invalide")
if errors != []: if errors != []:
err: str = ", ".join(errors) err: str = ", ".join(errors)
return json_error(404, err) return json_error(404, err)

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -19,6 +19,7 @@ from app.models import FormSemestre, FormSemestreInscription, Identite
from app.models import GroupDescr, Partition from app.models import GroupDescr, Partition
from app.models.groups import group_membership from app.models.groups import group_membership
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu 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) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group = query.first_or_404() 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}: 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") return json_error(404, "etud non inscrit au formsemestre du groupe")
groups = (
GroupDescr.query.filter_by(partition_id=group.partition.id) sco_groups.change_etud_group_in_partition(
.join(group_membership) etudid, group_id, group.partition.to_dict()
.filter_by(etudid=etudid)
) )
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}) 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) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group = query.first_or_404() group = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if etud in group.etuds: if etud in group.etuds:
group.etuds.remove(etud) group.etuds.remove(etud)
db.session.commit() db.session.commit()
@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition = query.first_or_404() partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
groups = ( groups = (
GroupDescr.query.filter_by(partition_id=partition_id) GroupDescr.query.filter_by(partition_id=partition_id)
.join(group_membership) .join(group_membership)
@ -262,8 +258,10 @@ def group_create(partition_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404() partition: Partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.groups_editable: 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 data = request.get_json(force=True) # may raise 400 Bad Request
group_name = data.get("group_name") group_name = data.get("group_name")
if group_name is None: 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) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group: GroupDescr = query.first_or_404() group: GroupDescr = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not group.partition.groups_editable: 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 formsemestre_id = group.partition.formsemestre_id
log(f"deleting {group}") log(f"deleting {group}")
db.session.delete(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) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group: GroupDescr = query.first_or_404() group: GroupDescr = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not group.partition.groups_editable: 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 data = request.get_json(force=True) # may raise 400 Bad Request
group_name = data.get("group_name") group_name = data.get("group_name")
if group_name is not None: if group_name is not None:
@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_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 data = request.get_json(force=True) # may raise 400 Bad Request
partition_name = data.get("partition_name") partition_name = data.get("partition_name")
if partition_name is None: if partition_name is None:
@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_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 partition_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(partition_ids, int) and not all( if not isinstance(partition_ids, int) and not all(
isinstance(x, int) for x in partition_ids isinstance(x, int) for x in partition_ids
@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404() 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 group_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(group_ids, int) and not all( if not isinstance(group_ids, int) and not all(
isinstance(x, int) for x in group_ids isinstance(x, int) for x in group_ids
@ -484,6 +492,8 @@ def partition_edit(partition_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404() 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 data = request.get_json(force=True) # may raise 400 Bad Request
modified = False modified = False
partition_name = data.get("partition_name") partition_name = data.get("partition_name")
@ -542,6 +552,8 @@ def partition_delete(partition_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404() partition: Partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.partition_name: if not partition.partition_name:
return json_error(404, "ne peut pas supprimer la partition par défaut") return json_error(404, "ne peut pas supprimer la partition par défaut")
is_parcours = partition.is_parcours() is_parcours = partition.is_parcours()

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""ScoDoc 9 API : outils """ScoDoc 9 API : outils

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -361,7 +361,7 @@ class BulletinBUT:
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etat_inscription": etat_inscription, "etat_inscription": etat_inscription,
"options": sco_preferences.bulletin_option_affichage( "options": sco_preferences.bulletin_option_affichage(
formsemestre.id, self.prefs formsemestre, self.prefs
), ),
} }
if not published: if not published:
@ -465,6 +465,7 @@ class BulletinBUT:
"ressources": {}, "ressources": {},
"saes": {}, "saes": {},
"ues": {}, "ues": {},
"ues_capitalisees": {},
} }
) )

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7:
avec la même interface. avec la même interface.
""" """
import collections
from typing import Union from typing import Union
from flask import g, url_for 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 import sco_codes_parcours as sco_codes
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
from app.scodoc import sco_utils as scu 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 from app.scodoc import sco_cursus_dut
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT): def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
super().__init__(etud, formsemestre_id, res) super().__init__(etud, formsemestre_id, res)
# Ajustements pour le BUT # Ajustements pour le BUT
@ -65,3 +67,117 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
def parcours_validated(self): def parcours_validated(self):
"True si le parcours est validé" "True si le parcours est validé"
return False # XXX TODO 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()
}

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
from xml.etree import ElementTree from xml.etree import ElementTree

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -64,7 +64,7 @@ import re
from typing import Union from typing import Union
import numpy as np import numpy as np
from flask import g, url_for from flask import flash, g, url_for
from app import db from app import db
from app import log from app import log
@ -91,9 +91,15 @@ from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours as sco_codes 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 import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException, ScoValueError from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
class NoRCUEError(ScoValueError): class NoRCUEError(ScoValueError):
@ -170,7 +176,7 @@ class DecisionsProposees:
def __repr__(self) -> str: def __repr__(self) -> str:
return f"""<{self.__class__.__name__} valid={self.code_valide return f"""<{self.__class__.__name__} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}""" } codes={self.codes} explanation={self.explanation}>"""
class DecisionsProposeesAnnee(DecisionsProposees): class DecisionsProposeesAnnee(DecisionsProposees):
@ -204,7 +210,12 @@ class DecisionsProposeesAnnee(DecisionsProposees):
etud: Identite, etud: Identite,
formsemestre: FormSemestre, formsemestre: FormSemestre,
): ):
assert formsemestre.formation.is_apc()
if formsemestre.formation.referentiel_competence is None:
raise ScoNoReferentielCompetences(formation=formsemestre.formation)
super().__init__(etud=etud) super().__init__(etud=etud)
self.formsemestre = formsemestre
"le formsemestre utilisé pour construire ce deca"
self.formsemestre_id = formsemestre.id self.formsemestre_id = formsemestre.id
"l'id du formsemestre utilisé pour construire ce deca" "l'id du formsemestre utilisé pour construire ce deca"
formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre) 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 # 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) # (mais on pourra évidemment valider des UE et même des RCUE)
self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6) self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6)
"vrai si jury de fin d'année scolaire (propose code annuel)" "vrai si jury de fin d'année scolaire (propose code annuel)"
self.formsemestre_impair = formsemestre_impair 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 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 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 self.annee_but = (formsemestre_last.semestre_id + 1) // 2
"le rang de l'année dans le BUT: 1, 2, 3" "le rang de l'année dans le BUT: 1, 2, 3"
assert self.annee_but in (1, 2, 3) assert self.annee_but in (1, 2, 3)
self.rcues_annee = [] 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) 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)" "état de l'inscription dans le semestre le plus avancé (pair si année complète)"
self.inscription_etat_pair = ( 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" "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) 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) 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 # XXX TODO ajouter condition pour passage en S5
# Enfin calcule les codes des UE: # Enfin calcule les codes des UE:
@ -343,12 +366,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue.compute_codes() dec_ue.compute_codes()
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR # Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
expl_rcues = ( plural = self.nb_validables > 1
f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}" expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
) "s" if plural else ""} sur {self.nb_competences}"""
if self.admis: if self.admis:
self.codes = [sco_codes.ADM] + self.codes self.codes = [sco_codes.ADM] + self.codes
self.explanation = expl_rcues
# elif not self.jury_annuel: # elif not self.jury_annuel:
# self.codes = [] # pas de décision annuelle sur semestres impairs # self.codes = [] # pas de décision annuelle sur semestres impairs
elif self.inscription_etat != scu.INSCRIT: elif self.inscription_etat != scu.INSCRIT:
@ -364,9 +386,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ABL, sco_codes.ABL,
sco_codes.EXCLU, sco_codes.EXCLU,
] ]
expl_rcues = ""
elif self.passage_de_droit: elif self.passage_de_droit:
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes 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 elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante
self.codes = [ self.codes = [
sco_codes.RED, sco_codes.RED,
@ -374,7 +396,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.PAS1NCI, sco_codes.PAS1NCI,
sco_codes.ADJ, sco_codes.ADJ,
] + self.codes ] + 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: else:
self.codes = [ self.codes = [
sco_codes.RED, sco_codes.RED,
@ -383,17 +405,21 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.ADJ, sco_codes.ADJ,
sco_codes.PASD, # voir #488 (discutable, conventions locales) sco_codes.PASD, # voir #488 (discutable, conventions locales)
] + self.codes ] + 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 # Si l'un des semestres est extérieur, propose ADM
if ( if (
self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT" self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT"
) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"): ) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"):
self.codes.insert(0, sco_codes.ADM) 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: def infos(self) -> str:
@ -526,9 +552,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def compute_rcues_annee(self) -> list[RegroupementCoherentUE]: def compute_rcues_annee(self) -> list[RegroupementCoherentUE]:
"""Liste des regroupements d'UE à considérer cette année. """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. 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: if self.formsemestre_pair is None or self.formsemestre_impair is None:
return [] return []
@ -537,6 +562,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
for ue_pair in self.ues_pair: for ue_pair in self.ues_pair:
rcue = None rcue = None
for ue_impair in self.ues_impair: 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: if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
rcue = RegroupementCoherentUE( rcue = RegroupementCoherentUE(
self.etud, self.etud,
@ -548,19 +587,22 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) )
ues_impair_sans_rcue.discard(ue_impair.id) ues_impair_sans_rcue.discard(ue_impair.id)
break break
if rcue is None: # if rcue is None and not self.a_cheval:
raise NoRCUEError(deca=self, ue=ue_pair) # raise NoRCUEError(deca=self, ue=ue_pair)
if rcue is not None:
rcues_annee.append(rcue) rcues_annee.append(rcue)
if len(ues_impair_sans_rcue) > 0: # Si jury annuel (pas à cheval), on doit avoir tous les RCUEs:
ue = UniteEns.query.get(ues_impair_sans_rcue.pop()) # if len(ues_impair_sans_rcue) > 0 and not self.a_cheval:
raise NoRCUEError(deca=self, ue=ue) # ue = UniteEns.query.get(ues_impair_sans_rcue.pop())
# raise NoRCUEError(deca=self, ue=ue)
return rcues_annee return rcues_annee
def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]: def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]:
"""Pour chaque niveau de compétence de cette année, construit """Pour chaque niveau de compétence de cette année, construit
le DecisionsProposeesRCUE, le DecisionsProposeesRCUE, ou None s'il n'y en a pas
ou None s'il n'y en a pas
(ne devrait pas arriver car compute_rcues_annee vérifie déjà cela). (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 } Return: { niveau_id : DecisionsProposeesRCUE }
""" """
# Retrouve le RCUE associé à chaque niveau # Retrouve le RCUE associé à chaque niveau
@ -591,22 +633,42 @@ class DecisionsProposeesAnnee(DecisionsProposees):
d[dec_rcue.rcue.ue_2.id] = dec_rcue d[dec_rcue.rcue.ue_2.id] = dec_rcue
return d return d
def next_annee_semestre_id(self, code: str) -> int: def next_semestre_ids(self, code: str) -> set[int]:
"""L'indice du semestre dans lequel l'étudiant est autorisé à """Les indices des semestres dans lequels l'étudiant est autorisé
poursuivre l'année suivante. None si aucun.""" à poursuivre après le semestre courant.
if self.formsemestre_pair is None: """
return None # seulement sur année ids = set()
if code == RED: # La poursuite d'études dans un semestre pair dune même année
return self.formsemestre_pair.semestre_id - 1 # est de droit pour tout étudiant:
elif ( if (self.formsemestre.semestre_id % 2) and sco_codes.ParcoursBUT.NB_SEM:
code in sco_codes.BUT_CODES_PASSAGE 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 dUE ;
# - 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 and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM
): ):
return self.formsemestre_pair.semestre_id + 1 ids.add(self.formsemestre.semestre_id + 1)
return None
if code == RED:
ids.add(
self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2
)
return ids
def record_form(self, form: dict): def record_form(self, form: dict):
"""Enregistre les codes de jury en base """Enregistre les codes de jury en base
à partir d'un dict représentant le formulaire jury BUT:
form dict: form dict:
- 'code_ue_1896' : 'AJ' code pour l'UE id 1896 - 'code_ue_1896' : 'AJ' code pour l'UE id 1896
- 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6 - '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. et qu'il n'y en a pas déjà, enregistre ceux par défaut.
""" """
log("jury_but.DecisionsProposeesAnnee.record_form") 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: for key in form:
code = form[key] code = form[key]
# Codes d'UE # Codes d'UE
@ -626,7 +690,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue = self.decisions_ues.get(ue_id) dec_ue = self.decisions_ues.get(ue_id)
if not dec_ue: if not dec_ue:
raise ScoValueError(f"UE invalide ue_id={ue_id}") raise ScoValueError(f"UE invalide ue_id={ue_id}")
dec_ue.record(code) codes_ues.append((dec_ue, code))
else: else:
# Codes de RCUE # Codes de RCUE
m = re.match(r"^code_rcue_(\d+)$", key) 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) dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id)
if not dec_rcue: if not dec_rcue:
raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}") raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}")
dec_rcue.record(code) codes_rcues.append((dec_rcue, code))
elif key == "code_annee": elif key == "code_annee":
# Code annuel # 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() self.record_all()
db.session.commit() 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. """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 no_overwrite, ne fait rien si un code est déjà enregistré.
Si l'étudiant est DEM ou DEF, ne fait rien. Si l'étudiant est DEM ou DEF, ne fait rien.
""" """
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:
return return False
if code and not code in self.codes: if code and not code in self.codes:
raise ScoValueError( raise ScoValueError(
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}" 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 if code != self.code_valide and (self.code_valide is None or not no_overwrite):
return # no change # Enregistrement du code annuel BUT
if self.validation: if self.validation:
db.session.delete(self.validation) db.session.delete(self.validation)
db.session.flush() db.session.commit()
if code is None: if code is None:
self.validation = None self.validation = None
else: else:
@ -670,32 +742,32 @@ class DecisionsProposeesAnnee(DecisionsProposees):
annee_scolaire=self.annee_scolaire(), annee_scolaire=self.annee_scolaire(),
code=code, code=code,
) )
db.session.add(self.validation)
db.session.commit()
log(f"Recording {self}: {code}")
Scolog.logdb( Scolog.logdb(
method="jury_but", method="jury_but",
etudid=self.etud.id, etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}", msg=f"Validation année BUT{self.annee_but}: {code}",
) )
db.session.add(self.validation)
# --- Autorisation d'inscription dans semestre suivant ? # --- Autorisation d'inscription dans semestre suivant ?
if self.formsemestre_pair is not None:
if code is None:
ScolarAutorisationInscription.delete_autorisation_etud( ScolarAutorisationInscription.delete_autorisation_etud(
etudid=self.etud.id, etudid=self.etud.id,
origin_formsemestre_id=self.formsemestre_pair.id, origin_formsemestre_id=self.formsemestre.id,
) )
else: for next_semestre_id in self.next_semestre_ids(code):
next_semestre_id = self.next_annee_semestre_id(code)
if next_semestre_id is not None:
ScolarAutorisationInscription.autorise_etud( ScolarAutorisationInscription.autorise_etud(
self.etud.id, self.etud.id,
self.formsemestre_pair.formation.formation_code, self.formsemestre.formation.formation_code,
self.formsemestre_pair.id, self.formsemestre.id,
next_semestre_id, next_semestre_id,
) )
self.recorded = True
db.session.commit() db.session.commit()
self.recorded = True
self.invalidate_formsemestre_cache() self.invalidate_formsemestre_cache()
return True
def invalidate_formsemestre_cache(self): def invalidate_formsemestre_cache(self):
"invalide le résultats des deux formsemestres" "invalide le résultats des deux formsemestres"
@ -706,29 +778,71 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_pair is not None: if self.formsemestre_pair is not None:
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id) 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, """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 = ( modif = False
list(self.decisions_ues.values()) # Toujours valider dans l'ordre UE, RCUE, Année
+ list(self.decisions_rcue_by_niveau.values()) annee_scolaire = self.formsemestre.annee_scolaire()
+ [self] # UEs
) for dec_ue in self.decisions_ues.values():
for dec in decisions: if (
if not dec.recorded: not dec_ue.recorded
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
# rappel: le code par défaut est en tête # 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 # 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): def erase(self, only_one_sem=False):
"""Efface les décisions de jury de cet étudiant """Efface les décisions de jury de cet étudiant
pour cette année: décisions d'UE, de RCUE, d'année, pour cette année: décisions d'UE, de RCUE, d'année,
et autorisations d'inscription émises. et autorisations d'inscription émises.
Efface même si étudiant DEM ou DEF. 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, # N'efface que les autorisations venant de ce semestre,
# et les validations de ses UEs # et les validations de ses UEs
ScolarAutorisationInscription.delete_autorisation_etud( ScolarAutorisationInscription.delete_autorisation_etud(
@ -757,22 +871,37 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) )
for validation in validations: for validation in validations:
db.session.delete(validation) 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() self.invalidate_formsemestre_cache()
def get_autorisations_passage(self) -> list[int]: def get_autorisations_passage(self) -> list[int]:
"""Les liste des indices de semestres auxquels on est autorisé à """Liste des indices de semestres auxquels on est autorisé à
s'inscrire depuis cette année""" s'inscrire depuis le semestre courant.
formsemestre = self.formsemestre_pair or self.formsemestre_impair """
if not formsemestre: return sorted(
return [] [
return [
a.semestre_id a.semestre_id
for a in ScolarAutorisationInscription.query.filter_by( for a in ScolarAutorisationInscription.query.filter_by(
etudid=self.etud.id, 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: def descr_niveaux_validation(self, line_sep: str = "\n") -> str:
"""Description textuelle des niveaux validés (enregistrés) """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)) validations.append(", ".join(v for v in valids if v))
return line_sep.join(validations) 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( def list_ue_parcour_etud(
formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT
) -> tuple[ApcParcours, list[UniteEns]]: ) -> tuple[ApcParcours, list[UniteEns]]:
"""Parcour dans lequel l'étudiant est inscrit, """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: if res.etuds_parcour_id[etud.id] is None:
parcour = None parcour = None
@ -820,6 +970,7 @@ def list_ue_parcour_etud(
.order_by(UniteEns.numero) .order_by(UniteEns.numero)
.all() .all()
) )
ues = [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues]
return parcour, ues return parcour, ues
@ -845,6 +996,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
inscription_etat: str = scu.INSCRIT, inscription_etat: str = scu.INSCRIT,
): ):
super().__init__(etud=dec_prop_annee.etud) super().__init__(etud=dec_prop_annee.etud)
self.deca = dec_prop_annee
self.rcue = rcue self.rcue = rcue
if rcue is None: # RCUE non dispo, eg un seul semestre if rcue is None: # RCUE non dispo, eg un seul semestre
self.codes = [] self.codes = []
@ -876,30 +1028,48 @@ class DecisionsProposeesRCUE(DecisionsProposees):
or dec_prop_annee.formsemestre_pair.modalite == "EXT" or dec_prop_annee.formsemestre_pair.modalite == "EXT"
): ):
self.codes.insert(0, sco_codes.ADM) 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): def __repr__(self) -> str:
"""Enregistre le code""" 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: if self.rcue is None:
return # pas de RCUE a enregistrer return False # pas de RCUE a enregistrer
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:
return return False
if code and not code in self.codes: if code and not code in self.codes:
raise ScoValueError( raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" 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): if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True self.recorded = True
return # no change return False # no change
parcours_id = self.parcour.id if self.parcour is not None else None parcours_id = self.parcour.id if self.parcour is not None else None
if self.validation: if self.validation:
db.session.delete(self.validation) db.session.delete(self.validation)
db.session.flush() db.session.commit()
if code is None: if code is None:
self.validation = None self.validation = None
else: 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( self.validation = ApcValidationRCUE(
etudid=self.etud.id, etudid=self.etud.id,
formsemestre_id=self.rcue.formsemestre_2.id, formsemestre_id=self.rcue.formsemestre_2.id,
@ -908,12 +1078,31 @@ class DecisionsProposeesRCUE(DecisionsProposees):
parcours_id=parcours_id, parcours_id=parcours_id,
code=code, code=code,
) )
db.session.add(self.validation)
db.session.commit()
Scolog.logdb( Scolog.logdb(
method="jury_but", method="jury_but",
etudid=self.etud.id, 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: if self.rcue.formsemestre_1 is not None:
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
formsemestre_id=self.rcue.formsemestre_1.id formsemestre_id=self.rcue.formsemestre_1.id
@ -922,13 +1111,16 @@ class DecisionsProposeesRCUE(DecisionsProposees):
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
formsemestre_id=self.rcue.formsemestre_2.id formsemestre_id=self.rcue.formsemestre_2.id
) )
self.code_valide = code # mise à jour état
self.recorded = True self.recorded = True
return True
def erase(self): def erase(self):
"""Efface la décision de jury de cet étudiant pour cet RCUE""" """Efface la décision de jury de cet étudiant pour cet RCUE"""
# par prudence, on requete toutes les validations, en cas de doublons # par prudence, on requete toutes les validations, en cas de doublons
validations = self.rcue.query_validations() validations = self.rcue.query_validations()
for validation in validations: for validation in validations:
log(f"DecisionsProposeesRCUE: deleting {validation}")
db.session.delete(validation) db.session.delete(validation)
db.session.flush() db.session.flush()
@ -960,7 +1152,7 @@ class DecisionsProposeesUE(DecisionsProposees):
sinon si compensation dans RCUE: CMP sinon si compensation dans RCUE: CMP
sinon: ADJ, AJ 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: # Codes toujours proposés sauf si include_communs est faux:
@ -968,6 +1160,7 @@ class DecisionsProposeesUE(DecisionsProposees):
sco_codes.RAT, sco_codes.RAT,
sco_codes.DEF, sco_codes.DEF,
sco_codes.ABAN, sco_codes.ABAN,
sco_codes.ADJR,
sco_codes.ATJ, sco_codes.ATJ,
sco_codes.DEM, sco_codes.DEM,
sco_codes.UEBSL, 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) # 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) # 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 etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id
).first() ).first()
super().__init__( super().__init__(
etud=etud, 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.formsemestre = formsemestre
self.ue: UniteEns = ue self.ue: UniteEns = ue
self.rcue: RegroupementCoherentUE = None self.rcue: RegroupementCoherentUE = None
@ -1026,9 +1219,13 @@ class DecisionsProposeesUE(DecisionsProposees):
self.moy_ue_with_cap = ue_status["moy"] self.moy_ue_with_cap = ue_status["moy"]
self.ue_status = ue_status 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): def set_rcue(self, rcue: RegroupementCoherentUE):
"""Rattache cette UE à un RCUE. Cela peut modifier les codes """Rattache cette UE à un RCUE. Cela peut modifier les codes
proposés (si compensation)""" proposés par compute_codes() (si compensation)"""
self.rcue = rcue self.rcue = rcue
def compute_codes(self): def compute_codes(self):
@ -1048,9 +1245,10 @@ class DecisionsProposeesUE(DecisionsProposees):
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
self.explanation = "notes insuffisantes" 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. """Enregistre le code jury pour cette UE.
Si no_overwrite, n'enregistre pas s'il y a déjà un code. 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: if code and not code in self.codes:
raise ScoValueError( raise ScoValueError(
@ -1058,10 +1256,16 @@ class DecisionsProposeesUE(DecisionsProposees):
) )
if code == self.code_valide or (self.code_valide is not None and no_overwrite): if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True self.recorded = True
return # no change return False # no change
self.erase() self.erase()
if code is None: if code is None:
self.validation = 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: else:
self.validation = ScolarFormSemestreValidation( self.validation = ScolarFormSemestreValidation(
etudid=self.etud.id, etudid=self.etud.id,
@ -1070,16 +1274,20 @@ class DecisionsProposeesUE(DecisionsProposees):
code=code, code=code,
moy_ue=self.moy_ue, moy_ue=self.moy_ue,
) )
db.session.add(self.validation)
db.session.commit()
Scolog.logdb( Scolog.logdb(
method="jury_but", method="jury_but",
etudid=self.etud.id, 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}") log(f"DecisionsProposeesUE: recording {self.validation}")
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id) sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
self.code_valide = code # mise à jour
self.recorded = True self.recorded = True
return True
def erase(self): def erase(self):
"""Efface la décision de jury de cet étudiant pour cette UE""" """Efface la décision de jury de cet étudiant pour cette UE"""
@ -1090,7 +1298,13 @@ class DecisionsProposeesUE(DecisionsProposees):
for validation in validations: for validation in validations:
log(f"DecisionsProposeesUE: deleting {validation}") log(f"DecisionsProposeesUE: deleting {validation}")
db.session.delete(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: def descr_validation(self) -> str:
"""Description validation niveau enregistrée, pour PV jury. """Description validation niveau enregistrée, pour PV jury.
@ -1106,7 +1320,7 @@ class BUTCursusEtud: # WIP TODO
def __init__(self, formsemestre: FormSemestre, etud: Identite): def __init__(self, formsemestre: FormSemestre, etud: Identite):
if formsemestre.formation.referentiel_competence is None: 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 assert len(etud.formsemestre_inscriptions) > 0
self.formsemestre = formsemestre self.formsemestre = formsemestre
self.etud = etud self.etud = etud

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,12 +1,13 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""Jury BUT: table recap annuelle et liens saisie """Jury BUT: table recap annuelle et liens saisie
""" """
import collections
import time import time
import numpy as np import numpy as np
from flask import g, url_for 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_formsemestre_status
from app.scodoc import sco_pvjury from app.scodoc import sco_pvjury
from app.scodoc import sco_utils as scu 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( 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") # raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
if formsemestre2.formation.referentiel_competence is None: if formsemestre2.formation.referentiel_competence is None:
raise ScoValueError( raise ScoNoReferentielCompetences(formation=formsemestre2.formation)
"""
<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>
"""
)
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 formsemestre2, read_only=read_only, mode=mode
) )
if not rows: if not rows:
@ -153,6 +147,28 @@ def formsemestre_saisie_jury_but(
f""" f"""
</div> </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()} {html_sco_header.sco_footer()}
""" """
) )
@ -268,6 +284,10 @@ class RowCollector:
self["_nom_disp_order"] = etud.sort_key self["_nom_disp_order"] = etud.sort_key
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
self["_nom_short_data"] = {
"etudid": etud.id,
"nomprenom": etud.nomprenom,
}
if with_links: if with_links:
self["_nom_short_order"] = etud.sort_key self["_nom_short_order"] = etud.sort_key
self["_nom_short_target"] = url_for( self["_nom_short_target"] = url_for(
@ -352,10 +372,6 @@ class RowCollector:
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
"col_rcue col_rcues_validables" + klass, "col_rcue col_rcues_validables" + klass,
) )
self["_rcues_validables_data"] = {
"etudid": deca.etud.id,
"nomprenom": deca.etud.nomprenom,
}
if len(deca.rcues_annee) > 0: if len(deca.rcues_annee) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair # 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: 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( def get_jury_but_table(
formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True
) -> tuple[list[dict], list[str], list[str]]: ) -> tuple[list[dict], list[str], list[str], dict]:
"""Construit la table des résultats annuels pour le jury BUT""" """Construit la table des résultats annuels pour le jury BUT
=> rows_dict, titles, column_ids, jury_stats
jury_stats est un dict donnant des comptages sur le jury.
"""
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
titles = {} # column_id : title titles = {} # column_id : title
jury_stats = {
"nb_etuds": len(formsemestre2.etuds_inscriptions),
"codes_annuels": collections.Counter(),
}
column_classes = {} column_classes = {}
rows = [] rows = []
for etudid in formsemestre2.etuds_inscriptions: for etudid in formsemestre2.etuds_inscriptions:
@ -417,6 +440,8 @@ def get_jury_but_table(
f"""{deca.code_valide or ''}""", f"""{deca.code_valide or ''}""",
"col_code_annee", "col_code_annee",
) )
if deca.code_valide:
jury_stats["codes_annuels"][deca.code_valide] += 1
# --- Le lien de saisie # --- Le lien de saisie
if mode != "recap" and with_links: if mode != "recap" and with_links:
row.add_cell( row.add_cell(
@ -439,11 +464,14 @@ def get_jury_but_table(
rows.append(row) rows.append(row)
rows_dict = [row.get_row_dict() for row in rows] rows_dict = [row.get_row_dict() for row in rows]
if len(rows_dict) > 0: 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 = [title for title in titles if not title.startswith("_")]
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000)) column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
rows_dict.sort(key=lambda row: row["_nom_disp_order"]) 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]: def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]:

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -15,25 +15,32 @@ from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_validation_auto_but(formsemestre: FormSemestre, only_adm=True) -> int: def formsemestre_validation_auto_but(
"""Calcul automatique des décisions de jury sur une année BUT. formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit). ) -> int:
Si only_adm est faux, on enregistre la première décision proposée par ScoDoc """Calcul automatique des décisions de jury sur une "année" BUT.
(mode à n'utiliser que pour les tests)
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(): if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT") raise ScoValueError("fonction réservée aux formations BUT")
nb_admis = 0 nb_etud_modif = 0
with sco_cache.DeferredSemCacheManager(): with sco_cache.DeferredSemCacheManager():
for etudid in formsemestre.etuds_inscriptions: for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid) etud: Identite = Identite.query.get(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.admis: # année réussie nb_etud_modif += deca.record_all(
nb_admis += 1 no_overwrite=no_overwrite, only_validantes=only_adm
if deca.admis or not only_adm: )
deca.record_all()
db.session.commit() db.session.commit()
return nb_admis return nb_etud_modif

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -8,25 +8,34 @@
""" """
import re import re
import numpy as np
import flask import flask
from flask import flash, url_for from flask import flash, render_template, url_for
from flask import g, request from flask import g, request
from app import db from app import db
from app.but import jury_but 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 import res_sem
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import ( from app.models import (
ApcNiveau,
FormSemestre, FormSemestre,
FormSemestreInscription, FormSemestreInscription,
Identite, Identite,
UniteEns, UniteEns,
ScolarAutorisationInscription, ScolarAutorisationInscription,
ScolarFormSemestreValidation,
) )
from app.models.config import ScoDocSiteConfig
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu 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. Si pas read_only, menus sélection codes jury.
""" """
H = [] 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: if deca.jury_annuel:
H.append( H.append(
f""" f"""
<div class="but_section_annee">
<div> <div>
<b>Décision de jury pour l'année :</b> { <b>Décision de jury pour l'année :</b> {
_gen_but_select("code_annee", deca.codes, deca.code_valide, _gen_but_select("code_annee", deca.codes, deca.code_valide,
disabled=True, klass="manual") disabled=True, klass="manual")
} }
<span>({'non ' if deca.code_valide is None else ''}enregistrée)</span> <span>({deca.code_valide or 'non'} enregistrée)</span>
<span>{erase_span}</span> </div>
</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: formsemestre_1 = deca.formsemestre_impair
annee_sco_pair = deca.formsemestre_pair.annee_scolaire() formsemestre_2 = deca.formsemestre_pair
avertissement_redoublement = ( # Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval):
f"année {annee_sco_pair}-{annee_sco_pair+1}" reverse_semestre = (
if annee_sco_pair != deca.annee_scolaire() deca.formsemestre_pair
else "" and deca.formsemestre_impair
and deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut
) )
else: if reverse_semestre:
avertissement_redoublement = "" formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1
H.append( H.append(
f""" 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="but_annee">
<div class="titre"></div> <div class="titre"></div>
<div class="titre">S{deca.formsemestre_impair.semestre_id <div class="titre">{"S" +str(formsemestre_1.semestre_id)
if deca.formsemestre_impair else "-"}</div> if formsemestre_1 else "-"}
<div class="titre">S{deca.formsemestre_pair.semestre_id <span class="avertissement_redoublement">{formsemestre_1.annee_scolaire_str()
if deca.formsemestre_pair else "-"} if formsemestre_1 else ""}</span>
<span class="avertissement_redoublement">{avertissement_redoublement}</span></div> </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> <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 title="{niveau.competence.titre_long}">{niveau.competence.titre}</div>
</div>""" </div>"""
) )
dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None
if dec_rcue is None: ues = [
H.append( ue
"""<div class="niveau_vide"></div><div class="niveau_vide"></div><div class="niveau_vide"></div>""" for ue in deca.ues_impair
) if ue.niveau_competence and ue.niveau_competence.id == niveau.id
continue ]
# Semestre impair 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( H.append(
_gen_but_niveau_ue( _gen_but_niveau_ue(
dec_rcue.rcue.ue_1, ue,
deca.decisions_ues[dec_rcue.rcue.ue_1.id], deca.decisions_ues[ue.id],
disabled=read_only, disabled=read_only or ue_read_only,
annee_prec=ue_read_only,
niveau_id=ue.niveau_competence.id,
) )
) )
# Semestre pair else:
H.append( H.append("""<div class="niveau_vide"></div>""")
_gen_but_niveau_ue(
dec_rcue.rcue.ue_2, # Colonne RCUE
deca.decisions_ues[dec_rcue.rcue.ue_2.id], H.append(_gen_but_rcue(dec_rcue, niveau))
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>"""
)
H.append("</div>") # but_annee H.append("</div>") # but_annee
return "\n".join(H) return "\n".join(H)
@ -138,9 +153,12 @@ def _gen_but_select(
code_valide: str, code_valide: str,
disabled: bool = False, disabled: bool = False,
klass: str = "", klass: str = "",
data: dict = {},
) -> str: ) -> str:
"Le menu html select avec les codes" "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}" f"""<option value="{code}"
{'selected' if code == code_valide else ''} {'selected' if code == code_valide else ''}
@ -151,33 +169,54 @@ def _gen_but_select(
) )
return f"""<select required name="{name}" return f"""<select required name="{name}"
class="but_code {klass}" 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);" onchange="change_menu_code(this);"
{"disabled" if disabled else ""} {"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"]: if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
moy_ue_str = f"""<span class="ue_cap">{ moy_ue_str = f"""<span class="ue_cap">{
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>""" scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
scoplement = f"""<div class="scoplement"> scoplement = f"""<div class="scoplement">
<div> <div>
<b>UE {ue.acronyme} capitalisée le <b>UE {ue.acronyme} capitalisée </b>
{dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")} <span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
</b> </span>
</div> </div>
<div>UE en cours avec moyenne <div>UE en cours
{scu.fmt_note(dec_ue.moy_ue)} { "sans notes" if np.isnan(dec_ue.moy_ue)
else
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
}
</div> </div>
</div> </div>
""" """
else: else:
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>""" 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 = "" scoplement = ""
return f"""<div class="but_niveau_ue { return f"""<div class="but_niveau_ue {
'recorded' if dec_ue.code_valide is not None else ''} 'recorded' if dec_ue.code_valide is not None else ''}
{'annee_prec' if annee_prec else ''}
"> ">
<div title="{ue.titre}">{ue.acronyme}</div> <div title="{ue.titre}">{ue.acronyme}</div>
<div class="but_note with_scoplement"> <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">{ <div class="but_code">{
_gen_but_select("code_ue_"+str(ue.id), _gen_but_select("code_ue_"+str(ue.id),
dec_ue.codes, 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>
</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( def jury_but_semestriel(
formsemestre: FormSemestre, formsemestre: FormSemestre,
etud: Identite, etud: Identite,
read_only: bool, read_only: bool,
navigation_div: str = "", navigation_div: str = "",
) -> 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) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res) parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res)
inscription_etat = etud.inscription_etat(formsemestre.id) inscription_etat = etud.inscription_etat(formsemestre.id)
semestre_terminal = ( semestre_terminal = (
formsemestre.semestre_id >= formsemestre.formation.get_parcours().NB_SEM 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, # Par défaut: autorisé à passer dans le semestre suivant si sem. impair,
# ou si décision déjà enregistrée: # ou si décision déjà enregistrée:
est_autorise_a_passer = (formsemestre.semestre_id % 2) or ( est_autorise_a_passer = (formsemestre.semestre_id % 2) or (
formsemestre.semestre_id + 1 formsemestre.semestre_id + 1
) in ( ) in (a.semestre_id for a in autorisations_passage)
a.semestre_id
for a in ScolarAutorisationInscription.query.filter_by(
etudid=etud.id,
origin_formsemestre_id=formsemestre.id,
)
)
decisions_ues = { decisions_ues = {
ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat) ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat)
for ue in ues for ue in ues
@ -230,9 +314,9 @@ def jury_but_semestriel(
for key in request.form: for key in request.form:
code = request.form[key] code = request.form[key]
# Codes d'UE # Codes d'UE
m = re.match(r"^code_ue_(\d+)$", key) code_match = re.match(r"^code_ue_(\d+)$", key)
if m: if code_match:
ue_id = int(m.group(1)) ue_id = int(code_match.group(1))
dec_ue = decisions_ues.get(ue_id) dec_ue = decisions_ues.get(ue_id)
if not dec_ue: if not dec_ue:
raise ScoValueError(f"UE invalide ue_id={ue_id}") raise ScoValueError(f"UE invalide ue_id={ue_id}")
@ -241,7 +325,9 @@ def jury_but_semestriel(
flash("codes enregistrés") flash("codes enregistrés")
if not semestre_terminal: if not semestre_terminal:
if request.form.get("autorisation_passage"): 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( ScolarAutorisationInscription.autorise_etud(
etud.id, etud.id,
formsemestre.formation.formation_code, formsemestre.formation.formation_code,
@ -250,7 +336,8 @@ def jury_but_semestriel(
) )
db.session.commit() db.session.commit()
flash( 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: else:
if est_autorise_a_passer: if est_autorise_a_passer:
@ -279,7 +366,7 @@ def jury_but_semestriel(
warning = "" warning = ""
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Validation BUT", page_title=f"Validation BUT S{formsemestre.semestre_id}",
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
etudid=etud.id, etudid=etud.id,
cssstyles=("css/jury_but.css",), cssstyles=("css/jury_but.css",),
@ -300,25 +387,35 @@ def jury_but_semestriel(
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a> }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div> </div>
</div> </div>
<h3>Jury sur un semestre BUT isolé</h3> <h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3>
{warning} {warning}
</div> </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="{ erase_span = f"""<a href="{
url_for("notes.formsemestre_jury_but_erase", url_for("notes.formsemestre_jury_but_erase",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, 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: 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( H.append(
f""" f"""
<div class="but_section_annee"> <div class="but_section_annee">
<span>{erase_span}</span>
</div> </div>
<div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></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 H.append("</div>") # but_annee
div_autorisations_passage = (
f"""
<div class="but_autorisations_passage">
<span>Autorisé à passer en&nbsp;:</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: if read_only:
H.append( H.append(
"""<div class="but_explanation"> f"""<div class="but_explanation">
Vous n'avez pas la permission de modifier ces décisions. {"Vous n'avez pas la permission de modifier ces décisions."
Les champs entourés en vert sont enregistrés.</div>""" if formsemestre.etat
else "Semestre verrouillé."}
Les champs entourés en vert sont enregistrés.
</div>
"""
) )
else: else:
if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM: if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM:
@ -375,13 +488,26 @@ def jury_but_semestriel(
else: else:
H.append("""<div class="help">dernier semestre de la formation.</div>""") H.append("""<div class="help">dernier semestre de la formation.</div>""")
H.append( H.append(
""" f"""
<div class="but_buttons"> <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> </div>
""" """
) )
H.append(navigation_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) return "\n".join(H)
@ -407,7 +533,6 @@ def infos_fiche_etud_html(etudid: int) -> str:
# temporaire quick & dirty: affiche le dernier # temporaire quick & dirty: affiche le dernier
try: try:
deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1]) deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1])
if True: # len(deca.rcues_annee) > 0:
return f"""<div class="infos_but"> return f"""<div class="infos_but">
{show_etud(deca, read_only=True)} {show_etud(deca, read_only=True)}
</div> </div>

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # 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): class BonusBethune(BonusSportMultiplicatif):
""" """
Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune. 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 : dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
<ul> <ul>
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant. <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> </ul>
""" """

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -122,6 +122,10 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
event_date : 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 = """ query = """
SELECT DISTINCT SFV.*, ue.ue_code SELECT DISTINCT SFV.*, ue.ue_code
FROM FROM

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # 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 numpy as np
import pandas as pd import pandas as pd
import app
from app import db from app import db
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -484,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults):
if nb_etuds == 0: if nb_etuds == 0:
return pd.Series() return pd.Series()
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1) 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) evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
# Les coefs des évals pour chaque étudiant: là où il a des notes # Les coefs des évals pour chaque étudiant: là où il a des notes
# non neutralisées # non neutralisées

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # 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 db
from app import models from app import models
from app.models import ( from app.models import (
DispenseUE,
FormSemestre, FormSemestre,
FormSemestreInscription,
Identite,
Module, Module,
ModuleImpl, ModuleImpl,
ModuleUECoef, 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( def compute_ue_moys_apc(
sem_cube: np.array, sem_cube: np.array,
etuds: list, etuds: list,

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -16,7 +16,7 @@ from app.comp.res_compat import NotesTableCompat
from app.comp.bonus_spo import BonusSport from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
from app.models.moduleimpls import ModuleImpl 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.sco_codes_parcours import UE_SPORT
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -72,7 +72,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
modimpl.module.ue.type != UE_SPORT modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted 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.formsemestre, self.modimpl_inscr_df.index, self.ues
) )
self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.etud_moy_ue = moy_ue.compute_ue_moys_apc(

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -494,7 +494,7 @@ class ResultatsSemestre(ResultatsCache):
classes: str = "", classes: str = "",
idx: int = 100, 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 row[col_id] = content
if classes: if classes:
row[f"_{col_id}_class"] = classes + f" c{idx}" 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 row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
) )
# --- Rang # --- Rang
if not self.formsemestre.block_moyenne_generale:
idx = add_cell( idx = add_cell(
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
) )
@ -542,12 +543,17 @@ class ResultatsSemestre(ResultatsCache):
formsemestre_id=self.formsemestre.id, formsemestre_id=self.formsemestre.id,
etudid=etudid, etudid=etudid,
) )
row["_nom_short_data"] = {
"etudid": etud.id,
"nomprenom": etud.nomprenom,
}
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"' row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
row["_nom_disp_target"] = row["_nom_short_target"] row["_nom_disp_target"] = row["_nom_short_target"]
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"] row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
idx = 30 # début des colonnes de notes idx = 30 # début des colonnes de notes
# --- Moyenne générale # --- Moyenne générale
if not self.formsemestre.block_moyenne_generale:
moy_gen = self.etud_moy_gen.get(etudid, False) moy_gen = self.etud_moy_gen.get(etudid, False)
note_class = "" note_class = ""
if moy_gen is False: if moy_gen is False:
@ -567,7 +573,8 @@ class ResultatsSemestre(ResultatsCache):
) )
# --- Moyenne d'UE # --- Moyenne d'UE
nb_ues_validables, nb_ues_warning = 0, 0 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) ue_status = self.get_etud_ue_status(etudid, ue.id)
if ue_status is not None: if ue_status is not None:
col_id = f"moy_ue_{ue.id}" col_id = f"moy_ue_{ue.id}"
@ -588,7 +595,7 @@ class ResultatsSemestre(ResultatsCache):
ue.acronyme, ue.acronyme,
fmt_note(val), fmt_note(val),
"col_ue" + note_class, "col_ue" + note_class,
idx, idx_ue * 10000 + idx_ue_start,
) )
titles_bot[ titles_bot[
f"_{col_id}_target_attrs" f"_{col_id}_target_attrs"
@ -609,7 +616,7 @@ class ResultatsSemestre(ResultatsCache):
f"Bonus {ue.acronyme}", f"Bonus {ue.acronyme}",
val_fmt_html if allow_html else val_fmt, val_fmt_html if allow_html else val_fmt,
"col_ue_bonus", "col_ue_bonus",
idx, idx_ue * 10000 + idx_ue_start + 1,
) )
row[f"_bonus_ue_{ue.id}_xls"] = val_fmt row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE # Les moyennes des modules (ou ressources et SAÉs) dans cette UE
@ -654,7 +661,11 @@ class ResultatsSemestre(ResultatsCache):
val_fmt_html, val_fmt_html,
# class col_res mod_ue_123 # class col_res mod_ue_123
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}", 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 row[f"_{col_id}_xls"] = val_fmt
if modimpl.module.module_type == scu.ModuleType.MALUS: if modimpl.module.module_type == scu.ModuleType.MALUS:
@ -704,7 +715,7 @@ class ResultatsSemestre(ResultatsCache):
else: else:
jury_code_sem = "" jury_code_sem = ""
else: else:
# formations classiqes: code semestre # formations classiques: code semestre
dec_sem = self.validations.decisions_jury.get(etudid) dec_sem = self.validations.decisions_jury.get(etudid)
jury_code_sem = dec_sem["code"] if dec_sem else "" jury_code_sem = dec_sem["code"] if dec_sem else ""
idx = add_cell( idx = add_cell(
@ -722,17 +733,22 @@ class ResultatsSemestre(ResultatsCache):
f"""<a href="{url_for('notes.formsemestre_validation_etud_form', f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid 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", "col_jury_link",
idx, idx,
) )
rows.append(row) 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) self._recap_add_admissions(rows, titles)
# tri par rang croissant # tri par rang croissant
if not self.formsemestre.block_moyenne_generale:
rows.sort(key=lambda e: e["_rang_order"]) rows.sort(key=lambda e: e["_rang_order"])
else:
rows.sort(key=lambda e: e["_ues_validables_order"], reverse=True)
# INFOS POUR FOOTER # INFOS POUR FOOTER
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note) 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(): for row in bottom_infos.values():
row[c_class] = row.get(c_class, "") + " col_empty" 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... # --- TABLE FOOTER: ECTS, moyennes, min, max...
footer_rows = [] footer_rows = []
for (bottom_line, row) in bottom_infos.items(): for (bottom_line, row) in bottom_infos.items():
@ -772,7 +802,7 @@ class ResultatsSemestre(ResultatsCache):
return (rows, footer_rows, titles, column_ids) return (rows, footer_rows, titles, column_ids)
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict: 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 = ( row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
{"_tr_class": "bottom_info", "_title": "Min."}, {"_tr_class": "bottom_info", "_title": "Min."},
{"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"},
@ -832,7 +862,7 @@ class ResultatsSemestre(ResultatsCache):
row_moy[f"_{colid}_class"] = "col_empty" row_moy[f"_{colid}_class"] = "col_empty"
row_apo[colid] = modimpl.module.code_apogee or "" 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, "min": row_min,
"max": row_max, "max": row_max,
"moy": row_moy, "moy": row_moy,
@ -880,7 +910,7 @@ class ResultatsSemestre(ResultatsCache):
} }
first = True first = True
for i, cid in enumerate(fields): 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: if first:
titles[f"_{cid}_class"] = "admission admission_first" titles[f"_{cid}_class"] = "admission admission_first"
first = False first = False
@ -899,10 +929,29 @@ class ResultatsSemestre(ResultatsCache):
else: else:
row[f"_{cid}_class"] = "admission" 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 """Ajoute les colonnes indiquant les groupes
rows est une liste de dict avec une clé "etudid" rows est une liste de dict avec une clé "etudid"
Les colonnes ont la classe css "partition" 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( partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
self.formsemestre.id self.formsemestre.id
@ -951,6 +1000,7 @@ class ResultatsSemestre(ResultatsCache):
row[rg_cid] = rang.get(row["etudid"], "") row[rg_cid] = rang.get(row["etudid"], "")
first_partition = False first_partition = False
return col_order
def _recap_add_evaluations( def _recap_add_evaluations(
self, rows: list[dict], titles: dict, bottom_infos: dict self, rows: list[dict], titles: dict, bottom_infos: dict

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -16,6 +16,7 @@ import flask_login
import app import app
from app.auth.models import User from app.auth.models import User
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError
class ZUser(object): class ZUser(object):
@ -180,18 +181,23 @@ def scodoc7func(func):
else: else:
arg_names = argspec.args arg_names = argspec.args
for arg_name in arg_names: # pour chaque arg de la fonction vue 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: # peut produire une KeyError s'il manque un argument attendu:
v = req_args[arg_name] v = req_args[arg_name]
# try to convert all arguments to INTEGERS # try to convert all arguments to INTEGERS
# necessary for db ids and boolean values # necessary for db ids and boolean values
try: try:
v = int(v) v = int(v) if v else v
except (ValueError, TypeError): except (ValueError, TypeError) as exc:
pass 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) pos_arg_values.append(v)
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values) # current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
# current_app.logger.info("req_args=%s" % req_args) # current_app.logger.info("req_args=%s" % req_args)

View File

@ -1,7 +1,7 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# ScoDoc # 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 # 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 # 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") ABL = _build_code_field("ABL")
ADC = _build_code_field("ADC") ADC = _build_code_field("ADC")
ADJ = _build_code_field("ADJ") ADJ = _build_code_field("ADJ")
ADJR = _build_code_field("ADJR")
ADM = _build_code_field("ADM") ADM = _build_code_field("ADM")
AJ = _build_code_field("AJ") AJ = _build_code_field("AJ")
ATB = _build_code_field("ATB") ATB = _build_code_field("ATB")

View File

@ -5,7 +5,7 @@
# #
# ScoDoc # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# ScoDoc # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# ScoDoc # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -4,8 +4,8 @@
from app import db from app import db
from app.models import ModuleImpl, ModuleImplInscription from app.models import ModuleImpl, ModuleImplInscription
from app.models.etudiants import Identite 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 from datetime import datetime
@ -14,6 +14,7 @@ class Assiduite(db.Model):
Représente une assiduité: Représente une assiduité:
- une plage horaire lié à un état et un étudiant - une plage horaire lié à un état et un étudiant
- un module si spécifiée - un module si spécifiée
- une description si spécifiée
""" """
__tablename__ = "assiduites" __tablename__ = "assiduites"
@ -40,14 +41,17 @@ class Assiduite(db.Model):
) )
etat = db.Column(db.Integer, nullable=False) etat = db.Column(db.Integer, nullable=False)
desc = db.Column(db.Text)
def to_dict(self) -> dict: def to_dict(self) -> dict:
data = { data = {
"assiduiteid": self.assiduiteid, "assiduite_id": self.assiduite_id,
"etudid": self.etudid, "etudid": self.etudid,
"moduleimpl_id": self.moduleimpl_id, "moduleimpl_id": self.moduleimpl_id,
"date_debut": self.date_debut, "date_debut": self.date_debut,
"date_fin": self.date_fin, "date_fin": self.date_fin,
"etat": self.etat, "etat": self.etat,
"desc": self.desc,
} }
return data return data
@ -58,15 +62,10 @@ class Assiduite(db.Model):
date_debut: datetime, date_debut: datetime,
date_fin: datetime, date_fin: datetime,
etat: EtatAssiduite, etat: EtatAssiduite,
module: int moduleimpl: ModuleImpl = None,
or None = None, # XEV est-ce un id (alors module_id ou modimpl_id), ou un objet (ModuleImpl ??) => cela simplifiera le check d'erreur description: str = None,
) -> object or int: ) -> object or int:
"""Créer une nouvelle assiduité pour l'étudiant """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.
"""
# Vérification de non duplication des périodes # Vérification de non duplication des périodes
assiduites: list[Assiduite] = etud.assiduites.all() assiduites: list[Assiduite] = etud.assiduites.all()
@ -75,67 +74,40 @@ class Assiduite(db.Model):
assiduites = [ assiduites = [
ass ass
for ass in assiduites for ass in assiduites
if verif_interval( # XEV if is_period_overlapping(
(date_debut, date_fin), (date_debut, date_fin),
(ass.date_debut, ass.date_fin), (ass.date_debut, ass.date_fin),
) )
] ]
if len(assiduites) != 0: 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 # Vérification de l'existence du module pour l'étudiant
if cls.verif_moduleimpl(module, etud): if moduleimpl.est_inscrit(etud):
nouv_assiduite = Assiduite( nouv_assiduite = Assiduite(
date_debut=date_debut, date_debut=date_debut,
date_fin=date_fin, date_fin=date_fin,
etat=etat, etat=etat,
etudiant=etud, etudiant=etud,
moduleimpl_id=module, moduleimpl_id=moduleimpl.id,
desc=description,
) )
else: else:
return 2 raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
else: else:
nouv_assiduite = Assiduite( nouv_assiduite = Assiduite(
date_debut=date_debut, date_debut=date_debut,
date_fin=date_fin, date_fin=date_fin,
etat=etat, etat=etat,
etudiant=etud, etudiant=etud,
desc=description,
) )
return nouv_assiduite 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): class Justificatif(db.Model):
""" """
@ -147,7 +119,7 @@ class Justificatif(db.Model):
__tablename__ = "justificatifs" __tablename__ = "justificatifs"
justifid = db.Column(db.Integer, primary_key=True) justif_id = db.Column(db.Integer, primary_key=True)
date_debut = db.Column( date_debut = db.Column(
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
@ -167,12 +139,18 @@ class Justificatif(db.Model):
) )
raison = db.Column(db.Text()) 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: def to_dict(self) -> dict:
data = { data = {
"justifid": self.assiduiteid, "justif_id": self.assiduite_id,
"etudid": self.etudid, "etudid": self.etudid,
"date_debut": self.date_debut, "date_debut": self.date_debut,
"date_fin": self.date_fin, "date_fin": self.date_fin,

View File

@ -1,6 +1,6 @@
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""ScoDoc 9 models : Référentiel Compétence BUT 2021 """ScoDoc 9 models : Référentiel Compétence BUT 2021
@ -14,7 +14,7 @@ import sqlalchemy
from app import db from app import db
from app.scodoc.sco_utils import ModuleType 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 # 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é" "Référentiel de compétence d'une spécialité"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
annexe = db.Column(db.Text()) annexe = db.Column(db.Text()) # '1', '22', ...
specialite = db.Column(db.Text()) specialite = db.Column(db.Text()) # 'CJ', 'RT', 'INFO', ...
specialite_long = db.Column(db.Text()) specialite_long = db.Column(
type_titre = db.Column(db.Text()) db.Text()
type_structure = 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" 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 _xml_attribs = { # Orébut xml attrib : attribute
"type": "type_titre", "type": "type_titre",
"version": "version_orebut", "version": "version_orebut",
@ -92,9 +94,10 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return "" return ""
return self.version_orebut.split()[0] 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. """Représentation complète du ref. de comp.
comme un dict. comme un dict.
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
""" """
return { return {
"dept_id": self.dept_id, "dept_id": self.dept_id,
@ -109,8 +112,14 @@ class ApcReferentielCompetences(db.Model, XMLModel):
if self.scodoc_date_loaded if self.scodoc_date_loaded
else "", else "",
"scodoc_orig_filename": self.scodoc_orig_filename, "scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {x.titre: x.to_dict() for x in self.competences}, "competences": {
"parcours": {x.code: x.to_dict() for x in self.parcours}, 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( def get_niveaux_by_parcours(
@ -172,6 +181,27 @@ class ApcReferentielCompetences(db.Model, XMLModel):
niveaux_by_parcours_no_tc["TC"] = niveaux_tc niveaux_by_parcours_no_tc["TC"] = niveaux_tc
return parcours, niveaux_by_parcours_no_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): class ApcCompetence(db.Model, XMLModel):
"Compétence" "Compétence"
@ -213,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel):
def __repr__(self): def __repr__(self):
return f"<ApcCompetence {self.id} {self.titre!r}>" 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" "repr dict recursive sur situations, composantes, niveaux"
return { return {
"id_orebut": self.id_orebut, "id_orebut": self.id_orebut,
@ -225,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel):
"composantes_essentielles": [ "composantes_essentielles": [
x.to_dict() for x in self.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: 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={ return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>""" self.annee!r} {self.competence!r}>"""
def to_dict(self): def to_dict(self, with_app_critiques=True):
"as a dict, recursif sur les AC" "as a dict, recursif (ou non) sur les AC"
return { return {
"libelle": self.libelle, "libelle": self.libelle,
"annee": self.annee, "annee": self.annee,
"ordre": self.ordre, "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): def to_dict_bul(self):
@ -322,9 +357,8 @@ class ApcNiveau(db.Model, XMLModel):
if annee not in {1, 2, 3}: if annee not in {1, 2, 3}:
raise ValueError("annee invalide pour un parcours BUT") raise ValueError("annee invalide pour un parcours BUT")
if referentiel_competence is None: if referentiel_competence is None:
raise ScoValueError( raise ScoNoReferentielCompetences()
"Pas de référentiel de compétences associé à la formation !"
)
annee_formation = f"BUT{annee}" annee_formation = f"BUT{annee}"
if parcour is None: if parcour is None:
return ApcNiveau.query.filter( 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} d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
return d 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): class ApcAnneeParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View File

@ -2,19 +2,17 @@
"""Décisions de jury (validations) des RCUE et années du BUT """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 typing import Union
from app import db import flask_sqlalchemy
from app import db
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models.but_refcomp import ApcNiveau from app.models.but_refcomp import ApcNiveau
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.ues import UniteEns
from app.models.formations import Formation from app.models.formations import Formation
from app.models.formsemestre import FormSemestre 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_codes_parcours as sco_codes
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -63,13 +61,32 @@ class ApcValidationRCUE(db.Model):
self.ue1}/{self.ue2}:{self.code!r}>""" self.ue1}/{self.ue2}:{self.code!r}>"""
def __str__(self): 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: def niveau(self) -> ApcNiveau:
"""Le niveau de compétence associé à cet RCUE.""" """Le niveau de compétence associé à cet RCUE."""
# Par convention, il est donné par la seconde UE # Par convention, il est donné par la seconde UE
return self.ue2.niveau_competence 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: def to_dict_bul(self) -> dict:
"Export dict pour bulletins: le code et le niveau de compétence" "Export dict pour bulletins: le code et le niveau de compétence"
niveau = self.niveau() niveau = self.niveau()
@ -96,10 +113,6 @@ class RegroupementCoherentUE:
dec_ue_2: "DecisionsProposeesUE", dec_ue_2: "DecisionsProposeesUE",
inscription_etat: str, 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_1 = dec_ue_1.ue
ue_2 = dec_ue_2.ue ue_2 = dec_ue_2.ue
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)... # 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_1.acronyme}({self.moy_ue_1}) {
self.ue_2.acronyme}({self.moy_ue_2})>""" 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( def query_validations(
self, self,
) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE] ) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE]
@ -174,8 +192,9 @@ class RegroupementCoherentUE:
return self.query_validations().count() > 0 return self.query_validations().count() > 0
def est_compensable(self): def est_compensable(self):
"""Vrai si ce RCUE est validable par compensation """Vrai si ce RCUE est validable (uniquement) par compensation
c'est à dire que sa moyenne est > 10 avec une UE < 10 c'est à dire que sa moyenne est > 10 avec une UE < 10.
Note: si ADM, est_compensable est faux.
""" """
return ( return (
(self.moy_rcue is not None) (self.moy_rcue is not None)
@ -296,7 +315,8 @@ class ApcValidationAnnee(db.Model):
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees") formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
def __repr__(self): 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): def __str__(self):
return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}""" 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"]}""") titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
else: else:
titres_rcues.append( titres_rcues.append(
f"""{niveau["competence"]["titre"]}&nbsp;{niveau["ordre"]}:&nbsp;{dec_rcue["code"]}""" f"""{niveau["competence"]["titre"]}&nbsp;{niveau["ordre"]}:&nbsp;{
dec_rcue["code"]}"""
) )
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues) decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
decisions["descr_decisions_niveaux"] = ( decisions["descr_decisions_niveaux"] = (

View File

@ -13,6 +13,7 @@ from app.scodoc.sco_codes_parcours import (
ABL, ABL,
ADC, ADC,
ADJ, ADJ,
ADJR,
ADM, ADM,
AJ, AJ,
ATB, ATB,
@ -34,6 +35,7 @@ CODES_SCODOC_TO_APO = {
ABL: "ABL", ABL: "ABL",
ADC: "ADMC", ADC: "ADMC",
ADJ: "ADM", ADJ: "ADM",
ADJR: "ADM",
ADM: "ADM", ADM: "ADM",
AJ: "AJ", AJ: "AJ",
ATB: "AJAC", ATB: "AJAC",

View File

@ -55,7 +55,8 @@ class Formation(db.Model):
modules = db.relationship("Module", lazy="dynamic", backref="formation") modules = db.relationship("Module", lazy="dynamic", backref="formation")
def __repr__(self): 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: def to_html(self) -> str:
"titre complet pour affichage" "titre complet pour affichage"

View File

@ -1,7 +1,7 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -56,55 +56,58 @@ class FormSemestre(db.Model):
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id"))
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1") semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
titre = db.Column(db.Text()) titre = db.Column(db.Text(), nullable=False)
date_debut = db.Column(db.Date()) date_debut = db.Column(db.Date(), nullable=False)
date_fin = db.Column(db.Date()) date_fin = db.Column(db.Date(), nullable=False)
etat = db.Column( etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
db.Boolean(), nullable=False, default=True, server_default="true" "False si verrouillé"
) # False si verrouillé
modalite = db.Column( modalite = db.Column(
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite") 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( gestion_compensation = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( block_moyenne_generale = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( 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( resp_can_edit = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( resp_can_change_ens = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true" 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( ens_can_edit_eval = db.Column(
db.Boolean(), nullable=False, default=False, server_default="False" 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 ! 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()) elt_annee_apo = db.Column(db.Text())
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
# Relations: # Relations:
etapes = db.relationship( etapes = db.relationship(
@ -114,6 +117,7 @@ class FormSemestre(db.Model):
"ModuleImpl", "ModuleImpl",
backref="formsemestre", backref="formsemestre",
lazy="dynamic", lazy="dynamic",
cascade="all, delete-orphan",
) )
etuds = db.relationship( etuds = db.relationship(
"Identite", "Identite",
@ -153,6 +157,11 @@ class FormSemestre(db.Model):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>" 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: def to_dict(self, convert_objects=False) -> dict:
"""dict (compatible ScoDoc7). """dict (compatible ScoDoc7).
If convert_objects, convert all attributes to native types If convert_objects, convert all attributes to native types
@ -321,7 +330,7 @@ class FormSemestre(db.Model):
if self.formation.is_apc(): if self.formation.is_apc():
modimpls.sort( modimpls.sort(
key=lambda m: ( 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.numero or 0,
m.module.code or 0, m.module.code or 0,
) )

View File

@ -1,7 +1,7 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
############################################################################## ##############################################################################
# ScoDoc # ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
@ -87,6 +87,7 @@ class Partition(db.Model):
def to_dict(self, with_groups=False) -> dict: def to_dict(self, with_groups=False) -> dict:
"""as a dict, with or without groups""" """as a dict, with or without groups"""
d = dict(self.__dict__) d = dict(self.__dict__)
d["partition_id"] = self.id
d.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
d.pop("formsemestre", None) d.pop("formsemestre", None)

View File

@ -20,14 +20,12 @@ class ModuleImpl(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
moduleimpl_id = db.synonym("id") moduleimpl_id = db.synonym("id")
module_id = db.Column( module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
db.Integer,
db.ForeignKey("notes_modules.id"),
)
formsemestre_id = db.Column( formsemestre_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey("notes_formsemestre.id"), db.ForeignKey("notes_formsemestre.id"),
index=True, index=True,
nullable=False,
) )
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id")) responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
# formule de calcul moyenne: # formule de calcul moyenne:
@ -101,6 +99,22 @@ class ModuleImpl(db.Model):
d.pop("module", None) d.pop("module", None)
return d 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 # Enseignants (chargés de TD ou TP) d'un moduleimpl
notes_modules_enseignants = db.Table( notes_modules_enseignants = db.Table(

View File

@ -37,7 +37,9 @@ class Module(db.Model):
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum) # Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0") module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
# Relations: # 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) ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
tags = db.relationship( tags = db.relationship(
"NotesTag", "NotesTag",

View File

@ -1,6 +1,8 @@
"""ScoDoc 9 models : Unités d'Enseignement (UE) """ScoDoc 9 models : Unités d'Enseignement (UE)
""" """
import pandas as pd
from app import db, log from app import db, log
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
@ -109,6 +111,7 @@ class UniteEns(db.Model):
e["ects"] = e["ects"] e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0 e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None 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 with_module_ue_coefs:
if convert_objects: if convert_objects:
e["module_ue_coefs"] = [ e["module_ue_coefs"] = [
@ -217,6 +220,8 @@ class UniteEns(db.Model):
db.session.add(self) db.session.add(self)
db.session.commit() db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_niveau_competence( {self}, {niveau} )") log(f"ue.set_niveau_competence( {self}, {niveau} )")
def set_parcour(self, parcour: ApcParcours): def set_parcour(self, parcour: ApcParcours):
@ -244,17 +249,30 @@ class UniteEns(db.Model):
self.niveau_competence = None self.niveau_competence = None
db.session.add(self) db.session.add(self)
db.session.commit() db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_parcour( {self}, {parcour} )") log(f"ue.set_parcour( {self}, {parcour} )")
class DispenseUE(db.Model): class DispenseUE(db.Model):
"""Dispense d'UE """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. 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) 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( ue_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey(UniteEns.id, ondelete="CASCADE"), db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
@ -273,3 +291,25 @@ class DispenseUE(db.Model):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"""<{self.__class__.__name__} {self.id} etud={ return f"""<{self.__class__.__name__} {self.id} etud={
repr(self.etud)} ue={repr(self.ue)}>""" 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

View File

@ -4,6 +4,7 @@
""" """
from app import db from app import db
from app import log
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models.events import Scolog from app.models.events import Scolog
@ -58,7 +59,7 @@ class ScolarFormSemestreValidation(db.Model):
) )
def __repr__(self): 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): def __str__(self):
if self.ue_id: if self.ue_id:
@ -93,6 +94,10 @@ class ScolarAutorisationInscription(db.Model):
db.ForeignKey("notes_formsemestre.id"), 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: def to_dict(self) -> dict:
"as a dict" "as a dict"
d = dict(self.__dict__) d = dict(self.__dict__)
@ -116,7 +121,10 @@ class ScolarAutorisationInscription(db.Model):
semestre_id=semestre_id, semestre_id=semestre_id,
) )
db.session.add(autorisation) 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 @classmethod
def delete_autorisation_etud( def delete_autorisation_etud(
@ -130,10 +138,11 @@ class ScolarAutorisationInscription(db.Model):
) )
for autorisation in autorisations: for autorisation in autorisations:
db.session.delete(autorisation) db.session.delete(autorisation)
log(f"ScolarAutorisationInscription: deleting {autorisation}")
Scolog.logdb( Scolog.logdb(
"autorise_etud", "autorise_etud",
etudid=etudid, etudid=etudid,
msg=f"annule passage vers S{autorisation.semestre_id}", msg=f"Passage vers S{autorisation.semestre_id}: effacé",
) )
db.session.flush() db.session.flush()

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # 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) etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
(_, parcours) = sco_report.get_codeparcoursetud(etud) (_, parcours) = sco_report.get_codeparcoursetud(etud)
if ( if (
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values())) len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0
> 0
): # Eliminé car NAR apparait dans le parcours ): # Eliminé car NAR apparait dans le parcours
reponse = True reponse = True
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
@ -563,9 +562,8 @@ class JuryPE(object):
dec = nt.get_etud_decision_sem( dec = nt.get_etud_decision_sem(
etudid etudid
) # quelle est la décision du jury ? ) # quelle est la décision du jury ?
if dec and dec["code"] in list( if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
sco_codes_parcours.CODES_SEM_VALIDES.keys() # isinstance( sesMoyennes[i+1], float) and
): # isinstance( sesMoyennes[i+1], float) and
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide" # mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
leFid = sem["formsemestre_id"] leFid = sem["formsemestre_id"]
else: else:

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by
@ -105,13 +105,13 @@ class BaseArchiver(object):
try: try:
scu.GSL.acquire() scu.GSL.acquire()
if not os.path.isdir(path): if not os.path.isdir(path):
log("creating directory %s" % path) log(f"creating directory {path}")
os.mkdir(path) os.mkdir(path)
finally: finally:
scu.GSL.release() scu.GSL.release()
self.initialized = True 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). :return: path to directory of archives for this object (eg formsemestre_id or etudid).
If directory does not yet exist, create it. If directory does not yet exist, create it.
@ -142,7 +142,7 @@ class BaseArchiver(object):
dirs = glob.glob(base + "*") dirs = glob.glob(base + "*")
return [os.path.split(x)[1] for x in dirs] return [os.path.split(x)[1] for x in dirs]
def list_obj_archives(self, oid): def list_obj_archives(self, oid: int):
"""Returns """Returns
:return: list of archive identifiers for this object (paths to non empty dirs) :return: list of archive identifiers for this object (paths to non empty dirs)
""" """
@ -157,7 +157,7 @@ class BaseArchiver(object):
dirs.sort() dirs.sort()
return dirs return dirs
def delete_archive(self, archive_id): def delete_archive(self, archive_id: str):
"""Delete (forever) this archive""" """Delete (forever) this archive"""
self.initialize() self.initialize()
try: try:
@ -166,7 +166,7 @@ class BaseArchiver(object):
finally: finally:
scu.GSL.release() 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""" """Returns date (as a DateTime object) of an archive"""
return datetime.datetime( return datetime.datetime(
*[int(x) for x in os.path.split(archive_id)[1].split("-")] *[int(x) for x in os.path.split(archive_id)[1].split("-")]
@ -183,17 +183,17 @@ class BaseArchiver(object):
files.sort() files.sort()
return [f for f in files if f and f[0] != "_"] 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""" """name identifying archive, to be used in web URLs"""
return os.path.split(archive_id)[1] 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.""" """check if name is valid."""
return re.match( return re.match(
"^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name "^[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)""" """returns archive id (check that name is valid)"""
self.initialize() self.initialize()
if not self.is_valid_archive_name(archive_name): if not self.is_valid_archive_name(archive_name):
@ -206,7 +206,7 @@ class BaseArchiver(object):
raise ScoValueError(f"Archive {archive_name} introuvable") raise ScoValueError(f"Archive {archive_name} introuvable")
return archive_id return archive_id
def get_archive_description(self, archive_id): def get_archive_description(self, archive_id: str) -> str:
"""Return description of archive""" """Return description of archive"""
self.initialize() self.initialize()
filename = os.path.join(archive_id, "_description.txt") filename = os.path.join(archive_id, "_description.txt")
@ -247,7 +247,7 @@ class BaseArchiver(object):
data = data.encode(scu.SCO_ENCODING) data = data.encode(scu.SCO_ENCODING)
self.initialize() self.initialize()
filename = scu.sanitize_filename(filename) 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: try:
scu.GSL.acquire() scu.GSL.acquire()
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
@ -261,16 +261,18 @@ class BaseArchiver(object):
"""Retreive data""" """Retreive data"""
self.initialize() self.initialize()
if not scu.is_valid_filename(filename): 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 ?)") raise ScoValueError("archive introuvable (déjà supprimée ?)")
fname = os.path.join(archive_id, filename) 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: with open(fname, "rb") as f:
data = f.read() data = f.read()
return data return data
def get_archived_file(self, oid, archive_name, filename): 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) archive_id = self.get_id_from_name(oid, archive_name)
data = self.get(archive_id, filename) data = self.get(archive_id, filename)
mime = mimetypes.guess_type(filename)[0] mime = mimetypes.guess_type(filename)[0]

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # 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] recipients = [recipient_addr]
sender = sco_preferences.get_preference("email_from_addr", formsemestre_id) sender = sco_preferences.get_preference("email_from_addr", formsemestre_id)
if copy_addr: if copy_addr:
bcc = copy_addr.strip() bcc = copy_addr.strip().split(",")
else: else:
bcc = "" bcc = ""
@ -1094,7 +1094,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
subject, subject,
sender, sender,
recipients, recipients,
bcc=[bcc], bcc=bcc,
text_body=hea, text_body=hea,
attachments=[ attachments=[
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata} {"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # 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 plusminus = pluslink
try: try:
ects_txt = str(int(ue["ects"])) ects_txt = str(int(ue["ects"]))
except (ValueError, KeyError): except (ValueError, KeyError, TypeError):
ects_txt = "-" ects_txt = "-"
t = { t = {

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # 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é ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10) ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
ADJ = "ADJ" # admis par le jury ADJ = "ADJ" # admis par le jury
ADJR = "ADJR" # UE admise car son RCUE est ADJ
ATT = "ATT" # ATT = "ATT" #
ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant
ATB = "ATB" ATB = "ATB"
@ -158,6 +159,7 @@ CODES_EXPL = {
ABL: "Année blanche", ABL: "Année blanche",
ADC: "Validé par compensation", ADC: "Validé par compensation",
ADJ: "Validé par le Jury", ADJ: "Validé par le Jury",
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
ADM: "Validé", ADM: "Validé",
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)", 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)", 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: # Les codes de semestres:
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} 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_VALIDES_DE_DROIT = {ADM, ADC}
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente 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: # Pour le BUT:
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
CODES_RCUE = {ADM, AJ, CMP}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
@ -205,21 +214,36 @@ BUT_CODES_PASSAGE = {
PAS1NCI, PAS1NCI,
ATJ, 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: def code_semestre_validant(code: str) -> bool:
"Vrai si ce CODE entraine la validation du semestre" "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: def code_semestre_attente(code: str) -> bool:
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)" "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: def code_ue_validant(code: str) -> bool:
"Vrai si ce code d'UE est validant (ie attribue les ECTS)" "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 = { DEVENIR_EXPL = {

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# ScoDoc # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -5,7 +5,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # it under the terms of the GNU General Public License as published by

View File

@ -4,7 +4,7 @@
# #
# Gestion scolarite IUT # 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 # 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 # 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