WIP: fonctions d'import de semestres monomodules: import des images

This commit is contained in:
ilona 2024-12-31 20:32:13 +01:00
parent df422ad1d3
commit fd00e2f55d
4 changed files with 96 additions and 22 deletions

View File

@ -129,8 +129,14 @@ class FormSemestresImportFromDescrForm(ScoDocForm):
FileAllowed(["xlsx"], "Fichier .xlsx uniquement"),
],
)
image_archive_file = FileField(
"Fichier zip avec les images",
validators=[
FileAllowed(["zip"], "Fichier .zip uniquement"),
],
)
create_formation = BooleanField(
"Créer les programmes de formations si ils n'existent pas"
"Créer les programmes de formations s'ils n'existent pas", default=True
)
submit = SubmitField("Importer et créer les formations")
cancel = SubmitField("Annuler")

View File

@ -91,7 +91,7 @@ FORMSEMESTRE_FIELDS = (
FieldDescr("titre", "titre du semestre (si vide, sera déduit de la formation)"),
FieldDescr(
"capacite_accueil",
"capacité d'accueil (nombre ou vide)",
"capacité d'accueil (nombre ou vide).",
type="int",
default=None,
),
@ -102,7 +102,7 @@ FORMSEMESTRE_FIELDS = (
"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("etat", "déverrouillage.", type="bool", default=True),
FieldDescr("modalite", "modalité de formation: 'FI', 'FAP', 'FC'", default="FI"),
FieldDescr(
"elt_sem_apo",
@ -121,15 +121,15 @@ FORMSEMESTRE_DESCR_FIELDS = (
),
FieldDescr(
"descr_horaire",
"indication sur l'horaire, texte libre, ex.: les lundis 9h-12h.",
"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).",
"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"
"descr_date_fin_inscriptions", "Date de fin des inscriptions.", type="datetime"
),
FieldDescr(
"descr_wip",
@ -137,12 +137,16 @@ FORMSEMESTRE_DESCR_FIELDS = (
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_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.",
"modalité de formation: 0 présentiel, 1 online, 2 hybride",
type="int",
default=0,
),
@ -151,19 +155,19 @@ FORMSEMESTRE_DESCR_FIELDS = (
),
FieldDescr(
"descr_modalites_mcc",
"modalités de contrôle des connaissances",
"modalités de contrôle des connaissances.",
allow_html=True,
),
FieldDescr(
"descr_photo_ens",
"photo de l'enseignant(e) ou autre illustration",
"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.",
"responsable du cours ou personne chargée de l'organisation du semestre",
allow_html=True,
),
)
@ -178,7 +182,7 @@ def describe_field(key: str) -> str:
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 "")
return field.description + (" HTML autorisé" if field.allow_html else "")
def generate_sample():
@ -207,9 +211,7 @@ def check_and_convert(value, field: FieldDescr):
case "int":
return int(value)
case "image":
return None # XXX ignore
if value: # WIP
raise NotImplementedError("image import from Excel not implemented")
return str(value).strip() # image path
case "bool":
return scu.to_bool(value)
case "date":
@ -304,13 +306,14 @@ def _create_formation_and_modimpl(data) -> Formation:
def create_formsemestre_from_description(
data: dict, create_formation=False
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 = (
@ -365,13 +368,15 @@ def create_formsemestre_from_description(
def create_formsemestres_from_description(
infos: list[dict], create_formation: bool = False
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)
create_formsemestre_from_description(
data, create_formation=create_formation, images=images
)
for data in infos
]

View File

@ -18,7 +18,8 @@ décrits dans un fichier excel. </p>
</li>
<li style="margin-bottom:8px;">Vous ajoutez une ligne par semestre/formation
à créer, avec votre logiciel tableur préféré.
à créer, avec votre logiciel tableur préféré. En option, les colonnes images donnent
les noms complets (avec chemin) d'un fichier dans l'archive zip associée.
</li>
<li style="margin-bottom:32px;">Revenez sur cette page et chargez le fichier dans ScoDoc.

View File

@ -31,6 +31,7 @@ Emmanuel Viennet, 2023
import datetime
import io
import zipfile
from flask import flash, redirect, render_template, url_for
from flask import current_app, g, request
@ -375,15 +376,18 @@ def formsemestres_import_from_description():
)
)
datafile = request.files[form.fichier.name]
image_archive_file = request.files[form.image_archive_file.name]
create_formation = form.create_formation.data
infos = import_from_descr.read_excel(datafile)
images = _extract_images_from_zip(image_archive_file)
_load_images_refs(infos, images)
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
infos, create_formation=create_formation, images=images
)
current_app.logger.info(
f"formsemestres_import_from_description: {len(formsemestres)} semestres créés"
@ -405,6 +409,64 @@ def formsemestres_import_from_description():
)
def _extract_images_from_zip(image_archive_file) -> dict[str, bytes]:
"""Read archive file, and build dict: { path : image_data }
check that image_data is a valid image.
"""
# Image suffixes supported by PIL
exts = PIL.Image.registered_extensions()
supported_extensions = tuple(ex for ex, f in exts.items() if f in PIL.Image.OPEN)
images = {}
with zipfile.ZipFile(image_archive_file) as archive:
for file_info in archive.infolist():
if file_info.is_dir() or file_info.filename.startswith("__"):
continue
if not file_info.filename.lower().endswith(supported_extensions):
continue # ignore non image files
with archive.open(file_info) as file:
image_data = file.read()
try:
_ = PIL.Image.open(io.BytesIO(image_data))
images[file_info.filename] = image_data
except PIL.UnidentifiedImageError as exc:
current_app.logger.warning(
f"Invalid image in archive: {file_info.filename}"
)
raise ScoValueError(
f"Image invalide dans l'archive: {file_info.filename}",
dest_url=url_for(
"notes.formsemestres_import_from_description",
scodoc_dept=g.scodoc_dept,
),
dest_label="Reprendre",
) from exc
return images
def _load_images_refs(infos: list[dict], images: dict):
"""Check if all referenced images in excel (infos)
are present in the zip archive (images) and put them in the infos dicts.
"""
breakpoint()
for linenum, info in enumerate(infos, start=1):
for key in ("descr_image", "descr_photo_ens"):
info[key] = (
info[key].strip() if isinstance(info[key], str) else None
) or None
if info[key]:
if info[key] not in images:
raise ScoValueError(
f"Image référencée en ligne {linenum}, colonne {key} non trouvée dans le zip",
dest_url=url_for(
"notes.formsemestres_import_from_description",
scodoc_dept=g.scodoc_dept,
),
dest_label="Reprendre",
)
info[key] = images[info[key]]
@bp.route("/formsemestres/import_from_descr_sample", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)