diff --git a/app/__init__.py b/app/__init__.py index 0889da6424..c2b7895971 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -36,6 +36,7 @@ from jinja2 import select_autoescape import sqlalchemy as sa from flask_cas import CAS +import werkzeug.debug from app.scodoc.sco_exceptions import ( AccessDenied, @@ -273,6 +274,14 @@ def create_app(config_class=DevConfig): # flask_sqlalchemy/query (pb deprecation du model.get()) warnings.filterwarnings("error", module="flask_sqlalchemy/query") # warnings.filterwarnings("ignore", module="json/provider.py") xxx sans effet en test + if app.config["DEBUG"]: + # comme on a désactivé ci-dessus les logs de werkzeug, + # on affiche nous même le PIN en mode debug: + print( + f""" * Debugger is active! + * Debugger PIN: {werkzeug.debug.get_pin_and_cookie_name(app)[0]} + """ + ) # 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): diff --git a/app/but/apc_edit_ue.py b/app/but/apc_edit_ue.py index bdd88205da..079c282096 100644 --- a/app/but/apc_edit_ue.py +++ b/app/but/apc_edit_ue.py @@ -7,10 +7,10 @@ """ Edition associations UE <-> Ref. Compétence """ -from flask import g, render_template, url_for +from flask import g, url_for + from app.models import ApcReferentielCompetences, UniteEns from app.scodoc import codes_cursus -from app.forms.formation.ue_parcours_niveau import UEParcoursNiveauForm def form_ue_choix_niveau(ue: UniteEns) -> str: @@ -28,69 +28,17 @@ def form_ue_choix_niveau(ue: UniteEns) -> str: }">associer un référentiel de compétence """ - # Les parcours: - parcours_options = [] - for parcour in ref_comp.parcours: - parcours_options.append( - f"""""" - ) - newline = "\n" return f""" -
-
-
-
- Parcours : - -
-
- Niveau de compétence : - -
-
-
-
- """ - - -# Nouvelle version XXX WIP -def form_ue_choix_parcours_niveau(ue: UniteEns): - """formulaire (div) pour choix association des parcours et du niveau de compétence d'une UE""" - if ue.type != codes_cursus.UE_STANDARD: - return "" - ref_comp = ue.formation.referentiel_competence - if ref_comp is None: - return f"""
-
Pas de référentiel de compétence associé à cette formation !
-
associer un référentiel de compétence -
-
""" - parcours = ue.formation.referentiel_competence.parcours - form = UEParcoursNiveauForm(ue, parcours) - return f"""
- { render_template( "pn/ue_choix_parcours_niveau.j2", form_ue_parcours_niveau=form ) } +
+
""" diff --git a/app/forms/formation/ue_parcours_ects.py b/app/forms/formation/ue_parcours_ects.py new file mode 100644 index 0000000000..4d25557dc1 --- /dev/null +++ b/app/forms/formation/ue_parcours_ects.py @@ -0,0 +1,35 @@ +from flask import g, url_for +from flask_wtf import FlaskForm +from wtforms import FieldList, Form, DecimalField, validators + +from app.models import ApcParcours, ApcReferentielCompetences, UniteEns + + +class _UEParcoursECTSForm(FlaskForm): + "Formulaire association ECTS par parcours à une UE" + # construit dynamiquement ci-dessous + + +def UEParcoursECTSForm(ue: UniteEns) -> FlaskForm: + "Génère formulaire association ECTS par parcours à une UE" + + class F(_UEParcoursECTSForm): + pass + + parcours: list[ApcParcours] = ue.formation.referentiel_competence.parcours + # Initialise un champs de saisie par parcours + for parcour in parcours: + ects = ue.get_ects(parcour, only_parcours=True) + setattr( + F, + f"ects_parcour_{parcour.id}", + DecimalField( + f"Parcours {parcour.code}", + validators=[ + validators.Optional(), + validators.NumberRange(min=0, max=30), + ], + default=ects, + ), + ) + return F() diff --git a/app/forms/formation/ue_parcours_niveau.py b/app/forms/formation/ue_parcours_niveau.py deleted file mode 100644 index d9070c2800..0000000000 --- a/app/forms/formation/ue_parcours_niveau.py +++ /dev/null @@ -1,39 +0,0 @@ -from flask import g, url_for -from flask_wtf import FlaskForm -from wtforms.fields import SelectField, SelectMultipleField - -from app.models import ApcParcours, ApcReferentielCompetences, UniteEns - - -class UEParcoursNiveauForm(FlaskForm): - "Formulaire association parcours et niveau de compétence à une UE" - niveau_select = SelectField( - "Niveau de compétence:", render_kw={"class": "niveau_select"} - ) - parcours_multiselect = SelectMultipleField( - "Parcours :", - coerce=int, - option_widget={"class": "form-check-input"}, - # widget_attrs={"class": "form-check"}, - render_kw={"class": "multiselect select_ue_parcours", "multiple": "multiple"}, - ) - - def __init__(self, ue: UniteEns, parcours: list[ApcParcours], *args, **kwargs): - super().__init__(*args, **kwargs) - - # Initialise le menu des niveaux: - self.niveau_select.render_kw["data-ue_id"] = ue.id - self.niveau_select.choices = [ - (r.id, f"{r.type_titre} {r.specialite_long} ({r.get_version()})") - for r in ApcReferentielCompetences.query.filter_by(dept_id=g.scodoc_dept_id) - ] - # Initialise le menu des parcours - self.parcours_multiselect.render_kw["data-set_ue_parcours"] = url_for( - "apiweb.set_ue_parcours", ue_id=ue.id, scodoc_dept=g.scodoc_dept - ) - parcours_options = [(str(p.id), f"{p.libelle} ({p.code})") for p in parcours] - self.parcours_multiselect.choices = parcours_options - - # initialize checked items based on u instance - parcours_selected = [str(p.id) for p in ue.parcours] - self.parcours_multiselect.process_data(parcours_selected) diff --git a/app/models/ues.py b/app/models/ues.py index 7953fc1258..3212e32a0c 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -38,7 +38,7 @@ class UniteEns(db.Model): server_default=db.text("notes_newid_ucod()"), nullable=False, ) - ects = db.Column(db.Float) # nombre de credits ECTS + ects = db.Column(db.Float) # nombre de credits ECTS (sauf si parcours spécifié) is_external = db.Column(db.Boolean(), default=False, server_default="false") # id de l'element pedagogique Apogee correspondant: code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) @@ -100,8 +100,7 @@ class UniteEns(db.Model): return ue def to_dict(self, convert_objects=False, with_module_ue_coefs=True): - """as a dict, with the same conversions as in ScoDoc7 - (except ECTS: keep None) + """as a dict, with the same conversions as in ScoDoc7. If convert_objects, convert all attributes to native types (suitable for json encoding). """ @@ -111,7 +110,12 @@ class UniteEns(db.Model): # ScoDoc7 output_formators e["ue_id"] = self.id e["numero"] = e["numero"] if e["numero"] else 0 - e["ects"] = e["ects"] + e["ects"] = e["ects"] # legacy + e["ects_by_parcours"] = {} + for up in UEParcours.query.filter_by(ue_id=self.id): + p = ApcParcours.query.get(up.parcours_id) + e["ects_by_parcours"][p.code] = self.get_ects(p) + e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0 e["code_apogee"] = e["code_apogee"] or "" # pas de None e["parcours"] = [ @@ -164,6 +168,44 @@ class UniteEns(db.Model): db.session.add(self) db.session.commit() + def get_ects(self, parcour: ApcParcours = None, only_parcours=False) -> float: + """Crédits ECTS associés à cette UE. + En BUT, cela peut quelquefois dépendre du parcours. + Si only_parcours, renvoie None si pas de valeur spéciquement définie dans + le parcours indiqué. + """ + if parcour is not None: + ue_parcour = UEParcours.query.filter_by( + ue_id=self.id, parcours_id=parcour.id + ).first() + if ue_parcour is not None and ue_parcour.ects is not None: + return ue_parcour.ects + if only_parcours: + return None + return self.ects + + def set_ects(self, ects: float, parcour: ApcParcours = None): + """Fixe les crédits. Do not commit. + Si le parcours n'est pas spécifié, affecte les ECTS par défaut de l'UE. + Si ects est None et parcours indiqué, efface l'association. + """ + if parcour is not None: + ue_parcour = UEParcours.query.filter_by( + ue_id=self.id, parcours_id=parcour.id + ).first() + if ects is None: + if ue_parcour: + db.session.delete(ue_parcour) + else: + if ue_parcour is None: + ue_parcour = UEParcours(parcours_id=parcour.id, ue_id=self.id) + ue_parcour.ects = float(ects) + db.session.add(ue_parcour) + else: + self.ects = ects + log(f"ue.set_ects( ue_id={self.id}, acronyme={self.acronyme}, ects={ects} )") + db.session.add(self) + def get_ressources(self): "Liste des modules ressources rattachés à cette UE" return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all() @@ -334,12 +376,21 @@ class UEParcours(db.Model): """Association ue <-> parcours, indiquant les ECTS""" __tablename__ = "ue_parcours" - ue_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), primary_key=True) + ue_id = db.Column( + db.Integer, + db.ForeignKey("notes_ue.id", ondelete="CASCADE"), + primary_key=True, + ) parcours_id = db.Column( - db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True + db.Integer, + db.ForeignKey("apc_parcours.id", ondelete="CASCADE"), + primary_key=True, ) ects = db.Column(db.Float, nullable=True) # si NULL, on prendra les ECTS de l'UE + def __repr__(self): + return f"" + class DispenseUE(db.Model): """Dispense d'UE diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index 66d946c30b..dba8397134 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -111,7 +111,6 @@ def html_edit_formation_apc( icons=icons, ues_by_sem=ues_by_sem, ects_by_sem=ects_by_sem, - form_ue_choix_parcours_niveau=apc_edit_ue.form_ue_choix_parcours_niveau, scu=scu, codes_cursus=codes_cursus, ), diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 780e22dc8d..8467df1ce7 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -369,7 +369,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "min_value": 0, "max_value": 1000, "title": "ECTS", - "explanation": "nombre de crédits ECTS (indiquer 0 si UE bonus)", + "explanation": "nombre de crédits ECTS (indiquer 0 si UE bonus)" + + ( + ". (si les ECTS dépendent du parcours, voir plus bas.)" + if is_apc + else "" + ), "allow_null": not is_apc, # ects requis en APC }, ), diff --git a/app/static/css/parcour_formation.css b/app/static/css/parcour_formation.css index 9c79dde4c9..efd5adf354 100644 --- a/app/static/css/parcour_formation.css +++ b/app/static/css/parcour_formation.css @@ -103,12 +103,12 @@ div.competence { padding-bottom: 6px; } -.titre_niveau span.parcs { +span.parcs { margin-left: 12px; display: inline-block; } -.titre_niveau span.parc { +span.parc { font-size: 75%; font-weight: bold; /* color: rgb(92, 87, 255); */ diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 31daa21c3d..fa8ce74d53 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -2530,6 +2530,15 @@ div.cont_ue_choix_niveau select.select_niveau_ue { width: 490px; } +div.ue_advanced { + background-color: rgb(244, 253, 255); + border: 1px solid blue; + border-radius: 10px; + padding: 10px; + margin-top: 10px; + margin-right: 15px; +} + div#ue_list_modules { background-color: rgb(251, 225, 165); border: 1px solid blue; diff --git a/app/templates/but/parcour_formation.j2 b/app/templates/but/parcour_formation.j2 index e3bd72695a..b19cd4a092 100644 --- a/app/templates/but/parcour_formation.j2 +++ b/app/templates/but/parcour_formation.j2 @@ -116,11 +116,32 @@ Choisissez un parcours...
Référentiel de compétences
+{% if parcour %} +
+ +

Cette page représente le parcours {{parcour.code}} + du référentiel de compétence {{formation.referentiel_competence.specialite}}, et permet + d'associer à chaque semestre d'un niveau de compétence une UE de la formation + {{formation.to_html()}} + .

+ +

Le symbole TC désigne un niveau du tronc commun + (c'est à dire présent dans tous les parcours de la spécialité).

+ +

Ce formulaire ne vérifie pas si l'UE est bien conçue pour ce parcours.

+ +

Les modifications sont enregistrées au fur et à mesure.

+ +
+{% endif %} +