1
0
forked from ScoDoc/ScoDoc

Merge branch 'master' into assiduites_fixes

This commit is contained in:
Matthias HARTMANN 2024-06-03 08:01:21 +02:00
commit fed84559fc
11 changed files with 121 additions and 49 deletions

View File

@ -274,6 +274,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return "type_departement mismatch" return "type_departement mismatch"
# Table d'équivalences entre refs: # Table d'équivalences entre refs:
equiv = self._load_config_equivalences() equiv = self._load_config_equivalences()
# Même specialité (ou alias) ?
if self.specialite != other.specialite and other.specialite not in equiv.get(
"alias", []
):
return "specialite mismatch"
# mêmes parcours ? # mêmes parcours ?
eq_parcours = equiv.get("parcours", {}) eq_parcours = equiv.get("parcours", {})
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours} parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
@ -317,6 +322,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
def _load_config_equivalences(self) -> dict: def _load_config_equivalences(self) -> dict:
"""Load config file ressources/referentiels/equivalences.yaml """Load config file ressources/referentiels/equivalences.yaml
used to define equivalences between distinct referentiels used to define equivalences between distinct referentiels
return a dict, with optional keys:
alias: list of equivalent names for speciality (eg SD == STID)
parcours: dict with equivalent parcours acronyms
""" """
try: try:
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f: with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:

View File

@ -199,6 +199,11 @@ class Identite(models.ScoDocModel):
@classmethod @classmethod
def get_etud(cls, etudid: int) -> "Identite": def get_etud(cls, etudid: int) -> "Identite":
"""Etudiant ou 404, cherche uniquement dans le département courant""" """Etudiant ou 404, cherche uniquement dans le département courant"""
if not isinstance(etudid, int):
try:
etudid = int(etudid)
except (TypeError, ValueError):
abort(404, "etudid invalide")
if g.scodoc_dept: if g.scodoc_dept:
return cls.query.filter_by( return cls.query.filter_by(
id=etudid, dept_id=g.scodoc_dept_id id=etudid, dept_id=g.scodoc_dept_id

View File

@ -884,21 +884,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
""" """
) )
if can_edit_abs:
H.append(
f"""
<div>
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisir l'assiduité`">
(Saisie différée)</a>
</div>
"""
)
H.append("</div>") # /sem-groups-assi H.append("</div>") # /sem-groups-assi
if partition_is_empty: if partition_is_empty:
H.append( H.append(

View File

@ -96,13 +96,16 @@ def photo_portal_url(code_nip: str):
return None return None
def get_etud_photo_url(etudid, size="small"): def get_etud_photo_url(etudid, size="small", seed=None):
"L'URL scodoc vers la photo de l'étudiant"
kwargs = {"seed": seed} if seed else {}
return ( return (
url_for( url_for(
"scolar.get_photo_image", "scolar.get_photo_image",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etudid, etudid=etudid,
size=size, size=size,
**kwargs,
) )
if has_request_context() if has_request_context()
else "" else ""
@ -114,9 +117,11 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str:
If ScoDoc doesn't have an image and a portal is configured, link to it. 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: if fast:
return photo_url return get_etud_photo_url(etud["etudid"], size=size)
photo_url = get_etud_photo_url(
etud["etudid"], size=size, seed=hash(etud.get("photo_filename"))
)
path = photo_pathname(etud["photo_filename"], size=size) path = photo_pathname(etud["photo_filename"], size=size)
if not path: if not path:
# Portail ? # Portail ?
@ -374,7 +379,15 @@ def copy_portal_photo_to_fs(etudid: int):
portal_timeout = sco_preferences.get_preference("portal_timeout") portal_timeout = sco_preferences.get_preference("portal_timeout")
error_message = None error_message = None
try: try:
r = requests.get(url, timeout=portal_timeout) r = requests.get(
url,
timeout=portal_timeout,
params={
"nom": etud.nom or "",
"prenom": etud.prenom or "",
"civilite": etud.civilite,
},
)
except requests.ConnectionError: except requests.ConnectionError:
error_message = "ConnectionError" error_message = "ConnectionError"
except requests.Timeout: except requests.Timeout:

View File

@ -1085,18 +1085,35 @@ span.spanlink:hover {
} }
.trombi_box { .trombi_box {
display: inline-block;
width: 110px;
vertical-align: top;
margin-left: 5px; margin-left: 5px;
margin-top: 5px; margin-top: 5px;
width: 140px;
/* Constant width for the box */
display: inline-flex;
flex-direction: column;
/* Ensures trombi-photo is above trombi_legend */
align-items: center;
/* Centers content horizontally */
} }
span.trombi_legend { .trombi-photo {
display: inline-block; display: flex;
justify-content: center;
/* Centers image horizontally within the photo container */
margin-bottom: 10px;
/* Adds some space between the photo and the legend */
} }
span.trombi-photo { .trombi-photo img {
width: auto;
/* Maintains aspect ratio */
height: 120px;
/* Sets the height to 90px */
max-width: 100%;
/* Ensures the image doesn't exceed the container's width */
}
/* span.trombi_legend {
display: inline-block; display: inline-block;
} }
@ -1106,7 +1123,9 @@ span.trombi_box a {
span.trombi_box a img { span.trombi_box a img {
display: inline-block; display: inline-block;
} height: 128px;
width: auto;
} */
.trombi_nom { .trombi_nom {
display: block; display: block;

View File

@ -214,7 +214,7 @@
] // [0]=Lundi ... [6]=Dimanche -> à 00h00 ] // [0]=Lundi ... [6]=Dimanche -> à 00h00
//Une fonction d'action quand un bouton est cliqué //Une fonction d'action quand un bouton est cliqué
// 3 possibilités : // 3 possibilités :
// - assiduite_id = null -> créer nv assi avec état du bouton // - assiduite_id = null -> créer nv assi avec état du bouton
// - assiduite_id non null et bouton coché == etat assi -> suppression de l'assiduité // - assiduite_id non null et bouton coché == etat assi -> suppression de l'assiduité
// - assiduite_id non null et bouton coché != etat assi -> modification de l'assiduité // - assiduite_id non null et bouton coché != etat assi -> modification de l'assiduité
@ -418,14 +418,14 @@
// Peuplement des boutons en fonction des assiduités // Peuplement des boutons en fonction des assiduités
let boutons = ` let boutons = `
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}" <input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn retard" value="retard"> class="rbtn retard" value="retard">
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}" <input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn absent" value="absent"> class="rbtn absent" value="absent">
` `
if (!non_present) { if (!non_present) {
boutons = `<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}" boutons = `<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
class="rbtn present" value="present">`+boutons; class="rbtn present" value="present">`+boutons;
} }
@ -437,8 +437,8 @@
const deb = new Date(assi.date_debut); const deb = new Date(assi.date_debut);
const fin = new Date(assi.date_fin); const fin = new Date(assi.date_fin);
// si dates == periode -> cocher bouton correspondant // si dates == periode -> cocher bouton correspondant
// Sinon supprimer boutons et mettre case "rouge" + tooltip // Sinon supprimer boutons et mettre case "rouge" + tooltip
if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) { if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) {
let etat = assi.etat.toLowerCase(); let etat = assi.etat.toLowerCase();
@ -468,7 +468,7 @@
const deb = new Date(assi.date_debut); const deb = new Date(assi.date_debut);
const fin = new Date(assi.date_fin); const fin = new Date(assi.date_fin);
// si dates == periode -> cocher bouton correspondant // si dates == periode -> cocher bouton correspondant
// Sinon supprimer boutons et mettre case "rouge" + tooltip // Sinon supprimer boutons et mettre case "rouge" + tooltip
if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) { if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) {
@ -504,7 +504,7 @@
let target = e.target; let target = e.target;
let parent = target.parentElement; let parent = target.parentElement;
let isCancelled = await actionButton(target, !target.checked); let isCancelled = await actionButton(target, !target.checked);
if (isCancelled) { if (isCancelled) {
e.preventDefault(); e.preventDefault();
@ -690,7 +690,7 @@
} }
} }
document.getElementById("text-matin").addEventListener("click", (e)=>{ document.getElementById("text-matin").addEventListener("click", (e)=>{
e.preventDefault(); e.preventDefault();
@ -801,6 +801,7 @@ document.addEventListener("DOMContentLoaded", ()=>{
{{moduleimpl_select | safe}} {{moduleimpl_select | safe}}
</label> </label>
<button onclick="changeWeek(false)">Semaine suivante</button> <button onclick="changeWeek(false)">Semaine suivante</button>
<span><a href="{{url_choix_semaine}}" class="stdlink">autre semaine<a></span>
</div> </div>
<h3 id="tableau-dates"> <h3 id="tableau-dates">

View File

@ -2026,7 +2026,7 @@ def signal_assiduites_hebdo():
# Vérification semaine dans format iso 8601 et formsemestre # Vérification semaine dans format iso 8601 et formsemestre
regex_iso8601 = r"^\d{4}-W\d{2}$" regex_iso8601 = r"^\d{4}-W\d{2}$"
if not re.match(regex_iso8601, week): if week and not re.match(regex_iso8601, week):
raise ScoValueError("Semaine invalide", dest_url=request.referrer) raise ScoValueError("Semaine invalide", dest_url=request.referrer)
fs_deb_iso8601 = formsemestre.date_debut.strftime("%Y-W%W") fs_deb_iso8601 = formsemestre.date_debut.strftime("%Y-W%W")
@ -2035,10 +2035,12 @@ def signal_assiduites_hebdo():
# Utilisation de la propriété de la norme iso 8601 # Utilisation de la propriété de la norme iso 8601
# les chaines sont triables par ordre alphanumérique croissant # les chaines sont triables par ordre alphanumérique croissant
# et produiront le même ordre que les dates par ordre chronologique croissant # et produiront le même ordre que les dates par ordre chronologique croissant
if week < fs_deb_iso8601 or week > fs_fin_iso8601: if (not week) or week < fs_deb_iso8601 or week > fs_fin_iso8601:
flash( if week:
"La semaine n'est pas dans le semestre, choisissez la semaine sur laquelle saisir l'assiduité" flash(
) """La semaine n'est pas dans le semestre,
choisissez la semaine sur laquelle saisir l'assiduité"""
)
return sco_gen_cal.calendrier_choix_date( return sco_gen_cal.calendrier_choix_date(
date_debut=formsemestre.date_debut, date_debut=formsemestre.date_debut,
date_fin=formsemestre.date_fin, date_fin=formsemestre.date_fin,
@ -2122,6 +2124,15 @@ def signal_assiduites_hebdo():
for key, val in jours.items(): for key, val in jours.items():
hebdo_jours.append((key in non_travail, val)) hebdo_jours.append((key in non_travail, val))
url_choix_semaine = url_for(
"assiduites.signal_assiduites_hebdo",
group_ids=",".join(map(str, groups_infos.group_ids)),
week="",
scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id,
moduleimpl_id=moduleimpl_id,
)
return render_template( return render_template(
"assiduites/pages/signal_assiduites_hebdo.j2", "assiduites/pages/signal_assiduites_hebdo.j2",
title="Assiduité: saisie hebdomadaire", title="Assiduité: saisie hebdomadaire",
@ -2137,6 +2148,7 @@ def signal_assiduites_hebdo():
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
), ),
url_choix_semaine=url_choix_semaine,
) )

View File

@ -56,7 +56,9 @@ def refcomp(refcomp_id):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def refcomp_show(refcomp_id): def refcomp_show(refcomp_id):
"""Affichage du référentiel de compétences.""" """Affichage du référentiel de compétences."""
referentiel_competence = ApcReferentielCompetences.query.get_or_404(refcomp_id) referentiel_competence: ApcReferentielCompetences = (
ApcReferentielCompetences.query.get_or_404(refcomp_id)
)
# Autres référentiels "équivalents" pour proposer de changer les formations: # Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = referentiel_competence.equivalents() referentiels_equivalents = referentiel_competence.equivalents()
return render_template( return render_template(

View File

@ -15,4 +15,10 @@ QLIO: # la clé est 'specialite'
ATN: MTD ATN: MTD
# competences: # titres de compétences ('nom_court' dans le XML) # competences: # titres de compétences ('nom_court' dans le XML)
SD: STID STID: # passage de STID à SD
alias:
- SD
SD: # pour revenir en arrière au besoin
alias:
- STID

View File

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

View File

@ -28,7 +28,20 @@ script_dir = Path(os.path.abspath(__file__)).parent
os.chdir(script_dir) os.chdir(script_dir)
# Les "photos" des étudiants # Les "photos" des étudiants
FAKE_FACES_PATHS = list((Path("faces").glob("*.jpg"))) if os.path.exists("/opt/ExtraFaces"):
FAKE_FACES_PATHS = list((Path("extra_faces").glob("*/*.jpg")))
FAKE_FACES_PATHS_BY_CIVILITE = {
"M": list((Path("extra_faces").glob("M/*.jpg"))),
"F": list((Path("extra_faces").glob("F/*.jpg"))),
"X": list((Path("extra_faces").glob("X/*.jpg"))),
}
else:
FAKE_FACES_PATHS = list((Path("faces").glob("*.jpg")))
FAKE_FACES_PATHS_BY_CIVILITE = {
"M": FAKE_FACES_PATHS,
"F": FAKE_FACES_PATHS,
"X": FAKE_FACES_PATHS,
}
# Etudiant avec tous les champs (USPN) # Etudiant avec tous les champs (USPN)
ETUD_TEMPLATE_FULL = open(script_dir / "etud_template.xml").read() ETUD_TEMPLATE_FULL = open(script_dir / "etud_template.xml").read()
@ -84,16 +97,22 @@ def make_random_etape_etuds(etape, annee):
return "\n".join(L) return "\n".join(L)
def get_photo_filename(nip: str) -> str: def get_photo_filename(nip: str, civilite: str | None = None) -> str:
"""get an existing filename for a fake photo, found in faces/ """get an existing filename for a fake photo, found in faces/
Returns a path relative to the current working dir Returns a path relative to the current working dir
If civilite is not None, use it to select a subdir
""" """
# print("get_photo_filename")
nb_faces = len(FAKE_FACES_PATHS) if civilite:
faces = FAKE_FACES_PATHS_BY_CIVILITE[civilite]
else:
faces = FAKE_FACES_PATHS
nb_faces = len(faces)
if nb_faces == 0: if nb_faces == 0:
print("WARNING: aucun fichier image disponible !") print("WARNING: aucun fichier image disponible !")
return "" return ""
return FAKE_FACES_PATHS[hash(nip) % nb_faces] print(faces[hash(nip) % nb_faces])
return faces[hash(nip) % nb_faces]
class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler): class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
@ -139,7 +158,9 @@ class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
return return
elif ("getPhoto" in self.path) or ("scodocPhoto" in self.path): elif ("getPhoto" in self.path) or ("scodocPhoto" in self.path):
nip = query_components["nip"][0] nip = query_components["nip"][0]
self.path = str(get_photo_filename(nip)) civilite = query_components.get("civilite")
civilite = civilite[0] if civilite else None
self.path = str(get_photo_filename(nip, civilite=civilite))
print(f"photo for nip={nip}: {self.path}") print(f"photo for nip={nip}: {self.path}")
else: else:
print(f"Error 404: path={self.path}") print(f"Error 404: path={self.path}")