forked from ScoDoc/DocScoDoc
Trombino: amélioration messages d'erreur. Relecture du code.
This commit is contained in:
parent
d93ce9e9be
commit
fb89823c7b
@ -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
|
||||||
(
|
(
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user