forked from ScoDoc/ScoDoc
383 lines
13 KiB
Python
383 lines
13 KiB
Python
# -*- 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, url_for
|
|
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.codes_cursus import CodesCursus
|
|
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=True, default=1
|
|
),
|
|
FieldDescr(
|
|
"formation_commentaire",
|
|
"commentaire à usage interne",
|
|
optional=True,
|
|
default="",
|
|
),
|
|
)
|
|
# --- 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 (en excel, nom du fichier dans le zip associé)",
|
|
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 (en excel, nom du fichier dans le zip associé)",
|
|
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):
|
|
if value is None or value == "":
|
|
if not field.optional:
|
|
raise ValueError(f"champs {field.key} requis")
|
|
return field.default
|
|
match field.type:
|
|
case "str":
|
|
return str(value).strip()
|
|
case "int":
|
|
return int(value)
|
|
case "image":
|
|
return str(value).strip() # image path
|
|
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):
|
|
try:
|
|
return datetime.date.fromisoformat(value)
|
|
except ValueError:
|
|
# try datetime
|
|
return datetime.datetime.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.datetime.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=2):
|
|
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}",
|
|
dest_label="Reprendre",
|
|
dest_url=url_for(
|
|
"notes.formsemestres_import_from_description",
|
|
scodoc_dept=g.scodoc_dept,
|
|
),
|
|
) 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.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(
|
|
{
|
|
"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"],
|
|
"coefficient": 1.0,
|
|
}
|
|
)
|
|
return formation
|
|
|
|
|
|
def create_formsemestre_from_description(
|
|
data: dict, create_formation=False, images: dict | None = None
|
|
) -> FormSemestre:
|
|
"""Create from fields in data.
|
|
- Search formation: if needed and create_formation, create it;
|
|
- Create formsemestre
|
|
- Create formsemestre description
|
|
"""
|
|
images = images or {}
|
|
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)
|
|
db.session.flush()
|
|
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]
|
|
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,
|
|
"formsemestre_id": formsemestre.id,
|
|
"responsable_id": user.id,
|
|
}
|
|
)
|
|
# --- FormSemestreDescription
|
|
args = {
|
|
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)
|
|
#
|
|
db.session.commit()
|
|
return formsemestre
|
|
|
|
|
|
def create_formsemestres_from_description(
|
|
infos: list[dict], create_formation: bool = False, images: dict | None = None
|
|
) -> 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, images=images
|
|
)
|
|
for data in infos
|
|
]
|