From 0f61b0874aab631a6d2cf7701dc650f07bb06b5b Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sun, 12 Nov 2023 19:58:55 +0100
Subject: [PATCH] =?UTF-8?q?WIP:=20codes=20Apo=20et=20EDT=20sur=20chaque=20?=
=?UTF-8?q?modimpl.=20(pas=20encore=20utilis=C3=A9=20dans=20exports=20Apo)?=
=?UTF-8?q?.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../formsemestre/edit_modimpls_codes_apo.py | 52 +++++++++++
app/forms/main/config_personalized_links.py | 3 +-
app/models/moduleimpls.py | 22 ++++-
app/scodoc/sco_formsemestre_edit.py | 4 +
.../formsemestre/change_formation.j2 | 8 +-
.../formsemestre/edit_modimpls_codes.j2 | 91 +++++++++++++++++++
app/views/notes_formsemestre.py | 47 +++++++++-
.../c8f66652c77f_code_apo_sur_modimpls.py | 38 ++++++++
8 files changed, 255 insertions(+), 10 deletions(-)
create mode 100644 app/forms/formsemestre/edit_modimpls_codes_apo.py
create mode 100644 app/templates/formsemestre/edit_modimpls_codes.j2
create mode 100644 migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py
diff --git a/app/forms/formsemestre/edit_modimpls_codes_apo.py b/app/forms/formsemestre/edit_modimpls_codes_apo.py
new file mode 100644
index 000000000..bdd84a18d
--- /dev/null
+++ b/app/forms/formsemestre/edit_modimpls_codes_apo.py
@@ -0,0 +1,52 @@
+"""
+Formulaire configuration des codes Apo et EDT des modimps d'un formsemestre
+"""
+
+from flask_wtf import FlaskForm
+from wtforms import validators
+from wtforms.fields.simple import BooleanField, StringField, SubmitField
+
+from app.models import FormSemestre, ModuleImpl
+
+
+class _EditModimplsCodesForm(FlaskForm):
+ "form. définition des liens personnalisés"
+ # construit dynamiquement ci-dessous
+
+
+def EditModimplsCodesForm(formsemestre: FormSemestre) -> _EditModimplsCodesForm:
+ "Création d'un formulaire pour éditer les codes"
+
+ # Formulaire dynamique, on créé une classe ad-hoc
+ class F(_EditModimplsCodesForm):
+ pass
+
+ def _gen_mod_form(modimpl: ModuleImpl):
+ field = StringField(
+ modimpl.module.code,
+ validators=[
+ validators.Optional(),
+ validators.Length(min=1, max=80),
+ ],
+ default="",
+ render_kw={"size": 32},
+ )
+ setattr(F, f"modimpl_apo_{modimpl.id}", field)
+ field = StringField(
+ "",
+ validators=[
+ validators.Optional(),
+ validators.Length(min=1, max=80),
+ ],
+ default="",
+ render_kw={"size": 12},
+ )
+ setattr(F, f"modimpl_edt_{modimpl.id}", field)
+
+ for modimpl in formsemestre.modimpls_sorted:
+ _gen_mod_form(modimpl)
+
+ F.submit = SubmitField("Valider")
+ F.cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
+
+ return F()
diff --git a/app/forms/main/config_personalized_links.py b/app/forms/main/config_personalized_links.py
index b2293a2c2..2a261e766 100644
--- a/app/forms/main/config_personalized_links.py
+++ b/app/forms/main/config_personalized_links.py
@@ -2,9 +2,8 @@
Formulaire configuration liens personalisés (menu "Liens")
"""
-from flask import g, url_for
from flask_wtf import FlaskForm
-from wtforms import FieldList, Form, validators
+from wtforms import validators
from wtforms.fields.simple import BooleanField, StringField, SubmitField
from app.models import ScoDocSiteConfig
diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py
index 741abda1b..468888e6a 100644
--- a/app/models/moduleimpls.py
+++ b/app/models/moduleimpls.py
@@ -7,6 +7,7 @@ from flask_sqlalchemy.query import Query
from app import db
from app.auth.models import User
from app.comp import df_cache
+from app.models import APO_CODE_STR_LEN
from app.models.etudiants import Identite
from app.models.modules import Module
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
@@ -21,6 +22,10 @@ class ModuleImpl(db.Model):
__table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),)
id = db.Column(db.Integer, primary_key=True)
+ code_apogee = db.Column(db.String(APO_CODE_STR_LEN), index=True, nullable=True)
+ "id de l'element pedagogique Apogee correspondant"
+ edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
+ "identifiant emplois du temps (unicité non imposée)"
moduleimpl_id = db.synonym("id")
module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False)
formsemestre_id = db.Column(
@@ -45,11 +50,21 @@ class ModuleImpl(db.Model):
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
+ def get_codes_apogee(self) -> set[str]:
+ """Les codes Apogée (codés en base comme "VRT1,VRT2").
+ (si non renseigné, ceux du module)
+ """
+ if self.code_apogee:
+ return {x.strip() for x in self.code_apogee.split(",") if x}
+ return self.module.get_codes_apogee()
+
def get_edt_id(self) -> str:
- "l'id pour l'emploi du temps: actuellement celui du module"
+ "l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
return (
- self.module.get_edt_id()
- ) # TODO à décliner pour autoriser des codes différents ?
+ self.edt_id
+ or (self.code_apogee.split(",")[0] if self.code_apogee else "")
+ or self.module.get_edt_id()
+ )
def get_evaluations_poids(self) -> pd.DataFrame:
"""Les poids des évaluations vers les UE (accès via cache)"""
@@ -102,6 +117,7 @@ class ModuleImpl(db.Model):
d["module"] = self.module.to_dict(convert_objects=convert_objects)
else:
d.pop("module", None)
+ d["code_apogee"] = d["code_apogee"] or "" # pas de None
return d
def can_edit_evaluation(self, user) -> bool:
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 4f5ae61de..bff18edb3 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -464,6 +464,10 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Modifier les coefficients des UE capitalisées
+ Modifier les codes Apogée et emploi du temps des modules
+
Sélectionner les modules, leurs responsables et les étudiants
à inscrire:
"""
diff --git a/app/templates/formsemestre/change_formation.j2 b/app/templates/formsemestre/change_formation.j2
index 0b3820cae..8c0b8b5b3 100644
--- a/app/templates/formsemestre/change_formation.j2
+++ b/app/templates/formsemestre/change_formation.j2
@@ -11,11 +11,11 @@
Changement de la formation du semestre
On ne peut pas changer la formation d'un semestre existant car
-elle défini son organisation (modules, ...), SAUF si la nouvelle formation a
-exactement le même contenu que l'existante.
+elle définit son organisation (modules, ...), SAUF si la nouvelle formation a
+exactement le même contenu que l'existante.
Cela peut arriver par exemple lorsqu'on crée une nouvelle version (pas encore modifiée)
et que l'on a oublié d'y rattacher un semestre.
-
+
{% if formations %}
@@ -27,4 +27,4 @@ et que l'on a oublié d'y rattacher un semestre.
Aucune formation ne peut se substituer à celle de ce semestre.
{% endif %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/app/templates/formsemestre/edit_modimpls_codes.j2 b/app/templates/formsemestre/edit_modimpls_codes.j2
new file mode 100644
index 000000000..d70b6e280
--- /dev/null
+++ b/app/templates/formsemestre/edit_modimpls_codes.j2
@@ -0,0 +1,91 @@
+{% extends "sco_page.j2" %}
+{% import 'bootstrap/wtf.html' as wtf %}
+
+{% block styles %}
+{{super()}}
+
+{% endblock %}
+
+{% macro render_text_field(field_apo, field_edt, codes_apo_module) %}
+
+
+ {{ field_apo.label(class_="form-label") }}
+ {{codes_apo_module|join(", ") or ("non défini"|safe)}}
+
+ {{field_apo(class_="form-field")}}
+ {{field_edt(class_="field-edt")}}
+ {%- for error in field_apo.errors %}
+ {{ error }}
+ {% endfor %}
+ {%- for error in field_edt.errors %}
+ {{ error }}
+ {% endfor %}
+
+{% endmacro %}
+
+
+{% block app_content %}
+
+
+
Codes Apogée et emploi du temps des modules du semestre
+
+
Les codes élément Apogée sont utilisés pour les exports des
+résultats et peuvent aussi l'être pour connecter l'emploi du temps. Si votre
+logiciel d'emploi du temps utilise des codes différents, vous pouvez aussi
+indiquer un code EDT spécifique.
+
+
+
Les codes Apogée modules rappelés à gauche sont ceux définis
+dans la formation: il sont utilisés sauf si on spécifie un code ici.
+Pour les modifier, aller dans l'édition de la formation.
+
+
+
+
+
+
+{% endblock %}
diff --git a/app/views/notes_formsemestre.py b/app/views/notes_formsemestre.py
index eac987603..84d1509a7 100644
--- a/app/views/notes_formsemestre.py
+++ b/app/views/notes_formsemestre.py
@@ -32,11 +32,12 @@ Emmanuel Viennet, 2023
from flask import flash, redirect, render_template, url_for
from flask import g, request
+from app import db, log
from app.decorators import (
scodoc,
permission_required,
)
-from app.forms.formsemestre import change_formation
+from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo
from app.models import Formation, FormSemestre
from app.scodoc import sco_formations, sco_formation_versions
from app.scodoc.sco_permissions import Permission
@@ -107,6 +108,50 @@ def formsemestre_change_formation(formsemestre_id: int):
)
+@bp.route(
+ "/formsemestre_edit_modimpls_codes/", methods=["GET", "POST"]
+)
+@scodoc
+@permission_required(Permission.EditFormSemestre)
+def formsemestre_edit_modimpls_codes(formsemestre_id: int):
+ """Edition des codes Apogée et EDT"""
+ formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
+ form = edit_modimpls_codes_apo.EditModimplsCodesForm(formsemestre)
+
+ if request.method == "POST" and form.validate:
+ if not form.cancel.data:
+ # record codes
+ for modimpl in formsemestre.modimpls_sorted:
+ field_apo = getattr(form, f"modimpl_apo_{modimpl.id}")
+ field_edt = getattr(form, f"modimpl_edt_{modimpl.id}")
+ if field_apo and field_edt:
+ modimpl.code_apogee = field_apo.data.strip() or None
+ modimpl.edt_id = field_edt.data.strip() or None
+ log(f"setting codes for {modimpl}: apo={field_apo} edt={field_edt}")
+ db.session.add(modimpl)
+ db.session.commit()
+ flash("Codes enregistrés")
+ return redirect(
+ url_for(
+ "notes.formsemestre_status",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=formsemestre_id,
+ )
+ )
+ # GET
+ for modimpl in formsemestre.modimpls_sorted:
+ field_apo = getattr(form, f"modimpl_apo_{modimpl.id}")
+ field_edt = getattr(form, f"modimpl_edt_{modimpl.id}")
+ field_apo.data = modimpl.code_apogee or ""
+ field_edt.data = modimpl.edt_id or ""
+ return render_template(
+ "formsemestre/edit_modimpls_codes.j2",
+ form=form,
+ formsemestre=formsemestre,
+ sco=ScoData(formsemestre=formsemestre),
+ )
+
+
@bp.route("/formsemestre/edt/")
@scodoc
@permission_required(Permission.ScoView)
diff --git a/migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py b/migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py
new file mode 100644
index 000000000..5d818b767
--- /dev/null
+++ b/migrations/versions/c8f66652c77f_code_apo_sur_modimpls.py
@@ -0,0 +1,38 @@
+"""code apo sur modimpls
+
+Revision ID: c8f66652c77f
+Revises: 6fb956addd69
+Create Date: 2023-11-12 10:01:42.424734
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "c8f66652c77f"
+down_revision = "6fb956addd69"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op:
+ batch_op.add_column(
+ sa.Column("code_apogee", sa.String(length=512), nullable=True)
+ )
+ batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
+ batch_op.create_index(
+ batch_op.f("ix_notes_moduleimpl_code_apogee"), ["code_apogee"], unique=False
+ )
+ batch_op.create_index(
+ batch_op.f("ix_notes_moduleimpl_edt_id"), ["edt_id"], unique=False
+ )
+
+
+def downgrade():
+ with op.batch_alter_table("notes_moduleimpl", schema=None) as batch_op:
+ batch_op.drop_index(batch_op.f("ix_notes_moduleimpl_edt_id"))
+ batch_op.drop_index(batch_op.f("ix_notes_moduleimpl_code_apogee"))
+ batch_op.drop_column("edt_id")
+ batch_op.drop_column("code_apogee")