Trombino: amélioration messages d'erreur. Relecture du code.

This commit is contained in:
Emmanuel Viennet 2022-09-02 20:56:55 +02:00
parent d93ce9e9be
commit fb89823c7b
6 changed files with 164 additions and 142 deletions

View File

@ -190,20 +190,23 @@ def etud_upload_file_form(etudid):
) )
def _store_etud_file_to_new_archive(etud_archive_id, data, filename, description=""): def _store_etud_file_to_new_archive(
etud_archive_id, data, filename, description=""
) -> tuple[bool, str]:
"""Store data to new archive.""" """Store data to new archive."""
filesize = len(data) filesize = len(data)
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE: if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
return 0, "Fichier image de taille invalide ! (%d)" % filesize return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})"
archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description) archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description)
EtudsArchive.store(archive_id, filename, data) EtudsArchive.store(archive_id, filename, data)
return True, "ok"
def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
"""Delete an archive""" """Delete an archive"""
# check permission # check permission
if not can_edit_etud_archive(current_user): if not can_edit_etud_archive(current_user):
raise AccessDenied("opération non autorisée pour %s" % str(current_user)) raise AccessDenied(f"opération non autorisée pour {current_user}")
etuds = sco_etud.get_etud_info(filled=True) etuds = sco_etud.get_etud_info(filled=True)
if not etuds: if not etuds:
raise ScoValueError("étudiant inexistant") raise ScoValueError("étudiant inexistant")
@ -354,7 +357,9 @@ def etudarchive_import_files(
"Importe des fichiers" "Importe des fichiers"
def callback(etud, data, filename): def callback(etud, data, filename):
_store_etud_file_to_new_archive(etud["etudid"], data, filename, description) return _store_etud_file_to_new_archive(
etud["etudid"], data, filename, description
)
# Utilise la fontion developpée au depart pour les photos # Utilise la fontion developpée au depart pour les photos
( (

View File

@ -872,6 +872,7 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
admission = ( admission = (
Admission.query.filter_by(etudid=etudid).first().to_dict(no_nulls=True) Admission.query.filter_by(etudid=etudid).first().to_dict(no_nulls=True)
) )
del admission["id"] # pour garder id == etudid dans etud
etud.update(admission) etud.update(admission)
# #
adrs = adresse_list(cnx, {"etudid": etudid}) adrs = adresse_list(cnx, {"etudid": etudid})
@ -883,6 +884,7 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
adr = adrs[0] adr = adrs[0]
if len(adrs) > 1: if len(adrs) > 1:
log("fill_etuds_info: etudid=%s a %d adresses" % (etudid, len(adrs))) log("fill_etuds_info: etudid=%s a %d adresses" % (etudid, len(adrs)))
adr.pop("id", None)
etud.update(adr) etud.update(adr)
format_etud_ident(etud) format_etud_ident(etud)

View File

@ -76,7 +76,7 @@ UNKNOWN_IMAGE_URL = "get_photo_image?etudid=" # with empty etudid => unknown fa
IMAGE_EXT = ".jpg" IMAGE_EXT = ".jpg"
JPG_QUALITY = 0.92 JPG_QUALITY = 0.92
REDUCED_HEIGHT = 90 # pixels REDUCED_HEIGHT = 90 # pixels
MAX_FILE_SIZE = 1024 * 1024 # max allowed size for uploaded image, in bytes MAX_FILE_SIZE = 4 * 1024 * 1024 # max allowed size for uploaded image, in bytes
H90 = ".h90" # suffix for reduced size images H90 = ".h90" # suffix for reduced size images
@ -244,34 +244,36 @@ def photo_pathname(photo_filename: str, size="orig"):
return False return False
def store_photo(etud, data): def store_photo(etud: dict, data, filename: str) -> tuple[bool, str]:
"""Store image for this etud. """Store image for this etud.
If there is an existing photo, it is erased and replaced. If there is an existing photo, it is erased and replaced.
data is a bytes string with image raw data. data is a bytes string with image raw data.
Update database to store filename. Update database to store filename.
Returns (status, msg) Returns (status, err_msg)
""" """
# basic checks # basic checks
filesize = len(data) filesize = len(data)
if filesize < 10 or filesize > MAX_FILE_SIZE: if filesize < 10 or filesize > MAX_FILE_SIZE:
return 0, "Fichier image de taille invalide ! (%d)" % filesize return False, f"Fichier image '{filename}' de taille invalide ! ({filesize})"
try: try:
filename = save_image(etud["etudid"], data) saved_filename = save_image(etud["etudid"], data)
except PIL.UnidentifiedImageError: except PIL.UnidentifiedImageError as exc:
raise ScoGenError(msg="Fichier d'image invalide ou non format non supporté") raise ScoGenError(
msg="Fichier d'image '{filename}' invalide ou format non supporté"
) from exc
# update database: # update database:
etud["photo_filename"] = filename etud["photo_filename"] = saved_filename
etud["foto"] = None etud["foto"] = None
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
sco_etud.identite_edit_nocheck(cnx, etud) sco_etud.identite_edit_nocheck(cnx, etud)
cnx.commit() cnx.commit()
# #
logdb(cnx, method="changePhoto", msg=filename, etudid=etud["etudid"]) logdb(cnx, method="changePhoto", msg=saved_filename, etudid=etud["etudid"])
# #
return 1, "ok" return True, "ok"
def suppress_photo(etud: Identite) -> None: def suppress_photo(etud: Identite) -> None:
@ -366,9 +368,9 @@ def copy_portal_photo_to_fs(etud):
portal_timeout = sco_preferences.get_preference("portal_timeout") portal_timeout = sco_preferences.get_preference("portal_timeout")
f = None f = None
try: try:
log("copy_portal_photo_to_fs: getting %s" % url) log(f"copy_portal_photo_to_fs: getting {url}")
r = requests.get(url, timeout=portal_timeout) r = requests.get(url, timeout=portal_timeout)
except: except Exception:
# log("download failed: exception:\n%s" % traceback.format_exc()) # log("download failed: exception:\n%s" % traceback.format_exc())
# log("called from:\n" + "".join(traceback.format_stack())) # log("called from:\n" + "".join(traceback.format_stack()))
log("copy_portal_photo_to_fs: error.") log("copy_portal_photo_to_fs: error.")
@ -378,16 +380,16 @@ def copy_portal_photo_to_fs(etud):
return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url) return None, "%s: erreur chargement de %s" % (etud["nomprenom"], url)
data = r.content # image bytes data = r.content # image bytes
try: try:
status, diag = store_photo(etud, data) status, err_msg = store_photo(etud, data, "(inconnue)")
except: except Exception:
status = 0 status = False
diag = "Erreur chargement photo du portail" err_msg = "Erreur chargement photo du portail"
log("copy_portal_photo_to_fs: failure (exception in store_photo)!") log("copy_portal_photo_to_fs: failure (exception in store_photo)!")
if status == 1: if status:
log("copy_portal_photo_to_fs: copied %s" % url) log(f"copy_portal_photo_to_fs: copied {url}")
return ( return (
photo_pathname(etud["photo_filename"]), photo_pathname(etud["photo_filename"]),
f"{etud['nomprenom']}: photo chargée", f"{etud['nomprenom']}: photo chargée",
) )
else: else:
return None, "%s: <b>%s</b>" % (etud["nomprenom"], diag) return None, "%s: <b>%s</b>" % (etud["nomprenom"], err_msg)

View File

@ -33,14 +33,10 @@ from zipfile import ZipFile, BadZipfile
from flask.templating import render_template from flask.templating import render_template
import reportlab import reportlab
from reportlab.lib.units import cm, mm from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY from reportlab.platypus import Paragraph
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak from reportlab.platypus import Table, TableStyle
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame from reportlab.platypus.doctemplate import BaseDocTemplate
from reportlab.platypus.flowables import Flowable
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import styles from reportlab.lib import styles
from reportlab.lib.colors import Color
from reportlab.lib import colors from reportlab.lib import colors
from PIL import Image as PILImage from PIL import Image as PILImage
@ -82,7 +78,7 @@ def trombino(
# #
if format != "html" and not dialog_confirmed: if format != "html" and not dialog_confirmed:
ok, dialog = check_local_photos_availability(groups_infos, format=format) ok, dialog = check_local_photos_availability(groups_infos, fmt=format)
if not ok: if not ok:
return dialog return dialog
@ -96,7 +92,6 @@ def trombino(
return sco_trombino_doc.trombino_doc(groups_infos) return sco_trombino_doc.trombino_doc(groups_infos)
else: else:
raise Exception("invalid format") raise Exception("invalid format")
# return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer()
def _trombino_html_header(): def _trombino_html_header():
@ -105,7 +100,7 @@ def _trombino_html_header():
def trombino_html(groups_infos): def trombino_html(groups_infos):
"HTML snippet for trombino (with title and menu)" "HTML snippet for trombino (with title and menu)"
menuTrombi = [ menu_trombi = [
{ {
"title": "Charger des photos...", "title": "Charger des photos...",
"endpoint": "scolar.photos_import_files_form", "endpoint": "scolar.photos_import_files_form",
@ -125,19 +120,22 @@ def trombino_html(groups_infos):
if groups_infos.members: if groups_infos.members:
if groups_infos.tous_les_etuds_du_sem: if groups_infos.tous_les_etuds_du_sem:
ng = "Tous les étudiants" group_txt = "Tous les étudiants"
else: else:
ng = "Groupe %s" % groups_infos.groups_titles group_txt = f"Groupe {groups_infos.groups_titles}"
else: else:
ng = "Aucun étudiant inscrit dans ce groupe !" group_txt = "Aucun étudiant inscrit dans ce groupe !"
H = [ H = [
'<table style="padding-top: 10px; padding-bottom: 10px;"><tr><td><span style="font-style: bold; font-size: 150%%; padding-right: 20px;">%s</span></td>' f"""<table style="padding-top: 10px; padding-bottom: 10px;">
% (ng) <tr>
<td><span
style="font-style: bold; font-size: 150%%; padding-right: 20px;"
>{group_txt}</span></td>"""
] ]
if groups_infos.members: if groups_infos.members:
H.append( H.append(
"<td>" "<td>"
+ htmlutils.make_menu("Gérer les photos", menuTrombi, alone=True) + htmlutils.make_menu("Gérer les photos", menu_trombi, alone=True)
+ "</td>" + "</td>"
) )
H.append("</tr></table>") H.append("</tr></table>")
@ -186,7 +184,7 @@ def trombino_html(groups_infos):
return "\n".join(H) return "\n".join(H)
def check_local_photos_availability(groups_infos, format=""): def check_local_photos_availability(groups_infos, fmt=""):
"""Vérifie que toutes les photos (des groupes indiqués) sont copiées """Vérifie que toutes les photos (des groupes indiqués) sont copiées
localement dans ScoDoc (seules les photos dont nous disposons localement localement dans ScoDoc (seules les photos dont nous disposons localement
peuvent être exportées en pdf ou en zip). peuvent être exportées en pdf ou en zip).
@ -199,15 +197,15 @@ def check_local_photos_availability(groups_infos, format=""):
if not sco_photos.etud_photo_is_local(t): if not sco_photos.etud_photo_is_local(t):
nb_missing += 1 nb_missing += 1
if nb_missing > 0: if nb_missing > 0:
parameters = {"group_ids": groups_infos.group_ids, "format": format} parameters = {"group_ids": groups_infos.group_ids, "format": fmt}
return ( return (
False, False,
scu.confirm_dialog( scu.confirm_dialog(
"""<p>Attention: %d photos ne sont pas disponibles et ne peuvent pas être exportées.</p><p>Vous pouvez <a class="stdlink" href="%s">exporter seulement les photos existantes</a>""" f"""<p>Attention: {nb_missing} photos ne sont pas disponibles
% ( et ne peuvent pas être exportées.</p>
nb_missing, <p>Vous pouvez <a class="stdlink"
groups_infos.base_url + "&dialog_confirmed=1&format=%s" % format, href="{groups_infos.base_url}&dialog_confirmed=1&format={fmt}"
), >exporter seulement les photos existantes</a>""",
dest_url="trombino", dest_url="trombino",
OK="Exporter seulement les photos existantes", OK="Exporter seulement les photos existantes",
cancel_url="groups_view?curtab=tab-photos&" cancel_url="groups_view?curtab=tab-photos&"
@ -215,28 +213,26 @@ def check_local_photos_availability(groups_infos, format=""):
parameters=parameters, parameters=parameters,
), ),
) )
else: return True, ""
return True, ""
def _trombino_zip(groups_infos): def _trombino_zip(groups_infos):
"Send photos as zip archive" "Send photos as zip archive"
data = io.BytesIO() data = io.BytesIO()
Z = ZipFile(data, "w") with ZipFile(data, "w") as zip_file:
# assume we have the photos (or the user acknowledged the fact) # assume we have the photos (or the user acknowledged the fact)
# Archive originals (not reduced) images, in JPEG # Archive originals (not reduced) images, in JPEG
for t in groups_infos.members: for t in groups_infos.members:
im_path = sco_photos.photo_pathname(t["photo_filename"], size="orig") im_path = sco_photos.photo_pathname(t["photo_filename"], size="orig")
if not im_path: if not im_path:
continue continue
img = open(im_path, "rb").read() img = open(im_path, "rb").read()
code_nip = t["code_nip"] code_nip = t["code_nip"]
if code_nip: if code_nip:
filename = code_nip + ".jpg" filename = code_nip + ".jpg"
else: else:
filename = f'{t["nom"]}_{t["prenom"]}_{t["etudid"]}.jpg' filename = f'{t["nom"]}_{t["prenom"]}_{t["etudid"]}.jpg'
Z.writestr(filename, img) zip_file.writestr(filename, img)
Z.close()
size = data.tell() size = data.tell()
log(f"trombino_zip: {size} bytes") log(f"trombino_zip: {size} bytes")
data.seek(0) data.seek(0)
@ -258,19 +254,22 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
header = html_sco_header.sco_header(page_title="Chargement des photos") header = html_sco_header.sco_header(page_title="Chargement des photos")
footer = html_sco_header.sco_footer() footer = html_sco_header.sco_footer()
if not portal_url: if not portal_url:
return ( return f"""{ header }
header <p>portail non configuré</p>
+ '<p>portail non configuré</p><p><a href="%s">Retour au trombinoscope</a></p>' <p><a href="{back_url}" class="stdlink">Retour au trombinoscope</a></p>
% back_url { footer }
+ footer """
)
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Copier les photos du portail vers ScoDoc ?</h2> f"""<h2>Copier les photos du portail vers ScoDoc ?</h2>
<p>Les photos du groupe %s présentes dans ScoDoc seront remplacées par celles du portail (si elles existent).</p> <p>Les photos du groupe {groups_infos.groups_titles} présentes
<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> dans ScoDoc seront remplacées par celles du portail (si elles existent).
""" </p>
% (groups_infos.groups_titles), <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="", dest_url="",
cancel_url=back_url, cancel_url=back_url,
parameters={"group_ids": group_ids}, parameters={"group_ids": group_ids},
@ -284,16 +283,16 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
if path: if path:
nok += 1 nok += 1
msg.append("<b>%d photos correctement chargées</b>" % nok) msg.append(f"<b>{nok} photos correctement chargées</b>")
return ( return f"""{ header }
header <h2>Chargement des photos depuis le portail</h2>
+ "<h2>Chargement des photos depuis le portail</h2><ul><li>" <ul><li>
+ "</li><li>".join(msg) { '</li><li>'.join(msg) }
+ "</li></ul>" </li></ul>
+ '<p><a href="%s">retour au trombinoscope</a>' % back_url <p><a href="{back_url}">retour au trombinoscope</a>
+ footer { footer }
) """
def _get_etud_platypus_image(t, image_width=2 * cm): def _get_etud_platypus_image(t, image_width=2 * cm):
@ -323,38 +322,38 @@ def _get_etud_platypus_image(t, image_width=2 * cm):
def _trombino_pdf(groups_infos): def _trombino_pdf(groups_infos):
"Send photos as pdf page" "Send photos as pdf page"
# Generate PDF page # Generate PDF page
filename = "trombino_%s" % groups_infos.groups_filename + ".pdf" filename = f"trombino_{groups_infos.groups_filename}.pdf"
sem = groups_infos.formsemestre # suppose 1 seul semestre sem = groups_infos.formsemestre # suppose 1 seul semestre
PHOTOWIDTH = 3 * cm PHOTO_WIDTH = 3 * cm
COLWIDTH = 3.6 * cm COL_WIDTH = 3.6 * cm
N_PER_ROW = 5 # XXX should be in ScoDoc preferences N_PER_ROW = 5 # XXX should be in ScoDoc preferences
StyleSheet = styles.getSampleStyleSheet() style_sheet = styles.getSampleStyleSheet()
report = io.BytesIO() # in-memory document, no disk file report = io.BytesIO() # in-memory document, no disk file
objects = [ objects = [
Paragraph( Paragraph(
SU("Trombinoscope " + sem["titreannee"] + " " + groups_infos.groups_titles), SU("Trombinoscope " + sem["titreannee"] + " " + groups_infos.groups_titles),
StyleSheet["Heading3"], style_sheet["Heading3"],
) )
] ]
L = [] L = []
n = 0 n = 0
currow = [] currow = []
log("_trombino_pdf %d elements" % len(groups_infos.members)) log(f"_trombino_pdf {len(groups_infos.members)} elements")
for t in groups_infos.members: for t in groups_infos.members:
img = _get_etud_platypus_image(t, image_width=PHOTOWIDTH) img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
elem = Table( elem = Table(
[ [
[img], [img],
[ [
Paragraph( Paragraph(
SU(sco_etud.format_nomprenom(t)), SU(sco_etud.format_nomprenom(t)),
StyleSheet["Normal"], style_sheet["Normal"],
) )
], ],
], ],
colWidths=[PHOTOWIDTH], colWidths=[PHOTO_WIDTH],
) )
currow.append(elem) currow.append(elem)
if n == (N_PER_ROW - 1): if n == (N_PER_ROW - 1):
@ -365,11 +364,11 @@ def _trombino_pdf(groups_infos):
currow += [" "] * (N_PER_ROW - len(currow)) currow += [" "] * (N_PER_ROW - len(currow))
L.append(currow) L.append(currow)
if not L: if not L:
table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"]) table = Paragraph(SU("Aucune photo à exporter !"), style_sheet["Normal"])
else: else:
table = Table( table = Table(
L, L,
colWidths=[COLWIDTH] * N_PER_ROW, colWidths=[COL_WIDTH] * N_PER_ROW,
style=TableStyle( style=TableStyle(
[ [
# ('RIGHTPADDING', (0,0), (-1,-1), -5*mm), # ('RIGHTPADDING', (0,0), (-1,-1), -5*mm),
@ -400,39 +399,36 @@ def _trombino_pdf(groups_infos):
# --------------------- Sur une idée de l'IUT d'Orléans: # --------------------- Sur une idée de l'IUT d'Orléans:
def _listeappel_photos_pdf(groups_infos): def _listeappel_photos_pdf(groups_infos):
"Doc pdf pour liste d'appel avec photos" "Doc pdf pour liste d'appel avec photos"
filename = "trombino_%s" % groups_infos.groups_filename + ".pdf" filename = f"trombino_{groups_infos.groups_filename}.pdf"
sem = groups_infos.formsemestre # suppose 1 seul semestre sem = groups_infos.formsemestre # suppose 1 seul semestre
PHOTOWIDTH = 2 * cm PHOTO_WIDTH = 2 * cm
# COLWIDTH = 3.6 * cm # COLWIDTH = 3.6 * cm
# ROWS_PER_PAGE = 26 # XXX should be in ScoDoc preferences # ROWS_PER_PAGE = 26 # XXX should be in ScoDoc preferences
StyleSheet = styles.getSampleStyleSheet() style_sheet = styles.getSampleStyleSheet()
report = io.BytesIO() # in-memory document, no disk file report = io.BytesIO() # in-memory document, no disk file
objects = [ objects = [
Paragraph( Paragraph(
SU( SU(
sem["titreannee"] f"""{sem["titreannee"]} {groups_infos.groups_titles} ({len(groups_infos.members)})"""
+ " "
+ groups_infos.groups_titles
+ " (%d)" % len(groups_infos.members)
), ),
StyleSheet["Heading3"], style_sheet["Heading3"],
) )
] ]
L = [] L = []
n = 0 n = 0
currow = [] currow = []
log("_listeappel_photos_pdf %d elements" % len(groups_infos.members)) log(f"_listeappel_photos_pdf {len(groups_infos.members)} elements")
n = len(groups_infos.members) n = len(groups_infos.members)
# npages = n / 2*ROWS_PER_PAGE + 1 # nb de pages papier # npages = n / 2*ROWS_PER_PAGE + 1 # nb de pages papier
# for page in range(npages): # for page in range(npages):
for i in range(n): # page*2*ROWS_PER_PAGE, (page+1)*2*ROWS_PER_PAGE): for i in range(n): # page*2*ROWS_PER_PAGE, (page+1)*2*ROWS_PER_PAGE):
t = groups_infos.members[i] t = groups_infos.members[i]
img = _get_etud_platypus_image(t, image_width=PHOTOWIDTH) img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
txt = Paragraph( txt = Paragraph(
SU(sco_etud.format_nomprenom(t)), SU(sco_etud.format_nomprenom(t)),
StyleSheet["Normal"], style_sheet["Normal"],
) )
if currow: if currow:
currow += [""] currow += [""]
@ -444,7 +440,7 @@ def _listeappel_photos_pdf(groups_infos):
currow += [" "] * 3 currow += [" "] * 3
L.append(currow) L.append(currow)
if not L: if not L:
table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"]) table = Paragraph(SU("Aucune photo à exporter !"), style_sheet["Normal"])
else: else:
table = Table( table = Table(
L, L,
@ -544,7 +540,7 @@ def photos_import_files_form(group_ids=()):
else: else:
def callback(etud, data, filename): def callback(etud, data, filename):
sco_photos.store_photo(etud, data) return sco_photos.store_photo(etud, data, filename)
( (
ignored_zipfiles, ignored_zipfiles,
@ -555,6 +551,7 @@ def photos_import_files_form(group_ids=()):
zipfile=tf[2]["zipfile"], zipfile=tf[2]["zipfile"],
callback=callback, callback=callback,
filename_title="fichier_photo", filename_title="fichier_photo",
back_url=back_url,
) )
return render_template( return render_template(
"scolar/photos_import_files.html", "scolar/photos_import_files.html",
@ -571,11 +568,22 @@ def photos_import_files_form(group_ids=()):
) )
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( def zip_excel_import_files(
xlsfile=None, xlsfile=None,
zipfile=None, zipfile=None,
callback=None, callback=None,
filename_title="", # doit obligatoirement etre specifié filename_title="", # doit obligatoirement etre specifié
back_url=None,
): ):
"""Importation de fichiers à partir d'un excel et d'un zip """Importation de fichiers à partir d'un excel et d'un zip
La fonction La fonction
@ -595,26 +603,22 @@ def zip_excel_import_files(
try: try:
etudid_idx = titles.index("etudid") etudid_idx = titles.index("etudid")
filename_idx = titles.index(filename_title) filename_idx = titles.index(filename_title)
except: except Exception as exc:
raise ScoValueError( raise ScoValueError(
"Fichier excel incorrect (il faut une colonne etudid et une colonne %s) !" f"Fichier excel incorrect (il faut une colonne etudid et une colonne {filename_title}) !"
% filename_title ) from exc
)
def normfilename(fn, lowercase=True): filename_to_etudid = {} # filename : etudid
"normalisation used to match filenames" for line_num, l in enumerate(data[1:]):
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
filename_to_etud = {} # filename : etudid
for l in data[1:]:
filename = l[filename_idx].strip() filename = l[filename_idx].strip()
if filename: if filename:
filename_to_etud[normfilename(filename)] = l[etudid_idx] 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 # 2- Ouvre le zip et
try: try:
@ -625,35 +629,43 @@ def zip_excel_import_files(
stored_etud_filename = [] # [ (etud, filename) ] stored_etud_filename = [] # [ (etud, filename) ]
for name in z.namelist(): for name in z.namelist():
if len(name) > 4 and name[-1] != "/" and "." in name: if len(name) > 4 and name[-1] != "/" and "." in name:
data = z.read(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 # match zip filename with name given in excel
normname = normfilename(name) normname = _norm_zip_filename(name)
if normname in filename_to_etud: if normname in filename_to_etudid:
etudid = filename_to_etud[normname] etudid = filename_to_etudid[normname]
# ok, store photo # ok, store photo
try: try:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
del filename_to_etud[normname] del filename_to_etudid[normname]
except: except Exception as exc:
raise ScoValueError("ID étudiant invalide: %s" % etudid) raise ScoValueError(
f"ID étudiant invalide: {etudid}", dest_url=back_url
) from exc
callback( status, err_msg = callback(
etud, etud,
data, data,
normfilename(name, lowercase=False), _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)) stored_etud_filename.append((etud, name))
else: else:
log("zip: zip name %s not in excel !" % name) log(f"zip: zip name {name} not in excel !")
ignored_zipfiles.append(name) ignored_zipfiles.append(name)
else: else:
if name[-1] != "/": if name[-1] != "/":
ignored_zipfiles.append(name) ignored_zipfiles.append(name)
log("zip: ignoring %s" % name) log(f"zip: ignoring {name}")
if filename_to_etud: if filename_to_etudid:
# lignes excel non traitées # lignes excel non traitées
unmatched_files = list(filename_to_etud.keys()) unmatched_files = list(filename_to_etudid.keys())
else: else:
unmatched_files = [] unmatched_files = []
return ignored_zipfiles, unmatched_files, stored_etud_filename return ignored_zipfiles, unmatched_files, stored_etud_filename

View File

@ -1020,11 +1020,13 @@ def formChangePhoto(etudid=None):
return flask.redirect(dest_url) return flask.redirect(dest_url)
else: else:
data = tf[2]["photofile"].read() data = tf[2]["photofile"].read()
status, diag = sco_photos.store_photo(etud, data) status, err_msg = sco_photos.store_photo(
if status != 0: etud, data, tf[2]["photofile"].filename
)
if status:
return flask.redirect(dest_url) return flask.redirect(dest_url)
else: else:
H.append('<p class="warning">Erreur:' + diag + "</p>") H.append(f"""<p class="warning">Erreur: {err_msg}</p>""")
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()

View File

@ -473,7 +473,6 @@ def photos_import_files(formsemestre_id: int, xlsfile: str, zipfile: str):
"""Import des photos d'étudiants à partir d'une liste excel et d'un zip avec les images.""" """Import des photos d'étudiants à partir d'une liste excel et d'un zip avec les images."""
import app as mapp import app as mapp
from app.scodoc import sco_trombino, sco_photos from app.scodoc import sco_trombino, sco_photos
from app.scodoc import notesdb as ndb
from flask_login import login_user from flask_login import login_user
from app.auth.models import get_super_admin from app.auth.models import get_super_admin
@ -488,7 +487,7 @@ def photos_import_files(formsemestre_id: int, xlsfile: str, zipfile: str):
login_user(admin_user) login_user(admin_user)
def callback(etud, data, filename): def callback(etud, data, filename):
sco_photos.store_photo(etud, data) return sco_photos.store_photo(etud, data, filename)
( (
ignored_zipfiles, ignored_zipfiles,