1
0
Fork 0

Compare commits

...

11 Commits

Author SHA1 Message Date
Sébastien Lehmann 4ac9db35ed Correction superposition 2023-08-25 11:22:43 +02:00
Emmanuel Viennet 905bc934e3 Revert "Assiduites : metrique interne externe"
Changement non compatible avec les préférences en production.

This reverts commit afe2caac2d.
2023-08-25 11:22:43 +02:00
iziram b1386e9529 Assiduites : metrique interne externe 2023-08-25 11:22:43 +02:00
iziram ddb148a4ef Assiduites: Assiduites_metric_label + correction typing 2023-08-25 11:22:43 +02:00
Emmanuel Viennet 4c7f65f0b4 WIP: assiduités 2023-08-25 11:22:43 +02:00
Emmanuel Viennet b24280b30a version 9.6.8 2023-08-25 11:22:43 +02:00
iziram d3379298e2 Assiduites : bugfix justif import fichier 2023-08-25 11:22:43 +02:00
iziram 22d4be7b14 Assiduites : bugfix differee massAction 2023-08-25 11:22:43 +02:00
iziram 466dbe4859 Assiduites : bugfix external_data + differee 2023-08-25 11:22:43 +02:00
Emmanuel Viennet 4a877212c7 - API: added POST etudiant/etudid/int:etudid/photo
- API: added unit tests for photos
- Photos: code cleaning.
2023-08-25 11:22:43 +02:00
Emmanuel Viennet 6c3c0cec53 API: /formsemestres/query et /formsemestres_courants : ajout tri résultat. Ajout paramètre etat au query. 2023-08-25 11:22:43 +02:00
33 changed files with 343 additions and 183 deletions

View File

@ -14,6 +14,7 @@ from flask_login import current_user, login_required
from app import db, log
import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
from app.api import api_bp as bp
from app.api import api_web_bp, get_model_api_object, tools
from app.decorators import permission_required, scodoc
@ -25,6 +26,7 @@ from app.models import (
Scolog,
Justificatif,
)
from flask_sqlalchemy.query import Query
from app.models.assiduites import get_assiduites_justif
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
@ -256,7 +258,7 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
404,
message="étudiant inconnu",
)
assiduites_query = etud.assiduites
assiduites_query: Query = etud.assiduites
if with_query:
assiduites_query = _filter_manager(request, assiduites_query)
@ -372,7 +374,9 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
assiduites_query = scass.filter_by_formsemestre(Assiduite.query,Assiduite, formsemestre)
assiduites_query = scass.filter_by_formsemestre(
Assiduite.query, Assiduite, formsemestre
)
if with_query:
assiduites_query = _filter_manager(request, assiduites_query)
@ -597,8 +601,8 @@ def _create_singular(
desc: str = data.get("desc", None)
external_data = data.get("external_data", False)
if external_data is not False:
external_data = data.get("external_data", None)
if external_data is not None:
if not isinstance(external_data, dict):
errors.append("param 'external_data' : n'est pas un objet JSON")
@ -959,7 +963,7 @@ def _count_manager(requested) -> tuple[str, dict]:
return (metric, filtered)
def _filter_manager(requested, assiduites_query: Assiduite):
def _filter_manager(requested, assiduites_query: Query) -> Query:
"""
Retourne les assiduites entrées filtrées en fonction de la request
"""
@ -977,7 +981,7 @@ def _filter_manager(requested, assiduites_query: Assiduite):
fin = scu.is_iso_formated(fin, True)
if (deb, fin) != (None, None):
assiduites_query: Assiduite = scass.filter_by_date(
assiduites_query: Query = scass.filter_by_date(
assiduites_query, Assiduite, deb, fin
)
@ -1015,11 +1019,11 @@ def _filter_manager(requested, assiduites_query: Assiduite):
falses: tuple[str] = ("f", "faux", "false")
if est_just.lower() in trues:
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
assiduites_query: Query = scass.filter_assiduites_by_est_just(
assiduites_query, True
)
elif est_just.lower() in falses:
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
assiduites_query: Query = scass.filter_assiduites_by_est_just(
assiduites_query, False
)
@ -1027,7 +1031,7 @@ def _filter_manager(requested, assiduites_query: Assiduite):
user_id = requested.args.get("user_id", False)
if user_id is not False:
assiduites_query: Assiduite = scass.filter_by_user_id(assiduites_query, user_id)
assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id)
return assiduites_query

View File

@ -281,7 +281,15 @@ def dept_formsemestres_courants(acronym: str):
FormSemestre.date_debut <= test_date,
FormSemestre.date_fin >= test_date,
)
return [d.to_dict_api() for d in formsemestres]
return [
d.to_dict_api()
for d in formsemestres.order_by(
FormSemestre.date_debut.desc(),
FormSemestre.modalite,
FormSemestre.semestre_id,
FormSemestre.titre,
)
]
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")

View File

@ -154,8 +154,6 @@ def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
etudid : l'etudid de l'étudiant
nip : le code nip de l'étudiant
ine : le code ine de l'étudiant
Attention : Ne peut être qu'utilisée en tant que route de département
"""
etud = tools.get_etud(etudid, nip, ine)
@ -176,6 +174,44 @@ def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
return res
@bp.route("/etudiant/etudid/<int:etudid>/photo", methods=["POST"])
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeAdr)
@as_json
def set_photo_image(etudid: int = None):
"""Enregistre la photo de l'étudiant."""
allowed_depts = current_user.get_depts_with_permission(Permission.ScoEtudChangeAdr)
query = Identite.query.filter_by(id=etudid)
if not None in allowed_depts:
# restreint aux départements autorisés:
query = query.join(Departement).filter(
or_(Departement.acronym == acronym for acronym in allowed_depts)
)
if g.scodoc_dept is not None:
query = query.filter_by(dept_id=g.scodoc_dept_id)
etud: Identite = query.first()
if etud is None:
return json_error(404, message="etudiant inexistant")
# Récupère l'image
if len(request.files) == 0:
return json_error(404, "Il n'y a pas de fichier joint")
file = list(request.files.values())[0]
if not file.filename:
return json_error(404, "Il n'y a pas de fichier joint")
data = file.stream.read()
status, err_msg = sco_photos.store_photo(etud, data, file.filename)
if status:
return {"etudid": etud.id, "message": "recorded photo"}
return json_error(
404,
message=f"Erreur: {err_msg}",
)
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])

View File

@ -99,18 +99,20 @@ def formsemestre_infos(formsemestre_id: int):
def formsemestres_query():
"""
Retourne les formsemestres filtrés par
étape Apogée ou année scolaire ou département (acronyme ou id)
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant
etape_apo : un code étape apogée
annee_scolaire : année de début de l'année scolaire
dept_acronym : acronyme du département (eg "RT")
dept_id : id du département
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
etat: 0 si verrouillé, 1 sinon
"""
etape_apo = request.args.get("etape_apo")
annee_scolaire = request.args.get("annee_scolaire")
dept_acronym = request.args.get("dept_acronym")
dept_id = request.args.get("dept_id")
etat = request.args.get("etat")
nip = request.args.get("nip")
ine = request.args.get("ine")
formsemestres = FormSemestre.query
@ -126,6 +128,12 @@ def formsemestres_query():
formsemestres = formsemestres.filter(
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
)
if etat is not None:
try:
etat = bool(int(etat))
except ValueError:
return json_error(404, "invalid etat: integer expected")
formsemestres = formsemestres.filter_by(etat=etat)
if dept_acronym is not None:
formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
if dept_id is not None:
@ -151,7 +159,15 @@ def formsemestres_query():
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
formsemestres = formsemestres.filter_by(code_ine=ine)
return [formsemestre.to_dict_api() for formsemestre in formsemestres]
return [
formsemestre.to_dict_api()
for formsemestre in formsemestres.order_by(
FormSemestre.date_debut.desc(),
FormSemestre.modalite,
FormSemestre.semestre_id,
FormSemestre.titre,
)
]
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")

View File

@ -26,6 +26,7 @@ from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error
from flask_sqlalchemy.query import Query
# Partie Modèle
@ -261,7 +262,7 @@ def _create_singular(
# TOUT EST OK
try:
nouv_justificatif: Justificatif = Justificatif.create_justificatif(
nouv_justificatif: Query = Justificatif.create_justificatif(
date_debut=deb,
date_fin=fin,
etat=etat,
@ -307,7 +308,7 @@ def justif_edit(justif_id: int):
"date_fin"?: str
}
"""
justificatif_unique: Justificatif = Justificatif.query.filter_by(
justificatif_unique: Query = Justificatif.query.filter_by(
id=justif_id
).first_or_404()
@ -426,9 +427,7 @@ def justif_delete():
def _delete_singular(justif_id: int, database):
justificatif_unique: Justificatif = Justificatif.query.filter_by(
id=justif_id
).first()
justificatif_unique: Query = Justificatif.query.filter_by(id=justif_id).first()
if justificatif_unique is None:
return (404, "Justificatif non existant")
@ -470,7 +469,7 @@ def justif_import(justif_id: int = None):
if file.filename == "":
return json_error(404, "Il n'y a pas de fichier joint")
query = Justificatif.query.filter_by(id=justif_id)
query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
@ -509,11 +508,11 @@ def justif_export(justif_id: int = None, filename: str = None):
Retourne un fichier d'une archive d'un justificatif
"""
query = Justificatif.query.filter_by(id=justif_id)
query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404()
justificatif_unique: Justificaitf = query.first_or_404()
archive_name: str = justificatif_unique.fichier
if archive_name is None:
@ -551,7 +550,7 @@ def justif_remove(justif_id: int = None):
data: dict = request.get_json(force=True)
query = Justificatif.query.filter_by(id=justif_id)
query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
@ -604,7 +603,7 @@ def justif_list(justif_id: int = None):
Liste les fichiers du justificatif
"""
query = Justificatif.query.filter_by(id=justif_id)
query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
@ -642,7 +641,7 @@ def justif_justifies(justif_id: int = None):
Liste assiduite_id justifiées par le justificatif
"""
query = Justificatif.query.filter_by(id=justif_id)
query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
@ -676,13 +675,13 @@ def _filter_manager(requested, justificatifs_query):
fin = scu.is_iso_formated(fin, True)
if (deb, fin) != (None, None):
justificatifs_query: Justificatif = scass.filter_by_date(
justificatifs_query: Query = scass.filter_by_date(
justificatifs_query, Justificatif, deb, fin
)
user_id = requested.args.get("user_id", False)
if user_id is not False:
justificatifs_query: Justificatif = scass.filter_by_user_id(
justificatifs_query: Query = scass.filter_by_user_id(
justificatifs_query, user_id
)

View File

@ -14,6 +14,8 @@ from app.scodoc.sco_utils import (
localize_datetime,
)
from flask_sqlalchemy.query import Query
class Assiduite(db.Model):
"""
@ -124,7 +126,7 @@ class Assiduite(db.Model):
) -> object or int:
"""Créer une nouvelle assiduité pour l'étudiant"""
# Vérification de non duplication des périodes
assiduites: list[Assiduite] = etud.assiduites
assiduites: Query = etud.assiduites
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
raise ScoValueError(
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
@ -307,7 +309,7 @@ class Justificatif(db.Model):
def is_period_conflicting(
date_debut: datetime,
date_fin: datetime,
collection: list[Assiduite or Justificatif],
collection: Query,
collection_cls: Assiduite or Justificatif,
) -> bool:
"""

View File

@ -34,6 +34,7 @@ from flask import flash, render_template, url_for
from flask import g, request
from flask_login import current_user
from app.models import Identite
import app.scodoc.sco_utils as scu
from app.scodoc import sco_import_etuds
from app.scodoc import sco_groups
@ -351,10 +352,8 @@ def etudarchive_import_files(
):
"Importe des fichiers"
def callback(etud, data, filename):
return _store_etud_file_to_new_archive(
etud["etudid"], data, filename, description
)
def callback(etud: Identite, data, filename):
return _store_etud_file_to_new_archive(etud.id, data, filename, description)
# Utilise la fontion developpée au depart pour les photos
(

View File

@ -13,6 +13,7 @@ from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_preferences
from app.scodoc import sco_cache
from app.scodoc import sco_etud
from flask_sqlalchemy.query import Query
class CountCalculator:
@ -167,7 +168,7 @@ class CountCalculator:
self.hours += delta.total_seconds() / 3600
def to_dict(self) -> dict[str, object]:
def to_dict(self) -> dict[str, int or float]:
"""Retourne les métriques sous la forme d'un dictionnaire"""
return {
"compte": self.count,
@ -178,8 +179,8 @@ class CountCalculator:
def get_assiduites_stats(
assiduites: Assiduite, metric: str = "all", filtered: dict[str, object] = None
) -> Assiduite:
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
) -> dict[str, int or float]:
"""Compte les assiduités en fonction des filtres"""
if filtered is not None:
@ -218,7 +219,7 @@ def get_assiduites_stats(
return output if output else count
def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Query:
"""
Filtrage d'une collection d'assiduites en fonction de leur état
"""
@ -227,9 +228,7 @@ def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
return assiduites.filter(Assiduite.etat.in_(etats))
def filter_assiduites_by_est_just(
assiduites: Assiduite, est_just: bool
) -> Justificatif:
def filter_assiduites_by_est_just(assiduites: Assiduite, est_just: bool) -> Query:
"""
Filtrage d'une collection d'assiduites en fonction de s'ils sont justifiés
"""
@ -239,7 +238,7 @@ def filter_assiduites_by_est_just(
def filter_by_user_id(
collection: Assiduite or Justificatif,
user_id: int,
) -> Justificatif:
) -> Query:
"""
Filtrage d'une collection en fonction de l'user_id
"""
@ -252,7 +251,7 @@ def filter_by_date(
date_deb: datetime = None,
date_fin: datetime = None,
strict: bool = False,
):
) -> Query:
"""
Filtrage d'une collection d'assiduites en fonction d'une date
"""
@ -272,9 +271,7 @@ def filter_by_date(
)
def filter_justificatifs_by_etat(
justificatifs: Justificatif, etat: str
) -> Justificatif:
def filter_justificatifs_by_etat(justificatifs: Justificatif, etat: str) -> Query:
"""
Filtrage d'une collection de justificatifs en fonction de leur état
"""
@ -283,9 +280,7 @@ def filter_justificatifs_by_etat(
return justificatifs.filter(Justificatif.etat.in_(etats))
def filter_by_module_impl(
assiduites: Assiduite, module_impl_id: int or None
) -> Assiduite:
def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int or None) -> Query:
"""
Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl
"""
@ -296,7 +291,7 @@ def filter_by_formsemestre(
collection_query: Assiduite or Justificatif,
collection_class: Assiduite or Justificatif,
formsemestre: FormSemestre,
):
) -> Query:
"""
Filtrage d'une collection en fonction d'un formsemestre
"""
@ -323,7 +318,7 @@ def filter_by_formsemestre(
return collection_result.filter(collection_class.date_fin <= form_date_fin)
def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query:
"""
Retourne la liste des assiduite_id qui sont justifié par la justification
Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT comprise dans la plage du justificatif
@ -347,7 +342,7 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
def get_all_justified(
etudid: int, date_deb: datetime = None, date_fin: datetime = None
) -> list[Assiduite]:
) -> Query:
"""Retourne toutes les assiduités justifiées sur une période"""
if date_deb is None:
@ -432,7 +427,7 @@ def invalidate_assiduites_count(etudid, sem):
"""Invalidate (clear) cached counts"""
date_debut = sem["date_debut_iso"]
date_fin = sem["date_fin_iso"]
for met in ["demi", "journee", "compte", "heure"]:
for met in sco_preferences.ASSIDUITES_METRIC_LABEL.values():
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
sco_cache.AbsSemEtudCache.delete(key)

View File

@ -846,11 +846,15 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
"""
form_abs_tmpl += f"""
<a class="btn" href="{
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Saisie Journalière</button></a>
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&jour={
datetime.date.today().isoformat()
}&formsemestre_id={formsemestre.id}"><button>Saisie journalière</button></a>
<a class="btn" href="{
url_for("assiduites.signal_assiduites_diff", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&formsemestre_id={formsemestre.formsemestre_id}"><button>Saisie Différée</button></a>
}?group_ids=%(group_id)s&formsemestre_id={
formsemestre.formsemestre_id
}"><button>Saisie différée</button></a>
</td>
"""
else:

View File

@ -59,7 +59,7 @@ from flask.helpers import make_response, url_for
from app import log
from app import db
from app.models import Identite
from app.models import Identite, Scolog
from app.scodoc import sco_etud
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences
@ -86,12 +86,12 @@ def unknown_image_url() -> str:
return url_for("scolar.get_photo_image", scodoc_dept=g.scodoc_dept, etudid="")
def photo_portal_url(etud):
def photo_portal_url(code_nip: str):
"""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"]
if photo_url and code_nip:
return photo_url + "?nip=" + code_nip
else:
return None
@ -120,13 +120,13 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str:
path = photo_pathname(etud["photo_filename"], size=size)
if not path:
# Portail ?
ext_url = photo_portal_url(etud)
ext_url = photo_portal_url(etud["code_nip"])
if not ext_url:
# fallback: Photo "unknown"
photo_url = unknown_image_url()
else:
# essaie de copier la photo du portail
new_path, _ = copy_portal_photo_to_fs(etud)
new_path, _ = copy_portal_photo_to_fs(etud["etudid"])
if not new_path:
# copy failed, can we use external url ?
# nb: rarement utile, car le portail est rarement accessible sans authentification
@ -185,8 +185,8 @@ def build_image_response(filename):
return response
def etud_photo_is_local(etud: dict, size="small"):
return photo_pathname(etud["photo_filename"], size=size)
def etud_photo_is_local(photo_filename: str, size="small"):
return photo_pathname(photo_filename, size=size)
def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") -> str:
@ -205,7 +205,7 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") ->
nom = etud.get("nomprenom", etud["nom_disp"])
if title is None:
title = nom
if not etud_photo_is_local(etud):
if not etud_photo_is_local(etud["photo_filename"]):
fallback = (
f"""onerror='this.onerror = null; this.src="{unknown_image_url()}"'"""
)
@ -254,7 +254,7 @@ def photo_pathname(photo_filename: str, size="orig"):
return False
def store_photo(etud: dict, data, filename: str) -> tuple[bool, str]:
def store_photo(etud: Identite, 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.
@ -268,21 +268,17 @@ def store_photo(etud: dict, data, filename: str) -> tuple[bool, str]:
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)
saved_filename = save_image(etud, 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"])
etud.photo_filename = saved_filename
db.session.add(etud)
Scolog.logdb(method="changePhoto", msg=saved_filename, etudid=etud.id)
db.session.commit()
#
return True, "ok"
@ -313,7 +309,7 @@ def suppress_photo(etud: Identite) -> None:
# Internal functions
def save_image(etudid, data):
def save_image(etud: Identite, data: bytes):
"""data is a bytes string.
Save image in JPEG in 2 sizes (original and h90).
Returns filename (relative to PHOTO_DIR), without extension
@ -322,7 +318,7 @@ def save_image(etudid, data):
data_file.write(data)
data_file.seek(0)
img = PILImage.open(data_file)
filename = get_new_filename(etudid)
filename = get_new_filename(etud)
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")
@ -342,12 +338,12 @@ def scale_height(img, W=None, H=REDUCED_HEIGHT):
return img
def get_new_filename(etudid):
def get_new_filename(etud: Identite):
"""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)
dept = etud.departement.acronym
return find_new_dir() + dept + "_" + str(etud.id)
def find_new_dir():
@ -367,15 +363,14 @@ def find_new_dir():
return d + "/"
def copy_portal_photo_to_fs(etud: dict):
def copy_portal_photo_to_fs(etudid: int):
"""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)
etud: Identite = Identite.query.get_or_404(etudid)
url = photo_portal_url(etud.code_nip)
if not url:
return None, f"""{etud['nomprenom']}: pas de code NIP"""
return None, f"""{etud.nomprenom}: pas de code NIP"""
portal_timeout = sco_preferences.get_preference("portal_timeout")
error_message = None
try:
@ -394,11 +389,11 @@ def copy_portal_photo_to_fs(etud: dict):
log(f"copy_portal_photo_to_fs: {error_message}")
return (
None,
f"""{etud["nomprenom"]}: erreur chargement de {url}\n{error_message}""",
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}"""
return None, f"""{etud.nomprenom}: erreur chargement de {url}"""
data = r.content # image bytes
try:
@ -410,8 +405,8 @@ def copy_portal_photo_to_fs(etud: dict):
if status:
log(f"copy_portal_photo_to_fs: copied {url}")
return (
photo_pathname(etud["photo_filename"]),
f"{etud['nomprenom']}: photo chargée",
photo_pathname(etud.photo_filename),
f"{etud.nomprenom}: photo chargée",
)
else:
return None, f"{etud['nomprenom']}: <b>{error_message}</b>"
return None, f"{etud.nomprenom}: <b>{error_message}</b>"

View File

@ -162,7 +162,7 @@ def _convert_pref_type(p, pref_spec):
# special case for float values (where NULL means 0)
p["value"] = float(p["value"] or 0)
elif typ == "int":
p["value"] = int(p["value"] or 0)
p["value"] = int(float(p["value"] or 0))
else:
raise ValueError("invalid preference type")
@ -198,6 +198,13 @@ def _get_pref_default_value_from_config(name, pref_spec):
_INSTALLED_FONTS = ", ".join(sco_pdf.get_available_font_names())
ASSIDUITES_METRIC_LABEL = {
# l'ordre est important, c'est celui-du menu. Le defaut en 1er donc.
"1/2 J.": "demi",
"J.": "journee",
"H.": "heure",
}
PREF_CATEGORIES = (
# sur page "Paramètres"
("general", {"title": ""}), # voir paramètre titlr de TrivialFormulator
@ -629,6 +636,7 @@ class BasePreferences(object):
"type": "float",
"category": "assi",
"only_global": True,
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie",
},
),
(
@ -658,10 +666,10 @@ class BasePreferences(object):
{
"initvalue": "1/2 J.",
"input_type": "menu",
"labels": ["1/2 J.", "J.", "H."],
"allowed_values": ["1/2 J.", "J.", "H."],
"labels": list(ASSIDUITES_METRIC_LABEL.keys()),
"allowed_values": list(ASSIDUITES_METRIC_LABEL.keys()),
"title": "Métrique de l'assiduité",
"explanation": "Unité utilisée dans la fiche étudiante, le bilan, et dans les calculs (J. = journée, H. = heure)",
"explanation": "Unité utilisée dans la fiche étudiante, les bilans et les calculs",
"category": "assi",
"only_global": True,
},
@ -669,10 +677,10 @@ class BasePreferences(object):
(
"assi_seuil",
{
"initvalue": 3.0,
"initvalue": 3,
"size": 10,
"title": "Seuil d'alerte des absences",
"type": "float",
"type": "int",
"explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )",
"category": "assi",
"only_global": True,

View File

@ -43,7 +43,8 @@ from PIL import Image as PILImage
import flask
from flask import url_for, g, send_file, request
from app import log
from app import db, log
from app.models import Identite
import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoValueError
@ -146,7 +147,7 @@ def trombino_html(groups_infos):
'<span class="trombi_box"><span class="trombi-photo" id="trombi-%s">'
% t["etudid"]
)
if sco_photos.etud_photo_is_local(t, size="small"):
if sco_photos.etud_photo_is_local(t["photo_filename"], size="small"):
foto = sco_photos.etud_photo_html(t, title="")
else: # la photo n'est pas immédiatement dispo
foto = f"""<span class="unloaded_img" id="{t["etudid"]
@ -194,7 +195,7 @@ def check_local_photos_availability(groups_infos, fmt=""):
nb_missing = 0
for t in groups_infos.members:
_ = sco_photos.etud_photo_url(t) # -> copy distant files if needed
if not sco_photos.etud_photo_is_local(t):
if not sco_photos.etud_photo_is_local(t["photo_filename"]):
nb_missing += 1
if nb_missing > 0:
parameters = {"group_ids": groups_infos.group_ids, "format": fmt}
@ -278,7 +279,7 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
msg = []
nok = 0
for etud in groups_infos.members:
path, diag = sco_photos.copy_portal_photo_to_fs(etud)
path, diag = sco_photos.copy_portal_photo_to_fs(etud["etudid"])
msg.append(diag)
if path:
nok += 1
@ -539,7 +540,7 @@ def photos_import_files_form(group_ids=()):
return flask.redirect(back_url)
else:
def callback(etud, data, filename):
def callback(etud: Identite, data, filename):
return sco_photos.store_photo(etud, data, filename)
(
@ -640,14 +641,12 @@ def zip_excel_import_files(
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_etudid[normname]
except Exception as exc:
etud: Identite = db.session.get(Identite, etudid)
if not etud:
raise ScoValueError(
f"ID étudiant invalide: {etudid}", dest_url=back_url
) from exc
)
del filename_to_etudid[normname]
status, err_msg = callback(
etud,
data,

View File

@ -79,7 +79,7 @@ div.competence {
padding-left: calc(var(--arrow-width) + 8px);
}
.niveaux>div:not(:last-child)::after {
.niveaux>div:not(:last-child)::before {
content: "";
position: absolute;

View File

@ -1792,6 +1792,10 @@ td.formsemestre_status_inscrits {
text-align: center;
}
div.formsemestre_status button {
margin-left: 12px;;
}
td.rcp_titre_sem a.jury_link {
margin-left: 8px;
color: red;

View File

@ -885,7 +885,7 @@ function createAssiduite(etat, etudid) {
(data, status) => {
//success
if (data.success.length > 0) {
let obj = data.success["0"].assiduite_id;
let obj = data.success["0"].message.assiduite_id;
}
},
(data, status) => {
@ -910,7 +910,7 @@ function deleteAssiduite(assiduite_id) {
(data, status) => {
//success
if (data.success.length > 0) {
let obj = data.success["0"].assiduite_id;
let obj = data.success["0"].message.assiduite_id;
}
},
(data, status) => {
@ -1411,7 +1411,10 @@ function getModuleImplId() {
function setModuleImplId(assiduite, module = null) {
const moduleimpl = module == null ? getModuleImplId() : module;
if (moduleimpl === "autre") {
if ("external_data" in assiduite && assiduite.external_data != undefined) {
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object
) {
if ("module" in assiduite.external_data) {
assiduite.external_data.module = "Autre";
} else {
@ -1423,7 +1426,10 @@ function setModuleImplId(assiduite, module = null) {
assiduite.moduleimpl_id = null;
} else {
assiduite["moduleimpl_id"] = moduleimpl;
if ("external_data" in assiduite && assiduite.external_data != undefined) {
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object
) {
if ("module" in assiduite.external_data) {
delete assiduite.external_data.module;
}

View File

@ -125,11 +125,9 @@ class RowAssi(tb.Row):
"absent": ["Absences", 0.0, 0.0],
}
assi_metric = {
"H.": "heure",
"J.": "journee",
"1/2 J.": "demi",
}.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id))
assi_metric = sco_preferences.ASSIDUITES_METRIC_LABEL.get(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id)
)
for etat, valeur in retour.items():
compte_etat = scass.get_assiduites_stats(

View File

@ -164,7 +164,7 @@
dateType: 'json',
contentType: false,
processData: false,
success: () => { },
success: () => { console.log("done") },
}
)
)
@ -192,8 +192,8 @@
errorAlert();
}
if (Object.keys(data.success).length > 0) {
couverture = data.success[0].couverture
justif_id = data.success[0].justif_id;
couverture = data.success[0].message.couverture
justif_id = data.success[0].message.justif_id;
importFiles(justif_id);
return;
}

View File

@ -2,7 +2,16 @@
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Configuration du Module d'assiduité</h1>
<div class="row">
<h1>Configuration du suivi de l'assiduité</h1>
<div class="help"> Ces paramètres seront utilisés par tous les départements et
affectent notamment les comptages d'absences de tous les bulletins des
étudiants&nbsp;: ne changer que lorsque c'est vraiment nécessaire.
</div>
</div>
<div class="row">
<div class="col-md-8">

View File

@ -40,8 +40,9 @@
{% if readonly == "false" %}
<div style="margin: 1vh 0;">
<div id="forcemodule" style="display: none; margin:10px 0px;">Une préférence du semestre vous impose d'indiquer
le module !</div>
<div id="forcemodule" style="display: none; margin:10px 0px;">
Vous devez spécifier le module ! (voir réglage préférence du semestre)
</div>
<div>Module :{{moduleimpl_select|safe}}</div>
</div>
{% else %}

View File

@ -21,6 +21,10 @@
{{tableau | safe}}
<div class=""help">
Les comptes sont exprimés en {{ assi_metric }}.
</div>
<script>
const date_debut = "{{date_debut}}";
const date_fin = "{{date_fin}}";

View File

@ -680,7 +680,7 @@
rbtn.parentElement.setAttribute('etat', etat);
asyncCreateAssiduite(assiduite, (data) => {
if (Object.keys(data.success).length > 0) {
const assi_id = data.success['0'].assiduite_id;
const assi_id = data.success['0'].message.assiduite_id;
etudLine.setAttribute('assiduite_id', assi_id);
assiduite["assiduite_id"] = assi_id;
assiduites[etudid].push(assiduite);
@ -917,7 +917,7 @@
).done((c, e) => {
Object.keys(c[0].success).forEach((k) => {
const assiduite = createList[Number.parseInt(k)];
assiduite["assiduite_id"] = c[0].success[k].assiduite_id;
assiduite["assiduite_id"] = c[0].success[k].message.assiduite_id;
assiduites[assiduite.etudid].push(assiduite);
})
Object.keys(e[0].success).forEach((k) => {

View File

@ -57,18 +57,18 @@
<div class="sco_help">Ces images peuvent être intégrées dans les documents
générés par ScoDoc: bulletins, PV, etc.
</div>
<p><a class="stdlink" href="{{url_for('scodoc.configure_logos')}}">configuration des images et logos</a>
<p><a class="stdlink" href="{{url_for('scodoc.configure_logos')}}">Configuration des images et logos</a>
</p>
</section>
<section>
<h2>Exports Apogée</h2>
<p><a class="stdlink" href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a>
<p><a class="stdlink" href="{{url_for('scodoc.config_codes_decisions')}}">Configuration des codes de décision</a>
</p>
</section>
<section>
<h2>Assiduités</h2>
<p><a class="stdlink" href="{{url_for('scodoc.config_assiduites')}}">configuration du module d'assiduités</a>
<p><a class="stdlink" href="{{url_for('scodoc.config_assiduites')}}">Configuration du suivi de l'assiduité</a>
</p>
</section>

View File

@ -28,7 +28,7 @@
<h4>Fichiers chargés:</h4>
<ul>
{% for (etud, name) in stored_etud_filename %}
<li>{{etud["nomprenom"]}}: <tt>{{name}}</tt></li>
<li>{{etud.nomprenom}}: <tt>{{name}}</tt></li>
{% endfor %}
</ul>
{% endif %}

View File

@ -18,6 +18,6 @@ Importation des photo effectuée
{% if stored_etud_filename %}
# Fichiers chargés:
{% for (etud, name) in stored_etud_filename %}
- {{etud["nomprenom"]}}: <tt>{{name}}</tt></li>
- {{etud.nomprenom}}: <tt>{{name}}</tt></li>
{% endfor %}
{% endif %}

View File

@ -327,11 +327,9 @@ def bilan_etud():
date_debut: str = f"{scu.annee_scolaire()}-09-01"
date_fin: str = f"{scu.annee_scolaire()+1}-06-30"
assi_metric = {
"H.": "heure",
"J.": "journee",
"1/2 J.": "demi",
}.get(sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id))
assi_metric = sco_preferences.ASSIDUITES_METRIC_LABEL.get(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id)
)
return HTMLBuilder(
header,
@ -807,8 +805,6 @@ def visu_assi_group():
fmt = request.args.get("format", "html")
group_ids: list[int] = request.args.get("group_ids", None)
etudiants: list[dict] = []
if group_ids is None:
group_ids = []
else:
@ -842,16 +838,17 @@ def visu_assi_group():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
)
print()
return render_template(
"assiduites/pages/visu_assi.j2",
tableau=table.html(),
gr_tit=gr_tit,
assi_metric=sco_preferences.ASSIDUITES_METRIC_LABEL.get(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id)
),
date_debut=dates["debut"],
date_fin=dates["fin"],
gr_tit=gr_tit,
group_ids=request.args.get("group_ids", None),
sco=ScoData(formsemestre=groups_infos.get_formsemestre()),
tableau=table.html(),
title=f"Assiduité {grp} {groups_infos.groups_titles}",
)

View File

@ -1016,27 +1016,28 @@ def etud_photo_orig_page(etudid=None):
@scodoc7func
def form_change_photo(etudid=None):
"""Formulaire changement photo étudiant"""
etud = sco_etud.get_etud_info(filled=True)[0]
if sco_photos.etud_photo_is_local(etud):
etud["photoloc"] = "dans ScoDoc"
etud = Identite.get_etud(etudid)
if sco_photos.etud_photo_is_local(etud.photo_filename):
photo_loc = "dans ScoDoc"
else:
etud["photoloc"] = "externe"
photo_loc = "externe"
H = [
html_sco_header.sco_header(page_title="Changement de photo"),
"""<h2>Changement de la photo de %(nomprenom)s</h2>
<p>Photo actuelle (%(photoloc)s):
"""
% etud,
sco_photos.etud_photo_html(etud, title="photo actuelle"),
"""</p><p>Le fichier ne doit pas dépasser 500Ko (recadrer l'image, format "portrait" de préférence).</p>
<p>L'image sera automagiquement réduite pour obtenir une hauteur de 90 pixels.</p>
""",
f"""<h2>Changement de la photo de {etud.nomprenom}</h2>
<p>Photo actuelle ({photo_loc}):
{sco_photos.etud_photo_html(etudid=etud.id, title="photo actuelle")}
</p>
<p>Le fichier ne doit pas dépasser {sco_photos.MAX_FILE_SIZE//1024}Ko
(recadrer l'image, format "portrait" de préférence).
</p>
<p>L'image sera automagiquement réduite pour obtenir une hauteur de 90 pixels.</p>
""",
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("etudid", {"default": etudid, "input_type": "hidden"}),
("etudid", {"default": etud.id, "input_type": "hidden"}),
(
"photofile",
{"input_type": "file", "title": "Fichier image", "size": 20},
@ -1045,16 +1046,18 @@ def form_change_photo(etudid=None):
submitlabel="Valider",
cancelbutton="Annuler",
)
dest_url = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
)
dest_url = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
if tf[0] == 0:
return (
"\n".join(H)
+ tf[1]
+ '<p><a class="stdlink" href="form_suppress_photo?etudid=%s">Supprimer cette photo</a></p>'
% etudid
+ html_sco_header.sco_footer()
+ f"""
{tf[1]}
<p><a class="stdlink" href="{
url_for("scolar.form_suppress_photo",
scodoc_dept=g.scodoc_dept, etudid=etud.id)
}">Supprimer cette photo</a></p>
{html_sco_header.sco_footer()}
"""
)
elif tf[0] == -1:
return flask.redirect(dest_url)

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.7"
SCOVERSION = "9.6.9"
SCONAME = "ScoDoc"

View File

@ -536,7 +536,7 @@ def photos_import_files(formsemestre_id: int, xlsfile: str, zipfile: str):
admin_user = get_super_admin()
login_user(admin_user)
def callback(etud, data, filename):
def callback(etud: Identite, data, filename):
return sco_photos.store_photo(etud, data, filename)
(

View File

@ -269,6 +269,11 @@ def test_route_create(api_admin_headers):
assert len(res["success"]) == 1
TO_REMOVE.append(res["success"][0]["message"]["assiduite_id"])
data = GET(
path=f'/assiduite/{res["success"][0]["message"]["assiduite_id"]}',
headers=api_admin_headers,
)
check_fields(data)
data2 = create_data("absent", "02", MODULE, "desc")
res = POST_JSON(f"/assiduite/{ETUDID}/create", [data2], api_admin_headers)

View File

@ -18,43 +18,53 @@ Utilisation :
"""
import re
import requests
from app.scodoc import sco_utils as scu
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
import requests
from app.scodoc import sco_utils as scu
from tests.api.setup_test_api import (
API_PASSWORD_ADMIN,
API_URL,
API_USER_ADMIN,
CHECK_CERTIFICATE,
POST_JSON,
api_headers,
get_auth_headers,
)
from tests.api.tools_test_api import (
verify_fields,
verify_occurences_ids_etuds,
BULLETIN_FIELDS,
BULLETIN_ETUDIANT_FIELDS,
BULLETIN_FIELDS,
BULLETIN_FORMATION_FIELDS,
BULLETIN_OPTIONS_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS,
BULLETIN_RESSOURCES_FIELDS,
BULLETIN_SAES_FIELDS,
BULLETIN_UES_FIELDS,
BULLETIN_SEMESTRE_ABSENCES_FIELDS,
BULLETIN_SEMESTRE_ECTS_FIELDS,
BULLETIN_SEMESTRE_FIELDS,
BULLETIN_SEMESTRE_NOTES_FIELDS,
BULLETIN_SEMESTRE_RANG_FIELDS,
BULLETIN_UES_FIELDS,
BULLETIN_UES_RT11_RESSOURCES_FIELDS,
BULLETIN_UES_RT11_SAES_FIELDS,
BULLETIN_UES_RT21_RESSOURCES_FIELDS,
BULLETIN_UES_RT31_RESSOURCES_FIELDS,
BULLETIN_UES_RT21_SAES_FIELDS,
BULLETIN_UES_RT31_RESSOURCES_FIELDS,
BULLETIN_UES_RT31_SAES_FIELDS,
BULLETIN_SEMESTRE_ABSENCES_FIELDS,
BULLETIN_SEMESTRE_ECTS_FIELDS,
BULLETIN_SEMESTRE_NOTES_FIELDS,
BULLETIN_SEMESTRE_RANG_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS,
BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS,
BULLETIN_UES_UE_ECTS_FIELDS,
BULLETIN_UES_UE_FIELDS,
BULLETIN_UES_UE_MOYENNE_FIELDS,
BULLETIN_UES_UE_RESSOURCES_RESSOURCE_FIELDS,
BULLETIN_UES_UE_SAES_SAE_FIELDS,
BULLETIN_UES_UE_ECTS_FIELDS,
ETUD_FIELDS,
FSEM_FIELDS,
verify_fields,
verify_occurences_ids_etuds,
)
from tests.api.tools_test_api import ETUD_FIELDS, FSEM_FIELDS
from tests.conftest import RESOURCES_DIR
ETUDID = 1
NIP = "NIP2"
@ -142,6 +152,7 @@ def test_etudiant(api_headers):
API_URL + "/etudiant/ine/" + code_ine,
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
etud_ine = r.json()
@ -252,6 +263,56 @@ def test_etudiants_by_name(api_headers):
assert etuds[0]["nom"] == "RÉGNIER"
def test_etudiant_photo(api_headers):
"""
Routes : /etudiant/etudid/<int:etudid>/photo en GET et en POST
"""
# Initialement, la photo par défaut
r = requests.get(
f"{API_URL}/etudiant/etudid/{ETUDID}/photo",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert len(r.content) > 1000
assert b"JFIF" in r.content
# Set an image
filename = f"{RESOURCES_DIR}/images/papillon.jpg"
with open(filename, "rb") as image_file:
url = f"{API_URL}/etudiant/etudid/{ETUDID}/photo"
req = requests.post(
url,
files={filename: image_file},
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert req.status_code == 401 # api_headers non autorisé
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
with open(filename, "rb") as image_file:
url = f"{API_URL}/etudiant/etudid/{ETUDID}/photo"
req = requests.post(
url,
files={filename: image_file},
headers=admin_header,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert req.status_code == 200
# Redemande la photo
# (on ne peut pas comparer avec l'originale car ScoDoc retaille et enleve les tags)
r = requests.get(
f"{API_URL}/etudiant/etudid/{ETUDID}/photo",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert req.status_code == 200
assert b"JFIF" in r.content
def test_etudiant_formsemestres(api_headers):
"""
Route: /etudiant/etudid/<etudid:int>/formsemestres

View File

@ -60,6 +60,7 @@ def test_lambda_access(api_headers):
assert response.status_code == 401
# XXX A REVOIR
def test_global_logos(api_admin_headers):
"""
Route:
@ -73,7 +74,7 @@ def test_global_logos(api_admin_headers):
assert response.status_code == 200
assert response.json() is not None
assert "header" in response.json()
assert "footer" in response.json()
# assert "footer" in response.json() # XXX ??? absent
assert "B" in response.json()
assert "C" in response.json()

View File

@ -38,7 +38,7 @@ def test_permissions(api_headers):
and "GET" in r.methods
]
assert len(api_rules) > 0
args = {
all_args = {
"acronym": "TAPI",
"code_type": "etudid",
"code": 1,
@ -66,7 +66,13 @@ def test_permissions(api_headers):
"justif_id": 1,
"etudids": "1",
}
# Arguments spécifiques pour certaines routes
# par défaut, on passe tous les arguments de all_args
endpoint_args = {
"api.formsemestres_query": {},
}
for rule in api_rules:
args = endpoint_args.get(rule.endpoint, all_args)
path = rule.build(args)[1]
if not "GET" in rule.methods:
# skip all POST routes

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB