forked from ScoDoc/ScoDoc
678 lines
23 KiB
Python
678 lines
23 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
#
|
|
##############################################################################
|
|
|
|
"""Photos: trombinoscopes
|
|
"""
|
|
|
|
import io
|
|
from zipfile import ZipFile, BadZipfile
|
|
from flask.templating import render_template
|
|
import reportlab
|
|
from reportlab.lib.units import cm, mm
|
|
from reportlab.platypus import Paragraph
|
|
from reportlab.platypus import Table, TableStyle
|
|
from reportlab.platypus.doctemplate import BaseDocTemplate
|
|
from reportlab.lib import styles
|
|
from reportlab.lib import colors
|
|
from PIL import Image as PILImage
|
|
|
|
import flask
|
|
from flask import url_for, g, send_file, request
|
|
|
|
from app import db, log
|
|
from app.models import Identite
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
|
from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
|
|
from app.scodoc.sco_pdf import SU
|
|
from app.scodoc import htmlutils
|
|
from app.scodoc import sco_import_etuds
|
|
from app.scodoc import sco_excel
|
|
from app.scodoc import sco_groups_view
|
|
from app.scodoc import sco_pdf
|
|
from app.scodoc import sco_photos
|
|
from app.scodoc import sco_portal_apogee
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc import sco_trombino_doc
|
|
|
|
|
|
def trombino(
|
|
group_ids=(), # liste des groupes à afficher
|
|
formsemestre_id=None, # utilisé si pas de groupes selectionné
|
|
etat=None,
|
|
fmt="html",
|
|
dialog_confirmed=False,
|
|
):
|
|
"""Trombinoscope"""
|
|
if not etat:
|
|
etat = None # may be passed as ''
|
|
# Informations sur les groupes à afficher:
|
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
|
group_ids, formsemestre_id=formsemestre_id, etat=etat
|
|
)
|
|
|
|
#
|
|
if fmt != "html" and not dialog_confirmed:
|
|
ok, dialog = check_local_photos_availability(groups_infos, fmt=fmt)
|
|
if not ok:
|
|
return dialog
|
|
|
|
if fmt == "zip":
|
|
return _trombino_zip(groups_infos)
|
|
elif fmt == "pdf":
|
|
return _trombino_pdf(groups_infos)
|
|
elif fmt == "pdflist":
|
|
return _listeappel_photos_pdf(groups_infos)
|
|
elif fmt == "doc":
|
|
return sco_trombino_doc.trombino_doc(groups_infos)
|
|
raise ValueError("invalid format")
|
|
|
|
|
|
def trombino_html(groups_infos):
|
|
"HTML snippet for trombino (with title and menu)"
|
|
menu_trombi = [
|
|
{
|
|
"title": "Charger des photos...",
|
|
"endpoint": "scolar.photos_import_files_form",
|
|
"args": {"group_ids": groups_infos.group_ids},
|
|
},
|
|
{
|
|
"title": "Obtenir archive Zip des photos",
|
|
"endpoint": "scolar.trombino",
|
|
"args": {"group_ids": groups_infos.group_ids, "fmt": "zip"},
|
|
},
|
|
{
|
|
"title": "Recopier les photos depuis le portail",
|
|
"endpoint": "scolar.trombino_copy_photos",
|
|
"args": {"group_ids": groups_infos.group_ids},
|
|
},
|
|
]
|
|
|
|
if groups_infos.members:
|
|
if groups_infos.tous_les_etuds_du_sem:
|
|
group_txt = "Tous les étudiants"
|
|
else:
|
|
group_txt = f"Groupe {groups_infos.groups_titles}"
|
|
else:
|
|
group_txt = "Aucun étudiant inscrit dans ce groupe !"
|
|
H = [
|
|
f"""<table style="padding-top: 10px; padding-bottom: 10px;">
|
|
<tr>
|
|
<td><span
|
|
style="font-style: bold; font-size: 150%%; padding-right: 20px;"
|
|
>{group_txt}</span></td>"""
|
|
]
|
|
if groups_infos.members:
|
|
H.append(
|
|
"<td>"
|
|
+ htmlutils.make_menu("Gérer les photos", menu_trombi, alone=True)
|
|
+ "</td>"
|
|
)
|
|
H.append("</tr></table>")
|
|
H.append("<div>")
|
|
i = 0
|
|
for t in groups_infos.members:
|
|
H.append(
|
|
'<span class="trombi_box"><span class="trombi-photo" id="trombi-%s">'
|
|
% t["etudid"]
|
|
)
|
|
if sco_photos.etud_photo_is_local(t["photo_filename"], size="small"):
|
|
foto = sco_photos.etud_photo_html(t, title="")
|
|
else: # la photo n'est pas immédiatement dispo
|
|
foto = f"""<span class="unloaded_img" id="{t["etudid"]
|
|
}"><img border="0" height="90" alt="en cours" src="{scu.STATIC_DIR}/icons/loading.jpg"/></span>"""
|
|
H.append(
|
|
'<a href="%s">%s</a>'
|
|
% (
|
|
url_for(
|
|
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=t["etudid"]
|
|
),
|
|
foto,
|
|
)
|
|
)
|
|
H.append("</span>")
|
|
H.append(
|
|
'<span class="trombi_legend"><span class="trombi_prenom">'
|
|
+ scu.format_prenom(t["prenom"])
|
|
+ '</span><span class="trombi_nom">'
|
|
+ scu.format_nom(t["nom"])
|
|
+ (" <i>(dem.)</i>" if t["etat"] == "D" else "")
|
|
)
|
|
H.append("</span></span></span>")
|
|
i += 1
|
|
|
|
H.append("</div>")
|
|
H.append(
|
|
f"""<div style="margin-bottom:15px;">
|
|
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
|
|
fmt='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a>
|
|
|
|
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
|
|
fmt='doc', group_ids=groups_infos.group_ids)}">Version doc</a>
|
|
</div>"""
|
|
)
|
|
return "\n".join(H)
|
|
|
|
|
|
def check_local_photos_availability(groups_infos, fmt=""):
|
|
"""Vérifie que toutes les photos (des groupes indiqués) sont copiées
|
|
localement dans ScoDoc (seules les photos dont nous disposons localement
|
|
peuvent être exportées en pdf ou en zip).
|
|
Si toutes ne sont pas dispo, retourne un dialogue d'avertissement
|
|
pour l'utilisateur.
|
|
"""
|
|
nb_missing = 0
|
|
for t in groups_infos.members:
|
|
_ = sco_photos.etud_photo_url(t) # -> copy distant files if needed
|
|
if not sco_photos.etud_photo_is_local(t["photo_filename"]):
|
|
nb_missing += 1
|
|
if nb_missing > 0:
|
|
parameters = {"group_ids": groups_infos.group_ids, "fmt": fmt}
|
|
return (
|
|
False,
|
|
scu.confirm_dialog(
|
|
f"""<p>Attention: {nb_missing} photos ne sont pas disponibles
|
|
et ne peuvent pas être exportées.</p>
|
|
<p>Vous pouvez <a class="stdlink"
|
|
href="{groups_infos.base_url}&dialog_confirmed=1&fmt={fmt}"
|
|
>exporter seulement les photos existantes</a>""",
|
|
dest_url="trombino",
|
|
OK="Exporter seulement les photos existantes",
|
|
cancel_url="groups_photos?" + groups_infos.groups_query_args,
|
|
parameters=parameters,
|
|
),
|
|
)
|
|
return True, ""
|
|
|
|
|
|
def _trombino_zip(groups_infos):
|
|
"Send photos as zip archive"
|
|
data = io.BytesIO()
|
|
with ZipFile(data, "w") as zip_file:
|
|
# assume we have the photos (or the user acknowledged the fact)
|
|
# Archive originals (not reduced) images, in JPEG
|
|
for t in groups_infos.members:
|
|
im_path = sco_photos.photo_pathname(t["photo_filename"], size="orig")
|
|
if not im_path:
|
|
continue
|
|
img = open(im_path, "rb").read()
|
|
code_nip = t["code_nip"]
|
|
if code_nip:
|
|
filename = code_nip + ".jpg"
|
|
else:
|
|
filename = f'{t["nom"]}_{t["prenom"]}_{t["etudid"]}.jpg'
|
|
zip_file.writestr(filename, img)
|
|
size = data.tell()
|
|
log(f"trombino_zip: {size} bytes")
|
|
data.seek(0)
|
|
return send_file(
|
|
data,
|
|
mimetype="application/zip",
|
|
download_name="trombi.zip",
|
|
as_attachment=True,
|
|
)
|
|
|
|
|
|
# Copy photos from portal to ScoDoc
|
|
def trombino_copy_photos(group_ids=None, dialog_confirmed=False):
|
|
"Copy photos from portal to ScoDoc (overwriting local copy)"
|
|
group_ids = [] if group_ids is None else group_ids
|
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
|
back_url = "groups_photos?" + str(groups_infos.groups_query_args)
|
|
|
|
portal_url = sco_portal_apogee.get_portal_url()
|
|
if not portal_url:
|
|
return render_template(
|
|
"sco_page.j2",
|
|
content=f"""
|
|
<p>portail non configuré</p>
|
|
<div>
|
|
<a class="stdlink" href="{back_url}" class="stdlink">
|
|
Retour au trombinoscope
|
|
</a>
|
|
</div>
|
|
""",
|
|
)
|
|
if not dialog_confirmed:
|
|
return scu.confirm_dialog(
|
|
f"""<h2>Copier les photos du portail vers ScoDoc ?</h2>
|
|
<p>Les photos du groupe {groups_infos.groups_titles} présentes
|
|
dans ScoDoc seront remplacées par celles du portail (si elles existent).
|
|
</p>
|
|
<p>(les photos sont normalement automatiquement copiées
|
|
lors de leur première utilisation, l'usage de cette fonction
|
|
n'est nécessaire que si les photos du portail ont été modifiées)
|
|
</p>
|
|
""",
|
|
dest_url="",
|
|
cancel_url=back_url,
|
|
parameters={"group_ids": group_ids},
|
|
)
|
|
|
|
msg = []
|
|
nok = 0
|
|
for etud in groups_infos.members:
|
|
path, diag = sco_photos.copy_portal_photo_to_fs(etud["etudid"])
|
|
msg.append(diag)
|
|
if path:
|
|
nok += 1
|
|
|
|
msg.append(f"<b>{nok} photos correctement chargées</b>")
|
|
|
|
return render_template(
|
|
"sco_page.j2",
|
|
content=f"""
|
|
<h2>Chargement des photos depuis le portail</h2>
|
|
<ul><li>
|
|
{ '</li><li>'.join(msg) }
|
|
</li></ul>
|
|
<div class="space-before-24">
|
|
<a class="stdlink" href="{back_url}">retour au trombinoscope</a>
|
|
</div>
|
|
""",
|
|
)
|
|
|
|
|
|
def _get_etud_platypus_image(t, image_width=2 * cm):
|
|
"""Returns a platypus object for the photo of student t"""
|
|
try:
|
|
path = sco_photos.photo_pathname(t["photo_filename"], size="small")
|
|
if not path:
|
|
# log('> unknown')
|
|
path = sco_photos.UNKNOWN_IMAGE_PATH
|
|
im = PILImage.open(path)
|
|
w0, h0 = im.size[0], im.size[1]
|
|
if w0 > h0:
|
|
W = image_width
|
|
H = h0 * W / w0
|
|
else:
|
|
H = image_width
|
|
W = w0 * H / h0
|
|
return reportlab.platypus.Image(path, width=W, height=H)
|
|
except:
|
|
log(
|
|
"*** exception while processing photo of %s (%s) (path=%s)"
|
|
% (t["nom"], t["etudid"], path)
|
|
)
|
|
raise
|
|
|
|
|
|
def _trombino_pdf(groups_infos):
|
|
"Send photos as pdf page"
|
|
# Generate PDF page
|
|
filename = f"trombino_{groups_infos.groups_filename}.pdf"
|
|
sem = groups_infos.formsemestre # suppose 1 seul semestre
|
|
|
|
PHOTO_WIDTH = 3 * cm
|
|
COL_WIDTH = 3.6 * cm
|
|
N_PER_ROW = 5 # XXX should be in ScoDoc preferences
|
|
|
|
style_sheet = styles.getSampleStyleSheet()
|
|
report = io.BytesIO() # in-memory document, no disk file
|
|
objects = [
|
|
Paragraph(
|
|
SU("Trombinoscope " + sem["titreannee"] + " " + groups_infos.groups_titles),
|
|
style_sheet["Heading3"],
|
|
)
|
|
]
|
|
L = []
|
|
n = 0
|
|
currow = []
|
|
log(f"_trombino_pdf {len(groups_infos.members)} elements")
|
|
for t in groups_infos.members:
|
|
img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
|
|
elem = Table(
|
|
[
|
|
[img],
|
|
[
|
|
Paragraph(
|
|
SU(scu.format_nomprenom(t)),
|
|
style_sheet["Normal"],
|
|
)
|
|
],
|
|
],
|
|
colWidths=[PHOTO_WIDTH],
|
|
)
|
|
currow.append(elem)
|
|
if n == (N_PER_ROW - 1):
|
|
L.append(currow)
|
|
currow = []
|
|
n = (n + 1) % N_PER_ROW
|
|
if currow:
|
|
currow += [" "] * (N_PER_ROW - len(currow))
|
|
L.append(currow)
|
|
if not L:
|
|
table = Paragraph(SU("Aucune photo à exporter !"), style_sheet["Normal"])
|
|
else:
|
|
table = Table(
|
|
L,
|
|
colWidths=[COL_WIDTH] * N_PER_ROW,
|
|
style=TableStyle(
|
|
[
|
|
# ('RIGHTPADDING', (0,0), (-1,-1), -5*mm),
|
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
|
|
]
|
|
),
|
|
)
|
|
objects.append(table)
|
|
# Build document
|
|
document = BaseDocTemplate(report)
|
|
document.addPageTemplates(
|
|
sco_pdf.ScoDocPageTemplate(
|
|
document,
|
|
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
|
|
)
|
|
)
|
|
try:
|
|
document.build(objects)
|
|
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
|
raise ScoPDFFormatError(str(exc)) from exc
|
|
report.seek(0)
|
|
return send_file(
|
|
report,
|
|
mimetype=scu.PDF_MIMETYPE,
|
|
download_name=scu.sanitize_filename(filename),
|
|
as_attachment=True,
|
|
)
|
|
|
|
|
|
# --------------------- Sur une idée de l'IUT d'Orléans:
|
|
def _listeappel_photos_pdf(groups_infos):
|
|
"Doc pdf pour liste d'appel avec photos"
|
|
filename = f"trombino_{groups_infos.groups_filename}.pdf"
|
|
sem = groups_infos.formsemestre # suppose 1 seul semestre
|
|
|
|
PHOTO_WIDTH = 2 * cm
|
|
# COLWIDTH = 3.6 * cm
|
|
# ROWS_PER_PAGE = 26 # XXX should be in ScoDoc preferences
|
|
|
|
style_sheet = styles.getSampleStyleSheet()
|
|
report = io.BytesIO() # in-memory document, no disk file
|
|
objects = [
|
|
Paragraph(
|
|
SU(
|
|
f"""{sem["titreannee"]} {groups_infos.groups_titles} ({len(groups_infos.members)})"""
|
|
),
|
|
style_sheet["Heading3"],
|
|
)
|
|
]
|
|
L = []
|
|
n = 0
|
|
currow = []
|
|
log(f"_listeappel_photos_pdf {len(groups_infos.members)} elements")
|
|
n = len(groups_infos.members)
|
|
# npages = n / 2*ROWS_PER_PAGE + 1 # nb de pages papier
|
|
# for page in range(npages):
|
|
for i in range(n): # page*2*ROWS_PER_PAGE, (page+1)*2*ROWS_PER_PAGE):
|
|
t = groups_infos.members[i]
|
|
img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
|
|
txt = Paragraph(
|
|
SU(scu.format_nomprenom(t)),
|
|
style_sheet["Normal"],
|
|
)
|
|
if currow:
|
|
currow += [""]
|
|
currow += [img, txt, ""]
|
|
if i % 2:
|
|
L.append(currow)
|
|
currow = []
|
|
if currow:
|
|
currow += [" "] * 3
|
|
L.append(currow)
|
|
if not L:
|
|
table = Paragraph(SU("Aucune photo à exporter !"), style_sheet["Normal"])
|
|
else:
|
|
table = Table(
|
|
L,
|
|
colWidths=[2 * cm, 4 * cm, 27 * mm, 5 * mm, 2 * cm, 4 * cm, 27 * mm],
|
|
style=TableStyle(
|
|
[
|
|
# ('RIGHTPADDING', (0,0), (-1,-1), -5*mm),
|
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
|
("GRID", (0, 0), (2, -1), 0.25, colors.grey),
|
|
("GRID", (4, 0), (-1, -1), 0.25, colors.grey),
|
|
]
|
|
),
|
|
)
|
|
objects.append(table)
|
|
# Build document
|
|
document = BaseDocTemplate(report)
|
|
document.addPageTemplates(
|
|
sco_pdf.ScoDocPageTemplate(
|
|
document,
|
|
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
|
|
)
|
|
)
|
|
try:
|
|
document.build(objects)
|
|
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
|
|
raise ScoPDFFormatError(str(exc)) from exc
|
|
data = report.getvalue()
|
|
|
|
return scu.sendPDFFile(data, filename)
|
|
|
|
|
|
# --------------------- Upload des photos de tout un groupe
|
|
def photos_generate_excel_sample(group_ids=()):
|
|
"""Feuille excel pour import fichiers photos"""
|
|
fmt = sco_import_etuds.sco_import_format()
|
|
data = sco_import_etuds.sco_import_generate_excel_sample(
|
|
fmt,
|
|
group_ids=group_ids,
|
|
only_tables=["identite"],
|
|
exclude_cols=[
|
|
"date_naissance",
|
|
"lieu_naissance",
|
|
"nationalite",
|
|
"statut",
|
|
"photo_filename",
|
|
],
|
|
extra_cols=["fichier_photo"],
|
|
)
|
|
return scu.send_file(
|
|
data, "ImportPhotos", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True
|
|
)
|
|
# return sco_excel.send_excel_file(data, "ImportPhotos" + scu.XLSX_SUFFIX)
|
|
|
|
|
|
def photos_import_files_form(group_ids=()):
|
|
"""Formulaire pour importation photos"""
|
|
if not group_ids:
|
|
raise ScoValueError("paramètre manquant !")
|
|
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
|
back_url = f"groups_photos?{groups_infos.groups_query_args}"
|
|
|
|
H = [
|
|
f"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
|
|
<p><b>Vous pouvez aussi charger les photos individuellement via la fiche
|
|
de chaque étudiant (menu "Étudiant" / "Changer la photo").</b>
|
|
</p>
|
|
<p class="help">Cette page permet de charger en une seule fois les photos
|
|
de plusieurs étudiants.<br>
|
|
Il faut d'abord remplir une feuille excel donnant les noms
|
|
des fichiers images (une image par étudiant).
|
|
</p>
|
|
<p class="help">Ensuite, réunir vos images dans un fichier zip, puis télécharger
|
|
simultanément le fichier excel et le fichier zip.
|
|
</p>
|
|
<ol>
|
|
<li><a class="stdlink" href="photos_generate_excel_sample?{groups_infos.groups_query_args}">
|
|
Obtenir la feuille excel à remplir</a>
|
|
</li>
|
|
<li style="padding-top: 2em;">
|
|
""",
|
|
]
|
|
vals = scu.get_request_args()
|
|
vals["group_ids"] = groups_infos.group_ids
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
vals,
|
|
(
|
|
("xlsfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}),
|
|
("zipfile", {"title": "Fichier zip:", "input_type": "file", "size": 40}),
|
|
("group_ids", {"input_type": "hidden", "type": "list"}),
|
|
),
|
|
)
|
|
|
|
if tf[0] == 0:
|
|
return render_template(
|
|
"sco_page.j2",
|
|
title="Import des photos des étudiants",
|
|
content="\n".join(H) + tf[1] + "</li></ol>",
|
|
)
|
|
if tf[0] == -1:
|
|
return flask.redirect(back_url)
|
|
|
|
def callback(etud: Identite, data, filename):
|
|
return sco_photos.store_photo(etud, data, filename)
|
|
|
|
(
|
|
ignored_zipfiles,
|
|
unmatched_files,
|
|
stored_etud_filename,
|
|
) = zip_excel_import_files(
|
|
xlsfile=tf[2]["xlsfile"],
|
|
zipfile=tf[2]["zipfile"],
|
|
callback=callback,
|
|
filename_title="fichier_photo",
|
|
back_url=back_url,
|
|
)
|
|
return render_template(
|
|
"scolar/photos_import_files.j2",
|
|
page_title="Téléchargement des photos des étudiants",
|
|
ignored_zipfiles=ignored_zipfiles,
|
|
unmatched_files=unmatched_files,
|
|
stored_etud_filename=stored_etud_filename,
|
|
next_page=url_for(
|
|
"scolar.groups_photos",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=groups_infos.formsemestre_id,
|
|
),
|
|
)
|
|
|
|
|
|
def _norm_zip_filename(fn, lowercase=True):
|
|
"normalisation used to match filenames"
|
|
fn = fn.replace("\\", "/") # not sure if this is necessary ?
|
|
fn = fn.strip()
|
|
if lowercase:
|
|
fn = fn.lower()
|
|
fn = fn.split("/")[-1] # use only last component, not directories
|
|
return fn
|
|
|
|
|
|
def zip_excel_import_files(
|
|
xlsfile=None,
|
|
zipfile=None,
|
|
callback=None,
|
|
filename_title="", # doit obligatoirement etre specifié
|
|
back_url=None,
|
|
):
|
|
"""Importation de fichiers à partir d'un excel et d'un zip
|
|
La fonction
|
|
callback()
|
|
est appelée pour chaque fichier trouvé.
|
|
Fonction utilisée pour les photos et les fichiers étudiants (archives).
|
|
"""
|
|
# 1- build mapping etudid -> filename
|
|
exceldata = xlsfile.read()
|
|
if not exceldata:
|
|
raise ScoValueError("Fichier excel vide ou invalide")
|
|
_, data = sco_excel.excel_bytes_to_list(exceldata)
|
|
if not data:
|
|
raise ScoValueError("Fichier excel vide !")
|
|
# on doit avoir une colonne etudid et une colonne filename_title ('fichier_photo')
|
|
titles = data[0]
|
|
try:
|
|
etudid_idx = titles.index("etudid")
|
|
filename_idx = titles.index(filename_title)
|
|
except Exception as exc:
|
|
raise ScoValueError(
|
|
f"Fichier excel incorrect (il faut une colonne etudid et une colonne {filename_title}) !"
|
|
) from exc
|
|
|
|
filename_to_etudid = {} # filename : etudid
|
|
for line_num, l in enumerate(data[1:]):
|
|
filename = l[filename_idx].strip()
|
|
if filename:
|
|
try:
|
|
filename_to_etudid[_norm_zip_filename(filename)] = int(l[etudid_idx])
|
|
except ValueError as exc:
|
|
raise ScoValueError(
|
|
f"etudid invalide ({l[etudid_idx]}) sur ligne {line_num+1} !",
|
|
dest_url=back_url,
|
|
) from exc
|
|
|
|
# 2- Ouvre le zip et
|
|
try:
|
|
z = ZipFile(zipfile)
|
|
except BadZipfile:
|
|
raise ScoValueError("Fichier ZIP incorrect !") from BadZipfile
|
|
ignored_zipfiles = []
|
|
stored_etud_filename = [] # [ (etud, filename) ]
|
|
for name in z.namelist():
|
|
if len(name) > 4 and name[-1] != "/" and "." in name:
|
|
try:
|
|
data = z.read(name)
|
|
except BadZipfile as exc:
|
|
raise ScoValueError(
|
|
f"Fichier Zip incorrect: erreur sur {name}", dest_url=back_url
|
|
) from exc
|
|
# match zip filename with name given in excel
|
|
normname = _norm_zip_filename(name)
|
|
if normname in filename_to_etudid:
|
|
etudid = filename_to_etudid[normname]
|
|
# ok, store photo
|
|
etud: Identite = db.session.get(Identite, etudid)
|
|
if not etud:
|
|
raise ScoValueError(
|
|
f"ID étudiant invalide: {etudid}", dest_url=back_url
|
|
)
|
|
del filename_to_etudid[normname]
|
|
status, err_msg = callback(
|
|
etud,
|
|
data,
|
|
_norm_zip_filename(name, lowercase=False),
|
|
)
|
|
if not status:
|
|
raise ScoValueError(f"Erreur: {err_msg}", dest_url=back_url)
|
|
stored_etud_filename.append((etud, name))
|
|
else:
|
|
log(f"zip: zip name {name} not in excel !")
|
|
ignored_zipfiles.append(name)
|
|
else:
|
|
if name[-1] != "/":
|
|
ignored_zipfiles.append(name)
|
|
log(f"zip: ignoring {name}")
|
|
if filename_to_etudid:
|
|
# lignes excel non traitées
|
|
unmatched_files = list(filename_to_etudid.keys())
|
|
else:
|
|
unmatched_files = []
|
|
return ignored_zipfiles, unmatched_files, stored_etud_filename
|