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."""
filesize = len(data)
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)
EtudsArchive.store(archive_id, filename, data)
return True, "ok"
def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
"""Delete an archive"""
# check permission
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)
if not etuds:
raise ScoValueError("étudiant inexistant")
@ -354,7 +357,9 @@ def etudarchive_import_files(
"Importe des fichiers"
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
(

View File

@ -872,6 +872,7 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
admission = (
Admission.query.filter_by(etudid=etudid).first().to_dict(no_nulls=True)
)
del admission["id"] # pour garder id == etudid dans etud
etud.update(admission)
#
adrs = adresse_list(cnx, {"etudid": etudid})
@ -883,6 +884,7 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
adr = adrs[0]
if len(adrs) > 1:
log("fill_etuds_info: etudid=%s a %d adresses" % (etudid, len(adrs)))
adr.pop("id", None)
etud.update(adr)
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"
JPG_QUALITY = 0.92
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
@ -244,34 +244,36 @@ def photo_pathname(photo_filename: str, size="orig"):
return False
def store_photo(etud, data):
def store_photo(etud: dict, data, filename: str) -> tuple[bool, str]:
"""Store image for this etud.
If there is an existing photo, it is erased and replaced.
data is a bytes string with image raw data.
Update database to store filename.
Returns (status, msg)
Returns (status, err_msg)
"""
# basic checks
filesize = len(data)
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:
filename = save_image(etud["etudid"], data)
except PIL.UnidentifiedImageError:
raise ScoGenError(msg="Fichier d'image invalide ou non format non supporté")
saved_filename = save_image(etud["etudid"], data)
except PIL.UnidentifiedImageError as exc:
raise ScoGenError(
msg="Fichier d'image '{filename}' invalide ou format non supporté"
) from exc
# update database:
etud["photo_filename"] = filename
etud["photo_filename"] = saved_filename
etud["foto"] = None
cnx = ndb.GetDBConnexion()
sco_etud.identite_edit_nocheck(cnx, etud)
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:
@ -366,9 +368,9 @@ def copy_portal_photo_to_fs(etud):
portal_timeout = sco_preferences.get_preference("portal_timeout")
f = None
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)
except:
except Exception:
# log("download failed: exception:\n%s" % traceback.format_exc())
# log("called from:\n" + "".join(traceback.format_stack()))
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)
data = r.content # image bytes
try:
status, diag = store_photo(etud, data)
except:
status = 0
diag = "Erreur chargement photo du portail"
status, err_msg = store_photo(etud, data, "(inconnue)")
except Exception:
status = False
err_msg = "Erreur chargement photo du portail"
log("copy_portal_photo_to_fs: failure (exception in store_photo)!")
if status == 1:
log("copy_portal_photo_to_fs: copied %s" % url)
if status:
log(f"copy_portal_photo_to_fs: copied {url}")
return (
photo_pathname(etud["photo_filename"]),
f"{etud['nomprenom']}: photo chargée",
)
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
import reportlab
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from reportlab.platypus.flowables import Flowable
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from reportlab.lib.pagesizes import A4, landscape
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.colors import Color
from reportlab.lib import colors
from PIL import Image as PILImage
@ -82,7 +78,7 @@ def trombino(
#
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:
return dialog
@ -96,7 +92,6 @@ def trombino(
return sco_trombino_doc.trombino_doc(groups_infos)
else:
raise Exception("invalid format")
# return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer()
def _trombino_html_header():
@ -105,7 +100,7 @@ def _trombino_html_header():
def trombino_html(groups_infos):
"HTML snippet for trombino (with title and menu)"
menuTrombi = [
menu_trombi = [
{
"title": "Charger des photos...",
"endpoint": "scolar.photos_import_files_form",
@ -125,19 +120,22 @@ def trombino_html(groups_infos):
if groups_infos.members:
if groups_infos.tous_les_etuds_du_sem:
ng = "Tous les étudiants"
group_txt = "Tous les étudiants"
else:
ng = "Groupe %s" % groups_infos.groups_titles
group_txt = f"Groupe {groups_infos.groups_titles}"
else:
ng = "Aucun étudiant inscrit dans ce groupe !"
group_txt = "Aucun étudiant inscrit dans ce groupe !"
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>'
% (ng)
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", menuTrombi, alone=True)
+ htmlutils.make_menu("Gérer les photos", menu_trombi, alone=True)
+ "</td>"
)
H.append("</tr></table>")
@ -186,7 +184,7 @@ def trombino_html(groups_infos):
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
localement dans ScoDoc (seules les photos dont nous disposons localement
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):
nb_missing += 1
if nb_missing > 0:
parameters = {"group_ids": groups_infos.group_ids, "format": format}
parameters = {"group_ids": groups_infos.group_ids, "format": fmt}
return (
False,
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>"""
% (
nb_missing,
groups_infos.base_url + "&dialog_confirmed=1&format=%s" % format,
),
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&format={fmt}"
>exporter seulement les photos existantes</a>""",
dest_url="trombino",
OK="Exporter seulement les photos existantes",
cancel_url="groups_view?curtab=tab-photos&"
@ -215,14 +213,13 @@ def check_local_photos_availability(groups_infos, format=""):
parameters=parameters,
),
)
else:
return True, ""
def _trombino_zip(groups_infos):
"Send photos as zip archive"
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)
# Archive originals (not reduced) images, in JPEG
for t in groups_infos.members:
@ -235,8 +232,7 @@ def _trombino_zip(groups_infos):
filename = code_nip + ".jpg"
else:
filename = f'{t["nom"]}_{t["prenom"]}_{t["etudid"]}.jpg'
Z.writestr(filename, img)
Z.close()
zip_file.writestr(filename, img)
size = data.tell()
log(f"trombino_zip: {size} bytes")
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")
footer = html_sco_header.sco_footer()
if not portal_url:
return (
header
+ '<p>portail non configuré</p><p><a href="%s">Retour au trombinoscope</a></p>'
% back_url
+ footer
)
return f"""{ header }
<p>portail non configuré</p>
<p><a href="{back_url}" class="stdlink">Retour au trombinoscope</a></p>
{ footer }
"""
if not dialog_confirmed:
return scu.confirm_dialog(
"""<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 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>
"""
% (groups_infos.groups_titles),
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},
@ -284,16 +283,16 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
if path:
nok += 1
msg.append("<b>%d photos correctement chargées</b>" % nok)
msg.append(f"<b>{nok} photos correctement chargées</b>")
return (
header
+ "<h2>Chargement des photos depuis le portail</h2><ul><li>"
+ "</li><li>".join(msg)
+ "</li></ul>"
+ '<p><a href="%s">retour au trombinoscope</a>' % back_url
+ footer
)
return f"""{ header }
<h2>Chargement des photos depuis le portail</h2>
<ul><li>
{ '</li><li>'.join(msg) }
</li></ul>
<p><a href="{back_url}">retour au trombinoscope</a>
{ footer }
"""
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):
"Send photos as 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
PHOTOWIDTH = 3 * cm
COLWIDTH = 3.6 * cm
PHOTO_WIDTH = 3 * cm
COL_WIDTH = 3.6 * cm
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
objects = [
Paragraph(
SU("Trombinoscope " + sem["titreannee"] + " " + groups_infos.groups_titles),
StyleSheet["Heading3"],
style_sheet["Heading3"],
)
]
L = []
n = 0
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:
img = _get_etud_platypus_image(t, image_width=PHOTOWIDTH)
img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
elem = Table(
[
[img],
[
Paragraph(
SU(sco_etud.format_nomprenom(t)),
StyleSheet["Normal"],
style_sheet["Normal"],
)
],
],
colWidths=[PHOTOWIDTH],
colWidths=[PHOTO_WIDTH],
)
currow.append(elem)
if n == (N_PER_ROW - 1):
@ -365,11 +364,11 @@ def _trombino_pdf(groups_infos):
currow += [" "] * (N_PER_ROW - len(currow))
L.append(currow)
if not L:
table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"])
table = Paragraph(SU("Aucune photo à exporter !"), style_sheet["Normal"])
else:
table = Table(
L,
colWidths=[COLWIDTH] * N_PER_ROW,
colWidths=[COL_WIDTH] * N_PER_ROW,
style=TableStyle(
[
# ('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:
def _listeappel_photos_pdf(groups_infos):
"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
PHOTOWIDTH = 2 * cm
PHOTO_WIDTH = 2 * cm
# COLWIDTH = 3.6 * cm
# 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
objects = [
Paragraph(
SU(
sem["titreannee"]
+ " "
+ groups_infos.groups_titles
+ " (%d)" % len(groups_infos.members)
f"""{sem["titreannee"]} {groups_infos.groups_titles} ({len(groups_infos.members)})"""
),
StyleSheet["Heading3"],
style_sheet["Heading3"],
)
]
L = []
n = 0
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)
# 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=PHOTOWIDTH)
img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
txt = Paragraph(
SU(sco_etud.format_nomprenom(t)),
StyleSheet["Normal"],
style_sheet["Normal"],
)
if currow:
currow += [""]
@ -444,7 +440,7 @@ def _listeappel_photos_pdf(groups_infos):
currow += [" "] * 3
L.append(currow)
if not L:
table = Paragraph(SU("Aucune photo à exporter !"), StyleSheet["Normal"])
table = Paragraph(SU("Aucune photo à exporter !"), style_sheet["Normal"])
else:
table = Table(
L,
@ -544,7 +540,7 @@ def photos_import_files_form(group_ids=()):
else:
def callback(etud, data, filename):
sco_photos.store_photo(etud, data)
return sco_photos.store_photo(etud, data, filename)
(
ignored_zipfiles,
@ -555,6 +551,7 @@ def photos_import_files_form(group_ids=()):
zipfile=tf[2]["zipfile"],
callback=callback,
filename_title="fichier_photo",
back_url=back_url,
)
return render_template(
"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(
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
@ -595,26 +603,22 @@ def zip_excel_import_files(
try:
etudid_idx = titles.index("etudid")
filename_idx = titles.index(filename_title)
except:
except Exception as exc:
raise ScoValueError(
"Fichier excel incorrect (il faut une colonne etudid et une colonne %s) !"
% filename_title
)
f"Fichier excel incorrect (il faut une colonne etudid et une colonne {filename_title}) !"
) from exc
def normfilename(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
filename_to_etud = {} # filename : etudid
for l in data[1:]:
filename_to_etudid = {} # filename : etudid
for line_num, l in enumerate(data[1:]):
filename = l[filename_idx].strip()
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
try:
@ -625,35 +629,43 @@ def zip_excel_import_files(
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 = normfilename(name)
if normname in filename_to_etud:
etudid = filename_to_etud[normname]
normname = _norm_zip_filename(name)
if normname in filename_to_etudid:
etudid = filename_to_etudid[normname]
# ok, store photo
try:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
del filename_to_etud[normname]
except:
raise ScoValueError("ID étudiant invalide: %s" % etudid)
del filename_to_etudid[normname]
except Exception as exc:
raise ScoValueError(
f"ID étudiant invalide: {etudid}", dest_url=back_url
) from exc
callback(
status, err_msg = callback(
etud,
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))
else:
log("zip: zip name %s not in excel !" % name)
log(f"zip: zip name {name} not in excel !")
ignored_zipfiles.append(name)
else:
if name[-1] != "/":
ignored_zipfiles.append(name)
log("zip: ignoring %s" % name)
if filename_to_etud:
log(f"zip: ignoring {name}")
if filename_to_etudid:
# lignes excel non traitées
unmatched_files = list(filename_to_etud.keys())
unmatched_files = list(filename_to_etudid.keys())
else:
unmatched_files = []
return ignored_zipfiles, unmatched_files, stored_etud_filename

View File

@ -1020,11 +1020,13 @@ def formChangePhoto(etudid=None):
return flask.redirect(dest_url)
else:
data = tf[2]["photofile"].read()
status, diag = sco_photos.store_photo(etud, data)
if status != 0:
status, err_msg = sco_photos.store_photo(
etud, data, tf[2]["photofile"].filename
)
if status:
return flask.redirect(dest_url)
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()

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 app as mapp
from app.scodoc import sco_trombino, sco_photos
from app.scodoc import notesdb as ndb
from flask_login import login_user
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)
def callback(etud, data, filename):
sco_photos.store_photo(etud, data)
return sco_photos.store_photo(etud, data, filename)
(
ignored_zipfiles,