From e465acdd732ab7486b521cc75713b0ce4c4cbec0 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 26 Apr 2022 12:57:41 +0200 Subject: [PATCH 001/140] Add index on news --- .../versions/af77ca6a89d0_news_index.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 migrations/versions/af77ca6a89d0_news_index.py diff --git a/migrations/versions/af77ca6a89d0_news_index.py b/migrations/versions/af77ca6a89d0_news_index.py new file mode 100644 index 0000000000..3af1fb41cc --- /dev/null +++ b/migrations/versions/af77ca6a89d0_news_index.py @@ -0,0 +1,41 @@ +"""news index + +Revision ID: af77ca6a89d0 +Revises: e97b2a10f86c +Create Date: 2022-04-26 12:56:22.862451 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "af77ca6a89d0" +down_revision = "e97b2a10f86c" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index( + op.f("ix_scolar_news_authenticated_user"), + "scolar_news", + ["authenticated_user"], + unique=False, + ) + op.create_index(op.f("ix_scolar_news_date"), "scolar_news", ["date"], unique=False) + op.create_index( + op.f("ix_scolar_news_object"), "scolar_news", ["object"], unique=False + ) + op.create_index(op.f("ix_scolar_news_type"), "scolar_news", ["type"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_scolar_news_type"), table_name="scolar_news") + op.drop_index(op.f("ix_scolar_news_object"), table_name="scolar_news") + op.drop_index(op.f("ix_scolar_news_date"), table_name="scolar_news") + op.drop_index(op.f("ix_scolar_news_authenticated_user"), table_name="scolar_news") + # ### end Alembic commands ### From cbfc0ef641f19355462e672752564df1563c5d4a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 28 Apr 2022 05:28:24 +0200 Subject: [PATCH 002/140] db: merge migrations --- migrations/versions/af77ca6a89d0_news_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/versions/af77ca6a89d0_news_index.py b/migrations/versions/af77ca6a89d0_news_index.py index 3af1fb41cc..0a4d2edfa3 100644 --- a/migrations/versions/af77ca6a89d0_news_index.py +++ b/migrations/versions/af77ca6a89d0_news_index.py @@ -11,7 +11,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "af77ca6a89d0" -down_revision = "e97b2a10f86c" +down_revision = "d5b3bdd1d622" branch_labels = None depends_on = None From 1a18fef3e0cfed040f271a950bc3b125569abbc9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 29 Apr 2022 08:17:04 +0200 Subject: [PATCH 003/140] BUT: association UE <-> niveau competence --- app/but/apc_edit_ue.py | 68 +++++++++++++++ app/models/but_refcomp.py | 85 +++++++++++++++++-- app/models/ues.py | 4 + app/scodoc/sco_edit_ue.py | 14 +++ app/scodoc/sco_moduleimpl_status.py | 4 +- app/static/css/scodoc.css | 11 ++- app/static/js/edit_ue.js | 23 ++++- app/templates/pn/form_ues.html | 3 + app/views/notes.py | 11 ++- .../versions/6002d7d366e5_assoc_ue_niveau.py | 34 ++++++++ 10 files changed, 245 insertions(+), 12 deletions(-) create mode 100644 app/but/apc_edit_ue.py create mode 100644 migrations/versions/6002d7d366e5_assoc_ue_niveau.py diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py new file mode 100644 index 0000000000..1e439c71c8 --- /dev/null +++ b/app/but/apc_edit_ue.py @@ -0,0 +1,68 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +""" +Edition associations UE <-> Ref. Compétence +""" +from flask import g, url_for +from app import db, log +from app.models import UniteEns +from app.models.but_refcomp import ApcNiveau + + +def form_ue_choix_niveau(ue: UniteEns) -> str: + """Form. HTML pour associer une UE à un niveau de compétence""" + ref_comp = ue.formation.referentiel_competence + if ref_comp is None: + return """
pas de référentiel de compétence
""" + annee = (ue.semestre_idx + 1) // 2 # 1, 2, 3 + niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee) + + options = [] + if niveaux_by_parcours["TC"]: + options.append("""""") + for n in niveaux_by_parcours["TC"]: + options.append( + f"""""" + ) + options.append("""""") + for parcour in ref_comp.parcours: + if len(niveaux_by_parcours[parcour.id]): + options.append(f"""""") + for n in niveaux_by_parcours[parcour.id]: + options.append( + f"""""" + ) + options.append("""""") + options_str = "\n".join(options) + return f""" +
+
+ Niveau de compétence associé: + +
+
+ """ + + +def set_ue_niveau_competence(ue_id: int, niveau_id: int): + """Associe le niveau et l'UE""" + log(f"set_ue_niveau_competence( {ue_id}, {niveau_id} )") + ue = UniteEns.query.get_or_404(ue_id) + if niveau_id == "": + # suppression de l'association + ue.niveau_competence = None + else: + niveau = ApcNiveau.query.get_or_404(niveau_id) + ue.niveau_competence = niveau + db.session.add(ue) + db.session.commit() + return "", 204 diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 0750f1a9f6..6a9205fb35 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -7,6 +7,7 @@ """ from datetime import datetime +import flask_sqlalchemy from sqlalchemy.orm import class_mapper import sqlalchemy @@ -105,6 +106,52 @@ class ApcReferentielCompetences(db.Model, XMLModel): "parcours": {x.code: x.to_dict() for x in self.parcours}, } + def get_niveaux_by_parcours(self, annee) -> dict: + """ + Construit la liste des niveaux de compétences pour chaque parcours + de ce référentiel. + Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun. + Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut: + on cherche les niveaux qui sont présents dans tous les parcours et les range sous + la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun). + + résultat: + { + "TC" : [ ApcNiveau ], + parcour.id : [ ApcNiveau ] + } + """ + parcours = self.parcours.order_by(ApcParcours.numero).all() + niveaux_by_parcours = { + parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee) + for parcour in parcours + } + # Cherche tronc commun + niveaux_ids_tc = set.intersection( + *[ + {n.id for n in niveaux_by_parcours[parcour_id]} + for parcour_id in niveaux_by_parcours + ] + ) + # Enleve les niveaux du tronc commun + niveaux_by_parcours_no_tc = { + parcour.id: [ + niveau + for niveau in niveaux_by_parcours[parcour.id] + if niveau.id not in niveaux_ids_tc + ] + for parcour in parcours + } + # Niveaux du TC + niveaux_tc = [] + if len(parcours): + niveaux_parcours_1 = niveaux_by_parcours[parcours[0].id] + niveaux_tc = [ + niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc + ] + niveaux_by_parcours_no_tc["TC"] = niveaux_tc + return niveaux_by_parcours_no_tc + class ApcCompetence(db.Model, XMLModel): "Compétence" @@ -186,13 +233,20 @@ class ApcComposanteEssentielle(db.Model, XMLModel): class ApcNiveau(db.Model, XMLModel): + """Niveau de compétence + Chaque niveau peut être associé à deux UE, + des semestres impair et pair de la même année. + """ + + __tablename__ = "apc_niveau" + id = db.Column(db.Integer, primary_key=True) competence_id = db.Column( db.Integer, db.ForeignKey("apc_competence.id"), nullable=False ) libelle = db.Column(db.Text(), nullable=False) - annee = db.Column(db.Text(), nullable=False) # "BUT2" - # L'ordre est l'année d'apparition de ce niveau + annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3" + # L'ordre est le niveau (1,2,3) ou (1,2) suivant la competence ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3 app_critiques = db.relationship( "ApcAppCritique", @@ -200,9 +254,10 @@ class ApcNiveau(db.Model, XMLModel): lazy="dynamic", cascade="all, delete-orphan", ) + ues = db.relationship("UniteEns", back_populates="niveau_competence") def __repr__(self): - return f"<{self.__class__.__name__} ordre={self.ordre}>" + return f"<{self.__class__.__name__} ordre={self.ordre} annee={self.annee} {self.competence}>" def to_dict(self): return { @@ -212,6 +267,24 @@ class ApcNiveau(db.Model, XMLModel): "app_critiques": {x.code: x.to_dict() for x in self.app_critiques}, } + @classmethod + def niveaux_annee_de_parcours( + cls, parcour: "ApcParcours", annee: int + ) -> flask_sqlalchemy.BaseQuery: + """Les niveaux de l'année du parcours""" + if annee not in {1, 2, 3}: + raise ValueError("annee invalide pour un parcours BUT") + annee_formation = f"BUT{annee}" + return ApcNiveau.query.filter( + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ApcParcours.id == ApcAnneeParcours.parcours_id, + ApcParcours.referentiel == parcour.referentiel, + ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id, + ApcCompetence.id == ApcNiveau.competence_id, + ApcAnneeParcours.parcours == parcour, + ApcNiveau.annee == annee_formation, + ) + class ApcAppCritique(db.Model, XMLModel): "Apprentissage Critique BUT" @@ -281,9 +354,10 @@ class ApcAnneeParcours(db.Model, XMLModel): db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False ) ordre = db.Column(db.Integer) + "numéro de l'année: 1, 2, 3" def __repr__(self): - return f"<{self.__class__.__name__} ordre={self.ordre}>" + return f"<{self.__class__.__name__} ordre={self.ordre} parcours={self.parcours.code}>" def to_dict(self): return { @@ -321,6 +395,7 @@ class ApcParcoursNiveauCompetence(db.Model): "annee_parcours", passive_deletes=True, cascade="save-update, merge, delete, delete-orphan", + lazy="dynamic", ), ) annee_parcours = db.relationship( @@ -333,4 +408,4 @@ class ApcParcoursNiveauCompetence(db.Model): ) def __repr__(self): - return f"<{self.__class__.__name__} {self.competence} {self.annee_parcours}>" + return f"<{self.__class__.__name__} {self.competence}<->{self.annee_parcours} niveau={self.niveau}>" diff --git a/app/models/ues.py b/app/models/ues.py index 518bd72192..7d5e0cb9db 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -42,6 +42,10 @@ class UniteEns(db.Model): color = db.Column(db.Text()) + # BUT + niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id")) + niveau_competence = db.relationship("ApcNiveau", back_populates="ues") + # relations matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") modules = db.relationship("Module", lazy="dynamic", backref="ue") diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index fd25e5da3f..b400d881d9 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -35,6 +35,7 @@ from flask_login import current_user from app import db from app import log +from app.but import apc_edit_ue from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import Formation, UniteEns, ModuleImpl, Module from app.models import ScolarNews @@ -283,6 +284,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No

Note: sauf exception, l'UE n'a pas de coefficient associé. Seuls les modules ont des coefficients.

""", + f""" +

UE du semestre S{ue.semestre_idx}

+ """ + if is_apc + else "", ] ue_types = parcours.ALLOWED_UE_TYPES @@ -416,8 +422,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No form_descr, initvalues=initvalues, submitlabel=submitlabel, + cancelbutton="Revenir à la formation", ) if tf[0] == 0: + niveau_competence_div = "" + if ue and is_apc: + niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(ue) if ue and ue.modules.count() and ue.semestre_idx is not None: modules_div = f"""
{ue.modules.count()} modules sont rattachés @@ -435,6 +445,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No return ( "\n".join(H) + tf[1] + + niveau_competence_div + modules_div + bonus_div + ue_div @@ -1004,6 +1015,9 @@ def _ue_table_ues( }">transformer en UE ordinaire """ ) H.append("") + breakpoint() + if ue.niveau_competence is None: + H.append(" pas de compétence associée ") ue_editable = editable and not ue_is_locked(ue["ue_id"]) if ue_editable: H.append( diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index d6def36973..c568a7f626 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -194,8 +194,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): raise ScoInvalidIdType("moduleimpl_id must be an integer !") modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) M = modimpl.to_dict() - formsemestre_id = M["formsemestre_id"] - Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] + formsemestre_id = modimpl.formsemestre_id + Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0] sem = sco_formsemestre.get_formsemestre(formsemestre_id) F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 6a0d9e73d6..2c7d4652fb 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2206,7 +2206,7 @@ ul.notes_module_list { list-style-type: none; } -div#ue_list_modules { +div#ue_choix_niveau { background-color: rgb(191, 242, 255); border: 1px solid blue; border-radius: 10px; @@ -2215,6 +2215,15 @@ div#ue_list_modules { margin-right: 15px; } +div#ue_list_modules { + background-color: rgb(251, 225, 165); + border: 1px solid blue; + border-radius: 10px; + padding: 10px; + margin-top: 10px; + margin-right: 15px; +} + div#ue_list_etud_validations { background-color: rgb(220, 250, 220); padding-left: 4px; diff --git a/app/static/js/edit_ue.js b/app/static/js/edit_ue.js index 8424496ca9..0293a82ab0 100644 --- a/app/static/js/edit_ue.js +++ b/app/static/js/edit_ue.js @@ -25,10 +25,27 @@ function update_bonus_description() { } function update_ue_list() { - var ue_id = $("#tf_ue_id")[0].value; - var ue_code = $("#tf_ue_code")[0].value; - var query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id; + let ue_id = $("#tf_ue_id")[0].value; + let ue_code = $("#tf_ue_code")[0].value; + let query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id; $.get(query, '', function (data) { $("#ue_list_code").html(data); }); } + +function set_ue_niveau_competence() { + let ue_id = document.querySelector("#tf_ue_id").value; + let select = document.querySelector("#form_ue_choix_niveau select"); + let niveau_id = select.value; + let set_ue_niveau_competence_url = select.dataset.setter; + $.post(set_ue_niveau_competence_url, + { + ue_id: ue_id, + niveau_id: niveau_id, + }, + function (result) { + // obj.classList.remove("sco_wait"); + // obj.classList.add("sco_modified"); + } + ); +} \ No newline at end of file diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html index a91f29425a..c603c6c522 100644 --- a/app/templates/pn/form_ues.html +++ b/app/templates/pn/form_ues.html @@ -40,6 +40,9 @@ else 'aucun'|safe}} ECTS) + {% if ue.niveau_competence is none %} + pas de compétence associée + {% endif %} {% if editable and not ue.is_locked() %} Date: Sat, 30 Apr 2022 06:10:45 +0200 Subject: [PATCH 004/140] Fix @permission_required to avoid double call --- app/decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/decorators.py b/app/decorators.py index d6c6ed2346..83441275ec 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -87,10 +87,10 @@ def permission_required(permission): def decorated_function(*args, **kwargs): scodoc_dept = getattr(g, "scodoc_dept", None) if not current_user.has_permission(permission, scodoc_dept): - abort(403) + return current_app.login_manager.unauthorized() return f(*args, **kwargs) - return login_required(decorated_function) + return decorated_function return decorator From 72dc72d286ddd30a2afac4863a92e14871c5ec20 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 1 May 2022 23:58:41 +0200 Subject: [PATCH 005/140] WIP: BUT association modules <-> parcours --- app/models/__init__.py | 13 +- app/models/but_refcomp.py | 20 + app/models/modules.py | 9 + app/scodoc/sco_edit_apc.py | 15 +- app/scodoc/sco_edit_module.py | 430 +++++++++++++----- app/scodoc/sco_edit_ue.py | 9 +- app/templates/pn/form_mods.html | 6 +- .../versions/6002d7d366e5_assoc_ue_niveau.py | 30 +- tests/unit/test_refcomp.py | 44 +- 9 files changed, 441 insertions(+), 135 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index d29b6bf3b1..d259966ff2 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,14 +1,25 @@ # -*- coding: UTF-8 -* """Modèles base de données ScoDoc -XXX version préliminaire ScoDoc8 #sco8 sans département """ +import sqlalchemy + CODE_STR_LEN = 16 # chaine pour les codes SHORT_STR_LEN = 32 # courtes chaine, eg acronymes APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs) GROUPNAME_STR_LEN = 64 +convention = { + "ix": "ix_%(column_0_label)s", + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s", +} + +metadata_obj = sqlalchemy.MetaData(naming_convention=convention) + from app.models.raw_sql_init import create_database_functions from app.models.absences import Absence, AbsenceNotification, BilletAbsence diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 6a9205fb35..3b28676514 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -321,6 +321,21 @@ ApcAppCritiqueModules = db.Table( ) +parcours_modules = db.Table( + "parcours_modules", + db.Column( + "parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True + ), + db.Column( + "module_id", + db.Integer, + db.ForeignKey("notes_modules.id", ondelete="CASCADE"), + primary_key=True, + ), +) +"""Association parcours <-> modules (many-to-many)""" + + class ApcParcours(db.Model, XMLModel): id = db.Column(db.Integer, primary_key=True) referentiel_id = db.Column( @@ -335,6 +350,11 @@ class ApcParcours(db.Model, XMLModel): lazy="dynamic", cascade="all, delete-orphan", ) + # modules = db.relationship( + # "Module", + # secondary=parcours_modules, + # back_populates="parcours", + # ) def __repr__(self): return f"<{self.__class__.__name__} {self.code}>" diff --git a/app/models/modules.py b/app/models/modules.py index 67ff3de0df..5ffac90435 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -3,6 +3,7 @@ from app import db from app.models import APO_CODE_STR_LEN +from app.models.but_refcomp import parcours_modules from app.scodoc import sco_utils as scu from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -44,6 +45,14 @@ class Module(db.Model): lazy=True, backref=db.backref("modules", lazy=True), ) + # BUT + parcours = db.relationship( + "ApcParcours", + secondary=parcours_modules, + lazy="subquery", + # cascade="all, delete", + backref=db.backref("modules", lazy=True), + ) def __init__(self, **kwargs): self.ue_coefs = [] diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index c53094ef53..11bb53af19 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -134,7 +134,10 @@ def html_edit_formation_apc( tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else "", render_template( "pn/form_mods.html", formation=formation, @@ -147,7 +150,10 @@ def html_edit_formation_apc( tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else "", render_template( "pn/form_mods.html", formation=formation, @@ -159,7 +165,10 @@ def html_edit_formation_apc( tag_editable=tag_editable, icons=icons, scu=scu, - ), + semestre_id=semestre_idx, + ) + if ues_by_sem[semestre_idx].count() > 0 + else """créer une UE pour pouvoir ajouter des modules""", ] return "\n".join(H) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ece30a3455..3aa00452e9 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -33,12 +33,13 @@ from flask import url_for, render_template from flask import g, request from flask_login import current_user -from app import log +from app import db, log from app import models from app.models import APO_CODE_STR_LEN from app.models import Formation, Matiere, Module, UniteEns from app.models import FormSemestre, ModuleImpl from app.models import ScolarNews +from app.models.but_refcomp import ApcParcours import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -121,6 +122,13 @@ def module_create( Sinon, donne le choix de l'UE de rattachement et utilise la première matière de cette UE (si elle n'existe pas, la crée). """ + return module_edit( + create=True, + matiere_id=matiere_id, + module_type=module_type, + semestre_id=semestre_id, + formation_id=formation_id, + ) if matiere_id: matiere = Matiere.query.get_or_404(matiere_id) ue = matiere.ue @@ -472,30 +480,56 @@ def check_module_code_unicity(code, field, formation_id, module_id=None): return len(Mods) == 0 -def module_edit(module_id=None): - """Edit a module""" - from app.scodoc import sco_formations +def module_edit( + module_id=None, + create=False, + matiere_id=None, + module_type=None, + semestre_id=None, + formation_id=None, +): + """Formulaire édition ou création module. + Si create, création nouveau module. + Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal). + Sinon, donne le choix de l'UE de rattachement et utilise la première matière + de cette UE (si elle n'existe pas, la crée). + """ from app.scodoc import sco_tag_module - if not module_id: - raise ScoValueError("invalid module !") - modules = module_list(args={"module_id": module_id}) - if not modules: - raise ScoValueError("invalid module !") - module = modules[0] - a_module = models.Module.query.get(module_id) - unlocked = not module_is_locked(module_id) - formation_id = module["formation_id"] - formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] - parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) + # --- Détermination de la formation + orig_semestre_idx = None + if create: + if matiere_id: + matiere = Matiere.query.get_or_404(matiere_id) + ue = matiere.ue + formation = ue.formation + orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id + else: + formation = Formation.query.get_or_404(formation_id) + module = None + unlocked = True + else: + if not module_id: + raise ValueError("missing module_id !") + module = models.Module.query.get_or_404(module_id) + module_dict = module.to_dict() + formation = module.formation + unlocked = not module_is_locked(module_id) + + parcours = sco_codes_parcours.get_parcours_from_code(formation.type_parcours) is_apc = parcours.APC_SAE # BUT - in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls + if not create: + orig_semestre_idx = module.ue.semestre_idx if is_apc else module.semestre_id + if orig_semestre_idx is None: + orig_semestre_idx = 1 + # il y a-t-il des modimpls ? + in_use = (module is not None) and (len(module.modimpls.all()) > 0) matieres = Matiere.query.filter( - Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation_id + Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation.id ).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero) if in_use: # restreint aux matières du même semestre - matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx) + matieres = matieres.filter(UniteEns.semestre_idx == module.ue.semestre_idx) if is_apc: # ne conserve que la 1ere matière de chaque UE, @@ -503,7 +537,8 @@ def module_edit(module_id=None): matieres = [ mat for mat in matieres - if a_module.matiere.id == mat.id or mat.id == mat.ue.matieres.first().id + if ((module is not None) and (module.matiere.id == mat.id)) + or (mat.id == mat.ue.matieres.first().id) ] mat_names = [ "S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres @@ -511,14 +546,43 @@ def module_edit(module_id=None): else: mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] - ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] - module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) + if module: # edition + ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] + module_dict["ue_matiere_id"] = "%s!%s" % ( + module_dict["ue_id"], + module_dict["matiere_id"], + ) semestres_indices = list(range(1, parcours.NB_SEM + 1)) + # Toutes les UE de la formation (tout parcours): + ues = formation.ues.order_by( + UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme + ).all() + + # --- Titre de la page + if create: + if is_apc and module_type is not None: + object_name = scu.MODULE_TYPE_NAMES[module_type] + else: + object_name = "Module" + page_title = f"Création {object_name}" + if matiere_id: + title = f"""Création {object_name} dans la matière + {matiere.titre}, + (UE {ue.acronyme}), semestre {ue.semestre_idx} + """ + else: + title = f"""Création {object_name} dans la formation + {formation.acronyme}""" + else: + page_title = "Modification du module {module.code or module.titre or ''}" + title = f"""Modification du module {module.code or ''} {module.titre or ''} + (formation {formation.acronyme}, version {formation.version}) + """ H = [ html_sco_header.sco_header( - page_title=f"Modification du module {a_module.code or a_module.titre or ''}", + page_title=page_title, cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"], javascripts=[ "libjs/jQuery-tagEditor/jquery.tag-editor.min.js", @@ -526,17 +590,19 @@ def module_edit(module_id=None): "js/module_tag_editor.js", ], ), - f"""

Modification du module {a_module.code or ''} {a_module.titre or ''}""", - """ (formation %(acronyme)s, version %(version)s)

""" % formation, + f"""

{title}

""", render_template( "scodoc/help/modules.html", is_apc=is_apc, + semestre_id=semestre_id, formsemestres=FormSemestre.query.filter( ModuleImpl.formsemestre_id == FormSemestre.id, ModuleImpl.module_id == module_id, ) .order_by(FormSemestre.date_debut) - .all(), + .all() + if not create + else None, ), ] if not unlocked: @@ -547,28 +613,55 @@ def module_edit(module_id=None): module_types = scu.ModuleType # tous les types else: # ne propose pas SAE et Ressources, sauf si déjà de ce type... - module_types = ( - set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} - ) | { - scu.ModuleType(a_module.module_type) - if a_module.module_type - else scu.ModuleType.STANDARD + module_types = set(scu.ModuleType) - { + scu.ModuleType.RESSOURCE, + scu.ModuleType.SAE, } + if module: + module_types |= { + scu.ModuleType(module.module_type) + if module.module_type + else scu.ModuleType.STANDARD + } + # Numéro du module + # cherche le numero adéquat (pour placer le module en fin de liste) + if module: + default_num = module.numero + else: + modules = formation.modules.all() + if modules: + default_num = max([m.numero or 0 for m in modules]) + 10 + else: + default_num = 10 descr = [ ( "code", { "size": 10, - "explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)", + "explanation": "code du module (issu du programme, exemple M1203, R2.01 , ou SAÉ 3.4. Doit être unique dans la formation)", "allow_null": False, - "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( - val, field, formation_id, module_id=module_id + "validator": lambda val, field, formation_id=formation.id: check_module_code_unicity( + val, field, formation_id, module_id=module.id if module else None ), }, ), - ("titre", {"size": 30, "explanation": "nom du module"}), - ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}), + ( + "titre", + { + "size": 30, + "explanation": """nom du module. Exemple: + Introduction à la démarche ergonomique""", + }, + ), + ( + "abbrev", + { + "size": 20, + "explanation": """nom abrégé (pour bulletins). + Exemple: Intro. à l'ergonomie""", + }, + ), ( "module_type", { @@ -583,50 +676,63 @@ def module_edit(module_id=None): ( "heures_cours", { - "title": "Heures CM :", + "title": "Heures cours :", "size": 4, "type": "float", - "explanation": "nombre d'heures de cours", + "explanation": "nombre d'heures de cours (optionnel)", }, ), ( "heures_td", { - "title": "Heures TD :", + "title": "Heures de TD :", "size": 4, "type": "float", - "explanation": "nombre d'heures de Travaux Dirigés", + "explanation": "nombre d'heures de Travaux Dirigés (optionnel)", }, ), ( "heures_tp", { - "title": "Heures TP :", + "title": "Heures de TP :", "size": 4, "type": "float", - "explanation": "nombre d'heures de Travaux Pratiques", + "explanation": "nombre d'heures de Travaux Pratiques (optionnel)", }, ), ] if is_apc: - coefs_lst = a_module.ue_coefs_list() - if coefs_lst: - coefs_descr_txt = ", ".join( - [f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst] - ) + if module: + coefs_lst = module.ue_coefs_list() + if coefs_lst: + coefs_descr_txt = ", ".join( + [f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst] + ) + else: + coefs_descr_txt = """non définis""" + descr += [ + ( + "ue_coefs", + { + "readonly": True, + "title": "Coefficients vers les UE ", + "default": coefs_descr_txt, + "explanation": "
(passer par la page d'édition de la formation pour modifier les coefficients)", + }, + ) + ] else: - coefs_descr_txt = """non définis""" - descr += [ - ( - "ue_coefs", - { - "readonly": True, - "title": "Coefficients vers les UE ", - "default": coefs_descr_txt, - "explanation": "
(passer par la page d'édition de la formation pour modifier les coefficients)", - }, - ) - ] + descr += [ + ( + "sep_ue_coefs", + { + "input_type": "separator", + "title": """ +
(les coefficients vers les UE se fixent sur la page dédiée) +
""", + }, + ), + ] else: # Module classique avec coef scalaire: descr += [ ( @@ -641,30 +747,64 @@ def module_edit(module_id=None): ), ] descr += [ - ("formation_id", {"input_type": "hidden"}), - ("ue_id", {"input_type": "hidden"}), - ("module_id", {"input_type": "hidden"}), ( - "ue_matiere_id", + "formation_id", { - "input_type": "menu", - "title": "Rattachement :" if is_apc else "Matière :", - "explanation": ( - "UE de rattachement" - + ( - " module utilisé, ne peut pas être changé de semestre" - if in_use - else "" - ) - ) - if is_apc - else "un module appartient à une seule matière.", - "labels": mat_names, - "allowed_values": ue_mat_ids, - "enabled": unlocked, + "input_type": "hidden", + "default": formation.id, }, ), ] + if module: + descr += [ + ("ue_id", {"input_type": "hidden"}), + ("module_id", {"input_type": "hidden"}), + ( + "ue_matiere_id", + { + "input_type": "menu", + "title": "Rattachement :" if is_apc else "Matière :", + "explanation": ( + "UE de rattachement, utilisée notamment pour les malus" + + ( + " (module utilisé, ne peut pas être changé de semestre)" + if in_use + else "" + ) + ) + if is_apc + else "un module appartient à une seule matière.", + "labels": mat_names, + "allowed_values": ue_mat_ids, + "enabled": unlocked, + }, + ), + ] + else: # Création + if matiere_id: + descr += [ + ("ue_id", {"default": ue.id, "input_type": "hidden"}), + ("matiere_id", {"default": matiere_id, "input_type": "hidden"}), + ] + else: + # choix de l'UE de rattachement + descr += [ + ( + "ue_id", + { + "input_type": "menu", + "type": "int", + "title": "UE de rattachement", + "explanation": "utilisée notamment pour les malus", + "labels": [ + f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}" + for u in ues + ], + "allowed_values": [u.id for u in ues], + }, + ), + ] + if is_apc: # le semestre du module est toujours celui de son UE descr += [ @@ -710,17 +850,56 @@ def module_edit(module_id=None): "numero", { "size": 2, - "explanation": "numéro (1,2,3,4...) pour ordre d'affichage", + "explanation": "numéro (1, 2, 3, 4, ...) pour ordre d'affichage", "type": "int", + "default": default_num, }, ), ] + # Choix des parcours + if is_apc: + ref_comp = formation.referentiel_competence + if ref_comp: + descr += [ + ( + "parcours", + { + "input_type": "checkbox", + "vertical": True, + "labels": [parcour.libelle for parcour in ref_comp.parcours], + "allowed_values": [ + str(parcour.id) for parcour in ref_comp.parcours + ], + "explanation": "parcours dans lesquels est utilisé ce module.", + }, + ) + ] + if module: + module_dict["parcours"] = [ + str(parcour.id) for parcour in module.parcours + ] + else: + descr += [ + ( + "parcours", + { + "input_type": "separator", + "title": f"""Pas de parcours: +
associer un référentiel de compétence + """, + }, + ) + ] # force module semestre_idx to its UE - if a_module.ue.semestre_idx: - module["semestre_id"] = a_module.ue.semestre_idx - # Filet de sécurité si jamais l'UE n'a pas non plus de semestre: - if not module["semestre_id"]: - module["semestre_id"] = 1 + if module: + if module.ue.semestre_idx is None: + # Filet de sécurité si jamais l'UE n'a pas non plus de semestre: + module_dict["semestre_id"] = 1 + else: + module_dict["semestre_id"] = module.ue.semestre_idx + tf = TrivialFormulator( request.base_url, scu.get_request_args(), @@ -728,8 +907,9 @@ def module_edit(module_id=None): html_foot_markup="""
""".format( module_id, ",".join(sco_tag_module.module_tag_list(module_id)) ), - initvalues=module, - submitlabel="Modifier ce module", + initvalues=module_dict if module else {}, + submitlabel="Modifier ce module" if module else "Créer ce module", + cancelbutton="Annuler", ) # if tf[0] == 0: @@ -739,38 +919,66 @@ def module_edit(module_id=None): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, - semestre_idx=module["semestre_id"], + formation_id=formation.id, + semestre_idx=orig_semestre_idx, ) ) else: - # l'UE de rattachement peut changer - tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") - x, y = tf[2]["ue_matiere_id"].split("!") - tf[2]["ue_id"] = int(x) - tf[2]["matiere_id"] = int(y) - old_ue_id = a_module.ue.id - new_ue_id = tf[2]["ue_id"] - if (old_ue_id != new_ue_id) and in_use: - new_ue = UniteEns.query.get_or_404(new_ue_id) - if new_ue.semestre_idx != a_module.ue.semestre_idx: - # pas changer de semestre un module utilisé ! - raise ScoValueError( - "Module utilisé: il ne peut pas être changé de semestre !" - ) - # En APC, force le semestre égal à celui de l'UE - if is_apc: - selected_ue = UniteEns.query.get(tf[2]["ue_id"]) - if selected_ue is None: - raise ValueError("UE invalide") - tf[2]["semestre_id"] = selected_ue.semestre_idx - # Check unicité code module dans la formation - do_module_edit(tf[2]) + if create: + if not matiere_id: + # formulaire avec choix UE de rattachement + ue = UniteEns.query.get(tf[2]["ue_id"]) + if ue is None: + raise ValueError("UE invalide") + matiere = ue.matieres.first() + if matiere: + tf[2]["matiere_id"] = matiere.id + else: + matiere_id = sco_edit_matiere.do_matiere_create( + {"ue_id": ue.id, "titre": ue.titre, "numero": 1}, + ) + tf[2]["matiere_id"] = matiere_id + + tf[2]["semestre_id"] = ue.semestre_idx + module_id = do_module_create(tf[2]) + module = Module.query.get(module_id) + else: # EDITION MODULE + # l'UE de rattachement peut changer + tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") + x, y = tf[2]["ue_matiere_id"].split("!") + tf[2]["ue_id"] = int(x) + tf[2]["matiere_id"] = int(y) + old_ue_id = module.ue.id + new_ue_id = tf[2]["ue_id"] + if (old_ue_id != new_ue_id) and in_use: + new_ue = UniteEns.query.get_or_404(new_ue_id) + if new_ue.semestre_idx != module.ue.semestre_idx: + # pas changer de semestre un module utilisé ! + raise ScoValueError( + "Module utilisé: il ne peut pas être changé de semestre !" + ) + # En APC, force le semestre égal à celui de l'UE + if is_apc: + selected_ue = UniteEns.query.get(tf[2]["ue_id"]) + if selected_ue is None: + raise ValueError("UE invalide") + tf[2]["semestre_id"] = selected_ue.semestre_idx + # Check unicité code module dans la formation + # ??? TODO + # + do_module_edit(tf[2]) + # Modifie les parcours + module.parcours = [ + ApcParcours.query.get(int(parcour_id_str)) + for parcour_id_str in tf[2]["parcours"] + ] + db.session.add(module) + db.session.commit() return flask.redirect( url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, + formation_id=formation.id, semestre_idx=tf[2]["semestre_id"], ) ) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index b400d881d9..066a49a521 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -255,7 +255,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No title = f"Modification de l'UE {ue.acronyme} {ue.titre}" initvalues = ue_dict submitlabel = "Modifier les valeurs" - can_change_semestre_id = (ue.modules.count() == 0) or (ue.semestre_idx is None) + can_change_semestre_id = ( + (ue.modules.count() == 0) or (ue.semestre_idx is None) + ) and ue.niveau_competence is None else: ue = None title = "Création d'une UE" @@ -287,7 +289,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No f"""

UE du semestre S{ue.semestre_idx}

""" - if is_apc + if is_apc and ue else "", ] @@ -1015,9 +1017,6 @@ def _ue_table_ues( }">transformer en UE ordinaire """ ) H.append("") - breakpoint() - if ue.niveau_competence is None: - H.append(" pas de compétence associée ") ue_editable = editable and not ue_is_locked(ue["ue_id"]) if ue_editable: H.append( diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html index 10927bc88b..4e3031ab28 100644 --- a/app/templates/pn/form_mods.html +++ b/app/templates/pn/form_mods.html @@ -84,13 +84,15 @@ url_for("notes.module_create", scodoc_dept=g.scodoc_dept, module_type=module_type|int, - matiere_id=matiere_parent.id + matiere_id=matiere_parent.id, + semestre_id=semestre_id, )}}" {% else %}"{{ url_for("notes.module_create", scodoc_dept=g.scodoc_dept, module_type=module_type|int, - formation_id=formation.id + formation_id=formation.id, + semestre_id=semestre_id, )}}" {% endif %} >{{create_element_msg}} diff --git a/migrations/versions/6002d7d366e5_assoc_ue_niveau.py b/migrations/versions/6002d7d366e5_assoc_ue_niveau.py index 449ff06cc0..cf6c5932e2 100644 --- a/migrations/versions/6002d7d366e5_assoc_ue_niveau.py +++ b/migrations/versions/6002d7d366e5_assoc_ue_niveau.py @@ -22,13 +22,39 @@ def upgrade(): "notes_ue", sa.Column("niveau_competence_id", sa.Integer(), nullable=True) ) op.create_foreign_key( - None, "notes_ue", "apc_niveau", ["niveau_competence_id"], ["id"] + "notes_ue_niveau_competence_id_fkey", + "notes_ue", + "apc_niveau", + ["niveau_competence_id"], + ["id"], + ) + op.create_table( + "parcours_modules", + sa.Column("parcours_id", sa.Integer(), nullable=False), + sa.Column("module_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["module_id"], + ["notes_modules.id"], + # nom ajouté manuellement: + name="parcours_modules_module_id_fkey", + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["parcours_id"], + ["apc_parcours.id"], + # nom ajouté manuellement: + name="parcours_modules_parcours_id_fkey", + ), + sa.PrimaryKeyConstraint("parcours_id", "module_id"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(None, "notes_ue", type_="foreignkey") + op.drop_constraint( + "notes_ue_niveau_competence_id_fkey", "notes_ue", type_="foreignkey" + ) op.drop_column("notes_ue", "niveau_competence_id") + op.drop_table("parcours_modules") # ### end Alembic commands ### diff --git a/tests/unit/test_refcomp.py b/tests/unit/test_refcomp.py index f9e92a37d6..2e56ed3c28 100644 --- a/tests/unit/test_refcomp.py +++ b/tests/unit/test_refcomp.py @@ -4,35 +4,57 @@ Utiliser par exemple comme: pytest tests/unit/test_refcomp.py """ -import io + from flask import g -import app + from app import db from app import models from app.but.import_refcomp import orebut_import_refcomp +from app.models import UniteEns from app.models.but_refcomp import ( ApcReferentielCompetences, ApcCompetence, ApcSituationPro, + ApcNiveau, ) +from tests.unit import setup + +REF_RT_XML = open( + "ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml" +).read() + def test_but_refcomp(test_client): """modèles ref. comp.""" - xml_data = open( - "ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml" - ).read() dept_id = models.Departement.query.first().id - ref = orebut_import_refcomp(xml_data, dept_id) - assert ref.competences.count() == 13 - assert ref.competences[0].situations.count() == 3 - assert ref.competences[0].situations[0].libelle.startswith("Conception ") + ref_comp: ApcReferentielCompetences = orebut_import_refcomp(REF_RT_XML, dept_id) + assert ref_comp.competences.count() == 13 + assert ref_comp.competences[0].situations.count() == 3 + assert ref_comp.competences[0].situations[0].libelle.startswith("Conception ") assert ( - ref.competences[-1].situations[-1].libelle + ref_comp.competences[-1].situations[-1].libelle == "Administration des services multimédia" ) # test cascades on delete - db.session.delete(ref) + db.session.delete(ref_comp) db.session.commit() assert ApcCompetence.query.count() == 0 assert ApcSituationPro.query.count() == 0 + + +def test_but_assoc_ue_parcours(test_client): + """Association UE / Niveau compétence""" + dept_id = models.Departement.query.first().id + G, formation_id, (ue1_id, ue2_id, ue3_id), module_ids = setup.build_formation_test() + ref_comp: ApcReferentielCompetences = orebut_import_refcomp(REF_RT_XML, dept_id) + ue = UniteEns.query.get(ue1_id) + assert ue.niveau_competence is None + niveau = ApcNiveau.query.first() + ue.niveau_competence = niveau + db.session.add(ue) + db.session.commit() + ue = UniteEns.query.get(ue1_id) + assert ue.niveau_competence == niveau + assert len(niveau.ues) == 1 + assert niveau.ues[0] == ue From c11fccab02409c4398571d62495c10100278444f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 2 May 2022 10:54:52 +0200 Subject: [PATCH 006/140] Edition tag sur modules --- app/scodoc/sco_edit_module.py | 7 +++++-- app/scodoc/sco_tag_module.py | 4 ++-- app/static/css/scodoc.css | 11 +++++++++++ app/views/notes.py | 17 +++++++++++------ 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 3aa00452e9..57b4f81050 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -866,6 +866,7 @@ def module_edit( { "input_type": "checkbox", "vertical": True, + "dom_id": "tf_module_parcours", "labels": [parcour.libelle for parcour in ref_comp.parcours], "allowed_values": [ str(parcour.id) for parcour in ref_comp.parcours @@ -904,9 +905,11 @@ def module_edit( request.base_url, scu.get_request_args(), descr, - html_foot_markup="""
""".format( + html_foot_markup="""
""".format( module_id, ",".join(sco_tag_module.module_tag_list(module_id)) - ), + ) + if not create + else "", initvalues=module_dict if module else {}, submitlabel="Modifier ce module" if module else "Créer ce module", cancelbutton="Annuler", diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index 1b41cd876e..20c340f1ff 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -235,7 +235,7 @@ def module_tag_list(module_id=""): def module_tag_set(module_id="", taglist=None): """taglist may either be: - a string with tag names separated by commas ("un;deux") + a string with tag names separated by commas ("un,deux") or a list of strings (["un", "deux"]) """ if not taglist: @@ -243,7 +243,7 @@ def module_tag_set(module_id="", taglist=None): elif isinstance(taglist, str): taglist = taglist.split(",") taglist = [t.strip() for t in taglist] - # log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist)) + log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist)) # Sanity check: Mod = sco_edit_module.module_list(args={"module_id": module_id}) if not Mod: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 2c7d4652fb..170d191744 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2267,6 +2267,10 @@ span.missing_value { color: red; } +tr#tf_module_parcours>td { + background-color: rgb(229, 229, 229); +} + /* tableau recap notes */ table.notes_recapcomplet { border: 2px solid blue; @@ -3635,6 +3639,13 @@ span.sco_tag_edit .tag-editor { margin-top: 2px; } +div.sco_tag_module_edit span.sco_tag_edit .tag-editor { + background-color: rgb(210, 210, 210); + border: 0px; + margin-left: 0px; + margin-top: 2px; +} + span.sco_tag_edit .tag-editor-delete { height: 20px; } diff --git a/app/views/notes.py b/app/views/notes.py index e288a9bc4a..c8b6cc20af 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -543,12 +543,17 @@ sco_publish( ) sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) -sco_publish( - "/module_tag_set", - sco_tag_module.module_tag_set, - Permission.ScoEditFormationTags, - methods=["GET", "POST"], -) + + +@bp.route("/module_tag_set", methods=["POST"]) +@scodoc +@permission_required(Permission.ScoEditFormationTags) +def module_tag_set(): + """Set tags on module""" + module_id = int(request.form.get("module_id")) + taglist = request.form.get("taglist") + return sco_tag_module.module_tag_set(module_id, taglist) + # @bp.route("/") From f537cd2e48a2fb1fd365a9b93eef4472d3ccf348 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 2 May 2022 11:13:56 +0200 Subject: [PATCH 007/140] Code cleaning --- app/scodoc/sco_edit_module.py | 257 +++------------------------------- 1 file changed, 17 insertions(+), 240 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 57b4f81050..2317d8b4ce 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -129,235 +129,6 @@ def module_create( semestre_id=semestre_id, formation_id=formation_id, ) - if matiere_id: - matiere = Matiere.query.get_or_404(matiere_id) - ue = matiere.ue - formation = ue.formation - else: - formation = Formation.query.get_or_404(formation_id) - parcours = formation.get_parcours() - is_apc = parcours.APC_SAE - ues = formation.ues.order_by( - UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme - ).all() - # cherche le numero adéquat (pour placer le module en fin de liste) - modules = formation.modules.all() - if modules: - default_num = max([m.numero or 0 for m in modules]) + 10 - else: - default_num = 10 - - if is_apc and module_type is not None: - object_name = scu.MODULE_TYPE_NAMES[module_type] - else: - object_name = "Module" - H = [ - html_sco_header.sco_header(page_title=f"Création {object_name}"), - ] - if not matiere_id: - H += [ - f"""

Création {object_name} dans la formation {formation.acronyme} -

- """ - ] - else: - H += [ - f"""

Création {object_name} dans la matière {matiere.titre}, - (UE {ue.acronyme}), semestre {ue.semestre_idx}

- """ - ] - - H += [ - render_template( - "scodoc/help/modules.html", - is_apc=is_apc, - semestre_id=semestre_id, - ) - ] - - descr = [ - ( - "code", - { - "size": 10, - "explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.", - "allow_null": False, - "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( - val, field, formation_id - ), - }, - ), - ( - "titre", - { - "size": 30, - "explanation": "nom du module. Exemple: Introduction à la démarche ergonomique", - }, - ), - ( - "abbrev", - { - "size": 20, - "explanation": "nom abrégé (pour les bulletins). Exemple: Intro. à l'ergonomie", - }, - ), - ] - - if is_apc: - module_types = scu.ModuleType # tous les types - else: - # ne propose pas SAE et Ressources: - module_types = set(scu.ModuleType) - { - scu.ModuleType.RESSOURCE, - scu.ModuleType.SAE, - } - - descr += [ - ( - "module_type", - { - "input_type": "menu", - "title": "Type", - "explanation": "", - "labels": [x.name.capitalize() for x in module_types], - "allowed_values": [str(int(x)) for x in module_types], - }, - ), - ( - "heures_cours", - { - "title": "Heures de cours", - "size": 4, - "type": "float", - "explanation": "nombre d'heures de cours (optionnel)", - }, - ), - ( - "heures_td", - { - "title": "Heures de TD", - "size": 4, - "type": "float", - "explanation": "nombre d'heures de Travaux Dirigés (optionnel)", - }, - ), - ( - "heures_tp", - { - "title": "Heures de TP", - "size": 4, - "type": "float", - "explanation": "nombre d'heures de Travaux Pratiques (optionnel)", - }, - ), - ] - if is_apc: - descr += [ - ( - "sep_ue_coefs", - { - "input_type": "separator", - "title": """ -
(les coefficients vers les UE se fixent sur la page dédiée) -
""", - }, - ), - ] - else: - descr += [ - ( - "coefficient", - { - "size": 4, - "type": "float", - "explanation": "coefficient dans la formation (PPN)", - "allow_null": False, - }, - ), - ] - - if matiere_id: - descr += [ - ("ue_id", {"default": ue.id, "input_type": "hidden"}), - ("matiere_id", {"default": matiere_id, "input_type": "hidden"}), - ] - else: - # choix de l'UE de rattachement - descr += [ - ( - "ue_id", - { - "input_type": "menu", - "type": "int", - "title": "UE de rattachement", - "explanation": "utilisée notamment pour les malus", - "labels": [ - f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}" - for u in ues - ], - "allowed_values": [u.id for u in ues], - }, - ), - ] - - descr += [ - # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }), - ("formation_id", {"default": formation.id, "input_type": "hidden"}), - ( - "code_apogee", - { - "title": "Code Apogée", - "size": 25, - "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", - "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, - }, - ), - ( - "numero", - { - "size": 2, - "explanation": "numéro (1,2,3,4...) pour ordre d'affichage", - "type": "int", - "default": default_num, - }, - ), - ] - args = scu.get_request_args() - tf = TrivialFormulator( - request.base_url, - args, - descr, - submitlabel="Créer ce module", - ) - if tf[0] == 0: - return "\n".join(H) + tf[1] + html_sco_header.sco_footer() - else: - if not matiere_id: - # formulaire avec choix UE de rattachement - ue = UniteEns.query.get(tf[2]["ue_id"]) - if ue is None: - raise ValueError("UE invalide") - matiere = ue.matieres.first() - if matiere: - tf[2]["matiere_id"] = matiere.id - else: - matiere_id = sco_edit_matiere.do_matiere_create( - {"ue_id": ue.id, "titre": ue.titre, "numero": 1}, - ) - tf[2]["matiere_id"] = matiere_id - - tf[2]["semestre_id"] = ue.semestre_idx - - _ = do_module_create(tf[2]) - - return flask.redirect( - url_for( - "notes.ue_table", - scodoc_dept=g.scodoc_dept, - formation_id=formation.id, - semestre_idx=tf[2]["semestre_id"], - ) - ) def can_delete_module(module): @@ -367,8 +138,6 @@ def can_delete_module(module): def do_module_delete(oid): "delete module" - from app.scodoc import sco_formations - module = Module.query.get_or_404(oid) mod = module_list({"module_id": oid})[0] # sco7 if module_is_locked(module.id): @@ -388,9 +157,14 @@ def do_module_delete(oid): # S'il y a des moduleimpls, on ne peut pas detruire le module ! mods = sco_moduleimpl.moduleimpl_list(module_id=oid) if mods: - err_page = f"""

Destruction du module impossible car il est utilisé dans des semestres existants !

-

Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de - laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place. + err_page = f""" +

Destruction du module impossible car il est utilisé dans des + semestres existants !

+

Il faut d'abord supprimer le semestre (ou en retirer + ce module). + Mais il est peut être préférable de laisser ce programme intact et + d'en créer une nouvelle version pour la modifier sans affecter + les semestres déjà en place.

reprendre @@ -473,11 +247,11 @@ def do_module_edit(vals: dict) -> None: def check_module_code_unicity(code, field, formation_id, module_id=None): "true si code module unique dans la formation" - Mods = module_list(args={"code": code, "formation_id": formation_id}) + modules = module_list(args={"code": code, "formation_id": formation_id}) if module_id: # edition: supprime le module en cours - Mods = [m for m in Mods if m["module_id"] != module_id] + modules = [m for m in modules if m["module_id"] != module_id] - return len(Mods) == 0 + return len(modules) == 0 def module_edit( @@ -607,7 +381,8 @@ def module_edit( ] if not unlocked: H.append( - """
Formation verrouillée, seuls certains éléments peuvent être modifiés
""" + """
Formation verrouillée, seuls + certains éléments peuvent être modifiés
""" ) if is_apc: module_types = scu.ModuleType # tous les types @@ -639,7 +414,8 @@ def module_edit( "code", { "size": 10, - "explanation": "code du module (issu du programme, exemple M1203, R2.01 , ou SAÉ 3.4. Doit être unique dans la formation)", + "explanation": """code du module (issu du programme, exemple M1203, + R2.01, ou SAÉ 3.4. Doit être unique dans la formation)""", "allow_null": False, "validator": lambda val, field, formation_id=formation.id: check_module_code_unicity( val, field, formation_id, module_id=module.id if module else None @@ -717,7 +493,8 @@ def module_edit( "readonly": True, "title": "Coefficients vers les UE ", "default": coefs_descr_txt, - "explanation": "
(passer par la page d'édition de la formation pour modifier les coefficients)", + "explanation": """
(passer par la page d'édition de la + formation pour modifier les coefficients)""", }, ) ] From 96248ccfd3270f34ea3bac1808456f164fe64304 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 2 May 2022 14:39:06 +0200 Subject: [PATCH 008/140] BUT: assoc. modules <-> App. Critiques --- app/models/but_refcomp.py | 58 ++++++++++++----- app/models/modules.py | 10 ++- app/scodoc/sco_edit_apc.py | 4 +- app/scodoc/sco_edit_module.py | 65 +++++++++++++++++-- app/static/css/scodoc.css | 4 ++ .../versions/6002d7d366e5_assoc_ue_niveau.py | 33 ++++++++++ 6 files changed, 145 insertions(+), 29 deletions(-) diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 3b28676514..b585019753 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -286,6 +286,21 @@ class ApcNiveau(db.Model, XMLModel): ) +app_critiques_modules = db.Table( + "apc_modules_acs", + db.Column( + "module_id", + db.ForeignKey("notes_modules.id", ondelete="CASCADE"), + primary_key=True, + ), + db.Column( + "app_crit_id", + db.ForeignKey("apc_app_critique.id"), + primary_key=True, + ), +) + + class ApcAppCritique(db.Model, XMLModel): "Apprentissage Critique BUT" id = db.Column(db.Integer, primary_key=True) @@ -293,12 +308,31 @@ class ApcAppCritique(db.Model, XMLModel): code = db.Column(db.Text(), nullable=False, index=True) libelle = db.Column(db.Text()) - modules = db.relationship( - "Module", - secondary="apc_modules_acs", - lazy="dynamic", - backref=db.backref("app_critiques", lazy="dynamic"), - ) + # modules = db.relationship( + # "Module", + # secondary="apc_modules_acs", + # lazy="dynamic", + # backref=db.backref("app_critiques", lazy="dynamic"), + # ) + + @classmethod + def app_critiques_ref_comp( + cls, + ref_comp: ApcReferentielCompetences, + annee: str, + competence: ApcCompetence = None, + ) -> flask_sqlalchemy.BaseQuery: + "Liste les AC de tous les parcours de ref_comp pour l'année indiquée" + assert annee in {"BUT1", "BUT2", "BUT3"} + query = cls.query.filter( + ApcAppCritique.niveau_id == ApcNiveau.id, + ApcNiveau.competence_id == ApcCompetence.id, + ApcNiveau.annee == annee, + ApcCompetence.referentiel_id == ref_comp.id, + ) + if competence is not None: + query = query.filter(ApcNiveau.competence == competence) + return query def to_dict(self) -> dict: return {"libelle": self.libelle} @@ -314,13 +348,6 @@ class ApcAppCritique(db.Model, XMLModel): return [m for m in self.modules if m.module_type == ModuleType.SAE] -ApcAppCritiqueModules = db.Table( - "apc_modules_acs", - db.Column("module_id", db.ForeignKey("notes_modules.id")), - db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")), -) - - parcours_modules = db.Table( "parcours_modules", db.Column( @@ -350,11 +377,6 @@ class ApcParcours(db.Model, XMLModel): lazy="dynamic", cascade="all, delete-orphan", ) - # modules = db.relationship( - # "Module", - # secondary=parcours_modules, - # back_populates="parcours", - # ) def __repr__(self): return f"<{self.__class__.__name__} {self.code}>" diff --git a/app/models/modules.py b/app/models/modules.py index 5ffac90435..28ef949b88 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -3,7 +3,7 @@ from app import db from app.models import APO_CODE_STR_LEN -from app.models.but_refcomp import parcours_modules +from app.models.but_refcomp import app_critiques_modules, parcours_modules from app.scodoc import sco_utils as scu from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -50,7 +50,13 @@ class Module(db.Model): "ApcParcours", secondary=parcours_modules, lazy="subquery", - # cascade="all, delete", + backref=db.backref("modules", lazy=True), + ) + + app_critiques = db.relationship( + "ApcAppCritique", + secondary=app_critiques_modules, + lazy="subquery", backref=db.backref("modules", lazy=True), ) diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index 11bb53af19..8cb516a5a2 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -127,7 +127,7 @@ def html_edit_formation_apc( formation=formation, titre=f"Ressources du S{semestre_idx}", create_element_msg="créer une nouvelle ressource", - matiere_parent=matiere_parent, + # matiere_parent=matiere_parent, modules=ressources_in_sem, module_type=ModuleType.RESSOURCE, editable=editable, @@ -143,7 +143,7 @@ def html_edit_formation_apc( formation=formation, titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}", create_element_msg="créer une nouvelle SAÉ", - matiere_parent=matiere_parent, + # matiere_parent=matiere_parent, modules=saes_in_sem, module_type=ModuleType.SAE, editable=editable, diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 2317d8b4ce..d986c60e06 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -39,7 +39,7 @@ from app.models import APO_CODE_STR_LEN from app.models import Formation, Matiere, Module, UniteEns from app.models import FormSemestre, ModuleImpl from app.models import ScolarNews -from app.models.but_refcomp import ApcParcours +from app.models.but_refcomp import ApcAppCritique, ApcParcours import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -272,6 +272,7 @@ def module_edit( # --- Détermination de la formation orig_semestre_idx = None + ue = None if create: if matiere_id: matiere = Matiere.query.get_or_404(matiere_id) @@ -286,6 +287,7 @@ def module_edit( if not module_id: raise ValueError("missing module_id !") module = models.Module.query.get_or_404(module_id) + ue = module.ue module_dict = module.to_dict() formation = module.formation unlocked = not module_is_locked(module_id) @@ -633,8 +635,9 @@ def module_edit( }, ), ] - # Choix des parcours + if is_apc: + # Choix des parcours ref_comp = formation.referentiel_competence if ref_comp: descr += [ @@ -656,13 +659,54 @@ def module_edit( module_dict["parcours"] = [ str(parcour.id) for parcour in module.parcours ] + module_dict["app_critiques"] = [ + str(app_crit.id) for app_crit in module.app_critiques + ] + # Choix des Apprentissages Critiques + if ue is not None: + annee = f"BUT{orig_semestre_idx//2 + 1}" + app_critiques = ApcAppCritique.app_critiques_ref_comp(ref_comp, annee) + descr += ( + [ + ( + "app_critiques", + { + "title": "Apprentissages Critiques", + "input_type": "checkbox", + "vertical": True, + "dom_id": "tf_module_app_critiques", + "labels": [ + app_crit.libelle for app_crit in app_critiques + ], + "allowed_values": [ + str(app_crit.id) for app_crit in app_critiques + ], + "explanation": "apprentissages critiques liés à ce module.", + }, + ) + ] + 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: descr += [ ( "parcours", { "input_type": "separator", - "title": f"""Pas de parcours: + "title": f"""{scu.EMO_WARNING } + Pas de parcours: associer un référentiel de compétence @@ -748,10 +792,17 @@ def module_edit( # do_module_edit(tf[2]) # Modifie les parcours - module.parcours = [ - ApcParcours.query.get(int(parcour_id_str)) - for parcour_id_str in tf[2]["parcours"] - ] + if "parcours" in tf[2]: + module.parcours = [ + ApcParcours.query.get(int(parcour_id_str)) + for parcour_id_str in tf[2]["parcours"] + ] + # Modifie les AC + if "app_critiques" in tf[2]: + module.app_critiques = [ + ApcAppCritique.query.get(int(ac_id_str)) + for ac_id_str in tf[2]["app_critiques"] + ] db.session.add(module) db.session.commit() return flask.redirect( diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 170d191744..c9933ffb58 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2271,6 +2271,10 @@ tr#tf_module_parcours>td { background-color: rgb(229, 229, 229); } +tr#tf_module_app_critiques>td { + background-color: rgb(194, 209, 228); +} + /* tableau recap notes */ table.notes_recapcomplet { border: 2px solid blue; diff --git a/migrations/versions/6002d7d366e5_assoc_ue_niveau.py b/migrations/versions/6002d7d366e5_assoc_ue_niveau.py index cf6c5932e2..7448ab60fa 100644 --- a/migrations/versions/6002d7d366e5_assoc_ue_niveau.py +++ b/migrations/versions/6002d7d366e5_assoc_ue_niveau.py @@ -47,6 +47,23 @@ def upgrade(): ), sa.PrimaryKeyConstraint("parcours_id", "module_id"), ) + op.alter_column( + "apc_modules_acs", "module_id", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "apc_modules_acs", "app_crit_id", existing_type=sa.INTEGER(), nullable=False + ) + op.drop_constraint( + "apc_modules_acs_module_id_fkey", "apc_modules_acs", type_="foreignkey" + ) + op.create_foreign_key( + "apc_modules_acs_module_id_fkey", + "apc_modules_acs", + "notes_modules", + ["module_id"], + ["id"], + ondelete="CASCADE", + ) # ### end Alembic commands ### @@ -57,4 +74,20 @@ def downgrade(): ) op.drop_column("notes_ue", "niveau_competence_id") op.drop_table("parcours_modules") + op.drop_constraint( + "apc_modules_acs_module_id_fkey", "apc_modules_acs", type_="foreignkey" + ) + op.create_foreign_key( + "apc_modules_acs_module_id_fkey", + "apc_modules_acs", + "notes_modules", + ["module_id"], + ["id"], + ) + op.alter_column( + "apc_modules_acs", "app_crit_id", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "apc_modules_acs", "module_id", existing_type=sa.INTEGER(), nullable=True + ) # ### end Alembic commands ### From f7e908c92d755e2130460326d79154fe9c24b7bb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 21 May 2022 07:27:00 +0200 Subject: [PATCH 009/140] repr --- app/models/etudiants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 912136e61f..6e19e5ee5a 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -60,7 +60,7 @@ class Identite(db.Model): admission = db.relationship("Admission", backref="identite", lazy="dynamic") def __repr__(self): - return f"" + return f"" @classmethod def from_request(cls, etudid=None, code_nip=None): From fd8116a77210e23fa1b218ac3836068be022b7a1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 May 2022 03:26:39 +0200 Subject: [PATCH 010/140] =?UTF-8?q?Parcours=20BUT=20/=20Ref.=20Comp=C3=A9t?= =?UTF-8?q?ences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + association UE -> ApcNiveau + choix sur la page ue_edit + association Module <-> ensemble de ApcParcours + choix sur la page module_edit + association Module - ApcAppCritique ~ choix sur la page module_edit TODO: revoir pour présenter les AC du semestre et parcours sélectionnés (JS) + association FormSemestre <-> ApcParcours + choix sur la page formsemestre_editwithmodules --- app/but/apc_edit_ue.py | 10 +- app/but/forms/refcomp_forms.py | 4 +- app/models/but_pn.py | 5 +- app/models/but_refcomp.py | 29 +++- app/models/etudiants.py | 16 +-- app/models/formations.py | 4 +- app/models/formsemestre.py | 14 +- app/models/modules.py | 2 +- app/scodoc/TrivialFormulator.py | 23 +-- app/scodoc/sco_edit_module.py | 51 ++++--- app/scodoc/sco_formsemestre_edit.py | 194 +++++++++++++++++--------- app/scodoc/sco_formsemestre_status.py | 10 +- app/static/css/scodoc.css | 2 +- app/static/js/module_edit.js | 8 ++ app/templates/but/refcomp_assoc.html | 22 ++- app/views/refcomp.py | 14 ++ sco_version.py | 2 +- 17 files changed, 278 insertions(+), 132 deletions(-) create mode 100644 app/static/js/module_edit.js diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py index 1e439c71c8..55687f0fed 100644 --- a/app/but/apc_edit_ue.py +++ b/app/but/apc_edit_ue.py @@ -17,7 +17,13 @@ def form_ue_choix_niveau(ue: UniteEns) -> str: """Form. HTML pour associer une UE à un niveau de compétence""" ref_comp = ue.formation.referentiel_competence if ref_comp is None: - return """
pas de référentiel de compétence
""" + return f"""
+
Pas de référentiel de compétence associé à cette formation !
+ +
""" annee = (ue.semestre_idx + 1) // 2 # 1, 2, 3 niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee) @@ -39,7 +45,7 @@ def form_ue_choix_niveau(ue: UniteEns) -> str: options.append("""""") options_str = "\n".join(options) return f""" -
+
Niveau de compétence associé:
""".format( - module_id, ",".join(sco_tag_module.module_tag_list(module_id)) - ) + html_foot_markup=f"""
+ """ if not create else "", initvalues=module_dict if module else {}, @@ -793,11 +805,14 @@ def module_edit( # do_module_edit(tf[2]) # Modifie les parcours - if "parcours" in tf[2]: - module.parcours = [ - ApcParcours.query.get(int(parcour_id_str)) - for parcour_id_str in tf[2]["parcours"] - ] + if ("parcours" in tf[2]) and formation.referentiel_competence: + if "-1" in tf[2]["parcours"]: # "tous" + module.parcours = formation.referentiel_competence.parcours.all() + else: + module.parcours = [ + ApcParcours.query.get(int(parcour_id_str)) + for parcour_id_str in tf[2]["parcours"] + ] # Modifie les AC if "app_critiques" in tf[2]: module.app_critiques = [ @@ -811,7 +826,7 @@ def module_edit( "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id, - semestre_idx=tf[2]["semestre_id"], + semestre_idx=tf[2]["semestre_id"] if is_apc else 1, ) ) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 3ccba77289..1acf5b95f8 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -39,23 +39,21 @@ from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteE from app.models import ScolarNews from app.models.formations import Formation from app.models.formsemestre import FormSemestre +from app.models.but_refcomp import ApcParcours import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc import sco_cache from app.scodoc import sco_groups from app import log -from app.scodoc.TrivialFormulator import TrivialFormulator, TF +from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours from app.scodoc import sco_compute_moy -from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module -from app.scodoc import sco_edit_ue from app.scodoc import sco_etud -from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations from app.scodoc import sco_formsemestre @@ -119,12 +117,12 @@ def formsemestre_editwithmodules(formsemestre_id): vals = scu.get_request_args() if not vals.get("tf_submitted", False): H.append( - """

Seuls les modules cochés font partie de ce semestre. + """

Seuls les modules cochés font partie de ce semestre. Pour les retirer, les décocher et appuyer sur le bouton "modifier".

-

Attention : s'il y a déjà des évaluations dans un module, +

Attention : s'il y a déjà des évaluations dans un module, il ne peut pas être supprimé !

-

Les modules ont toujours un responsable. +

Les modules ont toujours un responsable. Par défaut, c'est le directeur des études.

Un semestre ne peut comporter qu'une seule UE "bonus sport/culture"

@@ -153,7 +151,7 @@ def do_formsemestre_createwithmodules(edit=False): formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if not current_user.has_permission(Permission.ScoImplement): if not edit: - # il faut ScoImplement pour creer un semestre + # il faut ScoImplement pour créer un semestre raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") else: if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]: @@ -175,6 +173,7 @@ def do_formsemestre_createwithmodules(edit=False): formation = Formation.query.get(formation_id) if formation is None: raise ScoValueError("Formation inexistante !") + is_apc = formation.is_apc() if not edit: initvalues = {"titre": _default_sem_title(formation)} semestre_id = int(vals["semestre_id"]) @@ -210,12 +209,12 @@ def do_formsemestre_createwithmodules(edit=False): if NB_SEM == 1: semestre_id_list = [-1] else: - if edit and formation.is_apc(): + if edit and is_apc: # en APC, ne permet pas de changer de semestre semestre_id_list = [formsemestre.semestre_id] else: semestre_id_list = list(range(1, NB_SEM + 1)) - if not formation.is_apc(): + if not is_apc: # propose "pas de semestre" seulement en classique semestre_id_list.insert(0, -1) @@ -226,7 +225,7 @@ def do_formsemestre_createwithmodules(edit=False): else: semestre_id_labels.append(f"S{sid}") # Liste des modules dans cette formation - if formation.is_apc(): + if is_apc: modules = formation.modules.order_by(Module.module_type, Module.numero) else: modules = ( @@ -318,10 +317,10 @@ def do_formsemestre_createwithmodules(edit=False): { "size": 40, "title": "Nom de ce semestre", - "explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans + "explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans le titre: ils seront automatiquement ajoutés """ - % _default_sem_title(formation), + value="remettre titre par défaut" onClick="document.tf.titre.value='{ + _default_sem_title(formation)}';"/>""", }, ), ( @@ -343,11 +342,9 @@ def do_formsemestre_createwithmodules(edit=False): "allowed_values": semestre_id_list, "labels": semestre_id_labels, "explanation": "en BUT, on ne peut pas modifier le semestre après création" - if formation.is_apc() - else "", - "attributes": ['onchange="change_semestre_id();"'] - if formation.is_apc() + if is_apc else "", + "attributes": ['onchange="change_semestre_id();"'] if is_apc else "", }, ), ) @@ -386,7 +383,7 @@ def do_formsemestre_createwithmodules(edit=False): mf = mf_manual for n in range(1, scu.EDIT_NB_ETAPES + 1): - mf["title"] = "Etape Apogée (%d)" % n + mf["title"] = f"Etape Apogée ({n})" modform.append(("etape_apo" + str(n), mf.copy())) modform.append( ( @@ -443,15 +440,19 @@ def do_formsemestre_createwithmodules(edit=False): ) ) if edit: - formtit = ( - """ -

Modifier les coefficients des UE capitalisées

-

Sélectionner les modules, leurs responsables et les étudiants à inscrire:

+ formtit = f""" +

Modifier les coefficients des UE capitalisées

+

Sélectionner les modules, leurs responsables et les étudiants + à inscrire:

""" - % formsemestre_id - ) else: - formtit = """

Sélectionner les modules et leurs responsables

Si vous avez des parcours (options), ne sélectionnez que les modules du tronc commun.

""" + formtit = """

Sélectionner les modules et leurs responsables

+

Si vous avez des parcours (options), dans un premier + ne sélectionnez que les modules du tronc commun, puis après inscriptions, + revenez ajouter les modules de parcours en sélectionnant les groupes d'étudiants + à y inscrire. +

""" modform += [ ( @@ -531,12 +532,52 @@ def do_formsemestre_createwithmodules(edit=False): "explanation": "empêcher le calcul des moyennes d'UE et générale.", }, ), + ] + # Choix des parcours + if is_apc: + ref_comp = formation.referentiel_competence + if ref_comp: + modform += [ + ( + "parcours", + { + "input_type": "checkbox", + "vertical": True, + "dom_id": "tf_module_parcours", + "labels": [parcour.libelle for parcour in ref_comp.parcours], + "allowed_values": [ + str(parcour.id) for parcour in ref_comp.parcours + ], + "explanation": "Parcours proposés dans ce semestre.", + }, + ) + ] + if edit: + sem["parcours"] = [str(parcour.id) for parcour in formsemestre.parcours] + else: + modform += [ + ( + "parcours", + { + "input_type": "separator", + "title": f"""{scu.EMO_WARNING } + Pas de parcours: + vérifier la formation + """, + }, + ) + ] + + # Choix des modules + modform += [ ( "sep", { "input_type": "separator", "title": "", - "template": "%s" % formtit, + "template": f"
{formtit}", }, ), ] @@ -544,8 +585,8 @@ def do_formsemestre_createwithmodules(edit=False): nbmod = 0 for semestre_id in semestre_ids: - if formation.is_apc(): - # pour restreindre l'édition aux module du semestre sélectionné + if is_apc: + # pour restreindre l'édition aux modules du semestre sélectionné tr_class = f'class="sem{semestre_id}"' else: tr_class = "" @@ -560,7 +601,7 @@ def do_formsemestre_createwithmodules(edit=False): "sep", { "input_type": "separator", - "title": "Semestre %s" % semestre_id, + "title": f"Semestre {semestre_id}", "template": templ_sep, }, ) @@ -568,13 +609,13 @@ def do_formsemestre_createwithmodules(edit=False): for mod in mods: if mod["semestre_id"] == semestre_id and ( (not edit) # creation => tous modules - or (not formation.is_apc()) # pas BUT, on peut mixer les semestres + or (not is_apc) # pas BUT, on peut mixer les semestres or (semestre_id == formsemestre.semestre_id) # module du semestre or (mod["module_id"] in module_ids_set) # module déjà présent ): nbmod += 1 if edit: - select_name = "%s!group_id" % mod["module_id"] + select_name = f"{mod['module_id']}!group_id" def opt_selected(gid): if gid == vals.get(select_name): @@ -603,13 +644,16 @@ def do_formsemestre_createwithmodules(edit=False): group["group_name"], ) fcg += "" - itemtemplate = ( - f"""" - ) + itemtemplate = f""" + + + + """ else: - itemtemplate = f"""""" + itemtemplate = f""" + + + """ modform.append( ( "MI" + str(mod["module_id"]), @@ -742,7 +786,8 @@ def do_formsemestre_createwithmodules(edit=False): for module_id in tf[2]["tf-checked"]: mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id]) if mod_resp_id is None: - # Si un module n'a pas de responsable (ou inconnu), l'affecte au 1er directeur des etudes: + # Si un module n'a pas de responsable (ou inconnu), + # l'affecte au 1er directeur des etudes: mod_resp_id = tf[2]["responsable_id"] tf[2][module_id] = mod_resp_id @@ -763,7 +808,7 @@ def do_formsemestre_createwithmodules(edit=False): module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]] _formsemestre_check_ue_bonus_unicity(module_ids_checked) if not edit: - if formation.is_apc(): + if is_apc: _formsemestre_check_module_list( module_ids_checked, tf[2]["semestre_id"] ) @@ -777,14 +822,6 @@ def do_formsemestre_createwithmodules(edit=False): "responsable_id": tf[2][f"MI{module_id}"], } _ = sco_moduleimpl.do_moduleimpl_create(modargs) - flash("Nouveau semestre créé") - return flask.redirect( - url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) else: # Modification du semestre: # on doit creer les modules nouvellement selectionnés @@ -794,7 +831,7 @@ def do_formsemestre_createwithmodules(edit=False): module_ids_tocreate = [ x for x in module_ids_checked if not x in module_ids_existing ] - if formation.is_apc(): + if is_apc: _formsemestre_check_module_list( module_ids_tocreate, tf[2]["semestre_id"] ) @@ -868,27 +905,46 @@ def do_formsemestre_createwithmodules(edit=False): modargs, formsemestre_id=formsemestre_id ) mod = sco_edit_module.module_list({"module_id": module_id})[0] - - if msg: - msg_html = ( - '
Attention !
  • ' - + "
  • ".join(msg) - + "
" - ) - if ok: - msg_html += "

Modification effectuée

" - else: - msg_html += "

Modification effectuée (mais modules cités non supprimés)

" - msg_html += ( - 'retour au tableau de bord' - % formsemestre_id - ) - return msg_html + # --- Assocation des parcours + formsemestre = FormSemestre.query.get(formsemestre_id) + if "parcours" in tf[2]: + formsemestre.parcours = [ + ApcParcours.query.get(int(parcour_id_str)) + for parcour_id_str in tf[2]["parcours"] + ] + db.session.add(formsemestre) + db.session.commit() + # --- Fin + if edit: + if msg: + msg_html = ( + '
Attention !
  • ' + + "
  • ".join(msg) + + "
" + ) + if ok: + msg_html += "

Modification effectuée

" else: - return flask.redirect( - "formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié" - % formsemestre_id - ) + msg_html += "

Modification effectuée (mais modules cités non supprimés)

" + msg_html += ( + 'retour au tableau de bord' + % formsemestre_id + ) + return msg_html + else: + return flask.redirect( + "formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié" + % formsemestre_id + ) + else: + flash("Nouveau semestre créé") + return flask.redirect( + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) def _formsemestre_check_module_list(module_ids, semestre_idx): diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 299318ed75..fbf16c3de6 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -929,10 +929,18 @@ def formsemestre_status_head(formsemestre_id=None, page_title=None): })""" ) H.append("") + if sem.parcours: + H.append( + f""" + + + + """ + ) evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) H.append( - '") H.append("
%(label)s%(elem)s""" - + fcg - + "
%(label)s%(elem)s{fcg}
%(label)s%(elem)s
%(label)s%(elem)s
Parcours: {', '.join(parcours.code for parcours in sem.parcours)}
Evaluations: %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides' + '
Évaluations: %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides' % evals ) if evals["last_modif"]: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index c9933ffb58..1696009a93 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2206,7 +2206,7 @@ ul.notes_module_list { list-style-type: none; } -div#ue_choix_niveau { +div.ue_choix_niveau { background-color: rgb(191, 242, 255); border: 1px solid blue; border-radius: 10px; diff --git a/app/static/js/module_edit.js b/app/static/js/module_edit.js new file mode 100644 index 0000000000..9882fdff56 --- /dev/null +++ b/app/static/js/module_edit.js @@ -0,0 +1,8 @@ +/* Page édition module */ + + +$(document).ready(function () { + +}); + + diff --git a/app/templates/but/refcomp_assoc.html b/app/templates/but/refcomp_assoc.html index 9f8335be2a..9ba07dee3d 100644 --- a/app/templates/but/refcomp_assoc.html +++ b/app/templates/but/refcomp_assoc.html @@ -6,11 +6,25 @@

Associer un référentiel de compétences

Association d'un référentiel de compétence à la formation - {{formation.titre}} ({{formation.acronyme}}) + {{formation.titre}} ({{formation.acronyme}})
-
-
- {{ wtf.quick_form(form) }} +
+ + Référentiel actuellement associé: + {% if formation.referentiel_competence is not none %} + {{ formation.referentiel_competence.specialite_long }} + supprimer + {% else %} + aucun + {% endif %} +
+
+ {{ wtf.quick_form(form) }} +
diff --git a/app/views/refcomp.py b/app/views/refcomp.py index 27e5c83e22..fb9e2eb1b9 100644 --- a/app/views/refcomp.py +++ b/app/views/refcomp.py @@ -160,6 +160,20 @@ def refcomp_assoc_formation(formation_id: int): ) +@bp.route("/referentiel/comp/desassoc_formation/", methods=["GET"]) +@scodoc +@permission_required(Permission.ScoChangeFormation) +def refcomp_desassoc_formation(formation_id: int): + """Désassocie la formation de son ref. de compétence""" + formation = Formation.query.get_or_404(formation_id) + formation.referentiel_competence = None + db.session.add(formation) + db.session.commit() + return redirect( + url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id) + ) + + @bp.route( "/referentiel/comp/load", defaults={"formation_id": None}, methods=["GET", "POST"] ) diff --git a/sco_version.py b/sco_version.py index 3222274fa8..242844ed37 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.22" +SCOVERSION = "9.3a" SCONAME = "ScoDoc" From 40f0bca74d994e6025623b8c9e97668627b36d26 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 May 2022 05:01:25 +0200 Subject: [PATCH 011/140] =?UTF-8?q?Cr=C3=A9ation=20d'une=20partition=20ave?= =?UTF-8?q?c=20groupes=20de=20parcours?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_formsemestre_status.py | 4 +- app/scodoc/sco_groups.py | 64 +++++++++++++------ ...artitionForm.js => edit_partition_form.js} | 0 app/views/scolar.py | 47 +++++++++----- 4 files changed, 77 insertions(+), 38 deletions(-) rename app/static/js/{editPartitionForm.js => edit_partition_form.js} (100%) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index fbf16c3de6..71823e10f1 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -325,7 +325,7 @@ def formsemestre_status_menubar(sem): }, { "title": "Créer/modifier les partitions...", - "endpoint": "scolar.editPartitionForm", + "endpoint": "scolar.edit_partition_form", "args": {"formsemestre_id": formsemestre_id}, "enabled": sco_groups.sco_permissions_check.can_change_groups( formsemestre_id @@ -854,7 +854,7 @@ def _make_listes_sem(sem, with_absences=True): H.append( f"""

@@ -1000,28 +1001,49 @@ def editPartitionForm(formsemestre_id=None): # H.append("

") - H.append('
') H.append( - '' % formsemestre_id + f"""
+ + + + + """ ) - H.append('') + if formsemestre.formation.is_apc() and "Parcours" not in ( + p["partition_name"] for p in partitions + ): + # propose création partition "Parcours" + H.append( + f""" + + """ + ) H.append( - '' + """ +
+ + """ ) - H.append('') - H.append("
") H.append( """
-

Les partitions sont des découpages de l'ensemble des étudiants. - Par exemple, les "groupes de TD" sont une partition. - On peut créer autant de partitions que nécessaire. +

Les partitions sont des découpages de l'ensemble des étudiants. + Par exemple, les "groupes de TD" sont une partition. + On peut créer autant de partitions que nécessaire.

    -
  • Dans chaque partition, un nombre de groupes quelconque peuvent être créés (suivre le lien "répartir"). -
  • On peut faire afficher le classement de l'étudiant dans son groupe d'une partition en cochant "afficher rang sur bulletins" (ainsi, on peut afficher le classement en groupes de TD mais pas en groupe de TP, si ce sont deux partitions). +
  • Dans chaque partition, un nombre de groupes quelconque peuvent + être créés (suivre le lien "répartir"). +
  • On peut faire afficher le classement de l'étudiant dans son + groupe d'une partition en cochant "afficher rang sur bulletins" + (ainsi, on peut afficher le classement en groupes de TD mais pas en + groupe de TP, si ce sont deux partitions). +
  • +
  • Décocher "afficher sur noms groupes" pour ne pas que cette partition + apparaisse dans les noms de groupes
  • -
  • Décocher "afficher sur noms groupes" pour ne pas que cette partition apparaisse dans les noms de groupes -
""" @@ -1077,7 +1099,7 @@ def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=Fal """ % (partition["partition_name"], grnames), dest_url="", - cancel_url="editPartitionForm?formsemestre_id=%s" % formsemestre_id, + cancel_url="edit_partition_form?formsemestre_id=%s" % formsemestre_id, parameters={"redirect": redirect, "partition_id": partition_id}, ) @@ -1091,7 +1113,7 @@ def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=Fal # redirect to partition edit page: if redirect: return flask.redirect( - "editPartitionForm?formsemestre_id=" + str(formsemestre_id) + "edit_partition_form?formsemestre_id=" + str(formsemestre_id) ) @@ -1148,7 +1170,7 @@ def partition_move(partition_id, after=0, redirect=1): # redirect to partition edit page: if redirect: return flask.redirect( - "editPartitionForm?formsemestre_id=" + str(formsemestre_id) + "edit_partition_form?formsemestre_id=" + str(formsemestre_id) ) @@ -1188,7 +1210,7 @@ def partition_rename(partition_id): ) elif tf[0] == -1: return flask.redirect( - "editPartitionForm?formsemestre_id=" + str(formsemestre_id) + "edit_partition_form?formsemestre_id=" + str(formsemestre_id) ) else: # form submission @@ -1229,7 +1251,7 @@ def partition_set_name(partition_id, partition_name, redirect=1): # redirect to partition edit page: if redirect: return flask.redirect( - "editPartitionForm?formsemestre_id=" + str(formsemestre_id) + "edit_partition_form?formsemestre_id=" + str(formsemestre_id) ) diff --git a/app/static/js/editPartitionForm.js b/app/static/js/edit_partition_form.js similarity index 100% rename from app/static/js/editPartitionForm.js rename to app/static/js/edit_partition_form.js diff --git a/app/views/scolar.py b/app/views/scolar.py index e0472af1c4..4bc3f27f55 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -54,6 +54,7 @@ from app.decorators import ( from app.models.etudiants import Identite from app.models.etudiants import make_etud_args from app.models.events import ScolarNews +from app.models.formsemestre import FormSemestre from app.views import scolar_bp as bp from app.views import ScoData @@ -860,8 +861,8 @@ sco_publish( ) sco_publish( - "/editPartitionForm", - sco_groups.editPartitionForm, + "/edit_partition_form", + sco_groups.edit_partition_form, Permission.ScoView, methods=["GET", "POST"], ) @@ -904,21 +905,37 @@ sco_publish( sco_publish( "/partition_create", sco_groups.partition_create, - Permission.ScoView, + Permission.ScoView, # controle d'access ad-hoc methods=["GET", "POST"], ) -# @bp.route("/partition_create", methods=["GET", "POST"]) -# @scodoc -# @permission_required(Permission.ScoView) -# @scodoc7func -# def partition_create( -# -# formsemestre_id, -# partition_name="", -# default=False, -# numero=None, -# redirect=1): -# return sco_groups.partition_create( formsemestre_id, + + +@bp.route("/create_partition_parcours", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def create_partition_parcours(formsemestre_id): + """Création d'une partitions nommée "Parcours" avec un groupe par parcours.""" + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + if "Parcours" in (p.partition_name for p in formsemestre.partitions): + flash("""Partition "Parcours" déjà existante""") + else: + partition_id = sco_groups.partition_create( + formsemestre_id, partition_name="Parcours", redirect=False + ) + n = 0 + for parcour in formsemestre.parcours: + if parcour.code: + _ = sco_groups.create_group(partition_id, group_name=parcour.code) + n += 1 + flash(f"Partition Parcours créée avec {n} groupes.") + return flask.redirect( + url_for( + "scolar.edit_partition_form", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) sco_publish("/etud_info_html", sco_page_etud.etud_info_html, Permission.ScoView) From 6030d12acaa61c18a39c30c00fdcb6f14ee49733 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 May 2022 07:02:01 +0200 Subject: [PATCH 012/140] Ajout colonne parcours sur table description semestre --- app/scodoc/sco_formsemestre_status.py | 47 ++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 71823e10f1..10e5dbe932 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -40,6 +40,7 @@ from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import Module from app.models.formsemestre import FormSemestre +from app.models.moduleimpls import ModuleImpl import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb @@ -578,7 +579,9 @@ def fill_formsemestre(sem): # Description du semestre sous forme de table exportable -def formsemestre_description_table(formsemestre_id, with_evals=False): +def formsemestre_description_table( + formsemestre_id, with_evals=False, with_parcours=False +): """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients """ @@ -618,7 +621,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): ue_info["Coef._class"] = "ue_coef" R.append(ue_info) - ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list( + mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=M["moduleimpl_id"] ) enseignants = ", ".join( @@ -629,7 +632,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): "Code": M["module"]["code"] or "", "Module": M["module"]["abbrev"] or M["module"]["titre"], "_Module_class": "scotext", - "Inscrits": len(ModInscrits), + "Inscrits": len(mod_inscrits), "Responsable": sco_users.user_info(M["responsable_id"])["nomprenom"], "_Responsable_class": "scotext", "Enseignants": enseignants, @@ -648,10 +651,15 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): moduleimpl_id=M["moduleimpl_id"], ), } - R.append(l) if M["module"]["coefficient"]: sum_coef += M["module"]["coefficient"] + if with_parcours: + module = Module.query.get(M["module_id"]) + l["parcours"] = ", ".join(sorted([pa.code for pa in module.parcours])) + + R.append(l) + if with_evals: # Ajoute lignes pour evaluations evals = nt.get_mod_evaluation_etat_list(M["moduleimpl_id"]) @@ -676,7 +684,10 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef} R.append(sums) - columns_ids = ["UE", "Code", "Module", "Coef."] + columns_ids = ["UE", "Code", "Module"] + if with_parcours: + columns_ids += ["parcours"] + columns_ids += ["Coef."] if sco_preferences.get_preference("bul_show_ects", formsemestre_id): columns_ids += ["ects"] columns_ids += ["Inscrits", "Responsable", "Enseignants"] @@ -696,6 +707,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): titles["description"] = "" titles["coefficient"] = "Coef. éval." titles["evalcomplete_str"] = "Complète" + titles["parcours"] = "Parcours" titles["publish_incomplete_str"] = "Toujours Utilisée" title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois()) @@ -720,21 +732,26 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): ) -def formsemestre_description(formsemestre_id, format="html", with_evals=False): +def formsemestre_description( + formsemestre_id, format="html", with_evals=False, with_parcours=False +): """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients """ with_evals = int(with_evals) - tab = formsemestre_description_table(formsemestre_id, with_evals=with_evals) - tab.html_before_table = """
- - + + indiquer les évaluations + indiquer les parcours BUT + """ return tab.make_page(format=format) From 06be6d0ac5cc36f54f18b7e18a5fb029f9dcf95b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 22 May 2022 07:03:20 +0200 Subject: [PATCH 013/140] Methode .modimpls_parcours listant les modimpls d'un parcours --- app/models/formsemestre.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 3bf349cb40..2f1f925931 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -6,6 +6,7 @@ import datetime from functools import cached_property import flask_sqlalchemy +from sqlalchemy.sql import text from app import db from app import log @@ -14,6 +15,7 @@ from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN import app.scodoc.sco_utils as scu +from app.models.but_refcomp import ApcParcours from app.models.but_refcomp import parcours_formsemestre from app.models.etudiants import Identite from app.models.modules import Module @@ -233,6 +235,28 @@ class FormSemestre(db.Model): ) return modimpls + def modimpls_parcours(self, parcours: ApcParcours) -> list[ModuleImpl]: + """Liste des modimpls du semestre (sans les bonus (?)) dans le parcours donné. + - triée par type/numéro/code ?? + """ + cursor = db.session.execute( + text( + """ + SELECT modimpl.id + FROM notes_moduleimpl modimpl, notes_modules mod, + parcours_modules pm, parcours_formsemestre pf + WHERE modimpl.formsemestre_id = :formsemestre_id + AND modimpl.module_id = mod.id + AND pm.module_id = mod.id + AND pm.parcours_id = pf.parcours_id + AND pf.parcours_id = :parcours_id + AND pf.formsemestre_id = :formsemestre_id + """ + ), + {"formsemestre_id": self.id, "parcours_id": parcours.id}, + ) + return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor] + def can_be_edited_by(self, user): """Vrai si user peut modifier ce semestre""" if not user.has_permission(Permission.ScoImplement): # pas chef From 5af4b5bed6cceeb66b9e0eb1b667671321106290 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 26 May 2022 23:45:57 +0200 Subject: [PATCH 014/140] WIP: Partitions non editables (pour groupes de parcours) --- app/models/groups.py | 6 ++- app/scodoc/sco_formsemestre_inscriptions.py | 27 ++++++---- app/scodoc/sco_formsemestre_status.py | 2 +- app/scodoc/sco_groups.py | 51 ++++++++++++++----- app/scodoc/sco_import_etuds.py | 17 +++++-- app/scodoc/sco_inscr_passage.py | 11 ++-- ...a2771105c21c_parcours_inscriptions_casc.py | 10 ++++ 7 files changed, 90 insertions(+), 34 deletions(-) diff --git a/app/models/groups.py b/app/models/groups.py index 1d24b60c08..27b763d112 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -23,7 +23,7 @@ class Partition(db.Model): ) # "TD", "TP", ... (NULL for 'all') partition_name = db.Column(db.String(SHORT_STR_LEN)) - # numero = ordre de presentation) + # Numero = ordre de presentation) numero = db.Column(db.Integer) # Calculer le rang ? bul_show_rank = db.Column( @@ -33,6 +33,10 @@ class Partition(db.Model): show_in_lists = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) + # Editable ? (faux pour les groupes de parcours) + groups_editable = db.Column( + db.Boolean(), nullable=False, default=True, server_default="true" + ) groups = db.relationship( "GroupDescr", backref=db.backref("partition", lazy=True), diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index b3fe985327..394af3be57 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -35,6 +35,7 @@ from flask import url_for, g, request from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre +from app.models.groups import GroupDescr, Partition import app.scodoc.sco_utils as scu from app import log from app.scodoc.scolog import logdb @@ -263,8 +264,7 @@ def do_formsemestre_inscription_with_modules( args["etat"] = etat do_formsemestre_inscription_create(args, method=method) log( - "do_formsemestre_inscription_with_modules: etudid=%s formsemestre_id=%s" - % (etudid, formsemestre_id) + f"do_formsemestre_inscription_with_modules: etudid={etudid} formsemestre_id={formsemestre_id}" ) # inscriptions aux groupes # 1- inscrit au groupe 'tous' @@ -275,8 +275,14 @@ def do_formsemestre_inscription_with_modules( # 2- inscrit aux groupes for group_id in group_ids: if group_id and not group_id in gdone: - sco_groups.set_group(etudid, group_id) - gdone[group_id] = 1 + group = GroupDescr.query.get_or_404(group_id) + if group.partition.groups_editable: + sco_groups.set_group(etudid, group_id) + gdone[group_id] = 1 + else: + log( + f"do_formsemestre_inscription_with_modules: group {group:r} belongs to non editable partition" + ) # inscription a tous les modules de ce semestre modimpls = sco_moduleimpl.moduleimpl_withmodule_list( @@ -534,11 +540,14 @@ def formsemestre_inscription_option(etudid, formsemestre_id): ue_status = nt.get_etud_ue_status(etudid, ue_id) if ue_status and ue_status["is_capitalized"]: sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"]) - ue_descr += ' (capitalisée le %s)' % ( - sem_origin["formsemestre_id"], - etudid, - sem_origin["titreannee"], - ndb.DateISOtoDMY(ue_status["event_date"]), + ue_descr += ( + ' (capitalisée le %s)' + % ( + sem_origin["formsemestre_id"], + etudid, + sem_origin["titreannee"], + ndb.DateISOtoDMY(ue_status["event_date"]), + ) ) descr.append( ( diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 10e5dbe932..5cc5c47a60 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -346,7 +346,7 @@ def formsemestre_status_menubar(sem): "title": "%s" % partition["partition_name"], "endpoint": "scolar.affect_groups", "args": {"partition_id": partition["partition_id"]}, - "enabled": enabled, + "enabled": enabled and partition["groups_editable"], } ) menuGroupes.append( diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 8eae60e040..50e0e70ee0 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -76,10 +76,12 @@ partitionEditor = ndb.EditableTable( "numero", "bul_show_rank", "show_in_lists", + "editable", ), input_formators={ "bul_show_rank": bool, "show_in_lists": bool, + "editable": bool, }, ) @@ -621,10 +623,12 @@ def comp_origin(etud, cur_sem): return "" # parcours normal, ne le signale pas -def set_group(etudid, group_id): +def set_group(etudid: int, group_id: int) -> bool: """Inscrit l'étudiant au groupe. Return True if ok, False si deja inscrit. - Warning: don't check if group_id exists (the caller should check). + Warning: + - don't check if group_id exists (the caller should check). + - don't check if group's partition is editable """ cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) @@ -698,14 +702,28 @@ def setGroups( groupsToCreate="", # name and members of new groups groupsToDelete="", # groups to delete ): - """Affect groups (Ajax request) + """Affect groups (Ajax request): renvoie du XML groupsLists: lignes de la forme "group_id;etudid;...\n" groupsToCreate: lignes "group_name;etudid;...\n" groupsToDelete: group_id;group_id;... + + Ne peux pas modifier les groupes des partitions non éditables. """ from app.scodoc import sco_formsemestre + def xml_error(msg, code=404): + data = ( + f'Error: {msg}' + ) + response = make_response(data, code) + response.headers["Content-Type"] = scu.XML_MIMETYPE + return response + partition = get_partition(partition_id) + if not partition["group_editable"]: + msg = "setGroups: partition non editable" + log(msg) + return xml_error(msg, code=403) formsemestre_id = partition["formsemestre_id"] if not sco_permissions_check.can_change_groups(formsemestre_id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") @@ -727,8 +745,8 @@ def setGroups( continue try: group_id = int(group_id) - except ValueError as exc: - log("setGroups: ignoring invalid group_id={group_id}") + except ValueError: + log(f"setGroups: ignoring invalid group_id={group_id}") continue group = get_group(group_id) # Anciens membres du groupe: @@ -967,14 +985,19 @@ def edit_partition_form(formsemestre_id=None): for group in get_partition_groups(p) ] H.append(", ".join(lg)) - H.append( - f"""répartir - """ - ) + H.append("""""") + if p["groups_editable"]: + H.append( + f"""répartir + """ + ) + else: + H.append("""non éditable""") + H.append("""""") H.append( 'renommer' % p["partition_id"] @@ -1334,6 +1357,8 @@ def groups_auto_repartition(partition_id=None): from app.scodoc import sco_formsemestre partition = get_partition(partition_id) + if not partition["groups_editable"]: + raise AccessDenied("Partition non éditable") formsemestre_id = partition["formsemestre_id"] formsemestre = FormSemestre.query.get(formsemestre_id) # renvoie sur page édition groupes diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index 32b2530d47..fcad66228d 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -33,14 +33,13 @@ import io import os import re import time -from datetime import date from flask import g, url_for import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log -from app.models import ScolarNews +from app.models import ScolarNews, GroupDescr from app.scodoc.sco_excel import COLORS from app.scodoc.sco_formsemestre_inscriptions import ( @@ -718,9 +717,17 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None ) for group_id in group_ids: - sco_groups.change_etud_group_in_partition( - args["etudid"], group_id - ) + group = GroupDescr.query.get(group_id) + if group.partition.groups_editable: + sco_groups.change_etud_group_in_partition( + args["etudid"], group_id + ) + else: + log("scolars_import_admission: partition non editable") + diag.append( + f"Attention: partition {group.partition} non editable (ignorée)" + ) + # diag.append("import de %s" % (etud["nomprenom"])) n_import += 1 diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 807792fb00..4f0146c1e1 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -219,11 +219,12 @@ def do_inscrit(sem, etudids, inscrit_groupes=False): # inscrit aux groupes for partition_group in partition_groups: - sco_groups.change_etud_group_in_partition( - etudid, - partition_group["group_id"], - partition_group, - ) + if partition_group["groups_editable"]: + sco_groups.change_etud_group_in_partition( + etudid, + partition_group["group_id"], + partition_group, + ) def do_desinscrit(sem, etudids): diff --git a/migrations/versions/a2771105c21c_parcours_inscriptions_casc.py b/migrations/versions/a2771105c21c_parcours_inscriptions_casc.py index 55d194e025..74da8acb93 100644 --- a/migrations/versions/a2771105c21c_parcours_inscriptions_casc.py +++ b/migrations/versions/a2771105c21c_parcours_inscriptions_casc.py @@ -102,6 +102,13 @@ def upgrade(): ["id"], ondelete="CASCADE", ) + # GROUPES + op.add_column( + "partition", + sa.Column( + "groups_editable", sa.Boolean(), server_default="true", nullable=False + ), + ) # INSCRIPTIONS op.drop_constraint( "notes_formsemestre_inscription_etudid_fkey", @@ -192,6 +199,7 @@ def upgrade(): # ### end Alembic commands ### +# -------------------------------------------------------------- def downgrade(): # ### commands auto generated by Alembic - please adjust! ### op.drop_constraint( @@ -232,6 +240,8 @@ def downgrade(): op.create_foreign_key( "notes_notes_etudid_fkey", "notes_notes", "identite", ["etudid"], ["id"] ) + # GROUPES + op.drop_column("partition", "groups_editable") # INSCRIPTIONS op.drop_constraint( "notes_formsemestre_inscription_etudid_fkey", From 45449f0465853e3ee27c4545ab30ce9ba5e8b00f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 28 May 2022 11:38:22 +0200 Subject: [PATCH 015/140] BUT: Partition de parcours et inscriptions --- app/models/__init__.py | 1 + app/models/formsemestre.py | 81 +++++++++++++++++++++++ app/scodoc/sco_formsemestre.py | 4 +- app/scodoc/sco_formsemestre_edit.py | 7 +- app/scodoc/sco_formsemestre_validation.py | 18 +++-- app/scodoc/sco_groups.py | 59 +++++++++++------ app/scodoc/sco_groups_edit.py | 1 + app/static/css/scodoc.css | 8 +++ app/views/scolar.py | 13 +--- 9 files changed, 151 insertions(+), 41 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index d259966ff2..84f1332edd 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -76,5 +76,6 @@ from app.models.but_refcomp import ( ApcCompetence, ApcSituationPro, ApcAppCritique, + ApcParcours, ) from app.models.config import ScoDocSiteConfig diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 90461c988b..393e117868 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -5,6 +5,7 @@ import datetime from functools import cached_property +from flask import flash import flask_sqlalchemy from sqlalchemy.sql import text @@ -13,6 +14,7 @@ from app import log from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN +from app.models.groups import GroupDescr, Partition import app.scodoc.sco_utils as scu from app.models.but_refcomp import ApcParcours @@ -480,6 +482,85 @@ class FormSemestre(db.Model): """Map { etudid : inscription } (incluant DEM et DEF)""" return {ins.etud.id: ins for ins in self.inscriptions} + def setup_parcours_groups(self) -> None: + """Vérifie et créee si besoin la partition et les groupes de parcours BUT.""" + if not self.formation.is_apc(): + return + partition = Partition.query.filter_by( + formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS + ).first() + if partition is None: + # Création de la partition de parcours + partition = Partition( + formsemestre_id=self.id, + partition_name=scu.PARTITION_PARCOURS, + numero=-1, + ) + db.session.add(partition) + db.session.flush() # pour avoir un id + flash(f"Partition Parcours créée.") + + for parcour in self.parcours: + if parcour.code: + group = GroupDescr.query.filter_by( + partition_id=partition.id, group_name=parcour.code + ).first() + if not group: + partition.groups.append(GroupDescr(group_name=parcour.code)) + db.session.commit() + + def update_inscriptions_parcours_from_groups(self) -> None: + """Met à jour les inscriptions dans les parcours du semestres en + fonction des groupes de parcours. + Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS + et leur nom est le code du parcours (eg "Cyber"). + """ + partition = Partition.query.filter_by( + formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS + ).first() + if partition is None: # pas de partition de parcours + return + + # Efface les inscriptions aux parcours: + db.session.execute( + text( + """UPDATE notes_formsemestre_inscription + SET parcour_id=NULL + WHERE formsemestre_id=:formsemestre_id + """ + ), + { + "formsemestre_id": self.id, + }, + ) + # Inscrit les étudiants des groupes de parcours: + for group in partition.groups: + query = ApcParcours.query.filter_by(code=group.group_name) + if query.count() != 1: + log( + f"""update_inscriptions_parcours_from_groups: { + query.count()} parcours with code {group.group_name}""" + ) + continue + parcour = query.first() + db.session.execute( + text( + """UPDATE notes_formsemestre_inscription ins + SET parcour_id=:parcour_id + FROM group_membership gm + WHERE formsemestre_id=:formsemestre_id + AND gm.etudid = ins.etudid + AND gm.group_id = :group_id + """ + ), + { + "formsemestre_id": self.id, + "parcour_id": parcour.id, + "group_id": group.id, + }, + ) + db.session.commit() + # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 11792057cd..8d286b2279 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -141,7 +141,9 @@ def do_formsemestre_list(*a, **kw): def _formsemestre_enrich(sem): - """Ajoute champs souvent utiles: titre + annee et dateord (pour tris)""" + """Ajoute champs souvent utiles: titre + annee et dateord (pour tris). + XXX obsolete: préférer formsemestre.to_dict() ou, mieux, les méthodes de FormSemestre. + """ # imports ici pour eviter refs circulaires from app.scodoc import sco_formsemestre_edit diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 1acf5b95f8..c4e329462a 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -548,7 +548,8 @@ def do_formsemestre_createwithmodules(edit=False): "allowed_values": [ str(parcour.id) for parcour in ref_comp.parcours ], - "explanation": "Parcours proposés dans ce semestre.", + "explanation": """Parcours proposés dans ce semestre. + S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours.""", }, ) ] @@ -905,7 +906,7 @@ def do_formsemestre_createwithmodules(edit=False): modargs, formsemestre_id=formsemestre_id ) mod = sco_edit_module.module_list({"module_id": module_id})[0] - # --- Assocation des parcours + # --- Association des parcours formsemestre = FormSemestre.query.get(formsemestre_id) if "parcours" in tf[2]: formsemestre.parcours = [ @@ -914,6 +915,8 @@ def do_formsemestre_createwithmodules(edit=False): ] db.session.add(formsemestre) db.session.commit() + # --- Crée ou met à jour les groupes de parcours BUT + formsemestre.setup_parcours_groups() # --- Fin if edit: if msg: diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index f4896a8dc2..1f22f536e4 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -581,14 +581,20 @@ def formsemestre_recap_parcours_table( else: pm = plusminus % sem["formsemestre_id"] - H.append( - '%s%s' - % (bgcolor, num_sem, pm) + inscr = formsemestre.etuds_inscriptions.get(etudid) + parcours_name = ( + f' {inscr.parcour.code}' + if (inscr and inscr.parcour) + else "" ) - H.append('%(mois_debut)s' % sem) H.append( - '%s' - % (a_url, sem["formsemestre_id"], etudid, sem["titreannee"]) + f""" + {num_sem}{pm} + {sem['mois_debut']} + {formsemestre.titre_annee()}{parcours_name} + """ ) if decision_sem: H.append('%s' % decision_sem["code"]) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 50e0e70ee0..21fb5b7229 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -107,14 +107,19 @@ def get_group(group_id: int): return r[0] -def group_delete(group, force=False): +def group_delete(group_id: int): """Delete a group.""" # if not group['group_name'] and not force: # raise ValueError('cannot suppress this group') # remove memberships: - ndb.SimpleQuery("DELETE FROM group_membership WHERE group_id=%(group_id)s", group) + ndb.SimpleQuery( + "DELETE FROM group_membership WHERE group_id=%(group_id)s", + {"group_id": group_id}, + ) # delete group: - ndb.SimpleQuery("DELETE FROM group_descr WHERE id=%(group_id)s", group) + ndb.SimpleQuery( + "DELETE FROM group_descr WHERE id=%(group_id)s", {"group_id": group_id} + ) def get_partition(partition_id): @@ -690,7 +695,12 @@ def change_etud_group_in_partition(etudid, group_id, partition=None): % (formsemestre_id, partition["partition_name"], group["group_name"]), ) cnx.commit() - # 4- invalidate cache + + # 5- Update parcours + formsemestre = FormSemestre.query.get(formsemestre_id) + formsemestre.update_inscriptions_parcours_from_groups() + + # 6- invalidate cache sco_cache.invalidate_formsemestre( formsemestre_id=formsemestre_id ) # > change etud group @@ -720,7 +730,7 @@ def setGroups( return response partition = get_partition(partition_id) - if not partition["group_editable"]: + if not partition["groups_editable"]: msg = "setGroups: partition non editable" log(msg) return xml_error(msg, code=403) @@ -796,6 +806,10 @@ def setGroups( for etudid in fs[1:-1]: change_etud_group_in_partition(etudid, group_id, partition) + # Update parcours + formsemestre = FormSemestre.query.get(formsemestre_id) + formsemestre.update_inscriptions_parcours_from_groups() + data = ( 'Groupes enregistrés' ) @@ -835,21 +849,18 @@ def delete_group(group_id, partition_id=None): affectation aux groupes) partition_id est optionnel et ne sert que pour verifier que le groupe est bien dans cette partition. + S'il s'agit d'un groupe de parcours, affecte l'inscription des étudiants aux parcours. """ - group = get_group(group_id) + group = GroupDescr.query.get_or_404(group_id) if partition_id: - if partition_id != group["partition_id"]: + if partition_id != group.partition_id: raise ValueError("inconsistent partition/group") - else: - partition_id = group["partition_id"] - partition = get_partition(partition_id) - if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]): + if not sco_permissions_check.can_change_groups(group.partition.formsemestre_id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") - log( - "delete_group: group_id=%s group_name=%s partition_name=%s" - % (group_id, group["group_name"], partition["partition_name"]) - ) - group_delete(group) + log(f"delete_group: group={group:r} partition={group.partition}") + formsemestre = group.partition.formsemestre + group_delete(group.id) + formsemestre.update_inscriptions_parcours_from_groups() def partition_create( @@ -1097,11 +1108,14 @@ def partition_set_attr(partition_id, attr, value): def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False): """Suppress a partition (and all groups within). - default partition cannot be suppressed (unless force)""" + The default partition cannot be suppressed (unless force). + Si la partition de parcours est supprimée, les étudiants sont désinscrits des parcours. + """ partition = get_partition(partition_id) formsemestre_id = partition["formsemestre_id"] if not sco_permissions_check.can_change_groups(formsemestre_id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if not partition["partition_name"] and not force: raise ValueError("cannot suppress this partition") @@ -1127,10 +1141,12 @@ def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=Fal log("partition_delete: partition_id=%s" % partition_id) # 1- groups for group in groups: - group_delete(group, force=force) + group_delete(group["group_id"]) # 2- partition partitionEditor.delete(cnx, partition_id) + formsemestre.update_inscriptions_parcours_from_groups() + # redirect to partition edit page: if redirect: return flask.redirect( @@ -1214,7 +1230,8 @@ def partition_rename(partition_id): "default": partition["partition_name"], "allow_null": False, "size": 12, - "validator": lambda val, _: len(val) < SHORT_STR_LEN, + "validator": lambda val, _: (len(val) < SHORT_STR_LEN) + and (val != scu.PARTITION_PARCOURS), }, ), ), @@ -1246,6 +1263,8 @@ def partition_set_name(partition_id, partition_name, redirect=1): partition = get_partition(partition_id) if partition["partition_name"] is None: raise ValueError("can't set a name to default partition") + if partition_name == scu.PARTITION_PARCOURS: + raise ScoValueError(f"nom de partition {scu.PARTITION_PARCOURS} réservé.") formsemestre_id = partition["formsemestre_id"] # check unicity @@ -1415,7 +1434,7 @@ def groups_auto_repartition(partition_id=None): group_names = sorted(set([x.strip() for x in groupNames.split(",")])) # Détruit les groupes existant de cette partition for old_group in get_partition_groups(partition): - group_delete(old_group) + group_delete(old_group["group_id"]) # Crée les nouveaux groupes group_ids = [] for group_name in group_names: diff --git a/app/scodoc/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py index 4829046788..b4fa4511b9 100644 --- a/app/scodoc/sco_groups_edit.py +++ b/app/scodoc/sco_groups_edit.py @@ -43,6 +43,7 @@ def affect_groups(partition_id): formsemestre_id = partition["formsemestre_id"] if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): raise AccessDenied("vous n'avez pas la permission de modifier les groupes") + partition.formsemestre.setup_parcours_groups() return render_template( "scolar/affect_groups.html", sco_header=html_sco_header.sco_header( diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 1696009a93..062c69c6dd 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2267,6 +2267,14 @@ span.missing_value { color: red; } +span.code_parcours { + color: white; + background-color: rgb(254, 95, 246); + padding-left: 4px; + padding-right: 4px; + border-radius: 2px; +} + tr#tf_module_parcours>td { background-color: rgb(229, 229, 229); } diff --git a/app/views/scolar.py b/app/views/scolar.py index 8a07d48955..f1e860deb9 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -918,18 +918,7 @@ 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) - if scu.PARTITION_PARCOURS in (p.partition_name for p in formsemestre.partitions): - flash(f"""Partition "{scu.PARTITION_PARCOURS}" déjà existante""") - else: - partition_id = sco_groups.partition_create( - formsemestre_id, partition_name=scu.PARTITION_PARCOURS, redirect=False - ) - n = 0 - for parcour in formsemestre.parcours: - if parcour.code: - _ = sco_groups.create_group(partition_id, group_name=parcour.code) - n += 1 - flash(f"Partition Parcours créée avec {n} groupes.") + formsemestre.setup_parcours_groups() return flask.redirect( url_for( "scolar.edit_partition_form", From 6596bd778c5fedac7e39fac96395a4ea32acc4a7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 29 May 2022 17:31:29 +0200 Subject: [PATCH 016/140] =?UTF-8?q?Am=C3=A9lioration=20=C3=A9dition=20modu?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_edit_module.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 04a2cd9753..b4392fa3b3 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -272,7 +272,7 @@ def module_edit( from app.scodoc import sco_tag_module # --- Détermination de la formation - orig_semestre_idx = None + orig_semestre_idx = semestre_id ue = None if create: if matiere_id: @@ -331,10 +331,16 @@ def module_edit( ) semestres_indices = list(range(1, parcours.NB_SEM + 1)) - # Toutes les UE de la formation (tout parcours): + # Toutes les UEs de la formation (tout parcours): ues = formation.ues.order_by( UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme ).all() + # L'UE de rattachement par défaut: 1ere du semestre + ue_default = ( + formation.ues.filter_by(semestre_idx=orig_semestre_idx) + .order_by(UniteEns.numero, UniteEns.acronyme) + .first() + ) # --- Titre de la page if create: @@ -535,6 +541,13 @@ def module_edit( "default": formation.id, }, ), + ( + "semestre_id", + { + "input_type": "hidden", + "default": orig_semestre_idx, + }, + ), ] if module: descr += [ @@ -582,6 +595,7 @@ def module_edit( for u in ues ], "allowed_values": [u.id for u in ues], + "default": ue_default.id if ue_default is not None else "", }, ), ] From b25ba0bc3968399153eb4d65521b696a5db56254 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 29 May 2022 17:34:03 +0200 Subject: [PATCH 017/140] WIP: BUT validations/parcours. --- app/models/__init__.py | 2 + app/models/but_validations.py | 100 ++++++++++++++ app/models/formations.py | 19 +++ app/models/formsemestre.py | 22 +++ app/scodoc/sco_codes_parcours.py | 15 +- app/scodoc/sco_formsemestre_inscriptions.py | 5 +- app/scodoc/sco_formsemestre_status.py | 5 +- app/scodoc/sco_groups_edit.py | 10 +- app/templates/scolar/affect_groups.html | 40 +++--- .../versions/4311cc342dbd_validations_but.py | 128 ++++++++++++++++++ 10 files changed, 314 insertions(+), 32 deletions(-) create mode 100644 app/models/but_validations.py create mode 100644 migrations/versions/4311cc342dbd_validations_but.py diff --git a/app/models/__init__.py b/app/models/__init__.py index 84f1332edd..c7a183ec34 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -78,4 +78,6 @@ from app.models.but_refcomp import ( ApcAppCritique, ApcParcours, ) +from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE + from app.models.config import ScoDocSiteConfig diff --git a/app/models/but_validations.py b/app/models/but_validations.py new file mode 100644 index 0000000000..6b5bb6e901 --- /dev/null +++ b/app/models/but_validations.py @@ -0,0 +1,100 @@ +# -*- coding: UTF-8 -* + +"""Décisions de jury validations) des RCUE et années du BUT +""" + +from app import db +from app import log + +from app.models import CODE_STR_LEN +from app.models.ues import UniteEns +from app.models.formsemestre import FormSemestre, FormSemestreInscription + + +class ApcValidationRCUE(db.Model): + """Validation des niveaux de compétences + + aka "regroupements cohérents d'UE" dans le jargon BUT. + + """ + + __tablename__ = "apc_validation_rcue" + # Assure unicité de la décision: + __table_args__ = ( + db.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"), + ) + + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id", ondelete="CASCADE"), + index=True, + nullable=False, + ) + formsemestre_id = db.Column( + db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True + ) + # Les deux UE associées à ce niveau: + ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) + ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) + # optionnel, le parcours dans lequel se trouve la compétence: + parcours_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), nullable=True) + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) + + etud = db.relationship("Identite", backref="apc_validations_rcues") + formsemestre = db.relationship("FormSemestre", backref="apc_validations_rcues") + ue1 = db.relationship("UniteEns", foreign_keys=ue1_id) + ue2 = db.relationship("UniteEns", foreign_keys=ue2_id) + parcour = db.relationship("ApcParcours") + + def __repr__(self): + return f"<{self.__class__.__name__} {self.id} {self.etud} {self.ue1}/{self.ue2}:{self.code!r}>" + + +def get_other_ue_rcue(ue: UniteEns, etudid: int) -> UniteEns: + """L'autre UE du RCUE (niveau de compétence) pour cet étudiant, + None si pas trouvée. + """ + if (ue.niveau_competence is None) or (ue.semestre_idx is None): + return None + q = UniteEns.query.filter( + FormSemestreInscription.etudid == etudid, + FormSemestreInscription.formsemestre_id == FormSemestre.id, + FormSemestre.formation_id == UniteEns.formation_id, + FormSemestre.semestre_id == UniteEns.semestre_idx, + UniteEns.niveau_competence_id == ue.niveau_competence_id, + UniteEns.semestre_idx != ue.semestre_idx, + ) + if q.count() > 1: + log("Warning: get_other_ue_rcue: {q.count()} candidates UE") + return q.first() + + +class ApcValidationAnnee(db.Model): + """Validation des années du BUT""" + + __tablename__ = "apc_validation_annee" + # Assure unicité de la décision: + __table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire"),) + id = db.Column(db.Integer, primary_key=True) + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id", ondelete="CASCADE"), + index=True, + nullable=False, + ) + ordre = db.Column(db.Integer, nullable=False) + "numéro de l'année: 1, 2, 3" + formsemestre_id = db.Column( + db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True + ) + annee_scolaire = db.Column(db.Integer, nullable=False) # 2021 + date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True) + + etud = db.relationship("Identite", backref="apc_validations_annees") + formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees") + + def __repr__(self): + return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}:{self.code!r}>" diff --git a/app/models/formations.py b/app/models/formations.py index 5933bcbff5..725dd7e748 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -1,10 +1,17 @@ """ScoDoc 9 models : Formations """ +import flask_sqlalchemy import app from app import db from app.comp import df_cache from app.models import SHORT_STR_LEN +from app.models.but_refcomp import ( + ApcAnneeParcours, + ApcNiveau, + ApcParcours, + ApcParcoursNiveauCompetence, +) from app.models.modules import Module from app.models.ues import UniteEns from app.scodoc import sco_cache @@ -148,6 +155,18 @@ class Formation(db.Model): if change: app.clear_scodoc_cache() + def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:: + """Les UEs d'un parcours de la formation. + Exemple: pour avoir les UE du semestre 3, faire + `formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)` + """ + return UniteEns.query.filter_by(formation=self).filter( + UniteEns.niveau_competence_id == ApcNiveau.id, + ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id, + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ApcAnneeParcours.parcours_id == parcour.id, + ) + class Matiere(db.Model): """Matières: regroupe les modules d'une UE diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 393e117868..d3375a638f 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -14,6 +14,12 @@ from app import log from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN +from app.models.but_refcomp import ( + ApcAnneeParcours, + ApcNiveau, + ApcParcours, + ApcParcoursNiveauCompetence, +) from app.models.groups import GroupDescr, Partition import app.scodoc.sco_utils as scu @@ -211,6 +217,22 @@ class FormSemestre(db.Model): sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT) return sem_ues.order_by(UniteEns.numero) + def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery: + """UE que suit l'étudiant dans ce semestre BUT + en fonction du parcours dans lequel il est inscrit. + + Si voulez les UE d'un parcour, il est plus efficace de passer par + `formation.query_ues_parcour(parcour)`. + """ + return self.query_ues().filter( + FormSemestreInscription.etudid == etudid, + FormSemestreInscription.formsemestre == self, + UniteEns.niveau_competence_id == ApcNiveau.id, + ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id, + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id, + ) + @cached_property def modimpls_sorted(self) -> list[ModuleImpl]: """Liste des modimpls du semestre (y compris bonus) diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index e77c711d2b..17c2dae7e9 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -35,7 +35,7 @@ from app import log @enum.unique class CodesParcours(enum.IntEnum): - """Codes numériques de sparcours, enregistrés en base + """Codes numériques des parcours, enregistrés en base dans notes_formations.type_parcours Ne pas modifier. """ @@ -122,10 +122,12 @@ ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant ATB = "ATB" AJ = "AJ" CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis) -NAR = "NAR" -RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission) DEM = "DEM" +JSD = "JSD" # jurytenu mais pas de code (Jury Sans Décision) +NAR = "NAR" +RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée + # codes actions REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1) @@ -156,9 +158,7 @@ CODES_EXPL = { RAT: "En attente d'un rattrapage", DEM: "Démission", } -# Nota: ces explications sont personnalisables via le fichier -# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py -# variable: CONFIG.CODES_EXP + # Les codes de semestres: CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} @@ -169,6 +169,9 @@ CODES_SEM_REO = {NAR: 1} # reorientation CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée +# Pour le BUT: +CODES_RCUE = {ADM, AJ, CMP} + def code_semestre_validant(code: str) -> bool: "Vrai si ce CODE entraine la validation du semestre" diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index 394af3be57..3f95add7c2 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -258,6 +258,7 @@ def do_formsemestre_inscription_with_modules( """Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS (donc sauf le sport) """ + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) # inscription au semestre args = {"formsemestre_id": formsemestre_id, "etudid": etudid} if etat is not None: @@ -284,7 +285,7 @@ def do_formsemestre_inscription_with_modules( f"do_formsemestre_inscription_with_modules: group {group:r} belongs to non editable partition" ) - # inscription a tous les modules de ce semestre + # Inscription à tous les modules de ce semestre modimpls = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id ) @@ -294,6 +295,8 @@ def do_formsemestre_inscription_with_modules( {"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid}, formsemestre_id=formsemestre_id, ) + # Mise à jour des inscriptions aux parcours: + formsemestre.update_inscriptions_parcours_from_groups() def formsemestre_inscription_with_modules_etud( diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 5cc5c47a60..672108a892 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -149,7 +149,10 @@ def formsemestre_status_menubar(sem): { "title": "Voir la formation %(acronyme)s (v%(version)s)" % F, "endpoint": "notes.ue_table", - "args": {"formation_id": sem["formation_id"]}, + "args": { + "formation_id": sem["formation_id"], + "semestre_idx": sem["semestre_id"], + }, "enabled": True, "helpmsg": "Tableau de bord du semestre", }, diff --git a/app/scodoc/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py index b4fa4511b9..088f74e519 100644 --- a/app/scodoc/sco_groups_edit.py +++ b/app/scodoc/sco_groups_edit.py @@ -29,6 +29,7 @@ """ from flask import render_template +from app.models import Partition from app.scodoc import html_sco_header from app.scodoc import sco_groups from app.scodoc.sco_exceptions import AccessDenied @@ -39,8 +40,8 @@ def affect_groups(partition_id): Permet aussi la creation et la suppression de groupes. """ # réécrit pour 9.0.47 avec un template - partition = sco_groups.get_partition(partition_id) - formsemestre_id = partition["formsemestre_id"] + partition = Partition.query.get_or_404(partition_id) + formsemestre_id = partition.formsemestre_id if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): raise AccessDenied("vous n'avez pas la permission de modifier les groupes") partition.formsemestre.setup_parcours_groups() @@ -53,8 +54,9 @@ def affect_groups(partition_id): ), sco_footer=html_sco_header.sco_footer(), partition=partition, - partitions_list=sco_groups.get_partitions_list( - formsemestre_id, with_default=False + # Liste des partitions sans celle par defaut: + partitions_list=partition.formsemestre.partitions.filter( + Partition.partition_name != None ), formsemestre_id=formsemestre_id, ) diff --git a/app/templates/scolar/affect_groups.html b/app/templates/scolar/affect_groups.html index 8d01eca3e6..323a42e523 100644 --- a/app/templates/scolar/affect_groups.html +++ b/app/templates/scolar/affect_groups.html @@ -1,13 +1,13 @@ {# -*- mode: jinja-html -*- #} {{ sco_header|safe }} -

Affectation aux groupes de {{ partition["partition_name"] }}

+

Affectation aux groupes de {{ partition.partition_name }}

Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne sont enregistrées que lorsque vous cliquez sur le bouton "Enregistrer ces groupes". Vous pouvez créer de nouveaux groupes. Pour supprimer un groupe, utiliser le lien "suppr." en haut à droite de sa boite. Vous pouvez aussi répartir automatiquement les groupes.

@@ -15,24 +15,24 @@ href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, pa
- - - -       - -       -    Éditer groupes de - + + + +        + +        +     Éditer groupes de +
diff --git a/migrations/versions/4311cc342dbd_validations_but.py b/migrations/versions/4311cc342dbd_validations_but.py new file mode 100644 index 0000000000..7020eff147 --- /dev/null +++ b/migrations/versions/4311cc342dbd_validations_but.py @@ -0,0 +1,128 @@ +"""Validations BUT + +Revision ID: 4311cc342dbd +Revises: a2771105c21c +Create Date: 2022-05-28 16:46:09.861248 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "4311cc342dbd" +down_revision = "a2771105c21c" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "apc_validation_annee", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=False), + sa.Column("ordre", sa.Integer(), nullable=False), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("annee_scolaire", sa.Integer(), nullable=False), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("code", sa.String(length=16), nullable=False), + sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("etudid", "annee_scolaire"), + ) + op.create_index( + op.f("ix_apc_validation_annee_code"), + "apc_validation_annee", + ["code"], + unique=False, + ) + op.create_index( + op.f("ix_apc_validation_annee_etudid"), + "apc_validation_annee", + ["etudid"], + unique=False, + ) + op.create_table( + "apc_validation_rcue", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("etudid", sa.Integer(), nullable=False), + sa.Column("formsemestre_id", sa.Integer(), nullable=True), + sa.Column("ue1_id", sa.Integer(), nullable=False), + sa.Column("ue2_id", sa.Integer(), nullable=False), + sa.Column("parcours_id", sa.Integer(), nullable=True), + sa.Column( + "date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("code", sa.String(length=16), nullable=False), + sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["formsemestre_id"], + ["notes_formsemestre.id"], + ), + sa.ForeignKeyConstraint( + ["parcours_id"], + ["apc_parcours.id"], + ), + sa.ForeignKeyConstraint( + ["ue1_id"], + ["notes_ue.id"], + ), + sa.ForeignKeyConstraint( + ["ue2_id"], + ["notes_ue.id"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"), + ) + op.create_index( + op.f("ix_apc_validation_rcue_code"), + "apc_validation_rcue", + ["code"], + unique=False, + ) + op.create_index( + op.f("ix_apc_validation_rcue_etudid"), + "apc_validation_rcue", + ["etudid"], + unique=False, + ) + op.create_index( + op.f("ix_apc_validation_rcue_formsemestre_id"), + "apc_validation_rcue", + ["formsemestre_id"], + unique=False, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + op.f("ix_apc_validation_rcue_formsemestre_id"), table_name="apc_validation_rcue" + ) + op.drop_index( + op.f("ix_apc_validation_rcue_etudid"), table_name="apc_validation_rcue" + ) + op.drop_index(op.f("ix_apc_validation_rcue_code"), table_name="apc_validation_rcue") + op.drop_table("apc_validation_rcue") + op.drop_index( + op.f("ix_apc_validation_annee_etudid"), table_name="apc_validation_annee" + ) + op.drop_index( + op.f("ix_apc_validation_annee_code"), table_name="apc_validation_annee" + ) + op.drop_table("apc_validation_annee") + # ### end Alembic commands ### From a9924a3884a712cb35c6d6b4d0eac91234ce4197 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 30 May 2022 17:23:45 +0200 Subject: [PATCH 018/140] typo --- app/models/formations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/formations.py b/app/models/formations.py index 725dd7e748..df857d39cc 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -155,7 +155,7 @@ class Formation(db.Model): if change: app.clear_scodoc_cache() - def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:: + def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery: """Les UEs d'un parcours de la formation. Exemple: pour avoir les UE du semestre 3, faire `formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)` From ad5bdd03d13411b13be44a6f81dadb1c52d80687 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 2 Jun 2022 03:14:13 +0200 Subject: [PATCH 019/140] =?UTF-8?q?BUT:=20moyenne=20gen.=20consid=C3=A9ran?= =?UTF-8?q?t=20les=20UE=20du=20parcours=20de=20chaque=20=C3=A9tudiant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_but.py | 47 ++++++++++++++++++++++++++++++++++++++++ app/comp/res_common.py | 11 +++++++++- app/models/formations.py | 5 ++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index d7fec78639..d86d10e798 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -28,6 +28,7 @@ class ResultatsSemestreBUT(NotesTableCompat): "modimpl_coefs_df", "modimpls_evals_poids", "sem_cube", + "ues_inscr_parcours_df", # inscriptions aux UE / parcours ) def __init__(self, formsemestre): @@ -55,6 +56,7 @@ class ResultatsSemestreBUT(NotesTableCompat): self.modimpls_results, ) = moy_ue.notes_sem_load_cube(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) + self.ues_inscr_parcours_df = self.load_ues_inscr_parcours() self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( self.formsemestre, modimpls=self.formsemestre.modimpls_sorted ) @@ -108,6 +110,9 @@ class ResultatsSemestreBUT(NotesTableCompat): # Clippe toutes les moyennes d'UE dans [0,20] self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) + # Nanifie les moyennes d'UE hors parcours pour chaque étudiant + self.etud_moy_ue *= self.ues_inscr_parcours_df + # Moyenne générale indicative: # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # donc la moyenne indicative) @@ -175,3 +180,45 @@ class ResultatsSemestreBUT(NotesTableCompat): i = self.modimpl_coefs_df.columns.get_loc(modimpl_id) j = self.modimpl_coefs_df.index.get_loc(ue_id) return self.sem_cube[:, i, j] + + def load_ues_inscr_parcours(self) -> pd.DataFrame: + """Chargement des inscriptions aux parcours et calcul de la + matrice d'inscriptions (etuds, ue). + S'il n'y pas de référentiel de compétence, donc pas de parcours, + on considère l'étudiant inscrit à toutes les ue. + La matrice avec ue ne comprend que les UE non bonus. + 1.0 si étudiant inscrit à l'UE, NaN sinon. + """ + etuds_parcours = { + inscr.etudid: inscr.parcour_id for inscr in self.formsemestre.inscriptions + } + ue_ids = [ue.id for ue in self.ues] + # matrice de 1, inscrits par défaut à toutes les UE: + ues_inscr_parcours_df = pd.DataFrame( + 1.0, index=etuds_parcours.keys(), columns=ue_ids, dtype=float + ) + if self.formsemestre.formation.referentiel_competence is None: + return ues_inscr_parcours_df + + ue_by_parcours = {} # parcours_id : {ue_id:0|1} + for parcour in self.formsemestre.formation.referentiel_competence.parcours: + ue_by_parcours[parcour.id] = { + ue.id: 1.0 + for ue in self.formsemestre.formation.query_ues_parcour( + parcour + ).filter_by(semestre_idx=self.formsemestre.semestre_id) + } + for etudid in etuds_parcours: + parcour = etuds_parcours[etudid] + if parcour is not None: + ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[ + etuds_parcours[etudid] + ] + return ues_inscr_parcours_df + + def etud_ues(self, etudid: int) -> list[int]: + """Liste des id d'UE auxquelles l'étudiant est inscrit (sans bonus). + (surchargée en BUT pour prendre en compte les parcours) + """ + s = self.ues_inscr_parcours_df.loc[etudid] + return s.index[s.notna()] diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 645f067e60..3c9eb810f9 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -112,6 +112,14 @@ class ResultatsSemestre(ResultatsCache): "dict { etudid : indice dans les inscrits }" return {e.id: idx for idx, e in enumerate(self.etuds)} + def etud_ues(self, etudid: int) -> list[int]: + """Liste des UE auxquelles l'etudiant est inscrit, sans bonus + (surchargée en BUT pour prendre en compte les parcours) + """ + # Pour les formations classiques, etudid n'est pas utilisé + # car tous les étudiants sont inscrits à toutes les UE + return [ue.id for ue in self.ues if ue.type != UE_SPORT] + def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray: """Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue. Utile pour stats bottom tableau recap. @@ -622,9 +630,10 @@ class ResultatsSemestre(ResultatsCache): f"_{col_id}_target_attrs" ] = f""" title="{modimpl.module.titre} ({nom_resp})" """ modimpl_ids.add(modimpl.id) + nb_ues_etud_parcours = len(self.etud_ues(etudid)) ue_valid_txt = ( ue_valid_txt_html - ) = f"{nb_ues_validables}/{len(ues_sans_bonus)}" + ) = f"{nb_ues_validables}/{nb_ues_etud_parcours}" if nb_ues_warning: ue_valid_txt_html += " " + scu.EMO_WARNING add_cell( diff --git a/app/models/formations.py b/app/models/formations.py index df857d39cc..dc8ccb8015 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -62,7 +62,10 @@ class Formation(db.Model): return e def get_parcours(self): - """get l'instance de TypeParcours de cette formation""" + """get l'instance de TypeParcours de cette formation + (le TypeParcours définit le genre de formation, à ne pas confondre + avec les parcours du BUT). + """ return sco_codes_parcours.get_parcours_from_code(self.type_parcours) def get_titre_version(self) -> str: From 2b2fb80403f8cc2bc46a041757659a981f9ba14f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 2 Jun 2022 10:48:28 +0200 Subject: [PATCH 020/140] =?UTF-8?q?Bulletin=20BUT:=20n'affiche=20que=20les?= =?UTF-8?q?=20UE=20du=20parcours=20de=20l'=C3=A9tudiant.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 12 ++++++++---- app/comp/res_but.py | 7 ++++++- app/comp/res_common.py | 4 ++-- app/templates/pn/form_modules_ue_coefs.html | 6 ++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index f9dbb87062..3c58157e03 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -274,6 +274,13 @@ class BulletinBUT: etat_inscription = etud.inscription_etat(formsemestre.id) nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT] published = (not formsemestre.bul_hide_xml) or force_publishing + if formsemestre.formation.referentiel_competence is None: + etud_ues_ids = { + ue.id for ue in res.ues if res.modimpls_in_ue(ue.id, etud.id) + } + else: + etud_ues_ids = res.etud_ues_ids(etud.id) + d = { "version": "0", "type": "BUT", @@ -365,10 +372,7 @@ class BulletinBUT: ) for ue in res.ues # si l'UE comporte des modules auxquels on est inscrit: - if ( - (ue.type == UE_SPORT) - or self.res.modimpls_in_ue(ue.id, etud.id) - ) + if ((ue.type == UE_SPORT) or ue.id in etud_ues_ids) }, "semestre": semestre_infos, }, diff --git a/app/comp/res_but.py b/app/comp/res_but.py index d86d10e798..330f019189 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -6,6 +6,7 @@ """Résultats semestres BUT """ +from collections.abc import Generator import time import numpy as np import pandas as pd @@ -216,9 +217,13 @@ class ResultatsSemestreBUT(NotesTableCompat): ] return ues_inscr_parcours_df - def etud_ues(self, etudid: int) -> list[int]: + def etud_ues_ids(self, etudid: int) -> list[int]: """Liste des id d'UE auxquelles l'étudiant est inscrit (sans bonus). (surchargée en BUT pour prendre en compte les parcours) """ s = self.ues_inscr_parcours_df.loc[etudid] return s.index[s.notna()] + + def etud_ues(self, etudid: int) -> Generator[UniteEns]: + """Liste des UE auxquelles l'étudiant est inscrit (sans bonus).""" + return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid)) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 3c9eb810f9..56a10d3f4f 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -112,7 +112,7 @@ class ResultatsSemestre(ResultatsCache): "dict { etudid : indice dans les inscrits }" return {e.id: idx for idx, e in enumerate(self.etuds)} - def etud_ues(self, etudid: int) -> list[int]: + def etud_ues_ids(self, etudid: int) -> list[int]: """Liste des UE auxquelles l'etudiant est inscrit, sans bonus (surchargée en BUT pour prendre en compte les parcours) """ @@ -630,7 +630,7 @@ class ResultatsSemestre(ResultatsCache): f"_{col_id}_target_attrs" ] = f""" title="{modimpl.module.titre} ({nom_resp})" """ modimpl_ids.add(modimpl.id) - nb_ues_etud_parcours = len(self.etud_ues(etudid)) + nb_ues_etud_parcours = len(self.etud_ues_ids(etudid)) ue_valid_txt = ( ue_valid_txt_html ) = f"{nb_ues_validables}/{nb_ues_etud_parcours}" diff --git a/app/templates/pn/form_modules_ue_coefs.html b/app/templates/pn/form_modules_ue_coefs.html index f022eccd3e..75ed3de6e7 100644 --- a/app/templates/pn/form_modules_ue_coefs.html +++ b/app/templates/pn/form_modules_ue_coefs.html @@ -2,10 +2,12 @@

{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs

{% if not read_only %} - Double-cliquer pour changer une valeur. +

Double-cliquer pour changer une valeur. Les valeurs sont automatiquement enregistrées au fur et à mesure. +

{% endif %} - +

Chaque ligne représente une ressource ou SAÉ, et chaque colonne une Unité d'Enseignement (UE). +

Semestre: {h}""" + + @bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) From d4a8b74c0a99b6d79961ea5646219c1d59341471 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 21 Jun 2022 18:33:43 +0200 Subject: [PATCH 031/140] WIP: jury BUT: formulaire avec style (merci @sebL) --- app/but/jury_but.py | 2 + app/static/css/jury_but.css | 68 ++++++++++++++++++++++++++++++++++ app/static/js/jury_but.js | 6 +++ app/views/notes.py | 74 +++++++++++++++++++++++++------------ 4 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 app/static/css/jury_but.css create mode 100644 app/static/js/jury_but.js diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 74fd9687c7..561ba54338 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -266,6 +266,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): explanation: {self.explanation} """ + def annee_scolaire_sr(self) + def comp_formsemestres( self, formsemestre: FormSemestre ) -> tuple[FormSemestre, FormSemestre]: diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css new file mode 100644 index 0000000000..beffba9758 --- /dev/null +++ b/app/static/css/jury_but.css @@ -0,0 +1,68 @@ +/* Saisie décision de jury BUT */ + +.jury_but { + font-family: Verdana, Geneva, Tahoma, sans-serif; +} + +.but_annee { + display: inline-grid; + grid-template-columns: repeat(4, auto); + gap: 4px; +} + +.but_annee_caption { + grid-column: 4 / 5; +} + +.but_annee_caption, +.but_niveau_titre { + background: #09c !important; + color: #FFF; + padding: 8px !important; +} + +.but_annee>* { + display: flex; + align-items: center; + padding: 0px 16px; + background: #FFF; + border: 1px solid #aaa; + border-radius: 8px; +} + +.but_annee>div.titre { + background: rgb(242, 242, 238); + border: none; + border-radius: 0px; + border-bottom: 1px solid gray; +} + +.but_niveau_ue>div:nth-child(1), +.but_note { + border-right: 1px solid #aaa; + padding: 8px; +} + +.but_annee select { + padding: 8px 8px; + border: none; +} + +.but_niveau_rcue, +.but_niveau_rcue>* { + border-color: #09c; + font-weight: bold; +} + +div.but_section_annee { + margin-bottom: 10px; +} + +div.but_settings { + margin-top: 16px; +} + +span.but_explanation { + color: blueviolet; + font-style: italic; +} \ No newline at end of file diff --git a/app/static/js/jury_but.js b/app/static/js/jury_but.js new file mode 100644 index 0000000000..a2f13dd181 --- /dev/null +++ b/app/static/js/jury_but.js @@ -0,0 +1,6 @@ + + +// active les menus des codes "manuels" (année, RCUEs) +function enable_manual_codes(elt) { + $(".jury_but select.manual").prop("disabled", !elt.checked); +} diff --git a/app/views/notes.py b/app/views/notes.py index 99cd224e6e..020d1cca4a 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2234,10 +2234,13 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): # XXX TODO Page expérimentale pour les devs H = [ html_sco_header.sco_header( - page_title="Validation BUT", formsemestre_id=formsemestre_id, etudid=etudid + page_title="Validation BUT", + formsemestre_id=formsemestre_id, + etudid=etudid, + cssstyles=("css/jury_but.css",), + javascripts=("js/jury_but.js",), ), f""" -

Jury BUT

""", ] @@ -2248,21 +2251,30 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): H.append( f""" -
Parcours: {deca.parcour.libelle or "non spécifié"} - en BUT{deca.annee_but} + +

Jury BUT{deca.annee_but} - Parcours {deca.parcour.libelle or "non spécifié"} + - {deca.formsemestre_impair.annee_scolaire_str()}

-

Année: {deca.explanation}
{ - _gen_but_select("code_annee", deca.codes, deca.code_valide) - }

-

Niveaux de compétences

+
+
+ Décision de jury pour l'année : { + _gen_but_select("code_annee", deca.codes, deca.code_valide, disabled=True, klass="manual") + }
+ {deca.explanation} +
+ Niveaux de compétences et unités d'enseignement :
+
+
S{1}
+
S{2}
+
RCUE
""" ) for niveau in deca.niveaux_competences: H.append( f"""
- {niveau.competence.titre} +
{niveau.competence.titre}
""" ) dec_rcue = deca.decisions_rcue_by_niveau[niveau.id] @@ -2270,44 +2282,52 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): ue = dec_rcue.rcue.ue_1 H.append( f"""
- {ue.acronyme} - {scu.fmt_note(dec_rcue.rcue.moy_ue_1)} - { +
{ue.acronyme}
+
{scu.fmt_note(dec_rcue.rcue.moy_ue_1)}
+
{ _gen_but_select("code_ue_"+str(ue.id), deca.decisions_ues[ue.id].codes, deca.decisions_ues[ue.id].code_valide ) - } + }
""" ) # Semestre pair ue = dec_rcue.rcue.ue_2 H.append( f"""
- {ue.acronyme} - {scu.fmt_note(dec_rcue.rcue.moy_ue_2)} - { +
{ue.acronyme}
+
{scu.fmt_note(dec_rcue.rcue.moy_ue_2)}
+
{ _gen_but_select("code_ue_"+str(ue.id), deca.decisions_ues[ue.id].codes, deca.decisions_ues[ue.id].code_valide ) - } + }
""" ) # RCUE H.append( f"""
- {scu.fmt_note(dec_rcue.rcue.moy_rcue)} - { - _gen_but_select("code_rcue_"+str(niveau.id), +
{scu.fmt_note(dec_rcue.rcue.moy_rcue)}
+
{ + _gen_but_select("code_rcue_"+str(niveau.id), dec_rcue.codes, - dec_rcue.code_valide + dec_rcue.code_valide, + disabled=True, klass="manual" ) - } + }
""" ) H.append("
") # but_annee + H.append( + """
+ permettre la saisie manuelles des codes d'année et de niveaux +
""" + ) + H.append("") # but_annee + # ---- Toutes les UEs, pour infos H.append(f"
    ") for ue in formsemestre.query_ues(): # volontairement toutes les UE @@ -2318,7 +2338,13 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): return "\n".join(H) + html_sco_header.sco_footer() -def _gen_but_select(name: str, codes: list[str], code_valide: str) -> str: +def _gen_but_select( + name: str, + codes: list[str], + code_valide: str, + disabled: bool = False, + klass: str = "", +) -> str: "Le menu html select avec les codes" h = "\n".join( [ @@ -2326,7 +2352,7 @@ def _gen_but_select(name: str, codes: list[str], code_valide: str) -> str: for code in codes ] ) - return f"""""" + return f"""""" @bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) From c17e2bae47888f9c325dcab1e13a3a4a535782b4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 22 Jun 2022 11:44:03 +0200 Subject: [PATCH 032/140] =?UTF-8?q?WIP:=20jury=20BUT:=20enregistrement=20d?= =?UTF-8?q?es=20d=C3=A9cisions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 167 ++++++++++++++++++++++++++++++++-- app/models/but_validations.py | 2 +- app/models/events.py | 15 +++ app/models/validations.py | 2 +- app/static/css/jury_but.css | 24 +++++ app/static/js/jury_but.js | 8 ++ app/views/notes.py | 102 ++++++++++++++------- 7 files changed, 278 insertions(+), 42 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 561ba54338..5c3d04934e 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -58,9 +58,12 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT DecisionsProposeesRCUE appelera .set_compensable() si on a la possibilité de la compenser dans le RCUE. """ +import html from operator import attrgetter +import re from typing import Union +from app import db from app import log from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem @@ -72,7 +75,7 @@ from app.models.but_refcomp import ( ApcParcours, ApcParcoursNiveauCompetence, ) -from app.models import but_validations +from app.models import Scolog from app.models.but_validations import ( ApcValidationAnnee, ApcValidationRCUE, @@ -122,10 +125,14 @@ class DecisionsProposees: self.codes = code + self.codes elif code is not None: self.codes = [code] + self.codes + self.validation = None + "Validation enregistrée" self.code_valide: str = code_valide - "La décision actuelle enregistrée" + "Code décision actuel enregistré" self.explanation: str = explanation "Explication à afficher à côté de la décision" + self.recorded = False + "true si la décision vient d'être enregistrée" def __repr__(self) -> str: return f"""<{self.__class__.__name__} valid={self.code_valide @@ -266,7 +273,15 @@ class DecisionsProposeesAnnee(DecisionsProposees): explanation: {self.explanation} """ - def annee_scolaire_sr(self) + def annee_scolaire(self) -> int: + "L'année de début de l'année scolaire" + formsemestre = self.formsemestre_impair or self.formsemestre_pair + return formsemestre.annee_scolaire() + + def annee_scolaire_str(self) -> str: + "L'année scolaire, eg '2021 - 2022'" + formsemestre = self.formsemestre_impair or self.formsemestre_pair + return formsemestre.annee_scolaire_str().replace(" ", "") def comp_formsemestres( self, formsemestre: FormSemestre @@ -397,6 +412,90 @@ class DecisionsProposeesAnnee(DecisionsProposees): decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux} return decisions_rcue_by_niveau + # def lookup_ue(self, ue_id: int) -> UniteEns: + # "check that ue_id belongs to our UE, if not returns None" + # ues = [ue for ue in self.ues_impair + self.ues_pair if ue.id == ue_id] + # assert len(ues) < 2 + # if len(ues): + # return ues[0] + # return None + + def record_form(self, form: dict): + """Enregistre les codes de jury en base + form dict: + - 'code_ue_1896' : 'AJ' code pour l'UE id 1896 + - 'code_rcue_6" : 'ADM' code pour le RCUE du niveau 6 + - 'code_annee' : 'ADM' code pour l'année + + Si les code_rcue et le code_annee ne sont pas fournis, + enregistre ceux par défaut. + """ + 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}") + 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) + + self.record_all() + db.session.commit() + + def record(self, code: str): + """Enregistre le code""" + if 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: + return # no change + if self.validation: + db.session.delete(self.validation) + db.session.flush() + + 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) + self.recorded = True + + def record_all(self): + """Enregistre les codes qui n'ont pas été spécifiés par le formulaire, + et sont donc en mode "automatique" + """ + decisions = ( + list(self.decisions_ues.values()) + + list(self.decisions_rcue_by_niveau.values()) + + [self] + ) + for dec in decisions: + if not dec.recorded: + dec.record(dec.codes[0]) # rappel: le code par défaut est en tête + class DecisionsProposeesRCUE(DecisionsProposees): """Liste des codes de décisions que l'on peut proposer pour @@ -417,10 +516,10 @@ class DecisionsProposeesRCUE(DecisionsProposees): ): super().__init__(etud=dec_prop_annee.etud) self.rcue = rcue - - validation = rcue.query_validations().first() - if validation is not None: - self.code_valide = validation.code + self.parcour = dec_prop_annee.parcour + self.validation = rcue.query_validations().first() + if self.validation is not None: + self.code_valide = self.validation.code if rcue.est_compensable(): self.codes.insert(0, sco_codes.CMP) elif rcue.est_validable(): @@ -428,6 +527,34 @@ class DecisionsProposeesRCUE(DecisionsProposees): else: self.codes.insert(0, sco_codes.AJ) + def record(self, code: str): + """Enregistre le code""" + if 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: + return # 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() + self.validation = ApcValidationRCUE( + etudid=self.etud.id, + formsemestre_id=self.rcue.formsemestre_2.id, + ue1_id=self.rcue.ue_1.id, + ue2_id=self.rcue.ue_2.id, + parcours_id=parcours_id, + code=code, + ) + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation RCUE {repr(self.rcue)}", + ) + db.session.add(self.validation) + self.recorded = True + class DecisionsProposeesUE(DecisionsProposees): """Décisions de jury sur une UE du BUT @@ -460,6 +587,7 @@ class DecisionsProposeesUE(DecisionsProposees): ue: UniteEns, ): super().__init__(etud=etud) + self.formsemestre = formsemestre self.ue: UniteEns = ue self.rcue: RegroupementCoherentUE = None "Le rcu auquel est rattaché cette UE, ou None" @@ -503,6 +631,31 @@ class DecisionsProposeesUE(DecisionsProposees): self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes self.explanation = "notes insuffisantes" + def record(self, code: str): + """Enregistre le code""" + if 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: + return # no change + if self.validation: + db.session.delete(self.validation) + db.session.flush() + self.validation = ScolarFormSemestreValidation( + etudid=self.etud.id, + formsemestre_id=self.formsemestre.id, + ue_id=self.ue.id, + code=code, + ) + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation UE {self.ue.id}", + ) + db.session.add(self.validation) + self.recorded = True + class BUTCursusEtud: # WIP TODO """Validation du cursus d'un étudiant""" diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 51d172872c..27aee4299c 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -277,4 +277,4 @@ 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.code!r}>" + return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>" diff --git a/app/models/events.py b/app/models/events.py index b94549e764..4e566cbd96 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -32,6 +32,21 @@ class Scolog(db.Model): authenticated_user = db.Column(db.Text) # login, sans contrainte # zope_remote_addr suppressed + @classmethod + def logdb( + cls, method: str = None, etudid: int = None, msg: str = None, commit=False + ): + """Add entry in student's log (replacement for old scolog.logdb)""" + entry = Scolog( + method=method, + msg=msg, + etudid=etudid, + authenticated_user=current_user.user_name, + ) + db.session.add(entry) + if commit: + db.session.commit() + class ScolarNews(db.Model): """Nouvelles pour page d'accueil""" diff --git a/app/models/validations.py b/app/models/validations.py index 976e35f9f9..00d170729f 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -36,7 +36,7 @@ class ScolarFormSemestreValidation(db.Model): # NULL pour les UE, True|False pour les semestres: assidu = db.Column(db.Boolean) event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - # NULL sauf si compense un semestre: + # NULL sauf si compense un semestre: (pas utilisé pour BUT) compense_formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css index beffba9758..1182096a95 100644 --- a/app/static/css/jury_but.css +++ b/app/static/css/jury_but.css @@ -65,4 +65,28 @@ div.but_settings { span.but_explanation { color: blueviolet; font-style: italic; +} + +select:disabled { + font-weight: bold; + color: blue; +} + +select:invalid { + background: red; +} + +select.but_code option.recorded { + color: rgb(3, 157, 3); + font-weight: bold; +} + +div.but_niveau_ue.recorded, +div.but_niveau_rcue.recorded { + border-color: rgb(136, 252, 136); + border-width: 2px; +} + +div.but_niveau_ue.modified { + background-color: rgb(255, 214, 254); } \ No newline at end of file diff --git a/app/static/js/jury_but.js b/app/static/js/jury_but.js index a2f13dd181..e67362cb23 100644 --- a/app/static/js/jury_but.js +++ b/app/static/js/jury_but.js @@ -4,3 +4,11 @@ function enable_manual_codes(elt) { $(".jury_but select.manual").prop("disabled", !elt.checked); } + +// changement 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"); +} \ No newline at end of file diff --git a/app/views/notes.py b/app/views/notes.py index 020d1cca4a..c43eb6366b 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2231,7 +2231,6 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): formsemestre_id=formsemestre_id, ), ) - # XXX TODO Page expérimentale pour les devs H = [ html_sco_header.sco_header( page_title="Validation BUT", @@ -2244,22 +2243,37 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
    """, ] + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) etud = Identite.query.get_or_404(etudid) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) - + if request.method == "POST": + deca.record_form(request.form) + flash("codes enregistrés") + return flask.redirect( + url_for( + "notes.formsemestre_validation_but", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etudid=etudid, + ) + ) H.append( f"""
    -

    Jury BUT{deca.annee_but} - Parcours {deca.parcour.libelle or "non spécifié"} - - {deca.formsemestre_impair.annee_scolaire_str()}

    +
    +

    Jury BUT{deca.annee_but} + - Parcours {deca.parcour.libelle or "non spécifié"} + - {deca.annee_scolaire_str()}

    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) +
    {deca.explanation}
    Niveaux de compétences et unités d'enseignement : @@ -2279,36 +2293,26 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): ) dec_rcue = deca.decisions_rcue_by_niveau[niveau.id] # Semestre impair - ue = dec_rcue.rcue.ue_1 H.append( - f"""
    -
    {ue.acronyme}
    -
    {scu.fmt_note(dec_rcue.rcue.moy_ue_1)}
    -
    { - _gen_but_select("code_ue_"+str(ue.id), - deca.decisions_ues[ue.id].codes, - deca.decisions_ues[ue.id].code_valide - ) - }
    -
    """ + _gen_but_niveau_ue( + dec_rcue.rcue.ue_1, + dec_rcue.rcue.moy_ue_1, + deca.decisions_ues[dec_rcue.rcue.ue_1.id], + ) ) # Semestre pair - ue = dec_rcue.rcue.ue_2 H.append( - f"""
    -
    {ue.acronyme}
    -
    {scu.fmt_note(dec_rcue.rcue.moy_ue_2)}
    -
    { - _gen_but_select("code_ue_"+str(ue.id), - deca.decisions_ues[ue.id].codes, - deca.decisions_ues[ue.id].code_valide - ) - }
    -
    """ + _gen_but_niveau_ue( + dec_rcue.rcue.ue_2, + dec_rcue.rcue.moy_ue_2, + deca.decisions_ues[dec_rcue.rcue.ue_2.id], + ) ) # RCUE H.append( - f"""
    + f"""
    {scu.fmt_note(dec_rcue.rcue.moy_rcue)}
    { _gen_but_select("code_rcue_"+str(niveau.id), @@ -2322,9 +2326,16 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): H.append("
    ") # but_annee H.append( - """
    - permettre la saisie manuelles des codes d'année et de niveaux -
    """ + """
    + + 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 ! + +
    + + + """ ) H.append("") # but_annee @@ -2348,11 +2359,36 @@ def _gen_but_select( "Le menu html select avec les codes" h = "\n".join( [ - f"""""" + f"""""" for code in codes ] ) - return f"""""" + return f""" + """ + + +def _gen_but_niveau_ue( + ue: UniteEns, moy_ue: float, dec_ue: jury_but.DecisionsProposeesUE +): + return f"""
    +
    {ue.acronyme}
    +
    {scu.fmt_note(moy_ue)}
    +
    { + _gen_but_select("code_ue_"+str(ue.id), + dec_ue.codes, + dec_ue.code_valide + ) + }
    +
    """ @bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) From 0939feb9fc1cc1f2258b8b4806f0efe63626d4e6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 22 Jun 2022 14:09:08 +0200 Subject: [PATCH 033/140] =?UTF-8?q?WIP:=20jurys=20BUT:=20force=20jury=20an?= =?UTF-8?q?nuel=20(en=20attendant=20page=20d=C3=A9di=C3=A9e=20pour=20semes?= =?UTF-8?q?tres=20isol=C3=A9s)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 126 ++++++++++++++++++++++---------------- app/comp/res_but.py | 2 +- app/models/but_refcomp.py | 34 ++++++---- app/models/formations.py | 2 + app/views/notes.py | 11 +--- 5 files changed, 102 insertions(+), 73 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 5c3d04934e..1d8c08fcec 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -87,6 +87,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation from app.scodoc import sco_codes_parcours as sco_codes +from app.scodoc.sco_codes_parcours import UE_STANDARD from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoException, ScoValueError @@ -211,13 +212,17 @@ class DecisionsProposeesAnnee(DecisionsProposees): for ue in self.ues_pair } ) - assert self.parcour is not None self.rcues_annee = self.compute_rcues_annee() "RCUEs de l'année" + formation = ( + self.formsemestre_impair.formation + if self.formsemestre_impair + else self.formsemestre_pair.formation + ) self.niveaux_competences = ApcNiveau.niveaux_annee_de_parcours( - self.parcour, self.annee_but - ).all() # XXX à trier, selon l'ordre des UE associées ? + self.parcour, self.annee_but, formation.referentiel_competence + ).all() # non triés "liste des niveaux de compétences associés à cette année" self.decisions_rcue_by_niveau = self.compute_decisions_niveaux() "les décisions rcue associées aux niveau_id" @@ -327,7 +332,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): # Parcour dans lequel l'étudiant est inscrit, et liste des UEs if res.etuds_parcour_id[etudid] is None: # pas de parcour: prend toutes les UEs (non bonus) - ues = list(res.etud_ues(etudid)) + ues = [ue for ue in res.etud_ues(etudid) if ue.type == UE_STANDARD] else: parcour = ApcParcours.query.get(res.etuds_parcour_id[etudid]) if parcour is not None: @@ -402,11 +407,12 @@ class DecisionsProposeesAnnee(DecisionsProposees): if rc.ue_1.niveau_competence_id == niveau.id: rcue = rc break - dec_rcue = DecisionsProposeesRCUE(self, rcue) - rc_niveaux.append((dec_rcue, niveau.id)) - # prévient les UE concernées :-) - self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue) - self.decisions_ues[dec_rcue.rcue.ue_2.id].set_rcue(dec_rcue.rcue) + if rcue is not None: + dec_rcue = DecisionsProposeesRCUE(self, rcue) + rc_niveaux.append((dec_rcue, niveau.id)) + # prévient les UE concernées :-) + self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue) + self.decisions_ues[dec_rcue.rcue.ue_2.id].set_rcue(dec_rcue.rcue) # Ordonne par numéro d'UE rc_niveaux.sort(key=lambda x: x[0].rcue.ue_1.numero) decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux} @@ -458,7 +464,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): def record(self, code: str): """Enregistre le code""" - if not code in self.codes: + if code and not code in self.codes: raise ScoValueError( f"code annee {html.escape(code)} invalide pour formsemestre {html.escape(self.formsemestre)}" ) @@ -467,20 +473,22 @@ class DecisionsProposeesAnnee(DecisionsProposees): if self.validation: db.session.delete(self.validation) db.session.flush() - - 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) + 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) self.recorded = True def record_all(self): @@ -494,7 +502,10 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) for dec in decisions: if not dec.recorded: - dec.record(dec.codes[0]) # rappel: le code par défaut est en tête + # rappel: le code par défaut est en tête + code = dec.codes[0] if dec.codes else None + # s'il n'y a pas de codee, efface + dec.record(dec.codes[0]) class DecisionsProposeesRCUE(DecisionsProposees): @@ -516,6 +527,9 @@ class DecisionsProposeesRCUE(DecisionsProposees): ): super().__init__(etud=dec_prop_annee.etud) self.rcue = rcue + if rcue is None: # RCUE non dispo, eg un seul semestre + self.codes = [] + return self.parcour = dec_prop_annee.parcour self.validation = rcue.query_validations().first() if self.validation is not None: @@ -529,7 +543,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): def record(self, code: str): """Enregistre le code""" - if not code in self.codes: + if code and not code in self.codes: raise ScoValueError( f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" ) @@ -539,20 +553,23 @@ class DecisionsProposeesRCUE(DecisionsProposees): if self.validation: db.session.delete(self.validation) db.session.flush() - self.validation = ApcValidationRCUE( - etudid=self.etud.id, - formsemestre_id=self.rcue.formsemestre_2.id, - ue1_id=self.rcue.ue_1.id, - ue2_id=self.rcue.ue_2.id, - parcours_id=parcours_id, - code=code, - ) - Scolog.logdb( - method="jury_but", - etudid=self.etud.id, - msg=f"Validation RCUE {repr(self.rcue)}", - ) - db.session.add(self.validation) + if code is None: + self.validation = None + else: + self.validation = ApcValidationRCUE( + etudid=self.etud.id, + formsemestre_id=self.rcue.formsemestre_2.id, + ue1_id=self.rcue.ue_1.id, + ue2_id=self.rcue.ue_2.id, + parcours_id=parcours_id, + code=code, + ) + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation RCUE {repr(self.rcue)}", + ) + db.session.add(self.validation) self.recorded = True @@ -633,7 +650,7 @@ class DecisionsProposeesUE(DecisionsProposees): def record(self, code: str): """Enregistre le code""" - if not code in self.codes: + if code and not code in self.codes: raise ScoValueError( f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" ) @@ -642,18 +659,21 @@ class DecisionsProposeesUE(DecisionsProposees): if self.validation: db.session.delete(self.validation) db.session.flush() - self.validation = ScolarFormSemestreValidation( - etudid=self.etud.id, - formsemestre_id=self.formsemestre.id, - ue_id=self.ue.id, - code=code, - ) - Scolog.logdb( - method="jury_but", - etudid=self.etud.id, - msg=f"Validation UE {self.ue.id}", - ) - db.session.add(self.validation) + if code is None: + self.validation = None + else: + self.validation = ScolarFormSemestreValidation( + etudid=self.etud.id, + formsemestre_id=self.formsemestre.id, + ue_id=self.ue.id, + code=code, + ) + Scolog.logdb( + method="jury_but", + etudid=self.etud.id, + msg=f"Validation UE {self.ue.id}", + ) + db.session.add(self.validation) self.recorded = True diff --git a/app/comp/res_but.py b/app/comp/res_but.py index b139a6ab0a..10eef7c677 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -228,5 +228,5 @@ class ResultatsSemestreBUT(NotesTableCompat): return s.index[s.notna()] def etud_ues(self, etudid: int) -> Generator[UniteEns]: - """Liste des UE auxquelles l'étudiant est inscrit (sans bonus).""" + """Liste des UE auxquelles l'étudiant est inscrit.""" return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid)) diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index da697baa4c..5db968e3db 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -270,21 +270,33 @@ class ApcNiveau(db.Model, XMLModel): @classmethod def niveaux_annee_de_parcours( - cls, parcour: "ApcParcours", annee: int + cls, + parcour: "ApcParcours", + annee: int, + referentiel_competence: ApcReferentielCompetences = None, ) -> flask_sqlalchemy.BaseQuery: - """Les niveaux de l'année du parcours""" + """Les niveaux de l'année du parcours + Si le parcour est None, tous les niveaux de l'année + """ if annee not in {1, 2, 3}: raise ValueError("annee invalide pour un parcours BUT") annee_formation = f"BUT{annee}" - return ApcNiveau.query.filter( - ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, - ApcParcours.id == ApcAnneeParcours.parcours_id, - ApcParcours.referentiel == parcour.referentiel, - ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id, - ApcCompetence.id == ApcNiveau.competence_id, - ApcAnneeParcours.parcours == parcour, - ApcNiveau.annee == annee_formation, - ) + if parcour is None: + return ApcNiveau.query.filter( + ApcNiveau.annee == annee_formation, + ApcCompetence.id == ApcNiveau.competence_id, + ApcCompetence.referentiel_id == referentiel_competence.id, + ) + else: + return ApcNiveau.query.filter( + ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, + ApcParcours.id == ApcAnneeParcours.parcours_id, + ApcParcours.referentiel == parcour.referentiel, + ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id, + ApcCompetence.id == ApcNiveau.competence_id, + ApcAnneeParcours.parcours == parcour, + ApcNiveau.annee == annee_formation, + ) app_critiques_modules = db.Table( diff --git a/app/models/formations.py b/app/models/formations.py index 9f460cff8e..f35a48ce67 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -18,6 +18,7 @@ from app.models.ues import UniteEns from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours from app.scodoc import sco_utils as scu +from app.scodoc.sco_codes_parcours import UE_STANDARD class Formation(db.Model): @@ -166,6 +167,7 @@ class Formation(db.Model): """ return UniteEns.query.filter_by(formation=self).filter( UniteEns.niveau_competence_id == ApcNiveau.id, + UniteEns.type == UE_STANDARD, ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id, ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id, ApcAnneeParcours.parcours_id == parcour.id, diff --git a/app/views/notes.py b/app/views/notes.py index c43eb6366b..ddb83f56b8 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2248,6 +2248,8 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): etud = Identite.query.get_or_404(etudid) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + if len(deca.rcues_annee) == 0: + raise ScoValueError("année incomplète: pas de jury BUT annuel possible") if request.method == "POST": deca.record_form(request.form) flash("codes enregistrés") @@ -2264,7 +2266,7 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):

    Jury BUT{deca.annee_but} - - Parcours {deca.parcour.libelle or "non spécifié"} + - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"} - {deca.annee_scolaire_str()}

    @@ -2339,13 +2341,6 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): ) H.append("") # but_annee - # ---- Toutes les UEs, pour infos - H.append(f"
      ") - for ue in formsemestre.query_ues(): # volontairement toutes les UE - dec_proposee = jury_but.DecisionsProposeesUE(etud, formsemestre, ue) - H.append("
    • " + html.escape(f"""{ue} : {dec_proposee}""") + "
    • ") - H.append(f"
    ") - H.append(f"
    ") return "\n".join(H) + html_sco_header.sco_footer() From cdef38b62b89f6714f6a6f9c2e3fc29a8d787d03 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 06:33:03 +0200 Subject: [PATCH 034/140] =?UTF-8?q?WIP:=20jury=20BUT:=20d=C3=A9but=20table?= =?UTF-8?q?=20recap=20(inachev=C3=A9e)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 19 ++- app/but/jury_but_recap.py | 179 ++++++++++++++++++++++++++ app/scodoc/sco_formsemestre_status.py | 3 +- app/views/notes.py | 28 +++- 4 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 app/but/jury_but_recap.py diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 1d8c08fcec..818809ebb7 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -188,14 +188,21 @@ class DecisionsProposeesAnnee(DecisionsProposees): "le 1er semestre de l'année scolaire considérée (S1, S3, S5)" self.formsemestre_pair = formsemestre_pair "le second formsemestre de la même année scolaire (S2, S4, S6)" - self.annee_but = formsemestre_impair.semestre_id // 2 + 1 + self.annee_but = ( + formsemestre_impair.semestre_id // 2 + 1 + if formsemestre_impair + else formsemestre_pair.semestre_id // 2 + ) "le rang de l'année dans le BUT: 1, 2, 3" assert self.annee_but in (1, 2, 3) - self.validation = ApcValidationAnnee.query.filter_by( - etudid=self.etud.id, - formsemestre_id=formsemestre_impair.id, - ordre=self.annee_but, - ).first() + if self.formsemestre_impair is not None: + self.validation = ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + formsemestre_id=formsemestre_impair.id, + ordre=self.annee_but, + ).first() + else: + self.validation = None if self.validation is not None: self.code_valide = self.validation.code self.parcour = None diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py new file mode 100644 index 0000000000..0fafa02b53 --- /dev/null +++ b/app/but/jury_but_recap.py @@ -0,0 +1,179 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: table recap annuelle et liens saisie +""" + +import time +from flask import g, url_for +from app import db + +from app.but import jury_but +from app.comp.res_but import ResultatsSemestreBUT +from app.comp import res_sem +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre +from app.models.ues import UniteEns + +from app.scodoc import sco_formsemestre_status +from app.scodoc import html_sco_header +from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoException, ScoValueError + + +class RowCollector: + def __init__(self, cells: dict = None, titles: dict = None): + self.titles = titles + self.row = cells or {} # col_id : str + self.idx = 0 + + def add_cell( + self, + col_id: str, + title: str, + content: str, + classes: str = "", + idx: int = None, + ): + "Add a row to our table. classes is a list of css class names" + self.idx = idx if idx is not None else self.idx + self.row[col_id] = content + if classes: + self.row[f"_{col_id}_class"] = classes + f" c{self.idx}" + if not col_id in self.titles: + self.titles[col_id] = title + self.titles[f"_{col_id}_col_order"] = self.idx + if classes: + self.titles[f"_{col_id}_class"] = classes + self.idx += 1 + + def __setitem__(self, key, value): + self.row[key] = value + + def __getitem__(self, key): + return self.row[key] + + +def formsemestre_saisie_jury_but( + formsemestre2: FormSemestre, readonly: bool = False +) -> str: + """formsemestre est un semestre PAIR + Si readonly, ne montre pas le lien "saisir la décision" + + => page html complète + """ + # Quick & Dirty + # pour chaque etud de res2 trié + # S1: UE1, ..., UEn + # S2: UE1, ..., UEn + # + # UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue + # + # Pour chaque etud de res2 trié + # DecisionsProposeesAnnee(etud, formsemestre2) + # Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur + # -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc + if formsemestre2.semestre_id % 2 != 0: + raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs") + + rows, titles, column_ids = get_table_jury_but(formsemestre2, readonly=readonly) + if not rows: + return ( + '
    aucun étudiant !
    ' + ) + filename = scu.sanitize_filename( + f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + table_html = build_table_jury_but_html(filename, rows, titles, column_ids) + H = [ + html_sco_header.sco_header( + page_title=f"{formsemestre2.sem_modalite()}: moyennes", + no_side_bar=True, + init_qtip=True, + javascripts=["js/etud_info.js", "js/table_recap.js"], + ), + sco_formsemestre_status.formsemestre_status_head( + formsemestre_id=formsemestre2.id + ), + ] + H.append(table_html) + H.append(html_sco_header.sco_footer()) + return "\n".join(H) + + +def build_table_jury_but_html(filename: str, rows, titles, column_ids) -> str: + """assemble la table html""" + footer_rows = [] # inutile pour l'instant, à voir XXX + selected_etudid = None # inutile pour l'instant, à voir XXX + H = [ + f"""
    """ + ] + # header + H.append( + f""" + + {scu.gen_row(column_ids, titles, "th")} + + """ + ) + # body + H.append("") + for row in rows: + H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n") + H.append("\n") + # footer + H.append("") + idx_last = len(footer_rows) - 1 + for i, row in enumerate(footer_rows): + H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n') + H.append( + """ + +
    +
    + """ + ) + return "".join(H) + + +def get_table_jury_but( + formsemestre2: FormSemestre, readonly: bool = False +) -> tuple[list[dict], list[str], list[str]]: + """Construit la table des résultats annuels pour le jury BUT""" + res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) + rcues = [] + + titles = {} # column_id : title + rows = [] + for etudid in formsemestre2.etuds_inscriptions: + etud = Identite.query.get(etudid) + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2) + row = RowCollector(titles=titles) + # --- Codes (seront cachés, mais exportés en excel) + row.add_cell("etudid", "etudid", etudid, "codes") + row.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes") + # --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser) + row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") + row.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail") + row["_nom_disp_order"] = etud.sort_key + row.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") + row.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") + row["_nom_short_order"] = etud.sort_key + row["_nom_short_target"] = url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre2.id, + etudid=etudid, + ) + 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"] + + rows.append(row.row) + 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)) + return rows, titles, column_ids diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index e06394e526..aea7eb0a76 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -410,10 +410,9 @@ def formsemestre_status_menubar(sem): }, { "title": "Saisie des décisions du jury", - "endpoint": "notes.formsemestre_recapcomplet", + "endpoint": "notes.formsemestre_saisie_jury", "args": { "formsemestre_id": formsemestre_id, - "modejury": 1, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), }, diff --git a/app/views/notes.py b/app/views/notes.py index ddb83f56b8..d7f4cbd854 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -40,7 +40,6 @@ import flask from flask import abort, flash, jsonify, redirect, render_template, url_for from flask import current_app, g, request from flask_login import current_user -from werkzeug.utils import redirect from app.but import jury_but from app.comp import res_sem @@ -57,7 +56,7 @@ from app import db from app import models from app.models import ScolarNews from app.auth.models import User -from app.but import apc_edit_ue, bulletin_but +from app.but import apc_edit_ue, bulletin_but, jury_but_recap from app.decorators import ( scodoc, scodoc7func, @@ -2535,6 +2534,31 @@ def formsemestre_validation_suppress_etud( # ------------- PV de JURY et archives sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.ScoView) + + +@bp.route("/formsemestre_saisie_jury") +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_saisie_jury(formsemestre_id: int): + """Page de saisie: liste des étudiants et lien vers page jury + en semestres pairs de BUT, table spécifique avec l'année + sinon, redirect vers page recap en mode jury + """ + readonly = 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: + return jury_but_recap.formsemestre_saisie_jury_but(formsemestre, readonly) + return redirect( + url_for( + "notes.formsemestre_recapcomplet", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + modejury=1, + ) + ) + + sco_publish( "/formsemestre_lettres_individuelles", sco_pvjury.formsemestre_lettres_individuelles, From 4ec1b9c90602765d8cfc9cf583fb55173f0a053c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 08:40:33 +0200 Subject: [PATCH 035/140] WIP: jury but: table annuelle --- app/but/jury_but_recap.py | 131 +++++++++++++++++++++++-------- app/scodoc/sco_codes_parcours.py | 3 +- 2 files changed, 99 insertions(+), 35 deletions(-) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 0fafa02b53..1a74f9294b 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -14,49 +14,23 @@ from app import db from app.but import jury_but from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem +from app.models.but_validations import RegroupementCoherentUE from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre from app.models.ues import UniteEns +from app.scodoc.sco_codes_parcours import ( + BUT_BARRE_RCUE, + BUT_BARRE_UE, + BUT_BARRE_UE8, + BUT_RCUE_SUFFISANT, +) from app.scodoc import sco_formsemestre_status from app.scodoc import html_sco_header from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoException, ScoValueError -class RowCollector: - def __init__(self, cells: dict = None, titles: dict = None): - self.titles = titles - self.row = cells or {} # col_id : str - self.idx = 0 - - def add_cell( - self, - col_id: str, - title: str, - content: str, - classes: str = "", - idx: int = None, - ): - "Add a row to our table. classes is a list of css class names" - self.idx = idx if idx is not None else self.idx - self.row[col_id] = content - if classes: - self.row[f"_{col_id}_class"] = classes + f" c{self.idx}" - if not col_id in self.titles: - self.titles[col_id] = title - self.titles[f"_{col_id}_col_order"] = self.idx - if classes: - self.titles[f"_{col_id}_class"] = classes - self.idx += 1 - - def __setitem__(self, key, value): - self.row[key] = value - - def __getitem__(self, key): - return self.row[key] - - def formsemestre_saisie_jury_but( formsemestre2: FormSemestre, readonly: bool = False ) -> str: @@ -140,6 +114,77 @@ def build_table_jury_but_html(filename: str, rows, titles, column_ids) -> str: return "".join(H) +class RowCollector: + """Une ligne de la table""" + + def __init__(self, cells: dict = None, titles: dict = None, convert_values=True): + self.titles = titles + self.row = cells or {} # col_id : str + self.idx = 0 + if convert_values: + self.fmt_note = scu.fmt_note + else: + self.fmt_note = lambda x: x + + def __setitem__(self, key, value): + self.row[key] = value + + def __getitem__(self, key): + return self.row[key] + + def add_cell( + self, + col_id: str, + title: str, + content: str, + classes: str = "", + idx: int = None, + ): + "Add a row to our table. classes is a list of css class names" + self.idx = idx if idx is not None else self.idx + self.row[col_id] = content + if classes: + self.row[f"_{col_id}_class"] = classes + f" c{self.idx}" + if not col_id in self.titles: + self.titles[col_id] = title + self.titles[f"_{col_id}_col_order"] = self.idx + if classes: + self.titles[f"_{col_id}_class"] = classes + self.idx += 1 + + def add_ue_cell(self, ue: UniteEns, val): + "cell de moyenne d'UE" + col_id = f"moy_ue_{ue.id}" + note_class = "" + if isinstance(val, float): + if val < BUT_BARRE_UE: + note_class = " moy_inf" + elif val >= BUT_BARRE_UE: + note_class = " moy_ue_valid" + if val < BUT_BARRE_UE8: + note_class = " moy_ue_warning" # notes très basses + self.add_cell(col_id, ue.acronyme, self.fmt_note(val), "col_ue" + note_class) + + def add_rcue_cell(self, rcue: RegroupementCoherentUE): + "cell de moyenne d'UE" + col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id + note_class = "" + val = rcue.moy_rcue + if isinstance(val, float): + if val < BUT_BARRE_RCUE: + note_class = " moy_inf" + elif val >= BUT_BARRE_RCUE: + note_class = " moy_ue_valid" + if val < BUT_RCUE_SUFFISANT: + note_class = " moy_ue_warning" # notes très basses + self.add_cell( + col_id, + f"{rcue.ue_1.acronyme}-{rcue.ue_1.acronyme}", + self.fmt_note(val), + "col_ue" + note_class, + ) + + def get_table_jury_but( formsemestre2: FormSemestre, readonly: bool = False ) -> tuple[list[dict], list[str], list[str]]: @@ -156,7 +201,7 @@ def get_table_jury_but( # --- Codes (seront cachés, mais exportés en excel) row.add_cell("etudid", "etudid", etudid, "codes") row.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes") - # --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser) + # --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO) row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") row.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail") row["_nom_disp_order"] = etud.sort_key @@ -172,7 +217,25 @@ def get_table_jury_but( 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"] + # --- Les RCUEs + for rcue in deca.rcues_annee: + row.add_ue_cell(rcue.ue_1, rcue.moy_ue_1) + row.add_ue_cell(rcue.ue_2, rcue.moy_ue_2) + row.add_rcue_cell(rcue) + # Le lien de saisie + if not readonly: + row.add_cell( + "lien_saisie", + "", + f"""saisie décision + """, + ) rows.append(row.row) 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)) diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index b9e4943e54..cfc4401597 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -193,7 +193,8 @@ CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée # Pour le BUT: CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} CODES_RCUE = {ADM, AJ, CMP} -BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE +BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE +BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE From e87e8723d6b766eb7b6d9b046a3fae467a2bdb19 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 09:00:52 +0200 Subject: [PATCH 036/140] Fix: titre col RCUE --- app/but/jury_but_recap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 1a74f9294b..7b9335a1a9 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -179,7 +179,7 @@ class RowCollector: note_class = " moy_ue_warning" # notes très basses self.add_cell( col_id, - f"{rcue.ue_1.acronyme}-{rcue.ue_1.acronyme}", + f"{rcue.ue_1.acronyme}-{rcue.ue_2.acronyme}", self.fmt_note(val), "col_ue" + note_class, ) From cfef65fc6897e06a7b82d2aeaf1382c837e6fa22 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 09:06:17 +0200 Subject: [PATCH 037/140] =?UTF-8?q?ue=5Fedit:=20alert()=20lors=20de=20l'en?= =?UTF-8?q?registtement=20du=20niveau=20de=20comp=C3=A9tence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/edit_ue.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/static/js/edit_ue.js b/app/static/js/edit_ue.js index 0293a82ab0..18bae83fb5 100644 --- a/app/static/js/edit_ue.js +++ b/app/static/js/edit_ue.js @@ -44,6 +44,7 @@ function set_ue_niveau_competence() { niveau_id: niveau_id, }, function (result) { + alert("niveau de compétence enregistré"); // XXX #frontend à améliorer // obj.classList.remove("sco_wait"); // obj.classList.add("sco_modified"); } From baccf122fe466c2a26f73b578b908b206aef8fb9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 09:37:35 +0200 Subject: [PATCH 038/140] =?UTF-8?q?Fix:=20cl=C3=A9=20de=20tri=20des=20?= =?UTF-8?q?=C3=A9tudiants=20(caract=C3=A8res=20sp=C3=A9ciaux)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/etudiants.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 6c9504e24b..21b3a6384a 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -135,8 +135,10 @@ class Identite(db.Model): def sort_key(self) -> tuple: "clé pour tris par ordre alphabétique" return ( - scu.suppress_accents(self.nom_usuel or self.nom or "").lower(), - scu.suppress_accents(self.prenom or "").lower(), + scu.sanitize_string( + scu.suppress_accents(self.nom_usuel or self.nom or "").lower() + ), + scu.sanitize_string(scu.suppress_accents(self.prenom or "").lower()), ) def get_first_email(self, field="email") -> str: From f1d8b6dedf63b9d4670749c60e0f0c30341cc47a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 10:13:18 +0200 Subject: [PATCH 039/140] =?UTF-8?q?Jurys=20BUT:=20corrige=20tri=20=C3=A9tu?= =?UTF-8?q?diants,=20ajout=20des=20groupes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but_recap.py | 52 +++++++++++++++++++++------------------ app/comp/res_common.py | 8 +++--- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 7b9335a1a9..672603d228 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -152,6 +152,28 @@ class RowCollector: self.titles[f"_{col_id}_class"] = classes self.idx += 1 + def add_etud_cells(self, etud: Identite, formsemestre: FormSemestre): + "Les cells code, nom, prénom etc." + # --- Codes (seront cachés, mais exportés en excel) + self.add_cell("etudid", "etudid", etud.id, "codes") + self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes") + # --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO) + self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") + self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail") + 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_order"] = etud.sort_key + self["_nom_short_target"] = url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + etudid=etud.id, + ) + self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"' + self["_nom_disp_target"] = self["_nom_short_target"] + self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"] + def add_ue_cell(self, ue: UniteEns, val): "cell de moyenne d'UE" col_id = f"moy_ue_{ue.id}" @@ -190,40 +212,20 @@ def get_table_jury_but( ) -> tuple[list[dict], list[str], list[str]]: """Construit la table des résultats annuels pour le jury BUT""" res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) - rcues = [] - titles = {} # column_id : title rows = [] for etudid in formsemestre2.etuds_inscriptions: - etud = Identite.query.get(etudid) + etud: Identite = Identite.query.get(etudid) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2) row = RowCollector(titles=titles) - # --- Codes (seront cachés, mais exportés en excel) - row.add_cell("etudid", "etudid", etudid, "codes") - row.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes") - # --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO) - row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail") - row.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail") - row["_nom_disp_order"] = etud.sort_key - row.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") - row.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") - row["_nom_short_order"] = etud.sort_key - row["_nom_short_target"] = url_for( - "notes.formsemestre_bulletinetud", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre2.id, - etudid=etudid, - ) - 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"] + row.add_etud_cells(etud, formsemestre2) + # --- Les RCUEs for rcue in deca.rcues_annee: row.add_ue_cell(rcue.ue_1, rcue.moy_ue_1) row.add_ue_cell(rcue.ue_2, rcue.moy_ue_2) row.add_rcue_cell(rcue) - - # Le lien de saisie + # --- Le lien de saisie if not readonly: row.add_cell( "lien_saisie", @@ -237,6 +239,8 @@ def get_table_jury_but( """, ) rows.append(row.row) + res2.recap_add_partitions(rows, titles) 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.sort(key=lambda row: row["_nom_disp_order"]) return rows, titles, column_ids diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 56a10d3f4f..722d4ecb26 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -664,7 +664,7 @@ class ResultatsSemestre(ResultatsCache): ) rows.append(row) - self._recap_add_partitions(rows, titles) + self.recap_add_partitions(rows, titles) self._recap_add_admissions(rows, titles) # tri par rang croissant @@ -771,7 +771,9 @@ class ResultatsSemestre(ResultatsCache): "apo": row_apo, } - def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict): + def _recap_etud_groups_infos( + self, etudid: int, row: dict, titles: dict + ): # XXX non utilisé """Table recap: ajoute à row les colonnes sur les groupes pour cet etud""" # dec = self.get_etud_decision_sem(etudid) # if dec: @@ -827,7 +829,7 @@ class ResultatsSemestre(ResultatsCache): else: row[f"_{cid}_class"] = "admission" - def _recap_add_partitions(self, rows: list[dict], titles: dict): + def recap_add_partitions(self, rows: list[dict], titles: dict): """Ajoute les colonnes indiquant les groupes rows est une liste de dict avec une clé "etudid" Les colonnes ont la classe css "partition" From 5dc20aece063b4d58b4b0cdb806f65bae9e79eaf Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 11:10:41 +0200 Subject: [PATCH 040/140] Fix pour base de IUT Calais --- .../versions/3c31bb0b27c9_fix_calais.py | 88 +++++++++++++++++++ sco_version.py | 2 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/3c31bb0b27c9_fix_calais.py diff --git a/migrations/versions/3c31bb0b27c9_fix_calais.py b/migrations/versions/3c31bb0b27c9_fix_calais.py new file mode 100644 index 0000000000..ad961abef0 --- /dev/null +++ b/migrations/versions/3c31bb0b27c9_fix_calais.py @@ -0,0 +1,88 @@ +"""fix_calais + +Revision ID: 3c31bb0b27c9 +Revises: d5b3bdd1d622 +Create Date: 2022-06-23 10:48:27.787550 + +Pour réparer les bases auxquelles il manquerait le ref. de comp.: +bug dit "de Calais" + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "3c31bb0b27c9" +down_revision = "d5b3bdd1d622" +branch_labels = None +depends_on = None + + +# Voir https://stackoverflow.com/questions/24082542/check-if-a-table-column-exists-in-the-database-using-sqlalchemy-and-alembic +from sqlalchemy import inspect + + +def column_exists(table_name, column_name): + bind = op.get_context().bind + insp = inspect(bind) + columns = insp.get_columns(table_name) + return any(c["name"] == column_name for c in columns) + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + if not column_exists("apc_competence", "id_orebut"): + op.add_column( + "apc_competence", sa.Column("id_orebut", sa.Text(), nullable=True) + ) + op.create_index( + op.f("ix_apc_competence_id_orebut"), + "apc_competence", + ["id_orebut"], + unique=False, + ) + op.drop_constraint( + "apc_competence_referentiel_id_titre_key", "apc_competence", type_="unique" + ) + if not column_exists("apc_referentiel_competences", "annexe"): + op.add_column( + "apc_referentiel_competences", sa.Column("annexe", sa.Text(), nullable=True) + ) + if not column_exists("apc_referentiel_competences", "type_structure"): + op.add_column( + "apc_referentiel_competences", + sa.Column("type_structure", sa.Text(), nullable=True), + ) + if not column_exists("apc_referentiel_competences", "type_departement"): + op.add_column( + "apc_referentiel_competences", + sa.Column("type_departement", sa.Text(), nullable=True), + ) + if not column_exists("apc_referentiel_competences", "version_orebut"): + op.add_column( + "apc_referentiel_competences", + sa.Column("version_orebut", sa.Text(), nullable=True), + ) + + # op.create_index( + # op.f("ix_scolar_news_authenticated_user"), + # "scolar_news", + # ["authenticated_user"], + # unique=False, + # ) + # op.create_index(op.f("ix_scolar_news_date"), "scolar_news", ["date"], unique=False) + # op.create_index( + # op.f("ix_scolar_news_object"), "scolar_news", ["object"], unique=False + # ) + # op.create_index(op.f("ix_scolar_news_type"), "scolar_news", ["type"], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + # ne fait rien ! ce script est idempotent + pass + # ### end Alembic commands ### diff --git a/sco_version.py b/sco_version.py index be28c9f10b..1b8a307831 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.2.25" +SCOVERSION = "9.2.26" SCONAME = "ScoDoc" From 137e04be047c504a0812813d74853725253123e1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 11:28:20 +0200 Subject: [PATCH 041/140] raccordement migrations (insertion "calais") --- migrations/versions/3c31bb0b27c9_fix_calais.py | 12 ------------ migrations/versions/af77ca6a89d0_news_index.py | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/migrations/versions/3c31bb0b27c9_fix_calais.py b/migrations/versions/3c31bb0b27c9_fix_calais.py index ad961abef0..7efb5927d1 100644 --- a/migrations/versions/3c31bb0b27c9_fix_calais.py +++ b/migrations/versions/3c31bb0b27c9_fix_calais.py @@ -65,18 +65,6 @@ def upgrade(): sa.Column("version_orebut", sa.Text(), nullable=True), ) - # op.create_index( - # op.f("ix_scolar_news_authenticated_user"), - # "scolar_news", - # ["authenticated_user"], - # unique=False, - # ) - # op.create_index(op.f("ix_scolar_news_date"), "scolar_news", ["date"], unique=False) - # op.create_index( - # op.f("ix_scolar_news_object"), "scolar_news", ["object"], unique=False - # ) - # op.create_index(op.f("ix_scolar_news_type"), "scolar_news", ["type"], unique=False) - # ### end Alembic commands ### diff --git a/migrations/versions/af77ca6a89d0_news_index.py b/migrations/versions/af77ca6a89d0_news_index.py index 0a4d2edfa3..eb1c036813 100644 --- a/migrations/versions/af77ca6a89d0_news_index.py +++ b/migrations/versions/af77ca6a89d0_news_index.py @@ -11,7 +11,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "af77ca6a89d0" -down_revision = "d5b3bdd1d622" +down_revision = "3c31bb0b27c9" branch_labels = None depends_on = None From 9c80c36425c2f25fbc8fb8d69262b589838a1bf2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 13:48:43 +0200 Subject: [PATCH 042/140] Fix: set_ue_niveau_competence --- app/views/notes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/notes.py b/app/views/notes.py index d7f4cbd854..715be55a4b 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -420,8 +420,9 @@ sco_publish( @bp.route("/set_ue_niveau_competence", methods=["POST"]) +@scodoc @permission_required(Permission.ScoChangeFormation) -def set_ue_niveau_competence(scodoc_dept=""): +def set_ue_niveau_competence(): "associe UE et niveau" ue_id = request.form.get("ue_id") niveau_id = request.form.get("niveau_id") From bb68b85b618af001f0adfed1de58d8794a19b33e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 13:51:56 +0200 Subject: [PATCH 043/140] Fix: ues_impair_sans_rcue --- app/but/jury_but.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 818809ebb7..f81dab4fd3 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -390,7 +390,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.formsemestre_pair, ue_pair, ) - ues_impair_sans_rcue.remove(ue_impair.id) + ues_impair_sans_rcue.discard(ue_impair.id) break if rcue is None: raise ScoValueError(f"pas de RCUE pour l'UE {ue_pair.acronyme}") From 99c22bdb83693cf8ad3bd1e545e92a87d7434c43 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 14:16:53 +0200 Subject: [PATCH 044/140] Fix: chargement ref. MT2E --- app/views/refcomp.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/views/refcomp.py b/app/views/refcomp.py index fb9e2eb1b9..53a2a4647e 100644 --- a/app/views/refcomp.py +++ b/app/views/refcomp.py @@ -197,10 +197,11 @@ def refcomp_load(formation_id=None): refs_distrib_dict = [{"id": 0, "specialite": "Aucun", "created": "", "serial": ""}] i = 1 for filename in refs_distrib_files: - m = re.match(r".*/but-([A-Za-z_]+)-([0-9]+)-([0-9]+).xml", str(filename)) - if ( - m - and ApcReferentielCompetences.query.filter_by( + m = re.match(r".*/but-([A-Za-z0-9_]+)-([0-9]+)-([0-9]+).xml", str(filename)) + if not m: + log(f"refcomp_load: ignoring {filename} (invalid filename)") + elif ( + ApcReferentielCompetences.query.filter_by( scodoc_orig_filename=Path(filename).name, dept_id=g.scodoc_dept_id ).count() == 0 @@ -216,7 +217,7 @@ def refcomp_load(formation_id=None): ) i += 1 else: - log(f"refcomp_load: ignoring {filename} (invalid filename)") + log(f"refcomp_load: ignoring {filename} (already loaded)") form = RefCompLoadForm() form.referentiel_standard.choices = [ From a41f92d550e22c30d3c17788c7ad0148b732be23 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 15:04:08 +0200 Subject: [PATCH 045/140] =?UTF-8?q?ue=5Fedit:=20interdit=20association=20d?= =?UTF-8?q?e=202=20UE=20du=20meme=20sem.=20auy=20m=C3=AAme=20niveau=20de?= =?UTF-8?q?=20comp.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/apc_edit_ue.py | 39 ++++++++++++++++++++++++++++++++++----- app/scodoc/sco_edit_ue.py | 2 +- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py index 55687f0fed..b88e43cf74 100644 --- a/app/but/apc_edit_ue.py +++ b/app/but/apc_edit_ue.py @@ -9,11 +9,11 @@ Edition associations UE <-> Ref. Compétence """ from flask import g, url_for from app import db, log -from app.models import UniteEns +from app.models import Formation, UniteEns from app.models.but_refcomp import ApcNiveau -def form_ue_choix_niveau(ue: UniteEns) -> str: +def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str: """Form. HTML pour associer une UE à un niveau de compétence""" ref_comp = ue.formation.referentiel_competence if ref_comp is None: @@ -27,20 +27,39 @@ def form_ue_choix_niveau(ue: UniteEns) -> str: annee = (ue.semestre_idx + 1) // 2 # 1, 2, 3 niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee) + # Les niveaux déjà associés à d'autres UE du même semestre + autres_ues = formation.ues.filter_by(semestre_idx=ue.semestre_idx) + niveaux_autres_ues = { + oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id + } options = [] - if niveaux_by_parcours["TC"]: + if niveaux_by_parcours["TC"]: # TC pour Tronc Commun options.append("""""") for n in niveaux_by_parcours["TC"]: + if n.id in niveaux_autres_ues: + disabled = "disabled" + else: + disabled = "" options.append( - f"""""" + f"""""" ) options.append("""""") for parcour in ref_comp.parcours: if len(niveaux_by_parcours[parcour.id]): options.append(f"""""") for n in niveaux_by_parcours[parcour.id]: + if n.id in niveaux_autres_ues: + disabled = "disabled" + else: + disabled = "" options.append( - f"""""" + f"""""" ) options.append("""""") options_str = "\n".join(options) @@ -63,6 +82,16 @@ def set_ue_niveau_competence(ue_id: int, niveau_id: int): """Associe le niveau et l'UE""" log(f"set_ue_niveau_competence( {ue_id}, {niveau_id} )") ue = UniteEns.query.get_or_404(ue_id) + + autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx) + niveaux_autres_ues = { + oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id + } + if niveau_id in niveaux_autres_ues: + log( + f"set_ue_niveau_competence: denying association of {ue} to already associated {niveau_id}" + ) + return "", 409 # conflict if niveau_id == "": # suppression de l'association ue.niveau_competence = None diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index a0a4e1d423..a2a6c05c8d 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -423,7 +423,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No if tf[0] == 0: niveau_competence_div = "" if ue and is_apc: - niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(ue) + niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(formation, ue) if ue and ue.modules.count() and ue.semestre_idx is not None: modules_div = f"""
    {ue.modules.count()} modules sont rattachés From 995fe1981b8c57a42bade4e1b57b81cf347188ab Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 15:33:01 +0200 Subject: [PATCH 046/140] =?UTF-8?q?Pas=20de=20niveau=20de=20comp=C3=A9tenc?= =?UTF-8?q?e=20pour=20les=20UE=20bonus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/apc_edit_ue.py | 3 +++ app/templates/pn/form_ues.html | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py index b88e43cf74..dd8e60a861 100644 --- a/app/but/apc_edit_ue.py +++ b/app/but/apc_edit_ue.py @@ -11,10 +11,13 @@ from flask import g, url_for from app import db, log from app.models import Formation, UniteEns from app.models.but_refcomp import ApcNiveau +from app.scodoc import sco_codes_parcours def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str: """Form. HTML pour associer une UE à un niveau de compétence""" + if ue.type != sco_codes_parcours.UE_STANDARD: + return "" ref_comp = ue.formation.referentiel_competence if ref_comp is None: return f"""
    diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html index 8364885494..a867bfc953 100644 --- a/app/templates/pn/form_ues.html +++ b/app/templates/pn/form_ues.html @@ -32,8 +32,13 @@ ue.color if ue.color is not none else 'blue'}}"> {{ue.acronyme}} {{ue.titre}} {% set virg = joiner(", ") %} ( @@ -42,7 +47,7 @@ else 'aucun'|safe}} ECTS) - {% if ue.niveau_competence is none %} + {% if (ue.niveau_competence is none) and ue.type == 0 %} pas de compétence associée {% endif %} From 6f842dc8777b5caa87c43bf8bb4ebe2f715ae5ce Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Jun 2022 17:53:27 +0200 Subject: [PATCH 047/140] =?UTF-8?q?Fix:=20bug=20"rennes"=20sur=20les=20for?= =?UTF-8?q?msemestres=20sans=20formation=20associ=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_bulletins.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index df790649a6..daf4a9419d 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -158,9 +158,24 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): I["server_name"] = request.url_root # Formation et parcours - I["formation"] = sco_formations.formation_list( - args={"formation_id": I["sem"]["formation_id"]} - )[0] + if I["sem"]["formation_id"]: + I["formation"] = sco_formations.formation_list( + args={"formation_id": I["sem"]["formation_id"]} + )[0] + else: # what's the fuck ? + I["formation"] = { + "acronyme": "?", + "code_specialite": "", + "dept_id": 1, + "formation_code": "?", + "formation_id": -1, + "id": -1, + "referentiel_competence_id": None, + "titre": "?", + "titre_officiel": "?", + "type_parcours": 0, + "version": 0, + } I["parcours"] = sco_codes_parcours.get_parcours_from_code( I["formation"]["type_parcours"] ) From f546837821f4f0e63a02871caebf2b7e0920a2f2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 03:32:48 +0200 Subject: [PATCH 048/140] Modif config log: moins verbeux, evite requetes ordinaires --- app/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/__init__.py b/app/__init__.py index a1862aaa70..66c6effd1c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -205,6 +205,10 @@ def create_app(config_class=DevConfig): app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static") app.wsgi_app = ReverseProxied(app.wsgi_app) app.logger.setLevel(logging.DEBUG) + + # Evite de logguer toutes les requetes dans notre log + logging.getLogger("werkzeug").disabled = True + app.config.from_object(config_class) db.init_app(app) From 3e5ae5d96138aac161ed58318ff2eb1c781ac97b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 03:34:52 +0200 Subject: [PATCH 049/140] Fix "Da Nang": invalidation caches poids evals sur modifs UEs --- app/models/formations.py | 8 ++++++++ app/scodoc/sco_cache.py | 10 +++++++++- app/scodoc/sco_edit_ue.py | 29 ++++++++++++----------------- app/scodoc/sco_moduleimpl_status.py | 2 +- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/app/models/formations.py b/app/models/formations.py index 43d3d680c0..8429571157 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -6,6 +6,7 @@ from app import db from app.comp import df_cache from app.models import SHORT_STR_LEN from app.models.modules import Module +from app.models.moduleimpls import ModuleImpl from app.models.ues import UniteEns from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours @@ -97,6 +98,13 @@ class Formation(db.Model): else: keys = f"{self.id}.{semestre_idx}" df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"}) + # Invalidate aussi les poids de toutes les évals de la formation + for modimpl in ModuleImpl.query.filter( + ModuleImpl.module_id == Module.id, + Module.formation_id == self.id, + ): + modimpl.invalidate_evaluations_poids() + sco_cache.invalidate_formsemestre() def invalidate_cached_sems(self): diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 8fcc6f7efe..7975e3b2cf 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -67,6 +67,7 @@ class ScoDocCache: timeout = None # ttl, infinite by default prefix = "" + verbose = False # if true, verbose logging (debug) @classmethod def _get_key(cls, oid): @@ -87,7 +88,10 @@ class ScoDocCache: def set(cls, oid, value): """Store value""" key = cls._get_key(oid) - # log(f"CACHE key={key}, type={type(value)}, timeout={cls.timeout}") + if cls.verbose: + log( + f"{cls.__name__}.set key={key}, type={type(value).__name__}, timeout={cls.timeout}" + ) try: status = CACHE.set(key, value, timeout=cls.timeout) if not status: @@ -101,11 +105,15 @@ class ScoDocCache: @classmethod def delete(cls, oid): """Remove from cache""" + # if cls.verbose: + # log(f"{cls.__name__}.delete({oid})") CACHE.delete(cls._get_key(oid)) @classmethod def delete_many(cls, oids): """Remove multiple keys at once""" + if cls.verbose: + log(f"{cls.__name__}.delete_many({oids})") # delete_many seems bugged: # CACHE.delete_many([cls._get_key(oid) for oid in oids]) for oid in oids: diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 33ffc69cf0..82dd77b1ed 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -121,12 +121,7 @@ def do_ue_create(args): # create ue_id = _ueEditor.create(cnx, args) - # Invalidate cache: vire les poids de toutes les évals de la formation - for modimpl in ModuleImpl.query.filter( - ModuleImpl.module_id == Module.id, Module.formation_id == args["formation_id"] - ): - modimpl.invalidate_evaluations_poids() - formation = Formation.query.get(args["formation_id"]) + formation: Formation = Formation.query.get(args["formation_id"]) formation.invalidate_module_coefs() # news ue = UniteEns.query.get(ue_id) @@ -144,11 +139,10 @@ def do_ue_create(args): def do_ue_delete(ue_id, delete_validations=False, force=False): "delete UE and attached matieres (but not modules)" - from app.scodoc import sco_formations from app.scodoc import sco_parcours_dut ue = UniteEns.query.get_or_404(ue_id) - formation_id = ue.formation_id + formation = ue.formation semestre_idx = ue.semestre_idx if not ue.can_be_deleted(): raise ScoNonEmptyFormationObject( @@ -157,7 +151,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): dest_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, + formation_id=formation.id, semestre_idx=semestre_idx, ), ) @@ -181,7 +175,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): cancel_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, + formation_id=formation.id, semestre_idx=semestre_idx, ), parameters={"ue_id": ue.id, "dialog_confirmed": 1}, @@ -207,13 +201,13 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): _ueEditor.delete(cnx, ue.id) # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement # utilisé: acceptable de tout invalider): - sco_cache.invalidate_formsemestre() + formation.invalidate_module_coefs() + # -> invalide aussi .invalidate_formsemestre() # news - F = sco_formations.formation_list(args={"formation_id": formation_id})[0] ScolarNews.add( typ=ScolarNews.NEWS_FORM, - obj=formation_id, - text=f"Modification de la formation {F['acronyme']}", + obj=formation.id, + text=f"Modification de la formation {formation.acronyme}", max_frequency=10 * 60, ) # @@ -222,7 +216,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=formation_id, + formation_id=formation.id, semestre_idx=semestre_idx, ) ) @@ -1304,8 +1298,9 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False): formation = Formation.query.get(ue["formation_id"]) if not dont_invalidate_cache: - # Invalide les semestres utilisant cette formation: - formation.invalidate_cached_sems() + # Invalide les semestres utilisant cette formation + # ainsi que les poids et coefs + formation.invalidate_module_coefs() # essai edition en ligne: diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index b9e6a3ee05..59bcea3a80 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -192,7 +192,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): """Tableau de bord module (liste des evaluations etc)""" if not isinstance(moduleimpl_id, int): raise ScoInvalidIdType("moduleimpl_id must be an integer !") - modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) + modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) M = modimpl.to_dict() formsemestre_id = M["formsemestre_id"] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] From 27271e5c9621b6d243fb1c5f8bd0ae6c8e7d884b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 04:16:31 +0200 Subject: [PATCH 050/140] Cosmetic / n'affiche pas ECTS pour UE bonus --- app/but/forms/refcomp_forms.py | 2 +- app/scodoc/sco_edit_apc.py | 2 +- app/static/css/jury_but.css | 13 ++++++++++++- app/templates/pn/form_ues.html | 10 ++++++++-- app/views/notes.py | 10 ++++++---- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/app/but/forms/refcomp_forms.py b/app/but/forms/refcomp_forms.py index b39ea21317..ee67cba284 100644 --- a/app/but/forms/refcomp_forms.py +++ b/app/but/forms/refcomp_forms.py @@ -25,7 +25,7 @@ class RefCompLoadForm(FlaskForm): "Choisir un référentiel de compétences officiel BUT" ) upload = FileField( - label="Ou bien sélectionner un fichier XML au format Orébut", + label="... ou bien sélectionner un fichier XML au format Orébut (réservé aux développeurs !)", validators=[ FileAllowed( [ diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index 8cb516a5a2..7e71f51d1f 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -76,7 +76,7 @@ def html_edit_formation_apc( ues_by_sem[semestre_idx] = formation.ues.filter_by( semestre_idx=semestre_idx ).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme) - ects = [ue.ects for ue in ues_by_sem[semestre_idx]] + ects = [ue.ects for ue in ues_by_sem[semestre_idx] if ue.type != UE_SPORT] if None in ects: ects_by_sem[semestre_idx] = 'manquant' else: diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css index 1182096a95..642fab4c6a 100644 --- a/app/static/css/jury_but.css +++ b/app/static/css/jury_but.css @@ -1,9 +1,20 @@ /* Saisie décision de jury BUT */ -.jury_but { +.jury_but form { font-family: Verdana, Geneva, Tahoma, sans-serif; } +.jury_but .titre_parcours { + font-size: 130%; + padding-bottom: 12px; +} + +.jury_but .nom_etud { + font-size: 100%; + font-weight: bold; + padding-bottom: 12px; +} + .but_annee { display: inline-grid; grid-template-columns: repeat(4, auto); diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html index a867bfc953..fed118333b 100644 --- a/app/templates/pn/form_ues.html +++ b/app/templates/pn/form_ues.html @@ -43,8 +43,14 @@ {% set virg = joiner(", ") %} ( {%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%} - {{ virg() }}{{ue.ects if ue.ects is not none - else 'aucun'|safe}} ECTS) + {{ virg() }} + {%- if ue.type == 0 -%} + {{ue.ects + if ue.ects is not none + else 'aucun'|safe + }} ECTS + {%- endif -%} + ) {% if (ue.niveau_competence is none) and ue.type == 0 %} diff --git a/app/views/notes.py b/app/views/notes.py index 715be55a4b..b13b2667fd 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2263,12 +2263,14 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): ) H.append( f""" -
    -
    -

    Jury BUT{deca.annee_but} +
    +
    Jury BUT{deca.annee_but} - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"} - - {deca.annee_scolaire_str()}

    + - {deca.annee_scolaire_str()}
    +
    {etud.nomprenom}
    + +
    Décision de jury pour l'année : { From 145f69aee229b0dfd351614da4ae11aefa453521 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 07:28:27 +0200 Subject: [PATCH 051/140] Jury BUT: navigation liste/saisie --- app/but/jury_but.py | 6 +++++- app/but/jury_but_recap.py | 28 ++++++++++++++++++++-------- app/static/css/jury_but.css | 8 ++++++++ app/static/css/scodoc.css | 6 ++++++ app/views/notes.py | 20 ++++++++++++++------ 5 files changed, 53 insertions(+), 15 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index f81dab4fd3..9b1c5e8c9d 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -269,7 +269,11 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8" else: self.codes = [sco_codes.RED, sco_codes.NAR, sco_codes.ADJ] + self.codes - self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} niveau < 8" + self.explanation = ( + expl_rcues + + f""" et {self.nb_rcues_under_8} + niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8""" + ) # def infos(self) -> str: diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 672603d228..e19d5fc3fe 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -9,7 +9,6 @@ import time from flask import g, url_for -from app import db from app.but import jury_but from app.comp.res_but import ResultatsSemestreBUT @@ -28,11 +27,11 @@ from app.scodoc.sco_codes_parcours import ( from app.scodoc import sco_formsemestre_status from app.scodoc import html_sco_header from app.scodoc import sco_utils as scu -from app.scodoc.sco_exceptions import ScoException, ScoValueError +from app.scodoc.sco_exceptions import ScoValueError def formsemestre_saisie_jury_but( - formsemestre2: FormSemestre, readonly: bool = False + formsemestre2: FormSemestre, readonly: bool = False, selected_etudid: int = None ) -> str: """formsemestre est un semestre PAIR Si readonly, ne montre pas le lien "saisir la décision" @@ -61,7 +60,9 @@ def formsemestre_saisie_jury_but( filename = scu.sanitize_filename( f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}""" ) - table_html = build_table_jury_but_html(filename, rows, titles, column_ids) + table_html = build_table_jury_but_html( + filename, rows, titles, column_ids, selected_etudid=selected_etudid + ) H = [ html_sco_header.sco_header( page_title=f"{formsemestre2.sem_modalite()}: moyennes", @@ -78,10 +79,11 @@ def formsemestre_saisie_jury_but( return "\n".join(H) -def build_table_jury_but_html(filename: str, rows, titles, column_ids) -> str: +def build_table_jury_but_html( + filename: str, rows, titles, column_ids, selected_etudid: int = None +) -> str: """assemble la table html""" footer_rows = [] # inutile pour l'instant, à voir XXX - selected_etudid = None # inutile pour l'instant, à voir XXX H = [ f"""
    """ @@ -225,17 +227,27 @@ def get_table_jury_but( row.add_ue_cell(rcue.ue_1, rcue.moy_ue_1) row.add_ue_cell(rcue.ue_2, rcue.moy_ue_2) row.add_rcue_cell(rcue) + # --- Le code annuel existant + row.add_cell( + "code_annee", + "Année", + f"""{deca.code_valide or ''}""", + "col_code_annee", + ) # --- Le lien de saisie if not readonly: row.add_cell( "lien_saisie", "", - f"""saisie décision + )}" class="stdlink"> + {"modif." if deca.code_valide else "saisie"} + décision """, ) rows.append(row.row) diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css index 642fab4c6a..f3266bc63e 100644 --- a/app/static/css/jury_but.css +++ b/app/static/css/jury_but.css @@ -100,4 +100,12 @@ div.but_niveau_rcue.recorded { div.but_niveau_ue.modified { background-color: rgb(255, 214, 254); +} + +div.but_buttons { + margin-top: 16px; +} + +div.but_buttons span { + margin-right: 16px; } \ No newline at end of file diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 062c69c6dd..95e76fb2b0 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -3799,6 +3799,12 @@ table.table_recap a:visited { color: black; } +table.table_recap a.stdlink:link, +table.table_recap a.stdlink:visited { + color: blue; + text-decoration: underline; +} + table.table_recap tfoot th, table.table_recap thead th { text-align: left; diff --git a/app/views/notes.py b/app/views/notes.py index b13b2667fd..04fe1da350 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2280,7 +2280,7 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): {deca.explanation} - Niveaux de compétences et unités d'enseignement : +
    Niveaux de compétences et unités d'enseignement :
    S{1}
    @@ -2330,15 +2330,21 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): H.append("
    ") # but_annee 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 !
    - - + + """ ) H.append("") # but_annee @@ -2543,7 +2549,7 @@ sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.S @scodoc @permission_required(Permission.ScoView) @scodoc7func -def formsemestre_saisie_jury(formsemestre_id: int): +def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None): """Page de saisie: liste des étudiants et lien vers page jury en semestres pairs de BUT, table spécifique avec l'année sinon, redirect vers page recap en mode jury @@ -2551,7 +2557,9 @@ def formsemestre_saisie_jury(formsemestre_id: int): readonly = 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: - return jury_but_recap.formsemestre_saisie_jury_but(formsemestre, readonly) + return jury_but_recap.formsemestre_saisie_jury_but( + formsemestre, readonly, selected_etudid=selected_etudid + ) return redirect( url_for( "notes.formsemestre_recapcomplet", From dcee2a441fb2b8f54a80afbe1f47fca1862cb955 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 08:59:09 +0200 Subject: [PATCH 052/140] Tableau jury BUT: col. RCUEs avec tri sur moy. gen. S_pair --- app/but/jury_but.py | 21 +++++++++++++---- app/but/jury_but_recap.py | 48 ++++++++++++++++++++++++++++++++++----- app/comp/res_common.py | 4 ++-- app/static/css/scodoc.css | 18 +++++++++++---- 4 files changed, 74 insertions(+), 17 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 9b1c5e8c9d..429f96a736 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -207,6 +207,19 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.code_valide = self.validation.code self.parcour = None "Le parcours considéré (celui du semestre pair, ou à défaut impair)" + if self.formsemestre_pair is not None: + self.res_pair: ResultatsSemestreBUT = res_sem.load_formsemestre_results( + self.formsemestre_pair + ) + else: + self.res_pair = None + if self.formsemestre_impair is not None: + self.res_impair: ResultatsSemestreBUT = res_sem.load_formsemestre_results( + self.formsemestre_impair + ) + else: + self.res_impair = None + self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all self.decisions_ues = { ue.id: DecisionsProposeesUE(etud, formsemestre_impair, ue) @@ -332,14 +345,14 @@ class DecisionsProposeesAnnee(DecisionsProposees): """ etudid = self.etud.id ues_sems = [] - for formsemestre in self.formsemestre_impair, self.formsemestre_pair: + for (formsemestre, res) in ( + (self.formsemestre_impair, self.res_impair), + (self.formsemestre_pair, self.res_pair), + ): if formsemestre is None: ues = [] else: formation: Formation = formsemestre.formation - res: ResultatsSemestreBUT = res_sem.load_formsemestre_results( - formsemestre - ) # Parcour dans lequel l'étudiant est inscrit, et liste des UEs if res.etuds_parcour_id[etudid] is None: # pas de parcour: prend toutes les UEs (non bonus) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index e19d5fc3fe..346b89e5ee 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -11,6 +11,7 @@ import time from flask import g, url_for from app.but import jury_but +from app.but.jury_but import DecisionsProposeesAnnee from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem from app.models.but_validations import RegroupementCoherentUE @@ -65,7 +66,7 @@ def formsemestre_saisie_jury_but( ) H = [ html_sco_header.sco_header( - page_title=f"{formsemestre2.sem_modalite()}: moyennes", + page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel", no_side_bar=True, init_qtip=True, javascripts=["js/etud_info.js", "js/table_recap.js"], @@ -123,6 +124,7 @@ class RowCollector: self.titles = titles self.row = cells or {} # col_id : str self.idx = 0 + self.last_etud_cell_idx = 0 if convert_values: self.fmt_note = scu.fmt_note else: @@ -175,6 +177,7 @@ class RowCollector: self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"' self["_nom_disp_target"] = self["_nom_short_target"] self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"] + self.last_etud_cell_idx = self.idx def add_ue_cell(self, ue: UniteEns, val): "cell de moyenne d'UE" @@ -196,18 +199,48 @@ class RowCollector: val = rcue.moy_rcue if isinstance(val, float): if val < BUT_BARRE_RCUE: - note_class = " moy_inf" + note_class = " moy_ue_inf" elif val >= BUT_BARRE_RCUE: note_class = " moy_ue_valid" if val < BUT_RCUE_SUFFISANT: note_class = " moy_ue_warning" # notes très basses self.add_cell( col_id, - f"{rcue.ue_1.acronyme}-{rcue.ue_2.acronyme}", + f"
    {rcue.ue_1.acronyme}
    {rcue.ue_2.acronyme}
    ", self.fmt_note(val), - "col_ue" + note_class, + "col_rcue" + note_class, ) + def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee): + "cell avec nb niveaux validables / total" + klass = " " + if deca.nb_rcues_under_8 > 0: + klass += "moy_ue_warning" + elif deca.nb_validables < deca.nb_competences: + klass += "moy_ue_inf" + else: + klass += "moy_ue_valid" + self.add_cell( + "rcues_validables", + "RCUEs", + f"""{deca.nb_validables}/{deca.nb_competences}""" + + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), + "col_rcue col_rcues_validables" + klass, + ) + 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: + moy_gen_d = f"{int(deca.res_pair.etud_moy_gen[deca.etud.id]*1000):05}" + else: + moy_gen_d = "x" + self["_rcues_validables_order"] = f"{deca.nb_validables:04d}-{moy_gen_d}" + else: + # etudiants sans RCUE: pas de semestre impair, ... + # les classe à la fin + self[ + "_rcues_validables_order" + ] = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}" + def get_table_jury_but( formsemestre2: FormSemestre, readonly: bool = False @@ -221,7 +254,9 @@ def get_table_jury_but( deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2) row = RowCollector(titles=titles) row.add_etud_cells(etud, formsemestre2) - + row.idx = 100 # laisse place pour les colonnes de groupes + # --- Nombre de niveaux + row.add_nb_rcues_cell(deca) # --- Les RCUEs for rcue in deca.rcues_annee: row.add_ue_cell(rcue.ue_1, rcue.moy_ue_1) @@ -251,7 +286,8 @@ def get_table_jury_but( """, ) rows.append(row.row) - res2.recap_add_partitions(rows, titles) + if len(rows) > 0: + res2.recap_add_partitions(rows, titles, col_idx=row.last_etud_cell_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.sort(key=lambda row: row["_nom_disp_order"]) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 722d4ecb26..3b5f08964c 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -829,7 +829,7 @@ class ResultatsSemestre(ResultatsCache): else: row[f"_{cid}_class"] = "admission" - def recap_add_partitions(self, rows: list[dict], titles: dict): + def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None): """Ajoute les colonnes indiquant les groupes rows est une liste de dict avec une clé "etudid" Les colonnes ont la classe css "partition" @@ -838,7 +838,7 @@ class ResultatsSemestre(ResultatsCache): self.formsemestre.id ) first_partition = True - col_order = 10 + col_order = 10 if col_idx is None else col_idx for partition in partitions: cid = f"part_{partition['partition_id']}" rg_cid = cid + "_rg" # rang dans la partition diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 95e76fb2b0..6b41b761b1 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2371,7 +2371,6 @@ td.recap_col_ue_inf { padding-right: 1.2em; padding-left: 4px; text-align: left; - font-weight: bold; color: rgb(255, 0, 0); border-left: 1px solid blue; } @@ -2380,7 +2379,6 @@ td.recap_col_ue_val { padding-right: 1.2em; padding-left: 4px; text-align: left; - font-weight: bold; color: rgb(0, 140, 0); border-left: 1px solid blue; } @@ -3773,6 +3771,19 @@ table.table_recap .group { border-left: 1px solid blue; } +table.table_recap .col_ue { + font-weight: bold; +} + +table.table_recap.jury .col_ue { + font-weight: normal; +} + +table.table_recap.jury .col_rcue { + font-weight: bold; +} + + table.table_recap .group { border-left: 1px dashed rgb(160, 160, 160); white-space: nowrap; @@ -3817,18 +3828,15 @@ table.table_recap td.moy_inf { } table.table_recap td.moy_ue_valid { - font-weight: bold; color: rgb(0, 140, 0); } table.table_recap td.moy_ue_warning { - font-weight: bold; color: rgb(255, 0, 0); } table.table_recap td.col_ues_validables { white-space: nowrap; - font-weight: bold; font-style: normal !important; } From 00f66be1c541882c6019b95e874b81667fcc9a6d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 12:00:08 +0200 Subject: [PATCH 053/140] Table jury BUT: pas de colonnes rangs et admissions --- app/but/jury_but_recap.py | 2 +- app/static/js/table_recap.js | 44 +++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 346b89e5ee..47f826083f 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -86,7 +86,7 @@ def build_table_jury_but_html( """assemble la table html""" footer_rows = [] # inutile pour l'instant, à voir XXX H = [ - f"""
    """ ] # header diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index fd5068e609..0c87ecdaaf 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -22,16 +22,21 @@ $(function () { dt.buttons('toggle_partitions:name').text(visible ? "Montrer groupes" : "Cacher les groupes"); } }, - { - name: "toggle_partitions_rangs", - text: "Rangs groupes", - action: function (e, dt, node, config) { - let rangs_visible = dt.columns(".partition_rangs").visible()[0]; - dt.columns(".partition_rangs").visible(!rangs_visible); - dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes"); - } - }, ]; + // Bouton "rangs groupes", sauf pour table jury BUT + if (!$('table.table_recap').hasClass("table_jury_but")) { + buttons.push( + { + name: "toggle_partitions_rangs", + text: "Rangs groupes", + action: function (e, dt, node, config) { + let rangs_visible = dt.columns(".partition_rangs").visible()[0]; + dt.columns(".partition_rangs").visible(!rangs_visible); + dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes"); + } + }); + } + if (!$('table.table_recap').hasClass("jury")) { buttons.push( $('table.table_recap').hasClass("apc") ? @@ -80,15 +85,18 @@ $(function () { } }) } - buttons.push({ - name: "toggle_admission", - text: "Montrer infos admission", - action: function (e, dt, node, config) { - let visible = dt.columns(".admission").visible()[0]; - dt.columns(".admission").visible(!visible); - dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission"); - } - }) + // Boutons admission, sauf pour table jury BUT + if (!$('table.table_recap').hasClass("table_jury_but")) { + buttons.push({ + name: "toggle_admission", + text: "Montrer infos admission", + action: function (e, dt, node, config) { + let visible = dt.columns(".admission").visible()[0]; + dt.columns(".admission").visible(!visible); + dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission"); + } + }); + } $('table.table_recap').DataTable( { paging: false, From 8a83e416986671eef0580441aceb90d94acc81ff Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 12:39:54 +0200 Subject: [PATCH 054/140] =?UTF-8?q?BUT:=20pond=C3=A9ration=20des=20UE=20da?= =?UTF-8?q?ns=20les=20RCUEs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/but_validations.py | 6 ++- app/models/ues.py | 3 ++ app/scodoc/sco_edit_ue.py | 43 ++++++++++++++----- migrations/versions/c0c225192d61_coef_rcue.py | 28 ++++++++++++ 4 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 migrations/versions/c0c225192d61_coef_rcue.py diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 27aee4299c..651e6fc2d4 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -120,8 +120,10 @@ class RegroupementCoherentUE: self.moy_ue_2_val = 0.0 # Calcul de la moyenne au RCUE if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None): - # Moyenne RCUE non pondérée (pour le moment -- TODO) - self.moy_rcue = (self.moy_ue_1 + self.moy_ue_2) / 2 + # Moyenne RCUE (les pondérations par défaut sont 1.) + self.moy_rcue = ( + self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue + ) / (ue_1.coef_rcue + ue_2.coef_rcue) else: self.moy_rcue = None diff --git a/app/models/ues.py b/app/models/ues.py index aa87e7ee2e..52cd3788ce 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -40,6 +40,9 @@ class UniteEns(db.Model): # coef UE, utilise seulement si l'option use_ue_coefs est activée: coefficient = db.Column(db.Float) + # coef. pour le calcul de moyennes de RCUE. Par défaut, 1. + coef_rcue = db.Column(db.Float, nullable=False, default=1.0, server_default="1.0") + color = db.Column(db.Text()) # BUT diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index fd40ac2b85..e8d8ba02bf 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -78,6 +78,7 @@ _ueEditor = ndb.EditableTable( "is_external", "code_apogee", "coefficient", + "coef_rcue", "color", ), sortkey="numero", @@ -252,6 +253,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No initvalues = { "semestre_idx": default_semestre_idx, "color": ue_guess_color_default(formation_id, default_semestre_idx), + "coef_rcue": 1.0, } submitlabel = "Créer cette UE" can_change_semestre_id = True @@ -341,22 +343,43 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "allow_null": not is_apc, # ects requis en APC }, ), - ( - "coefficient", - { - "size": 4, - "type": "float", - "title": "Coefficient", - "explanation": """les coefficients d'UE ne sont utilisés que + ] + if is_apc: # coef pour la moyenne RCUE + form_descr.append( + ( + "coef_rcue", + { + "size": 4, + "type": "float", + "title": "Coef. RCUE", + "explanation": """pondération utilisée pour le calcul de la moyenne du RCUE. Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations. + """, + "defaut": 1.0, + "allow_null": False, + "enabled": is_apc, + }, + ) + ) + else: # non APC, coef d'UE + form_descr.append( + ( + "coefficient", + { + "size": 4, + "type": "float", + "title": "Coefficient", + "explanation": """les coefficients d'UE ne sont utilisés que lorsque l'option Utiliser les coefficients d'UE pour calculer la moyenne générale est activée. Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules dans lesquels l'étudiant a des notes. Jamais utilisé en BUT. """, - "enabled": not is_apc, - }, - ), + "enabled": not is_apc, + }, + ) + ) + form_descr += [ ( "ue_code", { diff --git a/migrations/versions/c0c225192d61_coef_rcue.py b/migrations/versions/c0c225192d61_coef_rcue.py new file mode 100644 index 0000000000..5a3166e842 --- /dev/null +++ b/migrations/versions/c0c225192d61_coef_rcue.py @@ -0,0 +1,28 @@ +"""coef_rcue + +Revision ID: c0c225192d61 +Revises: 4311cc342dbd +Create Date: 2022-06-24 12:19:58.723862 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c0c225192d61' +down_revision = '4311cc342dbd' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notes_ue', sa.Column('coef_rcue', sa.Float(), server_default='1.0', nullable=False)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('notes_ue', 'coef_rcue') + # ### end Alembic commands ### From 441200dd5be8732e11cf4b6df929eea1dcca5400 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 13:40:05 +0200 Subject: [PATCH 055/140] Jury BUT: documentation des codes --- app/static/css/jury_but.css | 48 +++++ .../but/documentation_codes_jury.html | 189 ++++++++++++++++++ app/views/notes.py | 2 + 3 files changed, 239 insertions(+) create mode 100644 app/templates/but/documentation_codes_jury.html diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css index f3266bc63e..e933b6fd5e 100644 --- a/app/static/css/jury_but.css +++ b/app/static/css/jury_but.css @@ -108,4 +108,52 @@ div.but_buttons { div.but_buttons span { margin-right: 16px; +} + +div.but_doc_codes { + margin: 16px; + background-color: rgb(227, 254, 254); + font-size: 75%; + border: 2px solid rgb(4, 4, 118); + border-radius: 4px; + padding-left: 16px; + padding-right: 16px; + padding-bottom: 16px; +} + +div.but_doc_section { + margin-top: 16px; + font-size: 125%; + font-weight: bold; + margin-bottom: 8px; +} + +div.but_doc table { + border-collapse: collapse; + font-family: Tahoma, Geneva, sans-serif; +} + +div.but_doc table td { + padding: 7px; +} + +div.but_doc table thead td { + background-color: #54585d; + color: #ffffff; + font-weight: bold; + font-size: 13px; + border: 1px solid #54585d; +} + +div.but_doc table tbody td { + color: #636363; + border: 1px solid #dddfe1; +} + +div.but_doc table tbody tr { + background-color: #f9fafb; +} + +div.but_doc table tbody tr:nth-child(odd) { + background-color: #ffffff; } \ No newline at end of file diff --git a/app/templates/but/documentation_codes_jury.html b/app/templates/but/documentation_codes_jury.html new file mode 100644 index 0000000000..df6aa32623 --- /dev/null +++ b/app/templates/but/documentation_codes_jury.html @@ -0,0 +1,189 @@ +
    +

    Ci-dessous la signification de chaque code est expliquée, + ainsi que la correspondance avec les codes préconisés par + l'AMUE pour Apogée dans un document informel qui a circulé début + 2022 (les éventuelles erreurs n'engagent personne). +

    +
    Codes d'année
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScoDocAMUE
    ADMAdmis
    ADJAdmis par décision jury
    PASDPASDNon admis, mais passage de droit
    PAS1NCIPAS1NCINon admis, mais passage par décision de jury (Passage en Année + Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8))< /td> +
    REDREDAjourné, mais autorisé à redoubler
    NARREONon admis, réorientation
    DEMDémission
    ABANABAN ABANdon constaté (sans lettre de démission)
    RATEn attente d’un rattrapage
    EXCLUEXCEXClusion, décision réservée à des décisions disciplinaires
    DEF (défaillance) Non évalué par manque assiduité
    ABLABLAnnée Blanche
    +
    + +
    Codes RCUE (niveaux de compétences annuels)
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ADM + VAL + Acquis +
    CMPAcquis par compensation annuelle
    ADJCODJAcquis par décision du jury
    AJAJAttente pour problème de moyenne
    RATEn attente d’un rattrapage
    DEFDéfaillant
    ABANNon évalué pour manque assiduité
    +
    + +
    Codes des Unités d'Enseignement (UE)
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ADM Admis
    ADJ Admis par décision jury
    PASD PASDNon admis, mais passage de droit
    PAS1NCIPAS1NCINon admis, mais passage par décision de jury + (Passage en Année Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8)) +
    REDREDAjourné, mais autorisé à redoubler
    NARREONon admis, réorientation
    DEMDémission
    ABANABANdon constaté (sans lettre de démission)
    RATEn attente d’un rattrapage
    EXCLUEXCEXClusion, décision réservée à des décisions disciplinaires
    DEFNon évalué par manque assiduité (défaillance)
    ABLABLAnnée Blanche
    +
    +
    \ No newline at end of file diff --git a/app/views/notes.py b/app/views/notes.py index 04fe1da350..199a323f25 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2349,6 +2349,8 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): ) H.append("") # but_annee + H.append(render_template("but/documentation_codes_jury.html")) + return "\n".join(H) + html_sco_header.sco_footer() From 7b1aec46e195fed05e09c87d23ac128d12056ac7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 15:24:33 +0200 Subject: [PATCH 056/140] =?UTF-8?q?Fix:=20enregistrement=20codes=20d=C3=A9?= =?UTF-8?q?cisions=20BUT=20+=20moyennes=20UEs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 429f96a736..8db372b1f2 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -493,6 +493,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): f"code annee {html.escape(code)} invalide pour formsemestre {html.escape(self.formsemestre)}" ) if code == self.code_valide: + self.recorded = True return # no change if self.validation: db.session.delete(self.validation) @@ -572,6 +573,7 @@ class DecisionsProposeesRCUE(DecisionsProposees): f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" ) if code == self.code_valide: + self.recorded = True return # no change parcours_id = self.parcour.id if self.parcour is not None else None if self.validation: @@ -679,6 +681,7 @@ class DecisionsProposeesUE(DecisionsProposees): f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" ) if code == self.code_valide: + self.recorded = True return # no change if self.validation: db.session.delete(self.validation) @@ -691,6 +694,7 @@ class DecisionsProposeesUE(DecisionsProposees): formsemestre_id=self.formsemestre.id, ue_id=self.ue.id, code=code, + moy_ue=self.moy_ue, ) Scolog.logdb( method="jury_but", From 5335e5cfaeb34e86ce05c5779eeb8f362c9ab92c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 15:24:51 +0200 Subject: [PATCH 057/140] version history --- app/templates/but/documentation_codes_jury.html | 3 ++- sco_version.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/templates/but/documentation_codes_jury.html b/app/templates/but/documentation_codes_jury.html index df6aa32623..0b38a972e4 100644 --- a/app/templates/but/documentation_codes_jury.html +++ b/app/templates/but/documentation_codes_jury.html @@ -31,7 +31,8 @@ PAS1NCI PAS1NCI Non admis, mais passage par décision de jury (Passage en Année - Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8))< /td> + Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8)) + RED diff --git a/sco_version.py b/sco_version.py index 242844ed37..08d97f4c8b 100644 --- a/sco_version.py +++ b/sco_version.py @@ -8,6 +8,12 @@ SCONAME = "ScoDoc" SCONEWS = """

    Année 2021

      +
    • ScoDoc 9.3
    • +
        +
      • Prise en charge des parcours BUT
      • +
      • Association des UE aux compétences du référentiel
      • +
      • Jury BUT1
      • +
    • ScoDoc 9.2:
      • Tableau récap. complet pour BUT et autres formations.
      • From be3d692202e3c4e7682122c0dbaf723eac69ed97 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 24 Jun 2022 16:57:44 +0200 Subject: [PATCH 058/140] =?UTF-8?q?Table=20jury=20BUT:=20affichage=20optio?= =?UTF-8?q?nnel=20des=20codes=20enregistr=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 20 +++++++++++++--- app/but/jury_but_recap.py | 39 ++++++++++++++++++++++++-------- app/scodoc/sco_codes_parcours.py | 2 +- app/static/js/table_recap.js | 14 +++++++++++- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 8db372b1f2..391f77ed1e 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -246,6 +246,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): "liste des niveaux de compétences associés à cette année" self.decisions_rcue_by_niveau = self.compute_decisions_niveaux() "les décisions rcue associées aux niveau_id" + self.dec_rcue_by_ue = self._dec_rcue_by_ue() + "{ ue_id : DecisionsProposeesRCUE }" self.nb_competences = len(self.niveaux_competences) "le nombre de niveaux de compétences à valider cette année" self.nb_validables = len( @@ -357,6 +359,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): if res.etuds_parcour_id[etudid] is None: # pas de parcour: prend toutes les UEs (non bonus) ues = [ue for ue in res.etud_ues(etudid) if ue.type == UE_STANDARD] + ues.sort(key=lambda u: u.numero) else: parcour = ApcParcours.query.get(res.etuds_parcour_id[etudid]) if parcour is not None: @@ -364,6 +367,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): ues = ( formation.query_ues_parcour(parcour) .filter_by(semestre_idx=formsemestre.semestre_id) + .order_by(UniteEns.numero) .all() ) ues_sems.append(ues) @@ -418,9 +422,10 @@ class DecisionsProposeesAnnee(DecisionsProposees): return rcues_annee def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]: - """Pour chaque niveau de compétence de cette année, donne le DecisionsProposeesRCUE - ou None s'il n'y en a pas (ne devrait pas arriver car - compute_rcues_annee vérifie déjà cela). + """Pour chaque niveau de compétence de cette année, construit + le DecisionsProposeesRCUE, + ou None s'il n'y en a pas + (ne devrait pas arriver car compute_rcues_annee vérifie déjà cela). Return: { niveau_id : DecisionsProposeesRCUE } """ # Retrouve le RCUE associé à chaque niveau @@ -442,6 +447,15 @@ class DecisionsProposeesAnnee(DecisionsProposees): decisions_rcue_by_niveau = {x[1]: x[0] for x in rc_niveaux} return decisions_rcue_by_niveau + def _dec_rcue_by_ue(self) -> dict[int, "DecisionsProposeesRCUE"]: + """construit dict { ue_id : DecisionsProposeesRCUE } + à partir de self.decisions_rcue_by_niveau""" + d = {} + for dec_rcue in self.decisions_rcue_by_niveau.values(): + d[dec_rcue.rcue.ue_1.id] = dec_rcue + d[dec_rcue.rcue.ue_2.id] = dec_rcue + return d + # def lookup_ue(self, ue_id: int) -> UniteEns: # "check that ue_id belongs to our UE, if not returns None" # ues = [ue for ue in self.ues_impair + self.ues_pair if ue.id == ue_id] diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 47f826083f..b9ed57a304 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -11,7 +11,11 @@ import time from flask import g, url_for from app.but import jury_but -from app.but.jury_but import DecisionsProposeesAnnee +from app.but.jury_but import ( + DecisionsProposeesAnnee, + DecisionsProposeesRCUE, + DecisionsProposeesUE, +) from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem from app.models.but_validations import RegroupementCoherentUE @@ -179,10 +183,11 @@ class RowCollector: self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"] self.last_etud_cell_idx = self.idx - def add_ue_cell(self, ue: UniteEns, val): + def add_ue_cells(self, dec_ue: DecisionsProposeesUE): "cell de moyenne d'UE" - col_id = f"moy_ue_{ue.id}" + col_id = f"moy_ue_{dec_ue.ue.id}" note_class = "" + val = dec_ue.moy_ue if isinstance(val, float): if val < BUT_BARRE_UE: note_class = " moy_inf" @@ -190,10 +195,19 @@ class RowCollector: note_class = " moy_ue_valid" if val < BUT_BARRE_UE8: note_class = " moy_ue_warning" # notes très basses - self.add_cell(col_id, ue.acronyme, self.fmt_note(val), "col_ue" + note_class) + self.add_cell( + col_id, dec_ue.ue.acronyme, self.fmt_note(val), "col_ue" + note_class + ) + self.add_cell( + col_id + "_code", + dec_ue.ue.acronyme, + dec_ue.code_valide or "", + "col_ue_code recorded_code", + ) - def add_rcue_cell(self, rcue: RegroupementCoherentUE): - "cell de moyenne d'UE" + def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE): + "2 cells: moyenne du RCUE, code enregistré" + rcue = dec_rcue.rcue col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id note_class = "" val = rcue.moy_rcue @@ -210,6 +224,12 @@ class RowCollector: self.fmt_note(val), "col_rcue" + note_class, ) + self.add_cell( + col_id + "_code", + f"
        {rcue.ue_1.acronyme}
        {rcue.ue_2.acronyme}
        ", + dec_rcue.code_valide or "", + "col_rcue_code recorded_code", + ) def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee): "cell avec nb niveaux validables / total" @@ -259,9 +279,10 @@ def get_table_jury_but( row.add_nb_rcues_cell(deca) # --- Les RCUEs for rcue in deca.rcues_annee: - row.add_ue_cell(rcue.ue_1, rcue.moy_ue_1) - row.add_ue_cell(rcue.ue_2, rcue.moy_ue_2) - row.add_rcue_cell(rcue) + dec_rcue = deca.dec_rcue_by_ue[rcue.ue_1.id] + row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id]) + row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id]) + row.add_rcue_cells(dec_rcue) # --- Le code annuel existant row.add_cell( "code_annee", diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index cfc4401597..d4479939b5 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -188,7 +188,7 @@ CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente CODES_SEM_REO = {NAR: 1} # reorientation -CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée +CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée # Pour le BUT: CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 0c87ecdaaf..18829d8edb 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -35,6 +35,18 @@ $(function () { dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes"); } }); + } else { + // table jury BUT: avec ou sans codes enregistrés + buttons.push( + { + name: "toggle_recorded_code", + text: "Code jury enregistrés", + action: function (e, dt, node, config) { + let visible = dt.columns(".recorded_code").visible()[0]; + dt.columns(".recorded_code").visible(!visible); + dt.buttons('toggle_recorded_code:name').text(visible ? "Code jury enregistrés" : "Cacher codes jury"); + } + }); } if (!$('table.table_recap').hasClass("jury")) { @@ -113,7 +125,7 @@ $(function () { "columnDefs": [ { // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides - targets: ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"], + targets: ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty", "recorded_code"], visible: false, }, { From d364d3017675ebd07a7455cb14b59ae361dbaac2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 02:59:43 +0200 Subject: [PATCH 059/140] =?UTF-8?q?Jury=20BUT:=20calcul=20auto=20des=20d?= =?UTF-8?q?=C3=A9cisions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/forms/jury_but_forms.py | 18 +++++++ app/but/jury_but.py | 5 +- app/but/jury_but_recap.py | 19 ++++++-- app/but/jury_but_validation_auto.py | 34 ++++++++++++++ app/static/css/scodoc.css | 5 ++ .../but/formsemestre_validation_auto_but.html | 30 ++++++++++++ app/views/notes.py | 47 +++++++++++++++++-- 7 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 app/but/forms/jury_but_forms.py create mode 100644 app/but/jury_but_validation_auto.py create mode 100644 app/templates/but/formsemestre_validation_auto_but.html diff --git a/app/but/forms/jury_but_forms.py b/app/but/forms/jury_but_forms.py new file mode 100644 index 0000000000..0f37196003 --- /dev/null +++ b/app/but/forms/jury_but_forms.py @@ -0,0 +1,18 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""ScoDoc 9.3 : Formulaires / jurys BUT +""" + + +from flask_wtf import FlaskForm +from wtforms import SubmitField + + +class FormSemestreValidationAutoBUTForm(FlaskForm): + "simple form de confirmation" + submit = SubmitField("Lancer le calcul") + cancel = SubmitField("Annuler") diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 391f77ed1e..8ab3a213ff 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -259,7 +259,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) "le nb de comp. sous la barre de 8/20" # année ADM si toutes RCUE validées (sinon PASD) - admis = self.nb_validables == self.nb_competences + self.admis = self.nb_validables == self.nb_competences + "vrai si l'année est réussie, tous niveaux validables" self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2) # Peut passer si plus de la moitié validables et tous > 8 self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0) @@ -273,7 +274,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): expl_rcues = ( f"{self.nb_validables} niveau validable(s) sur {self.nb_competences}" ) - if admis: + if self.admis: self.codes = [sco_codes.ADM] + self.codes self.explanation = expl_rcues elif self.passage_de_droit: diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index b9ed57a304..38e1fd68d8 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -18,10 +18,8 @@ from app.but.jury_but import ( ) from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem -from app.models.but_validations import RegroupementCoherentUE from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre -from app.models.ues import UniteEns from app.scodoc.sco_codes_parcours import ( BUT_BARRE_RCUE, @@ -79,8 +77,21 @@ def formsemestre_saisie_jury_but( formsemestre_id=formsemestre2.id ), ] - H.append(table_html) - H.append(html_sco_header.sco_footer()) + H.append( + f""" + + {table_html} + + + + {html_sco_header.sco_footer()} + """ + ) return "\n".join(H) diff --git a/app/but/jury_but_validation_auto.py b/app/but/jury_but_validation_auto.py new file mode 100644 index 0000000000..99512168bf --- /dev/null +++ b/app/but/jury_but_validation_auto.py @@ -0,0 +1,34 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury BUT: clacul des décisions de jury annuelles "automatiques" +""" + +from flask import g, url_for + +from app import db +from app.but import jury_but +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre +from app.scodoc.sco_exceptions import ScoValueError + + +def formsemestre_validation_auto_but(formsemestre: FormSemestre) -> int: + """Calcul automatique des décisions de jury sur une année BUT. + Returns: nombre d'étudiants "admis" + """ + if not formsemestre.formation.is_apc(): + raise ScoValueError("fonction réservée aux formations BUT") + nb_admis = 0 + 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 + deca.record_all() + nb_admis += 1 + + db.session.commit() + return nb_admis diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 6b41b761b1..961d789bc4 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -3963,6 +3963,11 @@ table.table_recap td.evaluation.non_inscrit { color: rgb(101, 101, 101); } +div.table_jury_but_links { + margin-top: 16px; + margin-bottom: 16px; +} + /* ------------- Tableau etat evals ------------ */ div.evaluations_recap table.evaluations_recap { diff --git a/app/templates/but/formsemestre_validation_auto_but.html b/app/templates/but/formsemestre_validation_auto_but.html new file mode 100644 index 0000000000..c9397fc0d1 --- /dev/null +++ b/app/templates/but/formsemestre_validation_auto_but.html @@ -0,0 +1,30 @@ +{# -*- mode: jinja-html -*- #} +{% extends "sco_page.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block styles %} +{{super()}} +{% endblock %} + +{% block app_content %} + +

        Calcul automatique des décisions de jury annuelle BUT

        +
          +
        • Seuls les étudiants qui valident l'année seront affectés: + tous les niveaux de compétences (RCUE) validables + (moyenne annuelle au dessus de 10); +
        • +
        • l'assiduité n'est pas prise en compte;
        • +
        +

        + Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure ! +

        + + +
        +
        + {{ wtf.quick_form(form) }} +
        +
        + +{% endblock %} \ No newline at end of file diff --git a/app/views/notes.py b/app/views/notes.py index 199a323f25..f00758b606 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -41,7 +41,8 @@ from flask import abort, flash, jsonify, redirect, render_template, url_for from flask import current_app, g, request from flask_login import current_user -from app.but import jury_but +from app.but import jury_but, jury_but_validation_auto +from app.but.forms import jury_but_forms from app.comp import res_sem from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_compat import NotesTableCompat @@ -2224,7 +2225,7 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): "Form. saisie décision jury semestre BUT" if not sco_permissions_check.can_validate_sem(formsemestre_id): return scu.confirm_dialog( - message="

        Opération non autorisée pour %s" % current_user, + message=f"

        Opération non autorisée pour {current_user}", dest_url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, @@ -2274,7 +2275,8 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):

        Décision de jury pour l'année : { - _gen_but_select("code_annee", deca.codes, deca.code_valide, disabled=True, klass="manual") + _gen_but_select("code_annee", deca.codes, deca.code_valide, + disabled=True, klass="manual") } ({'non ' if deca.code_valide is None else ''}enregistrée)
        @@ -2396,6 +2398,45 @@ def _gen_but_niveau_ue(
        """ +@bp.route( + "/formsemestre_validation_auto_but/", methods=["GET", "POST"] +) +@scodoc +@permission_required(Permission.ScoView) +def formsemestre_validation_auto_but(formsemestre_id: int = None): + "Saisie automatique des décisions de jury BUT" + if not sco_permissions_check.can_validate_sem(formsemestre_id): + return scu.confirm_dialog( + message=f"

        Opération non autorisée pour {current_user}", + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ), + ) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + form = jury_but_forms.FormSemestreValidationAutoBUTForm() + if request.method == "POST": + if not form.cancel.data: + nb_admis = jury_but_validation_auto.formsemestre_validation_auto_but( + formsemestre + ) + flash(f"Décisions enregistrées ({nb_admis} admis)") + return redirect( + url_for( + "notes.formsemestre_saisie_jury", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + return render_template( + "but/formsemestre_validation_auto_but.html", + form=form, + sco=ScoData(formsemestre=formsemestre), + title=f"Calcul automatique jury BUT", + ) + + @bp.route("/formsemestre_validate_previous_ue", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoView) From f9b50bb290eb1d898fcb2b8ea73214c3d7271f37 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 03:52:28 +0200 Subject: [PATCH 060/140] =?UTF-8?q?Jury=20BUT:=20g=C3=A9n=C3=A9ration=20de?= =?UTF-8?q?s=20autorisations=20d'inscription?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 42 +++++++++++++++++++++++++------- app/models/validations.py | 41 ++++++++++++++++++++++++++++++- app/scodoc/sco_codes_parcours.py | 6 +++++ 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 8ab3a213ff..fb255f0d22 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -67,6 +67,7 @@ from app import db from app import log from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem +from app.models import formsemestre from app.models.but_refcomp import ( ApcAnneeParcours, @@ -75,7 +76,7 @@ from app.models.but_refcomp import ( ApcParcours, ApcParcoursNiveauCompetence, ) -from app.models import Scolog +from app.models import Scolog, ScolarAutorisationInscription from app.models.but_validations import ( ApcValidationAnnee, ApcValidationRCUE, @@ -87,7 +88,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation from app.scodoc import sco_codes_parcours as sco_codes -from app.scodoc.sco_codes_parcours import UE_STANDARD +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 @@ -457,13 +458,19 @@ class DecisionsProposeesAnnee(DecisionsProposees): d[dec_rcue.rcue.ue_2.id] = dec_rcue return d - # def lookup_ue(self, ue_id: int) -> UniteEns: - # "check that ue_id belongs to our UE, if not returns None" - # ues = [ue for ue in self.ues_impair + self.ues_pair if ue.id == ue_id] - # assert len(ues) < 2 - # if len(ues): - # return ues[0] - # return None + 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 + and self.formsemestre_pair.semestre_id < sco_codes.ParcoursBUT.NB_SEM + ): + return self.formsemestre_pair.semestre_id + 1 + return None def record_form(self, form: dict): """Enregistre les codes de jury en base @@ -529,6 +536,23 @@ class DecisionsProposeesAnnee(DecisionsProposees): 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 def record_all(self): diff --git a/app/models/validations.py b/app/models/validations.py index 00d170729f..42d7ba0d65 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -6,6 +6,7 @@ from app import db from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN +from app.models.events import Scolog class ScolarFormSemestreValidation(db.Model): @@ -69,7 +70,7 @@ class ScolarAutorisationInscription(db.Model): db.ForeignKey("identite.id", ondelete="CASCADE"), ) formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False) - # semestre ou on peut s'inscrire: + # Indice du semestre où on peut s'inscrire: semestre_id = db.Column(db.Integer) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) origin_formsemestre_id = db.Column( @@ -77,6 +78,44 @@ class ScolarAutorisationInscription(db.Model): db.ForeignKey("notes_formsemestre.id"), ) + @classmethod + def autorise_etud( + cls, + etudid: int, + formation_code: str, + origin_formsemestre_id: int, + semestre_id: int, + ): + """Enregistre une autorisation, remplace celle émanant du même semestre si elle existe.""" + cls.delete_autorisation_etud(etudid, origin_formsemestre_id) + autorisation = cls( + etudid=etudid, + formation_code=formation_code, + origin_formsemestre_id=origin_formsemestre_id, + semestre_id=semestre_id, + ) + db.session.add(autorisation) + Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}") + + @classmethod + def delete_autorisation_etud( + cls, + etudid: int, + origin_formsemestre_id: int, + ): + """Efface les autorisations de cette étudiant venant du sem. origine""" + autorisations = cls.query.filter_by( + etudid=etudid, origin_formsemestre_id=origin_formsemestre_id + ) + for autorisation in autorisations: + db.session.delete(autorisation) + Scolog.logdb( + "autorise_etud", + etudid=etudid, + msg=f"annule passage vers S{autorisation.semestre_id}", + ) + db.session.flush() + class ScolarEvent(db.Model): """Evenement dans le parcours scolaire d'un étudiant""" diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index d4479939b5..6b4dd946d9 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -196,6 +196,12 @@ 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 +BUT_CODES_PASSAGE = { + ADM, + ADJ, + PASD, + PAS1NCI, +} def code_semestre_validant(code: str) -> bool: From ca5fd33e4eae2404821b1a1fc605b19b4842d3df Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 07:41:37 +0200 Subject: [PATCH 061/140] Fix: pd PDF si prefs. vides --- app/scodoc/sco_pvpdf.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py index 3ac0096d63..b2515895c7 100644 --- a/app/scodoc/sco_pvpdf.py +++ b/app/scodoc/sco_pvpdf.py @@ -446,8 +446,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): else: params["decisions_ue_descr_plural"] = "" - params["INSTITUTION_CITY"] = sco_preferences.get_preference( - "INSTITUTION_CITY", formsemestre_id + params["INSTITUTION_CITY"] = ( + sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or "" ) if decision["prev_decision_sem"]: params["prev_semestre_id"] = decision["prev"]["semestre_id"] @@ -528,8 +528,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): sco_preferences.get_preference( "PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id ) - % params - ) + or "" + ) % params sig = _simulate_br(sig, '') objects += sco_pdf.makeParas( ( @@ -545,8 +545,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None): sco_preferences.get_preference( "PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id ) - % params - ) + or "" + ) % params sig = _simulate_br(sig, '') objects += sco_pdf.makeParas( ( @@ -731,7 +731,7 @@ def _pvjury_pdf_type( """ % ( titre_jury, - sco_preferences.get_preference("DeptName", formsemestre_id), + sco_preferences.get_preference("DeptName", formsemestre_id) or "(sans nom)", sem["anneescolaire"], ), style, @@ -761,7 +761,7 @@ def _pvjury_pdf_type( objects += sco_pdf.makeParas( "" - + sco_preferences.get_preference("PV_INTRO", formsemestre_id) + + (sco_preferences.get_preference("PV_INTRO", formsemestre_id) or "") % { "Decnum": numeroArrete, "VDICode": VDICode, From d776bdca660e3a9ba2443e51c67600d9dd8c2a06 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 08:22:08 +0200 Subject: [PATCH 062/140] Fix: affichage coef bonus. Closes #382 --- app/static/js/table_editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/js/table_editor.js b/app/static/js/table_editor.js index cfd60d1e16..2b6af16e09 100644 --- a/app/static/js/table_editor.js +++ b/app/static/js/table_editor.js @@ -35,7 +35,7 @@ function build_table(data) { ${cellule.data}

    `; - if (cellule.editable) { + if (cellule.style.includes("champs")) { sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + (parseFloat(cellule.data) || 0); sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + (parseFloat(cellule.data) || 0); } From cc4fd761874022ec27efa4c06906e301e320a639 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 12:32:00 +0200 Subject: [PATCH 063/140] Tableau recap. codes jury BUT --- app/but/jury_but_recap.py | 58 +++++++++++++++++++++++++++++------- app/static/js/table_recap.js | 21 +++++++++---- app/views/notes.py | 17 +++++++++++ 3 files changed, 81 insertions(+), 15 deletions(-) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index 38e1fd68d8..a0a74b3134 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -34,12 +34,17 @@ from app.scodoc.sco_exceptions import ScoValueError def formsemestre_saisie_jury_but( - formsemestre2: FormSemestre, readonly: bool = False, selected_etudid: int = None + formsemestre2: FormSemestre, + readonly: bool = False, + selected_etudid: int = None, + mode="jury", ) -> str: """formsemestre est un semestre PAIR Si readonly, ne montre pas le lien "saisir la décision" => page html complète + + Si mode == "recap", table recap des codes, sans liens de saisie. """ # Quick & Dirty # pour chaque etud de res2 trié @@ -55,7 +60,9 @@ def formsemestre_saisie_jury_but( if formsemestre2.semestre_id % 2 != 0: raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs") - rows, titles, column_ids = get_table_jury_but(formsemestre2, readonly=readonly) + rows, titles, column_ids = get_table_jury_but( + formsemestre2, readonly=readonly, mode=mode + ) if not rows: return ( '
    aucun étudiant !
    ' @@ -63,8 +70,9 @@ def formsemestre_saisie_jury_but( filename = scu.sanitize_filename( f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}""" ) + klass = "table_jury_but_bilan" if mode == "recap" else "" table_html = build_table_jury_but_html( - filename, rows, titles, column_ids, selected_etudid=selected_etudid + filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass ) H = [ html_sco_header.sco_header( @@ -77,16 +85,45 @@ def formsemestre_saisie_jury_but( formsemestre_id=formsemestre2.id ), ] + if mode == "recap": + H.append( + """

    Décisions de jury enregistrées pour les étudiants de ce semestre

    """ + ) H.append( f""" {table_html} {html_sco_header.sco_footer()} @@ -96,12 +133,12 @@ def formsemestre_saisie_jury_but( def build_table_jury_but_html( - filename: str, rows, titles, column_ids, selected_etudid: int = None + filename: str, rows, titles, column_ids, selected_etudid: int = None, klass="" ) -> str: """assemble la table html""" - footer_rows = [] # inutile pour l'instant, à voir XXX + footer_rows = [] # inutilisé pour l'instant H = [ - f"""
    """ ] # header @@ -274,7 +311,7 @@ class RowCollector: def get_table_jury_but( - formsemestre2: FormSemestre, readonly: bool = False + formsemestre2: FormSemestre, readonly: bool = False, mode="jury" ) -> tuple[list[dict], list[str], list[str]]: """Construit la table des résultats annuels pour le jury BUT""" res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2) @@ -302,7 +339,7 @@ def get_table_jury_but( "col_code_annee", ) # --- Le lien de saisie - if not readonly: + if not readonly and not mode == "recap": row.add_cell( "lien_saisie", "", @@ -316,6 +353,7 @@ def get_table_jury_but( {"modif." if deca.code_valide else "saisie"} décision """, + "col_lien_saisie_but", ) rows.append(row.row) if len(rows) > 0: diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 18829d8edb..8d52644124 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -1,6 +1,14 @@ // Tableau recap notes $(function () { $(function () { + let hidden_colums = ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"]; + let mode_jury_but_bilan = $('table.table_recap').hasClass("table_jury_but_bilan"); + if (mode_jury_but_bilan) { + // table bilan décisions: cache les notes + hidden_colums = hidden_colums.concat(["col_ue", "col_rcue", "col_lien_saisie_but"]); + } else { + hidden_colums = hidden_colums.concat(["recorded_code"]); + } // Les boutons dépendent du mode BUT ou classique: let buttons = [ { @@ -125,7 +133,7 @@ $(function () { "columnDefs": [ { // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides - targets: ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty", "recorded_code"], + targets: hidden_colums, visible: false, }, { @@ -175,10 +183,13 @@ $(function () { $(this).addClass('selected'); } }); - // Pour montrer et highlihter l'étudiant sélectionné: + // Pour montrer et surligner l'étudiant sélectionné: $(function () { - document.querySelector("#row_selected").scrollIntoView(); - window.scrollBy(0, -50); - document.querySelector("#row_selected").classList.add("selected"); + let row_selected = document.querySelector("#row_selected"); + if (row_selected) { + row_selected.scrollIntoView(); + window.scrollBy(0, -50); + row_selected.classList.add("selected"); + } }); }); diff --git a/app/views/notes.py b/app/views/notes.py index f00758b606..97aa43bafe 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2613,6 +2613,23 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None): ) +@bp.route("/formsemestre_jury_but_recap") +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_jury_but_recap(formsemestre_id: int, selected_etudid: int = None): + """Tableau affichage des codes""" + readonly = not sco_permissions_check.can_validate_sem(formsemestre_id) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not (formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0): + raise ScoValueError( + "formsemestre_jury_but_recap: réservé aux semestres pairs de BUT" + ) + return jury_but_recap.formsemestre_saisie_jury_but( + formsemestre, readonly=readonly, selected_etudid=selected_etudid, mode="recap" + ) + + sco_publish( "/formsemestre_lettres_individuelles", sco_pvjury.formsemestre_lettres_individuelles, From b5138d3dfe39486f8aa5150d3c52409ebd97ad1f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 14:18:34 +0200 Subject: [PATCH 064/140] =?UTF-8?q?Suppression=20d=C3=A9cisions=20de=20jur?= =?UTF-8?q?y=20BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 44 +++++++++++++++++++++++++++++++ app/scodoc/sco_pvjury.py | 4 +-- app/templates/confirm_dialog.html | 22 ++++++++++++++++ app/views/notes.py | 43 ++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 app/templates/confirm_dialog.html diff --git a/app/but/jury_but.py b/app/but/jury_but.py index fb255f0d22..840933c087 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -571,6 +571,32 @@ class DecisionsProposeesAnnee(DecisionsProposees): # s'il n'y a pas de codee, efface dec.record(dec.codes[0]) + def erase(self): + """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. + """ + for dec_ue in self.decisions_ues.values(): + dec_ue.erase() + for dec_rcue in self.decisions_rcue_by_niveau.values(): + dec_rcue.erase() + if self.formsemestre_impair: + ScolarAutorisationInscription.delete_autorisation_etud( + self.etud.id, self.formsemestre_impair.id + ) + if self.formsemestre_pair: + ScolarAutorisationInscription.delete_autorisation_etud( + self.etud.id, self.formsemestre_pair.id + ) + validations = ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + formsemestre_id=self.formsemestre_impair.id, + ordre=self.annee_but, + ) + for validation in validations: + db.session.delete(validation) + db.session.flush() + class DecisionsProposeesRCUE(DecisionsProposees): """Liste des codes de décisions que l'on peut proposer pour @@ -637,6 +663,14 @@ class DecisionsProposeesRCUE(DecisionsProposees): db.session.add(self.validation) self.recorded = 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: + db.session.delete(validation) + db.session.flush() + class DecisionsProposeesUE(DecisionsProposees): """Décisions de jury sur une UE du BUT @@ -743,6 +777,16 @@ class DecisionsProposeesUE(DecisionsProposees): db.session.add(self.validation) self.recorded = True + def erase(self): + """Efface la décision de jury de cet étudiant pour cette UE""" + # par prudence, on requete toutes les validations, en cas de doublons + validations = ScolarFormSemestreValidation.query.filter_by( + etudid=self.etud.id, formsemestre_id=self.formsemestre.id, ue_id=self.ue.id + ) + for validation in validations: + db.session.delete(validation) + db.session.flush() + class BUTCursusEtud: # WIP TODO """Validation du cursus d'un étudiant""" diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index cfa7bae615..7ddc0aa1d9 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -492,9 +492,7 @@ def pvjury_table( def formsemestre_pvjury(formsemestre_id, format="html", publish=True): - """Page récapitulant les décisions de jury - dpv: result of dict_pvjury - """ + """Page récapitulant les décisions de jury""" footer = html_sco_header.sco_footer() dpv = dict_pvjury(formsemestre_id, with_prev=True) diff --git a/app/templates/confirm_dialog.html b/app/templates/confirm_dialog.html new file mode 100644 index 0000000000..8067f6c74b --- /dev/null +++ b/app/templates/confirm_dialog.html @@ -0,0 +1,22 @@ +{# -*- mode: jinja-html -*- #} +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} + +

    {{ title }}

    + +
    + {{ explanation }} +
    +
    +
    + + {% if cancel_url %} + + {% endif %} + +
    + +{% endblock %} \ No newline at end of file diff --git a/app/views/notes.py b/app/views/notes.py index 97aa43bafe..005e7963a6 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2262,6 +2262,13 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): etudid=etudid, ) ) + if deca.code_valide: + erase_span = f"""effacer décisions""" + else: + erase_span = "" H.append( f"""
    @@ -2279,6 +2286,7 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): disabled=True, klass="manual") } ({'non ' if deca.code_valide is None else ''}enregistrée) + {erase_span}
    {deca.explanation} @@ -2630,6 +2638,41 @@ def formsemestre_jury_but_recap(formsemestre_id: int, selected_etudid: int = Non ) +@bp.route( + "/formsemestre_jury_but_erase//", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.ScoView) +def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None): + """Supprime la décision de jury BUT pour cette année""" + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.formation.is_apc(): + raise ScoValueError("semestre non BUT") + etud: Identite = Identite.query.get_or_404(etudid) + if not sco_permissions_check.can_validate_sem(formsemestre_id): + raise ScoValueError("opération non autorisée") + dest_url = url_for( + "notes.formsemestre_validation_but", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etudid=etudid, + ) + if request.method == "POST": + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + deca.erase() + db.session.commit() + flash("décisions de jury effacées") + return redirect(dest_url) + + return render_template( + "confirm_dialog.html", + title=f"Effacer les validations de jury de {etud.nomprenom} ?", + explanation="""Les validations de toutes les UE, RCUE (compétences) et année seront effacées.""", + cancel_url=dest_url, + ) + + sco_publish( "/formsemestre_lettres_individuelles", sco_pvjury.formsemestre_lettres_individuelles, From 01d28eac90f24b88bfd288ff2fb6eb026868d9d3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 16:23:39 +0200 Subject: [PATCH 065/140] =?UTF-8?q?Table=20recap=20/=20jury:=20m=C3=A9mori?= =?UTF-8?q?se=20ordre=20de=20tri?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/table_recap.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/app/static/js/table_recap.js b/app/static/js/table_recap.js index 8d52644124..805153fdab 100644 --- a/app/static/js/table_recap.js +++ b/app/static/js/table_recap.js @@ -9,6 +9,22 @@ $(function () { } else { hidden_colums = hidden_colums.concat(["recorded_code"]); } + // Etat (tri des colonnes) de la table: + + const url = new URL(document.URL); + const formsemestre_id = url.searchParams.get("formsemestre_id"); + const order_info_key = JSON.stringify([url.pathname, formsemestre_id]); + let order_info; + if (formsemestre_id) { + const x = localStorage.getItem(order_info_key); + if (x) { + try { + order_info = JSON.parse(x); + } catch (error) { + console.error(error); + } + } + } // Les boutons dépendent du mode BUT ou classique: let buttons = [ { @@ -169,7 +185,15 @@ $(function () { autoClose: true, buttons: buttons, }, - ] + ], + "drawCallback": function (settings) { + // permet de conserver l'ordre de tri des colonnes + let order_info = JSON.stringify($('table.table_recap').DataTable().order()); + if (formsemestre_id) { + localStorage.setItem(order_info_key, order_info); + } + }, + "order": order_info, } ); From 1d3ccb565a58bbf5cc85066ae5c22c76e570e016 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 17:00:00 +0200 Subject: [PATCH 066/140] Fix: enregistrement code jury BUT --- app/but/jury_but.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 840933c087..543a29eef1 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -480,7 +480,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): - 'code_annee' : 'ADM' code pour l'année Si les code_rcue et le code_annee ne sont pas fournis, - enregistre ceux par défaut. + et qu'il n'y en a pas déjà, enregistre ceux par défaut. """ for key in form: code = form[key] @@ -508,13 +508,15 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.record_all() db.session.commit() - def record(self, code: str): - """Enregistre le code""" + def record(self, code: str, no_overwrite=False): + """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é. + """ 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: + if code == self.code_valide or (self.code_valide is not None and no_overwrite): self.recorded = True return # no change if self.validation: @@ -556,9 +558,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.recorded = True def record_all(self): - """Enregistre les codes qui n'ont pas été spécifiés par le formulaire, - et sont donc en mode "automatique" - """ + """Enregistre les codes qui n'ont pas été spécifiés par le formulaire, et sont donc en mode "automatique" """ decisions = ( list(self.decisions_ues.values()) + list(self.decisions_rcue_by_niveau.values()) @@ -568,8 +568,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): if not dec.recorded: # rappel: le code par défaut est en tête code = dec.codes[0] if dec.codes else None - # s'il n'y a pas de codee, efface - dec.record(dec.codes[0]) + # s'il n'y a pas de code, efface + dec.record(code, no_overwrite=True) def erase(self): """Efface les décisions de jury de cet étudiant @@ -631,13 +631,13 @@ class DecisionsProposeesRCUE(DecisionsProposees): else: self.codes.insert(0, sco_codes.AJ) - def record(self, code: str): + def record(self, code: str, no_overwrite=False): """Enregistre le code""" 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: + if code == self.code_valide or (self.code_valide is not None and no_overwrite): self.recorded = True return # no change parcours_id = self.parcour.id if self.parcour is not None else None @@ -747,13 +747,13 @@ class DecisionsProposeesUE(DecisionsProposees): self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes self.explanation = "notes insuffisantes" - def record(self, code: str): + def record(self, code: str, no_overwrite=False): """Enregistre le code""" 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: + if code == self.code_valide or (self.code_valide is not None and no_overwrite): self.recorded = True return # no change if self.validation: From c36a20c8b35f1200e6ab7d6e129fd5b9048f062d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 17:18:05 +0200 Subject: [PATCH 067/140] =?UTF-8?q?9.3.0=20|=20avec=20page=20d=C3=A9cision?= =?UTF-8?q?=20jury=20provisoire?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_pvjury.py | 15 ++++++++++++++- sco_version.py | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index 7ddc0aa1d9..fcc647b78d 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -52,7 +52,7 @@ from reportlab.platypus import Paragraph from reportlab.lib import styles import flask -from flask import url_for, g, request +from flask import url_for, g, redirect, request from app.comp import res_sem from app.comp.res_compat import NotesTableCompat @@ -493,6 +493,19 @@ def pvjury_table( def formsemestre_pvjury(formsemestre_id, format="html", publish=True): """Page récapitulant les décisions de jury""" + + # Bretelle provisoire pour BUT 9.3.0 + # XXX TODO + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0: + return redirect( + url_for( + "notes.formsemestre_jury_but_recap", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + # /XXX footer = html_sco_header.sco_footer() dpv = dict_pvjury(formsemestre_id, with_prev=True) diff --git a/sco_version.py b/sco_version.py index 08d97f4c8b..c0169257c8 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.3a" +SCOVERSION = "9.3.0b" SCONAME = "ScoDoc" From 29c2fb25e8ead08eefea8864863ad700c7453b55 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 22:54:46 +0200 Subject: [PATCH 068/140] =?UTF-8?q?Fix:=20message=20erreur=20jury=20BUT=20?= =?UTF-8?q?si=20pas=20de=20ref.=20de=20comp.=20associ=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but_recap.py | 10 ++++++++++ app/models/but_refcomp.py | 5 +++++ sco_version.py | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index a0a74b3134..dc30f4f3a9 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -60,6 +60,16 @@ def formsemestre_saisie_jury_but( if formsemestre2.semestre_id % 2 != 0: 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" + """ + ) + rows, titles, column_ids = get_table_jury_but( formsemestre2, readonly=readonly, mode=mode ) diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 5db968e3db..04fed31883 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -14,6 +14,7 @@ import sqlalchemy from app import db from app.scodoc.sco_utils import ModuleType +from app.scodoc.sco_exceptions import ScoValueError # from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns @@ -280,6 +281,10 @@ 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 !" + ) annee_formation = f"BUT{annee}" if parcour is None: return ApcNiveau.query.filter( diff --git a/sco_version.py b/sco_version.py index c0169257c8..d686c66dcb 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.3.0b" +SCOVERSION = "9.3.1" SCONAME = "ScoDoc" From 060b7ad7cdef89cfe1469d617b7943bfb04e071e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 23:22:20 +0200 Subject: [PATCH 069/140] Fix: Bulletin BUT: calcul des UE de chaque etud --- app/but/bulletin_but.py | 6 ++---- app/comp/res_but.py | 14 +++++++++++--- app/comp/res_common.py | 6 +++--- app/models/but_refcomp.py | 2 +- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 3c58157e03..279cf86e8e 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -244,7 +244,7 @@ class BulletinBUT: f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}" for ue in res.ues if ue.type != UE_SPORT - and res.modimpls_in_ue(ue.id, etudid) + and res.modimpls_in_ue(ue, etudid) and ue.id in res.bonus_ues and bonus_vect[ue.id] > 0.0 ] @@ -275,9 +275,7 @@ class BulletinBUT: nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT] published = (not formsemestre.bul_hide_xml) or force_publishing if formsemestre.formation.referentiel_competence is None: - etud_ues_ids = { - ue.id for ue in res.ues if res.modimpls_in_ue(ue.id, etud.id) - } + etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)} else: etud_ues_ids = res.etud_ues_ids(etud.id) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 10eef7c677..14cf63124f 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -157,16 +157,24 @@ class ResultatsSemestreBUT(NotesTableCompat): """ return self.modimpl_coefs_df.loc[ue.id].sum() - def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]: + def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]: """Liste des modimpl ayant des coefs non nuls vers cette UE et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant. """ # sert pour l'affichage ou non de l'UE sur le bulletin et la table recap - coefs = self.modimpl_coefs_df # row UE, cols modimpl + if ue.type == UE_SPORT: + return [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if modimpl.module.ue.id == ue.id + and self.modimpl_inscr_df[modimpl.id][etudid] + ] + coefs = self.modimpl_coefs_df # row UE (sans bonus), cols modimpl modimpls = [ modimpl for modimpl in self.formsemestre.modimpls_sorted - if (coefs[modimpl.id][ue_id] != 0) + if modimpl.module.ue.type != UE_SPORT + and (coefs[modimpl.id][ue.id] != 0) and self.modimpl_inscr_df[modimpl.id][etudid] ] if not with_bonus: diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 3b5f08964c..86230f06a2 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -187,7 +187,7 @@ class ResultatsSemestre(ResultatsCache): ues = sorted(list(ues), key=lambda x: x.numero or 0) return ues - def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]: + def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]: """Liste des modimpl de cette UE auxquels l'étudiant est inscrit. Utile en formations classiques, surchargée pour le BUT. Inclus modules bonus le cas échéant. @@ -197,7 +197,7 @@ class ResultatsSemestre(ResultatsCache): modimpls = [ modimpl for modimpl in self.formsemestre.modimpls_sorted - if modimpl.module.ue.id == ue_id + if modimpl.module.ue.id == ue.id and self.modimpl_inscr_df[modimpl.id][etudid] ] if not with_bonus: @@ -572,7 +572,7 @@ class ResultatsSemestre(ResultatsCache): # Les moyennes des modules (ou ressources et SAÉs) dans cette UE idx_malus = idx # place pour colonne malus à gauche des modules idx += 1 - for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False): + for modimpl in self.modimpls_in_ue(ue, etudid, with_bonus=False): if ue_status["is_capitalized"]: val = "-c-" else: diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 04fed31883..4e4b4b27f6 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -283,7 +283,7 @@ class ApcNiveau(db.Model, XMLModel): 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 !" + "Pas de référentiel de compétences associé à la formation !" ) annee_formation = f"BUT{annee}" if parcour is None: From 21460df51aacd1261fd242aa43a56bb1571c58d6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 25 Jun 2022 23:42:39 +0200 Subject: [PATCH 070/140] Fix: edition UE / ref. comp. --- app/models/but_refcomp.py | 2 +- app/scodoc/sco_pvjury.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 4e4b4b27f6..b1aea93e72 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -124,7 +124,7 @@ class ApcReferentielCompetences(db.Model, XMLModel): """ parcours = self.parcours.order_by(ApcParcours.numero).all() niveaux_by_parcours = { - parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee) + parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self) for parcour in parcours } # Cherche tronc commun diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index fcc647b78d..ecbe19f200 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -498,12 +498,10 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): # XXX TODO formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0: - return redirect( - url_for( - "notes.formsemestre_jury_but_recap", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) + from app.but import jury_but_recap + + return jury_but_recap.formsemestre_saisie_jury_but( + formsemestre, readonly=True, mode="recap" ) # /XXX footer = html_sco_header.sco_footer() From 4f7827f8c273e62bd34f63666818606e37e55a25 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 09:37:50 +0200 Subject: [PATCH 071/140] =?UTF-8?q?Jurys=20BUT:=20am=C3=A9liore=20message?= =?UTF-8?q?=20erreur=20si=20pas=20RCUE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 79 +++++++++++++++++++++++++++++++++------ app/models/formations.py | 4 ++ app/scodoc/sco_edit_ue.py | 4 +- 3 files changed, 72 insertions(+), 15 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 543a29eef1..b5dbdc5865 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -63,6 +63,8 @@ from operator import attrgetter import re from typing import Union +from flask import g, url_for + from app import db from app import log from app.comp.res_but import ResultatsSemestreBUT @@ -93,6 +95,32 @@ from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoException, ScoValueError +class NoRCUEError(ScoValueError): + """Erreur en cas de RCUE manquant""" + + def __init__(self, deca: "DecisionsProposeesAnnee", ue: UniteEns): + if all(u.niveau_competence for u in deca.ues_pair): + warning_pair = "" + else: + warning_pair = """

    certaines UE du semestre pair ne sont pas associées à un niveau de compétence
    """ + if all(u.niveau_competence for u in deca.ues_impair): + warning_impair = "" + else: + warning_impair = """
    certaines UE du semestre impair ne sont pas associées à un niveau de compétence
    """ + msg = ( + f"""

    Pas de RCUE pour l'UE {ue.acronyme}

    + {warning_impair} + {warning_pair} +
    UE {ue.acronyme}: niveau {html.escape(str(ue.niveau_competence))}
    +
    UEs impaires: {html.escape(', '.join(str(u.niveau_competence or "pas de niveau") + for u in deca.ues_impair))} +
    + """ + + deca.infos() + ) + super().__init__(msg) + + class DecisionsProposees: """Une décision de jury proposé, constituée d'une liste de codes et d'une explication. Super-classe, spécialisée pour les UE, les RCUE, les années et le diplôme. @@ -196,6 +224,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) "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" if self.formsemestre_impair is not None: self.validation = ApcValidationAnnee.query.filter_by( etudid=self.etud.id, @@ -234,7 +264,6 @@ class DecisionsProposeesAnnee(DecisionsProposees): } ) self.rcues_annee = self.compute_rcues_annee() - "RCUEs de l'année" formation = ( self.formsemestre_impair.formation @@ -295,15 +324,41 @@ class DecisionsProposeesAnnee(DecisionsProposees): def infos(self) -> str: "informations, for debugging purpose" - return f"""DecisionsProposeesAnnee - etud: {self.etud} - formsemestre_impair: {self.formsemestre_impair} - formsemestre_pair: {self.formsemestre_pair} - RCUEs: {self.rcues_annee} - nb_competences: {self.nb_competences} - nb_nb_validables: {self.nb_validables} - codes: {self.codes} - explanation: {self.explanation} + return f"""DecisionsProposeesAnnee + """ def annee_scolaire(self) -> int: @@ -416,11 +471,11 @@ class DecisionsProposeesAnnee(DecisionsProposees): ues_impair_sans_rcue.discard(ue_impair.id) break if rcue is None: - raise ScoValueError(f"pas de RCUE pour l'UE {ue_pair.acronyme}") + 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 ScoValueError(f"pas de RCUE pour l'UE {ue.acronyme}") + raise NoRCUEError(deca=self, ue=ue) return rcues_annee def compute_decisions_niveaux(self) -> dict[int, "DecisionsProposeesRCUE"]: diff --git a/app/models/formations.py b/app/models/formations.py index 7db99a1fb5..d2970fce98 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -57,6 +57,10 @@ class Formation(db.Model): def __repr__(self): return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>" + def to_html(self) -> str: + "titre complet pour affichage" + return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}""" + def to_dict(self): e = dict(self.__dict__) e.pop("_sa_instance_state", None) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index e8d8ba02bf..86bc2b9abe 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -676,9 +676,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list ], page_title=f"Programme {formation.acronyme}", ), - f"""

    Formation {formation.titre} ({formation.acronyme}) - [version {formation.version}] code {formation.formation_code} - {lockicon} + f"""

    {formation.to_html()} {lockicon}

    """, ] From 8fd696ff7e1a90b137ab8822d61d042f0ab9eff3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 09:52:50 +0200 Subject: [PATCH 072/140] =?UTF-8?q?Jury=20BUT:=20code=20par=20d=C3=A9faut?= =?UTF-8?q?=20RED=20si=201=20RCUE=20<=208?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/jury_but.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index b5dbdc5865..a64ac428a7 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -311,7 +311,12 @@ class DecisionsProposeesAnnee(DecisionsProposees): 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.PAS1NCI, sco_codes.ADJ] + self.codes + self.codes = [ + sco_codes.RED, + sco_codes.NAR, + sco_codes.PAS1NCI, + sco_codes.ADJ, + ] + self.codes self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8" else: self.codes = [sco_codes.RED, sco_codes.NAR, sco_codes.ADJ] + self.codes From 8863c6efa150452958c259dfa09d4f811b76c1c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 09:59:43 +0200 Subject: [PATCH 073/140] Jury BUT: codes annuels identiques pour les non admis, non passage de droit --- app/but/jury_but.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index a64ac428a7..848b83d703 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -319,7 +319,12 @@ class DecisionsProposeesAnnee(DecisionsProposees): ] + self.codes self.explanation = expl_rcues + f" et {self.nb_rcues_under_8} < 8" else: - self.codes = [sco_codes.RED, sco_codes.NAR, sco_codes.ADJ] + self.codes + self.codes = [ + sco_codes.RED, + sco_codes.NAR, + sco_codes.PAS1NCI, + sco_codes.ADJ, + ] + self.codes self.explanation = ( expl_rcues + f""" et {self.nb_rcues_under_8} From 40e21f6f0d6473c7b5ee634ddc5f5cb7cf14bcb4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 10:06:38 +0200 Subject: [PATCH 074/140] Corrige documentation codes jury BUT --- .../but/documentation_codes_jury.html | 63 ++++++------------- 1 file changed, 19 insertions(+), 44 deletions(-) diff --git a/app/templates/but/documentation_codes_jury.html b/app/templates/but/documentation_codes_jury.html index 0b38a972e4..090a830a29 100644 --- a/app/templates/but/documentation_codes_jury.html +++ b/app/templates/but/documentation_codes_jury.html @@ -125,65 +125,40 @@
    - - + - - - - - - - - - - - - - + - - - + + - - - - - - - - - - - - - + + - - - - - - - + - - + - - - + + + + + + + + + +
    ADM Admis Acquis (ECTS acquis)
    ADJ Admis par décision jury
    PASD PASDNon admis, mais passage de droit
    PAS1NCIPAS1NCINon admis, mais passage par décision de jury - (Passage en Année Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8)) + CMP Acquis par compensation UE compensée avec l’UE de même compétence et de même année (ECTS acquis)
    REDREDAjourné, mais autorisé à redoublerADJ Acquis par décision de jury (ECTS acquis)
    NARREONon admis, réorientation
    DEMDémission
    ABANABANdon constaté (sans lettre de démission)AJ Attente pour problème de moyenne
    RATEn attente d’un rattrapage
    EXCLUEXCEXClusion, décision réservée à des décisions disciplinaires En attente d’un rattrapage
    DEFNon évalué par manque assiduité (défaillance) Défaillant Pas ou peu de notes par arrêt de la formation
    ABLABLAnnée BlancheABAN Non évalué pour manque d’assiduité Non présentation des notes de l’étudiant au jury
    DEM Démission
    UEBSL UE blanchie
    From 69ceb8affcaef799cd901c064917bf02ed954a88 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 14:29:57 +0200 Subject: [PATCH 075/140] =?UTF-8?q?Fix:=20calcul=20evaluations=20en=20atte?= =?UTF-8?q?nte=20/=20d=C3=A9missionnaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_mod.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 8f8bd1a89f..c63394ca9e 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -161,8 +161,10 @@ class ModuleImplResults: evals_notes = evals_notes.merge( eval_df, how="left", left_index=True, right_index=True ) - # Notes en attente: (on prend dans evals_notes pour ne pas avoir les dem.) - nb_att = sum(evals_notes[str(evaluation.id)] == scu.NOTES_ATTENTE) + # Notes en attente: (ne prend en compte que les inscrits, non démissionnaires) + nb_att = sum( + evals_notes[str(evaluation.id)][inscrits_module] == scu.NOTES_ATTENTE + ) self.evaluations_etat[evaluation.id] = EvaluationEtat( evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete ) From 3d5361ce50075e2e72e3674538c29f40c885b0a3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 14:30:21 +0200 Subject: [PATCH 076/140] =?UTF-8?q?Fix:=20calcul=20evaluations=20en=20atte?= =?UTF-8?q?nte=20/=20d=C3=A9missionnaires?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index d686c66dcb..171fc11579 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.3.1" +SCOVERSION = "9.3.2" SCONAME = "ScoDoc" From 4087fd609682dea6ddd5eb1876b81f764081c5be Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 15:43:46 +0200 Subject: [PATCH 077/140] Codes jury: enregistrement transcodage BUT, tableau documentation --- app/forms/main/config_apo.py | 1 + app/models/config.py | 18 ++- app/static/css/jury_but.css | 5 + .../but/documentation_codes_jury.html | 110 +++++++++++++----- app/views/notes.py | 11 +- 5 files changed, 112 insertions(+), 33 deletions(-) diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py index 551727fcbb..946e6ff291 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -41,6 +41,7 @@ from app.scodoc import sco_codes_parcours def _build_code_field(code): return StringField( label=code, + default=code, description=sco_codes_parcours.CODES_EXPL[code], validators=[ validators.regexp( diff --git a/app/models/config.py b/app/models/config.py index 53ac96e9bc..817125c377 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -9,6 +9,8 @@ from app.comp import bonus_spo from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_codes_parcours import ( + ABAN, + ABL, ADC, ADJ, ADM, @@ -19,11 +21,16 @@ from app.scodoc.sco_codes_parcours import ( CMP, DEF, DEM, + EXCLU, NAR, + PASD, + PAS1NCI, RAT, ) CODES_SCODOC_TO_APO = { + ABAN: "ABAN", + ABL: "ABL", ADC: "ADMC", ADJ: "ADM", ADM: "ADM", @@ -34,7 +41,10 @@ CODES_SCODOC_TO_APO = { CMP: "COMP", DEF: "NAR", DEM: "NAR", + EXCLU: "EXC", NAR: "NAR", + PASD: "PASD", + PAS1NCI: "PAS1NCI", RAT: "ATT", "NOTES_FMT": "%3.2f", } @@ -161,9 +171,8 @@ class ScoDocSiteConfig(db.Model): @classmethod def get_code_apo(cls, code: str) -> str: """La représentation d'un code pour les exports Apogée. - Par exemple, à l'iUT du H., le code ADM est réprésenté par VAL + Par exemple, à l'IUT du H., le code ADM est réprésenté par VAL Les codes par défaut sont donnés dans sco_apogee_csv. - """ cfg = ScoDocSiteConfig.query.filter_by(name=code).first() if not cfg: @@ -172,6 +181,11 @@ class ScoDocSiteConfig(db.Model): code_apo = cfg.value return code_apo + @classmethod + def get_codes_apo_dict(cls) -> dict[str:str]: + "Un dict avec code jury : code exporté" + return {code: cls.get_code_apo(code) for code in CODES_SCODOC_TO_APO} + @classmethod def set_code_apo(cls, code: str, code_apo: str): """Enregistre nouvelle représentation du code""" diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css index e933b6fd5e..2804eab9a4 100644 --- a/app/static/css/jury_but.css +++ b/app/static/css/jury_but.css @@ -156,4 +156,9 @@ div.but_doc table tbody tr { div.but_doc table tbody tr:nth-child(odd) { background-color: #ffffff; +} + +div.but_doc table tr td.amue { + color: rgb(127, 127, 206); + font-size: 90%; } \ No newline at end of file diff --git a/app/templates/but/documentation_codes_jury.html b/app/templates/but/documentation_codes_jury.html index 090a830a29..499dabb57a 100644 --- a/app/templates/but/documentation_codes_jury.html +++ b/app/templates/but/documentation_codes_jury.html @@ -9,69 +9,82 @@ + - + - + + - + + - + + - + + - + + - + + - + + - - + + + - + + - + + - - + + + - + +
    ScoDoc{{nom_univ}} AMUESignification
    ADM{{codes["ADM"]}} Admis
    ADJ{{codes["ADJ"]}} Admis par décision jury
    PASDPASD{{codes["PASD"]}}PASD Non admis, mais passage de droit
    PAS1NCIPAS1NCI{{codes["PAS1NCI"]}}PAS1NCI Non admis, mais passage par décision de jury (Passage en Année Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8))
    REDRED{{codes["RED"]}}RED Ajourné, mais autorisé à redoubler
    NARREO{{codes["NAR"]}}REO Non admis, réorientation
    DEM{{codes["DEM"]}} Démission
    ABANABAN ABANdon constaté (sans lettre de démission){{codes["ABAN"]}}ABANABANdon constaté (sans lettre de démission)
    RAT{{codes["RAT"]}} En attente d’un rattrapage
    EXCLUEXC{{codes["EXCLU"]}}EXC EXClusion, décision réservée à des décisions disciplinaires
    DEF (défaillance) Non évalué par manque assiduité{{codes["DEF"]}}(défaillance) Non évalué par manque assiduité
    ABLABL{{codes["ABL"]}}ABL Année Blanche
    @@ -81,39 +94,52 @@
    + + + + + + + - + + - + + - + + + - + + - + +
    ScoDoc{{nom_univ}}AMUESignification
    ADM - VAL + {{codes["ADM"]}}VAL Acquis
    CMP{{codes["CMP"]}} Acquis par compensation annuelle
    ADJCODJ{{codes["ADJ"]}}CODJ Acquis par décision du jury
    AJAJ{{codes["AJ"]}}AJ Attente pour problème de moyenne
    RAT{{codes["RAT"]}} En attente d’un rattrapage
    DEF{{codes["DEF"]}} Défaillant
    ABAN{{codes["ABAN"]}} Non évalué pour manque assiduité
    @@ -123,42 +149,66 @@
    + + + + + + - + + + - + + - + + + - + + + - + + + - + + + - + + + - + + + - + + +
    ScoDoc{{nom_univ}}AMUESignification
    ADM Acquis (ECTS acquis){{codes["ADM"]}}VALAcquis (ECTS acquis)
    CMP Acquis par compensation UE compensée avec l’UE de même compétence et de même année (ECTS acquis) + {{codes["CMP"]}}COMPAcquis par compensation UE compensée avec l’UE de même compétence et de même année (ECTS acquis)
    ADJ Acquis par décision de jury (ECTS acquis){{codes["ADJ"]}}Acquis par décision de jury (ECTS acquis)
    AJ Attente pour problème de moyenne{{codes["AJ"]}}AJAttente pour problème de moyenne
    RAT En attente d’un rattrapage{{codes["RAT"]}}En attente d’un rattrapage
    DEF Défaillant Pas ou peu de notes par arrêt de la formation{{codes["DEF"]}}ABANDéfaillant Pas ou peu de notes par arrêt de la formation
    ABAN Non évalué pour manque d’assiduité Non présentation des notes de l’étudiant au jury{{codes["ABAN"]}}ABANNon évalué pour manque d’assiduité Non présentation des notes de l’étudiant au jury
    DEM Démission{{codes["DEM"]}}Démission
    UEBSL UE blanchie {{codes["UEBSL"]}}UEBSLUE blanchie
    diff --git a/app/views/notes.py b/app/views/notes.py index 005e7963a6..005f5796d5 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -46,6 +46,7 @@ from app.but.forms import jury_but_forms from app.comp import res_sem from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_compat import NotesTableCompat +from app.models.config import ScoDocSiteConfig from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestreUEComputationExpr @@ -2359,7 +2360,15 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): ) H.append("") # but_annee - H.append(render_template("but/documentation_codes_jury.html")) + 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) + html_sco_header.sco_footer() From b2098c833a6ffcfbea911812ae3ff3ad1d58edc1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 16:08:28 +0200 Subject: [PATCH 078/140] =?UTF-8?q?Jury=20BUT:=20Avertissement=20si=20plus?= =?UTF-8?q?ieurs=20parcours=20mais=20=C3=A9tduiant=20non=20assign=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/notes.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/views/notes.py b/app/views/notes.py index 005f5796d5..e6065c3640 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2270,6 +2270,12 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): etudid=etudid)}" class="stdlink">effacer décisions""" else: erase_span = "" + 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.
    """ H.append( f"""
    @@ -2277,6 +2283,7 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"} - {deca.annee_scolaire_str()}
    {etud.nomprenom}
    + {warning}
    @@ -2306,7 +2313,9 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int):
    {niveau.competence.titre}
    """ ) - dec_rcue = deca.decisions_rcue_by_niveau[niveau.id] + dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) + if dec_rcue is None: + break # Semestre impair H.append( _gen_but_niveau_ue( From a00296250f2e796549d5698c3b9bad36363a10b4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 16:37:08 +0200 Subject: [PATCH 079/140] Fix: ne compte pas les UE bonus dans le tabelau recap --- app/comp/res_but.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 14cf63124f..0b58521ae8 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -7,6 +7,7 @@ """Résultats semestres BUT """ from collections.abc import Generator +from re import U import time import numpy as np import pandas as pd @@ -204,7 +205,7 @@ class ResultatsSemestreBUT(NotesTableCompat): inscr.etudid: inscr.parcour_id for inscr in self.formsemestre.inscriptions } self.etuds_parcour_id = etuds_parcour_id - ue_ids = [ue.id for ue in self.ues] + ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT] # matrice de 1, inscrits par défaut à toutes les UE: ues_inscr_parcours_df = pd.DataFrame( 1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float From f7463cbbb082cb5c5a409b20abd91cf9b81200a7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 17:02:18 +0200 Subject: [PATCH 080/140] Jury BUT: pas de saisie pour les DEM --- app/views/notes.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/views/notes.py b/app/views/notes.py index e6065c3640..f433dadba2 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2248,6 +2248,23 @@ def formsemestre_validation_but(formsemestre_id: int, etudid: int): formsemestre = FormSemestre.query.get_or_404(formsemestre_id) etud = Identite.query.get_or_404(etudid) + if formsemestre.etuds_inscriptions[16405].etat != scu.INSCRIT: + return ( + "\n".join(H) + + f"""
    Impossible de statuer sur cet étudiant: + il est démissionnaire ou défaillant (voir sa fiche) +
    + +
    + """ + + html_sco_header.sco_footer() + ) + res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) if len(deca.rcues_annee) == 0: From 6b1d4d1afd6308882033aa629a2e774993240d45 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 26 Jun 2022 17:54:44 +0200 Subject: [PATCH 081/140] Modifie URL vers ressources statiques selon la version. Close #404 --- app/__init__.py | 6 ++ app/comp/moy_mod.py | 3 +- app/scodoc/html_sco_header.py | 114 ++++++++++++---------------- app/scodoc/sco_trombino.py | 6 +- app/scodoc/sco_utils.py | 9 +-- app/templates/but/bulletin.html | 2 +- app/templates/but/refcomp_show.html | 2 +- app/templates/sco_page.html | 39 +++++----- 8 files changed, 85 insertions(+), 96 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 66c6effd1c..738ad09705 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -211,6 +211,12 @@ def create_app(config_class=DevConfig): app.config.from_object(config_class) + # Vérifie/crée lien sym pour les URL statiques + link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}" + if not os.path.exists(link_filename): + app.logger.info(f"creating symlink {link_filename}") + os.symlink("..", link_filename) + db.init_app(app) migrate.init_app(app, db) login.init_app(app) diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index c63394ca9e..aba032fd90 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -163,7 +163,8 @@ class ModuleImplResults: ) # Notes en attente: (ne prend en compte que les inscrits, non démissionnaires) nb_att = sum( - evals_notes[str(evaluation.id)][inscrits_module] == scu.NOTES_ATTENTE + evals_notes[str(evaluation.id)][list(inscrits_module)] + == scu.NOTES_ATTENTE ) self.evaluations_etat[evaluation.id] = EvaluationEtat( evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 156f5560a6..0b2a3d5870 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -59,35 +59,29 @@ BOOTSTRAP_MULTISELECT_CSS = [ def standard_html_header(): """Standard HTML header for pages outside depts""" # not used in ZScolar, see sco_header - return """ + return f""" ScoDoc: accueil - + - + -%s""" % ( - scu.SCO_ENCODING, - scu.CUSTOM_HTML_HEADER_CNX, - ) +{scu.CUSTOM_HTML_HEADER_CNX}""" def standard_html_footer(): """Le pied de page HTML de la page d'accueil.""" - return """ -

    Problèmes et suggestions sur le logiciel: %s

    +

    Problèmes et suggestions sur le logiciel: {scu.SCO_USERS_LIST}

    ScoDoc est un logiciel libre développé par Emmanuel Viennet.

    -""" % ( - scu.SCO_USERS_LIST, - scu.SCO_USERS_LIST, - ) +""" -_HTML_BEGIN = """ +_HTML_BEGIN = f""" @@ -100,27 +94,27 @@ _HTML_BEGIN = """ %(page_title)s - + - - - - + + + + - - - + + + - + - - + + - - + + """ @@ -193,7 +187,7 @@ def sco_header( # jQuery UI # can modify loaded theme here H.append( - '\n' + f'\n' ) if init_google_maps: # It may be necessary to add an API key: @@ -202,72 +196,65 @@ def sco_header( # Feuilles de style additionnelles: for cssstyle in cssstyles: H.append( - """\n""" - % cssstyle + f"""\n""" ) H.append( - """ - - - + f""" + + + - - + + """ - % params ) # jQuery H.append( - """ - """ + f""" + """ ) - H.append('') # qTip if init_qtip: H.append( - '' - ) - H.append( - '' + f""" + """ ) H.append( - '' + f""" + """ ) - - H.append('') if init_google_maps: H.append( - '' + f'' ) if init_datatables: H.append( - '' + f""" + """ ) - H.append('') # H.append( - # '' + # f'' # ) # JS additionels for js in javascripts: - H.append("""\n""" % js) + H.append(f"""\n""") H.append( - """ """ - % params ) # Scripts de la page: if scripts: @@ -296,12 +283,11 @@ def sco_header( if user_check: if current_user.passwd_temp: H.append( - """
    + f"""
    Attention !
    Vous avez reçu un mot de passe temporaire.
    - Vous devez le changer: cliquez ici + Vous devez le changer: cliquez ici
    """ - % (scu.UsersURL, current_user.user_name) ) # if head_message: @@ -330,6 +316,6 @@ def html_sem_header( else: h = "" if with_h2: - return h + """

    %s

    """ % (title) + return h + f"""

    {title}

    """ else: return h diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index 9534cfb29d..a8a96a43ca 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -151,10 +151,8 @@ def trombino_html(groups_infos): if sco_photos.etud_photo_is_local(t, size="small"): foto = sco_photos.etud_photo_html(t, title="") else: # la photo n'est pas immédiatement dispo - foto = ( - 'en cours' - % t["etudid"] - ) + foto = f"""en cours""" H.append( '%s' % ( diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 924d59db1e..9207aa7b7f 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -63,6 +63,8 @@ from app.scodoc import sco_exceptions from app.scodoc import sco_xml import sco_version +# le répertoire static, lié à chaque release pour éviter les problèmes de caches +STATIC_DIR = "/ScoDoc/static/links/" + sco_version.SCOVERSION # ----- CALCUL ET PRESENTATION DES NOTES NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis @@ -957,12 +959,7 @@ def icontag(name, file_format="png", no_size=False, **attrs): if "alt" not in attrs: attrs["alt"] = "logo %s" % name s = " ".join(['%s="%s"' % (k, attrs[k]) for k in attrs]) - return '' % ( - name, - s, - name, - file_format, - ) + return f'' ICON_PDF = icontag("pdficon16x20_img", title="Version PDF") diff --git a/app/templates/but/bulletin.html b/app/templates/but/bulletin.html index 8c260e600d..8b133ce93b 100644 --- a/app/templates/but/bulletin.html +++ b/app/templates/but/bulletin.html @@ -10,7 +10,7 @@ {% include 'bul_head.html' %} - + {% include 'bul_foot.html' %} diff --git a/app/templates/but/refcomp_show.html b/app/templates/but/refcomp_show.html index 0e4ffef688..62b3a01544 100644 --- a/app/templates/but/refcomp_show.html +++ b/app/templates/but/refcomp_show.html @@ -10,7 +10,7 @@ - +
    Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à diff --git a/app/templates/sco_page.html b/app/templates/sco_page.html index f1590302a1..e55c5627e0 100644 --- a/app/templates/sco_page.html +++ b/app/templates/sco_page.html @@ -4,13 +4,14 @@ {% block styles %} {{super()}} - - - - -{# #} - + href="{{sco.scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" /> + + + + +{# + #} + {% endblock %} {% block title %} @@ -25,9 +26,9 @@
    {% with messages = get_flashed_messages(with_categories=true) %} - {% for category, message in messages %} - - {% endfor %} + {% for category, message in messages %} + + {% endfor %} {% endwith %}
    {% if sco.sem %} @@ -46,16 +47,16 @@ {{ super() }} {{ moment.include_moment() }} {{ moment.lang(g.locale) }} - - - - - - - + + + + + + + - - + +