forked from ScoDoc/ScoDoc
WIP: fonctions d'import de semestres monomodules: import des images
This commit is contained in:
parent
df422ad1d3
commit
fd00e2f55d
@ -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")
|
||||
|
@ -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
|
||||
]
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user