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"),
|
FileAllowed(["xlsx"], "Fichier .xlsx uniquement"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
image_archive_file = FileField(
|
||||||
|
"Fichier zip avec les images",
|
||||||
|
validators=[
|
||||||
|
FileAllowed(["zip"], "Fichier .zip uniquement"),
|
||||||
|
],
|
||||||
|
)
|
||||||
create_formation = BooleanField(
|
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")
|
submit = SubmitField("Importer et créer les formations")
|
||||||
cancel = SubmitField("Annuler")
|
cancel = SubmitField("Annuler")
|
||||||
|
@ -91,7 +91,7 @@ FORMSEMESTRE_FIELDS = (
|
|||||||
FieldDescr("titre", "titre du semestre (si vide, sera déduit de la formation)"),
|
FieldDescr("titre", "titre du semestre (si vide, sera déduit de la formation)"),
|
||||||
FieldDescr(
|
FieldDescr(
|
||||||
"capacite_accueil",
|
"capacite_accueil",
|
||||||
"capacité d'accueil (nombre ou vide)",
|
"capacité d'accueil (nombre ou vide).",
|
||||||
type="int",
|
type="int",
|
||||||
default=None,
|
default=None,
|
||||||
),
|
),
|
||||||
@ -102,7 +102,7 @@ FORMSEMESTRE_FIELDS = (
|
|||||||
"date_fin", "date fin des cours du semestre", type="date", optional=False
|
"date_fin", "date fin des cours du semestre", type="date", optional=False
|
||||||
),
|
),
|
||||||
FieldDescr("edt_id", "identifiant emplois du temps (optionnel)"),
|
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("modalite", "modalité de formation: 'FI', 'FAP', 'FC'", default="FI"),
|
||||||
FieldDescr(
|
FieldDescr(
|
||||||
"elt_sem_apo",
|
"elt_sem_apo",
|
||||||
@ -121,15 +121,15 @@ FORMSEMESTRE_DESCR_FIELDS = (
|
|||||||
),
|
),
|
||||||
FieldDescr(
|
FieldDescr(
|
||||||
"descr_horaire",
|
"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(
|
FieldDescr(
|
||||||
"descr_date_debut_inscriptions",
|
"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",
|
type="datetime",
|
||||||
),
|
),
|
||||||
FieldDescr(
|
FieldDescr(
|
||||||
"descr_date_fin_inscriptions", "Date de fin des inscriptions", type="datetime"
|
"descr_date_fin_inscriptions", "Date de fin des inscriptions.", type="datetime"
|
||||||
),
|
),
|
||||||
FieldDescr(
|
FieldDescr(
|
||||||
"descr_wip",
|
"descr_wip",
|
||||||
@ -137,12 +137,16 @@ FORMSEMESTRE_DESCR_FIELDS = (
|
|||||||
type="bool",
|
type="bool",
|
||||||
default=False,
|
default=False,
|
||||||
),
|
),
|
||||||
FieldDescr("descr_image", "image illustrant cette formation.", type="image"),
|
FieldDescr(
|
||||||
FieldDescr("descr_campus", "campus, par ex. Villetaneuse."),
|
"descr_image",
|
||||||
FieldDescr("descr_salle", "salle"),
|
"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(
|
FieldDescr(
|
||||||
"descr_dispositif",
|
"descr_dispositif",
|
||||||
"modalité de formation: 0 présentiel, 1 online, 2 hybride.",
|
"modalité de formation: 0 présentiel, 1 online, 2 hybride",
|
||||||
type="int",
|
type="int",
|
||||||
default=0,
|
default=0,
|
||||||
),
|
),
|
||||||
@ -151,19 +155,19 @@ FORMSEMESTRE_DESCR_FIELDS = (
|
|||||||
),
|
),
|
||||||
FieldDescr(
|
FieldDescr(
|
||||||
"descr_modalites_mcc",
|
"descr_modalites_mcc",
|
||||||
"modalités de contrôle des connaissances",
|
"modalités de contrôle des connaissances.",
|
||||||
allow_html=True,
|
allow_html=True,
|
||||||
),
|
),
|
||||||
FieldDescr(
|
FieldDescr(
|
||||||
"descr_photo_ens",
|
"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",
|
type="image",
|
||||||
),
|
),
|
||||||
FieldDescr("descr_public", "public visé"),
|
FieldDescr("descr_public", "public visé"),
|
||||||
FieldDescr("descr_prerequis", "prérequis", allow_html=True),
|
FieldDescr("descr_prerequis", "prérequis", allow_html=True),
|
||||||
FieldDescr(
|
FieldDescr(
|
||||||
"descr_responsable",
|
"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,
|
allow_html=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -178,7 +182,7 @@ def describe_field(key: str) -> str:
|
|||||||
if not FIELDS_BY_KEY:
|
if not FIELDS_BY_KEY:
|
||||||
FIELDS_BY_KEY.update({field.key: field for field in ALL_FIELDS})
|
FIELDS_BY_KEY.update({field.key: field for field in ALL_FIELDS})
|
||||||
field = FIELDS_BY_KEY[key]
|
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():
|
def generate_sample():
|
||||||
@ -207,9 +211,7 @@ def check_and_convert(value, field: FieldDescr):
|
|||||||
case "int":
|
case "int":
|
||||||
return int(value)
|
return int(value)
|
||||||
case "image":
|
case "image":
|
||||||
return None # XXX ignore
|
return str(value).strip() # image path
|
||||||
if value: # WIP
|
|
||||||
raise NotImplementedError("image import from Excel not implemented")
|
|
||||||
case "bool":
|
case "bool":
|
||||||
return scu.to_bool(value)
|
return scu.to_bool(value)
|
||||||
case "date":
|
case "date":
|
||||||
@ -304,13 +306,14 @@ def _create_formation_and_modimpl(data) -> Formation:
|
|||||||
|
|
||||||
|
|
||||||
def create_formsemestre_from_description(
|
def create_formsemestre_from_description(
|
||||||
data: dict, create_formation=False
|
data: dict, create_formation=False, images: dict | None = None
|
||||||
) -> FormSemestre:
|
) -> FormSemestre:
|
||||||
"""Create from fields in data.
|
"""Create from fields in data.
|
||||||
- Search formation: if needed and create_formation, create it;
|
- Search formation: if needed and create_formation, create it;
|
||||||
- Create formsemestre
|
- Create formsemestre
|
||||||
- Create formsemestre description
|
- Create formsemestre description
|
||||||
"""
|
"""
|
||||||
|
images = images or {}
|
||||||
created = [] # list of created objects XXX unused
|
created = [] # list of created objects XXX unused
|
||||||
user = current_user # resp. semestre et module
|
user = current_user # resp. semestre et module
|
||||||
formation = (
|
formation = (
|
||||||
@ -365,13 +368,15 @@ def create_formsemestre_from_description(
|
|||||||
|
|
||||||
|
|
||||||
def create_formsemestres_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]:
|
) -> list[FormSemestre]:
|
||||||
"Creation de tous les semestres mono-modules"
|
"Creation de tous les semestres mono-modules"
|
||||||
log(
|
log(
|
||||||
f"create_formsemestres_from_description: {len(infos)} items, create_formation={create_formation}"
|
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, images=images
|
||||||
|
)
|
||||||
for data in infos
|
for data in infos
|
||||||
]
|
]
|
||||||
|
@ -18,7 +18,8 @@ décrits dans un fichier excel. </p>
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li style="margin-bottom:8px;">Vous ajoutez une ligne par semestre/formation
|
<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>
|
||||||
|
|
||||||
<li style="margin-bottom:32px;">Revenez sur cette page et chargez le fichier dans ScoDoc.
|
<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 datetime
|
||||||
import io
|
import io
|
||||||
|
import zipfile
|
||||||
|
|
||||||
from flask import flash, redirect, render_template, url_for
|
from flask import flash, redirect, render_template, url_for
|
||||||
from flask import current_app, g, request
|
from flask import current_app, g, request
|
||||||
@ -375,15 +376,18 @@ def formsemestres_import_from_description():
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
datafile = request.files[form.fichier.name]
|
datafile = request.files[form.fichier.name]
|
||||||
|
image_archive_file = request.files[form.image_archive_file.name]
|
||||||
create_formation = form.create_formation.data
|
create_formation = form.create_formation.data
|
||||||
infos = import_from_descr.read_excel(datafile)
|
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):
|
for linenum, info in enumerate(infos, start=1):
|
||||||
info["formation_commentaire"] = (
|
info["formation_commentaire"] = (
|
||||||
info.get("formation_commentaire")
|
info.get("formation_commentaire")
|
||||||
or f"importé de {request.files[form.fichier.name].filename}, ligne {linenum}"
|
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, images=images
|
||||||
)
|
)
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
f"formsemestres_import_from_description: {len(formsemestres)} semestres créés"
|
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"])
|
@bp.route("/formsemestres/import_from_descr_sample", methods=["GET", "POST"])
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user