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 @@ +
    + Accueil semestres +
    + {% 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")