# -*- coding: utf-8 -*- ############################################################################## # # ScoDoc # # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@viennet.net # ############################################################################## """ Importation directe de formsemestres depuis un fichier excel donnant leurs paramètres et description. (2024 pour EL) - Formation: indiquée par ("dept", "formation_acronyme", "formation_titre", "formation_version") - Modules: ces formsemestres ne prennent que le premier module de la formation à laquelle ils sont rattachés. Les champs sont définis ci-dessous, pour chaque objet: Formation, FormSemestre, FormSemestreDescription. """ from collections import namedtuple import datetime from flask import g from flask_login import current_user from app import db, log from app.models import ( Formation, FormSemestre, FormSemestreDescription, Matiere, Module, ModuleImpl, UniteEns, ) from app.scodoc import sco_excel from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ScoValueError # Définition des champs FieldDescr = namedtuple( "DescrField", ("key", "description", "optional", "default", "type", "allow_html"), defaults=(None, "", True, "", "str", False), ) # --- Formation FORMATION_FIELDS = ( FieldDescr("formation_acronyme", "acronyme de la formation", optional=False), FieldDescr("formation_titre", "titre de la formation", optional=False), FieldDescr("formation_version", "version de la formation", optional=False), ) # --- FormSemestre FORMSEMESTRE_FIELDS = ( FieldDescr( "semestre_id", "indice du semestre dans la formation", optional=False, type="int", ), FieldDescr("titre", "titre du semestre (si vide, sera déduit de la formation)"), FieldDescr( "capacite_accueil", "capacité d'accueil (nombre ou vide)", type="int", default=None, ), FieldDescr( "date_debut", "date début des cours du semestre", type="date", optional=False ), FieldDescr( "date_fin", "date fin des cours du semestre", type="date", optional=False ), FieldDescr("edt_id", "identifiant emplois du temps (optionnel)"), FieldDescr("etat", "déverrouillage", type="bool", default=True), FieldDescr("modalite", "modalité de formation: 'FI', 'FAP', 'FC'", default="FI"), FieldDescr( "elt_sem_apo", "code(s) Apogée élement semestre, eg 'VRTW1' ou 'V2INCS4,V2INLS4'", ), FieldDescr("elt_annee_apo", "code(s) Apogée élement année"), FieldDescr("elt_passage_apo", "code(s) Apogée élement passage"), ) # --- Description externe (FormSemestreDescription) # --- champs préfixés par "descr_" FORMSEMESTRE_DESCR_FIELDS = ( FieldDescr( "descr_description", """description du cours: informations sur le contenu, les objectifs, les modalités d'évaluation, etc.""", allow_html=True, ), FieldDescr( "descr_horaire", "indication sur l'horaire, texte libre, ex.: les lundis 9h-12h.", ), FieldDescr( "descr_date_debut_inscriptions", "Date d'ouverture des inscriptions (laisser vide pour autoriser tout le temps).", type="datetime", ), FieldDescr( "descr_date_fin_inscriptions", "Date de fin des inscriptions", type="datetime" ), FieldDescr( "descr_wip", "work in progress: si vrai, affichera juste le titre du semestre", type="bool", default=False, ), FieldDescr("descr_image", "image illustrant cette formation.", type="image"), FieldDescr("descr_campus", "campus, par ex. Villetaneuse."), FieldDescr("descr_salle", "salle"), FieldDescr( "descr_dispositif", "modalité de formation: 0 présentiel, 1 online, 2 hybride.", type="int", default=0, ), FieldDescr( "descr_dispositif_descr", "décrit modalités de formation", allow_html=True ), FieldDescr( "descr_modalites_mcc", "modalités de contrôle des connaissances", allow_html=True, ), FieldDescr( "descr_photo_ens", "photo de l'enseignant(e) ou autre illustration", type="image", ), FieldDescr("descr_public", "public visé"), FieldDescr("descr_prerequis", "prérequis", allow_html=True), FieldDescr( "descr_responsable", "responsable du cours ou personne chargée de l'organisation du semestre.", allow_html=True, ), ) ALL_FIELDS = FORMATION_FIELDS + FORMSEMESTRE_FIELDS + FORMSEMESTRE_DESCR_FIELDS FIELDS_BY_KEY = {} def describe_field(key: str) -> str: """texte aide décrivant ce champ""" if not FIELDS_BY_KEY: FIELDS_BY_KEY.update({field.key: field for field in ALL_FIELDS}) field = FIELDS_BY_KEY[key] return field.description + (" HTML autorisé." if field.allow_html else "") def generate_sample(): """Generate excel xlsx for import""" titles = [fs.key for fs in ALL_FIELDS] comments = [fs.description for fs in ALL_FIELDS] style = sco_excel.excel_make_style(bold=True) titles_styles = [style] * len(titles) return sco_excel.excel_simple_table( titles=titles, titles_styles=titles_styles, sheet_name="Import semestres", comments=comments, ) def check_and_convert(value, field: FieldDescr): match field.type: case "str": return str(value).strip() case "int": return int(value) case "image": if value: raise NotImplementedError("image import from Excel not implemented") case "bool": return scu.to_bool(value) case "date": if isinstance(value, datetime.date): return value if isinstance(value, datetime.datetime): return value.date if isinstance(value, str): return datetime.date.fromisoformat(value) raise ValueError(f"invalid date for {field.key}") case "datetime": if isinstance(value, datetime.datetime): return value if isinstance(value, str): return datetime.date.fromisoformat(value) raise ValueError(f"invalid datetime for {field.key}") raise NotImplementedError(f"unimplemented type {field.type} for field {field.key}") def read_excel(datafile) -> list[dict]: "lecture fichier excel import formsemestres" 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 field in ALL_FIELDS: if field.key not in row: if field.optional: row[field.key] = field.default else: raise ScoValueError( f"Ligne {line_num}, colonne {field.key}: valeur requise" ) else: try: 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}" ) from exc log(diag) # XXX return rows 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["dept_id"] = g.scodoc_dept_id formation = Formation.create_from_dict(args) ue = UniteEns.create_from_dict( { "formation": formation, "acronyme": f"UE {data['formation_acronyme']}", "titre": data["formation_titre"], } ) matiere = Matiere.create_from_dict( { "ue": ue, "titre": data["formation_titre"], } ) module = Module.create_from_dict( { "ue": ue, "formation": formation, "matiere": matiere, "titre": data["formation_titre"], "abbrev": data["formation_titre"], "code": data["formation_acronyme"], } ) return formation def create_formsemestre_from_description( data: dict, create_formation=False ) -> FormSemestre: """Create from fields in data. - Search formation: if needed and create_formation, create it; - Create formsemestre - Create formsemestre description """ created = [] # list of created objects XXX unused user = current_user # resp. semestre et module formation = ( db.session.query(Formation) .filter_by( dept_id=g.scodoc_dept_id, acronyme=data["formation_acronyme"], titre=data["formation_titre"], version=data["formation_version"], ) .first() ) if not formation: if not create_formation: raise ScoValueError("formation inexistante dans ce département") formation = _create_formation_and_modimpl(data) created.append(formation) # Détermine le module à placer dans le formsemestre module = formation.modules.first() if not module: raise ScoValueError( f"La formation {formation.get_titre_version()} n'a aucun module" ) # --- FormSemestre args = {field.key: data[field.key] for field in FORMSEMESTRE_FIELDS} args["dept_id"] = g.scodoc_dept_id args["formation_id"] = formation.id args["responsables"] = [user] formsemestre = FormSemestre.create_formsemestre(data) modimpl = ModuleImpl.create_from_dict( { "module_id": module.id, "formsemestre_id": formsemestre.id, "responsable_id": user.id, } ) # --- FormSemestreDescription args = { field.key[6:] if field.key.startswith("descr_") else field.key: data[field.key] for field in FORMSEMESTRE_DESCR_FIELDS } args["formsemestre_id"] = formsemestre.id formsemestre_descr = FormSemestreDescription.create_from_dict(args) # db.session.commit() return formsemestre def create_formsemestres_from_description( infos: list[dict], create_formation=False ) -> list[FormSemestre]: "Creation de tous les semestres mono-modules" return [ create_formsemestre_from_description(data, create_formation=create_formation) for data in infos ]