forked from ScoDoc/ScoDoc
WIP: fonctions d'import de semestres monomodules, suite
This commit is contained in:
parent
6db0aa36cd
commit
f4b995c9d2
@ -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="{
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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"]):
|
||||||
|
@ -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:
|
||||||
|
@ -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]]:
|
||||||
|
@ -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> : <span>{{descr}}</span></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
33
migrations/versions/bc85a55e63e1_add_dispositif_descr.py
Normal file
33
migrations/versions/bc85a55e63e1_add_dispositif_descr.py
Normal 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")
|
Loading…
x
Reference in New Issue
Block a user