diff --git a/app/scodoc/sco_photos.py b/app/scodoc/sco_photos.py
index cbf8208c..c0f3cbd8 100644
--- a/app/scodoc/sco_photos.py
+++ b/app/scodoc/sco_photos.py
@@ -1,411 +1,411 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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@gmail.com
-#
-##############################################################################
-
-"""(Nouvelle) (Nouvelle) gestion des photos d'etudiants
-
-Les images sont stockées dans .../var/scodoc/photos
-L'attribut "photo_filename" de la table identite donne le nom du fichier image,
-sans extension (e.g. "F44/RT_EID31545").
-Toutes les images sont converties en jpg, et stockées dans photo_filename.jpg en taille originale.
-Elles sont aussi réduites en 90 pixels de hauteur, et stockées dans photo_filename.h90.jpg
-
-Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx
-
-
-## Historique:
- - jusqu'à novembre 2009, les images étaient stockées dans Zope (ZODB).
- - jusqu'à v1908, stockées dans .../static/photos (et donc accessibles sans authentification).
- - support for legacy ZODB removed in v1909.
-
-"""
-import datetime
-import glob
-import io
-import os
-import random
-import requests
-import time
-import traceback
-
-import PIL
-from PIL import Image as PILImage
-
-from flask import abort, request, g, has_request_context
-from flask.helpers import make_response, url_for
-
-from app import log
-from app import db
-from app.models import Identite
-from app.scodoc import sco_etud
-from app.scodoc import sco_portal_apogee
-from app.scodoc import sco_preferences
-from app.scodoc.sco_exceptions import ScoValueError
-from app.scodoc.scolog import logdb
-import app.scodoc.notesdb as ndb
-import app.scodoc.sco_utils as scu
-from config import Config
-
-# Full paths on server's filesystem. Something like "/opt/scodoc-data/photos"
-PHOTO_DIR = os.path.join(Config.SCODOC_VAR_DIR, "photos")
-ICONS_DIR = os.path.join(Config.SCODOC_DIR, "app", "static", "icons")
-UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg")
-UNKNOWN_IMAGE_URL = "get_photo_image?etudid=" # with empty etudid => unknown face image
-IMAGE_EXT = ".jpg"
-JPG_QUALITY = 0.92
-REDUCED_HEIGHT = 90 # pixels
-MAX_FILE_SIZE = 4 * 1024 * 1024 # max allowed size for uploaded image, in bytes
-H90 = ".h90" # suffix for reduced size images
-
-
-def photo_portal_url(etud):
- """Returns external URL to retreive photo on portal,
- or None if no portal configured"""
- photo_url = sco_portal_apogee.get_photo_url()
- if photo_url and etud["code_nip"]:
- return photo_url + "?nip=" + etud["code_nip"]
- else:
- return None
-
-
-def get_etud_photo_url(etudid, size="small"):
- return (
- url_for(
- "scolar.get_photo_image",
- scodoc_dept=g.scodoc_dept,
- etudid=etudid,
- size=size,
- )
- if has_request_context()
- else ""
- )
-
-
-def etud_photo_url(etud: dict, size="small", fast=False) -> str:
- """url to the image of the student, in "small" size or "orig" size.
- If ScoDoc doesn't have an image and a portal is configured, link to it.
-
- """
- photo_url = get_etud_photo_url(etud["etudid"], size=size)
- if fast:
- return photo_url
- path = photo_pathname(etud["photo_filename"], size=size)
- if not path:
- # Portail ?
- ext_url = photo_portal_url(etud)
- if not ext_url:
- # fallback: Photo "unknown"
- photo_url = scu.ScoURL() + "/" + UNKNOWN_IMAGE_URL
- else:
- # essaie de copier la photo du portail
- new_path, _ = copy_portal_photo_to_fs(etud)
- if not new_path:
- # copy failed, can we use external url ?
- # nb: rarement utile, car le portail est rarement accessible sans authentification
- if scu.CONFIG.PUBLISH_PORTAL_PHOTO_URL:
- photo_url = ext_url
- else:
- photo_url = UNKNOWN_IMAGE_URL
- return photo_url
-
-
-def get_photo_image(etudid=None, size="small"):
- """Returns photo image (HTTP response)
- If not etudid, use "unknown" image
- """
- if not etudid:
- filename = UNKNOWN_IMAGE_PATH
- else:
- etud = Identite.query.get_or_404(etudid)
- filename = photo_pathname(etud.photo_filename, size=size)
- if not filename:
- filename = UNKNOWN_IMAGE_PATH
- return _http_jpeg_file(filename)
-
-
-def _http_jpeg_file(filename):
- """returns an image as a Flask response"""
- st = os.stat(filename)
- last_modified = st.st_mtime # float timestamp
- file_size = st.st_size
- header = request.headers.get("If-Modified-Since")
- if header is not None:
- header = header.split(";")[0]
- # Some proxies seem to send invalid date strings for this
- # header. If the date string is not valid, we ignore it
- # rather than raise an error to be generally consistent
- # with common servers such as Apache (which can usually
- # understand the screwy date string as a lucky side effect
- # of the way they parse it).
- try:
- dt = datetime.datetime.strptime(header, "%a, %d %b %Y %H:%M:%S GMT")
- mod_since = dt.timestamp()
- except ValueError:
- mod_since = None
- if (mod_since is not None) and last_modified <= mod_since:
- return "", 304 # not modified
- #
- last_modified_str = time.strftime(
- "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified)
- )
- response = make_response(open(filename, mode="rb").read())
- response.headers["Content-Type"] = "image/jpeg"
- response.headers["Last-Modified"] = last_modified_str
- response.headers["Cache-Control"] = "max-age=3600"
- response.headers["Content-Length"] = str(file_size)
- return response
-
-
-def etud_photo_is_local(etud: dict, size="small"):
- return photo_pathname(etud["photo_filename"], size=size)
-
-
-def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"):
- """HTML img tag for the photo, either in small size (h90)
- or original size (size=="orig")
- """
- if not etud:
- if etudid:
- etuds = sco_etud.get_etud_info(filled=True, etudid=etudid)
- if not etuds:
- return abort(404, "etudiant inconnu")
- etud = etuds[0]
- else:
- raise ValueError("etud_photo_html: either etud or etudid must be specified")
- photo_url = etud_photo_url(etud, size=size)
- nom = etud.get("nomprenom", etud["nom_disp"])
- if title is None:
- title = nom
- if not etud_photo_is_local(etud):
- fallback = (
- """onerror='this.onerror = null; this.src="%s"'""" % UNKNOWN_IMAGE_URL
- )
- else:
- fallback = ""
- if size == "small":
- height_attr = 'height="%s"' % REDUCED_HEIGHT
- else:
- height_attr = ""
- return '' % (
- photo_url,
- nom,
- title,
- height_attr,
- fallback,
- )
-
-
-def etud_photo_orig_html(etud=None, etudid=None, title=None):
- """HTML img tag for the photo, in full size.
- Full-size images are always stored locally in the filesystem.
- They are the original uploaded images, converted in jpeg.
- """
- return etud_photo_html(etud=etud, etudid=etudid, title=title, size="orig")
-
-
-def photo_pathname(photo_filename: str, size="orig"):
- """Returns full path of image file if etud has a photo (in the filesystem),
- or False.
- Do not distinguish the cases: no photo, or file missing.
- Argument: photo_filename (Identite attribute)
- Resultat: False or str
- """
- if size == "small":
- version = H90
- elif size == "orig":
- version = ""
- else:
- raise ValueError("invalid size parameter for photo")
- if not photo_filename:
- return False
- path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT
- if os.path.exists(path):
- return path
- else:
- return False
-
-
-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, err_msg)
- """
- # basic checks
- filesize = len(data)
- if filesize < 10 or filesize > MAX_FILE_SIZE:
- return False, f"Fichier image '{filename}' de taille invalide ! ({filesize})"
- try:
- saved_filename = save_image(etud["etudid"], data)
- except (OSError, PIL.UnidentifiedImageError) as exc:
- raise ScoValueError(
- msg="Fichier d'image '{filename}' invalide ou format non supporté"
- ) from exc
-
- # update database:
- 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=saved_filename, etudid=etud["etudid"])
- #
- return True, "ok"
-
-
-def suppress_photo(etud: Identite) -> None:
- """Suppress a photo"""
- log(f"suppress_photo {etud}")
- rel_path = photo_pathname(etud.photo_filename)
- # 1- remove ref. from database
- etud.photo_filename = None
- db.session.add(etud)
-
- # 2- erase images files
- if rel_path:
- # remove extension and glob
- rel_path = rel_path[: -len(IMAGE_EXT)]
- filenames = glob.glob(rel_path + "*" + IMAGE_EXT)
- for filename in filenames:
- log(f"removing file {filename}")
- os.remove(filename)
- db.session.commit()
- # 3- log
- cnx = ndb.GetDBConnexion()
- logdb(cnx, method="changePhoto", msg="suppression", etudid=etud.id)
-
-
-# ---------------------------------------------------------------------------
-# Internal functions
-
-
-def save_image(etudid, data):
- """data is a bytes string.
- Save image in JPEG in 2 sizes (original and h90).
- Returns filename (relative to PHOTO_DIR), without extension
- """
- data_file = io.BytesIO()
- data_file.write(data)
- data_file.seek(0)
- img = PILImage.open(data_file)
- filename = get_new_filename(etudid)
- path = os.path.join(PHOTO_DIR, filename)
- log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path))
- img = img.convert("RGB")
- img.save(path + IMAGE_EXT, format="JPEG", quality=92)
- # resize:
- img = scale_height(img)
- log("saving %dx%d jpeg to %s.h90" % (img.size[0], img.size[1], filename))
- img.save(path + H90 + IMAGE_EXT, format="JPEG", quality=92)
- return filename
-
-
-def scale_height(img, W=None, H=REDUCED_HEIGHT):
- if W is None:
- # keep aspect
- W = int((img.size[0] * H) / img.size[1])
- img.thumbnail((W, H), PILImage.ANTIALIAS)
- return img
-
-
-def get_new_filename(etudid):
- """Constructs a random filename to store a new image.
- The path is constructed as: Fxx/etudid
- """
- dept = g.scodoc_dept
- return find_new_dir() + dept + "_" + str(etudid)
-
-
-def find_new_dir():
- """select randomly a new subdirectory to store a new file.
- We define 100 subdirectories named from F00 to F99.
- Returns a path relative to the PHOTO_DIR.
- """
- d = "F" + "%02d" % random.randint(0, 99)
- path = os.path.join(PHOTO_DIR, d)
- if not os.path.exists(path):
- # ensure photos directory exists
- if not os.path.exists(PHOTO_DIR):
- os.mkdir(PHOTO_DIR)
- # create subdirectory
- log(f"creating directory {path}")
- os.mkdir(path)
- return d + "/"
-
-
-def copy_portal_photo_to_fs(etud: dict):
- """Copy the photo from portal (distant website) to local fs.
- Returns rel. path or None if copy failed, with a diagnostic message
- """
- if "nomprenom" not in etud:
- sco_etud.format_etud_ident(etud)
- url = photo_portal_url(etud)
- if not url:
- return None, f"""{etud['nomprenom']}: pas de code NIP"""
- portal_timeout = sco_preferences.get_preference("portal_timeout")
- error_message = None
- try:
- r = requests.get(url, timeout=portal_timeout)
- except requests.ConnectionError:
- error_message = "ConnectionError"
- except requests.Timeout:
- error_message = "Timeout"
- except requests.TooManyRedirects:
- error_message = "TooManyRedirects"
- except requests.RequestException:
- error_message = "unknown requests error"
- if error_message is not None:
- log("sco_photos: download failed")
- log(traceback.format_exc())
- log(f"copy_portal_photo_to_fs: {error_message}")
- return (
- None,
- f"""{etud["nomprenom"]}: erreur chargement de {url}\n{error_message}""",
- )
- if r.status_code != 200:
- log(f"copy_portal_photo_to_fs: download failed {r.status_code }")
- return None, f"""{etud["nomprenom"]}: erreur chargement de {url}"""
-
- data = r.content # image bytes
- try:
- status, error_message = store_photo(etud, data, "(inconnue)")
- except Exception:
- status = False
- error_message = "Erreur chargement photo du portail"
- log("copy_portal_photo_to_fs: failure (exception in store_photo)!")
- 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, f"{etud['nomprenom']}: {error_message}"
+# -*- mode: python -*-
+# -*- coding: utf-8 -*-
+
+##############################################################################
+#
+# Gestion scolarite IUT
+#
+# Copyright (c) 1999 - 2022 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@gmail.com
+#
+##############################################################################
+
+"""(Nouvelle) (Nouvelle) gestion des photos d'etudiants
+
+Les images sont stockées dans .../var/scodoc/photos
+L'attribut "photo_filename" de la table identite donne le nom du fichier image,
+sans extension (e.g. "F44/RT_EID31545").
+Toutes les images sont converties en jpg, et stockées dans photo_filename.jpg en taille originale.
+Elles sont aussi réduites en 90 pixels de hauteur, et stockées dans photo_filename.h90.jpg
+
+Les images sont servies par ScoDoc, via la méthode getphotofile?etudid=xxx
+
+
+## Historique:
+ - jusqu'à novembre 2009, les images étaient stockées dans Zope (ZODB).
+ - jusqu'à v1908, stockées dans .../static/photos (et donc accessibles sans authentification).
+ - support for legacy ZODB removed in v1909.
+
+"""
+import datetime
+import glob
+import io
+import os
+import random
+import requests
+import time
+import traceback
+
+import PIL
+from PIL import Image as PILImage
+
+from flask import abort, request, g, has_request_context
+from flask.helpers import make_response, url_for
+
+from app import log
+from app import db
+from app.models import Identite
+from app.scodoc import sco_etud
+from app.scodoc import sco_portal_apogee
+from app.scodoc import sco_preferences
+from app.scodoc.sco_exceptions import ScoValueError
+from app.scodoc.scolog import logdb
+import app.scodoc.notesdb as ndb
+import app.scodoc.sco_utils as scu
+from config import Config
+
+# Full paths on server's filesystem. Something like "/opt/scodoc-data/photos"
+PHOTO_DIR = os.path.join(Config.SCODOC_VAR_DIR, "photos")
+ICONS_DIR = os.path.join(Config.SCODOC_DIR, "app", "static", "icons")
+UNKNOWN_IMAGE_PATH = os.path.join(ICONS_DIR, "unknown.jpg")
+UNKNOWN_IMAGE_URL = "get_photo_image?etudid=" # with empty etudid => unknown face image
+IMAGE_EXT = ".jpg"
+JPG_QUALITY = 0.92
+REDUCED_HEIGHT = 90 # pixels
+MAX_FILE_SIZE = 4 * 1024 * 1024 # max allowed size for uploaded image, in bytes
+H90 = ".h90" # suffix for reduced size images
+
+
+def photo_portal_url(etud):
+ """Returns external URL to retreive photo on portal,
+ or None if no portal configured"""
+ photo_url = sco_portal_apogee.get_photo_url()
+ if photo_url and etud["code_nip"]:
+ return photo_url + "?nip=" + etud["code_nip"]
+ else:
+ return None
+
+
+def get_etud_photo_url(etudid, size="small"):
+ return (
+ url_for(
+ "scolar.get_photo_image",
+ scodoc_dept=g.scodoc_dept,
+ etudid=etudid,
+ size=size,
+ )
+ if has_request_context()
+ else ""
+ )
+
+
+def etud_photo_url(etud: dict, size="small", fast=False) -> str:
+ """url to the image of the student, in "small" size or "orig" size.
+ If ScoDoc doesn't have an image and a portal is configured, link to it.
+
+ """
+ photo_url = get_etud_photo_url(etud["etudid"], size=size)
+ if fast:
+ return photo_url
+ path = photo_pathname(etud["photo_filename"], size=size)
+ if not path:
+ # Portail ?
+ ext_url = photo_portal_url(etud)
+ if not ext_url:
+ # fallback: Photo "unknown"
+ photo_url = scu.ScoURL() + "/" + UNKNOWN_IMAGE_URL
+ else:
+ # essaie de copier la photo du portail
+ new_path, _ = copy_portal_photo_to_fs(etud)
+ if not new_path:
+ # copy failed, can we use external url ?
+ # nb: rarement utile, car le portail est rarement accessible sans authentification
+ if scu.CONFIG.PUBLISH_PORTAL_PHOTO_URL:
+ photo_url = ext_url
+ else:
+ photo_url = scu.ScoURL() + "/" + UNKNOWN_IMAGE_URL
+ return photo_url
+
+
+def get_photo_image(etudid=None, size="small"):
+ """Returns photo image (HTTP response)
+ If not etudid, use "unknown" image
+ """
+ if not etudid:
+ filename = UNKNOWN_IMAGE_PATH
+ else:
+ etud = Identite.query.get_or_404(etudid)
+ filename = photo_pathname(etud.photo_filename, size=size)
+ if not filename:
+ filename = UNKNOWN_IMAGE_PATH
+ return _http_jpeg_file(filename)
+
+
+def _http_jpeg_file(filename):
+ """returns an image as a Flask response"""
+ st = os.stat(filename)
+ last_modified = st.st_mtime # float timestamp
+ file_size = st.st_size
+ header = request.headers.get("If-Modified-Since")
+ if header is not None:
+ header = header.split(";")[0]
+ # Some proxies seem to send invalid date strings for this
+ # header. If the date string is not valid, we ignore it
+ # rather than raise an error to be generally consistent
+ # with common servers such as Apache (which can usually
+ # understand the screwy date string as a lucky side effect
+ # of the way they parse it).
+ try:
+ dt = datetime.datetime.strptime(header, "%a, %d %b %Y %H:%M:%S GMT")
+ mod_since = dt.timestamp()
+ except ValueError:
+ mod_since = None
+ if (mod_since is not None) and last_modified <= mod_since:
+ return "", 304 # not modified
+ #
+ last_modified_str = time.strftime(
+ "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(last_modified)
+ )
+ response = make_response(open(filename, mode="rb").read())
+ response.headers["Content-Type"] = "image/jpeg"
+ response.headers["Last-Modified"] = last_modified_str
+ response.headers["Cache-Control"] = "max-age=3600"
+ response.headers["Content-Length"] = str(file_size)
+ return response
+
+
+def etud_photo_is_local(etud: dict, size="small"):
+ return photo_pathname(etud["photo_filename"], size=size)
+
+
+def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"):
+ """HTML img tag for the photo, either in small size (h90)
+ or original size (size=="orig")
+ """
+ if not etud:
+ if etudid:
+ etuds = sco_etud.get_etud_info(filled=True, etudid=etudid)
+ if not etuds:
+ return abort(404, "etudiant inconnu")
+ etud = etuds[0]
+ else:
+ raise ValueError("etud_photo_html: either etud or etudid must be specified")
+ photo_url = etud_photo_url(etud, size=size)
+ nom = etud.get("nomprenom", etud["nom_disp"])
+ if title is None:
+ title = nom
+ if not etud_photo_is_local(etud):
+ fallback = (
+ """onerror='this.onerror = null; this.src="%s"'""" % UNKNOWN_IMAGE_URL
+ )
+ else:
+ fallback = ""
+ if size == "small":
+ height_attr = 'height="%s"' % REDUCED_HEIGHT
+ else:
+ height_attr = ""
+ return '' % (
+ photo_url,
+ nom,
+ title,
+ height_attr,
+ fallback,
+ )
+
+
+def etud_photo_orig_html(etud=None, etudid=None, title=None):
+ """HTML img tag for the photo, in full size.
+ Full-size images are always stored locally in the filesystem.
+ They are the original uploaded images, converted in jpeg.
+ """
+ return etud_photo_html(etud=etud, etudid=etudid, title=title, size="orig")
+
+
+def photo_pathname(photo_filename: str, size="orig"):
+ """Returns full path of image file if etud has a photo (in the filesystem),
+ or False.
+ Do not distinguish the cases: no photo, or file missing.
+ Argument: photo_filename (Identite attribute)
+ Resultat: False or str
+ """
+ if size == "small":
+ version = H90
+ elif size == "orig":
+ version = ""
+ else:
+ raise ValueError("invalid size parameter for photo")
+ if not photo_filename:
+ return False
+ path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT
+ if os.path.exists(path):
+ return path
+ else:
+ return False
+
+
+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, err_msg)
+ """
+ # basic checks
+ filesize = len(data)
+ if filesize < 10 or filesize > MAX_FILE_SIZE:
+ return False, f"Fichier image '{filename}' de taille invalide ! ({filesize})"
+ try:
+ saved_filename = save_image(etud["etudid"], data)
+ except (OSError, PIL.UnidentifiedImageError) as exc:
+ raise ScoValueError(
+ msg="Fichier d'image '{filename}' invalide ou format non supporté"
+ ) from exc
+
+ # update database:
+ 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=saved_filename, etudid=etud["etudid"])
+ #
+ return True, "ok"
+
+
+def suppress_photo(etud: Identite) -> None:
+ """Suppress a photo"""
+ log(f"suppress_photo {etud}")
+ rel_path = photo_pathname(etud.photo_filename)
+ # 1- remove ref. from database
+ etud.photo_filename = None
+ db.session.add(etud)
+
+ # 2- erase images files
+ if rel_path:
+ # remove extension and glob
+ rel_path = rel_path[: -len(IMAGE_EXT)]
+ filenames = glob.glob(rel_path + "*" + IMAGE_EXT)
+ for filename in filenames:
+ log(f"removing file {filename}")
+ os.remove(filename)
+ db.session.commit()
+ # 3- log
+ cnx = ndb.GetDBConnexion()
+ logdb(cnx, method="changePhoto", msg="suppression", etudid=etud.id)
+
+
+# ---------------------------------------------------------------------------
+# Internal functions
+
+
+def save_image(etudid, data):
+ """data is a bytes string.
+ Save image in JPEG in 2 sizes (original and h90).
+ Returns filename (relative to PHOTO_DIR), without extension
+ """
+ data_file = io.BytesIO()
+ data_file.write(data)
+ data_file.seek(0)
+ img = PILImage.open(data_file)
+ filename = get_new_filename(etudid)
+ path = os.path.join(PHOTO_DIR, filename)
+ log("saving %dx%d jpeg to %s" % (img.size[0], img.size[1], path))
+ img = img.convert("RGB")
+ img.save(path + IMAGE_EXT, format="JPEG", quality=92)
+ # resize:
+ img = scale_height(img)
+ log("saving %dx%d jpeg to %s.h90" % (img.size[0], img.size[1], filename))
+ img.save(path + H90 + IMAGE_EXT, format="JPEG", quality=92)
+ return filename
+
+
+def scale_height(img, W=None, H=REDUCED_HEIGHT):
+ if W is None:
+ # keep aspect
+ W = int((img.size[0] * H) / img.size[1])
+ img.thumbnail((W, H), PILImage.ANTIALIAS)
+ return img
+
+
+def get_new_filename(etudid):
+ """Constructs a random filename to store a new image.
+ The path is constructed as: Fxx/etudid
+ """
+ dept = g.scodoc_dept
+ return find_new_dir() + dept + "_" + str(etudid)
+
+
+def find_new_dir():
+ """select randomly a new subdirectory to store a new file.
+ We define 100 subdirectories named from F00 to F99.
+ Returns a path relative to the PHOTO_DIR.
+ """
+ d = "F" + "%02d" % random.randint(0, 99)
+ path = os.path.join(PHOTO_DIR, d)
+ if not os.path.exists(path):
+ # ensure photos directory exists
+ if not os.path.exists(PHOTO_DIR):
+ os.mkdir(PHOTO_DIR)
+ # create subdirectory
+ log(f"creating directory {path}")
+ os.mkdir(path)
+ return d + "/"
+
+
+def copy_portal_photo_to_fs(etud: dict):
+ """Copy the photo from portal (distant website) to local fs.
+ Returns rel. path or None if copy failed, with a diagnostic message
+ """
+ if "nomprenom" not in etud:
+ sco_etud.format_etud_ident(etud)
+ url = photo_portal_url(etud)
+ if not url:
+ return None, f"""{etud['nomprenom']}: pas de code NIP"""
+ portal_timeout = sco_preferences.get_preference("portal_timeout")
+ error_message = None
+ try:
+ r = requests.get(url, timeout=portal_timeout)
+ except requests.ConnectionError:
+ error_message = "ConnectionError"
+ except requests.Timeout:
+ error_message = "Timeout"
+ except requests.TooManyRedirects:
+ error_message = "TooManyRedirects"
+ except requests.RequestException:
+ error_message = "unknown requests error"
+ if error_message is not None:
+ log("sco_photos: download failed")
+ log(traceback.format_exc())
+ log(f"copy_portal_photo_to_fs: {error_message}")
+ return (
+ None,
+ f"""{etud["nomprenom"]}: erreur chargement de {url}\n{error_message}""",
+ )
+ if r.status_code != 200:
+ log(f"copy_portal_photo_to_fs: download failed {r.status_code }")
+ return None, f"""{etud["nomprenom"]}: erreur chargement de {url}"""
+
+ data = r.content # image bytes
+ try:
+ status, error_message = store_photo(etud, data, "(inconnue)")
+ except Exception:
+ status = False
+ error_message = "Erreur chargement photo du portail"
+ log("copy_portal_photo_to_fs: failure (exception in store_photo)!")
+ 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, f"{etud['nomprenom']}: {error_message}"