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"""{parcour.libelle} ({parcour.code})
- """
- )
- newline = "\n"
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...