From f4b995c9d234c6918f3e848bd3ff902fdefb7d21 Mon Sep 17 00:00:00 2001
From: ilona
Date: Mon, 30 Dec 2024 19:34:47 +0100
Subject: [PATCH] WIP: fonctions d'import de semestres monomodules, suite
---
app/formations/edit_ue.py | 2 +-
app/formsemestre/import_from_descr.py | 62 +++++++++++++++----
app/models/formations.py | 9 ++-
app/models/moduleimpls.py | 4 +-
app/models/modules.py | 25 ++++++--
app/scodoc/sco_excel.py | 2 +-
.../formsemestre/import_from_description.j2 | 10 +++
.../import_from_description_result.j2 | 5 ++
app/views/notes_formsemestre.py | 16 ++++-
.../bc85a55e63e1_add_dispositif_descr.py | 33 ++++++++++
10 files changed, 143 insertions(+), 25 deletions(-)
create mode 100644 migrations/versions/bc85a55e63e1_add_dispositif_descr.py
diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py
index 83a484aad..eb37a6172 100644
--- a/app/formations/edit_ue.py
+++ b/app/formations/edit_ue.py
@@ -911,7 +911,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
"""
)
- if formsemestres:
+ if not formsemestres:
H.append(
f"""
list[dict]:
exceldata = datafile.read()
diag, rows = sco_excel.excel_bytes_to_dict(exceldata)
# check and convert types
- for line_num, row in enumerate(rows, start=1):
+ for line_num, row in enumerate(rows, start=2):
for field in ALL_FIELDS:
if field.key not in row:
if field.optional:
@@ -234,7 +252,12 @@ def read_excel(datafile) -> list[dict]:
row[field.key] = check_and_convert(row[field.key], field)
except ValueError as exc:
raise ScoValueError(
- f"Ligne {line_num}, colonne {field.key}: {exc.args}"
+ f"Ligne {line_num}, colonne {field.key}: {exc.args}",
+ dest_label="Reprendre",
+ dest_url=url_for(
+ "notes.formsemestres_import_from_description",
+ scodoc_dept=g.scodoc_dept,
+ ),
) from exc
log(diag) # XXX
return rows
@@ -242,8 +265,16 @@ def read_excel(datafile) -> list[dict]:
def _create_formation_and_modimpl(data) -> Formation:
"""Create a new formation, with a UE and module"""
- args = {field.key: data[field.key] for field in FORMATION_FIELDS}
+ args = {
+ field.key.removeprefix("formation_"): data[field.key]
+ for field in FORMATION_FIELDS
+ }
args["dept_id"] = g.scodoc_dept_id
+ # add some required fields:
+ if "titre_officiel" not in args:
+ args["titre_officiel"] = args["titre"]
+ if not args.get("type_parcours"):
+ args["type_parcours"] = CodesCursus.Mono
formation = Formation.create_from_dict(args)
ue = UniteEns.create_from_dict(
{
@@ -266,6 +297,7 @@ def _create_formation_and_modimpl(data) -> Formation:
"titre": data["formation_titre"],
"abbrev": data["formation_titre"],
"code": data["formation_acronyme"],
+ "coefficient": 1.0,
}
)
return formation
@@ -295,6 +327,7 @@ def create_formsemestre_from_description(
if not create_formation:
raise ScoValueError("formation inexistante dans ce département")
formation = _create_formation_and_modimpl(data)
+ db.session.flush()
created.append(formation)
# Détermine le module à placer dans le formsemestre
module = formation.modules.first()
@@ -307,7 +340,9 @@ def create_formsemestre_from_description(
args["dept_id"] = g.scodoc_dept_id
args["formation_id"] = formation.id
args["responsables"] = [user]
- formsemestre = FormSemestre.create_formsemestre(data)
+ if not args.get("titre"):
+ args["titre"] = formation.titre or formation.titre_officiel
+ formsemestre = FormSemestre.create_formsemestre(args)
modimpl = ModuleImpl.create_from_dict(
{
"module_id": module.id,
@@ -317,9 +352,11 @@ def create_formsemestre_from_description(
)
# --- FormSemestreDescription
args = {
- field.key[6:] if field.key.startswith("descr_") else field.key: data[field.key]
+ field.key.removeprefix("descr_"): data[field.key]
for field in FORMSEMESTRE_DESCR_FIELDS
}
+ args["image"] = args["image"] or None
+ args["photo_ens"] = args["photo_ens"] or None
args["formsemestre_id"] = formsemestre.id
formsemestre_descr = FormSemestreDescription.create_from_dict(args)
#
@@ -328,9 +365,12 @@ def create_formsemestre_from_description(
def create_formsemestres_from_description(
- infos: list[dict], create_formation=False
+ infos: list[dict], create_formation: bool = False
) -> list[FormSemestre]:
"Creation de tous les semestres mono-modules"
+ log(
+ f"create_formsemestres_from_description: {len(infos)} items, create_formation={create_formation}"
+ )
return [
create_formsemestre_from_description(data, create_formation=create_formation)
for data in infos
diff --git a/app/models/formations.py b/app/models/formations.py
index 559b6fe7d..9ed7cd0a3 100644
--- a/app/models/formations.py
+++ b/app/models/formations.py
@@ -381,19 +381,22 @@ class Matiere(ScoDocModel):
@classmethod
def create_from_dict(cls, data: dict) -> "Matiere":
"""Create matière from dict. Log, news, cache.
- data must include ue_id, a valid UE id.
+ data must include ue_id, a valid UE id, or ue.
Commit session.
"""
# check ue
if data.get("ue_id") is None:
- raise ScoValueError("UE id missing")
- _ = UniteEns.get_ue(data["ue_id"])
+ if data.get("ue") is None:
+ raise ScoValueError("UE missing")
+ else: # check ue_id
+ _ = UniteEns.get_ue(data["ue_id"])
mat = super().create_from_dict(data)
db.session.commit()
db.session.refresh(mat)
# news
formation = mat.ue.formation
+ log(f"Matiere.create_from_dict: created {mat} from {data}")
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py
index a3ce27a72..ffe7db113 100644
--- a/app/models/moduleimpls.py
+++ b/app/models/moduleimpls.py
@@ -77,7 +77,9 @@ class ModuleImpl(ScoDocModel):
# check required args
for required_arg in ("formsemestre_id", "module_id", "responsable_id"):
if required_arg not in data:
- raise ScoValueError(f"missing argument: {required_arg}")
+ raise ScoValueError(
+ f"ModuleImpl.create_from_dict: missing argument: {required_arg}"
+ )
_ = FormSemestre.get_formsemestre(data["formsemestre_id"])
_ = Module.get_instance(data["module_id"])
if not db.session.get(User, data["responsable_id"]):
diff --git a/app/models/modules.py b/app/models/modules.py
index 2df6a8b2c..7c7a38203 100644
--- a/app/models/modules.py
+++ b/app/models/modules.py
@@ -210,14 +210,27 @@ class Module(models.ScoDocModel):
from app.models.formations import Formation
# check required arguments
- for required_arg in ("code", "formation_id", "ue_id"):
- if required_arg not in data:
- raise ScoValueError(f"missing argument: {required_arg}")
+ if "code" not in data:
+ raise ScoValueError("Module.create_from_dict: missing 'code' argument")
if not data["code"]:
- raise ScoValueError("module code must be non empty")
+ raise ScoValueError(
+ "Module.create_from_dict: module code must be non empty"
+ )
+ # Check ue
+ if data.get("ue_id") is None:
+ ue = data.get("ue")
+ if ue is None or not isinstance(ue, UniteEns):
+ raise ScoValueError("Module.create_from_dict: UE missing")
+ else: # check ue_id
+ ue = UniteEns.get_ue(data["ue_id"])
# Check formation
- formation = Formation.get_formation(data["formation_id"])
- ue = UniteEns.get_ue(data["ue_id"])
+ if data.get("formation_id") is None:
+ formation = data.get("formation")
+ if formation is None or not isinstance(formation, Formation):
+ raise ScoValueError("Module.create_from_dict: formation missing")
+ else: # check ue_id
+ formation = UniteEns.get_ue(data["ue_id"])
+ # formation = Formation.get_formation(data["formation_id"])
# refuse de créer un module APC avec semestres semestre du module != semestre de l'UE
if formation.is_apc():
if int(data.get("semestre_id", 1)) != ue.semestre_idx:
diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index d1e78de84..c9da73362 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -544,7 +544,7 @@ def excel_bytes_to_dict(
keys = [k.strip().lower() for k in rows[0]]
else:
keys = [k.strip() for k in rows[0]]
- return diag, [dict(zip(keys, row)) for row in rows]
+ return diag, [dict(zip(keys, row)) for row in rows[1:]]
def excel_file_to_list(filelike) -> tuple[list, list[list]]:
diff --git a/app/templates/formsemestre/import_from_description.j2 b/app/templates/formsemestre/import_from_description.j2
index 3a464b3a3..65f93aefa 100644
--- a/app/templates/formsemestre/import_from_description.j2
+++ b/app/templates/formsemestre/import_from_description.j2
@@ -45,4 +45,14 @@ décrits dans un fichier excel.
+
+
+
+
Description des champs du fichier excel
+
+ {% for field, descr in fields_description.items() %}
+ - {{field}} : {{descr}}
+ {% endfor %}
+
+
{% endblock %}
diff --git a/app/templates/formsemestre/import_from_description_result.j2 b/app/templates/formsemestre/import_from_description_result.j2
index 66b09d93d..e264f6fa4 100644
--- a/app/templates/formsemestre/import_from_description_result.j2
+++ b/app/templates/formsemestre/import_from_description_result.j2
@@ -14,4 +14,9 @@
+
+
{% endblock %}
diff --git a/app/views/notes_formsemestre.py b/app/views/notes_formsemestre.py
index 5b3e9d3f1..deb7b3af6 100644
--- a/app/views/notes_formsemestre.py
+++ b/app/views/notes_formsemestre.py
@@ -375,8 +375,13 @@ def formsemestres_import_from_description():
)
)
datafile = request.files[form.fichier.name]
- create_formation = form.create_formation
+ create_formation = form.create_formation.data
infos = import_from_descr.read_excel(datafile)
+ for linenum, info in enumerate(infos, start=1):
+ info["formation_commentaire"] = (
+ info.get("formation_commentaire")
+ or f"importé de {request.files[form.fichier.name].filename}, ligne {linenum}"
+ )
formsemestres = import_from_descr.create_formsemestres_from_description(
infos, create_formation=create_formation
)
@@ -384,12 +389,19 @@ def formsemestres_import_from_description():
f"formsemestres_import_from_description: {len(formsemestres)} semestres créés"
)
flash(f"Importation et création de {len(formsemestres)} semestres")
- return render_template("formsemestre/import_from_description_result.j2")
+ return render_template(
+ "formsemestre/import_from_description_result.j2",
+ formsemestres=formsemestres,
+ )
return render_template(
"formsemestre/import_from_description.j2",
title="Importation de semestres de formations monomodules",
form=form,
+ fields_description={
+ key: import_from_descr.describe_field(key)
+ for key in sorted(import_from_descr.FIELDS_BY_KEY)
+ },
)
diff --git a/migrations/versions/bc85a55e63e1_add_dispositif_descr.py b/migrations/versions/bc85a55e63e1_add_dispositif_descr.py
new file mode 100644
index 000000000..0853f6993
--- /dev/null
+++ b/migrations/versions/bc85a55e63e1_add_dispositif_descr.py
@@ -0,0 +1,33 @@
+"""add dispositif_descr
+
+Revision ID: bc85a55e63e1
+Revises: bcd959a23aea
+Create Date: 2024-12-30 18:32:55.024694
+
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "bc85a55e63e1"
+down_revision = "bcd959a23aea"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ with op.batch_alter_table(
+ "notes_formsemestre_description", schema=None
+ ) as batch_op:
+ batch_op.add_column(
+ sa.Column("dispositif_descr", sa.Text(), server_default="", nullable=False)
+ )
+
+
+def downgrade():
+ with op.batch_alter_table(
+ "notes_formsemestre_description", schema=None
+ ) as batch_op:
+ batch_op.drop_column("dispositif_descr")