diff --git a/app/__init__.py b/app/__init__.py index 6946415f..51e122cd 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -502,12 +502,10 @@ def clear_scodoc_cache(): # --------- Logging -def log(msg: str, silent_test=True): +def log(msg: str): """log a message. If Flask app, use configured logger, else stderr. """ - if silent_test and current_app and current_app.config["TESTING"]: - return try: dept = getattr(g, "scodoc_dept", "") msg = f" ({dept}) {msg}" @@ -552,3 +550,22 @@ def scodoc_flash_status_messages(): f"Mode test: mails redirigés vers {email_test_mode_address}", category="warning", ) + + +def critical_error(msg): + """Handle a critical error: flush all caches, display message to the user""" + import app.scodoc.sco_utils as scu + + log(f"\n*** CRITICAL ERROR: {msg}") + send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg) + clear_scodoc_cache() + raise ScoValueError( + f""" + Une erreur est survenue. + + Si le problème persiste, merci de contacter le support ScoDoc via + {scu.SCO_DISCORD_ASSISTANCE} + + {msg} + """ + ) diff --git a/app/api/absences.py b/app/api/absences.py index e098c196..b049bfb7 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ScoDoc 9 API : Absences diff --git a/app/api/assiduites.py b/app/api/assiduites.py index d9e4abf5..b479ab69 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -19,39 +19,34 @@ from app.scodoc.sco_permissions import Permission from flask_login import login_required -from app.models import Identite, Assiduite, FormSemestreInscription, FormSemestre +from app.models import Identite, Assiduite, FormSemestre, ModuleImpl +from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu import app.scodoc.sco_assiduites as scass -@bp.route("/assiduite/") -@api_web_bp.route("/assiduite/") +@bp.route("/assiduite/") +@api_web_bp.route("/assiduite/") @scodoc @permission_required(Permission.ScoView) -# XEV à revoir pour les droits d'accès par département - - -def assiduite( - assiduite_id: int = None, -): # XEV xxx_id (sauf pour etudid qui est l'exception qui confirme la règle) +def assiduite(assiduite_id: int = None): """Retourne un objet assiduité à partir de son id Exemple de résultat: { - "assiduiteid": 1, + "assiduite_id": 1, "etudid": 2, "moduleimpl_id": 3, "date_debut": "2022-10-31T08:00+01:00", "date_fin": "2022-10-31T10:00+01:00", - "etat": "retard" + "etat": "retard", + "desc": "une description", } """ - # XEV je pense qu'il faut requeter ainsi pour vérifier qu'on est dans le bon département - # afin que quelqu'un avec la paermission ScoView dans son département n'ait pas - # accès aux infos des autres départements: à tester + query = Assiduite.query.filter_by(id=assiduite_id) - if g.scodoc_dept: - query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) + # if g.scodoc_dept: + # query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) assiduite = query.first_or_404() @@ -264,21 +259,13 @@ def count_assiduites_formsemestre( return jsonify(scass.get_assiduites_stats(assiduites, metric, filter)) -@bp.route("/assiduite//create", methods=["POST"], defaults={"batch": False}) -@api_web_bp.route( - "/assiduite//create", methods=["POST"], defaults={"batch": False} -) -@bp.route( - "/assiduite//create/batch", methods=["POST"], defaults={"batch": True} -) -@api_web_bp.route( - "/assiduite//create/batch", methods=["POST"], defaults={"batch": True} -) +@bp.route("/assiduite//create", methods=["POST"]) +@api_web_bp.route("/assiduite//create", methods=["POST"]) @scodoc @login_required @permission_required(Permission.ScoView) # @permission_required(Permission.ScoAssiduiteChange) -def create(etudid: int = None, batch: bool = False): +def create(etudid: int = None): """ Création d'une assiduité pour l'étudiant (etudid) La requête doit avoir un content type "application/json": @@ -298,26 +285,17 @@ def create(etudid: int = None, batch: bool = False): """ etud: Identite = Identite.query.filter_by(id=etudid).first_or_404() - if batch: - errors: dict[int, str] = {} - success: dict[ - int, - ] = {} - for i, data in enumerate(request.get_json(force=True).get("batch")): - code, obj = create_singular(data, etud) - if code == 404: - errors[i] = obj - else: - success[i] = obj - return jsonify({"errors": errors, "success": success}) - - else: - code, obj = create_singular(request.get_json(force=True), etud) + errors: dict[int, str] = {} + success: dict[int, object] = {} + for i, data in enumerate(request.get_json(force=True)): + code, obj = create_singular(data, etud) if code == 404: - return json_error(code, obj) + errors[i] = obj else: - return jsonify(obj) + success[i] = obj + + return jsonify({"errors": errors, "success": success}) def create_singular( @@ -355,84 +333,64 @@ def create_singular( # cas 4 : moduleimpl_id - moduleimpl_id = data.get("moduleimpl_id", None) - if moduleimpl_id is not None: - try: - moduleimpl_id: int = int(moduleimpl_id) - if moduleimpl_id < 0: - raise Exception - except: + moduleimpl_id = data.get("moduleimpl_id", False) + moduleimpl: ModuleImpl = None + + if moduleimpl_id is not False: + moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") + # cas 5 : desc + + desc:str = data.get("desc", None) + if errors != []: err: str = ", ".join(errors) return (404, err) # TOUT EST OK - nouv_assiduite: Assiduite or int = Assiduite.create_assiduite( - date_debut=deb, - date_fin=fin, - etat=etat, - etud=etud, - module=moduleimpl_id, - ) - if type(nouv_assiduite) is Assiduite: + try: + nouv_assiduite: Assiduite = Assiduite.create_assiduite( + date_debut=deb, + date_fin=fin, + etat=etat, + etud=etud, + moduleimpl=moduleimpl, + ) + db.session.add(nouv_assiduite) db.session.commit() - return (200, {"assiduiteid": nouv_assiduite.assiduiteid}) - - return ( - 404, - { - 1: "La période sélectionnée est déjà couverte par une autre assiduite", - 2: "L'étudiant ne participe pas au moduleimpl sélectionné", - }.get(nouv_assiduite), - ) + return (200, {"assiduite_id": nouv_assiduite.assiduite_id}) + except ScoValueError as excp: + return ( + 404, + excp.args[0], + ) -@bp.route("/assiduite/delete", methods=["POST"], defaults={"batch": False}) -@api_web_bp.route("/assiduite/delete", methods=["POST"], defaults={"batch": False}) -@bp.route( - "/assiduite/delete/batch", - methods=["POST"], - defaults={"batch": True}, -) -@api_web_bp.route( - "/assiduite/delete/batch", - methods=["POST"], - defaults={"batch": True}, -) +@bp.route("/assiduite/delete", methods=["POST"]) +@api_web_bp.route("/assiduite/delete", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoView) # @permission_required(Permission.ScoAssiduiteChange) -def delete(batch: bool = False): +def delete(): """ Suppression d'une assiduité à partir de son id """ - if batch: - assiduites: list[int] = request.get_json(force=True).get("batch", []) - output = {"errors": {}, "success": {}} + assiduites: list[int] = request.get_json(force=True) + output = {"errors": {}, "success": {}} - for i, ass in enumerate(assiduites): - code, msg = delete_singular(ass, db) - if code == 404: - output["errors"][f"{i}"] = msg - else: - output["success"][f"{i}"] = {"OK": True} - db.session.commit() - return jsonify(output) - - else: - code, msg = delete_singular( - request.get_json(force=True).get("assiduiteid", -1), db - ) + for i, ass in enumerate(assiduites): + code, msg = delete_singular(ass, db) if code == 404: - return json_error(code, msg) - if code == 200: - db.session.commit() - return jsonify({"OK": True}) + output["errors"][f"{i}"] = msg + else: + output["success"][f"{i}"] = {"OK": True} + db.session.commit() + return jsonify(output) def delete_singular(assiduite_id: int, db): @@ -443,13 +401,13 @@ def delete_singular(assiduite_id: int, db): return (200, "OK") -@bp.route("/assiduite//edit", methods=["POST"]) -@api_web_bp.route("/assiduite//edit", methods=["POST"]) +@bp.route("/assiduite//edit", methods=["POST"]) +@api_web_bp.route("/assiduite//edit", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoView) # @permission_required(Permission.ScoAssiduiteChange) -def edit(assiduiteid: int): +def edit(assiduite_id: int): """ Edition d'une assiduité à partir de son id La requête doit avoir un content type "application/json": @@ -458,7 +416,7 @@ def edit(assiduiteid: int): "moduleimpl_id": int } """ - assiduite: Assiduite = Assiduite.query.filter_by(id=assiduiteid).first_or_404() + assiduite: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first_or_404() errors: List[str] = [] data = request.get_json(force=True) @@ -474,19 +432,22 @@ def edit(assiduiteid: int): # Cas 2 : Moduleimpl_id moduleimpl_id = data.get("moduleimpl_id", False) + moduleimpl: ModuleImpl = None + if moduleimpl_id is not False: - try: - if moduleimpl_id is not None: - moduleimpl_id: int = int(moduleimpl_id) - if moduleimpl_id < 0 or not Assiduite.verif_moduleimpl( - moduleimpl_id, assiduite.etudid + if moduleimpl_id is not None: + moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() + if moduleimpl is None: + errors.append("param 'moduleimpl_id': invalide") + else: + if not moduleimpl.est_inscrit( + Identite.query.filter_by(id=assiduite.etudid).first() ): errors.append("param 'moduleimpl_id': etud non inscrit") - + else: + assiduite.moduleimpl_id = moduleimpl_id + else: assiduite.moduleimpl_id = moduleimpl_id - except: - errors.append("param 'moduleimpl_id': invalide") - if errors != []: err: str = ", ".join(errors) return json_error(404, err) diff --git a/app/api/billets_absences.py b/app/api/billets_absences.py index f15a9b49..69d6186f 100644 --- a/app/api/billets_absences.py +++ b/app/api/billets_absences.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/api/departements.py b/app/api/departements.py index 0782453c..bc6c38d6 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/api/etudiants.py b/app/api/etudiants.py index c1ebd07f..cb150fe7 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/api/evaluations.py b/app/api/evaluations.py index a8fb2c26..69e18e7b 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/api/formations.py b/app/api/formations.py index 002ce739..c784f110 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 1c1db110..acc749ca 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/api/jury.py b/app/api/jury.py index b58147c7..f31d1a78 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/api/logos.py b/app/api/logos.py index 5c31ed75..677c2b7b 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/api/partitions.py b/app/api/partitions.py index a6283f48..42307656 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -19,6 +19,7 @@ from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import GroupDescr, Partition from app.models.groups import group_membership from app.scodoc import sco_cache +from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission from app.scodoc import sco_utils as scu @@ -170,24 +171,15 @@ def set_etud_group(etudid: int, group_id: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() + if not group.partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: return json_error(404, "etud non inscrit au formsemestre du groupe") - groups = ( - GroupDescr.query.filter_by(partition_id=group.partition.id) - .join(group_membership) - .filter_by(etudid=etudid) + + sco_groups.change_etud_group_in_partition( + etudid, group_id, group.partition.to_dict() ) - ok = False - for other_group in groups: - if other_group.id == group_id: - ok = True - else: - other_group.etuds.remove(etud) - if not ok: - group.etuds.append(etud) - log(f"set_etud_group({etud}, {group})") - db.session.commit() - sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) + return jsonify({"group_id": group_id, "etudid": etudid}) @@ -207,6 +199,8 @@ def group_remove_etud(group_id: int, etudid: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() + if not group.partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if etud in group.etuds: group.etuds.remove(etud) db.session.commit() @@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") groups = ( GroupDescr.query.filter_by(partition_id=partition_id) .join(group_membership) @@ -262,8 +258,10 @@ def group_create(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if not partition.groups_editable: - return json_error(404, "partition non editable") + return json_error(403, "partition non editable") data = request.get_json(force=True) # may raise 400 Bad Request group_name = data.get("group_name") if group_name is None: @@ -294,8 +292,10 @@ def group_delete(group_id: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group: GroupDescr = query.first_or_404() + if not group.partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if not group.partition.groups_editable: - return json_error(404, "partition non editable") + return json_error(403, "partition non editable") formsemestre_id = group.partition.formsemestre_id log(f"deleting {group}") db.session.delete(group) @@ -318,8 +318,10 @@ def group_edit(group_id: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group: GroupDescr = query.first_or_404() + if not group.partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if not group.partition.groups_editable: - return json_error(404, "partition non editable") + return json_error(403, "partition non editable") data = request.get_json(force=True) # may raise 400 Bad Request group_name = data.get("group_name") if group_name is not None: @@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int): if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + if not formsemestre.etat: + return json_error(403, "formsemestre verrouillé") data = request.get_json(force=True) # may raise 400 Bad Request partition_name = data.get("partition_name") if partition_name is None: @@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int): if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + if not formsemestre.etat: + return json_error(403, "formsemestre verrouillé") partition_ids = request.get_json(force=True) # may raise 400 Bad Request if not isinstance(partition_ids, int) and not all( isinstance(x, int) for x in partition_ids @@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") group_ids = request.get_json(force=True) # may raise 400 Bad Request if not isinstance(group_ids, int) and not all( isinstance(x, int) for x in group_ids @@ -484,6 +492,8 @@ def partition_edit(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") data = request.get_json(force=True) # may raise 400 Bad Request modified = False partition_name = data.get("partition_name") @@ -542,6 +552,8 @@ def partition_delete(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() + if not partition.formsemestre.etat: + return json_error(403, "formsemestre verrouillé") if not partition.partition_name: return json_error(404, "ne peut pas supprimer la partition par défaut") is_parcours = partition.is_parcours() diff --git a/app/api/tools.py b/app/api/tools.py index 90750e12..ec0b8561 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ScoDoc 9 API : outils diff --git a/app/api/users.py b/app/api/users.py index ee2cce24..962aa9e7 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py index 2222dc65..01b1aacd 100644 --- a/app/but/apc_edit_ue.py +++ b/app/but/apc_edit_ue.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index f21e8ff6..d8dbd61c 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -361,7 +361,7 @@ class BulletinBUT: "formsemestre_id": formsemestre.id, "etat_inscription": etat_inscription, "options": sco_preferences.bulletin_option_affichage( - formsemestre.id, self.prefs + formsemestre, self.prefs ), } if not published: @@ -465,6 +465,7 @@ class BulletinBUT: "ressources": {}, "saes": {}, "ues": {}, + "ues_capitalisees": {}, } ) diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py index 526465f5..1ba7a760 100644 --- a/app/but/bulletin_but_pdf.py +++ b/app/but/bulletin_but_pdf.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 01e5c7cc..00564f14 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index e3ae8bb6..bc72ae86 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7: avec la même interface. """ - +import collections from typing import Union from flask import g, url_for @@ -47,12 +47,14 @@ from app.models.validations import ScolarFormSemestreValidation from app.scodoc import sco_codes_parcours as sco_codes from app.scodoc.sco_codes_parcours import RED, UE_STANDARD from app.scodoc import sco_utils as scu -from app.scodoc.sco_exceptions import ScoException, ScoValueError +from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError from app.scodoc import sco_cursus_dut class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): + """Pour compat ScoDoc 7: à revoir pour le BUT""" + def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT): super().__init__(etud, formsemestre_id, res) # Ajustements pour le BUT @@ -65,3 +67,117 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): def parcours_validated(self): "True si le parcours est validé" return False # XXX TODO + + +class EtudCursusBUT: + """L'état de l'étudiant dans son cursus BUT + Liste des niveaux validés/à valider + """ + + def __init__(self, etud: Identite, formation: Formation): + """formation indique la spécialité préparée""" + # Vérifie que l'étudiant est bien inscrit à un sem. de cette formation + if formation.id not in ( + ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions + ): + raise ScoValueError( + f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}" + ) + if not formation.referentiel_competence: + raise ScoNoReferentielCompetences(formation=formation) + # + self.etud = etud + self.formation = formation + self.inscriptions = sorted( + [ + ins + for ins in etud.formsemestre_inscriptions + if ins.formsemestre.formation.referentiel_competence + and ( + ins.formsemestre.formation.referentiel_competence.id + == formation.referentiel_competence.id + ) + ], + key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut), + ) + "Liste des inscriptions aux sem. de la formation, triées par indice et chronologie" + self.parcour: ApcParcours = self.inscriptions[-1].parcour + "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)" + self.niveaux_by_annee = {} + "{ annee : liste des niveaux à valider }" + self.niveaux: dict[int, ApcNiveau] = {} + "cache les niveaux" + for annee in (1, 2, 3): + niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours( + annee, self.parcour + )[1] + # groupe les niveaux de tronc commun et ceux spécifiques au parcour + self.niveaux_by_annee[annee] = niveaux_d["TC"] + ( + niveaux_d[self.parcour.id] if self.parcour else [] + ) + self.niveaux.update( + {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]} + ) + # Probablement inutile: + # # Cherche les validations de jury enregistrées pour chaque niveau + # self.validations_by_niveau = collections.defaultdict(lambda: []) + # " { niveau_id : [ ApcValidationRCUE ] }" + # for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud): + # self.validations_by_niveau[validation_rcue.niveau().id].append( + # validation_rcue + # ) + # self.validation_by_niveau = { + # niveau_id: sorted( + # validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code] + # )[0] + # for niveau_id, validations in self.validations_by_niveau.items() + # } + # "{ niveau_id : meilleure validation pour ce niveau }" + + self.validation_par_competence_et_annee = {} + "{ competence_id : { 'BUT1' : validation_rcue, ... } }" + for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud): + niveau = validation_rcue.niveau() + if not niveau.competence.id in self.validation_par_competence_et_annee: + self.validation_par_competence_et_annee[niveau.competence.id] = {} + previous_validation = self.validation_par_competence_et_annee.get( + niveau.competence.id + ).get(validation_rcue.annee()) + # prend la "meilleure" validation + if (not previous_validation) or ( + sco_codes.BUT_CODES_ORDERED[validation_rcue.code] + > sco_codes.BUT_CODES_ORDERED[previous_validation.code] + ): + self.validation_par_competence_et_annee[niveau.competence.id][ + niveau.annee + ] = validation_rcue + + self.competences = { + competence.id: competence + for competence in ( + self.parcour.query_competences() + if self.parcour + else self.formation.referentiel_competence.get_competences_tronc_commun() + ) + } + "cache { competence_id : competence }" + + def to_dict(self): + """ + { + competence_id : { + annee : meilleure_validation + } + } + """ + return { + competence.id: { + annee: { + self.validation_par_competence_et_annee.get(competence.id, {}).get( + annee + ) + } + for annee in ("BUT1", "BUT2", "BUT3") + } + for competence in self.competences.values() + } diff --git a/app/but/forms/jury_but_forms.py b/app/but/forms/jury_but_forms.py index 0f371960..67bd955a 100644 --- a/app/but/forms/jury_but_forms.py +++ b/app/but/forms/jury_but_forms.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/but/forms/refcomp_forms.py b/app/but/forms/refcomp_forms.py index 41ffff4a..2b817473 100644 --- a/app/but/forms/refcomp_forms.py +++ b/app/but/forms/refcomp_forms.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/but/import_refcomp.py b/app/but/import_refcomp.py index e1e62d48..666deb54 100644 --- a/app/but/import_refcomp.py +++ b/app/but/import_refcomp.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## from xml.etree import ElementTree diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 69fefd51..7ab98972 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -64,7 +64,7 @@ import re from typing import Union import numpy as np -from flask import g, url_for +from flask import flash, g, url_for from app import db from app import log @@ -91,9 +91,15 @@ from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours as sco_codes -from app.scodoc.sco_codes_parcours import RED, UE_STANDARD +from app.scodoc.sco_codes_parcours import ( + BUT_CODES_ORDERED, + CODES_RCUE_VALIDES, + CODES_UE_VALIDES, + RED, + UE_STANDARD, +) from app.scodoc import sco_utils as scu -from app.scodoc.sco_exceptions import ScoException, ScoValueError +from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError class NoRCUEError(ScoValueError): @@ -170,7 +176,7 @@ class DecisionsProposees: def __repr__(self) -> str: return f"""<{self.__class__.__name__} valid={self.code_valide - } codes={self.codes} explanation={self.explanation}""" + } codes={self.codes} explanation={self.explanation}>""" class DecisionsProposeesAnnee(DecisionsProposees): @@ -204,7 +210,12 @@ class DecisionsProposeesAnnee(DecisionsProposees): etud: Identite, formsemestre: FormSemestre, ): + assert formsemestre.formation.is_apc() + if formsemestre.formation.referentiel_competence is None: + raise ScoNoReferentielCompetences(formation=formsemestre.formation) super().__init__(etud=etud) + self.formsemestre = formsemestre + "le formsemestre utilisé pour construire ce deca" self.formsemestre_id = formsemestre.id "l'id du formsemestre utilisé pour construire ce deca" formsemestre_impair, formsemestre_pair = self.comp_formsemestres(formsemestre) @@ -219,23 +230,34 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) ) ) + # Si les années scolaires sont distinctes, on est "à cheval" + self.a_cheval = ( + formsemestre_impair + and formsemestre_pair + and formsemestre_impair.annee_scolaire() + != formsemestre_pair.annee_scolaire() + ) + "vrai si on groupe deux semestres d'années scolaires différentes" # Si on part d'un semestre IMPAIR, il n'y aura pas de décision année proposée # (mais on pourra évidemment valider des UE et même des RCUE) self.jury_annuel: bool = formsemestre.semestre_id in (2, 4, 6) "vrai si jury de fin d'année scolaire (propose code annuel)" self.formsemestre_impair = formsemestre_impair - "le 1er semestre de l'année scolaire considérée (S1, S3, S5)" + "le 1er semestre du groupement (S1, S3, S5)" self.formsemestre_pair = formsemestre_pair - "le second formsemestre de la même année scolaire (S2, S4, S6)" + "le second formsemestre (S2, S4, S6), de la même année scolaire ou d'une précédente" formsemestre_last = formsemestre_pair or formsemestre_impair - "le formsemestre le plus avancé dans cette année" + "le formsemestre le plus avancé (en indice de semestre) dans le groupement" self.annee_but = (formsemestre_last.semestre_id + 1) // 2 "le rang de l'année dans le BUT: 1, 2, 3" assert self.annee_but in (1, 2, 3) self.rcues_annee = [] - "RCUEs de l'année" + """RCUEs de l'année + (peuvent concerner l'année scolaire antérieur pour les redoublants + avec UE capitalisées) + """ self.inscription_etat = etud.inscription_etat(formsemestre_last.id) "état de l'inscription dans le semestre le plus avancé (pair si année complète)" self.inscription_etat_pair = ( @@ -334,8 +356,9 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) "vrai si l'année est réussie, tous niveaux validables ou validés par le jury" self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2) - "Peut passer si plus de la moitié validables et tous > 8" + "Vrai si plus de la moitié des RCUE validables" self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0) + "Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8" # XXX TODO ajouter condition pour passage en S5 # Enfin calcule les codes des UE: @@ -343,12 +366,11 @@ class DecisionsProposeesAnnee(DecisionsProposees): dec_ue.compute_codes() # Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR - expl_rcues = ( - f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}" - ) + plural = self.nb_validables > 1 + expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{ + "s" if plural else ""} sur {self.nb_competences}""" if self.admis: self.codes = [sco_codes.ADM] + self.codes - self.explanation = expl_rcues # elif not self.jury_annuel: # self.codes = [] # pas de décision annuelle sur semestres impairs elif self.inscription_etat != scu.INSCRIT: @@ -364,9 +386,9 @@ class DecisionsProposeesAnnee(DecisionsProposees): sco_codes.ABL, sco_codes.EXCLU, ] + expl_rcues = "" elif self.passage_de_droit: self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes - self.explanation = expl_rcues elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante self.codes = [ sco_codes.RED, @@ -374,7 +396,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): sco_codes.PAS1NCI, sco_codes.ADJ, ] + self.codes - self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8" + expl_rcues += f" et {self.nb_rcues_under_8} < 8" else: self.codes = [ sco_codes.RED, @@ -383,17 +405,21 @@ class DecisionsProposeesAnnee(DecisionsProposees): sco_codes.ADJ, sco_codes.PASD, # voir #488 (discutable, conventions locales) ] + self.codes - self.explanation = ( - expl_rcues - + f""" et {self.nb_rcues_under_8} - niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8""" - ) + expl_rcues += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8""" + # Si l'un des semestres est extérieur, propose ADM if ( self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT" ) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"): self.codes.insert(0, sco_codes.ADM) - + self.explanation = f"
{expl_rcues}
" + messages = self.descr_pb_coherence() + if messages: + self.explanation += ( + '
' + + '
'.join(messages) + + "
" + ) # def infos(self) -> str: @@ -526,9 +552,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): def compute_rcues_annee(self) -> list[RegroupementCoherentUE]: """Liste des regroupements d'UE à considérer cette année. - Pour le moment on ne considère pas de RCUE à cheval sur plusieurs années (redoublants). + On peut avoir un RCUE à cheval sur plusieurs années (redoublants avec UE capitalisées). Si on n'a pas les deux semestres, aucun RCUE. - Raises ScoValueError s'il y a des UE sans RCUE. """ if self.formsemestre_pair is None or self.formsemestre_impair is None: return [] @@ -537,6 +562,20 @@ class DecisionsProposeesAnnee(DecisionsProposees): for ue_pair in self.ues_pair: rcue = None for ue_impair in self.ues_impair: + if self.a_cheval: + # l'UE paire DOIT être capitalisée pour être utilisée + if ( + self.decisions_ues[ue_pair.id].code_valide + not in CODES_UE_VALIDES + ): + continue # ignore cette UE antérieure non capitalisée + # et l'UE impaire doit être actuellement meilleure que + # celle éventuellement capitalisée + if ( + self.decisions_ues[ue_impair.id].ue_status + and self.decisions_ues[ue_impair.id].ue_status["is_capitalized"] + ): + continue # ignore cette UE car capitalisée et actuelle moins bonne if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id: rcue = RegroupementCoherentUE( self.etud, @@ -548,19 +587,22 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) ues_impair_sans_rcue.discard(ue_impair.id) break - if rcue is None: - raise NoRCUEError(deca=self, ue=ue_pair) - rcues_annee.append(rcue) - if len(ues_impair_sans_rcue) > 0: - ue = UniteEns.query.get(ues_impair_sans_rcue.pop()) - raise NoRCUEError(deca=self, ue=ue) + # if rcue is None and not self.a_cheval: + # raise NoRCUEError(deca=self, ue=ue_pair) + if rcue is not None: + rcues_annee.append(rcue) + # Si jury annuel (pas à cheval), on doit avoir tous les RCUEs: + # if len(ues_impair_sans_rcue) > 0 and not self.a_cheval: + # ue = UniteEns.query.get(ues_impair_sans_rcue.pop()) + # raise NoRCUEError(deca=self, ue=ue) return rcues_annee def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]: """Pour chaque niveau de compétence de cette année, construit - le DecisionsProposeesRCUE, - ou None s'il n'y en a pas + le DecisionsProposeesRCUE, ou None s'il n'y en a pas (ne devrait pas arriver car compute_rcues_annee vérifie déjà cela). + + Appelé à la construction du deca, donc avant décisions manuelles. Return: { niveau_id : DecisionsProposeesRCUE } """ # Retrouve le RCUE associé à chaque niveau @@ -591,22 +633,42 @@ class DecisionsProposeesAnnee(DecisionsProposees): d[dec_rcue.rcue.ue_2.id] = dec_rcue return d - def next_annee_semestre_id(self, code: str) -> int: - """L'indice du semestre dans lequel l'étudiant est autorisé à - poursuivre l'année suivante. None si aucun.""" - if self.formsemestre_pair is None: - return None # seulement sur année - if code == RED: - return self.formsemestre_pair.semestre_id - 1 - elif ( - code in sco_codes.BUT_CODES_PASSAGE + def next_semestre_ids(self, code: str) -> set[int]: + """Les indices des semestres dans lequels l'étudiant est autorisé + à poursuivre après le semestre courant. + """ + ids = set() + # La poursuite d'études dans un semestre pair d’une même année + # est de droit pour tout étudiant: + if (self.formsemestre.semestre_id % 2) and sco_codes.ParcoursBUT.NB_SEM: + ids.add(self.formsemestre.semestre_id + 1) + + # La poursuite d’études dans un semestre impair est possible si + # et seulement si l’étudiant a obtenu : + # - la moyenne à plus de la moitié des regroupements cohérents d’UE ; + # - et une moyenne égale ou supérieure à 8 sur 20 à chaque RCUE. + # + # La condition a paru trop stricte à de nombreux collègues. + # ScoDoc ne contraint donc pas à la respecter strictement. + # Si le code est dans BUT_CODES_PASSAGE (ADM, ADJ, PASD, PAS1NCI, ATJ), + # autorise à passer dans le semestre suivant + if ( + self.jury_annuel + and code in sco_codes.BUT_CODES_PASSAGE and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM ): - return self.formsemestre_pair.semestre_id + 1 - return None + ids.add(self.formsemestre.semestre_id + 1) + + if code == RED: + ids.add( + self.formsemestre.semestre_id - (self.formsemestre.semestre_id + 1) % 2 + ) + + return ids def record_form(self, form: dict): """Enregistre les codes de jury en base + à partir d'un dict représentant le formulaire jury BUT: form dict: - 'code_ue_1896' : 'AJ' code pour l'UE id 1896 - 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6 @@ -616,86 +678,96 @@ class DecisionsProposeesAnnee(DecisionsProposees): et qu'il n'y en a pas déjà, enregistre ceux par défaut. """ log("jury_but.DecisionsProposeesAnnee.record_form") - with sco_cache.DeferredSemCacheManager(): - for key in form: - code = form[key] - # Codes d'UE - m = re.match(r"^code_ue_(\d+)$", key) + code_annee = None + codes_rcues = [] # [ (dec_rcue, code), ... ] + codes_ues = [] # [ (dec_ue, code), ... ] + for key in form: + code = form[key] + # Codes d'UE + m = re.match(r"^code_ue_(\d+)$", key) + if m: + ue_id = int(m.group(1)) + dec_ue = self.decisions_ues.get(ue_id) + if not dec_ue: + raise ScoValueError(f"UE invalide ue_id={ue_id}") + codes_ues.append((dec_ue, code)) + else: + # Codes de RCUE + m = re.match(r"^code_rcue_(\d+)$", key) if m: - ue_id = int(m.group(1)) - dec_ue = self.decisions_ues.get(ue_id) - if not dec_ue: - raise ScoValueError(f"UE invalide ue_id={ue_id}") - dec_ue.record(code) - else: - # Codes de RCUE - m = re.match(r"^code_rcue_(\d+)$", key) - if m: - niveau_id = int(m.group(1)) - dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id) - if not dec_rcue: - raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}") - dec_rcue.record(code) - elif key == "code_annee": - # Code annuel - self.record(code) + niveau_id = int(m.group(1)) + dec_rcue = self.decisions_rcue_by_niveau.get(niveau_id) + if not dec_rcue: + raise ScoValueError(f"RCUE invalide niveau_id={niveau_id}") + codes_rcues.append((dec_rcue, code)) + elif key == "code_annee": + # Code annuel + code_annee = code + with sco_cache.DeferredSemCacheManager(): + # Enregistre les codes, dans l'ordre UE, RCUE, Année + for dec_ue, code in codes_ues: + dec_ue.record(code) + for dec_rcue, code in codes_rcues: + dec_rcue.record(code) + self.record(code_annee) self.record_all() - db.session.commit() - def record(self, code: str, no_overwrite=False): + db.session.commit() + + def record(self, code: str, no_overwrite=False) -> bool: """Enregistre le code de l'année, et au besoin l'autorisation d'inscription. Si no_overwrite, ne fait rien si un code est déjà enregistré. Si l'étudiant est DEM ou DEF, ne fait rien. """ if self.inscription_etat != scu.INSCRIT: - return + return False if code and not code in self.codes: raise ScoValueError( f"code annee {html.escape(code)} invalide pour formsemestre {html.escape(self.formsemestre)}" ) - if code == self.code_valide or (self.code_valide is not None and no_overwrite): - self.recorded = True - return # no change - if self.validation: - db.session.delete(self.validation) - db.session.flush() - if code is None: - self.validation = None - else: - self.validation = ApcValidationAnnee( - etudid=self.etud.id, - formsemestre=self.formsemestre_impair, - ordre=self.annee_but, - annee_scolaire=self.annee_scolaire(), - code=code, - ) - Scolog.logdb( - method="jury_but", - etudid=self.etud.id, - msg=f"Validation année BUT{self.annee_but}: {code}", - ) - db.session.add(self.validation) - # --- Autorisation d'inscription dans semestre suivant ? - if self.formsemestre_pair is not None: - if code is None: - ScolarAutorisationInscription.delete_autorisation_etud( - etudid=self.etud.id, - origin_formsemestre_id=self.formsemestre_pair.id, - ) - else: - next_semestre_id = self.next_annee_semestre_id(code) - if next_semestre_id is not None: - ScolarAutorisationInscription.autorise_etud( - self.etud.id, - self.formsemestre_pair.formation.formation_code, - self.formsemestre_pair.id, - next_semestre_id, - ) - self.recorded = True + if code != self.code_valide and (self.code_valide is None or not no_overwrite): + # Enregistrement du code annuel BUT + if self.validation: + db.session.delete(self.validation) + db.session.commit() + if code is None: + self.validation = None + else: + self.validation = ApcValidationAnnee( + etudid=self.etud.id, + formsemestre=self.formsemestre_impair, + ordre=self.annee_but, + annee_scolaire=self.annee_scolaire(), + code=code, + ) + db.session.add(self.validation) + db.session.commit() + log(f"Recording {self}: {code}") + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation année BUT{self.annee_but}: {code}", + ) + + # --- Autorisation d'inscription dans semestre suivant ? + ScolarAutorisationInscription.delete_autorisation_etud( + etudid=self.etud.id, + origin_formsemestre_id=self.formsemestre.id, + ) + for next_semestre_id in self.next_semestre_ids(code): + ScolarAutorisationInscription.autorise_etud( + self.etud.id, + self.formsemestre.formation.formation_code, + self.formsemestre.id, + next_semestre_id, + ) + db.session.commit() + self.recorded = True self.invalidate_formsemestre_cache() + return True def invalidate_formsemestre_cache(self): "invalide le résultats des deux formsemestres" @@ -706,29 +778,71 @@ class DecisionsProposeesAnnee(DecisionsProposees): if self.formsemestre_pair is not None: sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id) - def record_all(self): + def record_all( + self, no_overwrite: bool = True, only_validantes: bool = False + ) -> bool: """Enregistre les codes qui n'ont pas été spécifiés par le formulaire, - et sont donc en mode "automatique" + et sont donc en mode "automatique". + - Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente. + - Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne. + + Si only_validantes, n'enregistre que des décisions "validantes" de droit: ADM ou CMP. + + Return: True si au moins un code modifié et enregistré. """ - decisions = ( - list(self.decisions_ues.values()) - + list(self.decisions_rcue_by_niveau.values()) - + [self] - ) - for dec in decisions: - if not dec.recorded: + modif = False + # Toujours valider dans l'ordre UE, RCUE, Année + annee_scolaire = self.formsemestre.annee_scolaire() + # UEs + for dec_ue in self.decisions_ues.values(): + if ( + not dec_ue.recorded + ) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire: # rappel: le code par défaut est en tête - code = dec.codes[0] if dec.codes else None - # enregistre le code jury seulement s'il n'y a pas déjà de code - dec.record(code, no_overwrite=True) + 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 + # (no_overwrite=True) sauf en mode test yaml + modif |= dec_ue.record(code, no_overwrite=no_overwrite) + # RCUE : + for dec_rcue in self.decisions_rcue_by_niveau.values(): + code = dec_rcue.codes[0] if dec_rcue.codes else None + if ( + (not dec_rcue.recorded) + and ( # enregistre seulement si pas déjà validé "mieux" + (not dec_rcue.validation) + or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0) + < BUT_CODES_ORDERED.get(code, 0) + ) + and ( # décision validante de droit ? + ( + (not only_validantes) + or code in sco_codes.CODES_RCUE_VALIDES_DE_DROIT + ) + ) + ): + modif |= dec_rcue.record(code, no_overwrite=no_overwrite) + # Année: + if not self.recorded: + # rappel: le code par défaut est en tête + code = self.codes[0] if self.codes else None + # enregistre le code jury seulement s'il n'y a pas déjà de code + # (no_overwrite=True) sauf en mode test yaml + if ( + not only_validantes + ) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT: + modif |= self.record(code, no_overwrite=no_overwrite) + + return modif def erase(self, only_one_sem=False): """Efface les décisions de jury de cet étudiant pour cette année: décisions d'UE, de RCUE, d'année, et autorisations d'inscription émises. Efface même si étudiant DEM ou DEF. + Si à cheval, n'efface que pour le semestre d'origine du deca. """ - if only_one_sem: + if only_one_sem or self.a_cheval: # N'efface que les autorisations venant de ce semestre, # et les validations de ses UEs ScolarAutorisationInscription.delete_autorisation_etud( @@ -757,22 +871,37 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) for validation in validations: db.session.delete(validation) - db.session.flush() + Scolog.logdb( + "jury_but", + etudid=self.etud.id, + msg=f"Validation année BUT{self.annee_but}: effacée", + ) + + # Efface éventuelles validations de semestre + # (en principe inutilisées en BUT) + # et autres UEs (en cas de changement d'architecture de formation depuis le jury ?) + # + for validation in ScolarFormSemestreValidation.query.filter_by( + etudid=self.etud.id, formsemestre_id=self.formsemestre_id + ): + db.session.delete(validation) + + db.session.commit() self.invalidate_formsemestre_cache() def get_autorisations_passage(self) -> list[int]: - """Les liste des indices de semestres auxquels on est autorisé à - s'inscrire depuis cette année""" - formsemestre = self.formsemestre_pair or self.formsemestre_impair - if not formsemestre: - return [] - return [ - a.semestre_id - for a in ScolarAutorisationInscription.query.filter_by( - etudid=self.etud.id, - origin_formsemestre_id=formsemestre.id, - ) - ] + """Liste des indices de semestres auxquels on est autorisé à + s'inscrire depuis le semestre courant. + """ + return sorted( + [ + a.semestre_id + for a in ScolarAutorisationInscription.query.filter_by( + etudid=self.etud.id, + origin_formsemestre_id=self.formsemestre.id, + ) + ] + ) def descr_niveaux_validation(self, line_sep: str = "\n") -> str: """Description textuelle des niveaux validés (enregistrés) @@ -800,12 +929,33 @@ class DecisionsProposeesAnnee(DecisionsProposees): validations.append(", ".join(v for v in valids if v)) return line_sep.join(validations) + def descr_pb_coherence(self) -> list[str]: + """Description d'éventuels problèmes de cohérence entre + les décisions *enregistrées* d'UE et de RCUE. + Note: en principe, la cohérence RCUE/UE est assurée au moment de + l'enregistrement (record). + Mais la base peut avoir été modifiée par d'autres voies. + """ + messages = [] + for dec_rcue in self.decisions_rcue_by_niveau.values(): + if dec_rcue.code_valide in CODES_RCUE_VALIDES: + for ue in (dec_rcue.rcue.ue_1, dec_rcue.rcue.ue_2): + dec_ue = self.decisions_ues.get(ue.id) + if dec_ue: + if dec_ue.code_valide not in CODES_UE_VALIDES: + messages.append( + f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !" + ) + else: + messages.append(f"L'UE {ue.acronyme} n'a pas décision (???)") + return messages + def list_ue_parcour_etud( formsemestre: FormSemestre, etud: Identite, res: ResultatsSemestreBUT ) -> tuple[ApcParcours, list[UniteEns]]: """Parcour dans lequel l'étudiant est inscrit, - et liste des UEs à valider pour ce semestre + et liste des UEs à valider pour ce semestre (sans les UE "dispensées") """ if res.etuds_parcour_id[etud.id] is None: parcour = None @@ -820,6 +970,7 @@ def list_ue_parcour_etud( .order_by(UniteEns.numero) .all() ) + ues = [ue for ue in ues if (etud.id, ue.id) not in res.dispense_ues] return parcour, ues @@ -845,6 +996,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): inscription_etat: str = scu.INSCRIT, ): super().__init__(etud=dec_prop_annee.etud) + self.deca = dec_prop_annee self.rcue = rcue if rcue is None: # RCUE non dispo, eg un seul semestre self.codes = [] @@ -876,30 +1028,48 @@ class DecisionsProposeesRCUE(DecisionsProposees): or dec_prop_annee.formsemestre_pair.modalite == "EXT" ): self.codes.insert(0, sco_codes.ADM) + # S'il y a une décision enregistrée: si elle est plus favorable que celle que l'on + # proposerait, la place en tête. + # Sinon, la place en seconde place + if self.code_valide and self.code_valide != self.codes[0]: + code_default = self.codes[0] + if self.code_valide in self.codes: + self.codes.remove(self.code_valide) + if sco_codes.BUT_CODES_ORDERED.get( + self.code_valide, 0 + ) > sco_codes.BUT_CODES_ORDERED.get(code_default, 0): + self.codes.insert(0, self.code_valide) + else: + self.codes.insert(1, self.code_valide) - def record(self, code: str, no_overwrite=False): - """Enregistre le code""" + def __repr__(self) -> str: + return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide + } codes={self.codes} explanation={self.explanation}""" + + def record(self, code: str, no_overwrite=False) -> bool: + """Enregistre le code RCUE. + Note: + - si le RCUE est ADJ, les UE non validées sont passées à ADJ + XXX on pourra imposer ici d'autres règles de cohérence + """ if self.rcue is None: - return # pas de RCUE a enregistrer + return False # pas de RCUE a enregistrer if self.inscription_etat != scu.INSCRIT: - return + return False if code and not code in self.codes: raise ScoValueError( f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" ) if code == self.code_valide or (self.code_valide is not None and no_overwrite): self.recorded = True - return # no change + return False # no change parcours_id = self.parcour.id if self.parcour is not None else None if self.validation: db.session.delete(self.validation) - db.session.flush() + db.session.commit() if code is None: self.validation = None else: - # log( - # f"RCUE.record(etudid={self.etud.id}, ue1_id={self.rcue.ue_1.id}, ue2_id={self.rcue.ue_2.id}, code={code} )" - # ) self.validation = ApcValidationRCUE( etudid=self.etud.id, formsemestre_id=self.rcue.formsemestre_2.id, @@ -908,12 +1078,31 @@ class DecisionsProposeesRCUE(DecisionsProposees): parcours_id=parcours_id, code=code, ) + db.session.add(self.validation) + db.session.commit() Scolog.logdb( method="jury_but", etudid=self.etud.id, - msg=f"Validation RCUE {repr(self.rcue)}", + msg=f"Validation {self.rcue}: {code}", + commit=True, ) - db.session.add(self.validation) + log(f"rcue.record {self}: {code}") + + # Modifie au besoin les codes d'UE + if code == "ADJ": + deca = self.deca + for ue_id in (self.rcue.ue_1.id, self.rcue.ue_2.id): + dec_ue = deca.decisions_ues.get(ue_id) + if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES: + log(f"rcue.record: force ADJR sur {dec_ue}") + flash( + f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR""" + ) + dec_ue.record(sco_codes.ADJR) + + # Valide les niveaux inférieurs de la compétence (code ADSUP) + # TODO + if self.rcue.formsemestre_1 is not None: sco_cache.invalidate_formsemestre( formsemestre_id=self.rcue.formsemestre_1.id @@ -922,13 +1111,16 @@ class DecisionsProposeesRCUE(DecisionsProposees): sco_cache.invalidate_formsemestre( formsemestre_id=self.rcue.formsemestre_2.id ) + self.code_valide = code # mise à jour état self.recorded = True + return True def erase(self): """Efface la décision de jury de cet étudiant pour cet RCUE""" # par prudence, on requete toutes les validations, en cas de doublons validations = self.rcue.query_validations() for validation in validations: + log(f"DecisionsProposeesRCUE: deleting {validation}") db.session.delete(validation) db.session.flush() @@ -960,7 +1152,7 @@ class DecisionsProposeesUE(DecisionsProposees): sinon si compensation dans RCUE: CMP sinon: ADJ, AJ - et proposer toujours: RAT, DEF, ABAN, DEM, UEBSL (codes_communs) + et proposer toujours: RAT, DEF, ABAN, ADJR, DEM, UEBSL (codes_communs) """ # Codes toujours proposés sauf si include_communs est faux: @@ -968,6 +1160,7 @@ class DecisionsProposeesUE(DecisionsProposees): sco_codes.RAT, sco_codes.DEF, sco_codes.ABAN, + sco_codes.ADJR, sco_codes.ATJ, sco_codes.DEM, sco_codes.UEBSL, @@ -982,14 +1175,14 @@ class DecisionsProposeesUE(DecisionsProposees): ): # Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non) # mais ici on a restreint au formsemestre donc une seule (prend la première) - self.validation = ScolarFormSemestreValidation.query.filter_by( + validation = ScolarFormSemestreValidation.query.filter_by( etudid=etud.id, formsemestre_id=formsemestre.id, ue_id=ue.id ).first() super().__init__( etud=etud, - code_valide=self.validation.code if self.validation is not None else None, + code_valide=validation.code if validation is not None else None, ) - # log(f"built {self}") + self.validation = validation self.formsemestre = formsemestre self.ue: UniteEns = ue self.rcue: RegroupementCoherentUE = None @@ -1026,9 +1219,13 @@ class DecisionsProposeesUE(DecisionsProposees): self.moy_ue_with_cap = ue_status["moy"] self.ue_status = ue_status + def __repr__(self) -> str: + return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide + } codes={self.codes} explanation={self.explanation}>""" + def set_rcue(self, rcue: RegroupementCoherentUE): """Rattache cette UE à un RCUE. Cela peut modifier les codes - proposés (si compensation)""" + proposés par compute_codes() (si compensation)""" self.rcue = rcue def compute_codes(self): @@ -1048,9 +1245,10 @@ class DecisionsProposeesUE(DecisionsProposees): self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes self.explanation = "notes insuffisantes" - def record(self, code: str, no_overwrite=False): + def record(self, code: str, no_overwrite=False) -> bool: """Enregistre le code jury pour cette UE. Si no_overwrite, n'enregistre pas s'il y a déjà un code. + Return: True si code enregistré (modifié) """ if code and not code in self.codes: raise ScoValueError( @@ -1058,10 +1256,16 @@ class DecisionsProposeesUE(DecisionsProposees): ) if code == self.code_valide or (self.code_valide is not None and no_overwrite): self.recorded = True - return # no change + return False # no change self.erase() if code is None: self.validation = None + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation UE {self.ue.id} {self.ue.acronyme}: effacée", + commit=True, + ) else: self.validation = ScolarFormSemestreValidation( etudid=self.etud.id, @@ -1070,16 +1274,20 @@ class DecisionsProposeesUE(DecisionsProposees): code=code, moy_ue=self.moy_ue, ) + db.session.add(self.validation) + db.session.commit() Scolog.logdb( method="jury_but", etudid=self.etud.id, - msg=f"Validation UE {self.ue.id}", + msg=f"Validation UE {self.ue.id} {self.ue.acronyme}({self.moy_ue}): {code}", + commit=True, ) - db.session.add(self.validation) log(f"DecisionsProposeesUE: recording {self.validation}") sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id) + self.code_valide = code # mise à jour self.recorded = True + return True def erase(self): """Efface la décision de jury de cet étudiant pour cette UE""" @@ -1090,7 +1298,13 @@ class DecisionsProposeesUE(DecisionsProposees): for validation in validations: log(f"DecisionsProposeesUE: deleting {validation}") db.session.delete(validation) - db.session.flush() + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation UE {validation.ue.id} {validation.ue.acronyme}: effacée", + ) + + db.session.commit() def descr_validation(self) -> str: """Description validation niveau enregistrée, pour PV jury. @@ -1106,7 +1320,7 @@ class BUTCursusEtud: # WIP TODO def __init__(self, formsemestre: FormSemestre, etud: Identite): if formsemestre.formation.referentiel_competence is None: - raise ScoException("BUTCursusEtud: pas de référentiel de compétences") + raise ScoNoReferentielCompetences(formation=formsemestre.formation) assert len(etud.formsemestre_inscriptions) > 0 self.formsemestre = formsemestre self.etud = etud diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py index 2940345e..208d1ef2 100644 --- a/app/but/jury_but_pv.py +++ b/app/but/jury_but_pv.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index d9dbd92a..80929440 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -1,12 +1,13 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Jury BUT: table recap annuelle et liens saisie """ +import collections import time import numpy as np from flask import g, url_for @@ -31,7 +32,7 @@ from app.scodoc.sco_codes_parcours import ( from app.scodoc import sco_formsemestre_status from app.scodoc import sco_pvjury from app.scodoc import sco_utils as scu -from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_exceptions import ScoNoReferentielCompetences def formsemestre_saisie_jury_but( @@ -62,16 +63,9 @@ def formsemestre_saisie_jury_but( # raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs") if formsemestre2.formation.referentiel_competence is None: - raise ScoValueError( - """ -

Pas de référentiel de compétences associé à la formation !

-

Pour associer un référentiel, passer par le menu Semestre / - Voir la formation... et suivre le lien "associer à un référentiel - de compétences" - """ - ) + raise ScoNoReferentielCompetences(formation=formsemestre2.formation) - rows, titles, column_ids = get_jury_but_table( + rows, titles, column_ids, jury_stats = get_jury_but_table( formsemestre2, read_only=read_only, mode=mode ) if not rows: @@ -153,6 +147,28 @@ def formsemestre_saisie_jury_but( f""" +

+
Nb d'étudiants avec décision annuelle: + {sum(jury_stats["codes_annuels"].values())} / {jury_stats["nb_etuds"]} +
+
Codes annuels octroyés:
+ + """ + ) + for code in sorted(jury_stats["codes_annuels"].keys()): + H.append( + f""" + + + + """ + ) + H.append( + f""" +
{code}{jury_stats["codes_annuels"][code]}{ + (100*jury_stats["codes_annuels"][code] / jury_stats["nb_etuds"]):2.1f}% +
+
{html_sco_header.sco_footer()} """ ) @@ -268,6 +284,10 @@ class RowCollector: self["_nom_disp_order"] = etud.sort_key self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") + self["_nom_short_data"] = { + "etudid": etud.id, + "nomprenom": etud.nomprenom, + } if with_links: self["_nom_short_order"] = etud.sort_key self["_nom_short_target"] = url_for( @@ -352,10 +372,6 @@ class RowCollector: + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), "col_rcue col_rcues_validables" + klass, ) - self["_rcues_validables_data"] = { - "etudid": deca.etud.id, - "nomprenom": deca.etud.nomprenom, - } if len(deca.rcues_annee) > 0: # permet un tri par nb de niveaux validables + moyenne gen indicative S_pair if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen: @@ -377,10 +393,17 @@ class RowCollector: def get_jury_but_table( formsemestre2: FormSemestre, read_only: bool = False, mode="jury", with_links=True -) -> tuple[list[dict], list[str], list[str]]: - """Construit la table des résultats annuels pour le jury BUT""" +) -> tuple[list[dict], list[str], list[str], dict]: + """Construit la table des résultats annuels pour le jury BUT + => rows_dict, titles, column_ids, jury_stats + où jury_stats est un dict donnant des comptages sur le jury. + """ res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) titles = {} # column_id : title + jury_stats = { + "nb_etuds": len(formsemestre2.etuds_inscriptions), + "codes_annuels": collections.Counter(), + } column_classes = {} rows = [] for etudid in formsemestre2.etuds_inscriptions: @@ -417,6 +440,8 @@ def get_jury_but_table( f"""{deca.code_valide or ''}""", "col_code_annee", ) + if deca.code_valide: + jury_stats["codes_annuels"][deca.code_valide] += 1 # --- Le lien de saisie if mode != "recap" and with_links: row.add_cell( @@ -439,11 +464,14 @@ def get_jury_but_table( rows.append(row) rows_dict = [row.get_row_dict() for row in rows] if len(rows_dict) > 0: - res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1) + col_idx = res2.recap_add_partitions( + rows_dict, titles, col_idx=row.last_etud_cell_idx + 1 + ) + res2.recap_add_cursus(rows_dict, titles, col_idx=col_idx + 1) column_ids = [title for title in titles if not title.startswith("_")] column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000)) rows_dict.sort(key=lambda row: row["_nom_disp_order"]) - return rows_dict, titles, column_ids + return rows_dict, titles, column_ids, jury_stats def get_jury_but_results(formsemestre: FormSemestre) -> list[dict]: diff --git a/app/but/jury_but_validation_auto.py b/app/but/jury_but_validation_auto.py index 918bd571..674389b5 100644 --- a/app/but/jury_but_validation_auto.py +++ b/app/but/jury_but_validation_auto.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -15,25 +15,32 @@ from app.scodoc import sco_cache from app.scodoc.sco_exceptions import ScoValueError -def formsemestre_validation_auto_but(formsemestre: FormSemestre, only_adm=True) -> int: - """Calcul automatique des décisions de jury sur une année BUT. - Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit). - Si only_adm est faux, on enregistre la première décision proposée par ScoDoc - (mode à n'utiliser que pour les tests) +def formsemestre_validation_auto_but( + formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True +) -> int: + """Calcul automatique des décisions de jury sur une "année" BUT. - Returns: nombre d'étudiants "admis" + - N'enregistre jamais de décisions de l'année scolaire précédente, même + si on a des RCUE "à cheval". + - Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux, + ce qui est utilisé pour certains tests unitaires). + - Normalement, only_adm est True et on n'enregistre que les décisions validantes + de droit: ADM ou CMP. + En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc + (mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys) + + Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code. """ if not formsemestre.formation.is_apc(): raise ScoValueError("fonction réservée aux formations BUT") - nb_admis = 0 + nb_etud_modif = 0 with sco_cache.DeferredSemCacheManager(): for etudid in formsemestre.etuds_inscriptions: etud: Identite = Identite.query.get(etudid) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) - if deca.admis: # année réussie - nb_admis += 1 - if deca.admis or not only_adm: - deca.record_all() + nb_etud_modif += deca.record_all( + no_overwrite=no_overwrite, only_validantes=only_adm + ) db.session.commit() - return nb_admis + return nb_etud_modif diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index c8efb357..a58f41e2 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -8,25 +8,34 @@ """ import re +import numpy as np import flask -from flask import flash, url_for +from flask import flash, render_template, url_for from flask import g, request from app import db from app.but import jury_but -from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE +from app.but.jury_but import ( + DecisionsProposeesAnnee, + DecisionsProposeesRCUE, + DecisionsProposeesUE, +) from app.comp import res_sem from app.comp.res_but import ResultatsSemestreBUT from app.models import ( + ApcNiveau, FormSemestre, FormSemestreInscription, Identite, UniteEns, ScolarAutorisationInscription, + ScolarFormSemestreValidation, ) +from app.models.config import ScoDocSiteConfig from app.scodoc import html_sco_header from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc import sco_preferences from app.scodoc import sco_utils as scu @@ -35,53 +44,50 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str: Si pas read_only, menus sélection codes jury. """ H = [] - if deca.code_valide and not read_only: - erase_span = f"""effacer décisions""" - else: - erase_span = "" - H.append("""
""") if deca.jury_annuel: H.append( f""" +
Décision de jury pour l'année : { _gen_but_select("code_annee", deca.codes, deca.code_valide, disabled=True, klass="manual") } - ({'non ' if deca.code_valide is None else ''}enregistrée) - {erase_span} + ({deca.code_valide or 'non'} enregistrée)
-
{deca.explanation}
+
""" ) - else: - H.append("""
Pas de décision annuelle (sem. impair)
""") - H.append("""
""") - - if deca.formsemestre_pair is not None: - annee_sco_pair = deca.formsemestre_pair.annee_scolaire() - avertissement_redoublement = ( - f"année {annee_sco_pair}-{annee_sco_pair+1}" - if annee_sco_pair != deca.annee_scolaire() - else "" - ) - else: - avertissement_redoublement = "" + formsemestre_1 = deca.formsemestre_impair + formsemestre_2 = deca.formsemestre_pair + # Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval): + reverse_semestre = ( + deca.formsemestre_pair + and deca.formsemestre_impair + and deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut + ) + if reverse_semestre: + formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1 H.append( f""" -
Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}
+
+ Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but} +
+
{deca.explanation}
-
S{deca.formsemestre_impair.semestre_id - if deca.formsemestre_impair else "-"}
-
S{deca.formsemestre_pair.semestre_id - if deca.formsemestre_pair else "-"} - {avertissement_redoublement}
+
{"S" +str(formsemestre_1.semestre_id) + if formsemestre_1 else "-"} + {formsemestre_1.annee_scolaire_str() + if formsemestre_1 else ""} +
+
{"S"+str(formsemestre_2.semestre_id) + if formsemestre_2 else "-"} + {formsemestre_2.annee_scolaire_str() + if formsemestre_2 else ""} +
RCUE
""" ) @@ -91,43 +97,52 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
{niveau.competence.titre}
""" ) - dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) - if dec_rcue is None: - H.append( - """
""" - ) - continue - # Semestre impair - H.append( - _gen_but_niveau_ue( - dec_rcue.rcue.ue_1, - deca.decisions_ues[dec_rcue.rcue.ue_1.id], - disabled=read_only, - ) - ) - # Semestre pair - H.append( - _gen_but_niveau_ue( - dec_rcue.rcue.ue_2, - deca.decisions_ues[dec_rcue.rcue.ue_2.id], - disabled=read_only, - ) - ) - # RCUE - H.append( - f"""
-
{scu.fmt_note(dec_rcue.rcue.moy_rcue)}
-
{ - _gen_but_select("code_rcue_"+str(niveau.id), - dec_rcue.codes, - dec_rcue.code_valide, - disabled=True, klass="manual" + dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None + ues = [ + ue + for ue in deca.ues_impair + if ue.niveau_competence and ue.niveau_competence.id == niveau.id + ] + ue_impair = ues[0] if ues else None + ues = [ + ue + for ue in deca.ues_pair + if ue.niveau_competence and ue.niveau_competence.id == niveau.id + ] + ue_pair = ues[0] if ues else None + # Les UEs à afficher, + # qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant + ues_ro = [ + ( + ue_impair, + (deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id), + ), + ( + ue_pair, + deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id, + ), + ] + # Ordonne selon les dates des 2 semestres considérés: + if reverse_semestre: + ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0] + # Colonnes d'UE: + for ue, ue_read_only in ues_ro: + if ue: + H.append( + _gen_but_niveau_ue( + ue, + deca.decisions_ues[ue.id], + disabled=read_only or ue_read_only, + annee_prec=ue_read_only, + niveau_id=ue.niveau_competence.id, + ) ) - }
-
""" - ) + else: + H.append("""
""") + + # Colonne RCUE + H.append(_gen_but_rcue(dec_rcue, niveau)) + H.append("") # but_annee return "\n".join(H) @@ -138,11 +153,14 @@ def _gen_but_select( code_valide: str, disabled: bool = False, klass: str = "", + data: dict = {}, ) -> str: "Le menu html select avec les codes" - h = "\n".join( + # if disabled: # mauvaise idée car le disabled est traité en JS + # return f"""
{code_valide}
""" + options_htm = "\n".join( [ - f"""""" @@ -151,33 +169,54 @@ def _gen_but_select( ) return f""" + {" ".join( f'data-{k}="{v}"' for (k,v) in data.items() )} + >{options_htm} """ -def _gen_but_niveau_ue(ue: UniteEns, dec_ue: DecisionsProposeesUE, disabled=False): +def _gen_but_niveau_ue( + ue: UniteEns, + dec_ue: DecisionsProposeesUE, + disabled: bool = False, + annee_prec: bool = False, + niveau_id: int = None, +) -> str: if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]: moy_ue_str = f"""{ scu.fmt_note(dec_ue.moy_ue_with_cap)}""" scoplement = f"""
- UE {ue.acronyme} capitalisée le - {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")} - + UE {ue.acronyme} capitalisée + le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")} +
-
UE en cours avec moyenne - {scu.fmt_note(dec_ue.moy_ue)} +
UE en cours + { "sans notes" if np.isnan(dec_ue.moy_ue) + else + ("avec moyenne " + scu.fmt_note(dec_ue.moy_ue) + "") + }
""" else: moy_ue_str = f"""{scu.fmt_note(dec_ue.moy_ue)}""" - scoplement = "" + if dec_ue.code_valide: + scoplement = f"""
+
Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")} + à {dec_ue.validation.event_date.strftime("%Hh%M")} +
+
+ """ + else: + scoplement = "" return f"""
{ue.acronyme}
@@ -186,38 +225,83 @@ def _gen_but_niveau_ue(ue: UniteEns, dec_ue: DecisionsProposeesUE, disabled=Fals
{ _gen_but_select("code_ue_"+str(ue.id), - dec_ue.codes, - dec_ue.code_valide, disabled=disabled + dec_ue.codes, + dec_ue.code_valide, + disabled=disabled, + klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else "" ) }
""" +def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str: + if dec_rcue is None: + return """ +
+
+
Pas de RCUE (UE non capitalisée ?)
+
+ """ + + scoplement = ( + f"""
{ + dec_rcue.validation.to_html() + }
""" + 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""" +
+
+
{scu.fmt_note(dec_rcue.rcue.moy_rcue)}
+ {scoplement} +
+
+ {_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)} + )} +
+
+ """ + + def jury_but_semestriel( formsemestre: FormSemestre, etud: Identite, read_only: bool, navigation_div: str = "", ) -> str: - """Formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel)""" + """Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel).""" res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res) inscription_etat = etud.inscription_etat(formsemestre.id) semestre_terminal = ( formsemestre.semestre_id >= formsemestre.formation.get_parcours().NB_SEM ) + autorisations_passage = ScolarAutorisationInscription.query.filter_by( + etudid=etud.id, + origin_formsemestre_id=formsemestre.id, + ).all() # Par défaut: autorisé à passer dans le semestre suivant si sem. impair, # ou si décision déjà enregistrée: est_autorise_a_passer = (formsemestre.semestre_id % 2) or ( formsemestre.semestre_id + 1 - ) in ( - a.semestre_id - for a in ScolarAutorisationInscription.query.filter_by( - etudid=etud.id, - origin_formsemestre_id=formsemestre.id, - ) - ) + ) in (a.semestre_id for a in autorisations_passage) decisions_ues = { ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat) for ue in ues @@ -230,9 +314,9 @@ def jury_but_semestriel( for key in request.form: code = request.form[key] # Codes d'UE - m = re.match(r"^code_ue_(\d+)$", key) - if m: - ue_id = int(m.group(1)) + code_match = re.match(r"^code_ue_(\d+)$", key) + if code_match: + ue_id = int(code_match.group(1)) dec_ue = decisions_ues.get(ue_id) if not dec_ue: raise ScoValueError(f"UE invalide ue_id={ue_id}") @@ -241,7 +325,9 @@ def jury_but_semestriel( flash("codes enregistrés") if not semestre_terminal: if request.form.get("autorisation_passage"): - if not est_autorise_a_passer: + if not formsemestre.semestre_id + 1 in ( + a.semestre_id for a in autorisations_passage + ): ScolarAutorisationInscription.autorise_etud( etud.id, formsemestre.formation.formation_code, @@ -250,7 +336,8 @@ def jury_but_semestriel( ) db.session.commit() flash( - f"autorisation de passage en S{formsemestre.semestre_id + 1} enregistrée" + f"""autorisation de passage en S{formsemestre.semestre_id + 1 + } enregistrée""" ) else: if est_autorise_a_passer: @@ -279,7 +366,7 @@ def jury_but_semestriel( warning = "" H = [ html_sco_header.sco_header( - page_title="Validation BUT", + page_title=f"Validation BUT S{formsemestre.semestre_id}", formsemestre_id=formsemestre.id, etudid=etud.id, cssstyles=("css/jury_but.css",), @@ -288,37 +375,47 @@ def jury_but_semestriel( f"""
-
-
-
Jury BUT S{formsemestre.id} - - Parcours {(parcour.libelle if parcour else False) or "non spécifié"} +
+
+
Jury BUT S{formsemestre.id} + - Parcours {(parcour.libelle if parcour else False) or "non spécifié"} +
+
{etud.nomprenom}
+
+
-
{etud.nomprenom}
-
- -
-

Jury sur un semestre BUT isolé

- {warning} +

Jury sur un semestre BUT isolé (ne concerne que les UEs)

+ {warning}
-
+ """, ] - if (not read_only) and any([dec.code_valide for dec in decisions_ues.values()]): - erase_span = f"""effacer décisions""" - else: - erase_span = "aucune décision enregistrée pour ce semestre" + + 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"""effacer les décisions enregistrées""" + else: + erase_span = ( + "Cet étudiant n'a aucune décision enregistrée pour ce semestre." + ) H.append( f"""
- {erase_span}
Unités d'enseignement de S{formsemestre.semestre_id}:
""" @@ -354,34 +451,63 @@ def jury_but_semestriel( ) H.append("
") # but_annee + div_autorisations_passage = ( + f""" +
+ Autorisé à passer en : + { ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )} +
+ """ + if autorisations_passage + else """
pas d'autorisations de passage enregistrées.
""" + ) + H.append(div_autorisations_passage) + if read_only: H.append( - """
- Vous n'avez pas la permission de modifier ces décisions. - Les champs entourés en vert sont enregistrés.
""" + f"""
+ {"Vous n'avez pas la permission de modifier ces décisions." + if formsemestre.etat + else "Semestre verrouillé."} + Les champs entourés en vert sont enregistrés. +
+ """ ) else: if formsemestre.semestre_id < formsemestre.formation.get_parcours().NB_SEM: H.append( f"""
- - autoriser à passer dans le semestre S{formsemestre.semestre_id+1} - + + autoriser à passer dans le semestre S{formsemestre.semestre_id+1} +
""" ) else: H.append("""
dernier semestre de la formation.
""") H.append( - """ + f"""
- + + {erase_span}
""" ) - H.append(navigation_div) + + H.append(navigation_div) + H.append("
") + H.append( + render_template( + "but/documentation_codes_jury.html", + nom_univ=f"""Export {sco_preferences.get_preference("InstituteName") + or sco_preferences.get_preference("UnivName") + or "Apogée"}""", + codes=ScoDocSiteConfig.get_codes_apo_dict(), + ) + ) + return "\n".join(H) @@ -407,11 +533,10 @@ def infos_fiche_etud_html(etudid: int) -> str: # temporaire quick & dirty: affiche le dernier try: deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1]) - if True: # len(deca.rcues_annee) > 0: - return f"""
+ return f"""
{show_etud(deca, read_only=True)}
- """ + """ except ScoValueError: pass diff --git a/app/comp/aux_stats.py b/app/comp/aux_stats.py index 3f25a5ad..b0d47924 100644 --- a/app/comp/aux_stats.py +++ b/app/comp/aux_stats.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 436ace89..da9f8acf 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -430,6 +430,22 @@ class BonusAmiens(BonusSportAdditif): # ) +class BonusBesanconVesoul(BonusSportAdditif): + """Bonus IUT Besançon - Vesoul pour les UE libres + +

Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point + sur toutes les moyennes d'UE. +

+ """ + + name = "bonus_besancon_vesoul" + displayed_name = "IUT de Besançon - Vesoul" + classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP + seuil_moy_gen = 0.0 # tous les points sont comptés + proportion_point = 1e10 # infini + bonus_max = 0.2 + + class BonusBethune(BonusSportMultiplicatif): """ Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune. @@ -647,7 +663,10 @@ class BonusCalais(BonusSportAdditif): dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
  • en DUT à la moyenne générale du semestre déjà obtenue par l'étudiant. -
  • en BUT et LP à la moyenne des UE dont l'acronyme fini par BS (ex : UE2.1BS, UE32BS) +
  • +
  • en BUT et LP à la moyenne des UE dont l'acronyme fini par BS + (ex : UE2.1BS, UE32BS) +
""" diff --git a/app/comp/df_cache.py b/app/comp/df_cache.py index 5b555fec..1dea77c2 100644 --- a/app/comp/df_cache.py +++ b/app/comp/df_cache.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/comp/jury.py b/app/comp/jury.py index cad1b2a5..e41af510 100644 --- a/app/comp/jury.py +++ b/app/comp/jury.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -122,6 +122,10 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame event_date : } ] """ + + # Note: pour récupérer aussi les UE validées en CMp ou ADJ, changer une ligne + # and ( SFV.code = 'ADM' or SFV.code = 'ADJ' or SFV.code = 'CMP' ) + query = """ SELECT DISTINCT SFV.*, ue.ue_code FROM diff --git a/app/comp/moy_mat.py b/app/comp/moy_mat.py index 0a752263..a0120f66 100644 --- a/app/comp/moy_mat.py +++ b/app/comp/moy_mat.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 963cae78..1fccbda1 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -39,6 +39,7 @@ from dataclasses import dataclass import numpy as np import pandas as pd +import app from app import db from app.models import Evaluation, EvaluationUEPoids, ModuleImpl from app.scodoc import sco_cache @@ -484,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults): if nb_etuds == 0: return pd.Series() evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1) - assert evals_coefs.shape == (nb_evals,) + if evals_coefs.shape != (nb_evals,): + app.critical_error("compute_module_moy: vals_coefs.shape != nb_evals") evals_notes_20 = self.get_eval_notes_sur_20(modimpl) # Les coefs des évals pour chaque étudiant: là où il a des notes # non neutralisées diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 195c5dc8..089cc68a 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index efca6662..c533c21a 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -33,10 +33,7 @@ import pandas as pd from app import db from app import models from app.models import ( - DispenseUE, FormSemestre, - FormSemestreInscription, - Identite, Module, ModuleImpl, ModuleUECoef, @@ -218,31 +215,6 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: ) -def load_dispense_ues( - formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns] -) -> set[tuple[int, int]]: - """Construit l'ensemble des - etudids = modimpl_inscr_df.index, # les etudids - ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport - - Résultat: set de (etudid, ue_id). - """ - dispense_ues = set() - ue_sem_by_code = {ue.ue_code: ue for ue in ues} - # Prend toutes les dispenses obtenues par des étudiants de ce formsemestre, - # puis filtre sur inscrits et code d'UE UE - for dispense_ue in DispenseUE.query.join( - Identite, FormSemestreInscription - ).filter_by(formsemestre_id=formsemestre.id): - if dispense_ue.etudid in etudids: - # UE dans le semestre avec même code ? - ue = ue_sem_by_code.get(dispense_ue.ue.ue_code) - if ue is not None: - dispense_ues.add((dispense_ue.etudid, ue.id)) - - return dispense_ues - - def compute_ue_moys_apc( sem_cube: np.array, etuds: list, diff --git a/app/comp/res_but.py b/app/comp/res_but.py index ba0f1314..f3f0c97d 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -16,7 +16,7 @@ from app.comp.res_compat import NotesTableCompat from app.comp.bonus_spo import BonusSport from app.models import ScoDocSiteConfig from app.models.moduleimpls import ModuleImpl -from app.models.ues import UniteEns +from app.models.ues import DispenseUE, UniteEns from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc import sco_preferences @@ -72,7 +72,7 @@ class ResultatsSemestreBUT(NotesTableCompat): modimpl.module.ue.type != UE_SPORT for modimpl in self.formsemestre.modimpls_sorted ] - self.dispense_ues = moy_ue.load_dispense_ues( + self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set( self.formsemestre, self.modimpl_inscr_df.index, self.ues ) self.etud_moy_ue = moy_ue.compute_ue_moys_apc( diff --git a/app/comp/res_cache.py b/app/comp/res_cache.py index 890526b9..941caad2 100644 --- a/app/comp/res_cache.py +++ b/app/comp/res_cache.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index f2393676..077c745c 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 567c658d..35f2a637 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -494,7 +494,7 @@ class ResultatsSemestre(ResultatsCache): classes: str = "", idx: int = 100, ): - "Add a row to our table. classes is a list of css class names" + "Add a cell to our table. classes is a list of css class names" row[col_id] = content if classes: row[f"_{col_id}_class"] = classes + f" c{idx}" @@ -519,10 +519,11 @@ class ResultatsSemestre(ResultatsCache): row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx ) # --- Rang - idx = add_cell( - row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx - ) - row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}" + if not self.formsemestre.block_moyenne_generale: + idx = add_cell( + row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx + ) + row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}" # --- Identité étudiant idx = add_cell( row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail", idx @@ -542,32 +543,38 @@ class ResultatsSemestre(ResultatsCache): formsemestre_id=self.formsemestre.id, etudid=etudid, ) + row["_nom_short_data"] = { + "etudid": etud.id, + "nomprenom": etud.nomprenom, + } row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"' row["_nom_disp_target"] = row["_nom_short_target"] row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"] idx = 30 # début des colonnes de notes # --- Moyenne générale - moy_gen = self.etud_moy_gen.get(etudid, False) - note_class = "" - if moy_gen is False: - moy_gen = NO_NOTE - elif isinstance(moy_gen, float) and moy_gen < barre_moy: - note_class = " moy_ue_warning" # en rouge - idx = add_cell( - row, - "moy_gen", - "Moy", - fmt_note(moy_gen), - "col_moy_gen" + note_class, - idx, - ) - titles_bot["_moy_gen_target_attrs"] = ( - 'title="moyenne indicative"' if self.is_apc else "" - ) + if not self.formsemestre.block_moyenne_generale: + moy_gen = self.etud_moy_gen.get(etudid, False) + note_class = "" + if moy_gen is False: + moy_gen = NO_NOTE + elif isinstance(moy_gen, float) and moy_gen < barre_moy: + note_class = " moy_ue_warning" # en rouge + idx = add_cell( + row, + "moy_gen", + "Moy", + fmt_note(moy_gen), + "col_moy_gen" + note_class, + idx, + ) + titles_bot["_moy_gen_target_attrs"] = ( + 'title="moyenne indicative"' if self.is_apc else "" + ) # --- Moyenne d'UE nb_ues_validables, nb_ues_warning = 0, 0 - for ue in ues_sans_bonus: + idx_ue_start = idx + for idx_ue, ue in enumerate(ues_sans_bonus): ue_status = self.get_etud_ue_status(etudid, ue.id) if ue_status is not None: col_id = f"moy_ue_{ue.id}" @@ -588,7 +595,7 @@ class ResultatsSemestre(ResultatsCache): ue.acronyme, fmt_note(val), "col_ue" + note_class, - idx, + idx_ue * 10000 + idx_ue_start, ) titles_bot[ f"_{col_id}_target_attrs" @@ -609,7 +616,7 @@ class ResultatsSemestre(ResultatsCache): f"Bonus {ue.acronyme}", val_fmt_html if allow_html else val_fmt, "col_ue_bonus", - idx, + idx_ue * 10000 + idx_ue_start + 1, ) row[f"_bonus_ue_{ue.id}_xls"] = val_fmt # Les moyennes des modules (ou ressources et SAÉs) dans cette UE @@ -654,7 +661,11 @@ class ResultatsSemestre(ResultatsCache): val_fmt_html, # class col_res mod_ue_123 f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}", - idx, + idx_ue * 10000 + + idx_ue_start + + 1 + + (modimpl.module.module_type or 0) * 1000 + + (modimpl.module.numero or 0), ) row[f"_{col_id}_xls"] = val_fmt if modimpl.module.module_type == scu.ModuleType.MALUS: @@ -704,7 +715,7 @@ class ResultatsSemestre(ResultatsCache): else: jury_code_sem = "" else: - # formations classiqes: code semestre + # formations classiques: code semestre dec_sem = self.validations.decisions_jury.get(etudid) jury_code_sem = dec_sem["code"] if dec_sem else "" idx = add_cell( @@ -722,17 +733,22 @@ class ResultatsSemestre(ResultatsCache): f"""{"saisir" if not jury_code_sem else "modifier"} décision""", + }">{("saisir" if not jury_code_sem else "modifier") + if self.formsemestre.etat else "voir"} décisions""", "col_jury_link", idx, ) rows.append(row) - self.recap_add_partitions(rows, titles) + col_idx = self.recap_add_partitions(rows, titles) + self.recap_add_cursus(rows, titles, col_idx=col_idx + 1) self._recap_add_admissions(rows, titles) # tri par rang croissant - rows.sort(key=lambda e: e["_rang_order"]) + if not self.formsemestre.block_moyenne_generale: + rows.sort(key=lambda e: e["_rang_order"]) + else: + rows.sort(key=lambda e: e["_ues_validables_order"], reverse=True) # INFOS POUR FOOTER bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note) @@ -749,6 +765,20 @@ class ResultatsSemestre(ResultatsCache): for row in bottom_infos.values(): row[c_class] = row.get(c_class, "") + " col_empty" + # Ligne avec la classe de chaque colonne + # récupère le type à partir des classes css (hack...) + row_class = {} + for col_id in titles: + klass = titles.get(f"_{col_id}_class") + if klass: + row_class[col_id] = " ".join( + cls[4:] for cls in klass.split() if cls.startswith("col_") + ) + # cette case (nb d'UE validables) a deux classes col_xxx, on en garde une seule: + if "ues_validables" in row_class[col_id]: + row_class[col_id] = "ues_validables" + bottom_infos["type_col"] = row_class + # --- TABLE FOOTER: ECTS, moyennes, min, max... footer_rows = [] for (bottom_line, row) in bottom_infos.items(): @@ -772,7 +802,7 @@ class ResultatsSemestre(ResultatsCache): return (rows, footer_rows, titles, column_ids) def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict: - """Les informations à mettre en bas de la table: min, max, moy, ECTS""" + """Les informations à mettre en bas de la table: min, max, moy, ECTS, Apo""" row_min, row_max, row_moy, row_coef, row_ects, row_apo = ( {"_tr_class": "bottom_info", "_title": "Min."}, {"_tr_class": "bottom_info"}, @@ -832,7 +862,7 @@ class ResultatsSemestre(ResultatsCache): row_moy[f"_{colid}_class"] = "col_empty" row_apo[colid] = modimpl.module.code_apogee or "" - return { # { key : row } avec key = min, max, moy, coef + return { # { key : row } avec key = min, max, moy, coef, ... "min": row_min, "max": row_max, "moy": row_moy, @@ -880,7 +910,7 @@ class ResultatsSemestre(ResultatsCache): } first = True for i, cid in enumerate(fields): - titles[f"_{cid}_col_order"] = 10000 + i # tout à droite + titles[f"_{cid}_col_order"] = 100000 + i # tout à droite if first: titles[f"_{cid}_class"] = "admission admission_first" first = False @@ -899,10 +929,29 @@ class ResultatsSemestre(ResultatsCache): else: row[f"_{cid}_class"] = "admission" - def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None): + def recap_add_cursus(self, rows: list[dict], titles: dict, col_idx: int = None): + """Ajoute colonne avec code cursus, eg 'S1 S2 S1'""" + cid = "code_cursus" + titles[cid] = "Cursus" + titles[f"_{cid}_col_order"] = col_idx + formation_code = self.formsemestre.formation.formation_code + for row in rows: + etud = Identite.query.get(row["etudid"]) + row[cid] = " ".join( + [ + f"S{ins.formsemestre.semestre_id}" + for ins in reversed(etud.inscriptions()) + if ins.formsemestre.formation.formation_code == formation_code + ] + ) + + def recap_add_partitions( + self, rows: list[dict], titles: dict, col_idx: int = None + ) -> int: """Ajoute les colonnes indiquant les groupes rows est une liste de dict avec une clé "etudid" Les colonnes ont la classe css "partition" + Renvoie l'indice de la dernière colonne utilisée """ partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups( self.formsemestre.id @@ -951,6 +1000,7 @@ class ResultatsSemestre(ResultatsCache): row[rg_cid] = rang.get(row["etudid"], "") first_partition = False + return col_order def _recap_add_evaluations( self, rows: list[dict], titles: dict, bottom_infos: dict diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index 1e4c80bc..8a835694 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py index 0897da01..35affb5c 100644 --- a/app/comp/res_sem.py +++ b/app/comp/res_sem.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/decorators.py b/app/decorators.py index 83441275..5338828f 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -16,6 +16,7 @@ import flask_login import app from app.auth.models import User import app.scodoc.sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError class ZUser(object): @@ -180,19 +181,24 @@ def scodoc7func(func): else: arg_names = argspec.args for arg_name in arg_names: # pour chaque arg de la fonction vue - if arg_name == "REQUEST": # ne devrait plus arriver ! - # debug check, TODO remove after tests - raise ValueError("invalid REQUEST parameter !") - else: - # peut produire une KeyError s'il manque un argument attendu: - v = req_args[arg_name] - # try to convert all arguments to INTEGERS - # necessary for db ids and boolean values - try: - v = int(v) - except (ValueError, TypeError): - pass - pos_arg_values.append(v) + # peut produire une KeyError s'il manque un argument attendu: + v = req_args[arg_name] + # try to convert all arguments to INTEGERS + # necessary for db ids and boolean values + try: + v = int(v) if v else v + except (ValueError, TypeError) as exc: + if arg_name in { + "etudid", + "formation_id", + "formsemestre_id", + "module_id", + "moduleimpl_id", + "partition_id", + "ue_id", + }: + raise ScoValueError("page introuvable (id invalide)") from exc + pos_arg_values.append(v) # current_app.logger.info("pos_arg_values=%s" % pos_arg_values) # current_app.logger.info("req_args=%s" % req_args) # Add keyword arguments diff --git a/app/email.py b/app/email.py index ebd3ae0d..241ef079 100644 --- a/app/email.py +++ b/app/email.py @@ -1,7 +1,7 @@ # -*- coding: UTF-8 -* ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/entreprises/app_relations_entreprises.py b/app/entreprises/app_relations_entreprises.py index c989c12f..8d1c9c0c 100644 --- a/app/entreprises/app_relations_entreprises.py +++ b/app/entreprises/app_relations_entreprises.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/entreprises/forms.py b/app/entreprises/forms.py index 44e679f9..fa43c148 100644 --- a/app/entreprises/forms.py +++ b/app/entreprises/forms.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py index 946e6ff2..34e04d8e 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -63,6 +63,7 @@ class CodesDecisionsForm(FlaskForm): ABL = _build_code_field("ABL") ADC = _build_code_field("ADC") ADJ = _build_code_field("ADJ") + ADJR = _build_code_field("ADJR") ADM = _build_code_field("ADM") AJ = _build_code_field("AJ") ATB = _build_code_field("ATB") diff --git a/app/forms/main/config_logos.py b/app/forms/main/config_logos.py index b35ac34e..2a0051f0 100644 --- a/app/forms/main/config_logos.py +++ b/app/forms/main/config_logos.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index 27eb9329..205c88fa 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/forms/main/create_dept.py b/app/forms/main/create_dept.py index c0e18eff..1ab7f667 100644 --- a/app/forms/main/create_dept.py +++ b/app/forms/main/create_dept.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 80ef1321..53e127d8 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -4,8 +4,8 @@ from app import db from app.models import ModuleImpl, ModuleImplInscription from app.models.etudiants import Identite -from app.scodoc.sco_utils import EtatAssiduite, localize_datetime, verif_interval - +from app.scodoc.sco_utils import EtatAssiduite, localize_datetime, is_period_overlapping +from app.scodoc.sco_exceptions import ScoValueError from datetime import datetime @@ -14,6 +14,7 @@ class Assiduite(db.Model): Représente une assiduité: - une plage horaire lié à un état et un étudiant - un module si spécifiée + - une description si spécifiée """ __tablename__ = "assiduites" @@ -40,14 +41,17 @@ class Assiduite(db.Model): ) etat = db.Column(db.Integer, nullable=False) + desc = db.Column(db.Text) + def to_dict(self) -> dict: data = { - "assiduiteid": self.assiduiteid, + "assiduite_id": self.assiduite_id, "etudid": self.etudid, "moduleimpl_id": self.moduleimpl_id, "date_debut": self.date_debut, "date_fin": self.date_fin, "etat": self.etat, + "desc": self.desc, } return data @@ -58,15 +62,10 @@ class Assiduite(db.Model): date_debut: datetime, date_fin: datetime, etat: EtatAssiduite, - module: int - or None = None, # XEV est-ce un id (alors module_id ou modimpl_id), ou un objet (ModuleImpl ??) => cela simplifiera le check d'erreur + moduleimpl: ModuleImpl = None, + description: str = None, ) -> object or int: - """Créer une nouvelle assiduité pour l'étudiant - Documentation des codes d'erreurs renvoyés: - 1: Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée) - 2: l'ID du module_impl n'existe pas. - #XEV => utiliser plutôt des exceptions. - """ + """Créer une nouvelle assiduité pour l'étudiant""" # Vérification de non duplication des périodes assiduites: list[Assiduite] = etud.assiduites.all() @@ -75,67 +74,40 @@ class Assiduite(db.Model): assiduites = [ ass for ass in assiduites - if verif_interval( # XEV + if is_period_overlapping( (date_debut, date_fin), (ass.date_debut, ass.date_fin), ) ] if len(assiduites) != 0: - return 1 # XEV raise une exception + raise ScoValueError( + "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)" + ) - if module is not None: + if moduleimpl is not None: # Vérification de l'existence du module pour l'étudiant - if cls.verif_moduleimpl(module, etud): + if moduleimpl.est_inscrit(etud): nouv_assiduite = Assiduite( date_debut=date_debut, date_fin=date_fin, etat=etat, etudiant=etud, - moduleimpl_id=module, + moduleimpl_id=moduleimpl.id, + desc=description, ) else: - return 2 + raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl") else: nouv_assiduite = Assiduite( date_debut=date_debut, date_fin=date_fin, etat=etat, etudiant=etud, + desc=description, ) return nouv_assiduite - @staticmethod - def verif_moduleimpl(moduleimpl_id: int, etud: Identite or int) -> bool: - """ - Vérifie si l'étudiant est bien inscrit au moduleimpl - - Retourne Vrai si c'est le cas, faux sinon - """ - # XEV: cette méthode n'a pas de raison d'être dans la classe Assiduite - # et pourrait etre ModuleImpl.est_inscrit(etud) - # + éviter les "Identite or int" : cela complique les tests, mieux vaut avoir un type unique bien défini. - output = True - - # XEV: "module" est un "modimpl": changer nom sinon on pense que c'est un Module - module: ModuleImpl = ModuleImpl.query.filter_by( - moduleimpl_id=moduleimpl_id - ).first() - if module is None: - output = False - - if output: - search_etudid: int = etud.id if type(etud) == Identite else etud - - # XEV: is_xxx indique un booléen, or ici is_module est un comptage - is_module: int = ModuleImplInscription.query.filter_by( - etudid=search_etudid, moduleimpl_id=moduleimpl_id - ).count() - - output = is_module > 0 - - return output - class Justificatif(db.Model): """ @@ -147,7 +119,7 @@ class Justificatif(db.Model): __tablename__ = "justificatifs" - justifid = db.Column(db.Integer, primary_key=True) + justif_id = db.Column(db.Integer, primary_key=True) date_debut = db.Column( db.DateTime(timezone=True), server_default=db.func.now(), nullable=False @@ -167,12 +139,18 @@ class Justificatif(db.Model): ) raison = db.Column(db.Text()) - fichier = db.Column(db.Integer()) # XEV qu'est-ce que cet entier ? - # XEV pour les fichiers stockés, on va utiliser sco_archives.py + + """ + Les justificatifs sont enregistrés dans + /justificatifs/// + + d'après sco_archives.py#JustificatifArchiver + """ + fichier = db.Column(db.Text()) def to_dict(self) -> dict: data = { - "justifid": self.assiduiteid, + "justif_id": self.assiduite_id, "etudid": self.etudid, "date_debut": self.date_debut, "date_fin": self.date_fin, diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index c7330877..425ff192 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ScoDoc 9 models : Référentiel Compétence BUT 2021 @@ -14,7 +14,7 @@ import sqlalchemy from app import db from app.scodoc.sco_utils import ModuleType -from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_exceptions import ScoNoReferentielCompetences # from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns @@ -54,13 +54,15 @@ class ApcReferentielCompetences(db.Model, XMLModel): "Référentiel de compétence d'une spécialité" id = db.Column(db.Integer, primary_key=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) - annexe = db.Column(db.Text()) - specialite = db.Column(db.Text()) - specialite_long = db.Column(db.Text()) - type_titre = db.Column(db.Text()) - type_structure = db.Column(db.Text()) + annexe = db.Column(db.Text()) # '1', '22', ... + specialite = db.Column(db.Text()) # 'CJ', 'RT', 'INFO', ... + specialite_long = db.Column( + db.Text() + ) # 'Carrière Juridique', 'Réseaux et télécommunications', ... + type_titre = db.Column(db.Text()) # 'B.U.T.' + type_structure = db.Column(db.Text()) # 'type1', 'type2', ... type_departement = db.Column(db.Text()) # "secondaire", "tertiaire" - version_orebut = db.Column(db.Text()) + version_orebut = db.Column(db.Text()) # '2021-12-11 00:00:00' _xml_attribs = { # Orébut xml attrib : attribute "type": "type_titre", "version": "version_orebut", @@ -92,9 +94,10 @@ class ApcReferentielCompetences(db.Model, XMLModel): return "" return self.version_orebut.split()[0] - def to_dict(self): + def to_dict(self, parcours: list["ApcParcours"] = None, with_app_critiques=True): """Représentation complète du ref. de comp. comme un dict. + Si parcours est une liste de parcours, restreint l'export aux parcours listés. """ return { "dept_id": self.dept_id, @@ -109,8 +112,14 @@ class ApcReferentielCompetences(db.Model, XMLModel): if self.scodoc_date_loaded else "", "scodoc_orig_filename": self.scodoc_orig_filename, - "competences": {x.titre: x.to_dict() for x in self.competences}, - "parcours": {x.code: x.to_dict() for x in self.parcours}, + "competences": { + x.titre: x.to_dict(with_app_critiques=with_app_critiques) + for x in self.competences + }, + "parcours": { + x.code: x.to_dict() + for x in (self.parcours if parcours is None else parcours) + }, } def get_niveaux_by_parcours( @@ -172,6 +181,27 @@ class ApcReferentielCompetences(db.Model, XMLModel): niveaux_by_parcours_no_tc["TC"] = niveaux_tc return parcours, niveaux_by_parcours_no_tc + def get_competences_tronc_commun(self) -> list["ApcCompetence"]: + """Liste des compétences communes à tous les parcours du référentiel.""" + parcours = self.parcours.all() + if not parcours: + return [] + + ids = set.intersection( + *[ + {competence.id for competence in parcour.query_competences()} + for parcour in parcours + ] + ) + return sorted( + [ + competence + for competence in parcours[0].query_competences() + if competence.id in ids + ], + key=lambda c: c.numero or 0, + ) + class ApcCompetence(db.Model, XMLModel): "Compétence" @@ -213,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel): def __repr__(self): return f"" - def to_dict(self): + def to_dict(self, with_app_critiques=True): "repr dict recursive sur situations, composantes, niveaux" return { "id_orebut": self.id_orebut, @@ -225,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel): "composantes_essentielles": [ x.to_dict() for x in self.composantes_essentielles ], - "niveaux": {x.annee: x.to_dict() for x in self.niveaux}, + "niveaux": { + x.annee: x.to_dict(with_app_critiques=with_app_critiques) + for x in self.niveaux + }, } def to_dict_bul(self) -> dict: @@ -291,13 +324,15 @@ class ApcNiveau(db.Model, XMLModel): return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={ self.annee!r} {self.competence!r}>""" - def to_dict(self): - "as a dict, recursif sur les AC" + def to_dict(self, with_app_critiques=True): + "as a dict, recursif (ou non) sur les AC" return { "libelle": self.libelle, "annee": self.annee, "ordre": self.ordre, - "app_critiques": {x.code: x.to_dict() for x in self.app_critiques}, + "app_critiques": {x.code: x.to_dict() for x in self.app_critiques} + if with_app_critiques + else {}, } def to_dict_bul(self): @@ -322,9 +357,8 @@ class ApcNiveau(db.Model, XMLModel): if annee not in {1, 2, 3}: raise ValueError("annee invalide pour un parcours BUT") if referentiel_competence is None: - raise ScoValueError( - "Pas de référentiel de compétences associé à la formation !" - ) + raise ScoNoReferentielCompetences() + annee_formation = f"BUT{annee}" if parcour is None: return ApcNiveau.query.filter( @@ -470,6 +504,14 @@ class ApcParcours(db.Model, XMLModel): d["annees"] = {x.ordre: x.to_dict() for x in self.annees} return d + def query_competences(self) -> flask_sqlalchemy.BaseQuery: + "Les compétences associées à ce parcours" + return ( + ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours) + .filter_by(parcours_id=self.id) + .order_by(ApcCompetence.numero) + ) + class ApcAnneeParcours(db.Model, XMLModel): id = db.Column(db.Integer, primary_key=True) diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 298cb98b..ccef89cd 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -2,19 +2,17 @@ """Décisions de jury (validations) des RCUE et années du BUT """ - -import flask_sqlalchemy -from sqlalchemy.sql import text from typing import Union -from app import db +import flask_sqlalchemy +from app import db from app.models import CODE_STR_LEN from app.models.but_refcomp import ApcNiveau from app.models.etudiants import Identite -from app.models.ues import UniteEns from app.models.formations import Formation from app.models.formsemestre import FormSemestre +from app.models.ues import UniteEns from app.scodoc import sco_codes_parcours as sco_codes from app.scodoc import sco_utils as scu @@ -63,13 +61,32 @@ class ApcValidationRCUE(db.Model): self.ue1}/{self.ue2}:{self.code!r}>""" def __str__(self): - return f"""décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {self.code}""" + return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: { + self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}""" + + def to_html(self) -> str: + "description en HTML" + return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: + {self.code} + enregistrée le {self.date.strftime("%d/%m/%Y")} + à {self.date.strftime("%Hh%M")}""" + + def annee(self) -> str: + """l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """ + niveau = self.niveau() + return niveau.annee if niveau else None def niveau(self) -> ApcNiveau: """Le niveau de compétence associé à cet RCUE.""" # Par convention, il est donné par la seconde UE return self.ue2.niveau_competence + def to_dict(self): + "as a dict" + d = dict(self.__dict__) + d.pop("_sa_instance_state", None) + return d + def to_dict_bul(self) -> dict: "Export dict pour bulletins: le code et le niveau de compétence" niveau = self.niveau() @@ -96,10 +113,6 @@ class RegroupementCoherentUE: dec_ue_2: "DecisionsProposeesUE", inscription_etat: str, ): - from app.comp import res_sem - from app.comp.res_but import ResultatsSemestreBUT - - # from app.but.jury_but import DecisionsProposeesUE ue_1 = dec_ue_1.ue ue_2 = dec_ue_2.ue # Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)... @@ -144,6 +157,11 @@ class RegroupementCoherentUE: self.ue_1.acronyme}({self.moy_ue_1}) { self.ue_2.acronyme}({self.moy_ue_2})>""" + def __str__(self) -> str: + return f"""RCUE { + self.ue_1.acronyme}({self.moy_ue_1}) + { + self.ue_2.acronyme}({self.moy_ue_2})""" + def query_validations( self, ) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE] @@ -174,8 +192,9 @@ class RegroupementCoherentUE: return self.query_validations().count() > 0 def est_compensable(self): - """Vrai si ce RCUE est validable par compensation - c'est à dire que sa moyenne est > 10 avec une UE < 10 + """Vrai si ce RCUE est validable (uniquement) par compensation + c'est à dire que sa moyenne est > 10 avec une UE < 10. + Note: si ADM, est_compensable est faux. """ return ( (self.moy_rcue is not None) @@ -296,7 +315,8 @@ class ApcValidationAnnee(db.Model): formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees") def __repr__(self): - return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>" + return f"""<{self.__class__.__name__} {self.id} {self.etud + } BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>""" def __str__(self): return f"""décision sur année BUT{self.ordre} {self.annee_scolaire} : {self.code}""" @@ -333,7 +353,8 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""") else: titres_rcues.append( - f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {dec_rcue["code"]}""" + f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: { + dec_rcue["code"]}""" ) decisions["descr_decisions_rcue"] = ", ".join(titres_rcues) decisions["descr_decisions_niveaux"] = ( diff --git a/app/models/config.py b/app/models/config.py index 1b31eb12..6872c6cd 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -13,6 +13,7 @@ from app.scodoc.sco_codes_parcours import ( ABL, ADC, ADJ, + ADJR, ADM, AJ, ATB, @@ -34,6 +35,7 @@ CODES_SCODOC_TO_APO = { ABL: "ABL", ADC: "ADMC", ADJ: "ADM", + ADJR: "ADM", ADM: "ADM", AJ: "AJ", ATB: "AJAC", diff --git a/app/models/formations.py b/app/models/formations.py index 36e35647..986ef7e7 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -55,7 +55,8 @@ class Formation(db.Model): modules = db.relationship("Module", lazy="dynamic", backref="formation") def __repr__(self): - return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>" + return f"""<{self.__class__.__name__}(id={self.id}, dept_id={ + self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>""" def to_html(self) -> str: "titre complet pour affichage" diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index fb84fb02..bafad116 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -1,7 +1,7 @@ # -*- coding: UTF-8 -* ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -56,55 +56,58 @@ class FormSemestre(db.Model): dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1") - titre = db.Column(db.Text()) - date_debut = db.Column(db.Date()) - date_fin = db.Column(db.Date()) - etat = db.Column( - db.Boolean(), nullable=False, default=True, server_default="true" - ) # False si verrouillé + titre = db.Column(db.Text(), nullable=False) + date_debut = db.Column(db.Date(), nullable=False) + date_fin = db.Column(db.Date(), nullable=False) + etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true") + "False si verrouillé" modalite = db.Column( db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite") - ) # "FI", "FAP", "FC", ... - # gestion compensation sem DUT: + ) + "Modalité de formation: 'FI', 'FAP', 'FC', ..." gestion_compensation = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # ne publie pas le bulletin XML ou JSON: + "gestion compensation sem DUT (inutilisé en APC)" bul_hide_xml = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # Bloque le calcul des moyennes (générale et d'UE) + "ne publie pas le bulletin XML ou JSON" block_moyennes = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # Bloque le calcul de la moyenne générale (utile pour BUT) + "Bloque le calcul des moyennes (générale et d'UE)" block_moyenne_generale = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # semestres decales (pour gestion jurys): + "Si vrai, la moyenne générale indicative BUT n'est pas calculée" gestion_semestrielle = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # couleur fond bulletins HTML: + "Semestres décalés (pour gestion jurys DUT, pas implémenté ou utile en BUT)" bul_bgcolor = db.Column( - db.String(SHORT_STR_LEN), default="white", server_default="white" + db.String(SHORT_STR_LEN), + default="white", + server_default="white", + nullable=False, ) - # autorise resp. a modifier semestre: + "couleur fond bulletins HTML" resp_can_edit = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # autorise resp. a modifier slt les enseignants: + "autorise resp. à modifier le formsemestre" resp_can_change_ens = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) - # autorise les ens a creer des evals: + "autorise resp. a modifier slt les enseignants" ens_can_edit_eval = db.Column( db.Boolean(), nullable=False, default=False, server_default="False" ) - # code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...' + "autorise les enseignants à créer des évals dans leurs modimpls" elt_sem_apo = db.Column(db.Text()) # peut être fort long ! - # code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...' + "code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'" elt_annee_apo = db.Column(db.Text()) + "code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'" # Relations: etapes = db.relationship( @@ -114,6 +117,7 @@ class FormSemestre(db.Model): "ModuleImpl", backref="formsemestre", lazy="dynamic", + cascade="all, delete-orphan", ) etuds = db.relationship( "Identite", @@ -153,6 +157,11 @@ class FormSemestre(db.Model): def __repr__(self): return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>" + def sort_key(self) -> tuple: + """clé pour tris par ordre alphabétique + (pour avoir le plus récent d'abord, sort avec reverse=True)""" + return (self.date_debut, self.semestre_id) + def to_dict(self, convert_objects=False) -> dict: """dict (compatible ScoDoc7). If convert_objects, convert all attributes to native types @@ -321,7 +330,7 @@ class FormSemestre(db.Model): if self.formation.is_apc(): modimpls.sort( key=lambda m: ( - m.module.module_type or 0, + m.module.module_type or 0, # ressources (2) avant SAEs (3) m.module.numero or 0, m.module.code or 0, ) diff --git a/app/models/groups.py b/app/models/groups.py index 92e8cc5c..6fafa234 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -1,7 +1,7 @@ # -*- coding: UTF-8 -* ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## @@ -87,6 +87,7 @@ class Partition(db.Model): def to_dict(self, with_groups=False) -> dict: """as a dict, with or without groups""" d = dict(self.__dict__) + d["partition_id"] = self.id d.pop("_sa_instance_state", None) d.pop("formsemestre", None) diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 5b81fa1b..88c3d0fb 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -20,14 +20,12 @@ class ModuleImpl(db.Model): id = db.Column(db.Integer, primary_key=True) moduleimpl_id = db.synonym("id") - module_id = db.Column( - db.Integer, - db.ForeignKey("notes_modules.id"), - ) + module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False) formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, + nullable=False, ) responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id")) # formule de calcul moyenne: @@ -101,6 +99,22 @@ class ModuleImpl(db.Model): d.pop("module", None) return d + def est_inscrit(self, etud: Identite) -> bool: + """ + Vérifie si l'étudiant est bien inscrit au moduleimpl + + Retourne Vrai si c'est le cas, faux sinon + """ + + is_module: int = ( + ModuleImplInscription.query.filter_by( + etudid=etud.id, moduleimpl_id=self.id + ).count() + > 0 + ) + + return is_module + # Enseignants (chargés de TD ou TP) d'un moduleimpl notes_modules_enseignants = db.Table( diff --git a/app/models/modules.py b/app/models/modules.py index 77c06274..d4c70c03 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -37,7 +37,9 @@ class Module(db.Model): # Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum) module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0") # Relations: - modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic") + modimpls = db.relationship( + "ModuleImpl", backref="module", lazy="dynamic", cascade="all, delete-orphan" + ) ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True) tags = db.relationship( "NotesTag", diff --git a/app/models/ues.py b/app/models/ues.py index b56f209c..596e0bef 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -1,6 +1,8 @@ """ScoDoc 9 models : Unités d'Enseignement (UE) """ +import pandas as pd + from app import db, log from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN @@ -109,6 +111,7 @@ class UniteEns(db.Model): e["ects"] = e["ects"] e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0 e["code_apogee"] = e["code_apogee"] or "" # pas de None + e["parcour"] = self.parcour.to_dict() if self.parcour else None if with_module_ue_coefs: if convert_objects: e["module_ue_coefs"] = [ @@ -217,6 +220,8 @@ class UniteEns(db.Model): db.session.add(self) db.session.commit() + # Invalidation du cache + self.formation.invalidate_cached_sems() log(f"ue.set_niveau_competence( {self}, {niveau} )") def set_parcour(self, parcour: ApcParcours): @@ -244,17 +249,30 @@ class UniteEns(db.Model): self.niveau_competence = None db.session.add(self) db.session.commit() + # Invalidation du cache + self.formation.invalidate_cached_sems() log(f"ue.set_parcour( {self}, {parcour} )") class DispenseUE(db.Model): """Dispense d'UE - Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée + Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée qu'ils ne refont pas. + La dispense d'UE n'est PAS une validation: + - elle n'est pas affectée par les décisions de jury (pas effacée) + - elle est associée à un formsemestre + - elle ne permet pas la délivrance d'ECTS ou du diplôme. + + On utilise cette dispense et non une "inscription" par souci d'efficacité: + en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours, + la dispense étant une exception. """ - __table_args__ = (db.UniqueConstraint("ue_id", "etudid"),) + __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),) id = db.Column(db.Integer, primary_key=True) + formsemestre_id = formsemestre_id = db.Column( + db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True + ) ue_id = db.Column( db.Integer, db.ForeignKey(UniteEns.id, ondelete="CASCADE"), @@ -273,3 +291,25 @@ class DispenseUE(db.Model): def __repr__(self) -> str: return f"""<{self.__class__.__name__} {self.id} etud={ repr(self.etud)} ue={repr(self.ue)}>""" + + @classmethod + def load_formsemestre_dispense_ues_set( + cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns] + ) -> set[tuple[int, int]]: + """Construit l'ensemble des + etudids = modimpl_inscr_df.index, # les etudids + ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport + + Résultat: set de (etudid, ue_id). + """ + # Prend toutes les dispenses obtenues par des étudiants de ce formsemestre, + # puis filtre sur inscrits et ues + ue_ids = {ue.id for ue in ues} + dispense_ues = { + (dispense_ue.etudid, dispense_ue.ue_id) + for dispense_ue in DispenseUE.query.filter_by( + formsemestre_id=formsemestre.id + ) + if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids + } + return dispense_ues diff --git a/app/models/validations.py b/app/models/validations.py index 2781dfe0..f0ec9749 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -4,6 +4,7 @@ """ from app import db +from app import log from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN from app.models.events import Scolog @@ -58,7 +59,7 @@ class ScolarFormSemestreValidation(db.Model): ) def __repr__(self): - return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})" + return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})" def __str__(self): if self.ue_id: @@ -93,6 +94,10 @@ class ScolarAutorisationInscription(db.Model): db.ForeignKey("notes_formsemestre.id"), ) + def __repr__(self) -> str: + return f"""{self.__class__.__name__}(id={self.id}, etudid={ + self.etudid}, semestre_id={self.semestre_id})""" + def to_dict(self) -> dict: "as a dict" d = dict(self.__dict__) @@ -116,7 +121,10 @@ class ScolarAutorisationInscription(db.Model): semestre_id=semestre_id, ) db.session.add(autorisation) - Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}") + Scolog.logdb( + "autorise_etud", etudid=etudid, msg=f"Passage vers S{semestre_id}: autorisé" + ) + log(f"ScolarAutorisationInscription: recording {autorisation}") @classmethod def delete_autorisation_etud( @@ -130,10 +138,11 @@ class ScolarAutorisationInscription(db.Model): ) for autorisation in autorisations: db.session.delete(autorisation) + log(f"ScolarAutorisationInscription: deleting {autorisation}") Scolog.logdb( "autorise_etud", etudid=etudid, - msg=f"annule passage vers S{autorisation.semestre_id}", + msg=f"Passage vers S{autorisation.semestre_id}: effacé", ) db.session.flush() diff --git a/app/pe/pe_avislatex.py b/app/pe/pe_avislatex.py index 5a507738..d68f9509 100644 --- a/app/pe/pe_avislatex.py +++ b/app/pe/pe_avislatex.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_jurype.py b/app/pe/pe_jurype.py index e4541f07..50202752 100644 --- a/app/pe/pe_jurype.py +++ b/app/pe/pe_jurype.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -459,8 +459,7 @@ class JuryPE(object): etud = self.get_cache_etudInfo_d_un_etudiant(etudid) (_, parcours) = sco_report.get_codeparcoursetud(etud) if ( - len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values())) - > 0 + len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0 ): # Eliminé car NAR apparait dans le parcours reponse = True if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: @@ -563,9 +562,8 @@ class JuryPE(object): dec = nt.get_etud_decision_sem( etudid ) # quelle est la décision du jury ? - if dec and dec["code"] in list( - sco_codes_parcours.CODES_SEM_VALIDES.keys() - ): # isinstance( sesMoyennes[i+1], float) and + if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES): + # isinstance( sesMoyennes[i+1], float) and # mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide" leFid = sem["formsemestre_id"] else: diff --git a/app/pe/pe_semestretag.py b/app/pe/pe_semestretag.py index bed33465..eb6b7c46 100644 --- a/app/pe/pe_semestretag.py +++ b/app/pe/pe_semestretag.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_settag.py b/app/pe/pe_settag.py index be5028f3..792175bb 100644 --- a/app/pe/pe_settag.py +++ b/app/pe/pe_settag.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_tagtable.py b/app/pe/pe_tagtable.py index 26cc8e24..e6ddb19c 100644 --- a/app/pe/pe_tagtable.py +++ b/app/pe/pe_tagtable.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index ad6e2aa2..932a2a00 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 06302cd8..bb5f386f 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/__init__.py b/app/scodoc/__init__.py index 54c845c8..1b9a5925 100644 --- a/app/scodoc/__init__.py +++ b/app/scodoc/__init__.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 0fab06ea..d1553238 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 409e4d13..7098757c 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index 98acc56c..a487fcf0 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/htmlutils.py b/app/scodoc/htmlutils.py index 725a4f4e..51a0b19b 100644 --- a/app/scodoc/htmlutils.py +++ b/app/scodoc/htmlutils.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index a8322f16..a54a7bda 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/notes_users.py b/app/scodoc/notes_users.py index 6dd5bf40..33d25a68 100644 --- a/app/scodoc/notes_users.py +++ b/app/scodoc/notes_users.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/safehtml.py b/app/scodoc/safehtml.py index 2988a0f3..5c897cd2 100644 --- a/app/scodoc/safehtml.py +++ b/app/scodoc/safehtml.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index 3cdd17e2..1e56ca87 100644 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_abs_billets.py b/app/scodoc/sco_abs_billets.py index d281b312..5f52e8f7 100644 --- a/app/scodoc/sco_abs_billets.py +++ b/app/scodoc/sco_abs_billets.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index 5f9670f5..fe973a3f 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index e24f25f4..bb585930 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_apogee_compare.py b/app/scodoc/sco_apogee_compare.py index b6cb042b..6c967604 100644 --- a/app/scodoc/sco_apogee_compare.py +++ b/app/scodoc/sco_apogee_compare.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 154846bf..212bb133 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 71acd815..71ff5c46 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -28,7 +28,7 @@ """ScoDoc : gestion des archives des PV et bulletins, et des dossiers etudiants (admission) - Archives are plain files, stored in + Archives are plain files, stored in /archives/ (where is usually /opt/scodoc-data, and a departement id (int)) @@ -42,7 +42,7 @@ Les maquettes Apogée pour l'export des notes sont dans /apo_csv//-//.csv - + Un répertoire d'archive contient des fichiers quelconques, et un fichier texte nommé _description.txt qui est une description (humaine, format libre) de l'archive. @@ -105,13 +105,13 @@ class BaseArchiver(object): try: scu.GSL.acquire() if not os.path.isdir(path): - log("creating directory %s" % path) + log(f"creating directory {path}") os.mkdir(path) finally: scu.GSL.release() self.initialized = True - def get_obj_dir(self, oid): + def get_obj_dir(self, oid: int): """ :return: path to directory of archives for this object (eg formsemestre_id or etudid). If directory does not yet exist, create it. @@ -142,7 +142,7 @@ class BaseArchiver(object): dirs = glob.glob(base + "*") return [os.path.split(x)[1] for x in dirs] - def list_obj_archives(self, oid): + def list_obj_archives(self, oid: int): """Returns :return: list of archive identifiers for this object (paths to non empty dirs) """ @@ -157,7 +157,7 @@ class BaseArchiver(object): dirs.sort() return dirs - def delete_archive(self, archive_id): + def delete_archive(self, archive_id: str): """Delete (forever) this archive""" self.initialize() try: @@ -166,7 +166,7 @@ class BaseArchiver(object): finally: scu.GSL.release() - def get_archive_date(self, archive_id): + def get_archive_date(self, archive_id: str): """Returns date (as a DateTime object) of an archive""" return datetime.datetime( *[int(x) for x in os.path.split(archive_id)[1].split("-")] @@ -183,17 +183,17 @@ class BaseArchiver(object): files.sort() return [f for f in files if f and f[0] != "_"] - def get_archive_name(self, archive_id): + def get_archive_name(self, archive_id: str): """name identifying archive, to be used in web URLs""" return os.path.split(archive_id)[1] - def is_valid_archive_name(self, archive_name): + def is_valid_archive_name(self, archive_name: str): """check if name is valid.""" return re.match( "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name ) - def get_id_from_name(self, oid, archive_name): + def get_id_from_name(self, oid, archive_name: str): """returns archive id (check that name is valid)""" self.initialize() if not self.is_valid_archive_name(archive_name): @@ -206,7 +206,7 @@ class BaseArchiver(object): raise ScoValueError(f"Archive {archive_name} introuvable") return archive_id - def get_archive_description(self, archive_id): + def get_archive_description(self, archive_id: str) -> str: """Return description of archive""" self.initialize() filename = os.path.join(archive_id, "_description.txt") @@ -247,7 +247,7 @@ class BaseArchiver(object): data = data.encode(scu.SCO_ENCODING) self.initialize() filename = scu.sanitize_filename(filename) - log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id)) + log(f"storing {filename} ({len(data)} bytes) in {archive_id}") try: scu.GSL.acquire() fname = os.path.join(archive_id, filename) @@ -261,16 +261,18 @@ class BaseArchiver(object): """Retreive data""" self.initialize() if not scu.is_valid_filename(filename): - log('Archiver.get: invalid filename "%s"' % filename) + log(f"""Archiver.get: invalid filename '{filename}'""") raise ScoValueError("archive introuvable (déjà supprimée ?)") fname = os.path.join(archive_id, filename) - log("reading archive file %s" % fname) + log(f"reading archive file {fname}") with open(fname, "rb") as f: data = f.read() return data def get_archived_file(self, oid, archive_name, filename): - """Recupere donnees du fichier indiqué et envoie au client""" + """Recupère les donnees du fichier indiqué et envoie au client. + Returns: Response + """ archive_id = self.get_id_from_name(oid, archive_name) data = self.get(archive_id, filename) mime = mimetypes.guess_type(filename)[0] diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index c0a40b14..799024db 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bac.py b/app/scodoc/sco_bac.py index 6f316a10..7933f351 100644 --- a/app/scodoc/sco_bac.py +++ b/app/scodoc/sco_bac.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 410b3972..e47177d6 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -1084,7 +1084,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): recipients = [recipient_addr] sender = sco_preferences.get_preference("email_from_addr", formsemestre_id) if copy_addr: - bcc = copy_addr.strip() + bcc = copy_addr.strip().split(",") else: bcc = "" @@ -1094,7 +1094,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): subject, sender, recipients, - bcc=[bcc], + bcc=bcc, text_body=hea, attachments=[ {"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata} diff --git a/app/scodoc/sco_bulletins_example.py b/app/scodoc/sco_bulletins_example.py index cf908a1b..83ad85c3 100644 --- a/app/scodoc/sco_bulletins_example.py +++ b/app/scodoc/sco_bulletins_example.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index 4f40a53e..0d5ad046 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 675737c6..2d56eeb0 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bulletins_legacy.py b/app/scodoc/sco_bulletins_legacy.py index 314abb0d..37a1b202 100644 --- a/app/scodoc/sco_bulletins_legacy.py +++ b/app/scodoc/sco_bulletins_legacy.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index f063f3a7..369187ca 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bulletins_signature.py b/app/scodoc/sco_bulletins_signature.py index be93e672..f3a711b5 100644 --- a/app/scodoc/sco_bulletins_signature.py +++ b/app/scodoc/sco_bulletins_signature.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index b326ef7b..cd535be0 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -435,7 +435,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): plusminus = pluslink try: ects_txt = str(int(ue["ects"])) - except (ValueError, KeyError): + except (ValueError, KeyError, TypeError): ects_txt = "-" t = { diff --git a/app/scodoc/sco_bulletins_ucac.py b/app/scodoc/sco_bulletins_ucac.py index fc49ffd9..c5a59243 100644 --- a/app/scodoc/sco_bulletins_ucac.py +++ b/app/scodoc/sco_bulletins_ucac.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index 2f3c2c6b..c67b8ce0 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 8cdeed43..f83cc0ec 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index a19ab4f4..df5484fb 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -122,6 +122,7 @@ ABL = "ABL" ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10) ADJ = "ADJ" # admis par le jury +ADJR = "ADJR" # UE admise car son RCUE est ADJ ATT = "ATT" # ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant ATB = "ATB" @@ -158,6 +159,7 @@ CODES_EXPL = { ABL: "Année blanche", ADC: "Validé par compensation", ADJ: "Validé par le Jury", + ADJR: "UE validée car son RCUE est validé ADJ par le jury", ADM: "Validé", AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)", ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)", @@ -185,16 +187,23 @@ CODES_EXPL = { # Les codes de semestres: CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} -CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé -CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente +CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC} +CODES_SEM_VALIDES = CODES_SEM_VALIDES_DE_DROIT | {ADJ} # semestre validé +CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente -CODES_SEM_REO = {NAR: 1} # reorientation +CODES_SEM_REO = {NAR} # reorientation + +CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit" +CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR} +"UE validée" + +CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP} +CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ} +"Niveau RCUE validé" -CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée -CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé # Pour le BUT: +CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} -CODES_RCUE = {ADM, AJ, CMP} BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE @@ -205,21 +214,36 @@ BUT_CODES_PASSAGE = { PAS1NCI, ATJ, } +# les codes, du plus "défavorable" à l'étudiant au plus favorable: +# (valeur par défaut 0) +BUT_CODES_ORDERED = { + NAR: 0, + DEF: 0, + AJ: 10, + ATJ: 20, + CMP: 50, + ADC: 50, + PASD: 50, + PAS1NCI: 60, + ADJR: 90, + ADJ: 100, + ADM: 100, +} def code_semestre_validant(code: str) -> bool: "Vrai si ce CODE entraine la validation du semestre" - return CODES_SEM_VALIDES.get(code, False) + return code in CODES_SEM_VALIDES def code_semestre_attente(code: str) -> bool: "Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)" - return CODES_SEM_ATTENTES.get(code, False) + return code in CODES_SEM_ATTENTES def code_ue_validant(code: str) -> bool: "Vrai si ce code d'UE est validant (ie attribue les ECTS)" - return CODES_UE_VALIDES.get(code, False) + return code in CODES_UE_VALIDES DEVENIR_EXPL = { diff --git a/app/scodoc/sco_compute_moy.py b/app/scodoc/sco_compute_moy.py index b35ca6d4..fb162170 100644 --- a/app/scodoc/sco_compute_moy.py +++ b/app/scodoc/sco_compute_moy.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_config_actions.py b/app/scodoc/sco_config_actions.py index 887b6608..61202781 100644 --- a/app/scodoc/sco_config_actions.py +++ b/app/scodoc/sco_config_actions.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_cost_formation.py b/app/scodoc/sco_cost_formation.py index 167ff770..09d1e122 100644 --- a/app/scodoc/sco_cost_formation.py +++ b/app/scodoc/sco_cost_formation.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_cursus.py b/app/scodoc/sco_cursus.py index c492f86b..d4d996de 100644 --- a/app/scodoc/sco_cursus.py +++ b/app/scodoc/sco_cursus.py @@ -4,7 +4,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_cursus_dut.py b/app/scodoc/sco_cursus_dut.py index 32d4796b..0549f667 100644 --- a/app/scodoc/sco_cursus_dut.py +++ b/app/scodoc/sco_cursus_dut.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -890,7 +890,7 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite) car ils ne dépendent que de la note d'UE et de la validation ou non du semestre. Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ). """ - valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False) + valid_semestre = code_etat_sem in CODES_SEM_VALIDES cnx = ndb.GetDBConnexion() formsemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) diff --git a/app/scodoc/sco_debouche.py b/app/scodoc/sco_debouche.py index ea96ff1c..3f83ba75 100644 --- a/app/scodoc/sco_debouche.py +++ b/app/scodoc/sco_debouche.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py index e31752a0..6fdf92e4 100644 --- a/app/scodoc/sco_dept.py +++ b/app/scodoc/sco_dept.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_dump_db.py b/app/scodoc/sco_dump_db.py index e1d8eef3..80a0b9c4 100644 --- a/app/scodoc/sco_dump_db.py +++ b/app/scodoc/sco_dump_db.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index 255448ff..b904bd5b 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -2,7 +2,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -99,7 +99,7 @@ def html_edit_formation_apc( H = [ render_template( - "pn/form_ues.html", + "pn/form_ues.j2", formation=formation, semestre_ids=semestre_ids, editable=editable, @@ -122,7 +122,7 @@ def html_edit_formation_apc( ).first() H += [ render_template( - "pn/form_mods.html", + "pn/form_mods.j2", formation=formation, titre=f"Ressources du S{semestre_idx}", create_element_msg="créer une nouvelle ressource", @@ -138,7 +138,7 @@ def html_edit_formation_apc( if ues_by_sem[semestre_idx].count() > 0 else "", render_template( - "pn/form_mods.html", + "pn/form_mods.j2", formation=formation, titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}", create_element_msg="créer une nouvelle SAÉ", @@ -154,7 +154,7 @@ def html_edit_formation_apc( if ues_by_sem[semestre_idx].count() > 0 else "", render_template( - "pn/form_mods.html", + "pn/form_mods.j2", formation=formation, titre=f"Autres modules (non BUT) du S{semestre_idx}", create_element_msg="créer un nouveau module", @@ -196,7 +196,7 @@ def html_ue_infos(ue): and ue.matieres.count() == 0 ) return render_template( - "pn/ue_infos.html", + "pn/ue_infos.j2", titre=f"UE {ue.acronyme} {ue.titre}", ue=ue, formsemestres=formsemestres, diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index 4077f633..c64d732e 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_edit_matiere.py b/app/scodoc/sco_edit_matiere.py index a2298fc2..f0f2ce5b 100644 --- a/app/scodoc/sco_edit_matiere.py +++ b/app/scodoc/sco_edit_matiere.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 91194ebf..c8e6d0fd 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -697,8 +697,8 @@ def module_edit( if ue is not None: annee = f"BUT{(orig_semestre_idx+1)//2}" app_critiques = ApcAppCritique.app_critiques_ref_comp(ref_comp, annee) - descr += ( - [ + if ue.niveau_competence is not None: + descr += [ ( "app_critiques", { @@ -715,28 +715,28 @@ def module_edit( ], "html_data": [], "explanation": """Apprentissages Critiques liés à ce module. - (si vous changez le semestre, revenez ensuite sur cette page - pour associer les AC.) - """, + (si vous changez le semestre, revenez ensuite sur cette page + pour associer les AC.) + """, }, ) ] - if (ue.niveau_competence is not None) - else [ - ( - "app_critiques", - { - "input_type": "separator", - "title": f"""{scu.EMO_WARNING } - L'UE {ue.acronyme} {ue.titre} - n'est pas associée à un niveau de compétences - """, - }, - ) - ] - ) + else: + if module.ue.type == sco_codes_parcours.UE_STANDARD: + descr += [ + ( + "app_critiques", + { + "input_type": "separator", + "title": f"""{scu.EMO_WARNING } + L'UE {ue.acronyme} {ue.titre} + n'est pas associée à un niveau de compétences + """, + }, + ) + ] else: descr += [ ( @@ -840,6 +840,8 @@ def module_edit( if selected_ue is None: raise ValueError("UE invalide") tf[2]["semestre_id"] = selected_ue.semestre_idx + if not tf[2].get("code"): + raise ScoValueError("Le code du module doit être spécifié.") # Check unicité code module dans la formation # ??? TODO # diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 1ce7c347..906493a2 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -39,6 +39,7 @@ from app.but import apc_edit_ue from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import ( Formation, + FormSemestre, FormSemestreUEComputationExpr, FormSemestreUECoef, Matiere, @@ -722,7 +723,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list "libjs/jQuery-tagEditor/jquery.caret.min.js", "js/module_tag_editor.js", ], - page_title=f"Programme {formation.acronyme}", + page_title=f"Programme {formation.acronyme} v{formation.version}", ), f"""

{formation.to_html()} {lockicon}

@@ -764,7 +765,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); # Description de la formation H.append( render_template( - "pn/form_descr.html", + "pn/form_descr.j2", formation=formation, parcours=parcours, editable=editable, @@ -912,8 +913,12 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
  • Export XML de la formation - (permet de la sauvegarder pour l'échanger avec un autre site) + }">Export XML de la formation ou + sans codes Apogée + (permet de l'enregistrer pour l'échanger avec un autre site)
  • Liste détaillée des modules de la formation (debug) + }">Liste détaillée des modules de la formation (debug)
  • """ @@ -936,19 +941,24 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);

    Semestres ou sessions de cette formation

      """ ) - for sem in sco_formsemestre.do_formsemestre_list( - args={"formation_id": formation_id} + for formsemestre in sorted( + FormSemestre.query.filter_by(formation_id=formation_id).all(), + key=lambda s: s.sort_key(), + reverse=True, ): H.append( - '
    • %(titremois)s' - % sem + f"""
    • {formsemestre.titre_mois()}""" ) - if not sem["etat"]: + if not formsemestre.etat: H.append(" [verrouillé]") else: H.append( - ' Modifier' - % sem + f"""  Modifier""" ) H.append("
    • ") H.append("
    ") diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index 39c9541d..06672f95 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_etape_apogee.py b/app/scodoc/sco_etape_apogee.py index 8a14f561..2be38c6d 100644 --- a/app/scodoc/sco_etape_apogee.py +++ b/app/scodoc/sco_etape_apogee.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py index 36322c5c..e37b0fd8 100644 --- a/app/scodoc/sco_etape_apogee_view.py +++ b/app/scodoc/sco_etape_apogee_view.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py index 2c945a7a..a3839fa7 100644 --- a/app/scodoc/sco_etape_bilan.py +++ b/app/scodoc/sco_etape_bilan.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index d4edc89c..6d9bc49f 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py index 21e10f53..d34137bc 100644 --- a/app/scodoc/sco_evaluation_check_abs.py +++ b/app/scodoc/sco_evaluation_check_abs.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index d3e27dae..b99af2cc 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -25,7 +25,7 @@ # ############################################################################## -"""Gestion evaluations (ScoDoc7, sans SQlAlchemy) +"""Gestion évaluations (ScoDoc7, code en voie de modernisation) """ import pprint @@ -34,16 +34,15 @@ import flask from flask import url_for, g from flask_login import current_user -from app import log +from app import db, log -from app.models import ModuleImpl, ScolarNews +from app.models import Evaluation, ModuleImpl, ScolarNews from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc import sco_cache -from app.scodoc import sco_edit_module from app.scodoc import sco_moduleimpl from app.scodoc import sco_permissions_check @@ -135,7 +134,7 @@ def do_evaluation_create( raise ValueError("module not found") check_evaluation_args(args) # Check numeros - module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) + moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) if not "numero" in args or args["numero"] is None: n = None # determine le numero avec la date @@ -158,7 +157,7 @@ def do_evaluation_create( next_eval = e break if next_eval: - n = module_evaluation_insert_before(mod_evals, next_eval) + n = moduleimpl_evaluation_insert_before(mod_evals, next_eval) else: n = None # a placer en fin if n is None: # pas de date ou en fin: @@ -215,13 +214,11 @@ def do_evaluation_edit(args): def do_evaluation_delete(evaluation_id): "delete evaluation" - the_evals = do_evaluation_list({"evaluation_id": evaluation_id}) - if not the_evals: - raise ValueError("evaluation inexistante !") - moduleimpl_id = the_evals[0]["moduleimpl_id"] - if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): + evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) + modimpl: ModuleImpl = evaluation.moduleimpl + if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=modimpl.id): raise AccessDenied( - "Modification évaluation impossible pour %s" % current_user.get_nomplogin() + f"Modification évaluation impossible pour {current_user.get_nomplogin()}" ) notes_db = do_evaluation_get_all_notes(evaluation_id) # { etudid : value } notes = [x["value"] for x in notes_db.values()] @@ -230,24 +227,24 @@ def do_evaluation_delete(evaluation_id): "Impossible de supprimer cette évaluation: il reste des notes" ) - cnx = ndb.GetDBConnexion() + db.session.delete(evaluation) + db.session.commit() - _evaluationEditor.delete(cnx, evaluation_id) # inval cache pour ce semestre - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) + sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) # news - - mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - mod["moduleimpl_id"] = M["moduleimpl_id"] - mod["url"] = ( - scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod + url = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, ) ScolarNews.add( typ=ScolarNews.NEWS_NOTE, - obj=moduleimpl_id, - text='Suppression d\'une évaluation dans %(titre)s' % mod, - url=mod["url"], + obj=modimpl.id, + text=f"""Suppression d'une évaluation dans {modimpl.module.titre}""", + url=url, ) @@ -263,7 +260,7 @@ def do_evaluation_get_all_notes( ) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant if do_cache: r = sco_cache.EvaluationCache.get(evaluation_id) - if r != None: + if r is not None: return r cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) @@ -291,13 +288,13 @@ def do_evaluation_get_all_notes( return d -def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0): +def moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=0): """Renumber evaluations in this module, according to their date. (numero=0: oldest one) Needed because previous versions of ScoDoc did not have eval numeros Note: existing numeros are ignored """ redirect = int(redirect) - # log('module_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id ) + # log('moduleimpl_evaluation_renumber( moduleimpl_id=%s )' % moduleimpl_id ) # List sorted according to date/heure, ignoring numeros: # (note that we place evaluations with NULL date at the end) mod_evals = do_evaluation_list( @@ -327,7 +324,7 @@ def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect= ) -def module_evaluation_insert_before(mod_evals, next_eval): +def moduleimpl_evaluation_insert_before(mod_evals, next_eval): """Renumber evals such that an evaluation with can be inserted before next_eval Returns numero suitable for the inserted evaluation """ @@ -335,7 +332,7 @@ def module_evaluation_insert_before(mod_evals, next_eval): n = next_eval["numero"] if not n: log("renumbering old evals") - module_evaluation_renumber(next_eval["moduleimpl_id"]) + moduleimpl_evaluation_renumber(next_eval["moduleimpl_id"]) next_eval = do_evaluation_list( args={"evaluation_id": next_eval["evaluation_id"]} )[0] @@ -353,19 +350,20 @@ def module_evaluation_insert_before(mod_evals, next_eval): return n -def module_evaluation_move(evaluation_id, after=0, redirect=1): +def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1): """Move before/after previous one (decrement/increment numero) (published) """ - e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] + evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) + moduleimpl_id = evaluation.moduleimpl_id redirect = int(redirect) # access: can change eval ? - if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=e["moduleimpl_id"]): + if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): raise AccessDenied( - "Modification évaluation impossible pour %s" % current_user.get_nomplogin() + f"Modification évaluation impossible pour {current_user.get_nomplogin()}" ) - module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=True) + moduleimpl_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) e = do_evaluation_list(args={"evaluation_id": evaluation_id})[0] after = int(after) # 0: deplace avant, 1 deplace apres @@ -381,8 +379,10 @@ def module_evaluation_move(evaluation_id, after=0, redirect=1): neigh = mod_evals[idx + 1] if neigh: # if neigh["numero"] == e["numero"]: - log("Warning: module_evaluation_move: forcing renumber") - module_evaluation_renumber(e["moduleimpl_id"], only_if_unumbered=False) + log("Warning: moduleimpl_evaluation_move: forcing renumber") + moduleimpl_evaluation_renumber( + e["moduleimpl_id"], only_if_unumbered=False + ) else: # swap numero with neighbor e["numero"], neigh["numero"] = neigh["numero"], e["numero"] diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index 268fde76..900827d4 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -132,7 +132,7 @@ def evaluation_create_form( min_note_max_str = "0" # H = [ - f"""

    {action} en + f"""

    {action} en {scu.MODULE_TYPE_NAMES[mod["module_type"]]} les poids ne sont pas modifiables (voir réglage paramétrage) +

    """, + }, + ) + ) for ue in sem_ues: coef_ue = ue_coef_dict.get(ue.id, 0.0) form.append( diff --git a/app/scodoc/sco_evaluation_recap.py b/app/scodoc/sco_evaluation_recap.py index 7f1120b7..635d6e8d 100644 --- a/app/scodoc/sco_evaluation_recap.py +++ b/app/scodoc/sco_evaluation_recap.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 09bf2102..799a0e03 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 1ea4cee8..dc875ca9 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 856c963f..7896336f 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -40,8 +40,9 @@ class InvalidNoteValue(ScoException): pass -# Exception qui stoque dest_url class ScoValueError(ScoException): + "Exception avec page d'erreur utilisateur, et qui stoque dest_url" + def __init__(self, msg, dest_url=None): super().__init__(msg) self.dest_url = dest_url @@ -74,7 +75,7 @@ class ScoFormatError(ScoValueError): class ScoInvalidParamError(ScoValueError): """Paramètres requete invalides. - A utilisée lorsqu'une route est appelée avec des paramètres invalides + Utilisée lorsqu'une route est appelée avec des paramètres invalides (id strings, ...) """ @@ -157,6 +158,23 @@ class ScoInvalidIdType(ScoValueError): super().__init__(msg) +class ScoNoReferentielCompetences(ScoValueError): + """Formation APC (BUT) non associée à référentiel de compétences""" + + def __init__(self, msg: str = "", formation: "Formation" = None): + formation_title = ( + f"{formation.titre} version {formation.version}" if formation else "" + ) + msg = f""" +

    Pas de référentiel de compétences associé à la formation {formation_title}! +

    +

    Pour associer un référentiel, passer par le menu Semestre / + Voir la formation... et suivre le lien "associer à un référentiel + de compétences" + """ + super().__init__(msg) + + class ScoGenError(ScoException): "exception avec affichage d'une page explicative ad-hoc" diff --git a/app/scodoc/sco_export_results.py b/app/scodoc/sco_export_results.py index 6c005306..858fc5ae 100644 --- a/app/scodoc/sco_export_results.py +++ b/app/scodoc/sco_export_results.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py index 163daf74..d3fa462e 100644 --- a/app/scodoc/sco_find_etud.py +++ b/app/scodoc/sco_find_etud.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_formation_recap.py b/app/scodoc/sco_formation_recap.py index 9d06dbad..e9ae85a3 100644 --- a/app/scodoc/sco_formation_recap.py +++ b/app/scodoc/sco_formation_recap.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 150f2af3..b28590eb 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -109,6 +109,7 @@ def formation_export( export_ids=False, export_tags=True, export_external_ues=False, + export_codes_apo=True, format=None, ): """Get a formation, with UE, matieres, modules @@ -116,30 +117,45 @@ def formation_export( """ formation: Formation = Formation.query.get_or_404(formation_id) f_dict = formation.to_dict(with_refcomp_attrs=True) - selector = {"formation_id": formation_id} + if not export_ids: + del f_dict["formation_id"] + del f_dict["dept_id"] + ues = formation.ues if not export_external_ues: - selector["is_external"] = False - ues = sco_edit_ue.ue_list(selector) - f_dict["ue"] = ues - for ue_dict in ues: - ue_id = ue_dict["ue_id"] + ues = ues.filter_by(is_external=False) + ues = ues.all() + ues.sort(key=lambda u: (u.semestre_idx or 0, u.numero or 0, u.acronyme)) + f_dict["ue"] = [] + for ue in ues: + ue_dict = ue.to_dict() + f_dict["ue"].append(ue_dict) + ue_dict.pop("module_ue_coefs", None) if formation.is_apc(): # BUT: indique niveau de compétence associé à l'UE - ue = UniteEns.query.get(ue_id) if ue.niveau_competence: ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre - ue_dict["reference"] = ue_id # pour les coefficients + # Et le parcour: + if ue.parcour: + ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)] + ue_dict["reference"] = ue.id # pour les coefficients if not export_ids: - del ue_dict["id"] - del ue_dict["ue_id"] - del ue_dict["formation_id"] - if "niveau_competence_id" in ue_dict: - del ue_dict["niveau_competence_id"] + for id_id in ( + "id", + "ue_id", + "formation_id", + "parcour_id", + "niveau_competence_id", + ): + ue_dict.pop(id_id, None) + + if not export_codes_apo: + ue_dict.pop("code_apogee", None) if ue_dict["ects"] is None: del ue_dict["ects"] - mats = sco_edit_matiere.matiere_list({"ue_id": ue_id}) + mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) + mats.sort(key=lambda m: m["numero"] or 0) ue_dict["matiere"] = mats for mat in mats: matiere_id = mat["matiere_id"] @@ -148,6 +164,7 @@ def formation_export( del mat["matiere_id"] del mat["ue_id"] mods = sco_edit_module.module_list({"matiere_id": matiere_id}) + mods.sort(key=lambda m: (m["numero"] or 0, m["code"])) mat["module"] = mods for mod in mods: module_id = mod["module_id"] @@ -183,6 +200,8 @@ def formation_export( del mod["matiere_id"] del mod["module_id"] del mod["formation_id"] + if not export_codes_apo: + del mod["code_apogee"] if mod["ects"] is None: del mod["ects"] @@ -323,14 +342,30 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False): referentiel_competence_id, ue_info[1] ) ue_id = sco_edit_ue.do_ue_create(ue_info[1]) + ue: UniteEns = UniteEns.query.get(ue_id) + assert ue if xml_ue_id: ues_old2new[xml_ue_id] = ue_id # élément optionnel présent dans les exports BUT: ue_reference = ue_info[1].get("reference") if ue_reference: ue_reference_to_id[int(ue_reference)] = ue_id + # -- create matieres for mat_info in ue_info[2]: + if mat_info[0] == "parcour": + # Parcours (BUT) + code_parcours = mat_info[1]["code"] + parcour = ApcParcours.query.filter_by( + code=code_parcours, + referentiel_id=referentiel_competence_id, + ).first() + if parcour: + ue.parcour = parcour + db.session.add(ue) + else: + log(f"Warning: parcours {code_parcours} inexistant !") + continue assert mat_info[0] == "matiere" mat_info[1]["ue_id"] = ue_id mat_id = sco_edit_matiere.do_matiere_create(mat_info[1]) @@ -382,12 +417,12 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False): # associe les parcours de ce module (BUT) if referentiel_competence_id is not None: code_parcours = child[1]["code"] - parcours = ApcParcours.query.filter_by( + parcour = ApcParcours.query.filter_by( code=code_parcours, referentiel_id=referentiel_competence_id, ).first() - if parcours: - module.parcours.append(parcours) + if parcour: + module.parcours.append(parcour) db.session.add(module) else: log( diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 93161895..2edd3655 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -87,6 +87,8 @@ _formsemestreEditor = ndb.EditableTable( "resp_can_edit": bool, "resp_can_change_ens": bool, "ens_can_edit_eval": bool, + "bul_bgcolor": lambda color: color or "white", + "titre": lambda titre: titre or "sans titre", }, ) diff --git a/app/scodoc/sco_formsemestre_custommenu.py b/app/scodoc/sco_formsemestre_custommenu.py index e879d622..ed39904b 100644 --- a/app/scodoc/sco_formsemestre_custommenu.py +++ b/app/scodoc/sco_formsemestre_custommenu.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 549bdb28..16ed01c7 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -28,7 +28,7 @@ """Form choix modules / responsables et creation formsemestre """ import flask -from flask import url_for, flash +from flask import url_for, flash, redirect from flask import g, request from flask_login import current_user @@ -177,10 +177,12 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N uid2display[u.id] = u.get_nomplogin() allowed_user_names = list(uid2display.values()) + [""] # - formation_id = int(vals["formation_id"]) - formation = Formation.query.get(formation_id) - if formation is None: - raise ScoValueError("Formation inexistante !") + if formsemestre: + formation = formsemestre.formation + else: + formation_id = int(vals["formation_id"]) + formation = Formation.query.get_or_404(formation_id) + is_apc = formation.is_apc() if not edit: initvalues = {"titre": _default_sem_title(formation)} @@ -236,7 +238,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N else: modules = ( Module.query.filter( - Module.formation_id == formation_id, UniteEns.id == Module.ue_id + Module.formation_id == formation.id, UniteEns.id == Module.ue_id ) .order_by(Module.module_type, UniteEns.numero, Module.numero) .all() @@ -254,7 +256,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N # modform = [ ("formsemestre_id", {"input_type": "hidden"}), - ("formation_id", {"input_type": "hidden", "default": formation_id}), + ("formation_id", {"input_type": "hidden", "default": formation.id}), ( "date_debut", { @@ -312,6 +314,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N le titre: ils seront automatiquement ajoutés """, + "allow_null": False, }, ), ( @@ -434,7 +437,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N ) if edit: formtit = f""" -

    Modifier les coefficients des UE capitalisées

    @@ -553,9 +556,10 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N str(parcour.id) for parcour in ref_comp.parcours ], "explanation": """Parcours proposés dans ce semestre. - S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours. - Attention, si aucun parcours n'est coché, toutes les UEs du - programme seront considérées, quel que soit leur parcours. + Cocher tous les parcours est exactement équivalent à n'en cocher aucun: + par exemple, pour un semestre de "tronc commun", on peut ne pas indiquer de parcours. + Si aucun parcours n'est coché, toutes les UEs du + programme seront donc considérées, quel que soit leur parcours. """, }, ) @@ -771,7 +775,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N if tf[0] == 0 or msg: return f"""

    Formation {formation.titre} ({formation.acronyme}), version { formation.version}, code {formation.formation_code}

    @@ -1182,7 +1186,10 @@ def do_formsemestre_clone( """Clone a semestre: make copy, same modules, same options, same resps, same partitions. New dates, responsable_id """ - log("cloning %s" % orig_formsemestre_id) + log(f"cloning orig_formsemestre_id") + formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404( + orig_formsemestre_id + ) orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id) cnx = ndb.GetDBConnexion() # 1- create sem @@ -1193,7 +1200,8 @@ def do_formsemestre_clone( args["date_fin"] = date_fin args["etat"] = 1 # non verrouillé formsemestre_id = sco_formsemestre.do_formsemestre_create(args) - log("created formsemestre %s" % formsemestre_id) + log(f"created formsemestre {formsemestre_id}") + formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) # 2- create moduleimpls mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id) for mod_orig in mods_orig: @@ -1255,7 +1263,12 @@ def do_formsemestre_clone( args["formsemestre_id"] = formsemestre_id _ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args) - # 5- Copy partitions and groups + # 6- Copie les parcours + formsemestre.parcours = formsemestre_orig.parcours + db.session.add(formsemestre) + db.session.commit() + + # 7- Copy partitions and groups if clone_partitions: sco_groups_copy.clone_partitions_and_groups( orig_formsemestre_id, formsemestre_id @@ -1768,7 +1781,13 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None): if tf[0] == 0: return "\n".join(H) + tf[1] + footer elif tf[0] == -1: - return "

    annulation

    " + return redirect( + url_for( + "notes.formsemestre_editwithmodules", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) else: # change values # 1- supprime les coef qui ne sont plus forcés diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index c33b7309..c82d176c 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index 3a2bf609..024ccbf9 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 7c93ee10..c3ab9690 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -179,7 +179,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: "title": "Modifier le semestre", "endpoint": "notes.formsemestre_editwithmodules", "args": { - "formation_id": formation.id, "formsemestre_id": formsemestre_id, }, "enabled": can_modify_sem, @@ -644,12 +643,12 @@ def formsemestre_description_table( titles = {title: title for title in columns_ids} titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues}) titles["ects"] = "ECTS" - titles["jour"] = "Evaluation" + titles["jour"] = "Évaluation" titles["description"] = "" titles["coefficient"] = "Coef. éval." titles["evalcomplete_str"] = "Complète" titles["parcours"] = "Parcours" - titles["publish_incomplete_str"] = "Toujours Utilisée" + titles["publish_incomplete_str"] = "Toujours utilisée" title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}" R = [] @@ -728,6 +727,8 @@ def formsemestre_description_table( evals.reverse() # ordre chronologique # Ajoute etat: for e in evals: + e["_jour_order"] = e["jour"].isoformat() + e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" e["UE"] = l["UE"] e["_UE_td_attrs"] = l["_UE_td_attrs"] e["Code"] = l["Code"] @@ -1081,10 +1082,27 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True): formsemestre_warning_etuds_sans_note(formsemestre, nt) if can_change_all_notes else "", - """

    Tableau de bord: - cliquez sur un module pour saisir des notes -

    """, + """

    Tableau de bord : """, ] + if formsemestre.est_courant(): + H.append( + """cliquez sur un module pour saisir des notes""" + ) + elif datetime.date.today() > formsemestre.date_fin: + if formsemestre.etat: + H.append( + """semestre du passé non verrouillé""" + ) + else: + H.append( + """semestre pas encore commencé""" + ) + H.append("

    ") + + if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id): + H.append( + """
    Toutes évaluations (même incomplètes) visibles
    """ + ) if nt.expr_diagnostics: H.append(html_expr_diagnostic(nt.expr_diagnostics)) @@ -1361,7 +1379,7 @@ def get_formsemestre_etudids_sans_notes( .count() ) if not nb_notes_sem: - return + return set() etudids_sans_notes = set.intersection( *[ set.intersection(*m_res.evals_etudids_sans_note.values()) diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index b94db987..3d2a7835 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -39,7 +39,7 @@ from app import db, log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app.models import FormSemestre, UniteEns from app.models.notes import etud_has_notes_attente from app.models.validations import ( ScolarAutorisationInscription, @@ -602,9 +602,21 @@ def formsemestre_recap_parcours_table( {sem['mois_debut']} {formsemestre.titre_annee()}{parcours_name} + title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name} """ ) + if nt.is_apc: + H.append( + f"""jury""" + ) + H.append("""""") + if nt.is_apc: H.append('BUT') elif decision_sem: @@ -781,8 +793,8 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None ) # Choix code semestre: - codes = list(sco_codes_parcours.CODES_JURY_SEM) - codes.sort() # fortuitement, cet ordre convient bien ! + codes = sorted(sco_codes_parcours.CODES_JURY_SEM) + # fortuitement, cet ordre convient bien ! H.append( 'Code semestre: - -  aide - """ - % sem, # " - """inscrire aux mêmes groupes""" - % inscrit_groupes_checked, - """inclure tous les étudiants (même sans décision de jury)""" - % ignore_jury_checked, - """
    Actuellement %s inscrits - et %d candidats supplémentaires -
    """ - % (len(inscrits), len(candidats_non_inscrits)), - etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs), - """

    """, - formsemestre_inscr_passage_help(sem), - """""", + f"""

    + + + + +  aide + + inscrire aux mêmes groupes + + inclure tous les étudiants (même sans décision de jury) + +
    Actuellement {len(inscrits)} inscrits + et {len(candidats_non_inscrits)} candidats supplémentaires +
    + +
    {scu.EMO_WARNING} Seuls les semestres dont la date de fin est antérieure à la date de début + de ce semestre ({formsemestre.date_debut.strftime("%d/%m/%Y")}) sont pris en compte.
    + {etuds_select_boxes(auth_etuds_by_sem, inscrits_ailleurs)} + + + + {formsemestre_inscr_passage_help(sem)} + +
    + """, ] - # Semestres sans etudiants autorisés + # Semestres sans étudiants autorisés empty_sems = [] for formsemestre_id in auth_etuds_by_sem.keys(): if not auth_etuds_by_sem[formsemestre_id]["etuds"]: @@ -474,18 +490,22 @@ def build_page( """

    Autres semestres sans candidats :

      """ ) for infos in empty_sems: - H.append("""
    • %(title)s
    • """ % infos) + H.append( + """
    • %(title)s
    • """ + % infos + ) H.append("""
    """) return H def formsemestre_inscr_passage_help(sem): - return ( - """

    Explications

    + return f"""

    Explications

    Cette page permet d'inscrire des étudiants dans le semestre destination %(titreannee)s, + href="{ + url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=sem["formsemestre_id"] ) + }">{sem['titreannee']}, et d'en désincrire si besoin.

    Les étudiants sont groupés par semestres d'origines. Ceux qui sont en caractères @@ -495,10 +515,13 @@ def formsemestre_inscr_passage_help(sem):

    Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter d'autres étudiants à inscrire dans le semestre destination.

    Si vous dé-selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit.

    +

    Le bouton inscrire aux mêmes groupes ne prend en compte que les groupes qui existent + dans les deux semestres: pensez à créer les partitions et groupes que vous souhaitez conserver + avant d'inscrire les étudiants. +

    Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton "Appliquer les modifications" !

    -
    """ - % sem - ) +
    + """ def etuds_select_boxes( @@ -574,13 +597,13 @@ def etuds_select_boxes( if with_checkbox: H.append( """ (Select. - tous - aucun""" # " + tous + aucun""" # " % infos ) if sel_inscrits: H.append( - """inscrits""" + """inscrits""" % infos ) if with_checkbox or sel_inscrits: diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index ba27b1f4..2a63078b 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -76,7 +76,7 @@ def do_evaluation_listenotes( else: raise ValueError("missing argument: evaluation or module") if not evals: - return "

    Aucune évaluation !

    ", f"ScoDoc" + return "

    Aucune évaluation !

    ", "ScoDoc" E = evals[0] # il y a au moins une evaluation modimpl = ModuleImpl.query.get(E["moduleimpl_id"]) @@ -244,7 +244,6 @@ def _make_table_notes( E = evals[0] moduleimpl_id = E["moduleimpl_id"] modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) - modimpl_o = modimpl.to_dict() # TODO temporaire - à refactorer module: Module = modimpl.module formsemestre: FormSemestre = modimpl.formsemestre is_apc = module.formation.get_parcours().APC_SAE diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index d742eba2..21a84bd0 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_lycee.py b/app/scodoc/sco_lycee.py index 1aba7b89..4d9cf02f 100644 --- a/app/scodoc/sco_lycee.py +++ b/app/scodoc/sco_lycee.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_modalites.py b/app/scodoc/sco_modalites.py index 65b9269b..b12e54e7 100644 --- a/app/scodoc/sco_modalites.py +++ b/app/scodoc/sco_modalites.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_moduleimpl.py b/app/scodoc/sco_moduleimpl.py index cdce8ac4..0e1edbee 100644 --- a/app/scodoc/sco_moduleimpl.py +++ b/app/scodoc/sco_moduleimpl.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index 75aad0aa..bf330adb 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -35,15 +35,14 @@ from flask_login import current_user from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns -import app.scodoc.notesdb as ndb -import app.scodoc.sco_utils as scu from app import log from app.scodoc.scolog import logdb from app.scodoc import html_sco_header from app.scodoc import htmlutils from app.scodoc import sco_cache +from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_module from app.scodoc import sco_edit_ue from app.scodoc import sco_etud @@ -51,8 +50,10 @@ from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl +import app.scodoc.notesdb as ndb from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission +import app.scodoc.sco_utils as scu def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False): @@ -297,7 +298,13 @@ def moduleimpl_inscriptions_stats(formsemestre_id): options.append(modimpl) # Page HTML: - H = [html_sco_header.html_sem_header("Inscriptions aux modules du semestre")] + H = [ + html_sco_header.html_sem_header( + "Inscriptions aux modules et UE du semestre", + javascripts=["js/etud_info.js", "js/moduleimpl_inscriptions_stats.js"], + init_qtip=True, + ) + ] H.append(f"

    Inscrits au semestre: {len(inscrits)} étudiants

    ") @@ -393,7 +400,9 @@ def moduleimpl_inscriptions_stats(formsemestre_id): # Etudiants "dispensés" d'une UE (capitalisée) ues_cap_info = get_etuds_with_capitalized_ue(formsemestre_id) if ues_cap_info: - H.append('

    Étudiants avec UEs capitalisées:

      ') + H.append( + '

      Étudiants avec UEs capitalisées (ADM):

      ") H.append("
    ") + # BUT: propose dispense de toutes UEs + if is_apc: + H.append(_list_but_ue_inscriptions(res, read_only=not can_change)) H.append( - """

    Cette page décrit les inscriptions actuelles. - Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en + """


    Cette page décrit les inscriptions actuelles. + Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en cliquant sur la ligne du module.

    -

    Note: la déinscription d'un module ne perd pas les notes. Ainsi, si +

    Note: la déinscription d'un module ne perd pas les notes. Ainsi, si l'étudiant est ensuite réinscrit au même module, il retrouvera ses notes.

    """ ) @@ -483,6 +496,122 @@ def moduleimpl_inscriptions_stats(formsemestre_id): return "\n".join(H) +def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) -> str: + """HTML pour dispenser/reinscrire chaque étudiant à chaque UE du BUT""" + H = [ + """ +
    +

    Inscriptions/déinscription aux UEs du BUT

    +
    + """ + ] + table_inscr = _table_but_ue_inscriptions(res) + ue_ids = ( + set.union(*(set(x.keys()) for x in table_inscr.values())) + if table_inscr + else set() + ) + ues = sorted( + (UniteEns.query.get(ue_id) for ue_id in ue_ids), + key=lambda u: (u.numero or 0, u.acronyme), + ) + H.append("""""") + for ue in ues: + H.append(f"""""") + H.append("""""") + + for etudid, ues_etud in table_inscr.items(): + etud: Identite = Identite.query.get(etudid) + H.append( + f"""""" + ) + for ue in ues: + td_class = "" + est_inscr = ues_etud.get(ue.id) # None si pas concerné + if est_inscr is None: + content = "" + else: + # Validations d'UE déjà enregistrées dans d'autres semestres + validations_ue = ( + ScolarFormSemestreValidation.query.filter_by(etudid=etudid) + .filter( + ScolarFormSemestreValidation.formsemestre_id + != res.formsemestre.id, + ScolarFormSemestreValidation.code.in_( + sco_codes_parcours.CODES_UE_VALIDES + ), + ) + .join(UniteEns) + .filter_by(ue_code=ue.ue_code) + .all() + ) + validations_ue.sort( + key=lambda v: sco_codes_parcours.BUT_CODES_ORDERED.get(v.code, 0) + ) + validation = validations_ue[-1] if validations_ue else None + expl_validation = ( + f"""Validée ({validation.code}) le {validation.event_date.strftime("%d/%m/%Y")}""" + if validation + else "" + ) + td_class = ' class="ue_validee"' if validation else "" + content = f""" + """ + + H.append(f"""{content}""") + H.append( + """
    {ue.acronyme}
    {etud.nomprenom}
    +
    +
    + L'inscription ou désinscription aux UEs du BUT n'affecte pas les inscriptions aux modules + mais permet de "dispenser" un étudiant de suivre certaines UEs de son parcours. + Il peut s'agit d'étudiants redoublants ayant déjà acquis l'UE, ou d'autres cas particuliers. + La dispense d'UE est réversible à tout moment (avant le jury de fin de semestre) + et n'affecte pas les notes saisies. +
    +
    + """ + ) + return "\n".join(H) + + +def _table_but_ue_inscriptions(res: NotesTableCompat) -> dict[int, dict]: + """ "table" avec les inscriptions aux UEs de chaque étudiant + { + etudid : { ue_id : True | False } + } + """ + return { + etudid: { + ue_id: (etudid, ue_id) not in res.dispense_ues + for ue_id in res.etud_ues_ids(etudid) + } + for etudid, inscr in res.formsemestre.etuds_inscriptions.items() + if inscr.etat == scu.INSCRIT + } + + def descr_inscrs_module(moduleimpl_id, set_all, partitions): """returns tous_inscrits, nb_inscrits, descr""" ins = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=moduleimpl_id) diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 67b4a8d9..5bf2a7f8 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -433,7 +433,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): if nb_evaluations > 0: top_table_links += f""" Trier par date """ @@ -544,7 +544,9 @@ def _ligne_evaluation( if not first_eval: H.append(""" """) tr_class_1 += " mievr_spaced" - H.append(f"""""") + H.append( + f"""""" + ) coef = evaluation.coefficient if is_apc: if not evaluation.get_ue_poids_dict(): @@ -588,7 +590,9 @@ def _ligne_evaluation( ) # H.append( - f"""
    + f""" + + { eval_index:2} @@ -597,7 +601,7 @@ def _ligne_evaluation( # Fleches: if eval_index != (nb_evals - 1) and can_edit_evals: H.append( - f"""{arrow_up}""" ) @@ -605,30 +609,19 @@ def _ligne_evaluation( H.append(arrow_none) if (eval_index > 0) and can_edit_evals: H.append( - f"""{arrow_down}""" ) else: H.append(arrow_none) - H.append( - f""" -
    - - -   - Durée - Coef. - Notes - Abs - N - Moyenne """ - ) - if etat["evalcomplete"]: etat_txt = """(prise en compte)""" etat_descr = "notes utilisées dans les moyennes" + elif etat["evalattente"] and not evaluation.publish_incomplete: + etat_txt = "(prise en compte, mais notes en attente)" + etat_descr = "il y a des notes en attente" elif evaluation.publish_incomplete: etat_txt = """(prise en compte immédiate)""" etat_descr = ( @@ -645,9 +638,19 @@ def _ligne_evaluation( }" title="{etat_descr}">{etat_txt}""" H.append( - f"""{etat_txt} - - """ + f""" + + +   + Durée + Coef. + Notes + Abs + N + Moyenne {etat_txt} + + + """ ) if can_edit_evals: H.append( @@ -723,7 +726,7 @@ def _ligne_evaluation( {etat["nb_notes"]} / {etat["nb_inscrits"]} {etat["nb_abs"]} {etat["nb_neutre"]} - """ + """ % etat ) if etat["moy"]: @@ -747,11 +750,11 @@ def _ligne_evaluation( H.append(f"""""") if modimpl.module.is_apc(): H.append( - f"""{ + f"""{ evaluation.get_ue_poids_str()}""" ) else: - H.append('') + H.append('') H.append("""""") else: # il y a deja des notes saisies gr_moyennes = etat["gr_moyennes"] @@ -770,7 +773,10 @@ def _ligne_evaluation( name = "Tous" # tous else: name = f"""Groupe {gr_moyenne["group_name"]}""" - H.append(f"""{name}  """) + H.append( + f"""{name}   + """ + ) if gr_moyenne["gr_nb_notes"] > 0: H.append( f"""{gr_moyenne["gr_moy"]}  (%s' % (m, m) - for m in [etud["email"], etud["emailperso"]] + for m in [etud_["email"], etud_["emailperso"]] if m ] ) @@ -277,7 +279,7 @@ def ficheEtud(etudid=None): sem_info[sem["formsemestre_id"]] = grlink if info["sems"]: - Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"]) + Se = sco_cursus.get_situation_etud_cursus(etud_, info["last_formsemestre_id"]) info["liste_inscriptions"] = formsemestre_recap_parcours_table( Se, etudid, @@ -452,7 +454,19 @@ def ficheEtud(etudid=None): info["bourse_span"] = "" # raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche... - info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid) + # info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid) + + # XXX dev + info["but_cursus_mkup"] = "" + if info["sems"]: + last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"]) + if last_sem.formation.is_apc(): + but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation) + info["but_cursus_mkup"] = render_template( + "but/cursus_etud.j2", + cursus=but_cursus, + scu=scu, + ) tmpl = """
    @@ -486,7 +500,7 @@ def ficheEtud(etudid=None): %(inscriptions_mkup)s -%(but_infos_mkup)s +%(but_cursus_mkup)s
    %(adm_data)s @@ -524,7 +538,11 @@ def ficheEtud(etudid=None): """ header = html_sco_header.sco_header( page_title="Fiche étudiant %(prenom)s %(nom)s" % info, - cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/jury_but.css"], + cssstyles=[ + "libjs/jQuery-tagEditor/jquery.tag-editor.css", + "css/jury_but.css", + "css/cursus_but.css", + ], javascripts=[ "libjs/jinplace-1.2.1.min.js", "js/ue_list.js", diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index 110917e1..e427b717 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_photos.py b/app/scodoc/sco_photos.py index c0f3cbd8..cdf0e9fb 100644 --- a/app/scodoc/sco_photos.py +++ b/app/scodoc/sco_photos.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -73,7 +73,7 @@ from config import Config PHOTO_DIR = os.path.join(Config.SCODOC_VAR_DIR, "photos") ICONS_DIR = os.path.join(Config.SCODOC_DIR, "app", "static", "icons") UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg") -UNKNOWN_IMAGE_URL = "get_photo_image?etudid=" # with empty etudid => unknown face image + IMAGE_EXT = ".jpg" JPG_QUALITY = 0.92 REDUCED_HEIGHT = 90 # pixels @@ -81,6 +81,11 @@ MAX_FILE_SIZE = 4 * 1024 * 1024 # max allowed size for uploaded image, in bytes H90 = ".h90" # suffix for reduced size images +def unknown_image_url() -> str: + "URL for 'unkwown' face image" + return url_for("scolar.get_photo_image", scodoc_dept=g.scodoc_dept, etudid="") + + def photo_portal_url(etud): """Returns external URL to retreive photo on portal, or None if no portal configured""" @@ -118,7 +123,7 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str: ext_url = photo_portal_url(etud) if not ext_url: # fallback: Photo "unknown" - photo_url = scu.ScoURL() + "/" + UNKNOWN_IMAGE_URL + photo_url = unknown_image_url() else: # essaie de copier la photo du portail new_path, _ = copy_portal_photo_to_fs(etud) @@ -128,7 +133,7 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str: if scu.CONFIG.PUBLISH_PORTAL_PHOTO_URL: photo_url = ext_url else: - photo_url = scu.ScoURL() + "/" + UNKNOWN_IMAGE_URL + photo_url = unknown_image_url() return photo_url @@ -143,7 +148,8 @@ def get_photo_image(etudid=None, size="small"): filename = photo_pathname(etud.photo_filename, size=size) if not filename: filename = UNKNOWN_IMAGE_PATH - return _http_jpeg_file(filename) + r = _http_jpeg_file(filename) + return r def _http_jpeg_file(filename): @@ -166,7 +172,7 @@ def _http_jpeg_file(filename): except ValueError: mod_since = None if (mod_since is not None) and last_modified <= mod_since: - return "", 304 # not modified + return make_response(b"", 304) # not modified # last_modified_str = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified) @@ -183,7 +189,7 @@ def etud_photo_is_local(etud: dict, size="small"): return photo_pathname(etud["photo_filename"], size=size) -def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"): +def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") -> str: """HTML img tag for the photo, either in small size (h90) or original size (size=="orig") """ @@ -194,14 +200,14 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"): return abort(404, "etudiant inconnu") etud = etuds[0] else: - raise ValueError("etud_photo_html: either etud or etudid must be specified") + abort(404, "etud_photo_html: either etud or etudid must be specified") photo_url = etud_photo_url(etud, size=size) nom = etud.get("nomprenom", etud["nom_disp"]) if title is None: title = nom if not etud_photo_is_local(etud): fallback = ( - """onerror='this.onerror = null; this.src="%s"'""" % UNKNOWN_IMAGE_URL + f"""onerror='this.onerror = null; this.src="{unknown_image_url()}"'""" ) else: fallback = "" @@ -218,7 +224,7 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"): ) -def etud_photo_orig_html(etud=None, etudid=None, title=None): +def etud_photo_orig_html(etud=None, etudid=None, title=None) -> str: """HTML img tag for the photo, in full size. Full-size images are always stored locally in the filesystem. They are the original uploaded images, converted in jpeg. @@ -238,7 +244,7 @@ def photo_pathname(photo_filename: str, size="orig"): elif size == "orig": version = "" else: - raise ValueError("invalid size parameter for photo") + abort(404, "invalid size parameter for photo") if not photo_filename: return False path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 898bd31b..0e0ec844 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_portal_apogee.py b/app/scodoc/sco_portal_apogee.py index 435ec4e8..61164c61 100644 --- a/app/scodoc/sco_portal_apogee.py +++ b/app/scodoc/sco_portal_apogee.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py index 36e9ca0e..2c7934dd 100644 --- a/app/scodoc/sco_poursuite_dut.py +++ b/app/scodoc/sco_poursuite_dut.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index c18e33e1..e0c8c0a3 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -1565,7 +1565,7 @@ class BasePreferences(object): "initvalue": "", "title": "e-mail copie bulletins", "size": 40, - "explanation": "adresse recevant une copie des bulletins envoyés aux étudiants", + "explanation": "adresse(s) recevant une copie des bulletins envoyés aux étudiants (si plusieurs, les séparer par des virgules)", "category": "bul_mail", }, ), @@ -2332,7 +2332,9 @@ def doc_preferences(): return "\n".join([" | ".join(x) for x in L]) -def bulletin_option_affichage(formsemestre_id: int, prefs: SemPreferences) -> dict: +def bulletin_option_affichage( + formsemestre: "FormSemestre", prefs: SemPreferences +) -> dict: "dict avec les options d'affichages (préférences) pour ce semestre" fields = ( "bul_show_abs", @@ -2356,4 +2358,8 @@ def bulletin_option_affichage(formsemestre_id: int, prefs: SemPreferences) -> di "bul_show_date_inscr", ) # on enlève le "bul_" de la clé: - return {field[4:]: prefs[field] for field in fields} + fields = {field[4:]: prefs[field] for field in fields} + # Ajoute les réglages du formsemestre qui ne sont pas des préférences: + fields["block_moyenne_generale"] = formsemestre.block_moyenne_generale + fields["bgcolor"] = formsemestre.bul_bgcolor + return fields diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index 21e23c70..53c3a9e0 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index 3edcf660..e323348d 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py index 70e80c93..d7462c18 100644 --- a/app/scodoc/sco_pvpdf.py +++ b/app/scodoc/sco_pvpdf.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 96378587..de629931 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,6 +37,7 @@ from flask import abort, url_for from app import log from app.but import bulletin_but from app.comp import res_sem +from app.comp.res_common import ResultatsSemestre from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.etudiants import Identite @@ -407,7 +408,7 @@ def gen_formsemestre_recapcomplet_html( def _gen_formsemestre_recapcomplet_html( formsemestre: FormSemestre, - res: NotesTableCompat, + res: ResultatsSemestre, include_evaluations=False, mode_jury=False, filename: str = "", diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py index 725337ae..f89aa1e1 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -1010,6 +1010,11 @@ def get_codeparcoursetud(etud, prefix="", separator=""): 12R pour un etudiant en S1, S2 réorienté en fin de S2 Construit aussi un dict: { semestre_id : decision_jury | None } """ + # Nota: approche plus moderne: + # ' '.join([ f"S{ins.formsemestre.semestre_id}" + # for ins in reversed(etud.inscriptions()) + # if ins.formsemestre.formation.formation_code == XXX ]) + # p = [] decisions_jury = {} # élimine les semestres spéciaux sans parcours (LP...) diff --git a/app/scodoc/sco_report_but.py b/app/scodoc/sco_report_but.py index a4fbf3e2..addc782c 100644 --- a/app/scodoc/sco_report_but.py +++ b/app/scodoc/sco_report_but.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 81e92d51..bf8d8c29 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -74,7 +74,7 @@ from app.scodoc import sco_etud def convert_note_from_string( - note, + note: str, note_max, note_min=scu.NOTES_MIN, etudid: int = None, @@ -128,7 +128,8 @@ def _displayNote(val): return val -def _check_notes(notes, evaluation, mod): +def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict): + # XXX typehint : float or str """notes is a list of tuples (etudid, value) mod is the module (used to ckeck type, for malus) returns list of valid notes (etudid, float value) @@ -321,20 +322,31 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) - return False # error -def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): +def do_evaluation_set_missing( + evaluation_id, value, dialog_confirmed=False, group_ids_str: str = "" +): """Initialisation des notes manquantes""" evaluation = Evaluation.query.get_or_404(evaluation_id) modimpl = evaluation.moduleimpl - # Check access # (admin, respformation, and responsable_id) if not sco_permissions_check.can_edit_notes(current_user, modimpl.id): raise AccessDenied(f"Modification des notes impossible pour {current_user}") # notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) + if not group_ids_str: + groups = None + else: + group_ids = [int(x) for x in str(group_ids_str).split(",")] + groups = sco_groups.listgroups(group_ids) + etudid_etats = sco_groups.do_evaluation_listeetuds_groups( - evaluation_id, getallstudents=True, include_demdef=False + evaluation_id, + getallstudents=groups is None, + groups=groups, + include_demdef=False, ) + notes = [] for etudid, _ in etudid_etats: # pour tous les inscrits if etudid not in notes_db: # pas de note @@ -360,17 +372,23 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): """ # Confirm action if not dialog_confirmed: + plural = len(L) > 1 return scu.confirm_dialog( f"""

    Mettre toutes les notes manquantes de l'évaluation à la valeur {value} ?

    Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC) n'a été rentrée seront affectés.

    -

    {len(L)} étudiants concernés par ce changement de note.

    -

    Attention, les étudiants sans notes de tous les groupes de ce semestre seront affectés.

    +

    {len(L)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""} + par ce changement de note. +

    """, dest_url="", cancel_url=dest_url, - parameters={"evaluation_id": evaluation_id, "value": value}, + parameters={ + "evaluation_id": evaluation_id, + "value": value, + "group_ids_str": group_ids_str, + }, ) # ok comment = "Initialisation notes manquantes" @@ -409,16 +427,16 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): "suppress all notes in this eval" - E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] + evaluation = Evaluation.query.get_or_404(evaluation_id) if sco_permissions_check.can_edit_notes( - current_user, E["moduleimpl_id"], allow_ens=False + current_user, evaluation.moduleimpl_id, allow_ens=False ): # On a le droit de modifier toutes les notes # recupere les etuds ayant une note notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) elif sco_permissions_check.can_edit_notes( - current_user, E["moduleimpl_id"], allow_ens=True + current_user, evaluation.moduleimpl_id, allow_ens=True ): # Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi notes_db = sco_evaluation_db.do_evaluation_get_all_notes( @@ -432,7 +450,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): status_url = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, - moduleimpl_id=E["moduleimpl_id"], + moduleimpl_id=evaluation.moduleimpl_id, ) if not dialog_confirmed: @@ -478,13 +496,12 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): """ ] # news - modimpl = ModuleImpl.query.get(E["moduleimpl_id"]) ScolarNews.add( typ=ScolarNews.NEWS_NOTE, - obj=modimpl.id, + obj=evaluation.moduleimpl.id, text=f"""Suppression des notes d'une évaluation dans {modimpl.module.titre or 'module sans titre'} + >{evaluation.moduleimpl.module.titre or 'module sans titre'} """, url=status_url, ) @@ -792,7 +809,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()): H.append( f"""
  • -
    + Mettre toutes les notes manquantes à @@ -950,31 +967,35 @@ def has_existing_decision(M, E, etudid): # Nouveau formulaire saisie notes (2016) -def saisie_notes(evaluation_id, group_ids=[]): +def saisie_notes(evaluation_id: int, group_ids: list = None): """Formulaire saisie notes d'une évaluation pour un groupe""" if not isinstance(evaluation_id, int): raise ScoInvalidParamError() - group_ids = [int(group_id) for group_id in group_ids] + group_ids = [int(group_id) for group_id in (group_ids or [])] evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: raise ScoValueError("évaluation inexistante") E = evals[0] M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] formsemestre_id = M["formsemestre_id"] + moduleimpl_status_url = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=E["moduleimpl_id"], + ) # Check access # (admin, respformation, and responsable_id) if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]): - return ( - html_sco_header.sco_header() - + "

    Modification des notes impossible pour %s

    " - % current_user.user_name - + """

    (vérifiez que le semestre n'est pas verrouillé et que vous + return f""" + {html_sco_header.sco_header()} +

    Modification des notes impossible pour {current_user.user_name}

    + +

    (vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)

    -

    Continuer

    - """ - % E["moduleimpl_id"] - + html_sco_header.sco_footer() - ) +

    Continuer +

    + {html_sco_header.sco_footer()} + """ # Informations sur les groupes à afficher: groups_infos = sco_groups_view.DisplayedGroupsInfos( @@ -1032,19 +1053,32 @@ def saisie_notes(evaluation_id, group_ids=[]): alone=True, ) ) - H.append("""
  • """) - - # Le formulaire de saisie des notes: - destination = url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=E["moduleimpl_id"], + H.append( + """ + + + + +
    + + """ ) - form = _form_saisie_notes(E, M, groups_infos.group_ids, destination=destination) + # Le formulaire de saisie des notes: + form = _form_saisie_notes(E, M, groups_infos, destination=moduleimpl_status_url) if form is None: - log(f"redirecting to {destination}") - return flask.redirect(destination) + return flask.redirect(moduleimpl_status_url) H.append(form) # H.append("") # /saisie_notes @@ -1075,6 +1109,9 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int): for etudid in etudids: # infos identite etudiant e = sco_etud.etudident_list(cnx, {"etudid": etudid})[0] + etud: Identite = Identite.query.get(etudid) + # TODO: refactor et eliminer etudident_list. + e["etud"] = etud # utilisé seulement pour le tri -- a refactorer sco_etud.format_etud_ident(e) etuds.append(e) # infos inscription dans ce semestre @@ -1126,12 +1163,12 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int): e["val"] = "DEM" e["explanation"] = "Démission" - etuds.sort(key=lambda x: (x["nom"], x["prenom"])) + etuds.sort(key=lambda x: x["etud"].sort_key) return etuds -def _form_saisie_notes(E, M, group_ids, destination=""): +def _form_saisie_notes(E, M, groups_infos, destination=""): """Formulaire HTML saisie des notes dans l'évaluation E du moduleimpl M pour les groupes indiqués. @@ -1161,7 +1198,10 @@ def _form_saisie_notes(E, M, group_ids, destination=""): descr = [ ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), ("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}), - ("group_ids", {"default": group_ids, "input_type": "hidden", "type": "list"}), + ( + "group_ids", + {"default": groups_infos.group_ids, "input_type": "hidden", "type": "list"}, + ), # ('note_method', { 'default' : note_method, 'input_type' : 'hidden'}), ("comment", {"size": 44, "title": "Commentaire", "return_focus_next": True}), ("changed", {"default": "0", "input_type": "hidden"}), # changed in JS @@ -1269,14 +1309,13 @@ def _form_saisie_notes(E, M, group_ids, destination=""): H = [] if nb_decisions > 0: H.append( - """
    + f"""
      -
    • Attention: il y a déjà des décisions de jury enregistrées pour %d étudiants. Après changement des notes, vérifiez la situation !
    • +
    • Attention: il y a déjà des décisions de jury enregistrées pour + {nb_decisions} étudiants. Après changement des notes, vérifiez la situation !
    """ - % nb_decisions ) - # H.append('''
    ''') tf = TF( destination, @@ -1299,17 +1338,20 @@ def _form_saisie_notes(E, M, group_ids, destination=""): elif (not tf.submitted()) or not tf.result: # ajout formulaire saisie notes manquantes H.append( - """ + f"""
    - - Mettre toutes les notes manquantes à - - - affecte tous les groupes. ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente" + + Mettre les notes manquantes à + + + + + ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"
    """ - % evaluation_id ) # affiche formulaire return "\n".join(H) diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index 30ee2ec2..a383f5dd 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index a4b99b58..2c80db6f 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -102,7 +102,7 @@ def formsemestre_synchro_etuds( if not sem["etapes"]: raise ScoValueError( """opération impossible: ce semestre n'a pas de code étape - (voir "Modifier ce semestre") + (voir "Modifier ce semestre") """ % sem ) diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index 20c340f1..809facac 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index a5239c78..cf725794 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_trombino_doc.py b/app/scodoc/sco_trombino_doc.py index 038832e5..b40da577 100644 --- a/app/scodoc/sco_trombino_doc.py +++ b/app/scodoc/sco_trombino_doc.py @@ -1,6 +1,6 @@ ############################################################################## # ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## diff --git a/app/scodoc/sco_trombino_tours.py b/app/scodoc/sco_trombino_tours.py index 85867b1e..6d017a8f 100644 --- a/app/scodoc/sco_trombino_tours.py +++ b/app/scodoc/sco_trombino_tours.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py index 8a8cff2b..2a863ff5 100644 --- a/app/scodoc/sco_ue_external.py +++ b/app/scodoc/sco_ue_external.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py index c70d133a..45307cdd 100644 --- a/app/scodoc/sco_undo_notes.py +++ b/app/scodoc/sco_undo_notes.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_up_to_date.py b/app/scodoc/sco_up_to_date.py index 1ca56b80..f819d76d 100644 --- a/app/scodoc/sco_up_to_date.py +++ b/app/scodoc/sco_up_to_date.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index a0f08c15..d3db5b8e 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index c2c6db15..ae178236 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -150,12 +150,11 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: - # XEV A documenter - if type(date) == str: # XEV utiliser isinstance + if isinstance(date, str): date = is_iso_formated(date, convert=True) new_date: datetime.datetime = date - if date.tzinfo == None: # XEV utiliser "is None" + if date.tzinfo is None: from app.models.assiduites import Assiduite first_assiduite = Assiduite.query.first() @@ -168,32 +167,30 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: return new_date -def verif_interval( # XEV à renommer pour avoir nom de fonction plus explicite, genre dates_se_recouvrent() ou en anglais is_overlapping ??? - periode: tuple[datetime.datetime], interval: tuple[datetime.datetime] +def is_period_overlapping( + periode: tuple[datetime.datetime, datetime.datetime], + interval: tuple[datetime.datetime, datetime.datetime], ) -> bool: - # XEV n'est-ce pas plutot tuple[datetime.datetime,datetime.datetime] ? """ - Vérifie si une période est comprise dans un interval, chevauche l'interval ou comprend l'interval - #XEV: "vrai si la période et de l'intervalle intersectent" + Vérifie si la période et l'interval s'intersectent + Retourne Vrai si c'est le cas, faux sinon """ - - # XEV: si je comprends bien, il suffirait de vérifier les bornes - # voir par exemple https://stackoverflow.com/questions/3269434/whats-the-most-efficient-way-to-test-if-two-ranges-overlap - p_deb, p_fin = periode i_deb, i_fin = interval - i = intervalmap() - p = intervalmap() - i[:] = 0 - p[:] = 0 - i[i_deb:i_fin] = 1 - p[p_deb:p_fin] = 1 + # i = intervalmap() + # p = intervalmap() + # i[:] = 0 + # p[:] = 0 + # i[i_deb:i_fin] = 1 + # p[p_deb:p_fin] = 1 - res: int = sum((i[p_deb], i[p_fin], p[i_deb], p[i_fin])) + # # TOTALK: Vérification des bornes de la période dans l'interval et inversement + # res: int = sum((i[p_deb], i[p_fin], p[i_deb], p[i_fin])) - return res > 0 + # return res > 0 + return p_deb <= i_fin and p_fin >= i_deb # Types de modules @@ -1265,6 +1262,8 @@ def gen_cell(key: str, row: dict, elt="td", with_col_class=False): if with_col_class: klass = key + " " + klass attrs = f'class="{klass}"' if klass else "" + if elt == "th": + attrs += ' scope="row"' data = row.get(f"_{key}_data") # dict if data: for k in data: @@ -1290,7 +1289,10 @@ def gen_row( tr_id = ( f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else "" ) - return f"""{"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) for key in keys if not key.startswith('_')])}""" + return f"""{ + "".join([gen_cell(key, row, elt, with_col_class=with_col_classes) + for key in keys if not key.startswith('_')]) + }""" # Pour accès depuis les templates jinja diff --git a/app/scodoc/sco_vdi.py b/app/scodoc/sco_vdi.py index f058b650..bb0a48da 100644 --- a/app/scodoc/sco_vdi.py +++ b/app/scodoc/sco_vdi.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/sco_xml.py b/app/scodoc/sco_xml.py index 4e9e6f21..8634b8a4 100644 --- a/app/scodoc/sco_xml.py +++ b/app/scodoc/sco_xml.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/scodoc/scolog.py b/app/scodoc/scolog.py index 0854ec94..a3b07f05 100644 --- a/app/scodoc/scolog.py +++ b/app/scodoc/scolog.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/static/css/cursus_but.css b/app/static/css/cursus_but.css new file mode 100644 index 00000000..79cde97c --- /dev/null +++ b/app/static/css/cursus_but.css @@ -0,0 +1,42 @@ +/* Affichage cursus BUT étudiant (sur sa fiche) */ + + +.cursus_but { + margin-left: 32px; + display: inline-grid; + grid-template-columns: repeat(4, auto); + gap: 8px; +} + +.cursus_but>* { + display: flex; + align-items: center; + padding-top: 0px; + padding-bottom: 0px; + padding-left: 16px; + padding-right: 0px; + + background: #FFF; + border: 1px solid #aaa; + border-radius: 8px; +} + +.cursus_but>div.cb_head { + background: rgb(242, 242, 238); + border: none; + border-radius: 0px; + border-bottom: 1px solid gray; + font-weight: bold; +} + +div.cb_titre_competence { + background: #09c !important; + color: #FFF; + padding: 8px !important; +} + +div.code_rcue { + padding-top: 8px; + padding-bottom: 8px; + position: relative; +} \ No newline at end of file diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css index 1829e73b..61f8db77 100644 --- a/app/static/css/jury_but.css +++ b/app/static/css/jury_but.css @@ -1,25 +1,46 @@ /* Saisie décision de jury BUT */ -.jury_but form { +:root { + --color-recorded: rgb(3, 157, 3); + --color-explanation: blueviolet; +} + +.jury_but { font-family: Verdana, Geneva, Tahoma, sans-serif; } -.jury_but .titre_parcours { +.jury_but .titre_parcours, +.jury_but .nom_etud { font-size: 130%; padding-bottom: 12px; } .jury_but .nom_etud { - font-size: 100%; font-weight: bold; - padding-bottom: 12px; +} + +.jury_but h3 { + margin-top: 0px; +} + +form#jury_but { + margin: 0px 16px 16px 16px; + background-color: rgb(253, 253, 231); + border: 2px solid rgb(4, 4, 118); + border-radius: 4px; + padding-top: 8px; + padding-left: 16px; + padding-right: 16px; + padding-bottom: 16px; + min-width: var(--sco-content-min-width); + max-width: var(--sco-content-max-width); } .but_annee { margin-left: 32px; display: inline-grid; grid-template-columns: repeat(4, auto); - gap: 4px; + gap: 8px; } .but_annee_caption { @@ -35,6 +56,7 @@ .niveau_vide { background-color: rgb(195, 195, 195) !important; + position: relative; } .but_annee>* { @@ -108,9 +130,10 @@ div.but_settings { } .but_explanation { - color: blueviolet; + color: var(--color-explanation); font-style: italic; - padding-top: 12px; + padding-top: 4px; + padding-bottom: 12px; } select:disabled { @@ -122,17 +145,37 @@ select:invalid { background: red; } -select.but_code option.recorded { - color: rgb(3, 157, 3); +select.but_code option.recorded, +div.but_code recorded { + color: var(--color-recorded); font-weight: bold; } +div.but_code { + font-weight: bold; + color: blue; + margin-left: 4px; + margin-right: 4px; +} + div.but_niveau_ue.recorded, div.but_niveau_rcue.recorded { - border-color: rgb(136, 252, 136); - border-width: 2px; + border-color: var(--color-recorded); + border-width: 3px; } +div.but_niveau_ue.recorded_different, +div.but_niveau_rcue.recorded_different { + box-shadow: 0 0 0 3px red; + outline: dashed 3px var(--color-recorded); +} + +div.but_niveau_ue.annee_prec { + background-color: rgb(167, 167, 0); +} + +div.but_section_annee.modified, +div.but_niveau_rcue.modified, div.but_niveau_ue.modified { background-color: rgb(255, 214, 254); } @@ -142,11 +185,20 @@ div.but_buttons { } div.but_buttons span { + margin-left: 16px; margin-right: 16px; } +div.but_doc_codes.but_warning_rcue_cap { + padding-top: 8px; + font-size: 100%; + font-style: italic; +} + div.but_doc_codes { margin: 16px; + min-width: var(--sco-content-min-width); + max-width: var(--sco-content-max-width); background-color: rgb(227, 254, 254); font-size: 75%; border: 2px solid rgb(4, 4, 118); @@ -196,4 +248,20 @@ div.but_doc table tbody tr:nth-child(odd) { div.but_doc table tr td.amue { color: rgb(127, 127, 206); font-size: 90%; +} + +.but_niveau_rcue .scoplement { + font-weight: normal; +} + +.but_autorisations_passage { + margin-top: 12px; + margin-left: 32px; + font-weight: bold; + color: var(--color-recorded); +} + +.but_autorisations_passage.but_explanation { + font-weight: normal; + color: var(--color-explanation); } \ No newline at end of file diff --git a/app/static/css/partition_editor.css b/app/static/css/partition_editor.css index b0b89fef..3d7140e4 100644 --- a/app/static/css/partition_editor.css +++ b/app/static/css/partition_editor.css @@ -1,3 +1,7 @@ +html { + overflow-x: hidden; +} + .wait { position: fixed; width: 50px; @@ -44,19 +48,43 @@ margin-bottom: 16px; display: inline-block; cursor: pointer; + pointer-events: none; +} + +.loaded .edition { + pointer-events: initial; +} + +.filtres>label { + display: none; +} + +.editionActivated .valider { + display: block; + width: fit-content; + margin-left: auto; + padding: 8px 32px; + background: #90c; + color: #fff; + box-shadow: 0 2px 2px rgb(0, 0, 0, 0.25); + border-radius: 4px; + cursor: pointer; } main { font-family: Verdana, Geneva, Tahoma, sans-serif; display: flex; flex-wrap: wrap; - gap: 32px; - row-gap: 4px; + gap: 8px; margin-right: 16px; + padding: 8px; + border-radius: 12px; + background: #424242; } main h2 { border-bottom: 4px solid #09c; + font-size: 150% !important; } main h2, @@ -64,11 +92,22 @@ main h3 { font-weight: 400; } -body:not(.editionActivated) .editing { - display: none !important; +section { + background: #fff; + padding: 8px; + border-radius: 8px; } -.editionActivated #zoneChoix .etudiants>div { +body:not(.editionActivated) .editing { + display: none; +} + +.nonEditable .editing { + display: none; +} + +.editionActivated #zoneChoix, +.editionActivated #zoneGroupes { pointer-events: none; opacity: 0.2; } @@ -80,7 +119,9 @@ body:not(.editionActivated) .editing { } @keyframes boing { - 100% {transform: translateY(-20px)} + 100% { + transform: translateY(-20px) + } } /****************/ @@ -89,13 +130,19 @@ body:not(.editionActivated) .editing { background: #0c9 !important; padding: 8px 16px !important; cursor: pointer; + color: #fff; + box-shadow: 0 2px 2px rgb(0, 0, 0, 0.25); + border-radius: 4px; + text-align: center; + margin-bottom: 4px; + width: fit-content; } .move, .modif, .suppr { color: #000; - padding: 4px; + padding: 0 4px; cursor: pointer; } @@ -172,10 +219,31 @@ body.editionActivated .filtres>div>div>div>div { .moving { opacity: 0.8; pointer-events: none; - ; } -.grabbing>div:not([data-idgroupe="aucun"]):hover:before { +.grabbing>div[data-idpartition]:not([data-idgroupe]):hover:before { + content: ""; + position: absolute; + left: -4px; + right: -4px; + bottom: calc(100% + 1px); + height: 2px; + width: auto; + background: #c44; + animation: insertPartion 0.2s infinite alternate ease-in-out; +} + +@keyframes insertPartion { + 0% { + transform: translateX(-4px) + } + + 100% { + transform: translateX(4px) + } +} + +.grabbing>*:not([data-idgroupe="aucun"]):hover:before { content: ""; position: absolute; bottom: -4px; @@ -183,10 +251,10 @@ body.editionActivated .filtres>div>div>div>div { right: calc(100% + 1px); width: 2px; background: #c44; - animation: insert 0.2s infinite alternate ease-in-out; + animation: insertGroupe 0.2s infinite alternate ease-in-out; } -@keyframes insert { +@keyframes insertGroupe { 0% { transform: translateY(-4px) } @@ -197,65 +265,116 @@ body.editionActivated .filtres>div>div>div>div { } /*****************************/ -/* Zone Partitions */ +/* Zone Filtres */ /*****************************/ #zonePartitions { width: 100%; } -.filtres { - display: table; +#zonePartitions>div { + width: fit-content; } -.filtres>div { - background: #ddd; +#zonePartitions h3{ + display: flex; +} +#zonePartitions h3 .onoff{ + margin-left: auto; + cursor: pointer; + display: flex; + align-items: center; + gap: 4px; + font-size: 16px; +} + +#zonePartitions .filtres { + width: fit-content; +} + +#zonePartitions .filtres>div { + background: #eee; padding: 8px; border-radius: 8px; margin-bottom: 8px; + position: relative; } -.filtres>div>div>div { +#zonePartitions .filtres .groupes { display: flex; flex-wrap: wrap; gap: 4px; row-gap: 2px; - margin: 8px 0; + margin: 4px 0 0 0; } -.filtres>div>div>div>div { +#zonePartitions .filtres .groupes>div { + position: relative; background: #09c; color: #FFF; border-radius: 4px; padding: 8px 32px; + margin: 0; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.25); } -body:not(.editionActivated) .filtres>div>div>div>div { +body:not(.editionActivated) .filtres .groupes>div { cursor: pointer; } -body:not(.editionActivated) .filtres>div>div>div>div:hover { - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); +body:not(.editionActivated) .filtres .groupes>div:hover { + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6) !important; } -body:not(.editionActivated) .filtres>div>div>div>div:active { - box-shadow: 0 0 0 #000; +body:not(.editionActivated) .filtres .groupes>div:active { + box-shadow: 0 0 0 #000 !important; transform: translateY(2px); } +body.editionActivated .filtres [data-idgroupe=aucun]{ + display: none; +} + +body.editionActivated .filtres .nonEditable .move{ + display: initial; +} .filtres .unselect { - background: rgba(0, 153, 204, 0.5); + background: rgba(0, 153, 204, 0.5) !important; } /*****************************/ /* Zone Etudiants */ /*****************************/ +#zoneChoix>.autoAffectation{ + background: #3c3c3c; + color: #fff; + padding: 4px 8px; + margin-bottom: 16px; + border-radius: 4px; +} +#zoneChoix>.autoAffectation>select{ + border: none; + padding: 4px; + border-radius: 4px; +} +#zoneChoix>.autoAffectation>.affectationGo{ + display: inline-block; + background: #0c9; + padding: 8px 16px; + cursor: pointer; + color: #fff; + box-shadow: 0 2px 2px rgb(0 0 0 / 25%); + border-radius: 4px; + text-align: center; + margin-top: 4px; + margin-bottom: 4px; + width: fit-content; +} #zoneChoix .etudiants>div { background: #FFF; border: 1px solid #aaa; border-radius: 4px; padding: 4px 8px; - margin: 4px 0; + margin: -1px 0; display: flex; flex-wrap: wrap; justify-content: space-between; @@ -266,16 +385,22 @@ body:not(.editionActivated) .filtres>div>div>div>div:active { flex: 1; } -#zoneChoix small { +#zoneChoix .small { color: #444; + font-size: 8px; font-style: italic; } +#zoneChoix .etudiants .grpPartitions { + display: flex; + flex-direction: column; + gap: 2px; +} + #zoneChoix .etudiants .partition { display: flex; flex-wrap: wrap; gap: 4px; - margin-bottom: 4px; } #zoneChoix label { @@ -313,9 +438,12 @@ body:not(.editionActivated) .filtres>div>div>div>div:active { color: #fff; } -.hide { +section:not(#zonePartitions) .hide { display: none !important; } +#zonePartitions .hide{ + opacity: 0.4; +} .saved+span { position: relative; @@ -346,11 +474,8 @@ body:not(.editionActivated) .filtres>div>div>div>div:active { flex: 1; } -#zoneGroupes .groupes { - display: flex; - flex-wrap: wrap; - align-items: flex-start; - gap: 16px; +#zoneGroupes h3 { + width: 100%; } #zoneGroupes .partition { @@ -358,8 +483,9 @@ body:not(.editionActivated) .filtres>div>div>div>div:active { padding: 8px; border-radius: 8px; display: flex; - flex-direction: column; + flex-wrap: wrap; gap: 8px; + margin-bottom: 8px; } h3 { @@ -390,5 +516,6 @@ h3 { } #zoneGroupes [data-idgroupe=aucun] { - background: #b5c2c3 !important; + background: #3c3c3c !important; + color: #fff; } \ No newline at end of file diff --git a/app/static/css/ref-competences.css b/app/static/css/ref-competences.css index 96e857d0..61586bc4 100644 --- a/app/static/css/ref-competences.css +++ b/app/static/css/ref-competences.css @@ -1,24 +1,27 @@ -:host{ +:host { font-family: Verdana; - background: #222; + background: rgb(14, 5, 73); display: block; padding: 12px 32px; color: #FFF; max-width: 1000px; margin: auto; } -h1{ + +h1 { font-weight: 100; } + /**********************/ /* Zone parcours */ /**********************/ -.parcours{ +.parcours { display: flex; gap: 4px; padding-right: 4px; } -.parcours>div{ + +.parcours>div { background: #09c; font-size: 18px; text-align: center; @@ -29,65 +32,89 @@ h1{ transition: 0.1s; opacity: 0.7; } + .parcours>div:hover, -.competence>div:hover{ +.competence>div:hover { color: #ccc; } -.parcours>.focus{ + +.parcours>.focus { opacity: 1; } /**********************/ /* Zone compétences */ /**********************/ -.competences{ - display: grid; +.competences { + display: grid; margin-top: 8px; row-gap: 4px; } -.competences>div{ + +.competences>div { padding: 4px 8px; - border-radius: 4px; + border-radius: 4px; cursor: pointer; width: var(--competence-size); margin-right: 4px; } -.comp1{background:#a44} -.comp2{background:#84a} -.comp3{background:#a84} -.comp4{background:#8a4} -.comp5{background:#4a8} -.comp6{background:#48a} +.comp1 { + background: #a44 +} -.competences>.focus{ +.comp2 { + background: #84a +} + +.comp3 { + background: #a84 +} + +.comp4 { + background: #8a4 +} + +.comp5 { + background: #4a8 +} + +.comp6 { + background: #48a +} + +.competences>.focus { outline: 2px solid; } /**********************/ /* Zone AC */ /**********************/ -h2{ +h2 { display: table; padding: 8px 16px; font-size: 20px; border-radius: 16px 0; } -.ACs{ + +.ACs { padding-right: 4px; } -.AC li{ + +.AC li { display: grid; grid-template-columns: auto 1fr; align-items: start; gap: 4px; margin-bottom: 4px; - border-bottom: 1px solid; + border-bottom: 1px solid; } -.AC li>div:nth-child(1){ + +.AC li>div:nth-child(1) { padding: 2px 4px; border-radius: 4px; } -.AC li>div:nth-child(2){ + +.AC li>div:nth-child(2) { padding-bottom: 2px; } \ No newline at end of file diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 795901f0..42c2ecac 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1,7 +1,11 @@ -/* # -*- mode: css -*- - ScoDoc, (c) Emmanuel Viennet 1998 - 2021 +/* ScoDoc, (c) Emmanuel Viennet 1998 - 2023 */ +:root { + --sco-content-min-width: 600px; + --sco-content-max-width: 1024px; +} + html, body { margin: 0; @@ -1585,7 +1589,7 @@ div.formsemestre_status { color: rgb(215, 90, 0); } -div.formsemestre_status_warning::before { +.formsemestre_status_warning::before { content: "\26a0 \fe0f \00a0"; /* EMO_WARNING, "⚠️" */ } @@ -1657,6 +1661,11 @@ td.formsemestre_status_inscrits { text-align: center; } +td.rcp_titre_sem a.jury_link { + margin-left: 8px; + color: red; +} + td.formsemestre_status_cell { white-space: nowrap; } @@ -1728,6 +1737,8 @@ ul.ue_inscr_list li.etud { div.moduleimpl_tableaubord { padding: 7px; border: 2px solid gray; + min-width: var(--sco-content-min-width); + max-width: var(--sco-content-max-width); } div.moduleimpl_type_sae { @@ -1855,11 +1866,19 @@ span.mievr_rattr { padding: 1px 3px 1px 3px; } -tr.mievr td.mievr_tit { +tr.mievr_tit td.mievr_tit { font-weight: bold; - background-color: #cccccc; border-top-left-radius: 8px; +} + +tr.mievr.mievr_tit td { + background-color: #e1e1e1; +} + +tr.mievr_tit td:last-child { border-top-right-radius: 8px; + text-align: right; + padding-right: 8px; } tr.mievr td { @@ -1922,11 +1941,6 @@ span.eval_warning_coef { background-color: rgb(255, 225, 0); } -div.evaluation_order { - position: absolute; - right: 1em; -} - span.evalindex { font-weight: normal; font-size: 80%; @@ -1955,6 +1969,59 @@ span.eval_coef_ue { span.eval_coef_ue_titre {} +/* Inscriptions modules/UE */ +div.list_but_ue_inscriptions { + margin-top: 16px; + margin-bottom: 16px; + padding-left: 8px; + padding-bottom: 8px; + border-radius: 16px; + border: 2px solid black; + background-color: #eafffa; +} + +div.list_but_ue_inscriptions h3 { + margin-top: 8px; +} + +div.list_but_ue_inscriptions table th:first-child { + border-left: 0px; + border-top: 0px; +} + +div.list_but_ue_inscriptions table th:last-child, +div.list_but_ue_inscriptions table td:last-child { + border-right: 1px solid salmon; +} + +div.list_but_ue_inscriptions table th { + border-top: 1px solid salmon; +} + +div.list_but_ue_inscriptions table th, +div.list_but_ue_inscriptions table td { + padding-left: 8px; + padding-right: 8px; + border-left: 1px solid salmon; + border-bottom: 1px solid salmon; +} + +div.list_but_ue_inscriptions table td.ue_validee { + background-color: #a1f539; +} + +form.list_but_ue_inscriptions { + margin-bottom: 16px; +} + +form.list_but_ue_inscriptions td:first-child { + text-align: left; +} + +form.list_but_ue_inscriptions td { + text-align: center; +} + /* Formulaire edition des partitions */ form#editpart table { border: 1px solid gray; @@ -2527,11 +2594,13 @@ div.bull_head { display: grid; justify-content: space-between; grid-template-columns: auto auto; + min-width: 600px; + max-width: 1072px; } div.bull_photo { display: inline-block; - margin-right: 10px; + margin-right: 8px; } span.bulletin_menubar_but { @@ -2884,6 +2953,17 @@ li.tf-msg { vertical-align: -80%; } +.warning-light { + font-style: italic; + color: rgb(166, 50, 159); +} + +.warning-light::before { + content: "\26a0 \fe0f \00a0"; + /* EMO_WARNING, "⚠️" */ +} + + .infop { font-weight: normal; color: rgb(26, 150, 26); @@ -4135,6 +4215,11 @@ table.table_recap tr.apo td { background-color: #d8f5fe; } +table.table_recap tr.type_col { + font-size: 50%; + font-family: monospace; +} + table.table_recap td.evaluation.first, table.table_recap th.evaluation.first { border-left: 2px solid rgb(4, 16, 159); @@ -4171,6 +4256,30 @@ div.table_jury_but_links { margin-bottom: 16px; } + +/* ------------- Tableau stats jury BUT -------- */ +table.jury_stats_codes { + margin-top: 8px; + margin-left: 42px; + border: 3px solid #000000; + text-align: left; + border-collapse: collapse; +} + +table.jury_stats_codes td { + margin-right: 12px; + border-top: 1px solid #000000; + padding: 5px 8px; +} + +div.jury_stats { + background-color: #c7f6c7; + border-radius: 12px; + border: 1px solid black; + padding: 8px; + width: fit-content; +} + /* ------------- Tableau etat evals ------------ */ div.evaluations_recap table.evaluations_recap { diff --git a/app/static/js/groups_view.js b/app/static/js/groups_view.js index 5c59018d..8daabe57 100644 --- a/app/static/js/groups_view.js +++ b/app/static/js/groups_view.js @@ -99,6 +99,13 @@ function toggle_visible_etuds() { // lien feuille excel: $("#lnk_feuille_saisie").attr("href", "feuille_saisie_notes?evaluation_id=" + evaluation_id + qargs); } + // Update champs form group_ids_str + let group_ids_str = Array.from( + document.querySelectorAll("#group_ids_sel option:checked") + ).map( + function (elem) { return elem.value; } + ).join(); + document.querySelectorAll("input.group_ids_str").forEach(elem => elem.value = group_ids_str); } $().ready(function () { diff --git a/app/static/js/jury_but.js b/app/static/js/jury_but.js index 1ef24c16..c53b223a 100644 --- a/app/static/js/jury_but.js +++ b/app/static/js/jury_but.js @@ -5,17 +5,38 @@ function enable_manual_codes(elt) { $(".jury_but select.manual").prop("disabled", !elt.checked); } -// changement menu code: +// changement d'un menu code: function change_menu_code(elt) { - elt.parentElement.parentElement.classList.remove("recorded"); - // TODO: comparer avec valeur enregistrée (à mettre en data-orig ?) - // et colorer en fonction - elt.parentElement.parentElement.classList.add("modified"); + // Ajuste styles pour visualiser codes enregistrés/modifiés + if (elt.value != elt.dataset.orig_code) { + elt.parentElement.parentElement.classList.add("modified"); + } else { + elt.parentElement.parentElement.classList.remove("modified"); + } + if (elt.value == elt.dataset.orig_recorded) { + elt.parentElement.parentElement.classList.add("recorded"); + } else { + elt.parentElement.parentElement.classList.remove("recorded"); + } + // Si RCUE passant en ADJ, change les menus des UEs associées ADJR + if (elt.classList.contains("code_rcue") + && elt.dataset.niveau_id + && elt.value == "ADJ" + && elt.value != elt.dataset.orig_recorded) { + let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll( + "select.ue_rcue_" + elt.dataset.niveau_id); + ue_selects.forEach(select => { + if (select.value != "ADM") { + select.value = "ADJR"; + change_menu_code(select); // pour changer les styles + } + }); + } } $(function () { // Recupère la liste ordonnées des etudids - // pour avoir le "suivant" etr le "précédent" + // pour avoir le "suivant" et le "précédent" // (liens de navigation) const url = new URL(document.URL); const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid @@ -57,6 +78,42 @@ $(function () { } else { document.querySelector("div.next").innerHTML = ""; } - + } else { + // Supprime les liens de navigation + document.querySelector("div.prev").innerHTML = ""; + document.querySelector("div.next").innerHTML = ""; } -}); \ No newline at end of file +}); + +// ----- Etat du formulaire jury pour éviter sortie sans enregistrer +let FORM_STATE = ""; +let IS_SUBMITTING = false; + +// Une chaine décrivant l'état du form +function get_form_state() { + let codes = []; + // il n'y a que des - Modifier les partitions et groupes - tout s'enregistre automatiquement dès qu'il y a modification + + Modifier les partitions et groupes -
    -
    -

    Afficher les partitions

    -
    -
    -
    -

    - Afficher les étudiants affectés aux groupes
    - Ne s'actualise pas automatiquement lors d'une modification -

    -
    -
    +
    +
    +
    Ajouter une partition
    +
    +

    Etudiants

    +
    + Affecter automatiquement les étudiants du groupe
    + + vers le groupe + +
    Valider
    +
    @@ -55,7 +55,9 @@ processDatas(partitions, etudiants); processEvents(); + listeGroupesAutoaffectation(); + document.querySelector("body").classList.add("loaded"); document.querySelector('.wait').style.display = "none"; } @@ -73,22 +75,25 @@ function processDatas(partitions, etudiants) { /* Filtres et groupes */ - let outputPartitions = "
    "; - let outputMasques = ""; + let divFiltres = document.querySelector(".filtres"); let outputGroupes = ""; let arrayPartitions = Object.values(partitions).sort((a, b) => { return a.numero - b.numero; }) arrayPartitions.forEach((partition) => { - // Filtres - if (partition.groups_editable) { - outputPartitions += `
    ||${partition.partition_name}✏️
    `; - } else { - outputPartitions += `
    ${partition.partition_name}
    `; - } - outputMasques += `
    Non affectés - ${partition.partition_name}
    `; + let divPartition = templateFiltres_partition(partition); + divFiltres.appendChild(divPartition); + + let arrayGroups = Object.values(partition.groups).sort((a, b) => { + return a.numero - b.numero; + }) + + arrayGroups.forEach((groupe) => { + let divPlus = divPartition.querySelector(".ajoutGroupe"); + divPlus.parentElement.insertBefore(templateFiltres_groupe(groupe), divPlus); + }) // Groupes outputGroupes += ` @@ -104,35 +109,21 @@ }) let output = ""; arrayGroups.forEach((groupe) => { - /***************/ - if (partition.groups_editable) { - outputMasques += `
    ||${groupe.group_name}✏️
    `; - } else { - outputMasques += `
    ${groupe.group_name}
    `; - } - /***************/ output += templateGroupe_zoneGroupes(groupe.id, groupe.group_name); }) return output; })()}
    `; - outputMasques += ` -
    +
    -
    `; }) - document.querySelector(".filtres>.partitions>div").innerHTML = outputPartitions + ` -
    +
    -
    `; - document.querySelector(".filtres>.masques>div").innerHTML = outputMasques; - document.querySelector("#zoneGroupes>.groupes").innerHTML = outputGroupes; + document.querySelector("#zoneGroupes>.groupes").innerHTML = outputGroupes; /* Etudiants */ output = ""; etudiants.forEach(etudiant => { output += `
    -
    ${etudiant.nom_disp} ${etudiant.prenom}
    ${etudiant.bac}
    +
    ${etudiant.nom_disp} ${etudiant.prenom}
    ${etudiant.bac}
    ${(() => { let output = "
    "; arrayPartitions.forEach((partition) => { @@ -168,6 +159,58 @@ document.querySelector("#zoneChoix>.etudiants").innerHTML = output; } + function templateFiltres_partition(partition) { + let div = document.createElement("div"); + div.dataset.idpartition = partition.id; + if (partition.groups_editable == false) { + div.classList.add("nonEditable"); + } + div.innerHTML = ` + +

    + || + ${partition.partition_name} + ✏️ + + +
    Masquer
    +

    + + +
    +
    + Non affectés +
    +
    +
    +
    `; + + div.querySelector(".move").addEventListener("mousedown", moveStart); + div.querySelector(".modif").addEventListener("click", editText); + div.querySelector(".suppr").addEventListener("click", suppr); + div.querySelector(".onoff").addEventListener("click", masquerPartitions); + div.querySelector("[data-idgroupe]").addEventListener("click", filtre); + div.querySelector(".ajoutGroupe").addEventListener("click", addGroupe); + + return div; + } + + function templateFiltres_groupe(groupe) { + let div = document.createElement("div"); + div.dataset.idgroupe = groupe.id; + div.innerHTML = ` + || + ${groupe.group_name} + ✏️ + `; + + div.addEventListener("click", filtre); + div.querySelector(".move").addEventListener("mousedown", moveStart); + div.querySelector(".modif").addEventListener("click", editText); + div.querySelector(".suppr").addEventListener("click", suppr); + + return div; + } + function templateGroupe_zoneGroupes(idGroupe, name) { return `
    ${name}
    @@ -179,6 +222,26 @@ return `
    ${etudiant.nom_disp} ${etudiant.prenom}
    ` } + function listeGroupesAutoaffectation() { + let output = ''; + + document.querySelectorAll('#zonePartitions .filtres>div').forEach(partition => { + + output += ` + + `; + partition.querySelectorAll('[data-idgroupe]:not([data-idgroupe="aucun"])').forEach(groupe => { + output += ``; + }) + output += ""; + + }) + + document.querySelector("#affectationFrom").innerHTML = output; + document.querySelector("#affectationTo").innerHTML = output; + + } + /******************************/ /* Gestionnaire d'événements */ /******************************/ @@ -187,11 +250,11 @@ if (!editing) { document.querySelector("body").classList.toggle("editionActivated"); return; - } + } this.checked = true; - if(!editing.classList.contains("highlight")) { + if (!editing.classList.contains("highlight")) { editing.classList.add("highlight"); - setTimeout(()=>{editing.classList.remove("highlight")}, 1000); + setTimeout(() => { editing.classList.remove("highlight") }, 1000); } } function processEvents() { @@ -199,26 +262,25 @@ /* Edition partitions */ /*--------------------*/ document.querySelector(".edition>input").addEventListener("input", setEditMode); - document.querySelectorAll(".ajoutPartition, .ajoutGroupe").forEach(btnPlus => { btnPlus.addEventListener("click", addPartition) }) - document.querySelectorAll(".modif").forEach(btn => { btn.addEventListener("click", editText) }) - document.querySelectorAll(".suppr").forEach(btn => { btn.addEventListener("click", suppr) }) - document.querySelectorAll(".move").forEach(btn => { btn.addEventListener("mousedown", moveStart) }) - - /*---------*/ - /* Filtres */ - /*---------*/ - document.querySelectorAll(".filtres>div>div>div>div:not(.editing)").forEach(btn => { btn.addEventListener("click", filtre) }) + document.querySelectorAll(".ajoutPartition").forEach(btnPlus => { btnPlus.addEventListener("click", addPartition) }) /*--------------------*/ /* Changement groupe */ /*--------------------*/ document.querySelectorAll("label").forEach(btn => { btn.addEventListener("mousedown", (event) => { event.preventDefault() }) }); document.querySelectorAll(".etudiants input").forEach(input => { input.addEventListener("input", assignment) }) + document.querySelector(".affectationGo").addEventListener("click", affectationGo); } /**********************/ /* Filtrage */ /**********************/ + function masquerPartitions() { + let idPartition = this.closest("[data-idpartition]").dataset.idpartition; + document.querySelectorAll(`[data-idpartition="${idPartition}"]`).forEach(e => { + e.classList.toggle("hide"); + }) + } function filtre() { if (document.querySelector("body").classList.contains("editionActivated")) { return; @@ -241,55 +303,68 @@ }) } - if (!this.dataset.idgroupe) { - // Partitions - let groupesSelected = []; - this.parentElement.querySelectorAll(":not(.unselect)").forEach(e => { - groupesSelected.push(e.dataset.idpartition); - }) - document.querySelectorAll(` - .etudiants .partition[data-idpartition], - #zoneGroupes [data-idpartition] - `).forEach(e => { - if (groupesSelected.includes(e.dataset.idpartition)) { - e.classList.remove("hide") - } else { - e.classList.add("hide") - } - }) - } else { - // Groupes - let groupesSelected = {}; + // Groupes + let groupesSelected = {}; - this.parentElement.parentElement.querySelectorAll("[data-idgroupe]:not(.unselect)").forEach(e => { - let idpartition = e.parentElement.dataset.idpartition; - if (!groupesSelected[idpartition]) { - groupesSelected[idpartition] = []; + document.querySelectorAll(".filtres [data-idgroupe]:not(.unselect)").forEach(e => { + let idpartition = e.closest("[data-idpartition]").dataset.idpartition; + if (!groupesSelected[idpartition]) { + groupesSelected[idpartition] = []; + } + groupesSelected[idpartition].push(e.dataset.idgroupe) + }) + document.querySelectorAll("#zoneChoix .etudiants>div").forEach(e => { + let found = true; + Object.entries(groupesSelected).forEach(([idpartition, tabGroupes]) => { + if (!tabGroupes.includes( + e.querySelector(`[data-idpartition="${idpartition}"] input:checked`).value + ) + ) { + found = false } - groupesSelected[idpartition].push(e.dataset.idgroupe) }) - document.querySelectorAll("#zoneChoix .etudiants>div").forEach(e => { - let found = true; - Object.entries(groupesSelected).forEach(([idpartition, tabGroupes]) => { - if (!tabGroupes.includes( - e.querySelector(`[data-idpartition="${idpartition}"] input:checked`).value - ) - ) { - found = false - } - }) - if (found) { - e.classList.remove("hide") - } else { - e.classList.add("hide") - } - }) - } + if (found) { + e.classList.remove("hide") + } else { + e.classList.add("hide") + } + }) } /****************************/ /* Affectation à un groupe */ /****************************/ + function affectationGo(){ + let from = document.querySelector("#affectationFrom").value; + let to = document.querySelector("#affectationTo").value; + + if(!from || !to){ + return; + } + + let elements = []; + + if(from[0] != "n"){ + elements = document.querySelectorAll(`#zoneChoix .etudiants [value="${from}"]:checked`) + } else { + document.querySelectorAll(`#zoneChoix .etudiants [data-idpartition="${from.split("-")[1]}"]`).forEach(element=>{ + if(!element.querySelector('input:not([value="aucun"]):checked')){ + elements.push(element); + } + }) + } + + console.log(elements); + + elements.forEach(groupeSelected=>{ + if(to[0] != "n"){ + groupeSelected.closest(".grpPartitions").querySelector(`[value="${to}"]`).click(); + }else{ + groupeSelected.closest(".grpPartitions").querySelector(".aucun").click(); + } + + }) + } function assignment() { let groupe = this.parentElement.parentElement.parentElement.parentElement; let nom = groupe.children[0].dataset.nom; @@ -346,32 +421,12 @@ /****************************/ function addPartition() { let date = new Date; - if (this.classList.contains("ajoutPartition")) { - // Partition - var name = "Nouvelle " + date.getSeconds(); - let params = (new URL(document.location)).searchParams; - let formsemestre_id = params.get('formsemestre_id'); - var url = "/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/" + formsemestre_id + "/partition/create"; - var payload = { partition_name: name }; - } else { - // Groupe - var name = "Nouveau " + date.getSeconds(); - var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/partition/${this.parentElement.dataset.idpartition}/group/create`; - var payload = { group_name: name }; - } - var div = document.createElement("div"); - div.innerHTML = ` - || - ${name} - ✏️ - `; - div.querySelector(".modif").addEventListener("click", editText); - div.querySelector(".suppr").addEventListener("click", suppr); - div.querySelector(".move").addEventListener("mousedown", moveStart); - this.parentElement.insertBefore(div, this); - - div.querySelector(".modif").click(); + var name = "Nouvelle " + date.getSeconds(); + let params = (new URL(document.location)).searchParams; + let formsemestre_id = params.get('formsemestre_id'); + var url = "/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/" + formsemestre_id + "/partition/create"; + var payload = { partition_name: name }; // Save fetch(url, @@ -390,76 +445,110 @@ div.remove(); return; } - if (this.classList.contains("ajoutPartition")) { - div.dataset.idpartition = r.id; - // Ajout dans la zone masques - div = document.createElement("div"); - div.dataset.idpartition = r.id; - div.innerHTML = ` -
    Non affectés - ${name}
    -
    +
    `; - - div.querySelector("div").addEventListener("click", filtre); - div.querySelector(".ajoutGroupe").addEventListener("click", addPartition); - - document.querySelector("#zonePartitions .masques>div").appendChild(div); - - // Ajout de la zone pour chaque étudiant - let outputGroupes = ""; - - document.querySelectorAll("#zoneChoix .grpPartitions").forEach(e => { - let etudid = e.previousElementSibling.dataset.etudid; - - // Préparation pour la section suivante - let etudiant = { - etudid: etudid, - nom_disp: e.previousElementSibling.dataset.nom, - prenom: e.previousElementSibling.dataset.prenom - } - outputGroupes += templateEtudiant_zoneGroupes(etudiant); - //////////////////////// - - let div = document.createElement("div"); - div.className = "partition"; - div.dataset.idpartition = r.id; - div.innerHTML = ` -
    ${name}
    - - `; - div.querySelector("input").addEventListener("input", assignment); - e.appendChild(div); - }); - - // Ajout de la zone groupes - document.querySelector("#zoneGroupes>.groupes").innerHTML += ` -
    -

    ${name}

    -
    -
    Non affecté(s)
    -
    ${outputGroupes}
    -
    -
    `; - } else { - div.dataset.idgroupe = r.id; - - // Ajout du bouton pour chaque étudiant - let idpartition = this.parentElement.dataset.idpartition; - document.querySelectorAll(`#zoneChoix .etudiants [data-idpartition="${idpartition}"]`).forEach(e => { - let etudid = e.parentElement.parentElement.dataset.etudid; - let label = document.createElement("label"); - label.innerHTML = `${name}`; - label.querySelector("input").addEventListener("input", assignment); - e.appendChild(label); - }) - - // Ajout du groupe dans la zone Groupes - document.querySelector(`#zoneGroupes .partition[data-idpartition="${idpartition}"]`).innerHTML += templateGroupe_zoneGroupes(r.id, name); + // Ajout dans la zone filtres + let partition = { + id: r.id, + partition_name: name } + let divPartition = templateFiltres_partition(partition); + document.querySelector("#zonePartitions .filtres").appendChild(divPartition); + divPartition.querySelector(".modif").click(); + + // Ajout de la zone pour chaque étudiant + let outputGroupes = ""; + + document.querySelectorAll("#zoneChoix .grpPartitions").forEach(e => { + let etudid = e.previousElementSibling.dataset.etudid; + + // Préparation pour la section suivante + let etudiant = { + etudid: etudid, + nom_disp: e.previousElementSibling.dataset.nom, + prenom: e.previousElementSibling.dataset.prenom + } + outputGroupes += templateEtudiant_zoneGroupes(etudiant); + //////////////////////// + + let div = document.createElement("div"); + div.className = "partition"; + div.dataset.idpartition = r.id; + div.innerHTML = ` +
    ${name}
    + + `; + div.querySelector("input").addEventListener("input", assignment); + e.appendChild(div); + }); + + // Ajout de la zone groupes + document.querySelector("#zoneGroupes>.groupes").innerHTML += ` +
    +

    ${name}

    +
    +
    Non affecté(s)
    +
    ${outputGroupes}
    +
    +
    `; + + listeGroupesAutoaffectation(); + }) + .catch(error => { + document.querySelector("main").innerHTML = "

    Une erreur s'est produite lors de la sauvegarde des données.

    "; + }) + } + + function addGroupe() { + let date = new Date; + // Groupe + var name = "Nouveau " + date.getSeconds(); + let idPartition = this.parentElement.previousElementSibling.dataset.idpartition; + var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/partition/${idPartition}/group/create`; + var payload = { group_name: name }; + + fetch(url, + { + method: "POST", + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + .then(r => { return r.json() }) + .then(r => { + if (r.message == "invalid partition_name" || r.message == "invalid group_name") { + message("Le nom " + name + " existe déjà"); + return; + } + + let groupe = { + id: r.id, + group_name: name + } + let divGroupe = templateFiltres_groupe(groupe); + this.parentElement.insertBefore(divGroupe, this); + + // Ajout du bouton pour chaque étudiant + document.querySelectorAll(`#zoneChoix .etudiants [data-idpartition="${idPartition}"]`).forEach(e => { + let etudid = e.parentElement.previousElementSibling.dataset.etudid; + let label = document.createElement("label"); + label.innerHTML = `${name}`; + label.querySelector("input").addEventListener("input", assignment); + e.appendChild(label); + }) + + // Ajout du groupe dans la zone Groupes + document.querySelector(`#zoneGroupes .partition[data-idpartition="${idPartition}"]`).innerHTML += templateGroupe_zoneGroupes(r.id, name); + + // Lancement de l'édition du nom + divGroupe.querySelector(".modif").click(); + + listeGroupesAutoaffectation(); }) .catch(error => { document.querySelector("main").innerHTML = "

    Une erreur s'est produite lors de la sauvegarde des données.

    "; @@ -474,6 +563,7 @@ e.classList.add("editingText"); e.setAttribute("contenteditable", "true"); e.addEventListener("keydown", writing); + e.addEventListener("focusout", () => { saveEditing(this.previousElementSibling) }); // On sélectionne la zone const range = document.createRange(); @@ -506,7 +596,6 @@ var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/partition/${obj.parentElement.dataset.idpartition}/edit`; var payload = { partition_name: obj.innerText } - document.querySelector(`.masques [data-idpartition="${obj.parentElement.dataset.idpartition}"][data-idgroupe="aucun"]`).innerText = "Non affectés - " + obj.innerText; document.querySelectorAll(`#zoneChoix .etudiants [data-idpartition="${obj.parentElement.dataset.idpartition}"]>div`).forEach(e => { e.innerText = obj.innerText }); document.querySelector(`#zoneGroupes [data-idpartition="${obj.parentElement.dataset.idpartition}"]>h3`).innerText = obj.innerText; } else { @@ -531,6 +620,7 @@ if (!r) { document.querySelector("main").innerHTML = "

    Une erreur s'est produite lors de la sauvegarde des données.

    "; } + listeGroupesAutoaffectation(); }) .catch(error => { document.querySelector("main").innerHTML = "

    Une erreur s'est produite lors de la sauvegarde des données.

    "; @@ -586,6 +676,7 @@ if (r.OK != true) { document.querySelector("main").innerHTML = "

    Une erreur s'est produite lors de la sauvegarde des données.

    "; } + listeGroupesAutoaffectation(); }) } @@ -600,14 +691,21 @@ function moveStart(event) { moveData.x = event.pageX; moveData.y = event.pageY; - moveData.element = this.parentElement; + if (this.parentElement.dataset.idpartition) { + moveData.element = this.parentElement.parentElement; + } else { + moveData.element = this.parentElement; + } moveData.element.classList.add("moving"); moveData.element.parentElement.classList.add('grabbing'); document.body.addEventListener("mousemove", move); - moveData.element.parentElement.querySelectorAll("div:not([data-idgroupe=aucun])").forEach(e => { - e.addEventListener("mouseup", newPosition) - }) document.body.addEventListener("mouseup", moveEnd); + Array.from(moveData.element.parentElement.children).forEach(e => { + if ((e.dataset.idpartition && e.classname != "nonEditable") || + (e.dataset.idgroupe != "aucun")) { + e.addEventListener("mouseup", newPosition) + } + }) } function move(event) { @@ -621,24 +719,28 @@ moveData.element.parentElement.classList.remove('grabbing'); moveData.element.style.transform = ""; moveData.element.classList.remove("moving"); - moveData.element.parentElement.querySelectorAll("div:not([data-idgroupe=aucun])").forEach(e => { - e.removeEventListener("mouseup", newPosition) + Array.from(moveData.element.parentElement.children).forEach(e => { + if ((e.dataset.idpartition && e.classname != "nonEditable") || + (e.dataset.idgroupe && e.dataset.idgroupe != "aucun")) { + e.removeEventListener("mouseup", newPosition) + } }) moveData = {}; } - function newPosition() { + function newPosition(event) { moveData.element.parentElement.insertBefore(moveData.element, this); let positions = []; Array.from(moveData.element.parentElement.children).forEach(e => { - if ((e.dataset.idpartition && e.dataset.idgroupe != "aucun") || (e.dataset.idgroupe && e.dataset.idgroupe != "aucun")) { - positions.push(parseInt(e.dataset.idgroupe || e.dataset.idpartition)) + if ((e.dataset.idpartition && e.classname != "nonEditable") || + (e.dataset.idgroupe && e.dataset.idgroupe != "aucun")) { + positions.push(parseInt(e.dataset.idpartition || e.dataset.idgroupe)) } }) // Save positions - if (this.parentElement.parentElement.parentElement.className == "partitions") { + if (this.dataset.idpartition) { let params = (new URL(document.location)).searchParams; let formsemestre_id = params.get('formsemestre_id'); var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/${formsemestre_id}/partitions/order`; @@ -659,14 +761,15 @@ }) }) } else { - var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/partition/${this.parentElement.dataset.idpartition}/groups/order`; + let idPartition = this.closest("[data-idpartition]").dataset.idpartition; + var url = `/ScoDoc/{{formsemestre.departement.acronym}}/api/partition/${idPartition}/groups/order`; - document.querySelectorAll(`#zoneChoix .etudiants .partition[data-idpartition="${this.parentElement.dataset.idpartition}"]`).forEach(partition => { + document.querySelectorAll(`#zoneChoix .etudiants .partition[data-idpartition="${idPartition}"]`).forEach(partition => { positions.forEach(position => { partition.append(partition.querySelector(`[value="${position}"]`).parentElement) }) }) - document.querySelectorAll(`#zoneGroupes .partition[data-idpartition="${this.parentElement.dataset.idpartition}"]`).forEach(partition => { + document.querySelectorAll(`#zoneGroupes .partition[data-idpartition="${idPartition}"]`).forEach(partition => { positions.forEach(position => { partition.append(partition.querySelector(`[data-idgroupe="${position}"]`)) }) @@ -687,11 +790,11 @@ if (!r) { document.querySelector("main").innerHTML = "

    Une erreur s'est produite lors de la sauvegarde des données.

    "; } + listeGroupesAutoaffectation(); }) .catch(error => { document.querySelector("main").innerHTML = "

    Une erreur s'est produite lors de la sauvegarde des données.

    "; }) - } /*************************/ diff --git a/app/views/absences.py b/app/views/absences.py index 9e4d5b87..c65a2d49 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -3,7 +3,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -715,7 +715,10 @@ def _gen_form_saisie_groupe( jn = sco_abs.day_names() for d in odates: idx_jour = d.weekday() - noms_jours.append(jn[idx_jour]) + if 0 <= idx_jour < len(jn): + noms_jours.append(jn[idx_jour]) + else: + noms_jours.append("???") # jour non travaillé for jour in noms_jours: H.append( f""" diff --git a/app/views/notes.py b/app/views/notes.py index 6ba9bf92..16bc0261 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -50,7 +50,7 @@ from app.but import jury_but_view from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import ScolarNews +from app.models import ScolarNews, Scolog from app.models.but_refcomp import ApcNiveau, ApcParcours from app.models.config import ScoDocSiteConfig from app.models.etudiants import Identite @@ -272,7 +272,8 @@ def formsemestre_bulletinetud( code_ine=None, ): format = format or "html" - + if not isinstance(etudid, int): + raise ScoInvalidIdType("formsemestre_bulletinetud: etudid must be an integer !") if formsemestre_id is not None and not isinstance(formsemestre_id, int): raise ScoInvalidIdType( "formsemestre_bulletinetud: formsemestre_id must be an integer !" @@ -366,13 +367,13 @@ sco_publish( Permission.ScoView, ) sco_publish( - "/module_evaluation_renumber", - sco_evaluation_db.module_evaluation_renumber, + "/moduleimpl_evaluation_renumber", + sco_evaluation_db.moduleimpl_evaluation_renumber, Permission.ScoView, ) sco_publish( - "/module_evaluation_move", - sco_evaluation_db.module_evaluation_move, + "/moduleimpl_evaluation_move", + sco_evaluation_db.moduleimpl_evaluation_move, Permission.ScoView, ) sco_publish( @@ -624,13 +625,14 @@ def index_html(): if editable: H.append( f""" -

    Une "formation" est un programme pédagogique structuré +

    Une "formation" est un programme pédagogique structuré en UE, matières et modules. Chaque semestre se réfère à une formation. - La modification d'une formation affecte tous les semestres qui s'y + La modification d'une formation affecte tous les semestres qui s'y réfèrent.

    - - """ + """ ) H.append(html_sco_header.sco_footer()) @@ -703,10 +704,15 @@ def formation_list(format=None, formation_id=None, args={}): @scodoc @permission_required(Permission.ScoView) @scodoc7func -def formation_export(formation_id, export_ids=False, format=None): +def formation_export( + formation_id, export_ids=False, format=None, export_codes_apo=True +): "Export de la formation au format indiqué (xml ou json)" return sco_formations.formation_export( - formation_id, export_ids=export_ids, format=format + formation_id, + export_ids=export_ids, + format=format, + export_codes_apo=export_codes_apo, ) @@ -855,7 +861,7 @@ def formsemestre_change_lock(formsemestre_id, dialog_confirmed=False): else: msg = "verrouillage" return scu.confirm_dialog( - "

    Confirmer le %s du semestre ?

    " % msg, + f"

    Confirmer le {msg} du semestre ?

    ", helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées. Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment (par son responsable ou un administrateur). @@ -863,7 +869,11 @@ def formsemestre_change_lock(formsemestre_id, dialog_confirmed=False): Le programme d'une formation qui a un semestre verrouillé ne peut plus être modifié. """, dest_url="", - cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id, + cancel_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ), parameters={"formsemestre_id": formsemestre_id}, ) @@ -940,21 +950,24 @@ def edit_enseignants_form(moduleimpl_id): H.append( f"""
  • {nom} (supprimer)
  • """ ) H.append("") - F = """

    Les enseignants d'un module ont le droit de + F = f"""

    Les enseignants d'un module ont le droit de saisir et modifier toutes les notes des évaluations de ce module.

    Pour changer le responsable du module, passez par la - page "Modification du semestre", accessible uniquement au responsable de la formation (chef de département) + page "Modification du semestre", + accessible uniquement au responsable de la formation (chef de département)

    - """ % ( - sem["formation_id"], - M["formsemestre_id"], - ) + """ modform = [ ("moduleimpl_id", {"input_type": "hidden"}), @@ -1009,14 +1022,18 @@ def edit_enseignants_form(moduleimpl_id): or ens_id == M["responsable_id"] ): H.append( - '

    Enseignant %s déjà dans la liste !

    ' % ens_id + f"""

    Enseignant {ens_id} déjà dans la liste !

    """ ) else: sco_moduleimpl.do_ens_create( {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id} ) return flask.redirect( - "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id + url_for( + "notes.edit_enseignants_form", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=moduleimpl_id, + ) ) return header + "\n".join(H) + tf[1] + F + footer @@ -1372,12 +1389,12 @@ def formsemestre_enseignants_list(formsemestre_id, format="html"): if not u: continue cursor.execute( - """SELECT * FROM scolog L, notes_formsemestre_inscription I - WHERE method = 'AddAbsence' - and authenticated_user = %(authenticated_user)s - and L.etudid = I.etudid - and I.formsemestre_id = %(formsemestre_id)s - and date > %(date_debut)s + """SELECT * FROM scolog L, notes_formsemestre_inscription I + WHERE method = 'AddAbsence' + and authenticated_user = %(authenticated_user)s + and L.etudid = I.etudid + and I.formsemestre_id = %(formsemestre_id)s + and date > %(date_debut)s and date < %(date_fin)s """, { @@ -1448,15 +1465,21 @@ def edit_enseignants_form_delete(moduleimpl_id, ens_id: int): ok = True break if not ok: - raise ScoValueError("invalid ens_id (%s)" % ens_id) + raise ScoValueError(f"invalid ens_id ({ens_id})") ndb.SimpleQuery( """DELETE FROM notes_modules_enseignants - WHERE moduleimpl_id = %(moduleimpl_id)s + WHERE moduleimpl_id = %(moduleimpl_id)s AND ens_id = %(ens_id)s """, {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id}, ) - return flask.redirect("edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id) + return flask.redirect( + url_for( + "notes.edit_enseignants_form", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=moduleimpl_id, + ) + ) # --- Gestion des inscriptions aux semestres @@ -1583,10 +1606,12 @@ sco_publish( ) -@bp.route("/etud_desinscrit_ue") +@bp.route( + "/etud_desinscrit_ue///", + methods=["GET", "POST"], +) @scodoc @permission_required(Permission.ScoEtudInscrit) -@scodoc7func def etud_desinscrit_ue(etudid, formsemestre_id, ue_id): """ - En classique: désinscrit l'etudiant de tous les modules de cette UE dans ce semestre. @@ -1596,10 +1621,24 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id): ue = UniteEns.query.get_or_404(ue_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if ue.formation.is_apc(): - if DispenseUE.query.filter_by(etudid=etudid, ue_id=ue_id).count() == 0: - disp = DispenseUE(ue_id=ue_id, etudid=etudid) + if ( + DispenseUE.query.filter_by( + formsemestre_id=formsemestre_id, etudid=etudid, ue_id=ue_id + ).count() + == 0 + ): + disp = DispenseUE( + formsemestre_id=formsemestre_id, ue_id=ue_id, etudid=etudid + ) db.session.add(disp) db.session.commit() + log(f"etud_desinscrit_ue {etud} {ue}") + Scolog.logdb( + method="etud_desinscrit_ue", + etudid=etud.id, + msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}", + commit=True, + ) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) else: sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic( @@ -1615,10 +1654,12 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id): ) -@bp.route("/etud_inscrit_ue") +@bp.route( + "/etud_inscrit_ue///", + methods=["GET", "POST"], +) @scodoc @permission_required(Permission.ScoEtudInscrit) -@scodoc7func def etud_inscrit_ue(etudid, formsemestre_id, ue_id): """ En classic: inscrit l'étudiant à tous les modules de cette UE dans ce semestre. @@ -1630,8 +1671,17 @@ def etud_inscrit_ue(etudid, formsemestre_id, ue_id): etud = Identite.query.get_or_404(etudid) ue = UniteEns.query.get_or_404(ue_id) if ue.formation.is_apc(): - for disp in DispenseUE.query.filter_by(etudid=etud.id, ue_id=ue_id): + for disp in DispenseUE.query.filter_by( + formsemestre_id=formsemestre_id, etudid=etud.id, ue_id=ue_id + ): db.session.delete(disp) + log(f"etud_inscrit_ue {etud} {ue}") + Scolog.logdb( + method="etud_inscrit_ue", + etudid=etud.id, + msg=f"Inscription à l'UE {ue.acronyme} de {formsemestre.titre_annee()}", + commit=True, + ) db.session.commit() sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) else: @@ -2305,30 +2355,38 @@ def formsemestre_validation_but( etud: Identite = Identite.query.filter_by( id=etudid, dept_id=g.scodoc_dept_id ).first_or_404() - + nb_etuds = formsemestre.etuds.count() # la route ne donne pas le type d'etudid pour pouvoir construire des URLs # provisoires avec NEXT et PREV try: etudid = int(etudid) - except: + except ValueError: abort(404, "invalid etudid") read_only = not sco_permissions_check.can_validate_sem(formsemestre_id) # --- Navigation - prev = f"""{scu.EMO_PREV_ARROW} précédent """ - next = f"""suivant {scu.EMO_NEXT_ARROW} """ + if nb_etuds > 1 + else "" + ) navigation_div = f"""
    """ H = [ html_sco_header.sco_header( - page_title="Validation BUT", + page_title=f"Validation BUT S{formsemestre.semestre_id}", formsemestre_id=formsemestre_id, etudid=etudid, cssstyles=("css/jury_but.css",), javascripts=("js/jury_but.js",), ), - f""" -
    + """
    """, ] @@ -2384,7 +2441,6 @@ def formsemestre_validation_but( deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) if len(deca.rcues_annee) == 0: - # raise ScoValueError("année incomplète: pas de jury BUT annuel possible") return jury_but_view.jury_but_semestriel( formsemestre, etud, read_only, navigation_div=navigation_div ) @@ -2403,10 +2459,16 @@ def formsemestre_validation_but( warning = "" if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): - warning += f"""
    Attention: {len(deca.niveaux_competences)} - niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.
    """ - if deca.parcour is None: - warning += """
    L'étudiant n'est pas inscrit à un parcours.
    """ + if deca.a_cheval: + warning += """
    Attention: regroupements RCUE + entre années (redoublement).
    """ + else: + warning += f"""
    Attention: {len(deca.niveaux_competences)} + niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.
    """ + if (deca.parcour is None) and len(formsemestre.parcours) > 0: + warning += ( + """
    L'étudiant n'est pas inscrit à un parcours.
    """ + ) if deca.formsemestre_impair and deca.inscription_etat_impair != scu.INSCRIT: etat_ins = scu.ETATS_INSCRIPTION.get(deca.inscription_etat_impair, "inconnu?") warning += f"""
    {etat_ins} en S{deca.formsemestre_impair.semestre_id}""" @@ -2431,36 +2493,71 @@ def formsemestre_validation_but( {warning}
    -
    + """ ) H.append(jury_but_view.show_etud(deca, read_only=read_only)) + autorisations_idx = deca.get_autorisations_passage() + div_autorisations_passage = ( + f""" +
    + Autorisé à passer en : + { ", ".join( ["S" + str(i) for i in autorisations_idx ] )} +
    + """ + if autorisations_idx + else """
    + pas d'autorisations de passage enregistrées. +
    + """ + ) + H.append(div_autorisations_passage) + if read_only: H.append( - """
    - Vous n'avez pas la permission de modifier ces décisions. - Les champs entourés en vert sont enregistrés.
    """ + f""" +
    + {"Vous n'avez pas la permission de modifier ces décisions." + if formsemestre.etat + else "Semestre verrouillé."} + Les champs entourés en vert sont enregistrés. +
    """ ) else: + erase_span = f"""effacer décisions""" H.append( f"""
    - permettre la saisie manuelles des codes d'année et de niveaux. - Dans ce cas, il vous revient de vous assurer de la cohérence entre - vos codes d'UE/RCUE/Année ! + permettre la saisie manuelles des codes + {"d'année et " if deca.jury_annuel else ""} + de niveaux. + Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année ! +
    - + + {erase_span}
    """ ) H.append(navigation_div) H.append("
    ") - + if deca.a_cheval: + H.append( + f"""
    + {scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE capitalisées (note > 10) + lors d'une année précédente peuvent être prise en compte pour former + un RCUE (associé à un niveau de compétence du BUT). +
    + """ + ) H.append( render_template( "but/documentation_codes_jury.html", @@ -2494,10 +2591,10 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None): form = jury_but_forms.FormSemestreValidationAutoBUTForm() if request.method == "POST": if not form.cancel.data: - nb_admis = jury_but_validation_auto.formsemestre_validation_auto_but( + nb_etud_modif = jury_but_validation_auto.formsemestre_validation_auto_but( formsemestre ) - flash(f"Décisions enregistrées ({nb_admis} admis)") + flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)") return redirect( url_for( "notes.formsemestre_saisie_jury", @@ -2509,7 +2606,7 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None): "but/formsemestre_validation_auto_but.html", form=form, sco=ScoData(formsemestre=formsemestre), - title=f"Calcul automatique jury BUT", + title="Calcul automatique jury BUT", ) @@ -2587,7 +2684,17 @@ def formsemestre_validation_auto(formsemestre_id): message="

    Opération non autorisée pour %s" % current_user, dest_url=scu.ScoURL(), ) - + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id, dept_id=g.scodoc_dept_id + ).first_or_404() + if formsemestre.formation.is_apc(): + return redirect( + url_for( + "notes.formsemestre_validation_auto_but", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ) + ) return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id) @@ -2693,7 +2800,7 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None): """ read_only = not sco_permissions_check.can_validate_sem(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - if formsemestre.formation.is_apc(): # and formsemestre.semestre_id % 2 == 0: + if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0: return jury_but_recap.formsemestre_saisie_jury_but( formsemestre, read_only, selected_etudid=selected_etudid ) @@ -2764,10 +2871,13 @@ def formsemestre_jury_but_erase( return render_template( "confirm_dialog.html", title=f"Effacer les validations de jury de {etud.nomprenom} ?", - explanation=f"""Les validations d'UE et autorisations de passage + explanation=f"""Les validations d'UE et autorisations de passage du semestre S{formsemestre.semestre_id} seront effacées.""" if only_one_sem - else """Les validations de toutes les UE, RCUE (compétences) et année seront effacées.""", + else """Ses validations de toutes les UE, RCUE (compétences) et année + issues de cette année scolaire seront effacées. + Les décisions des années scolaires précédentes ne seront pas modifiées. + """, cancel_url=dest_url, ) @@ -3148,7 +3258,7 @@ def check_sem_integrity(formsemestre_id, fix=False): else: H.append( """ -

    Problème détecté réparable: +

    Problème détecté réparable: réparer maintenant

    """ % (formsemestre_id,) diff --git a/app/views/pn_modules.py b/app/views/pn_modules.py index feee40a7..6868b8bd 100644 --- a/app/views/pn_modules.py +++ b/app/views/pn_modules.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -216,7 +216,7 @@ def edit_modules_ue_coefs(): """, render_template( - "pn/form_modules_ue_coefs.html", + "pn/form_modules_ue_coefs.j2", formation=formation, data_source=url_for( "notes.table_modules_ue_coefs", diff --git a/app/views/refcomp.py b/app/views/refcomp.py index 4aad122f..86606de8 100644 --- a/app/views/refcomp.py +++ b/app/views/refcomp.py @@ -34,6 +34,7 @@ from app.views import ScoData @scodoc @permission_required(Permission.ScoView) def refcomp(refcomp_id): + """Le référentiel de compétences, en JSON.""" ref = ApcReferentielCompetences.query.get_or_404(refcomp_id) return jsonify(ref.to_dict()) @@ -42,6 +43,7 @@ def refcomp(refcomp_id): @scodoc @permission_required(Permission.ScoView) def refcomp_show(refcomp_id): + """Affichage du référentiel de compétences.""" ref = ApcReferentielCompetences.query.get_or_404(refcomp_id) return render_template( "but/refcomp_show.html", @@ -60,6 +62,7 @@ def refcomp_show(refcomp_id): @scodoc @permission_required(Permission.ScoChangeFormation) def refcomp_delete(refcomp_id): + """Suppression du référentiel de la base. Le fichier source n'est pas affecté.""" ref = ApcReferentielCompetences.query.get_or_404(refcomp_id) db.session.delete(ref) db.session.commit() diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 494cf3b9..184fcdd1 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/app/views/scolar.py b/app/views/scolar.py index b82b082a..54e53716 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -3,7 +3,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -935,7 +935,7 @@ def partition_editor(formsemestre_id: int): def create_partition_parcours(formsemestre_id): """Création d'une partitions nommée "Parcours" (PARTITION_PARCOURS) avec un groupe par parcours.""" - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre.setup_parcours_groups() return flask.redirect( url_for( diff --git a/app/views/users.py b/app/views/users.py index 8c726f1a..4ad30aeb 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -5,7 +5,7 @@ # # ScoDoc # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/config.py b/config.py index 57c0fb1f..740860a6 100755 --- a/config.py +++ b/config.py @@ -72,7 +72,7 @@ class DevConfig(Config): class TestConfig(DevConfig): "Pour les tests unitaires" TESTING = True - DEBUG = True + DEBUG = False SQLALCHEMY_DATABASE_URI = ( os.environ.get("SCODOC_TEST_DATABASE_URI") or "postgresql:///SCODOC_TEST" ) diff --git a/migrations/versions/25e3ca6cc063_dispenseue_par_semestre.py b/migrations/versions/25e3ca6cc063_dispenseue_par_semestre.py new file mode 100644 index 00000000..942ef77d --- /dev/null +++ b/migrations/versions/25e3ca6cc063_dispenseue_par_semestre.py @@ -0,0 +1,86 @@ +"""DispenseUE par semestre + +Revision ID: 25e3ca6cc063 +Revises: 7e5b519a27e1 +Create Date: 2023-01-13 17:19:49.431591 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.orm import sessionmaker # added by ev + + +# revision identifiers, used by Alembic. +revision = "25e3ca6cc063" +down_revision = "7e5b519a27e1" +branch_labels = None +depends_on = None + +Session = sessionmaker() + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "dispenseUE", sa.Column("formsemestre_id", sa.Integer(), nullable=True) + ) + op.drop_constraint("dispenseUE_ue_id_etudid_key", "dispenseUE", type_="unique") + op.create_index( + op.f("ix_dispenseUE_formsemestre_id"), + "dispenseUE", + ["formsemestre_id"], + unique=False, + ) + op.create_unique_constraint( + None, "dispenseUE", ["formsemestre_id", "ue_id", "etudid"] + ) + op.create_foreign_key( + None, "dispenseUE", "notes_formsemestre", ["formsemestre_id"], ["id"] + ) + # ### end Alembic commands ### + + # Affecte les dispenses au formsemestre le plus récent ayant cette UE + bind = op.get_bind() + session = Session(bind=bind) + dispenses = session.execute( + """SELECT id, ue_id, etudid FROM "dispenseUE" WHERE formsemestre_id IS NULL;""" + ).all() + for dispense_id, ue_id, etudid in dispenses: + formsemestre_ids = session.execute( + """ + SELECT notes_formsemestre.id + FROM notes_formsemestre, notes_formations, notes_ue, notes_formsemestre_inscription + WHERE notes_formsemestre.formation_id = notes_formations.id + and notes_ue.formation_id = notes_formations.id + and notes_ue.semestre_idx=notes_formsemestre.semestre_id + and notes_formsemestre_inscription.formsemestre_id=notes_formsemestre.id + and notes_ue.id = :ue_id + and notes_formsemestre_inscription.etudid = :etudid + ORDER BY notes_formsemestre.date_debut DESC + LIMIT 1; + """, + {"ue_id": ue_id, "etudid": etudid}, + ).all() + if formsemestre_ids: + formsemestre_id = formsemestre_ids[0][0] + session.execute( + """ + UPDATE "dispenseUE" SET formsemestre_id=:formsemestre_id WHERE id=:dispense_id""", + {"formsemestre_id": formsemestre_id, "dispense_id": dispense_id}, + ) + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + "dispenseUE_formsemestre_id_fkey", "dispenseUE", type_="foreignkey" + ) + op.drop_constraint( + "dispenseUE_formsemestre_id_ue_id_etudid_key", "dispenseUE", type_="unique" + ) + op.drop_index(op.f("ix_dispenseUE_formsemestre_id"), table_name="dispenseUE") + op.create_unique_constraint( + "dispenseUE_ue_id_etudid_key", "dispenseUE", ["ue_id", "etudid"] + ) + op.drop_column("dispenseUE", "formsemestre_id") + # ### end Alembic commands ### diff --git a/migrations/versions/530594458193_ajout_modeles_assiduites_justificatifs.py b/migrations/versions/530594458193_ajout_modeles_assiduites_justificatifs.py deleted file mode 100644 index fb36c80c..00000000 --- a/migrations/versions/530594458193_ajout_modeles_assiduites_justificatifs.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Ajout Modeles Assiduites Justificatifs - -Revision ID: 530594458193 -Revises: 3c12f5850cff -Create Date: 2022-12-19 13:56:28.597632 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "530594458193" -down_revision = "3c12f5850cff" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "justificatifs", - sa.Column("justifid", sa.Integer(), nullable=False), - sa.Column( - "date_debut", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column( - "date_fin", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column("etudid", sa.Integer(), nullable=False), - sa.Column("etat", sa.Integer(), nullable=True), - sa.Column("raison", sa.Text(), nullable=True), - sa.Column("fichier", sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("justifid"), - ) - op.create_index( - op.f("ix_justificatifs_etudid"), "justificatifs", ["etudid"], unique=False - ) - op.create_table( - "assiduites", - sa.Column("id", sa.Integer(), nullable=False), - sa.Column( - "date_debut", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column( - "date_fin", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column("moduleimpl_id", sa.Integer(), nullable=True), - sa.Column("etudid", sa.Integer(), nullable=False), - sa.Column("etat", sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint( - ["moduleimpl_id"], ["notes_moduleimpl.id"], ondelete="SET NULL" - ), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - op.f("ix_assiduites_etudid"), "assiduites", ["etudid"], unique=False - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f("ix_assiduites_etudid"), table_name="assiduites") - op.drop_table("assiduites") - op.drop_index(op.f("ix_justificatifs_etudid"), table_name="justificatifs") - op.drop_table("justificatifs") - # ### end Alembic commands ### diff --git a/migrations/versions/554c13cea377_formsemestre_not_nulls.py b/migrations/versions/554c13cea377_formsemestre_not_nulls.py new file mode 100644 index 00000000..a7cdc9e5 --- /dev/null +++ b/migrations/versions/554c13cea377_formsemestre_not_nulls.py @@ -0,0 +1,66 @@ +"""formsemestre_not_nulls + +Revision ID: 554c13cea377 +Revises: 3c12f5850cff +Create Date: 2023-01-09 08:02:53.637488 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "554c13cea377" +down_revision = "3c12f5850cff" +branch_labels = None +depends_on = None + + +def upgrade(): + # + op.execute("UPDATE notes_formsemestre SET titre = '' WHERE titre is NULL;") + op.alter_column( + "notes_formsemestre", "titre", existing_type=sa.TEXT(), nullable=False + ) + op.execute( + "UPDATE notes_formsemestre SET date_debut = now() WHERE date_debut is NULL;" + ) + op.alter_column( + "notes_formsemestre", "date_debut", existing_type=sa.DATE(), nullable=False + ) + op.execute("UPDATE notes_formsemestre SET date_fin = now() WHERE date_fin is NULL;") + op.alter_column( + "notes_formsemestre", "date_fin", existing_type=sa.DATE(), nullable=False + ) + op.execute( + "UPDATE notes_formsemestre SET bul_bgcolor = 'white' WHERE bul_bgcolor is NULL;" + ) + op.alter_column( + "notes_formsemestre", + "bul_bgcolor", + existing_type=sa.VARCHAR(length=32), + nullable=False, + existing_server_default=sa.text("'white'::character varying"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "notes_formsemestre", + "bul_bgcolor", + existing_type=sa.VARCHAR(length=32), + nullable=True, + existing_server_default=sa.text("'white'::character varying"), + ) + op.alter_column( + "notes_formsemestre", "date_fin", existing_type=sa.DATE(), nullable=True + ) + op.alter_column( + "notes_formsemestre", "date_debut", existing_type=sa.DATE(), nullable=True + ) + op.alter_column( + "notes_formsemestre", "titre", existing_type=sa.TEXT(), nullable=True + ) + # ### end Alembic commands ### diff --git a/migrations/versions/7e5b519a27e1_cascade_modimpls.py b/migrations/versions/7e5b519a27e1_cascade_modimpls.py new file mode 100644 index 00000000..42870d01 --- /dev/null +++ b/migrations/versions/7e5b519a27e1_cascade_modimpls.py @@ -0,0 +1,43 @@ +"""cascade_modimpls + +Revision ID: 7e5b519a27e1 +Revises: 554c13cea377 +Create Date: 2023-01-12 08:49:01.744120 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "7e5b519a27e1" +down_revision = "554c13cea377" +branch_labels = None +depends_on = None + + +def upgrade(): + # + op.execute("DELETE FROM notes_moduleimpl WHERE module_id is NULL;") + op.alter_column( + "notes_moduleimpl", "module_id", existing_type=sa.INTEGER(), nullable=False + ) + op.execute("DELETE FROM notes_moduleimpl WHERE formsemestre_id is NULL;") + op.alter_column( + "notes_moduleimpl", + "formsemestre_id", + existing_type=sa.INTEGER(), + nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "notes_moduleimpl", "formsemestre_id", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "notes_moduleimpl", "module_id", existing_type=sa.INTEGER(), nullable=True + ) + # ### end Alembic commands ### diff --git a/migrations/versions/943eb2deb22e_modèle_assiduites_et_justificatifs.py b/migrations/versions/943eb2deb22e_modèle_assiduites_et_justificatifs.py new file mode 100644 index 00000000..f15db40b --- /dev/null +++ b/migrations/versions/943eb2deb22e_modèle_assiduites_et_justificatifs.py @@ -0,0 +1,55 @@ +"""Modèle Assiduites et Justificatifs + +Revision ID: 943eb2deb22e +Revises: 25e3ca6cc063 +Create Date: 2023-01-30 14:08:47.214916 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '943eb2deb22e' +down_revision = '25e3ca6cc063' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('justificatifs', + sa.Column('justif_id', sa.Integer(), nullable=False), + sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('etudid', sa.Integer(), nullable=False), + sa.Column('etat', sa.Integer(), nullable=True), + sa.Column('raison', sa.Text(), nullable=True), + sa.Column('fichier', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('justif_id') + ) + op.create_index(op.f('ix_justificatifs_etudid'), 'justificatifs', ['etudid'], unique=False) + op.create_table('assiduites', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('moduleimpl_id', sa.Integer(), nullable=True), + sa.Column('etudid', sa.Integer(), nullable=False), + sa.Column('etat', sa.Integer(), nullable=False), + sa.Column('desc', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['moduleimpl_id'], ['notes_moduleimpl.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_assiduites_etudid'), 'assiduites', ['etudid'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_assiduites_etudid'), table_name='assiduites') + op.drop_table('assiduites') + op.drop_index(op.f('ix_justificatifs_etudid'), table_name='justificatifs') + op.drop_table('justificatifs') + # ### end Alembic commands ### diff --git a/misc/csv2rules.py b/misc/csv2rules.py index 4eb01c9d..df4ca162 100755 --- a/misc/csv2rules.py +++ b/misc/csv2rules.py @@ -6,7 +6,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/misc/geolocalize_lycees.py b/misc/geolocalize_lycees.py index fc4e63dd..ab16cd2d 100644 --- a/misc/geolocalize_lycees.py +++ b/misc/geolocalize_lycees.py @@ -5,7 +5,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/misc/sco_archive_exemple.py b/misc/sco_archive_exemple.py new file mode 100644 index 00000000..12184c7c --- /dev/null +++ b/misc/sco_archive_exemple.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""ScoDoc : gestion des fichiers archivés associés aux assiduités + Il s'agit de fichiers quelconques, xxx +""" + +# Exemple minimal pour @iziram + +from app.scodoc import sco_archives + + +class AbsArchiver(sco_archives.BaseArchiver): + def __init__(self): + sco_archives.BaseArchiver.__init__( + self, archive_type="xp_abs" + ) # <<< adapter le nom du type pour refléter l'usage + + +AbsArchive = AbsArchiver() + +# A partir d'ici, essais interactifs avec flask shell + +mapp.set_sco_dept("RT") +ctx.push() + +# supposons que l'id de l'archive soit l'étudid +# (cela pourrait être autre chose, comme un id d'assiduité ou de justif...) + +mon_id = 1 +AbsArchive.list_obj_archives(mon_id) + +""" +On voit que ScoDoc s'occupe de créer les répertoires côté serveur +[Mon Dec 26 19:38:11 2022] scodoc: (RT) initialized archiver, path=/opt/scodoc-data/archives/xp_abs +[Mon Dec 26 19:38:11 2022] scodoc: (RT) creating directory /opt/scodoc-data/archives/xp_abs +[Mon Dec 26 19:38:11 2022] scodoc: (RT) creating directory /opt/scodoc-data/archives/xp_abs/5 +[Mon Dec 26 19:38:11 2022] scodoc: (RT) creating directory /opt/scodoc-data/archives/xp_abs/5/1 +[] + +et a renvoyé une liste vide +""" + +archive_id = AbsArchive.create_obj_archive( + mon_id, "la description du truc que je veux archiver" +) + +data = b"un paquet de donnees a stocker" + +AbsArchive.store(archive_id, "toto", data) + +AbsArchive.store(archive_id, "../../tmp/titi", data + data) +# ici on remarque que le chemin a été vérifié et sécurisé + +# ------ Relecture: +archive_ids = AbsArchive.list_obj_archives(mon_id) +# -> ['/opt/scodoc-data/archives/xp_abs/5/1/2022-12-26-19-41-54'] + +assert archive_ids[0] == archive_id + +AbsArchive.get_archive_description(archive_id) + +# Liste le contenu: +names = AbsArchive.list_archive(archive_id) +assert ["....tmptiti", "toto"] == names + +# Obtient un objet de l'archive +archive_name = AbsArchive.get_archive_name(archive_id) +data2 = AbsArchive.get_archived_file(mon_id, archive_name, names[0]) + +# Et enfin pour effacer une archive: +AbsArchive.delete_archive(mon_id) diff --git a/pytest.ini b/pytest.ini index a73c5d60..d0da2ce1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,8 @@ [pytest] markers = slow: marks tests as slow (deselect with '-m "not slow"') + but_gb lemans + lyon + test_test + diff --git a/sco_version.py b/sco_version.py index d3d81902..91b265ad 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.7" +SCOVERSION = "9.4.34" SCONAME = "ScoDoc" diff --git a/scodoc.py b/scodoc.py index 52c49302..12556c2c 100755 --- a/scodoc.py +++ b/scodoc.py @@ -24,16 +24,24 @@ from app import clear_scodoc_cache from app import models from app.auth.models import User, Role, UserRole -from app.scodoc.sco_logos import make_logo_local -from app.models import departements +from app.entreprises.models import entreprises_reset_database +from app.models import Departement, departements from app.models import Formation, UniteEns, Matiere, Module from app.models import FormSemestre, FormSemestreInscription from app.models import GroupDescr from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models import Partition +from app.models import ScolarAutorisationInscription, ScolarFormSemestreValidation +from app.models.but_refcomp import ( + ApcCompetence, + ApcNiveau, + ApcParcours, + ApcReferentielCompetences, +) +from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE from app.models.evaluations import Evaluation -from app.entreprises.models import entreprises_reset_database +from app.scodoc.sco_logos import make_logo_local from app.scodoc.sco_permissions import Permission from app.views import notes, scolar import tools @@ -52,12 +60,20 @@ def make_shell_context(): import app as mapp # le package app from app.scodoc import notesdb as ndb from app.comp import res_sem + from app.comp.res_but import ResultatsSemestreBUT from app.scodoc import sco_utils as scu return { + "ApcCompetence": ApcCompetence, + "ApcNiveau": ApcNiveau, + "ApcParcours": ApcParcours, + "ApcReferentielCompetences": ApcReferentielCompetences, + "ApcValidationRCUE": ApcValidationRCUE, + "ApcValidationAnnee": ApcValidationAnnee, "ctx": app.test_request_context(), "current_app": flask.current_app, "current_user": current_user, + "Departement": Departement, "db": db, "Evaluation": Evaluation, "flask": flask, @@ -69,21 +85,24 @@ def make_shell_context(): "login_user": login_user, "logout_user": logout_user, "mapp": mapp, - "models": models, "Matiere": Matiere, + "models": models, "Module": Module, "ModuleImpl": ModuleImpl, "ModuleImplInscription": ModuleImplInscription, - "Partition": Partition, "ndb": ndb, "notes": notes, "np": np, + "Partition": Partition, "pd": pd, "Permission": Permission, "pp": pp, - "Role": Role, "res_sem": res_sem, + "ResultatsSemestreBUT": ResultatsSemestreBUT, + "Role": Role, "scolar": scolar, + "ScolarAutorisationInscription": ScolarAutorisationInscription, + "ScolarFormSemestreValidation": ScolarFormSemestreValidation, "ScolarNews": models.ScolarNews, "scu": scu, "UniteEns": UniteEns, diff --git a/tests/api/test_api_assiduites.py b/tests/api/test_api_assiduites.py index 33a7767d..05a8b45e 100644 --- a/tests/api/test_api_assiduites.py +++ b/tests/api/test_api_assiduites.py @@ -15,15 +15,16 @@ MODULE = 1 ASSIDUITES_FIELDS = { - "assiduiteid": int, + "assiduite_id": int, "etudid": int, "moduleimpl_id": int, "date_debut": str, "date_fin": str, "etat": str, + "desc": str, } -CREATE_FIELD = {"assiduiteid": int} +CREATE_FIELD = {"assiduite_id": int} BATCH_FIELD = {"errors": dict, "success": dict} COUNT_FIELDS = {"compte": int, "journee": int, "demi": int, "heure": float} @@ -34,7 +35,7 @@ TO_REMOVE = [] def check_fields(data, fields=ASSIDUITES_FIELDS): assert set(data.keys()) == set(fields.keys()) for key in data: - if key == "moduleimpl_id": + if key in ("moduleimpl_id", "desc"): assert isinstance(data[key], fields[key]) or data[key] is None else: assert isinstance(data[key], fields[key]) @@ -62,7 +63,7 @@ def check_failure_post(path, headers, data, err=None): raise APIError("Le GET n'aurait pas du fonctionner") -def create_data(etat: str, day: str, module=None): +def create_data(etat: str, day: str, module: int = None, desc: str = None): data = { "date_debut": f"2022-01-{day}T08:00", "date_fin": f"2022-01-{day}T10:00", @@ -71,6 +72,8 @@ def create_data(etat: str, day: str, module=None): if module is not None: data["moduleimpl_id"] = module + if desc is not None: + data["desc"] = desc return data @@ -86,7 +89,6 @@ def test_route_assiduite(api_headers): check_failure_get( f"/assiduite/{FAUX}", api_headers, - "assiduité inexistante", ) @@ -188,73 +190,76 @@ def test_route_count_formsemestre_assiduites(api_headers): def test_route_create(api_headers): - # -== Sans batch ==- + # -== Unique ==- # Bon fonctionnement data = create_data("present", "01") - res = POST_JSON(f"/assiduite/{ETUDID}/create", data, api_headers) - check_fields(res, CREATE_FIELD) - TO_REMOVE.append(res["assiduiteid"]) + res = POST_JSON(f"/assiduite/{ETUDID}/create", [data], api_headers) + check_fields(res, BATCH_FIELD) + assert len(res["success"]) == 1 - data2 = create_data("absent", "02", MODULE) - res = POST_JSON(f"/assiduite/{ETUDID}/create", data2, api_headers) - check_fields(res, CREATE_FIELD) - TO_REMOVE.append(res["assiduiteid"]) + TO_REMOVE.append(res["success"]["0"]["assiduite_id"]) + + data2 = create_data("absent", "02", MODULE, "desc") + res = POST_JSON(f"/assiduite/{ETUDID}/create", [data2], api_headers) + check_fields(res, BATCH_FIELD) + assert len(res["success"]) == 1 + + TO_REMOVE.append(res["success"]["0"]["assiduite_id"]) # Mauvais fonctionnement - check_failure_post(f"/assiduite/{FAUX}/create", api_headers, data) - check_failure_post( - f"/assiduite/{ETUDID}/create", - api_headers, - data, - err="La période sélectionnée est déjà couverte par une autre assiduite", - ) - check_failure_post( - f"/assiduite/{ETUDID}/create", - api_headers, - create_data("absent", "03", FAUX), - err="L'étudiant ne participe pas au moduleimpl sélectionné", + check_failure_post(f"/assiduite/{FAUX}/create", api_headers, [data]) + + res = POST_JSON(f"/assiduite/{ETUDID}/create", [data], api_headers) + check_fields(res, BATCH_FIELD) + assert len(res["errors"]) == 1 + assert ( + res["errors"]["0"] + == "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)" ) - # -== Avec batch ==- + res = POST_JSON( + f"/assiduite/{ETUDID}/create", [create_data("absent", "03", FAUX)], api_headers + ) + check_fields(res, BATCH_FIELD) + assert len(res["errors"]) == 1 + assert res["errors"]["0"] == "param 'moduleimpl_id': invalide" + + # -== Multiple ==- # Bon Fonctionnement etats = ["present", "absent", "retard"] - data = { - "batch": [ - create_data(etats[d % 3], 10 + d, MODULE if d % 2 else None) - for d in range(randint(3, 5)) - ] - } + data = [ + create_data(etats[d % 3], 10 + d, MODULE if d % 2 else None) + for d in range(randint(3, 5)) + ] - res = POST_JSON(f"/assiduite/{ETUDID}/create/batch", data, api_headers) + res = POST_JSON(f"/assiduite/{ETUDID}/create", data, api_headers) check_fields(res, BATCH_FIELD) for dat in res["success"]: check_fields(res["success"][dat], CREATE_FIELD) - TO_REMOVE.append(res["success"][dat]["assiduiteid"]) + TO_REMOVE.append(res["success"][dat]["assiduite_id"]) # Mauvais Fonctionnement - data2 = { - "batch": [ - create_data("present", "01"), - create_data("present", "25", FAUX), - create_data("blabla", 26), - create_data("absent", 32), - ] - } + data2 = [ + create_data("present", "01"), + create_data("present", "25", FAUX), + create_data("blabla", 26), + create_data("absent", 32), + ] - res = POST_JSON(f"/assiduite/{ETUDID}/create/batch", data2, api_headers) + res = POST_JSON(f"/assiduite/{ETUDID}/create", data2, api_headers) check_fields(res, BATCH_FIELD) assert len(res["errors"]) == 4 assert ( res["errors"]["0"] - == "La période sélectionnée est déjà couverte par une autre assiduite" + == "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)" ) - assert res["errors"]["1"] == "L'étudiant ne participe pas au moduleimpl sélectionné" + assert res["errors"]["1"] == "param 'moduleimpl_id': invalide" assert res["errors"]["2"] == "param 'etat': invalide" assert ( res["errors"]["3"] @@ -287,43 +292,41 @@ def test_route_edit(api_headers): def test_route_delete(api_headers): - # -== Sans batch ==- + # -== Unique ==- # Bon fonctionnement - data = {"assiduiteid": TO_REMOVE[0]} + data = TO_REMOVE[0] - res = POST_JSON(f"/assiduite/delete", data, api_headers) - assert res == {"OK": True} + res = POST_JSON(f"/assiduite/delete", [data], api_headers) + check_fields(res, BATCH_FIELD) + for dat in res["success"]: + assert res["success"][dat] == {"OK": True} # Mauvais fonctionnement - check_failure_post( - f"/assiduite/delete", - api_headers, - {"assiduiteid": FAUX}, - err="Assiduite non existante", - ) - # -== Avec batch ==- + res = POST_JSON(f"/assiduite/delete", [data], api_headers) + check_fields(res, BATCH_FIELD) + assert len(res["errors"]) == 1 + + # -== Multiple ==- # Bon Fonctionnement - data = {"batch": TO_REMOVE[1:]} + data = TO_REMOVE[1:] - res = POST_JSON(f"/assiduite/delete/batch", data, api_headers) + res = POST_JSON(f"/assiduite/delete", data, api_headers) check_fields(res, BATCH_FIELD) for dat in res["success"]: assert res["success"][dat] == {"OK": True} # Mauvais Fonctionnement - data2 = { - "batch": [ - FAUX, - FAUX + 1, - FAUX + 2, - ] - } + data2 = [ + FAUX, + FAUX + 1, + FAUX + 2, + ] - res = POST_JSON(f"/assiduite/delete/batch", data2, api_headers) + res = POST_JSON(f"/assiduite/delete", data2, api_headers) check_fields(res, BATCH_FIELD) assert len(res["errors"]) == 3 diff --git a/tests/api/test_api_users.py b/tests/api/test_api_users.py index e9fe5aa4..8a347700 100644 --- a/tests/api/test_api_users.py +++ b/tests/api/test_api_users.py @@ -34,7 +34,11 @@ def test_list_users(api_admin_headers): # Tous les utilisateurs, vus par SuperAdmin: users = GET("/users/query", headers=admin_h) - + assert len(users) > 2 + # Les utilisateurs du dept. TAPI + users_TAPI = GET("/users/query?departement=TAPI", headers=admin_h) + nb_TAPI = len(users_TAPI) + assert nb_TAPI > 1 # Les utilisateurs de chaque département (+ ceux sans département) all_users = [] for acronym in [dept["acronym"] for dept in depts] + [""]: @@ -59,9 +63,8 @@ def test_list_users(api_admin_headers): for i, u in enumerate(u for u in u_users if u["dept"] != "TAPI"): headers = get_auth_headers(u["user_name"], "test") users_by_u = GET("/users/query", headers=headers) - assert len(users_by_u) == 4 + i - # explication: tous ont le droit de voir les 3 users de TAPI - # (test, other et u_TAPI) + assert len(users_by_u) == nb_TAPI + 1 + i + # explication: tous ont le droit de voir les users de TAPI # plus l'utilisateur de chaque département jusqu'au leur # (u_AA voit AA, u_BB voit AA et BB, etc) @@ -90,6 +93,10 @@ def test_edit_users(api_admin_headers): ) assert user["dept"] == "TAPI" assert user["active"] is False + user = GET(f"/user/{user['id']}", headers=admin_h) + assert user["nom"] == "Toto" + assert user["dept"] == "TAPI" + assert user["active"] is False def test_roles(api_admin_headers): @@ -229,3 +236,10 @@ def test_modif_users_depts(api_admin_headers): ok = True assert ok # Nettoyage: + # on ne peut pas supprimer l'utilisateur lambda, mais on + # le rend inactif et on le retire de son département + u = POST_JSON( + f"/user/{u_lambda['id']}/edit", + {"active": False, "dept": None}, + headers=admin_h, + ) diff --git a/tests/api/test_test.py b/tests/api/test_test.py new file mode 100644 index 00000000..28c62b27 --- /dev/null +++ b/tests/api/test_test.py @@ -0,0 +1,47 @@ +# -*- coding: UTF-8 -* + +"""Unit tests for... tests + +Ensure test DB is in the expected initial state. + +Usage: pytest tests/unit/test_test.py +""" + +import pytest + +from tests.api.setup_test_api import ( + api_headers, + GET, +) + + +@pytest.mark.test_test +def test_test_db(api_headers): + """Check that we indeed have: 2 users, 1 dept, 3 formsemestres. + Juste après init, les ensembles seront ceux donnés ci-dessous. + Les autres tests peuvent ajouter des éléments, c'edt pourquoi on utilise issubset(). + """ + headers = api_headers + assert { + "admin_api", + "admin", + "lecteur_api", + "other", + "test", + "u_AA", + "u_BB", + "u_CC", + "u_DD", + "u_TAPI", + }.issubset({u["user_name"] for u in GET("/users/query", headers=headers)}) + assert { + "AA", + "BB", + "CC", + "DD", + "TAPI", + }.issubset({d["acronym"] for d in GET("/departements", headers=headers)}) + assert 1 in ( + formsemestre["semestre_id"] + for formsemestre in GET("/formsemestres/query", headers=headers) + ) diff --git a/tests/ressources/formations/scodoc_formation_BUT_GEII_lyon_v1.xml b/tests/ressources/formations/scodoc_formation_BUT_GEII_lyon_v1.xml new file mode 100644 index 00000000..81cdb8e5 --- /dev/null +++ b/tests/ressources/formations/scodoc_formation_BUT_GEII_lyon_v1.xml @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ressources/samples.csv b/tests/ressources/samples.csv index 819d39c2..a8d92875 100644 --- a/tests/ressources/samples.csv +++ b/tests/ressources/samples.csv @@ -1,4 +1,24 @@ "entry_name";"url";"permission";"method";"content" +"assiduite";"/assiduite/1";"ScoView";"GET"; +"assiduites";"/assiduites/1";"ScoView";"GET"; +"assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET"; +"assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET"; +"assiduites_count";"/assiduites/1/count";"ScoView";"GET"; +"assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET"; +"assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET"; +"assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET"; +"assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET"; +"assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET"; +"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET"; +"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET"; +"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET"; +"assiduite_create";"/assiduite/1/create";"ScoView";"POST";"{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""}" +"assiduite_create";"/assiduite/1/create/batch";"ScoView";"POST";"{""batch"":[{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""},{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""retard""},{""date_debut"": ""2022-10-27T11:00"",""date_fin"": ""2022-10-27T13:00"",""etat"": ""present""}]}" +"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"":""absent""}" +"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""moduleimpl_id"":2}" +"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}" +"assiduite_delete";"/assiduite/delete";"ScoView";"POST";"{""assiduite_id"": 1}" +"assiduite_delete";"/assiduite/delete/batch";"ScoView";"POST";"{""batch"":[2,2,3]}" "departements";"/departements";"ScoView";"GET"; "departements-ids";"/departements_ids";"ScoView";"GET"; "departement";"/departement/TAPI";"ScoView";"GET"; diff --git a/tests/unit/cursus_but_gb.yaml b/tests/unit/cursus_but_gb.yaml index 30b1d6a2..0b3588a6 100644 --- a/tests/unit/cursus_but_gb.yaml +++ b/tests/unit/cursus_but_gb.yaml @@ -129,7 +129,7 @@ FormSemestres: Etudiants: - Aaaaa: + Aïaaa: # avec un i trema prenom: Étudiant_SEE civilite: M formsemestres: @@ -196,7 +196,7 @@ Etudiants: S3: parcours: SEE - Bbbbb: + Azbbbb: # Az devrait être trié après Aï. prenom: Étudiante_BMB civilite: F formsemestres: diff --git a/tests/unit/cursus_but_geii_lyon.yaml b/tests/unit/cursus_but_geii_lyon.yaml new file mode 100644 index 00000000..8d64c976 --- /dev/null +++ b/tests/unit/cursus_but_geii_lyon.yaml @@ -0,0 +1,1105 @@ +# Tests unitaires jury BUT - IUT Lyon GEII +# Essais avec un BUT GEII, 2 UE en BUT1 / 4 UE en BUT2-BUT3 et 3 parcours +# Contrib Pascal B. + +ReferentielCompetences: + filename: but-GEII-05012022-081639.xml + specialite: GEII + +Formation: + filename: scodoc_formation_BUT_GEII_lyon_v1.xml + # Association des UE aux compétences: + ues: + # S1 : Tronc commun GEII + 'UE11': + annee: BUT1 + competence: Concevoir + 'UE12': + annee: BUT1 + competence: Vérifier + + # S2 : Tronc commun GEII + 'UE21': + annee: BUT1 + competence: Concevoir + 'UE22': + annee: BUT1 + competence: Vérifier + + # S3 : Tronc commun GEII + 'UE31': + annee: BUT2 + competence: Concevoir + 'UE32': + annee: BUT2 + competence: Vérifier + 'UE33': + annee: BUT2 + competence: Maintenir + # S3 : Parcours EME + 'UE34EME': + annee: BUT2 + competence: Installer + parcours: EME + # S3 : Parcours ESE + 'UE34ESE': + annee: BUT2 + competence: Implanter + parcours: ESE + # S3 : Parcours AII + 'UE34AII': + annee: BUT2 + competence: Intégrer + parcours: AII + + # S4 : Tronc commun GEII + 'UE41': + annee: BUT2 + competence: Concevoir + 'UE42': + annee: BUT2 + competence: Vérifier + 'UE43': + annee: BUT2 + competence: Maintenir + # S4 : Parcours EME + 'UE44EME': + annee: BUT2 + competence: Installer + parcours: EME + # S4 : Parcours ESE + 'UE44ESE': + annee: BUT2 + competence: Implanter + parcours: ESE + # S4 : Parcours AII + 'UE44AII': + annee: BUT2 + competence: Intégrer + parcours: AII + + modules_parcours: + # cette section permet d'associer des modules à des parcours + # les codes modules peuvent être des regexp + EME: [ .*EME.* ] + ESE: [ .*ESE.* ] + AII: [ .*AII.* ] + +FormSemestres: + # S1 et S2 : + S1: + idx: 1 + date_debut: 2021-09-01 + date_fin: 2022-01-15 + S2: + idx: 2 + date_debut: 2022-01-16 + date_fin: 2022-06-30 + # S3 avec les trois parcours réunis: + # S3: + # idx: 3 + # date_debut: 2022-09-01 + # date_fin: 2023-01-13 + # codes_parcours: ['AII', 'EME', 'ESE'] + # Un S1 pour les redoublants + S1-red: + idx: 1 + date_debut: 2022-09-02 + date_fin: 2023-01-12 + +Etudiants: + geii8: + prenom: etugeii8 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 7.00 + "S1.2": 9.00 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.00 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.00 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.00 + "S2.2": 12.00 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.00 + "UE22": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.00 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.50 + est_compensable: False + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.50 + est_compensable: True + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.50 + "S1.2": 7.00 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + decisions_ues: + "UE11": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 9.50 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.00 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: CMP + rcue: + moy_rcue: 10.75 + est_compensable: True + "UE12": + code_valide: CMP # car validé en fin de S2 + rcue: + moy_rcue: 9.50 # la moyenne courante (et non enregistrée), donc pas 10.5 + est_compensable: False + decision_annee: ADM + geii43: + prenom: etugeii43 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.00 + "S1.2": 9.00 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.00 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.00 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 9.00 + "S2.2": 9.00 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "AJ", "..." ] + code_valide: AJ + moy_ue: 9.00 + "UE22": + codes: [ "AJ", "..." ] + code_valide: AJ # va basculer en ADJ car RCUE en ADJ (mais le test est AVANT !) + moy_ue: 9.00 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ # inutile de la préciser mais on peut + rcue: + moy_rcue: 9.00 + est_compensable: False + "UE12": + code_valide: AJ # code par défaut proposé + decision_jury: ADJ # code donné par le jury de S2 + rcue: + moy_rcue: 9.00 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 11.00 + "S1.2": 7.00 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: false + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 11.00 + "UE12": + code_valide: AJ + moy_ue: 7.00 + decision_annee: AJ + geii84: + prenom: etugeii84 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 11.95 + "S1.2": 12.76 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 11.95 + "UE12": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.76 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 7.83 + "S2.2": 8.15 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.83 + "UE22": + codes: [ "CMP", "..." ] + code_valide: CMP + moy_ue: 8.15 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.89 + est_compensable: False + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.455 # ! attention à la précision + est_compensable: True + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 13.71 + "S1.2": 9.50 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 13.71 + "UE12": + codes: [ "AJ", "ADJ", "RAT", "DEF", "ABAN", "ADJR", "ATJ", "DEM", "UEBSL" ] + code_valide: AJ # c'est l'UE12 du S1 de l'année prec. qui est ADM + moy_ue: 9.5 # moyenne non capitalisée ici + moy_ue_with_cap: 12.76 +# Pas de décisions RCUE +# "UE11": -- non applicable +# code_valide: ADM -- non applicable +# decision_jury: ADM -- non applicable +# rcue: -- non applicable +# moy_rcue: 10.94 -- non applicable +# est_compensable: False -- non applicable +# "UE12": -- non applicable +# code_valide: ADM -- non applicable +# decision_jury: ADM -- non applicable +# rcue: -- non applicable +# moy_rcue: 10.94 -- non applicable +# est_compensable: False -- non applicable + decision_annee: AJ +# Nouveaux cas RED (mardi 17/01/2023) + geii8bis: + prenom: "etugeii8 bis" + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 7.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ + moy_ue: 7.0000 + "UE12": + code_valide: AJ # ne sera compensée qu'en fin de S2 + moy_ue: 9.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.0000 + "UE22": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.5000 + est_compensable: True + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.5000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + decisions_ues: + "UE11": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 9.5000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.75 + est_compensable: True + decision_annee: ADM + geii10: + prenom: etugeii10 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.0000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ # en fin de S1, sera compensée en fin de S2 + moy_ue: 9.0000 + "UE12": + code_valide: AJ + moy_ue: 7.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE22": + code_valide: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: CMP + rcue: + moy_rcue: 10.5000 + est_compensable: True + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": 7.5000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.5000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: ADM + decision_jury: ADM + rcue: + moy_rcue: 12.00 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.75 + est_compensable: False + decision_annee: AJ + geii11: + prenom: etugeii11 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 7.0000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE22": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: True + nb_competences: 2 + nb_rcue_annee: 2 + decisions_ues: + "UE11": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 9.0000 + "UE12": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 9.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.50 + est_compensable: True + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.50 + est_compensable: True + decision_annee: ADM + geii13: + prenom: etugeii13 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.0000 + "S1.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + "UE12": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 9.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + code_valide: AJ + moy_ue: 9.0000 + "UE22": + code_valide: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.0000 + est_compensable: False + "UE12": + code_valide: ADM + decision_jury: ADM + rcue: + moy_rcue: 12.0000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": ATT + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: ADM + moy_ue: 12.0000 + "UE12": + code_valide: AJ + # PAS DE RCUE car UE12 capitalisée mailleure qu'actuelle + decision_annee: AJ + geii20: + prenom: etugeii20 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 7.0000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: ADJR + moy_ue: 7.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 9.0000 + "S2.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: ADJR + moy_ue: 9.0000 + "UE22": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 8.0000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: ADJ + rcue: + moy_rcue: 9.5000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": 4.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: false + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: ADM + moy_ue: 12.0000 + "UE12": + code_valide: AJ + moy_ue: 4.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE12": + code_valide: ADJ + rcue: + moy_rcue: 8.00 + est_compensable: 0 + decision_annee: AJ + geii33: + prenom: etugeii33 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + "UE22": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: ADM + decision_jury: ADM + rcue: + moy_rcue: 12.0000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.0000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 5.0000 + "S1.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ + moy_ue: 5. # LA MOYENNE COURANTE + moy_ue_with_cap: 12.0000 + "UE12": + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + # PAS DE RCUE ICI + decision_annee: AJ + geii43: + prenom: etugeii43 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 9.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ + decision_jury: ADJR + moy_ue: 9.0000 + "UE12": + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 9.0000 + "S2.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + code_valide: AJ + decision_jury: ADJR + moy_ue: 9.0000 + "UE22": + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.0000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: ADJ + rcue: + moy_rcue: 9.0000 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 11.0000 + "S1.2": 7.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 11.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.0000 + decision_annee: AJ + geii84bis: + prenom: "etugeii84 bis" + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 11.9500 + "S1.2": 12.7600 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 11.9500 + "UE12": + codes: [ "ADM", "..." ] + code_valide: ADM + decision_jury: ADM + moy_ue: 12.7600 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 7.8300 + "S2.2": 8.1500 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 7.8300 + "UE22": + codes: [ "CMP", "..." ] + code_valide: CMP + decision_jury: CMP + moy_ue: 8.1500 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.8900 + est_compensable: False + "UE12": + code_valide: CMP + decision_jury: CMP + rcue: + moy_rcue: 10.4550 + est_compensable: True + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 13.7100 + "S1.2": 9.5000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: ADM + moy_ue: 13.7100 + "UE12": + code_valide: AJ + moy_ue: 9.5000 + moy_ue_with_cap: 12.7600 + decision_annee: AJ + geii1000: + prenom: etugeii1000 + civilite: M + formsemestres: + S1: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 12.0000 + "S1.2": 9.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.0000 + "UE12": + codes: [ "AJ", "..." ] + code_valide: AJ + decision_jury: AJ + moy_ue: 9.0000 + S2: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S2.1": 12.0000 + "S2.2": 10.50 # capitalise mais ne compense pas + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 2 + valide_moitie_rcue: False + codes: [ "RED", "..." ] + decisions_ues: + "UE21": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.0000 + "UE22": + code_valide: ADM + moy_ue: 10.5 + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE11": + code_valide: ADM + decision_jury: ADM + rcue: + moy_rcue: 12.0000 + est_compensable: False + "UE12": + code_valide: AJ + decision_jury: AJ + rcue: + moy_rcue: 9.75 + est_compensable: False + decision_annee: RED + S1-red: + notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" + "S1.1": 5.0000 + "S1.2": 12.0000 + attendu: # les codes jury que l'on doit vérifier + deca: + passage_de_droit: False + nb_competences: 2 + nb_rcue_annee: 0 + decisions_ues: + "UE11": + code_valide: AJ + moy_ue: 5. # LA MOYENNE COURANTE + moy_ue_with_cap: 12.0000 + "UE12": + code_valide: ADM + decision_jury: ADM + moy_ue: 12.0000 + # RCUE inter-annuel + decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) + "UE12": + code_valide: ADM + rcue: + moy_rcue: 11.25 + est_compensable: False diff --git a/tests/unit/cursus_but_gmp_iutlm.yaml b/tests/unit/cursus_but_gmp_iutlm.yaml index e7899733..4d3da7a6 100644 --- a/tests/unit/cursus_but_gmp_iutlm.yaml +++ b/tests/unit/cursus_but_gmp_iutlm.yaml @@ -1,6 +1,9 @@ # Tests unitaires jury BUT - IUTLM GMP # Essais avec un BUT GMP, 4 UE + 1 bonus et deux parcours sur S3 S4 # Contrib Martin M. +# +# Pour ne jouer que ce scénario: +# pytest -m lemans tests/unit/test_but_jury.py ReferentielCompetences: filename: but-GMP-05012022-081650.xml @@ -108,10 +111,14 @@ FormSemestres: date_debut: 2023-09-01 date_fin: 2024-01-13 codes_parcours: ['II', 'SNRV'] - + # Un S1 pour les redoublants + S1-red: + idx: 1 + date_debut: 2023-09-02 + date_fin: 2024-01-12 Etudiants: - gmp01: + gmp01: # cursus S1, S2, S3 prenom: etugmp01 civilite: M formsemestres: @@ -169,6 +176,7 @@ Etudiants: "UE2.4-C4": codes: [ "AJ", "..." ] code_valide: AJ + decision_jury: ADJ # le jury force la décision ADJ moy_ue: 08.55 decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1) "UE1.1-C1": @@ -195,13 +203,14 @@ Etudiants: S3: + parcours: SNRV # Inscrit dans le parcours SNRV notes_modules: # on joue avec les SAE seulement car elles sont "diagonales" "S3.01": 9 "S3.SNRV.02": 12.5 attendu: # les codes jury que l'on doit vérifier deca: passage_de_droit: False - nb_competences: 4 # et non 5 car pas inscrit à un parcours + nb_competences: 5 # 4 de Tronc Commun + 1 de parcours nb_rcue_annee: 0 decisions_ues: "UE3.1-C1": @@ -220,12 +229,12 @@ Etudiants: codes: [ "AJ", "..." ] code_valide: AJ moy_ue: 9 - # "UE3.5.SNRV": - # codes: [ "ADM", "..." ] - # code_valide: ADM - # moy_ue: 12.5 + "UE3.5.SNRV": + codes: [ "ADM", "..." ] + code_valide: ADM + moy_ue: 12.5 - gmp02: + gmp02: # cursus S1, S2, S3 prenom: etugmp02 civilite: F formsemestres: @@ -340,7 +349,7 @@ Etudiants: # code_valide: ADM # moy_ue: 14 - gmp03: + gmp03: # cursus S1, S2, S1-red prenom: etugmp03 civilite: X formsemestres: @@ -418,4 +427,43 @@ Etudiants: code_valide: AJ rcue: moy_rcue: 9.1 - est_compensable: False \ No newline at end of file + est_compensable: False + S1-red: + # On a capitalisé les UE/RCUE UE1.1-C1 et UE1.3-C3 + # L'étudiant décide de refaire qd même l'UE UE1.1-C1 + notes_modules: # on ne note ici que les UE à refaire + "SAE1.1": 14. # il améliore son UE 1 + "SAE1.2": 12. # et cette fois reussi les autres + "SAE1.3": EXC # pour que l'éval soit complete + "SAE1.4": 13. + attendu: + nb_competences: 4 + nb_rcue_annee: 0 + decisions_ues: + "UE1.1-C1": + code_valide: ADM + moy_ue: 14 # nouvelle moyenne + "UE1.2-C2": + code_valide: ADM + moy_ue: 12 + "UE1.3-C3": + moy_ue: 10.1 # capitalisée du S1 précédent XXX à vérifier + "UE1.4-C4": + code_valide: ADM + moy_ue: 13 + + gmp04: # cursus S1-red (primo-entrant) + prenom: Primo + civilite: M + formsemestres: + S1-red: + notes_modules: + "SAE1.1": 11. + "SAE1.2": 12. + "SAE1.3": 13. + "SAE1.4": 9. + attendu: # les codes jury que l'on doit vérifier + deca: + "UE1.4-C4": + code_valide: "AJ" + moy_ue: 9. diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index 0c07a27e..e70c434c 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -232,7 +232,7 @@ class ScoFake(object): self, formation_id=None, semestre_id=None, - titre=None, + titre="", date_debut=None, date_fin=None, etat=None, @@ -253,6 +253,7 @@ class ScoFake(object): ) -> int: if responsables is None: responsables = (self.default_user.id,) + titre = titre or "sans titre" oid = sco_formsemestre.do_formsemestre_create(locals()) oids = sco_formsemestre.do_formsemestre_list( args={"formsemestre_id": oid} diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 1bb5b664..c553a6a2 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -14,7 +14,8 @@ from app import db from app.scodoc import sco_formsemestre import app.scodoc.sco_assiduites as scass -from app.models import Assiduite, Identite, FormSemestre +from app.models import Assiduite, Identite, FormSemestre, ModuleImpl +from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu @@ -62,29 +63,33 @@ def test_general(test_client): # Création des modulesimpls (4, 2 par semestre) - moduleimpl_id_1_1 = G.create_moduleimpl( + moduleimpl_1_1 = G.create_moduleimpl( module_id=module_id_1, formsemestre_id=formsemestre_id_1, ) - moduleimpl_id_1_2 = G.create_moduleimpl( + moduleimpl_1_2 = G.create_moduleimpl( module_id=module_id_2, formsemestre_id=formsemestre_id_1, ) - moduleimpl_id_2_1 = G.create_moduleimpl( + moduleimpl_2_1 = G.create_moduleimpl( module_id=module_id_1, formsemestre_id=formsemestre_id_2, ) - moduleimpl_id_2_2 = G.create_moduleimpl( + moduleimpl_2_2 = G.create_moduleimpl( module_id=module_id_2, formsemestre_id=formsemestre_id_2, ) moduleimpls = [ - moduleimpl_id_1_1, - moduleimpl_id_1_2, - moduleimpl_id_2_1, - moduleimpl_id_2_2, + moduleimpl_1_1, + moduleimpl_1_2, + moduleimpl_2_1, + moduleimpl_2_2, + ] + + moduleimpls = [ + ModuleImpl.query.filter_by(id=mi_id).first() for mi_id in moduleimpls ] # Création des étudiants (3) @@ -100,7 +105,12 @@ def test_general(test_client): assert None not in etuds, "Problème avec la conversion en Identite" - ajouter_assiduites(etuds, moduleimpls=moduleimpls) + # Etudiant faux + + etud_faux_dict = G.create_etud(code_nip=None, prenom="etudfaux") + etud_faux = Identite.query.filter_by(id=etud_faux_dict["id"]).first() + + ajouter_assiduites(etuds, moduleimpls, etud_faux) verifier_comptage_et_filtrage( etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3) ) @@ -114,8 +124,6 @@ def editer_supprimer_assiduiter(etuds: list[Identite], moduleimpls: list[int]): - Vérification de la suppression des assiduitées """ - from sqlalchemy.exc import IntegrityError - ass1: Assiduite = etuds[0].assiduites.first() ass2: Assiduite = etuds[1].assiduites.first() ass3: Assiduite = etuds[2].assiduites.first() @@ -124,7 +132,7 @@ def editer_supprimer_assiduiter(etuds: list[Identite], moduleimpls: list[int]): ass1.etat = scu.EtatAssiduite.RETARD db.session.add(ass1) # Modification du moduleimpl - ass2.moduleimpl_id = moduleimpls[0] + ass2.moduleimpl_id = moduleimpls[0].id db.session.add(ass2) db.session.commit() @@ -133,7 +141,7 @@ def editer_supprimer_assiduiter(etuds: list[Identite], moduleimpls: list[int]): scass.filter_by_etat(etuds[0].assiduites, "retard").count() == 3 ), "Edition d'assiduité mauvais" assert ( - scass.filter_by_module_impl(etuds[1].assiduites, moduleimpls[0]).count() == 2 + scass.filter_by_module_impl(etuds[1].assiduites, moduleimpls[0].id).count() == 2 ), "Edition d'assiduité mauvais" # Supression d'une assiduité @@ -144,7 +152,9 @@ def editer_supprimer_assiduiter(etuds: list[Identite], moduleimpls: list[int]): assert etuds[2].assiduites.count() == 5, "Supression d'assiduité mauvais" -def ajouter_assiduites(etuds: list[Identite], moduleimpls: list[int]): +def ajouter_assiduites( + etuds: list[Identite], moduleimpls: list[ModuleImpl], etud_faux: Identite +): """ Première partie: - Ajoute 6 assiduités à chaque étudiant @@ -160,43 +170,54 @@ def ajouter_assiduites(etuds: list[Identite], moduleimpls: list[int]): "etat": scu.EtatAssiduite.PRESENT, "deb": "2022-09-03T08:00+01:00", "fin": "2022-09-03T10:00+01:00", - "moduleimpl_id": None, + "moduleimpl": None, + "desc": None, }, { "etat": scu.EtatAssiduite.PRESENT, "deb": "2023-01-03T08:00+01:00", "fin": "2023-01-03T10:00+01:00", - "moduleimpl_id": moduleimpls[2], + "moduleimpl": moduleimpls[2], + "desc": None, }, { "etat": scu.EtatAssiduite.ABSENT, "deb": "2022-09-03T10:00:01+01:00", "fin": "2022-09-03T11:00+01:00", - "moduleimpl_id": moduleimpls[0], + "moduleimpl": moduleimpls[0], + "desc": None, }, { "etat": scu.EtatAssiduite.ABSENT, "deb": "2022-09-03T14:00:00+01:00", "fin": "2022-09-03T15:00+01:00", - "moduleimpl_id": moduleimpls[1], + "moduleimpl": moduleimpls[1], + "desc": "Description", }, { "etat": scu.EtatAssiduite.RETARD, "deb": "2023-01-03T11:00:01+01:00", "fin": "2023-01-03T12:00+01:00", - "moduleimpl_id": moduleimpls[3], + "moduleimpl": moduleimpls[3], + "desc": None, }, { "etat": scu.EtatAssiduite.RETARD, "deb": "2023-01-04T11:00:01+01:00", "fin": "2023-01-04T12:00+01:00", - "moduleimpl_id": moduleimpls[3], + "moduleimpl": moduleimpls[3], + "desc": "Description", }, ] assiduites = [ Assiduite.create_assiduite( - etud, ass["deb"], ass["fin"], ass["etat"], ass["moduleimpl_id"] + etud, + ass["deb"], + ass["fin"], + ass["etat"], + ass["moduleimpl"], + ass["desc"], ) for ass in obj_assiduites ] @@ -208,32 +229,39 @@ def ajouter_assiduites(etuds: list[Identite], moduleimpls: list[int]): # Vérification de la gestion des erreurs - test_assiduites = [ - { - "etat": scu.EtatAssiduite.RETARD, - "deb": "2023-01-04T11:00:01+01:00", - "fin": "2023-01-04T12:00+01:00", - "moduleimpl_id": moduleimpls[3], - }, - { - "etat": scu.EtatAssiduite.RETARD, - "deb": "2023-01-05T11:00:01+01:00", - "fin": "2023-01-05T12:00+01:00", - "moduleimpl_id": 1000, - }, - ] + test_assiduite = { + "etat": scu.EtatAssiduite.RETARD, + "deb": "2023-01-04T11:00:01+01:00", + "fin": "2023-01-04T12:00+01:00", + "moduleimpl": moduleimpls[3], + "desc": "Description", + } - assiduites_crees = [ + try: Assiduite.create_assiduite( - etuds[0], ass["deb"], ass["fin"], ass["etat"], ass["moduleimpl_id"] + etuds[0], + test_assiduite["deb"], + test_assiduite["fin"], + test_assiduite["etat"], + test_assiduite["moduleimpl"], + test_assiduite["desc"], ) - for ass in test_assiduites - ] - - assert [ass for ass in assiduites_crees if type(ass) != Assiduite] == [ - 1, - 2, - ], "La vérification des erreurs ne fonctionne pas" + except ScoValueError as excp: + assert ( + excp.args[0] + == "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)" + ) + try: + Assiduite.create_assiduite( + etud_faux, + test_assiduite["deb"], + test_assiduite["fin"], + test_assiduite["etat"], + test_assiduite["moduleimpl"], + test_assiduite["desc"], + ) + except ScoValueError as excp: + assert excp.args[0] == "L'étudiant n'est pas inscrit au moduleimpl" def verifier_comptage_et_filtrage( @@ -282,16 +310,16 @@ def verifier_comptage_et_filtrage( # Module assert ( - scass.filter_by_module_impl(etu3.assiduites, mod11).count() == 1 + scass.filter_by_module_impl(etu3.assiduites, mod11.id).count() == 1 ), "Filtrage par 'Moduleimpl' mauvais" assert ( - scass.filter_by_module_impl(etu3.assiduites, mod12).count() == 1 + scass.filter_by_module_impl(etu3.assiduites, mod12.id).count() == 1 ), "Filtrage par 'Moduleimpl' mauvais" assert ( - scass.filter_by_module_impl(etu3.assiduites, mod21).count() == 1 + scass.filter_by_module_impl(etu3.assiduites, mod21.id).count() == 1 ), "Filtrage par 'Moduleimpl' mauvais" assert ( - scass.filter_by_module_impl(etu3.assiduites, mod22).count() == 2 + scass.filter_by_module_impl(etu3.assiduites, mod22.id).count() == 2 ), "Filtrage par 'Moduleimpl' mauvais" assert ( scass.filter_by_module_impl(etu3.assiduites, None).count() == 1 diff --git a/tests/unit/test_but_jury.py b/tests/unit/test_but_jury.py index b52a432a..fec4976c 100644 --- a/tests/unit/test_but_jury.py +++ b/tests/unit/test_but_jury.py @@ -1,28 +1,39 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + """ Test jury BUT avec parcours + +Ces tests sont généralement lents (construction de la base), +et donc marqués par `@pytest.mark.slow`. + +Certains sont aussi marqués par @pytest.mark.lemans ou @pytest.mark.lyon +pour lancer certains tests spécifiques seulement. + +Exemple utilisation spécifique: +# test sur "Lyon" seulement: +pytest --pdb -m lyon tests/unit/test_but_jury.py + """ import pytest from tests.unit import yaml_setup import app -from app.but.jury_but import DecisionsProposeesAnnee from app.but.jury_but_validation_auto import formsemestre_validation_auto_but -from app.models import ( - Formation, - FormSemestre, - Identite, - UniteEns, -) -from app.scodoc import sco_utils as scu +from app.models import FormSemestre from config import TestConfig DEPT = TestConfig.DEPT_TEST @pytest.mark.slow +@pytest.mark.but_gb def test_but_jury_GB(test_client): """Tests sur un cursus GB - - construction des semestres et de leurs étudianst à partir du yaml + - construction des semestres et de leurs étudiants à partir du yaml - vérification jury de S1 - vérification jury de S2 - vérification jury de S3 @@ -35,7 +46,7 @@ def test_but_jury_GB(test_client): # Vérifie les deca de tous les semestres: for formsemestre in FormSemestre.query: - _check_deca(formsemestre) + yaml_setup.check_deca_fields(formsemestre) # Saisie de toutes les décisions de jury for formsemestre in FormSemestre.query.order_by(FormSemestre.semestre_id): @@ -43,92 +54,60 @@ def test_but_jury_GB(test_client): # Vérifie résultats attendus: S1: FormSemestre = FormSemestre.query.filter_by(titre="S1_SEE").first() - _test_but_jury(S1, doc) + yaml_setup.test_but_jury(S1, doc) S2: FormSemestre = FormSemestre.query.filter_by(titre="S2_SEE").first() - _test_but_jury(S2, doc) + yaml_setup.test_but_jury(S2, doc) S3: FormSemestre = FormSemestre.query.filter_by(titre="S3").first() - _test_but_jury(S3, doc) + yaml_setup.test_but_jury(S3, doc) # _test_but_jury(S1_redoublant, doc) @pytest.mark.slow @pytest.mark.lemans def test_but_jury_GMP_lm(test_client): - """Tests sur un cursus GMP fournit par Le Mans""" + """Tests sur un cursus GMP fourni par Le Mans""" app.set_sco_dept(DEPT) # Construit la base de test GB une seule fois # puis lance les tests de jury doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_gmp_iutlm.yaml") - formsemestres = FormSemestre.query.order_by(FormSemestre.semestre_id).all() + formsemestres = FormSemestre.query.order_by( + FormSemestre.date_debut, FormSemestre.semestre_id + ).all() + # Vérifie les deca de tous les semestres: for formsemestre in formsemestres: - _check_deca(formsemestre) + yaml_setup.check_deca_fields(formsemestre) - # Saisie de toutes les décisions de jury + # Saisie de toutes les décisions de jury qui ne le seraient pas déjà for formsemestre in formsemestres: formsemestre_validation_auto_but(formsemestre, only_adm=False) # Vérifie résultats attendus: for formsemestre in formsemestres: - _test_but_jury(formsemestre, doc) + yaml_setup.test_but_jury(formsemestre, doc) -def _check_deca(formsemestre: FormSemestre, etud: Identite = None): - """vérifie les champs principaux de l'instance de DecisionsProposeesAnnee""" - etud = etud or formsemestre.etuds.first() - assert etud # il faut au moins un étudiant dans le semestre - deca = DecisionsProposeesAnnee(etud, formsemestre) - assert deca.validation is None # pas encore de validation enregistrée - assert False is deca.recorded - assert deca.code_valide is None - if formsemestre.semestre_id % 2: - assert deca.formsemestre_impair == formsemestre - assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_impair - else: - assert deca.formsemestre_pair == formsemestre - assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_pair - if formsemestre.semestre_id == 1: - assert deca.formsemestre_pair is None # jury de S1, pas de S2 - assert deca.rcues_annee == [] # S1, pas de RCUEs - assert deca.inscription_etat == scu.INSCRIT - assert deca.inscription_etat_impair == scu.INSCRIT - assert (deca.parcour is None) or ( - deca.parcour.id in {p.id for p in formsemestre.parcours} - ) +@pytest.mark.slow +@pytest.mark.lyon +def test_but_jury_GEII_lyon(test_client): + """Tests sur un cursus GEII fourni par Lyon""" + app.set_sco_dept(DEPT) + # Construit la base de test GB une seule fois + # puis lance les tests de jury + doc = yaml_setup.setup_from_yaml("tests/unit/cursus_but_geii_lyon.yaml") + formsemestres = FormSemestre.query.order_by( + FormSemestre.date_debut, FormSemestre.semestre_id + ).all() - nb_ues = ( - len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all()) - if deca.formsemestre_pair - else 0 - ) - nb_ues += ( - len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all()) - if deca.formsemestre_impair - else 0 - ) - assert len(deca.decisions_ues) == nb_ues + # Vérifie les champs de DecisionsProposeesAnnee de tous les semestres: + for formsemestre in formsemestres: + yaml_setup.check_deca_fields(formsemestre) - nb_ues_un_sem = ( - len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all()) - if deca.formsemestre_impair - else len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all()) - ) - assert len(deca.niveaux_competences) == nb_ues_un_sem - assert deca.nb_competences == nb_ues_un_sem - - -def _test_but_jury(formsemestre: FormSemestre, doc: dict): - """Test jurys - Vérifie les champs de DecisionsProposeesAnnee et UEs - """ - for etud in formsemestre.etuds: - deca = DecisionsProposeesAnnee(etud, formsemestre) - doc_formsemestre = doc["Etudiants"][etud.nom]["formsemestres"][ - formsemestre.titre - ] - assert doc_formsemestre - if "attendu" in doc_formsemestre: - if "deca" in doc_formsemestre["attendu"]: - deca_att = doc_formsemestre["attendu"]["deca"] - yaml_setup.compare_decisions_annee(deca, deca_att) + # Saisie de toutes les décisions de jury "automatiques" + # et vérification des résultats attendus: + for formsemestre in formsemestres: + formsemestre_validation_auto_but( + formsemestre, only_adm=False, no_overwrite=False + ) + yaml_setup.test_but_jury(formsemestre, doc) diff --git a/tests/unit/yaml_setup.py b/tests/unit/yaml_setup.py index 18c922aa..99729846 100644 --- a/tests/unit/yaml_setup.py +++ b/tests/unit/yaml_setup.py @@ -1,7 +1,45 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + """ -Met en place une base pour les tests, à partir d'une description YAML -qui peut donner la formation, son ref. compétences, les formsemestres, -les étudiants et leurs notes. +Met en place une base pour les tests unitaires, à partir d'une description +YAML qui peut donner la formation, son ref. compétences, les formsemestres, +les étudiants et leurs notes et décisions de jury. + +Le traitement est effectué dans l'ordre suivant: + +setup_from_yaml() + + - setup_but_formation(): + - import de la formation (le test utilise une seule formation) + - associe_ues_et_parcours(): + - crée les associations formation <-> référentiel de compétence + - setup_formsemestres() + - crée les formsemestres décrits dans le YAML + avec tous les modules du semestre ou des parcours si indiqués + et une évaluation dans chaque moduleimpl. + - inscrit_les_etudiants() + - inscrit et place dans les groupes de parcours + - note_les_modules() + - saisie de toutes les notes indiquées dans le YAML + +check_deca_fields() + - vérifie les champs du deca (nb UE, compétences, ...) mais pas les décisions de jury. + +formsemestre_validation_auto_but(only_adm=False) + - enregistre toutes les décisions "par défaut" proposées (pas seulement les ADM) + +test_but_jury() + - compare décisions attendues indiquées dans le YAML avec celles de ScoDoc + et enregistre immédiatement APRES la décision manuelle indiquée par `decision_jury` + dans le YAML. + + +Les tests unitaires associés sont généralement lents (construction de la base), +et donc marqués par `@pytest.mark.slow`. """ import os @@ -157,7 +195,7 @@ def _un_semestre( date_debut: str, date_fin: str, ) -> FormSemestre: - "Création d'un formsemetre" + "Création d'un formsemestre" formsemestre = FormSemestre( formation=formation, parcours=parcours, @@ -237,10 +275,21 @@ def note_les_modules(doc: dict): raise ValueError(f"module de code '{code_module}' introuvable") for evaluation in modimpl.evaluations: # s'il y a plusieurs evals, affecte la même note à chacune + note_value, invalid = sco_saisie_notes.convert_note_from_string( + str(note), + 20.0, + note_min=scu.NOTES_MIN, + etudid=etud.id, + absents=[], + tosuppress=[], + invalids=[], + ) + assert not invalid # valeur note invalide + assert isinstance(note_value, float) sco_saisie_notes.notes_add( a_user, evaluation.id, - [(etud.id, float(note))], + [(etud.id, note_value)], comment="note_les_modules", ) @@ -325,7 +374,7 @@ def setup_from_yaml(filename: str) -> dict: def _check_codes_jury(codes: list[str], codes_att: list[str]): """Vérifie (assert) la liste des codes l'ordre n'a pas d'importance ici. - Si codes_att contient un "...", on se contente de vérifie que + Si codes_att contient un "...", on se contente de vérifier que les codes de codes_att sont tous présents dans codes. """ codes_set = set(codes) @@ -340,7 +389,9 @@ def _check_codes_jury(codes: list[str], codes_att: list[str]): def _check_decisions_ues( decisions_ues: dict[int, DecisionsProposeesUE], decisions_ues_att: dict[str:dict] ): - """Vérifie les décisions d'UE""" + """Vérifie les décisions d'UE + puis enregistre décision manuelle si indiquée dans le YAML. + """ for acronyme, dec_ue_att in decisions_ues_att.items(): # retrouve l'UE ues_d = [ @@ -353,9 +404,23 @@ def _check_decisions_ues( if "codes" in dec_ue_att: _check_codes_jury(dec_ue.codes, dec_ue_att["codes"]) - for attr in ("moy_ue", "moy_ue_with_cap", "explanation", "code_valide"): + for attr in ("explanation", "code_valide"): if attr in dec_ue_att: - assert getattr(dec_ue, attr) == dec_ue_att[attr] + if getattr(dec_ue, attr) != dec_ue_att[attr]: + raise ValueError( + f"""Erreur: décision d'UE: {dec_ue.ue.acronyme + } : champs {attr}={getattr(dec_ue, attr)} != attendu {dec_ue_att[attr]}""" + ) + for attr in ("moy_ue", "moy_ue_with_cap"): + if attr in dec_ue_att: + assert ( + abs(getattr(dec_ue, attr) - dec_ue_att[attr]) < scu.NOTES_PRECISION + ) + # Force décision de jury: + code_manuel = dec_ue_att.get("decision_jury") + if code_manuel is not None: + assert code_manuel in dec_ue.codes + dec_ue.record(code_manuel) def _check_decisions_rcues( @@ -363,7 +428,7 @@ def _check_decisions_rcues( ): "Vérifie les décisions d'RCUEs" for acronyme, dec_rcue_att in decisions_rcues_att.items(): - # retrouve la décision RCUE à partir de l'acronyme de la 1er UE + # retrouve la décision RCUE à partir de l'acronyme de la 1ère UE rcues_d = [ dec_rcue for dec_rcue in decisions_rcues @@ -388,10 +453,18 @@ def _check_decisions_rcues( dec_rcue.rcue.est_compensable() == dec_rcue_att["rcue"]["est_compensable"] ) + # Force décision de jury: + code_manuel = dec_rcue_att.get("decision_jury") + if code_manuel is not None: + assert code_manuel in dec_rcue.codes + dec_rcue.record(code_manuel) def compare_decisions_annee(deca: DecisionsProposeesAnnee, deca_att: dict): - """Vérifie que les résultats de jury calculés sont ceux attendus. + """Vérifie que les résultats de jury calculés pour l'année, les RCUEs et les UEs + sont ceux attendus, + puis enregistre les décisions manuelles indiquées dans le YAML. + deca est le résultat calculé par ScoDoc deca_att est un dict lu du YAML """ @@ -412,3 +485,70 @@ def compare_decisions_annee(deca: DecisionsProposeesAnnee, deca_att: dict): _check_decisions_rcues( deca.decisions_rcue_by_niveau.values(), deca_att["decisions_rcues"] ) + # Force décision de jury: + code_manuel = deca_att.get("decision_jury") + if code_manuel is not None: + assert code_manuel in deca.codes + deca.record(code_manuel) + assert deca.recorded + + +def check_deca_fields(formsemestre: FormSemestre, etud: Identite = None): + """Vérifie les champs principaux (inscription, nb UE, nb compétences) + de l'instance de DecisionsProposeesAnnee. + Ne vérifie pas les décisions de jury proprement dites. + Si etud n'est pas spécifié, prend le premier inscrit trouvé dans le semestre. + """ + etud = etud or formsemestre.etuds.first() + assert etud # il faut au moins un étudiant dans le semestre + deca = DecisionsProposeesAnnee(etud, formsemestre) + assert deca.validation is None # pas encore de validation enregistrée + assert False is deca.recorded + assert deca.code_valide is None + if formsemestre.semestre_id % 2: + assert deca.formsemestre_impair == formsemestre + assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_impair + else: + assert deca.formsemestre_pair == formsemestre + assert formsemestre.query_ues_parcours_etud(etud.id).all() == deca.ues_pair + assert deca.inscription_etat == scu.INSCRIT + assert deca.inscription_etat_impair == scu.INSCRIT + assert (deca.parcour is None) or ( + deca.parcour.id in {p.id for p in formsemestre.parcours} + ) + + nb_ues = ( + len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all()) + if deca.formsemestre_pair + else 0 + ) + nb_ues += ( + len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all()) + if deca.formsemestre_impair + else 0 + ) + assert len(deca.decisions_ues) == nb_ues + + nb_ues_un_sem = ( + len(deca.formsemestre_impair.query_ues_parcours_etud(etud.id).all()) + if deca.formsemestre_impair + else len(deca.formsemestre_pair.query_ues_parcours_etud(etud.id).all()) + ) + assert len(deca.niveaux_competences) == nb_ues_un_sem + assert deca.nb_competences == nb_ues_un_sem + + +def test_but_jury(formsemestre: FormSemestre, doc: dict): + """Test jurys BUT + Vérifie les champs de DecisionsProposeesAnnee et UEs + """ + for etud in formsemestre.etuds: + deca = DecisionsProposeesAnnee(etud, formsemestre) + doc_formsemestre = doc["Etudiants"][etud.nom]["formsemestres"][ + formsemestre.titre + ] + assert doc_formsemestre + if "attendu" in doc_formsemestre: + if "deca" in doc_formsemestre["attendu"]: + deca_att = doc_formsemestre["attendu"]["deca"] + compare_decisions_annee(deca, deca_att) diff --git a/tools/anonymize_db.py b/tools/anonymize_db.py index 34543fe4..a6825098 100755 --- a/tools/anonymize_db.py +++ b/tools/anonymize_db.py @@ -6,7 +6,7 @@ # # Gestion scolarite IUT # -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 74e07a23..f884b82d 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -383,7 +383,7 @@ def ajouter_assiduites(formsemestre: FormSemestre): """ Ajoute des assiduités semi-aléatoires à chaque étudiant du semestre """ - MODS = [moduleimpl.id for moduleimpl in formsemestre.modimpls] + MODS = [moduleimpl for moduleimpl in formsemestre.modimpls] MODS.append(None) from app.scodoc.sco_utils import localize_datetime @@ -394,11 +394,13 @@ def ajouter_assiduites(formsemestre: FormSemestre): for i in range(random.randint(1, 5)): etat = random.randint(0, 2) - module = random.choice(MODS) + moduleimpl = random.choice(MODS) deb_date = base_date + datetime.timedelta(days=i) fin_date = deb_date + datetime.timedelta(hours=i) - code = Assiduite.create_assiduite(etud, deb_date, fin_date, etat, module) + code = Assiduite.create_assiduite( + etud, deb_date, fin_date, etat, moduleimpl + ) assert isinstance( code, Assiduite diff --git a/tools/upgrade.sh b/tools/upgrade.sh index 7a7d9b46..551ca1a8 100755 --- a/tools/upgrade.sh +++ b/tools/upgrade.sh @@ -19,6 +19,7 @@ source "$SCRIPT_DIR/utils.sh" cd "$SCODOC_DIR" || { echo "Invalid directory"; exit 1; } +export DEBIAN_FRONTEND=noninteractive check_uid_root "$0"