WIP: fonctions d'import de semestres monomodules, suite

This commit is contained in:
ilona 2024-12-30 19:34:47 +01:00
parent 6db0aa36cd
commit f4b995c9d2
10 changed files with 143 additions and 25 deletions

View File

@ -911,7 +911,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
""" """
) )
if formsemestres: if not formsemestres:
H.append( H.append(
f""" f"""
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{

View File

@ -41,7 +41,7 @@ Les champs sont définis ci-dessous, pour chaque objet:
from collections import namedtuple from collections import namedtuple
import datetime import datetime
from flask import g from flask import g, url_for
from flask_login import current_user from flask_login import current_user
from app import db, log from app import db, log
@ -56,6 +56,7 @@ from app.models import (
) )
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import CodesCursus
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
# Définition des champs # Définition des champs
@ -69,7 +70,15 @@ FieldDescr = namedtuple(
FORMATION_FIELDS = ( FORMATION_FIELDS = (
FieldDescr("formation_acronyme", "acronyme de la formation", optional=False), FieldDescr("formation_acronyme", "acronyme de la formation", optional=False),
FieldDescr("formation_titre", "titre de la formation", optional=False), FieldDescr("formation_titre", "titre de la formation", optional=False),
FieldDescr("formation_version", "version 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
FORMSEMESTRE_FIELDS = ( FORMSEMESTRE_FIELDS = (
@ -188,13 +197,18 @@ def generate_sample():
def check_and_convert(value, field: FieldDescr): 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: match field.type:
case "str": case "str":
return str(value).strip() return str(value).strip()
case "int": case "int":
return int(value) return int(value)
case "image": case "image":
if value: return None # XXX ignore
if value: # WIP
raise NotImplementedError("image import from Excel not implemented") raise NotImplementedError("image import from Excel not implemented")
case "bool": case "bool":
return scu.to_bool(value) return scu.to_bool(value)
@ -204,13 +218,17 @@ def check_and_convert(value, field: FieldDescr):
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
return value.date return value.date
if isinstance(value, str): if isinstance(value, str):
try:
return datetime.date.fromisoformat(value) return datetime.date.fromisoformat(value)
except ValueError:
# try datetime
return datetime.datetime.fromisoformat(value)
raise ValueError(f"invalid date for {field.key}") raise ValueError(f"invalid date for {field.key}")
case "datetime": case "datetime":
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
return value return value
if isinstance(value, str): if isinstance(value, str):
return datetime.date.fromisoformat(value) return datetime.datetime.fromisoformat(value)
raise ValueError(f"invalid datetime for {field.key}") raise ValueError(f"invalid datetime for {field.key}")
raise NotImplementedError(f"unimplemented type {field.type} for field {field.key}") raise NotImplementedError(f"unimplemented type {field.type} for field {field.key}")
@ -220,7 +238,7 @@ def read_excel(datafile) -> list[dict]:
exceldata = datafile.read() exceldata = datafile.read()
diag, rows = sco_excel.excel_bytes_to_dict(exceldata) diag, rows = sco_excel.excel_bytes_to_dict(exceldata)
# check and convert types # 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: for field in ALL_FIELDS:
if field.key not in row: if field.key not in row:
if field.optional: if field.optional:
@ -234,7 +252,12 @@ def read_excel(datafile) -> list[dict]:
row[field.key] = check_and_convert(row[field.key], field) row[field.key] = check_and_convert(row[field.key], field)
except ValueError as exc: except ValueError as exc:
raise ScoValueError( 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 ) from exc
log(diag) # XXX log(diag) # XXX
return rows return rows
@ -242,8 +265,16 @@ def read_excel(datafile) -> list[dict]:
def _create_formation_and_modimpl(data) -> Formation: def _create_formation_and_modimpl(data) -> Formation:
"""Create a new formation, with a UE and module""" """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 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) formation = Formation.create_from_dict(args)
ue = UniteEns.create_from_dict( ue = UniteEns.create_from_dict(
{ {
@ -266,6 +297,7 @@ def _create_formation_and_modimpl(data) -> Formation:
"titre": data["formation_titre"], "titre": data["formation_titre"],
"abbrev": data["formation_titre"], "abbrev": data["formation_titre"],
"code": data["formation_acronyme"], "code": data["formation_acronyme"],
"coefficient": 1.0,
} }
) )
return formation return formation
@ -295,6 +327,7 @@ def create_formsemestre_from_description(
if not create_formation: if not create_formation:
raise ScoValueError("formation inexistante dans ce département") raise ScoValueError("formation inexistante dans ce département")
formation = _create_formation_and_modimpl(data) formation = _create_formation_and_modimpl(data)
db.session.flush()
created.append(formation) created.append(formation)
# Détermine le module à placer dans le formsemestre # Détermine le module à placer dans le formsemestre
module = formation.modules.first() module = formation.modules.first()
@ -307,7 +340,9 @@ def create_formsemestre_from_description(
args["dept_id"] = g.scodoc_dept_id args["dept_id"] = g.scodoc_dept_id
args["formation_id"] = formation.id args["formation_id"] = formation.id
args["responsables"] = [user] 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( modimpl = ModuleImpl.create_from_dict(
{ {
"module_id": module.id, "module_id": module.id,
@ -317,9 +352,11 @@ def create_formsemestre_from_description(
) )
# --- FormSemestreDescription # --- FormSemestreDescription
args = { 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 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 args["formsemestre_id"] = formsemestre.id
formsemestre_descr = FormSemestreDescription.create_from_dict(args) formsemestre_descr = FormSemestreDescription.create_from_dict(args)
# #
@ -328,9 +365,12 @@ def create_formsemestre_from_description(
def create_formsemestres_from_description( def create_formsemestres_from_description(
infos: list[dict], create_formation=False infos: list[dict], create_formation: bool = False
) -> list[FormSemestre]: ) -> list[FormSemestre]:
"Creation de tous les semestres mono-modules" "Creation de tous les semestres mono-modules"
log(
f"create_formsemestres_from_description: {len(infos)} items, create_formation={create_formation}"
)
return [ return [
create_formsemestre_from_description(data, create_formation=create_formation) create_formsemestre_from_description(data, create_formation=create_formation)
for data in infos for data in infos

View File

@ -381,12 +381,14 @@ class Matiere(ScoDocModel):
@classmethod @classmethod
def create_from_dict(cls, data: dict) -> "Matiere": def create_from_dict(cls, data: dict) -> "Matiere":
"""Create matière from dict. Log, news, cache. """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. Commit session.
""" """
# check ue # check ue
if data.get("ue_id") is None: if data.get("ue_id") is None:
raise ScoValueError("UE id missing") if data.get("ue") is None:
raise ScoValueError("UE missing")
else: # check ue_id
_ = UniteEns.get_ue(data["ue_id"]) _ = UniteEns.get_ue(data["ue_id"])
mat = super().create_from_dict(data) mat = super().create_from_dict(data)
@ -394,6 +396,7 @@ class Matiere(ScoDocModel):
db.session.refresh(mat) db.session.refresh(mat)
# news # news
formation = mat.ue.formation formation = mat.ue.formation
log(f"Matiere.create_from_dict: created {mat} from {data}")
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=formation.id, obj=formation.id,

View File

@ -77,7 +77,9 @@ class ModuleImpl(ScoDocModel):
# check required args # check required args
for required_arg in ("formsemestre_id", "module_id", "responsable_id"): for required_arg in ("formsemestre_id", "module_id", "responsable_id"):
if required_arg not in data: 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"]) _ = FormSemestre.get_formsemestre(data["formsemestre_id"])
_ = Module.get_instance(data["module_id"]) _ = Module.get_instance(data["module_id"])
if not db.session.get(User, data["responsable_id"]): if not db.session.get(User, data["responsable_id"]):

View File

@ -210,14 +210,27 @@ class Module(models.ScoDocModel):
from app.models.formations import Formation from app.models.formations import Formation
# check required arguments # check required arguments
for required_arg in ("code", "formation_id", "ue_id"): if "code" not in data:
if required_arg not in data: raise ScoValueError("Module.create_from_dict: missing 'code' argument")
raise ScoValueError(f"missing argument: {required_arg}")
if not data["code"]: if not data["code"]:
raise ScoValueError("module code must be non empty") raise ScoValueError(
# Check formation "Module.create_from_dict: module code must be non empty"
formation = Formation.get_formation(data["formation_id"]) )
# 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"]) ue = UniteEns.get_ue(data["ue_id"])
# Check formation
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 # refuse de créer un module APC avec semestres semestre du module != semestre de l'UE
if formation.is_apc(): if formation.is_apc():
if int(data.get("semestre_id", 1)) != ue.semestre_idx: if int(data.get("semestre_id", 1)) != ue.semestre_idx:

View File

@ -544,7 +544,7 @@ def excel_bytes_to_dict(
keys = [k.strip().lower() for k in rows[0]] keys = [k.strip().lower() for k in rows[0]]
else: else:
keys = [k.strip() for k in rows[0]] 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]]: def excel_file_to_list(filelike) -> tuple[list, list[list]]:

View File

@ -45,4 +45,14 @@ décrits dans un fichier excel. </p>
</div> </div>
</div> </div>
</div> </div>
<div class="scobox help explanation">
<div class="scobox-title">Description des champs du fichier excel</div>
<ul>
{% for field, descr in fields_description.items() %}
<li><tt>{{field}}</tt>&nbsp;: <span>{{descr}}</span></li>
{% endfor %}
</ul>
</div>
{% endblock %} {% endblock %}

View File

@ -14,4 +14,9 @@
</ul> </ul>
</div> </div>
<div>
<a class="stdlink" href="{{
url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}">Accueil semestres</a>
</div>
{% endblock %} {% endblock %}

View File

@ -375,8 +375,13 @@ def formsemestres_import_from_description():
) )
) )
datafile = request.files[form.fichier.name] datafile = request.files[form.fichier.name]
create_formation = form.create_formation create_formation = form.create_formation.data
infos = import_from_descr.read_excel(datafile) 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( formsemestres = import_from_descr.create_formsemestres_from_description(
infos, create_formation=create_formation infos, create_formation=create_formation
) )
@ -384,12 +389,19 @@ def formsemestres_import_from_description():
f"formsemestres_import_from_description: {len(formsemestres)} semestres créés" f"formsemestres_import_from_description: {len(formsemestres)} semestres créés"
) )
flash(f"Importation et création de {len(formsemestres)} semestres") 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( return render_template(
"formsemestre/import_from_description.j2", "formsemestre/import_from_description.j2",
title="Importation de semestres de formations monomodules", title="Importation de semestres de formations monomodules",
form=form, form=form,
fields_description={
key: import_from_descr.describe_field(key)
for key in sorted(import_from_descr.FIELDS_BY_KEY)
},
) )

View File

@ -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")