Compare commits

...

30 Commits

Author SHA1 Message Date
Emmanuel Viennet 70e3006981 merge 2024-03-26 14:37:02 +01:00
Emmanuel Viennet bae46c2794 Page accueil département: refonte liste semestres en cours 2024-03-26 14:17:42 +01:00
Iziram b1055a4ebe Assiduité : signal_assiduites_group : fix bug photo etud 2024-03-26 09:02:55 +01:00
Iziram b2ef6a4c53 Assiduité : liste_assiduites : formatage des dates 2024-03-25 16:19:05 +01:00
Iziram a7c7bd655d Assiduité : ajout_justif_etud : dates avec heures 2024-03-25 16:15:01 +01:00
Iziram 1309043a98 Assiduités : assiduites_bubble : ajout d'un bouton 📝 pour éditer l'assiduité visée 2024-03-25 15:19:59 +01:00
Iziram a75b41ca5f Assiduité : signal_assiduites_diff : vérification date 2024-03-25 15:12:08 +01:00
Emmanuel Viennet 8df25ca02f Ajout infos semestres dans bulletin classique JSON. Close #583 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 61f9dddeb6 Modif. clé trie étudiants et utilisation dans éditeur partition. 2024-03-25 14:41:20 +01:00
Emmanuel Viennet a1f5340935 Débouchés: tags. Implements #396 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 68128c27d5 Conversion date naissance étudiant. complète #593. 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 8ecaa2bed0 Conversion dates édition évaluations et formsemestres. Fix #593. 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 7c61dd8d63 Cosmetic + reorganisation css edit formation 2024-03-25 14:41:20 +01:00
Emmanuel Viennet f493ba344f Jury BUT: améliore présentation et information sur les UEs capitalisées. Closes #670 2024-03-25 14:41:20 +01:00
Emmanuel Viennet f5079d9aef Jury BUT: affiche la liste des modules avec note en ATTente 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 55add2ffb3 cosmetic: eye, table semestres 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 5865b67652 Adapte ref. pour test_api_formsemestre.py sans nom_short 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 3c8b088d5e Jury BUT auto: avertissement si semestres pairs non bloqués 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 2da359ae41 Fix export excel table jury. Closes #868 2024-03-25 14:41:20 +01:00
Emmanuel Viennet 09ec53f573 Ajout infos semestres dans bulletin classique JSON. Close #583 2024-03-24 15:47:42 +01:00
Emmanuel Viennet 3787e0145a Modif. clé trie étudiants et utilisation dans éditeur partition. 2024-03-24 14:34:55 +01:00
Emmanuel Viennet edf989ee04 Débouchés: tags. Implements #396 2024-03-24 11:23:40 +01:00
Emmanuel Viennet 203f3a5342 Conversion date naissance étudiant. complète #593. 2024-03-24 10:34:02 +01:00
Emmanuel Viennet 161f8476ca Conversion dates édition évaluations et formsemestres. Fix #593. 2024-03-24 09:17:01 +01:00
Emmanuel Viennet d419d75515 Cosmetic + reorganisation css edit formation 2024-03-24 08:27:09 +01:00
Emmanuel Viennet f23630d7fd Jury BUT: améliore présentation et information sur les UEs capitalisées. Closes #670 2024-03-24 07:39:47 +01:00
Emmanuel Viennet fa0417f0b1 Jury BUT: affiche la liste des modules avec note en ATTente 2024-03-23 13:23:26 +01:00
Emmanuel Viennet 12256dc3d4 cosmetic: eye, table semestres 2024-03-23 10:17:49 +01:00
Emmanuel Viennet 46529917ea Adapte ref. pour test_api_formsemestre.py sans nom_short 2024-03-22 22:05:24 +01:00
Emmanuel Viennet 2367984848 Jury BUT auto: avertissement si semestres pairs non bloqués 2024-03-22 21:56:52 +01:00
44 changed files with 1183 additions and 644 deletions

View File

@ -38,7 +38,7 @@ from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import ModuleType
import app.scodoc.sco_utils as scu
from app.tables.recap import TableRecap
from app.tables.recap import TableRecap, RowRecap
@bp.route("/formsemestre/<int:formsemestre_id>")
@ -543,16 +543,30 @@ def formsemestre_resultat(formsemestre_id: int):
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
table = TableRecap(
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
)
# Supprime les champs inutiles (mise en forme)
rows = table.to_list()
# Ajoute le groupe de chaque partition:
# Ajoute le groupe de chaque partition,
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
for row in rows:
row["partitions"] = etud_groups.get(row["etudid"], {})
class RowRecapAPI(RowRecap):
"""Pour table avec partitions et sort_key"""
def add_etud_cols(self):
"""Ajoute colonnes étudiant: codes, noms"""
super().add_etud_cols()
self.add_cell("partitions", "partitions", etud_groups.get(self.etud.id, {}))
self.add_cell("sort_key", "sort_key", self.etud.sort_key)
table = TableRecap(
res,
convert_values=convert_values,
include_evaluations=False,
mode_jury=False,
row_class=RowRecapAPI,
)
rows = table.to_list()
# for row in rows:
# row["partitions"] = etud_groups.get(row["etudid"], {})
return rows

View File

@ -542,9 +542,9 @@ def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int)
for parcour_code, niveaux in niveaux_sans_ue_by_parcour.items():
H.append(
f"""<li>Parcours {parcour_code} : {
len(niveaux)} niveaux sans UEs
<span>
{ ', '.join( f'{niveau.competence.titre} {niveau.ordre}'
len(niveaux)} niveaux sans UEs&nbsp;:
<span class="niveau-nom"><span>
{ '</span>, <span>'.join( f'{niveau.competence.titre} {niveau.ordre}'
for niveau in niveaux
)
}

View File

@ -77,7 +77,7 @@ from app.models.but_refcomp import (
ApcNiveau,
ApcParcours,
)
from app.models import Evaluation, Scolog, ScolarAutorisationInscription
from app.models import Evaluation, ModuleImpl, Scolog, ScolarAutorisationInscription
from app.models.but_validations import (
ApcValidationAnnee,
ApcValidationRCUE,
@ -413,12 +413,12 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Si validée par niveau supérieur:
if self.code_valide == sco_codes.ADSUP:
self.codes.insert(0, sco_codes.ADSUP)
self.explanation = f"<div>{explanation}</div>"
self.explanation = f'<div class="deca-expl">{explanation}</div>'
messages = self.descr_pb_coherence()
if messages:
self.explanation += (
'<div class="warning">'
+ '</div><div class="warning">'.join(messages)
'<div class="warning warning-info">'
+ '</div><div class="warning warning-info">'.join(messages)
+ "</div>"
)
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
@ -796,16 +796,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_pair is not None:
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
def has_notes_en_attente(self) -> bool:
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
res = (
def _get_current_res(self) -> ResultatsSemestreBUT:
"Les res. du semestre d'origine du deca"
return (
self.res_pair
if self.formsemestre_pair
and (self.formsemestre.id == self.formsemestre_pair.id)
else self.res_impair
)
def has_notes_en_attente(self) -> bool:
"Vrai si l'étudiant a au moins une note en attente dans le semestre origine de ce deca"
res = self._get_current_res()
return res and self.etud.id in res.get_etudids_attente()
def get_modimpls_attente(self) -> list[ModuleImpl]:
"Liste des ModuleImpl dans lesquels l'étudiant à au moins une note en ATTente"
res = self._get_current_res()
modimpls_results = [
modimpl_result
for modimpl_result in res.modimpls_results.values()
if self.etud.id in modimpl_result.etudids_attente
]
modimpls = [
db.session.get(ModuleImpl, mr.moduleimpl_id) for mr in modimpls_results
]
return sorted(modimpls, key=lambda mi: (mi.module.numero, mi.module.code))
def record_all(self, only_validantes: bool = False) -> bool:
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
et sont donc en mode "automatique".
@ -997,19 +1014,23 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if dec_ue.code_valide not in CODES_UE_VALIDES:
if (
dec_ue.ue_status
and dec_ue.ue_status["was_capitalized"]
and dec_ue.ue_status["is_capitalized"]
):
messages.append(
f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
)
else:
messages.append(
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est (probablement une validation antérieure)"
)
else:
messages.append(
f"L'UE {ue.acronyme} n'a pas décision (???)"
)
# Voyons si on est dispensé de cette ue ?
res = self.res_impair if ue.semestre_idx % 2 else self.res_pair
if res and (self.etud.id, ue.id) in res.dispense_ues:
messages.append(f"Pas (ré)inscrit à l'UE {ue.acronyme}")
return messages
def valide_diplome(self) -> bool:
@ -1514,7 +1535,7 @@ class DecisionsProposeesUE(DecisionsProposees):
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}>"""
} codes={self.codes} explanation="{self.explanation}">"""
def compute_codes(self):
"""Calcul des .codes attribuables et de l'explanation associée"""

View File

@ -16,8 +16,8 @@ from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_validation_auto_but(
formsemestre: FormSemestre, only_adm: bool = True
) -> int:
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False
) -> tuple[int, list[jury_but.DecisionsProposeesAnnee]]:
"""Calcul automatique des décisions de jury sur une "année" BUT.
- N'enregistre jamais de décisions de l'année scolaire précédente, même
@ -27,16 +27,22 @@ def formsemestre_validation_auto_but(
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
Returns:
- En mode normal, (nombre d'étudiants pour lesquels on a enregistré au moins un code, []])
- En mode dry_run, (0, list[DecisionsProposeesAnnee])
"""
if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT")
nb_etud_modif = 0
decas = []
with sco_cache.DeferredSemCacheManager():
for etudid in formsemestre.etuds_inscriptions:
etud = Identite.get_etud(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
nb_etud_modif += deca.record_all(only_validantes=only_adm)
if not dry_run:
nb_etud_modif += deca.record_all(only_validantes=only_adm)
else:
decas.append(deca)
db.session.commit()
ScolarNews.add(
@ -49,4 +55,4 @@ def formsemestre_validation_auto_but(
formsemestre_id=formsemestre.id,
),
)
return nb_etud_modif
return nb_etud_modif, decas

View File

@ -109,23 +109,29 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
</div>"""
)
ue_impair, ue_pair = rcue.ue_1, rcue.ue_2
# Les UEs à afficher,
# qui
ues_ro = [
# Les UEs à afficher : on regarde si read only et si dispense (non ré-inscription à l'UE)
ues_ro_dispense = [
(
ue_impair,
rcue.ue_cur_impair is None,
deca.res_impair
and (deca.etud.id, ue_impair.id) in deca.res_impair.dispense_ues,
),
(
ue_pair,
rcue.ue_cur_pair is None,
deca.res_pair
and (deca.etud.id, ue_pair.id) in deca.res_pair.dispense_ues,
),
]
# Ordonne selon les dates des 2 semestres considérés:
if reverse_semestre:
ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0]
ues_ro_dispense[0], ues_ro_dispense[1] = (
ues_ro_dispense[1],
ues_ro_dispense[0],
)
# Colonnes d'UE:
for ue, ue_read_only in ues_ro:
for ue, ue_read_only, ue_dispense in ues_ro_dispense:
if ue:
H.append(
_gen_but_niveau_ue(
@ -134,6 +140,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
disabled=read_only or ue_read_only,
annee_prec=ue_read_only,
niveau_id=ue.niveau_competence.id,
ue_dispense=ue_dispense,
)
)
else:
@ -188,21 +195,30 @@ def _gen_but_niveau_ue(
disabled: bool = False,
annee_prec: bool = False,
niveau_id: int = None,
ue_dispense: bool = False,
) -> str:
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
moy_ue_str = f"""<span class="ue_cap">{
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
if ue_dispense:
etat_en_cours = """Non (ré)inscrit à cette UE"""
else:
etat_en_cours = f"""UE en cours
{ "sans notes" if np.isnan(dec_ue.moy_ue)
else
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
}
"""
scoplement = f"""<div class="scoplement">
<div>
<b>UE {ue.acronyme} capitalisée </b>
<span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
</span>
</div>
<div>UE en cours
{ "sans notes" if np.isnan(dec_ue.moy_ue)
else
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
}
<div>
{ etat_en_cours }
</div>
</div>
"""
@ -244,7 +260,13 @@ def _gen_but_niveau_ue(
</div>
"""
else:
scoplement = ""
if dec_ue.ue_status and dec_ue.ue_status["was_capitalized"]:
scoplement = """<div class="scoplement">
UE déjà capitalisée avec résultat moins favorable.
</div>
"""
else:
scoplement = ""
ue_class = "" # 'recorded' if dec_ue.code_valide is not None else ''
if dec_ue.code_valide is not None and dec_ue.codes:

View File

@ -75,7 +75,7 @@ class RegroupementCoherentUE:
else None
)
# Autres validations pour l'UE paire
# Autres validations pour les UEs paire/impaire
self.validation_ue_best_pair = best_autre_ue_validation(
etud.id,
niveau.id,
@ -101,14 +101,24 @@ class RegroupementCoherentUE:
"résultats formsemestre de l'UE si elle est courante, None sinon"
self.ue_status_impair = None
if self.ue_cur_impair:
# UE courante
ue_status = res_impair.get_etud_ue_status(etud.id, self.ue_cur_impair.id)
self.moy_ue_1 = ue_status["moy"] if ue_status else None # avec capitalisée
self.ue_1 = self.ue_cur_impair
self.res_impair = res_impair
self.ue_status_impair = ue_status
elif self.validation_ue_best_impair:
# UE capitalisée
self.moy_ue_1 = self.validation_ue_best_impair.moy_ue
self.ue_1 = self.validation_ue_best_impair.ue
if (
res_impair
and self.validation_ue_best_impair
and self.validation_ue_best_impair.ue
):
self.ue_status_impair = res_impair.get_etud_ue_status(
etud.id, self.validation_ue_best_impair.ue.id
)
else:
self.moy_ue_1, self.ue_1 = None, None
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0

View File

@ -438,7 +438,7 @@ class ResultatsSemestre(ResultatsCache):
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict | None:
"""L'état de l'UE pour cet étudiant.
Result: dict, ou None si l'UE n'est pas dans ce semestre.
Result: dict, ou None si l'UE n'existe pas ou n'est pas dans ce semestre.
{
"is_capitalized": # vrai si la version capitalisée est celle prise en compte (meilleure)
"was_capitalized":# si elle a été capitalisée (meilleure ou pas)
@ -456,6 +456,8 @@ class ResultatsSemestre(ResultatsCache):
}
"""
ue: UniteEns = db.session.get(UniteEns, ue_id)
if not ue:
return None
ue_dict = ue.to_dict()
if ue.type == UE_SPORT:

View File

@ -334,16 +334,14 @@ class Identite(models.ScoDocModel):
return f"{(self.nom_usuel or self.nom or '?').upper()} {(self.prenom or '')[:2].capitalize()}."
@cached_property
def sort_key(self) -> tuple:
def sort_key(self) -> str:
"clé pour tris par ordre alphabétique"
# Note: scodoc7 utilisait sco_etud.etud_sort_key, à mettre à jour
# si on modifie cette méthode.
return (
scu.sanitize_string(
self.nom_usuel or self.nom or "", remove_spaces=False
).lower(),
scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(),
)
return scu.sanitize_string(
(self.nom_usuel or self.nom or "") + ";" + (self.prenom or ""),
remove_spaces=False,
).lower()
def get_first_email(self, field="email") -> str:
"Le mail associé à la première adresse de l'étudiant, ou None"

View File

@ -208,7 +208,7 @@ class FormSemestre(models.ScoDocModel):
return cls.query.filter_by(id=formsemestre_id).first_or_404()
def sort_key(self) -> tuple:
"""clé pour tris par ordre alphabétique
"""clé pour tris par ordre de date_debut, le plus ancien en tête
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
return (self.date_debut, self.semestre_id)

View File

@ -460,7 +460,8 @@ def dictfilter(d, fields, filter_nulls=True):
# --- Misc Tools
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None: # XXX deprecated
# XXX deprecated, voir convert_fr_date
def DateDMYtoISO(dmy: str, null_is_empty=False) -> str | None:
"""Convert date string from french format (or ISO) to ISO.
If null_is_empty (default false), returns "" if no input.
"""

View File

@ -89,7 +89,7 @@ def formsemestre_bulletinetud_published_dict(
version="long",
) -> dict:
"""Dictionnaire representant les informations _publiees_ du bulletin de notes
Utilisé pour JSON, devrait l'être aussi pour XML. (todo)
Utilisé pour JSON des formations classiques (mais pas pour le XML, qui est deprecated).
version:
short (sans les évaluations)
@ -169,6 +169,21 @@ def formsemestre_bulletinetud_published_dict(
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
# Il serait préférable de factoriser et d'avoir la même section
# "semestre" que celle des bulletins BUT.
etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True
)
d["semestre"] = {
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
"date_debut": formsemestre.date_debut.isoformat(),
"date_fin": formsemestre.date_fin.isoformat(),
"annee_universitaire": formsemestre.annee_scolaire_str(),
"numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
"groupes": [group.to_dict() for group in etud_groups],
}
ues_stat = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls_dict()
nbetuds = len(nt.etud_moy_gen_ranks)

View File

@ -71,12 +71,10 @@ def report_debouche_date(start_year=None, fmt="html"):
etudids = get_etudids_with_debouche(start_year)
tab = table_debouche_etudids(etudids, keep_numeric=keep_numeric)
tab.filename = scu.make_filename("debouche_scodoc_%s" % start_year)
tab.origin = (
"Généré par %s le " % sco_version.SCONAME + scu.timedate_human_repr() + ""
)
tab.caption = "Récapitulatif débouchés à partir du 1/1/%s." % start_year
tab.base_url = "%s?start_year=%s" % (request.base_url, start_year)
tab.filename = scu.make_filename(f"debouche_scodoc_{start_year}")
tab.origin = f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}"
tab.caption = f"Récapitulatif débouchés à partir du 1/1/{start_year}."
tab.base_url = f"{request.base_url}?start_year={start_year}"
return tab.make_page(
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
init_qtip=True,
@ -118,7 +116,16 @@ def get_etudids_with_debouche(start_year):
def table_debouche_etudids(etudids, keep_numeric=True):
"""Rapport pour ces étudiants"""
L = []
rows = []
# Recherche les débouchés:
itemsuivi_etuds = {etudid: itemsuivi_list_etud(etudid) for etudid in etudids}
all_tags = set()
for debouche in itemsuivi_etuds.values():
if debouche:
for it in debouche:
all_tags.update(tag.strip() for tag in it["tags"].split(","))
all_tags = tuple(sorted(all_tags))
for etudid in etudids:
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
# retrouve le "dernier" semestre (au sens de la date de fin)
@ -152,25 +159,33 @@ def table_debouche_etudids(etudids, keep_numeric=True):
"sem_ident": "%s %s"
% (last_sem["date_debut_iso"], last_sem["titre"]), # utile pour tris
}
# recherche des débouchés
debouche = itemsuivi_list_etud(etudid) # liste de plusieurs items
debouche = itemsuivi_etuds[etudid] # liste de plusieurs items
if debouche:
row["debouche"] = "<br>".join(
[
str(it["item_date"])
+ " : "
+ it["situation"]
+ " <i>"
+ it["tags"]
+ "</i>"
for it in debouche
]
) #
if keep_numeric: # pour excel:
row["debouche"] = "\n".join(
f"""{it["item_date"]}: {it["situation"]}""" for it in debouche
)
else:
row["debouche"] = "<br>".join(
[
str(it["item_date"])
+ " : "
+ it["situation"]
+ " <i>"
+ it["tags"]
+ "</i>"
for it in debouche
]
)
for it in debouche:
for tag in it["tags"].split(","):
tag = tag.strip()
row[f"tag_{tag}"] = tag
else:
row["debouche"] = "non renseigné"
L.append(row)
L.sort(key=lambda x: x["sem_ident"])
rows.append(row)
rows.sort(key=lambda x: x["sem_ident"])
titles = {
"civilite": "",
@ -184,21 +199,25 @@ def table_debouche_etudids(etudids, keep_numeric=True):
"effectif": "Eff.",
"debouche": "Débouché",
}
columns_ids = [
"semestre",
"semestre_id",
"periode",
"civilite",
"nom",
"prenom",
"moy",
"rang",
"effectif",
"debouche",
]
for tag in all_tags:
titles[f"tag_{tag}"] = tag
columns_ids.append(f"tag_{tag}")
tab = GenTable(
columns_ids=(
"semestre",
"semestre_id",
"periode",
"civilite",
"nom",
"prenom",
"moy",
"rang",
"effectif",
"debouche",
),
columns_ids=columns_ids,
titles=titles,
rows=L,
rows=rows,
# html_col_width='4em',
html_sortable=True,
html_class="table_leftalign table_listegroupe",

View File

@ -79,21 +79,30 @@ def index_html(showcodes=0, showsemtable=0, export_table_formsemestres=False):
else:
html_table_formsemestres = None
current_formsemestres_by_modalite, modalites = (
sco_modalites.group_formsemestres_by_modalite(current_formsemestres)
)
return render_template(
"scolar/index.j2",
current_user=current_user,
current_formsemestres=current_formsemestres,
current_formsemestres_by_modalite=current_formsemestres_by_modalite,
dept_name=sco_preferences.get_preference("DeptName"),
formsemestres=formsemestres,
html_current_formsemestres=_show_current_formsemestres(
current_formsemestres, showcodes
emptygroupicon=scu.icontag(
"emptygroupicon_img", title="Pas d'inscrits", border="0"
),
formsemestres=formsemestres,
groupicon=scu.icontag("groupicon_img", title="Inscrits", border="0"),
html_table_formsemestres=html_table_formsemestres,
locked_formsemestres=locked_formsemestres,
modalites=modalites,
nb_locked=locked_formsemestres.count(),
nb_user_accounts=sco_users.get_users_count(dept=g.scodoc_dept),
page_title=f"ScoDoc {g.scodoc_dept}",
Permission=Permission,
scolar_news_summary=ScolarNews.scolar_news_summary_html(),
showcodes=showcodes,
showsemtable=showsemtable,
sco=ScoData(),
)
@ -116,6 +125,7 @@ def _convert_formsemestres_to_dicts(
lockicon = "X"
# génère liste de dict
sems = []
formsemestre: FormSemestre
for formsemestre in formsemestres:
nb_inscrits = len(formsemestre.inscriptions)
formation = formsemestre.formation
@ -151,61 +161,6 @@ def _convert_formsemestres_to_dicts(
return sems
def _show_current_formsemestres(formsemestres: Query, showcodes: bool) -> str:
"""html div avec les formsemestres courants de la page d'accueil"""
H = []
if formsemestres.count():
H.append("""<div class="scobox-title">Sessions en cours</div>""")
H.append(_sem_table(_convert_formsemestres_to_dicts(formsemestres, showcodes)))
else:
# aucun semestre courant: affiche aide
H.append(
"""
<div class="scobox-title">Aucune session en cours !</div>
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
</p>
<p>, en bas de page, suivez le lien
"<em>Mettre en place un nouveau semestre de formation...</em>"
</p>"""
)
return "\n".join(H)
def _sem_table(sems: list[dict]) -> str:
"""Affiche liste des semestres, utilisée pour semestres en cours"""
tmpl = f"""<tr class="%(trclass)s">%(tmpcode)s
<td class="semicon">%(lockimg)s <a href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept)}?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
<td class="datesem">%(mois_debut)s <a title="%(session_id)s">-</a> %(mois_fin)s</td>
<td class="titresem"><a class="stdlink" href="{url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept)}?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>
</td>
</tr>
"""
# Liste des semestres, groupés par modalités
sems_by_mod, modalites = sco_modalites.group_sems_by_modalite(sems)
H = ['<table class="listesems">']
for modalite in modalites:
if len(modalites) > 1:
H.append('<tr><th colspan="3">%s</th></tr>' % modalite["titre"])
if sems_by_mod[modalite["modalite"]]:
cur_idx = sems_by_mod[modalite["modalite"]][0]["semestre_id"]
for sem in sems_by_mod[modalite["modalite"]]:
if cur_idx != sem["semestre_id"]:
sem["trclass"] = "firstsem" # separe les groupes de semestres
cur_idx = sem["semestre_id"]
else:
sem["trclass"] = ""
H.append(tmpl % sem)
H.append("</table>")
return "\n".join(H)
def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable:
"""Table des semestres
Utilise une datatables.
@ -223,9 +178,9 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
columns_ids = (
"lockimg",
"published",
"dash_mois_fin",
"semestre_id_n",
"modalite",
"dash_mois_fin",
"titre_resp",
"nb_inscrits",
"formation",
@ -275,16 +230,8 @@ def _style_sems(sems: list[dict], fmt="html") -> list[dict]:
"""ajoute quelques attributs de présentation pour la table"""
is_h = fmt == "html"
if is_h:
icon_published = scu.icontag(
"eye_img",
border="0",
title="Bulletins publiés sur la passerelle étudiants",
)
icon_hidden = scu.icontag(
"hide_img",
border="0",
title="Bulletins NON publiés sur la passerelle étudiants",
)
icon_published = scu.ICON_PUBLISHED
icon_hidden = scu.ICON_HIDDEN
else:
icon_published = "publié"
icon_hidden = "non publié"

View File

@ -756,7 +756,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
H = [
html_sco_header.sco_header(
cssstyles=html_sco_header.BOOTSTRAP_MULTISELECT_CSS
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
+ ["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/ue_table.css"],
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ [
"libjs/jinplace-1.2.1.min.js",

View File

@ -122,16 +122,14 @@ def format_pays(s):
return ""
def etud_sort_key(etud: dict) -> tuple:
def etud_sort_key(etud: dict) -> str:
"""Clé de tri pour les étudiants représentés par des dict (anciens codes).
Equivalent moderne: identite.sort_key
"""
return (
scu.sanitize_string(
etud.get("nom_usuel") or etud["nom"] or "", remove_spaces=False
).lower(),
scu.sanitize_string(etud["prenom"] or "", remove_spaces=False).lower(),
)
return scu.sanitize_string(
(etud.get("nom_usuel") or etud["nom"] or "") + ";" + (etud["prenom"] or ""),
remove_spaces=False,
).lower()
_identiteEditor = ndb.EditableTable(

View File

@ -374,13 +374,7 @@ def evaluation_create_form(
args = tf[2]
# modifie le codage des dates
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
if args.get("jour"):
try:
date_debut = datetime.datetime.strptime(args["jour"], "%d/%m/%Y")
except ValueError as exc:
raise ScoValueError("Date (j/m/a) invalide") from exc
else:
date_debut = None
date_debut = scu.convert_fr_date(args["jour"]) if args.get("jour") else None
args["date_debut"] = date_debut
args["date_fin"] = date_debut # même jour
args.pop("jour", None)

View File

@ -812,14 +812,18 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
)
msg = ""
if tf[0] == 1:
# check dates
if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
msg = '<ul class="tf-msg"><li class="tf-msg">Dates de début et fin incompatibles !</li></ul>'
# convert and check dates
tf[2]["date_debut"] = scu.convert_fr_date(tf[2]["date_debut"])
tf[2]["date_fin"] = scu.convert_fr_date(tf[2]["date_fin"])
if tf[2]["date_debut"] > tf[2]["date_fin"]:
msg = """<ul class="tf-msg">
<li class="tf-msg">Dates de début et fin incompatibles !</li>
</ul>"""
if (
sco_preferences.get_preference("always_require_apo_sem_codes")
and not any(
[tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)]
tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)
)
# n'impose pas d'Apo pour les sem. extérieurs
and ((formsemestre is None) or formsemestre.modalite != "EXT")

View File

@ -1495,7 +1495,12 @@ def formsemestre_note_etuds_sans_notes(
</div>
{message}
<form method="post">
<style>
.sco-std-form select, .sco-std-form input[type="submit"] {{
height: 24px;
}}
</style>
<form class="sco-std-form" method="post">
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}">
<input type="hidden" name="etudid" value="{etudid or ""}">
@ -1506,7 +1511,7 @@ def formsemestre_note_etuds_sans_notes(
<option value="ATT" selected>ATT (en attente)</option>
<option value="EXC">EXC (neutralisée)</option>
</select>
<input type="submit" name="enregistrer">
<input type="submit" value="Enregistrer">
</form>
{html_sco_header.sco_footer()}
"""

View File

@ -36,37 +36,45 @@ Elle n'est pas utilisée pour les parcours, ni pour rien d'autre
import collections
import app.scodoc.notesdb as ndb
from app import log
from app.models import FormSemestre
def list_formsemestres_modalites(sems):
def list_formsemestres_modalites(formsemestres: list[FormSemestre]) -> list[dict]:
"""Liste ordonnée des modalités présentes dans ces formsemestres"""
modalites = {}
for sem in sems:
if sem["modalite"] not in modalites:
m = do_modalite_list(args={"modalite": sem["modalite"]})[0]
for formsemestre in formsemestres:
if formsemestre.modalite not in modalites:
m = do_modalite_list(args={"modalite": formsemestre.modalite})[0]
modalites[m["modalite"]] = m
modalites = list(modalites.values())
modalites.sort(key=lambda x: x["numero"])
return modalites
def group_sems_by_modalite(sems: list[dict]):
def group_formsemestres_by_modalite(
formsemestres: list[FormSemestre],
) -> dict[str, list[FormSemestre]]:
"""Given the list of formsemestre, group them by modalite,
sorted in each one by semestre id and date
"""
sems_by_mod = collections.defaultdict(list)
modalites = list_formsemestres_modalites(sems)
for modalite in modalites:
for sem in sems:
if sem["semestre_id"] < 0: # formations en un semestre
sem["sortkey"] = (-100 * sem["semestre_id"], sem["dateord"])
else:
sem["sortkey"] = (sem["semestre_id"], sem["dateord"])
if sem["modalite"] == modalite["modalite"]:
sems_by_mod[modalite["modalite"]].append(sem)
modalites = list_formsemestres_modalites(formsemestres)
sems_by_mod = {
modalite["modalite"]: [
formsemestre
for formsemestre in formsemestres
if formsemestre.modalite == modalite["modalite"]
]
for modalite in modalites
}
# tri dans chaque modalité par indice de semestre et date debut
for modalite in modalites:
sems_by_mod[modalite["modalite"]].sort(key=lambda x: x["sortkey"])
sems_by_mod[modalite["modalite"]].sort(
key=lambda x: (
x.semestre_id if x.semestre_id > 0 else -1000 * x.semestre_id,
x.date_debut,
)
)
return sems_by_mod, modalites

View File

@ -109,6 +109,38 @@ ETATS_INSCRIPTION = {
}
def convert_fr_date(date_str: str, allow_iso=True) -> datetime.datetime:
"""Converti une date saisie par un humain français avant 2070
en un objet datetime.
12/2/1972 => 1972-02-12, 12/2/72 => 1972-02-12, mais 12/2/24 => 2024-02-12
Le pivot est 70.
ScoValueError si date invalide.
"""
try:
return datetime.datetime.strptime(date_str, "%d/%m/%Y")
except ValueError:
# Try to add century ?
m = re.match(r"^(\d{1,2})/(\d{1,2})/(\d\d)$", date_str)
if m:
year = int(m.group(3))
if year < 70:
year += 2000
else:
year += 1900
try:
return datetime.datetime.strptime(
f"{m.group(1)}/{m.group(2)}/{year}", "%d/%m/%Y"
)
except ValueError:
pass
if allow_iso:
try:
return datetime.datetime.fromisoformat(date_str)
except ValueError as exc:
raise ScoValueError("Date (j/m/a or ISO) invalide") from exc
raise ScoValueError("Date (j/m/a) invalide")
def print_progress_bar(
iteration,
total,
@ -1444,6 +1476,15 @@ def icontag(name, file_format="png", no_size=False, **attrs):
ICON_PDF = icontag("pdficon16x20_img", title="Version PDF")
ICON_XLS = icontag("xlsicon_img", title="Export tableur (xlsx)")
ICON_PUBLISHED = """<img src="/ScoDoc/static/icons/eye_visible_green.svg"
width="24" height="19" border="0"
title="Bulletins publiés sur la passerelle étudiants"
alt="Bulletins publiés sur la passerelle étudiants" />"""
ICON_HIDDEN = """<img src="/ScoDoc/static/icons/eye_hidden.svg"
width="24" height="19" border="0"
title="Bulletins NON publiés sur la passerelle étudiants"
alt="Bulletins NON publiés sur la passerelle étudiants" />"""
# HTML emojis
EMO_WARNING = "&#9888;&#65039;" # warning /!\
EMO_RED_TRIANGLE_DOWN = "&#128315;" # red triangle pointed down

View File

@ -449,7 +449,7 @@
transform: translateX(-50%);
}
.assiduite-infos {
.assiduite-actions {
position: absolute;
right: 0;
margin: 5px;

View File

@ -19,6 +19,13 @@
font-weight: bold;
}
ul.modimpls_att {
margin-top: 8px;
margin-left: 32px;
padding-top: 0;
color: black;
}
.jury_but h3 {
margin-top: 0px;
}
@ -272,4 +279,10 @@ div.but_doc table tr td.amue {
.but_autorisations_passage.but_explanation {
font-weight: normal;
color: var(--color-explanation);
}
.deca-expl {
font-size: 110%;
margin-bottom: 8px;
margin-left: 16px;
}

File diff suppressed because it is too large Load Diff

217
app/static/css/ue_table.css Normal file
View File

@ -0,0 +1,217 @@
div.formation_descr {
background-color: rgb(250, 250, 240);
border: 1px solid rgb(128, 128, 128);
padding-left: 5px;
padding-bottom: 5px;
margin-right: 12px;
}
div.formation_descr span.fd_t {
font-weight: bold;
margin-right: 5px;
}
div.formation_descr span.fd_n {
font-weight: bold;
font-style: italic;
color: green;
margin-left: 6em;
}
div.formation_ue_list {
border: 1px solid black;
background-color: rgb(232, 249, 255);
margin-top: 5px;
margin-right: 12px;
padding-left: 5px;
}
div.formation_list_ues_titre {
padding-top: 6px;
padding-bottom: 6px;
padding-left: 24px;
padding-right: 24px;
font-size: 120%;
font-weight: bold;
border-top-right-radius: 18px;
border-top-left-radius: 18px;
background-color: #0051a9;
color: #eee;
}
div.formation_list_modules,
div.formation_list_ues {
border-radius: 18px;
margin-left: 10px;
margin-right: 10px;
margin-bottom: 10px;
padding-bottom: 1px;
}
div.formation_list_ues {
background-color: #b7d2fa;
margin-top: 20px;
}
div.formation_list_ues_content {
margin-top: 4px;
}
div.formation_list_modules {
margin-top: 20px;
}
div.formation_list_modules_RESSOURCE {
background-color: var(--sco-color-ressources);
}
div.formation_list_modules_SAE {
background-color: var(--sco-color-saes);
}
div.formation_list_modules_STANDARD {
background-color: var(--sco-color-mod-std);
}
div.formation_list_modules_titre {
padding-left: 24px;
padding-right: 24px;
font-weight: bold;
font-size: 120%;
}
div.formation_list_ues ul.notes_module_list {
margin-top: 0px;
margin-bottom: -1px;
padding-top: 5px;
padding-bottom: 5px;
}
div.formation_list_modules ul.notes_module_list {
margin-top: 0px;
margin-bottom: -1px;
padding-top: 5px;
padding-bottom: 5px;
}
span.missing_ue_ects {
color: red;
font-weight: bold;
}
span.niveau-nom {
color: black;
}
span.niveau-nom>span {
text-decoration: dashed underline;
}
.formation_apc_infos ul li:not(:last-child) {
margin-bottom: 6px;
}
div.formation_parcs {
display: inline-flex;
margin-left: 8px;
margin-right: 8px;
column-gap: 8px;
}
div.formation_parcs>div {
font-size: 100%;
color: white;
background-color: #09c;
opacity: 0.7;
border-radius: 4px;
text-align: center;
padding: 2px 6px;
margin-top: 8px;
margin-bottom: 2px;
}
div.formation_parcs>div.ue_tc {
color: black;
font-style: italic;
}
div.formation_parcs>div.focus {
opacity: 1;
}
div.formation_parcs>div>a:hover {
color: #ccc;
}
div.formation_parcs>div>a,
div.formation_parcs>div>a:visited {
color: white;
}
div.ue_choix_niveau>div.formation_parcs>div {
font-size: 80%;
}
div.ue_list_tit {
font-weight: bold;
margin-top: 8px;
}
div.ue_list_tit form {
display: inline-block;
}
div.ue_list_tit span.lock_info {
color: red;
margin-left: 8px;
}
ul.apc_ue_list {
background-color: rgba(180, 189, 191, 0.14);
margin-left: 8px;
margin-right: 8px;
}
ul.notes_ue_list {
margin-top: 4px;
margin-right: 1em;
margin-left: 1em;
/* padding-top: 1em; */
padding-bottom: 1em;
font-weight: bold;
}
.formation_classic_infos ul.notes_ue_list {
padding-top: 0px;
}
.formation_classic_infos li.notes_ue_list {
margin-top: 9px;
list-style-type: none;
border: 1px solid maroon;
border-radius: 10px;
padding-bottom: 5px;
}
li.module_malus span.formation_module_tit {
color: red;
font-weight: bold;
text-decoration: underline;
}
span.invalid-module-type {
color: red;
font-style: italic;
}
span.formation_module_ue {
color: #6e7d92;
font-size: 75%;
}
span.notes_module_list_buts {
margin-right: 5px;
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Capa_1"
enable-background="new 0 0 511.985 511.985"
height="349.46161"
viewBox="0 0 511.985 349.45137"
width="512"
version="1.1"
sodipodi:docname="eye_hidden.svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4" />
<g
id="g4"
transform="translate(5e-4,-81.266625)">
<g
id="g3">
<g
id="Layer_2_00000011005494452173574160000003459929325544675728_">
<g
id="hide_00000049195420225258176850000009593604245528296082_">
<path
d="m 255.992,406.518 c -110.53,0 -208.51,-87.52 -245.5,-125.15 -13.99,-14.024 -13.99,-36.726 0,-50.75 37,-37.63 135,-125.16 245.5,-125.16 110.5,0 208.51,87.53 245.5,125.16 13.99,14.024 13.99,36.726 0,50.75 -37,37.63 -134.98,125.15 -245.5,125.15 z m 0,-271.06 c -99.18,0 -189.76,81.26 -224.11,116.19 -2.397,2.352 -2.434,6.201 -0.082,8.598 0.027,0.028 0.054,0.055 0.082,0.082 34.34,34.94 124.92,116.19 224.11,116.19 99.19,0 189.75,-81.25 224.1,-116.19 2.397,-2.352 2.434,-6.201 0.082,-8.598 -0.027,-0.028 -0.054,-0.055 -0.082,-0.082 -34.35,-34.93 -124.93,-116.19 -224.1,-116.19 z"
style="fill:red;"
id="path1" />
<path
d="m 255.992,346.268 c -49.871,0.006 -90.304,-40.419 -90.31,-90.29 -0.006,-49.871 40.419,-90.304 90.29,-90.31 49.871,-0.006 90.304,40.419 90.31,90.29 v 0.01 c -0.055,49.845 -40.445,90.239 -90.29,90.3 z m 0,-150.59 c -33.303,-0.006 -60.304,26.987 -60.31,60.29 -0.006,33.303 26.987,60.304 60.29,60.31 33.303,0.006 60.304,-26.987 60.31,-60.29 0,-0.007 0,-0.013 0,-0.02 -0.05,-33.273 -27.017,-60.231 -60.29,-60.27 z"
style="fill:red;"
id="path2" />
<path
d="m 96.262,430.718 c -8.284,0.004 -15.003,-6.709 -15.007,-14.993 -0.002,-3.982 1.58,-7.802 4.397,-10.617 l 319.45,-319.45 c 5.86,-5.857 15.358,-5.855 21.215,0.005 5.857,5.86 5.855,15.358 -0.005,21.215 l -319.45,319.44 c -2.806,2.819 -6.621,4.403 -10.6,4.4 z"
style="fill:red;"
id="path3" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Layer_1"
enable-background="new 0 0 512 512"
height="318.62299"
viewBox="0 0 474.22501 318.62299"
width="474.22501"
version="1.1"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
clip-rule="evenodd"
d="m 441.578,168.115 c -41.315,74.193 -119.667,120.282 -204.438,120.282 -84.826,0 -163.177,-46.089 -204.493,-120.282 -3.182,-5.761 -3.182,-11.851 0,-17.607 C 73.963,76.316 152.314,30.232 237.14,30.232 c 84.771,0 163.122,46.084 204.438,120.276 3.238,5.756 3.238,11.846 0,17.607 z M 467.97,135.798 C 421.332,52.031 332.885,0 237.14,0 141.34,0 52.893,52.031 6.255,135.798 c -8.34,14.946 -8.34,32.081 0,47.016 46.638,83.767 135.085,135.809 230.885,135.809 95.745,0 184.192,-52.042 230.83,-135.809 8.34,-14.934 8.34,-32.07 0,-47.016 z m -230.83,85.528 c 34.183,0 62.001,-27.818 62.001,-62.017 0,-34.199 -27.818,-62.017 -62.001,-62.017 -34.238,0 -62.056,27.818 -62.056,62.017 0,34.199 27.819,62.017 62.056,62.017 z m 0,-154.266 c -50.918,0 -92.288,41.387 -92.288,92.25 0,50.874 41.371,92.244 92.288,92.244 50.863,0 92.233,-41.371 92.233,-92.244 0,-50.863 -41.37,-92.25 -92.233,-92.25 z"
fill-rule="evenodd"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="Layer_1"
enable-background="new 0 0 512 512"
height="318.62299"
viewBox="0 0 474.22501 318.62299"
width="474.22501"
version="1.1"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
clip-rule="evenodd"
d="m 441.578,168.115 c -41.315,74.193 -119.667,120.282 -204.438,120.282 -84.826,0 -163.177,-46.089 -204.493,-120.282 -3.182,-5.761 -3.182,-11.851 0,-17.607 C 73.963,76.316 152.314,30.232 237.14,30.232 c 84.771,0 163.122,46.084 204.438,120.276 3.238,5.756 3.238,11.846 0,17.607 z M 467.97,135.798 C 421.332,52.031 332.885,0 237.14,0 141.34,0 52.893,52.031 6.255,135.798 c -8.34,14.946 -8.34,32.081 0,47.016 46.638,83.767 135.085,135.809 230.885,135.809 95.745,0 184.192,-52.042 230.83,-135.809 8.34,-14.934 8.34,-32.07 0,-47.016 z m -230.83,85.528 c 34.183,0 62.001,-27.818 62.001,-62.017 0,-34.199 -27.818,-62.017 -62.001,-62.017 -34.238,0 -62.056,27.818 -62.056,62.017 0,34.199 27.819,62.017 62.056,62.017 z m 0,-154.266 c -50.918,0 -92.288,41.387 -92.288,92.25 0,50.874 41.371,92.244 92.288,92.244 50.863,0 92.233,-41.371 92.233,-92.244 0,-50.863 -41.37,-92.25 -92.233,-92.25 z"
fill-rule="evenodd"
style="fill: rgb(29, 124, 39);"
id="path1" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-34.1074" y1="645.6909" x2="-34.1074" y2="622.0029" gradientTransform="matrix(20.48 0 0 -20.48 954.52 13234.4395)">
<stop offset="0" style="stop-color:#FF9400"/>
<stop offset="1" style="stop-color:#FF5F39"/>
</linearGradient>
<path style="fill:url(#SVGID_1_);" d="M296.182,382.792c-1.577,3.83-5.325,6.328-9.462,6.328h-20.48h-20.48h-20.48
c-4.137,0-7.885-2.499-9.462-6.328c-1.577-3.83-0.696-8.233,2.232-11.162l17.469-17.469V239.759l-17.469-17.49
c-2.929-2.929-3.809-7.332-2.232-11.162c1.577-3.83,5.325-6.308,9.462-6.308h20.48H256h10.24c5.652,0,10.24,4.567,10.24,10.24
v139.121l17.49,17.469C296.878,374.559,297.759,378.962,296.182,382.792 M256,122.88c16.937,0,30.72,13.763,30.72,30.72
c0,16.937-13.783,30.72-30.72,30.72s-30.72-13.783-30.72-30.72C225.28,136.643,239.063,122.88,256,122.88 M256,0
C114.852,0,0,114.831,0,256c0,141.148,114.852,256,256,256c141.169,0,256-114.852,256-256C512,114.831,397.169,0,256,0"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -217,13 +217,11 @@ function creerLigneEtudiant(etud, index) {
const nameField = document.createElement("div");
nameField.classList.add("name_field");
if ($("#pdp").is(":checked")) {
const pdp = document.createElement("img");
pdp.src = `../../api/etudiant/etudid/${etud.id}/photo?size=small`;
pdp.alt = `${etud.nom} ${etud.prenom}`;
pdp.classList.add("pdp");
nameField.appendChild(pdp);
}
const pdp = document.createElement("img");
pdp.src = `../../api/etudiant/etudid/${etud.id}/photo?size=small`;
pdp.alt = `${etud.nom} ${etud.prenom}`;
pdp.classList.add("pdp");
nameField.appendChild(pdp);
const nameSet = document.createElement("a");
nameSet.classList.add("name_set");
@ -857,13 +855,25 @@ function setupAssiduiteBubble(el, assiduite) {
// Ajout d'un lien pour plus d'informations
const infos = document.createElement("a");
infos.className = "assiduite-infos";
infos.className = "";
infos.textContent = ``;
infos.title = "Cliquez pour plus d'informations";
infos.target = "_blank";
infos.href = `tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assiduite.assiduite_id}`;
bubble.appendChild(infos);
// Ajout d'un lien pour modifier l'assiduité
const modifs = document.createElement("a");
modifs.className = "";
modifs.textContent = `📝`;
modifs.title = "Cliquez pour modifier l'assiduité";
modifs.target = "_blank";
modifs.href = `tableau_assiduite_actions?type=assiduite&action=modifier&obj_id=${assiduite.assiduite_id}`;
const actionsDiv = document.createElement("div");
actionsDiv.className = "assiduite-actions";
actionsDiv.appendChild(modifs);
actionsDiv.appendChild(infos);
bubble.appendChild(actionsDiv);
const idDiv = document.createElement("div");
idDiv.className = "assiduite-id";

View File

@ -391,11 +391,11 @@ class RowAssiJusti(tb.Row):
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
date_affichees: list[str] = [
self.ligne["date_debut"].strftime("%d/%m/%y de %H:%M"), # date début
self.ligne["date_fin"].strftime("%d/%m/%y de %H:%M"), # date fin
self.ligne["date_debut"].strftime("%d/%m/%y %H:%M"), # date début
self.ligne["date_fin"].strftime("%d/%m/%y %H:%M"), # date fin
]
if multi_days:
if multi_days and self.ligne["type"] != "justificatif":
date_affichees[0] = self.ligne["date_debut"].strftime("%d/%m/%y")
date_affichees[1] = self.ligne["date_fin"].strftime("%d/%m/%y")

View File

@ -74,8 +74,6 @@ div.submit > input {
<div>
{{ form.date_fin.label }}&nbsp;: {{ form.date_fin }}
<span class="help">si le jour de fin est différent,
les heures seront ignorées (journées complètes)</span>
{{ render_field_errors(form, 'date_fin') }}
</div>
</div>

View File

@ -20,7 +20,7 @@
#actions {
flex-direction: row;
align-items: center;
margin-bottom: 5px;
margin: 5px 0;
}
#actions label{
margin: 0;
@ -174,6 +174,48 @@ async function nouvellePeriode(period = null) {
}
}
// Vérification de la plage horaire
// On génère une date de début et de fin de la période
const date_debut = new Date(
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + debut
);
const date_fin = new Date(
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + fin
);
date_debut.add(1, "seconds");
// On vérifie que les dates sont valides
if (!date_debut.isValid()){
const p = document.createElement("p");
p.textContent = "La date de début n'est pas valide.";
openAlertModal(
"Erreur",
p,
);
return;
}
if (!date_fin.isValid()){
const p = document.createElement("p");
p.textContent = "La date de fin n'est pas valide.";
openAlertModal(
"Erreur",
p,
);
return;
}
// On vérifie que l'heure de fin est supérieure à l'heure de début
if (date_debut >= date_fin) {
const p = document.createElement("p");
p.textContent = "La plage horaire n'est pas valide. L'heure de fin doit être "+
"supérieure à l'heure de début.";
openAlertModal(
"Erreur",
p,
);
return;
}
// On ajoute la nouvelle période au tableau
let periodeDiv = document.createElement("div");
periodeDiv.classList.add("cell", "header");
@ -211,15 +253,6 @@ async function nouvellePeriode(period = null) {
...document.querySelectorAll(".ligne[data-etudid]"),
].map((e) => e.getAttribute("data-etudid"));
// On génère une date de début et de fin de la période
const date_debut = new Date(
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + debut
);
const date_fin = new Date(
$("#date").datepicker("getDate").format("YYYY-MM-DD") + "T" + fin
);
date_debut.add(1, "seconds");
// Préparation de la requête
const url =
`../../api/assiduites/group/query?date_debut=${date_debut.toFakeIso()}` +

View File

@ -54,7 +54,8 @@
}
document.getElementById("pdp").addEventListener("change", (e) => {
creerTousLesEtudiants(etuds);
afficherPDP(e.target.checked);
//creerTousLesEtudiants(etuds);
});
$('#date').on('change', async function(d) {
@ -87,6 +88,8 @@
}
creerTousLesEtudiants(etuds);
// affichage ou non des PDP
afficherPDP(localStorage.getItem("scodoc-etud-pdp") == "true" )
}
setTimeout(main, 0);

View File

@ -12,7 +12,7 @@
<h2>Calcul automatique des décisions de jury du BUT</h2>
<ul>
<li>N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval" sur deux années.
si on a des RCUEs "à cheval" sur deux années.
</li>
<li><b>Attention: peut modifier des décisions déjà enregistrées</b>, si la
@ -47,6 +47,20 @@
</div>
{% if formsemestres_suspects %}
<div class="scobox">
<div class="scobox-title">Attention</div>
<div class="warning">Les semestres pairs suivants vont être pris en compte, mais ils sont postérieurs et n'ont pas leurs moyennes bloquées
(voir la documentation sur les jurys BUT).
</div>
<ul>
{% for formsemestre in formsemestres_suspects.values() %}
<li>{{ formsemestre.html_link_status() | safe }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="row">
<div class="col-md-10">
{{ wtf.quick_form(form) }}

View File

@ -28,9 +28,9 @@
<a href="{{url_for('notes.formsemestre_change_publication_bul', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}">
{% if sco.sem.bul_hide_xml %}
{{ scu.icontag("hide_img", border="0", title="Bulletins NON publiés sur la passerelle étudiants")|safe}}
{{ scu.ICON_HIDDEN|safe}}
{% else %}
{{ scu.icontag("eye_img", border="0", title="Bulletins publiés sur la passerelle étudiants")|safe }}
{{ scu.ICON_PUBLISHED|safe }}
{% endif %}
{% endif %}
</span>

View File

@ -31,9 +31,9 @@
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )
}}">{%-
if formsemestre.bul_hide_xml -%}
{{scu.icontag("hide_img", border="0", title="Bulletins NON publiés sur la passerelle étudiants")|safe}}
{{scu.ICON_HIDDEN|safe}}
{%- else -%}
{{scu.icontag("eye_img", border="0", title="Bulletins publiés sur la passerelle étudiants")|safe}}
{{scu.ICON_PUBLISHED|safe}}
{%- endif -%}
</a></span>
</div>

View File

@ -16,9 +16,13 @@ table.listesems tr td.titresem {
font-weight: bold;
font-size: 110%;
}
div.semlist {
padding-right: 8px;
}
table.semlist tr td.datesem {
font-size: 80%;
text-align: center;
white-space: nowrap;
}
table.semlist tr td.semestre_id_n {
@ -28,6 +32,124 @@ table.semlist tr td.nb_inscrits {
text-align: center;
}
div#gtrcontent table.semlist tbody tr.css_S-1 td {
background-color:rgb(176, 214, 226);
}
div#gtrcontent table.semlist tbody tr.css_S1 td {
background-color:#e9efef;
}
div#gtrcontent table.semlist tbody tr.css_S2 td {
background-color: #d4ebd7;
}
div#gtrcontent table.semlist tbody tr.css_S3 td {
background-color: #bedebe;
}
div#gtrcontent table.semlist tbody tr.css_S4 td {
background-color: #afd7ad;
}
div#gtrcontent table.semlist tbody tr.css_S5 td {
background-color: #a0cd9a;
}
div#gtrcontent table.semlist tbody tr.css_S6 td {
background-color: #7dcf78;
}
div#gtrcontent table.semlist tbody tr.css_MEXT td {
color: #fefcdf;
}
table.semlist tr td {
border: none;
}
table.semlist tbody tr a.stdlink,
table.semlist tbody tr a.stdlink:visited {
color: navy;
text-decoration: none;
}
div#gtrcontent table.semlist tr a.stdlink:hover {
color: red;
text-decoration: underline;
}
table.semlist tbody tr td.modalite {
text-align: left;
padding-right: 1em;
}
<<<<<<< HEAD
div.modalite {
font-size: 16px;
font-weight: bold;
}
span.effectif {
display: inline-block;
min-width: 24px;
text-align: right;
}
.cur-formsemestres {
width: max-content; /* Fits content, but respects max-width */
max-width: 1024px; /* Maximum width */
margin: 0 auto 0 0; /* Centers divs if they are narrower than 1024px */
}
.cur-formsemestre {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #ddd;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;;
background-color: rgb(246, 255, 254);
padding: 0px 8px 0px 8px;
margin: 0px;
}
.cur-formsemestre.new-sem {
margin-top: 8px;
border-top: 1px solid #ddd;;
}
.left-section {
display: flex;
align-items: center;
}
.date {
display: flex;
flex-direction: column;
margin-left: 10px;
font-size: 12px;
}
.cur-formsemestre .title {
flex-grow: 1;
text-align: left;
margin: 0 20px;
display: flex;
align-items: center;
font-size: 16px;
}
.right-section {
display: flex;
flex-direction: column;
font-size: 14px;
}
.responsable {
font-weight: bold;
color: navy;
}
=======
>>>>>>> b1055a4ebe841f17860d2556cc4b03aa12ec3ab1
</style>
{# News #}
@ -48,7 +170,43 @@ table.semlist tr td.nb_inscrits {
{# Les semestres courants (cad non verrouillés) #}
<div class="scobox">
{{html_current_formsemestres|safe}}
{% if current_formsemestres.count() == 0 %}
<div class="scobox-title">Aucune session en cours !</div>
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
</p>
<p>Là, en bas de page, suivez le lien
"<em>Mettre en place un nouveau semestre de formation...</em>"
</p>
{% else %}
<div class="scobox-title">Sessions en cours</div>
<div class="cur-formsemestres">
{% for modalite in modalites %}
{% if modalites|length > 1 %}
<div class="modalite">{{modalite.titre}}</div>
{% endif %}
{% for formsemestre in current_formsemestres_by_modalite[modalite.modalite] %}
<div class="cur-formsemestre {{'new-sem' if loop.first or formsemestre.semestre_id != loop.previtem.semestre_id}}">
<div class="left-section">
{{groupicon|safe if formsemestre.inscriptions|length else emptygroupicon|safe}}
<div class="date">
<div class="date-begin"><a title="{{formsemestre.session_id()}}">{{formsemestre.mois_debut()}}</a></div>
<div class="date-end">{{formsemestre.mois_fin()}}</div>
</div>
</div>
<div class="title">
<a class="stdlink" href="{{ url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)}}">{{formsemestre.titre_num()}}</a>
</div>
<div class="right-section">
<div class="responsable">{{formsemestre.responsables_str()}}</div>
<div class="effectif">{{formsemestre.inscriptions|length}} étuds</div>
</div>
</div>
{% endfor %}
{% endfor %}
</div>
{% endif %}
</div>
{# Table de tous les semestres #}
@ -62,7 +220,9 @@ table.semlist tr td.nb_inscrits {
url_for('scolar.export_table_dept_formsemestres', scodoc_dept=g.scodoc_dept)
}}">{{scu.ICON_XLS|safe}}</a>
</summary>
{{ html_table_formsemestres|safe }}
<div class="semlist">
{{ html_table_formsemestres|safe }}
</div>
</details>
{% else %}
<p><a class="stdlink" href="{{

View File

@ -86,7 +86,7 @@ span.calendarEdit {
let etudiants = await fetchData("/ScoDoc/{{formsemestre.departement.acronym}}/api/formsemestre/" + formsemestre_id + "/resultats");
etudiants.sort((a, b) => {
return a.nom_short.localeCompare(b.nom_short)
return a.sort_key.localeCompare(b.sort_key)
})
processDatas(partitions, etudiants);

View File

@ -299,7 +299,7 @@ def ajout_assiduite_etud() -> str | Response:
def _get_dates_from_assi_form(
form: AjoutAssiOrJustForm,
all_day: bool = False,
from_justif: bool = False,
) -> tuple[
bool, datetime.datetime | None, datetime.datetime | None, datetime.datetime | None
]:
@ -327,32 +327,15 @@ def _get_dates_from_assi_form(
date_fin = None
form.set_error("date fin invalide", form.date_fin)
if date_fin:
# ignore les heures si plusieurs jours
# Assiduité : garde les heures inscritent dans le formulaire
# Justificatif : ignore les heures inscrites dans le formulaire (0h -> 23h59)
heure_debut = (
datetime.time.fromisoformat(debut_jour)
if not all_day
else datetime.time(0, 0, 0)
) # 0h ou ConfigAssiduite.MorningTime
heure_fin = (
datetime.time.fromisoformat(fin_jour)
if not all_day
else datetime.time(23, 59, 59)
) # 23h59 ou ConfigAssiduite.AfternoonTime
if not from_justif and date_fin:
# Ne prends pas en compte les heures pour les assiduités sur plusieurs jours
heure_debut = datetime.time.fromisoformat(debut_jour)
heure_fin = datetime.time.fromisoformat(fin_jour)
else:
try:
if all_day:
heure_debut = datetime.time.fromisoformat(
form.heure_debut.data or "00:00"
)
else:
heure_debut = datetime.time.fromisoformat(
form.heure_debut.data or debut_jour
)
heure_debut = datetime.time.fromisoformat(
form.heure_debut.data or debut_jour
)
except ValueError:
form.set_error("heure début invalide", form.heure_debut)
if bool(form.heure_debut.data) != bool(form.heure_fin.data):
@ -360,10 +343,7 @@ def _get_dates_from_assi_form(
"Les deux heures début et fin doivent être spécifiées, ou aucune"
)
try:
if all_day:
heure_fin = datetime.time.fromisoformat(form.heure_fin.data or "23:59")
else:
heure_fin = datetime.time.fromisoformat(form.heure_fin.data or fin_jour)
heure_fin = datetime.time.fromisoformat(form.heure_fin.data or fin_jour)
except ValueError:
form.set_error("heure fin invalide", form.heure_fin)
@ -398,6 +378,19 @@ def _get_dates_from_assi_form(
# Ajoute time zone serveur
dt_debut_tz_server = scu.TIME_ZONE.localize(dt_debut)
dt_fin_tz_server = scu.TIME_ZONE.localize(dt_fin)
if from_justif:
cas: list[bool] = [
# cas 1 (date de fin vide et pas d'heure de début)
not form.date_fin.data and not form.heure_debut.data,
# cas 2 (date de fin et pas d'heures)
form.date_fin.data != "" and not form.heure_debut.data,
]
if any(cas):
dt_debut_tz_server = dt_debut_tz_server.replace(hour=0, minute=0)
dt_fin_tz_server = dt_fin_tz_server.replace(hour=23, minute=59)
dt_entry_date_tz_server = (
scu.TIME_ZONE.localize(dt_entry_date) if dt_entry_date else None
)
@ -753,6 +746,34 @@ def ajout_justificatif_etud():
)
def _verif_date_form_justif(
form: AjoutJustificatifEtudForm, deb: datetime.datetime, fin: datetime.datetime
) -> tuple[datetime.datetime, datetime.datetime]:
"""Gère les cas suivants :
- si on indique seulement une date de debut : journée 0h-23h59
- si on indique date de debut et heures : journée +heure deb/fin
(déjà géré par _get_dates_from_assi_form)
- Si on indique une date de début et de fin sans heures : Journées 0h-23h59
- Si on indique une date de début et de fin avec heures : On fait un objet avec
datedeb/heuredeb + datefin/heurefin (déjà géré par _get_dates_from_assi_form)
"""
cas: list[bool] = [
# cas 1
not form.date_fin.data and not form.heure_debut.data,
# cas 3
form.date_fin.data != "" and not form.heure_debut.data,
]
if any(cas):
deb = deb.replace(hour=0, minute=0)
fin = fin.replace(hour=23, minute=59)
print(f"DEBUG {cas=}")
return deb, fin
def _record_justificatif_etud(
etud: Identite, form: AjoutJustificatifEtudForm, justif: Justificatif | None = None
) -> bool:
@ -769,7 +790,7 @@ def _record_justificatif_etud(
dt_debut_tz_server,
dt_fin_tz_server,
dt_entry_date_tz_server,
) = _get_dates_from_assi_form(form, all_day=True)
) = _get_dates_from_assi_form(form, from_justif=True)
if not ok:
log("_record_justificatif_etud: dates invalides")
form.set_error("Erreur: dates invalides")
@ -831,11 +852,6 @@ def _record_justificatif_etud(
db.session.rollback()
return False
db.session.commit()
# FIX TEMPORAIRE:
# on reprend toutes les assiduités et tous les justificatifs
# pour utiliser le "reset" (remise en "non_just") des assiduités
# (à terme, il faudrait ne recalculer que les assiduités impactées)
# VOIR TODO dans compute_assiduites_justified
justif.justifier_assiduites()
scass.simple_invalidate_cache(justif.to_dict(), etud.id)
flash(message)

View File

@ -30,6 +30,7 @@ Module notes: issu de ScoDoc7 / ZNotes.py
Emmanuel Viennet, 2021
"""
import datetime
import html
from operator import itemgetter
import time
@ -2392,17 +2393,16 @@ def formsemestre_validation_but(
<div class="bull_head">
<div>
<div class="titre_parcours">Jury BUT</div>
<div class="nom_etud">{etud.nomprenom}</div>
<div class="nom_etud">{etud.html_link_fiche()}</div>
</div>
<div class="bull_photo"><a href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
etud.url_fiche()
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div>
</div>
<div class="warning">Impossible de statuer sur cet étudiant:
il est démissionnaire ou défaillant (voir <a class="stdlink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
}">sa fiche</a>)
etud.url_fiche()}">sa fiche</a>)
</div>
</div>
{navigation_div}
@ -2449,22 +2449,38 @@ def formsemestre_validation_but(
warning += (
"""<div class="warning">L'étudiant n'est pas inscrit à un parcours.</div>"""
)
if formsemestre.date_fin - datetime.date.today() > datetime.timedelta(days=12):
# encore loin de la fin du semestre de départ de ce jury ?
warning += f"""<div class="warning">Le semestre S{formsemestre.semestre_id}
terminera le {formsemestre.date_fin.strftime("%d/%m/%Y")}&nbsp;:
êtes-vous certain de vouloir enregistrer une décision de jury&nbsp;?
</div>"""
if deca.formsemestre_impair:
inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id)
if (not inscription) or inscription.etat != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_impair.semestre_id}</div>"""
warning += f"""<div class="warning">{etat_ins}
en S{deca.formsemestre_impair.semestre_id}</div>"""
if deca.formsemestre_pair:
inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id)
if (not inscription) or inscription.etat != scu.INSCRIT:
etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?")
warning += f"""<div class="warning">{etat_ins} en S{deca.formsemestre_pair.semestre_id}</div>"""
warning += f"""<div class="warning">{etat_ins}
en S{deca.formsemestre_pair.semestre_id}</div>"""
if has_notes_en_attente:
warning += f"""<div class="warning-bloquant">{etud.nomprenom} a des notes en ATTente.
Vous devez régler cela avant de statuer en jury !</div>"""
warning += f"""<div class="warning-bloquant">{etud.html_link_fiche()
} a des notes en ATTente dans les modules suivants.
Vous devez régler cela avant de statuer en jury !
<ul class="modimpls_att">
"""
for modimpl in deca.get_modimpls_attente():
warning += f"""<li><a href="{
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}" class="stdlink">{modimpl.module.code} {modimpl.module.titre_str()}</a></li>"""
warning += "</ul></div>"
if evaluations_a_debloquer:
links_evals = [
f"""<a class="stdlink" href="{url_for(
@ -2477,6 +2493,8 @@ def formsemestre_validation_but(
voir {", ".join(links_evals)}
"""
if warning:
warning = f"""<div class="jury_but_warning jury_but_box">{warning}</div>"""
H.append(
f"""
<div>
@ -2485,16 +2503,13 @@ def formsemestre_validation_but(
<div class="titre_parcours">Jury BUT{deca.annee_but}
- Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"}
- {deca.annee_scolaire_str()}</div>
<div class="nom_etud">{etud.nomprenom}</div>
<div class="nom_etud">{etud.html_link_fiche()}</div>
</div>
<div class="bull_photo"><a href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
etud.url_fiche()}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
</div>
</div>
<div class="jury_but_warning jury_but_box">
{warning}
</div>
</div>
<form method="post" class="jury_but_box" id="jury_but">
@ -2614,13 +2629,17 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
formsemestre_id=formsemestre_id,
)
)
if not formsemestre.formation.is_apc():
raise ScoValueError(
"formsemestre_validation_auto_but est réservé aux formations APC"
)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
form = jury_but_forms.FormSemestreValidationAutoBUTForm()
if request.method == "POST":
if not form.cancel.data:
nb_etud_modif = jury_but_validation_auto.formsemestre_validation_auto_but(
formsemestre
nb_etud_modif, _ = (
jury_but_validation_auto.formsemestre_validation_auto_but(formsemestre)
)
flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
return redirect(
@ -2631,9 +2650,25 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
mode_jury=1,
)
)
# Avertissement si formsemestre impair
formsemestres_suspects = {}
if formsemestre.semestre_id % 2:
_, decas = jury_but_validation_auto.formsemestre_validation_auto_but(
formsemestre, dry_run=True
)
# regarde si il y a des semestres pairs postérieurs qui ne soient pas bloqués
formsemestres_suspects = {
deca.formsemestre_pair.id: deca.formsemestre_pair
for deca in decas
if deca.formsemestre_pair
and deca.formsemestre_pair.date_debut > formsemestre.date_debut
and not deca.formsemestre_pair.block_moyennes
}
return render_template(
"but/formsemestre_validation_auto_but.j2",
form=form,
formsemestres_suspects=formsemestres_suspects,
sco=ScoData(formsemestre=formsemestre),
title="Calcul automatique jury BUT",
)

View File

@ -1365,6 +1365,15 @@ def etudident_edit_form():
return _etudident_create_or_edit_form(edit=True)
def _validate_date_naissance(val: str, field) -> bool:
"vrai si date saisie valide"
try:
date_naissance = scu.convert_fr_date(val)
except ScoValueError:
return False
return date_naissance < datetime.datetime.now()
def _etudident_create_or_edit_form(edit):
"Le formulaire HTML"
H = [html_sco_header.sco_header()]
@ -1506,8 +1515,7 @@ def _etudident_create_or_edit_form(edit):
"title": "Date de naissance",
"input_type": "date",
"explanation": "j/m/a",
"validator": lambda val, _: DMY_REGEXP.match(val)
and (ndb.DateDMYtoISO(val) < datetime.date.today().isoformat()),
"validator": _validate_date_naissance,
},
),
("lieu_naissance", {"title": "Lieu de naissance", "size": 32}),
@ -1779,7 +1787,7 @@ def _etudident_create_or_edit_form(edit):
+ homonyms_html
+ F
)
tf[2]["date_naissance"] = scu.convert_fr_date(tf[2]["date_naissance"])
if not edit:
etud = sco_etud.create_etud(cnx, args=tf[2])
etudid = etud["etudid"]

View File

@ -1,7 +1,11 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.953"
<<<<<<< HEAD
SCOVERSION = "9.6.955"
=======
SCOVERSION = "9.6.954"
>>>>>>> b1055a4ebe841f17860d2556cc4b03aa12ec3ab1
SCONAME = "ScoDoc"

View File

@ -3,7 +3,6 @@
"etudid": 11,
"code_nip": "11",
"rang": "1",
"nom_short": "FLEURY Ma.",
"civilite_str": "Mme",
"nom_disp": "FLEURY",
"prenom": "MADELEINE",
@ -51,6 +50,7 @@
"moy_sae_15_3": "~",
"bac": "",
"specialite": "",
"sort_key": "fleury;madeleine",
"type_admission": "",
"classement": "",
"partitions": {
@ -61,7 +61,6 @@
"etudid": 8,
"code_nip": "NIP8",
"rang": "2",
"nom_short": "SAUNIER Ja.",
"civilite_str": "M.",
"nom_disp": "SAUNIER",
"prenom": "JACQUES",
@ -109,6 +108,7 @@
"moy_sae_15_3": "~",
"bac": "",
"specialite": "",
"sort_key": "saunier;jacques",
"type_admission": "",
"classement": "",
"partitions": {
@ -119,7 +119,6 @@
"etudid": 6,
"code_nip": "NIP6",
"rang": "3",
"nom_short": "LENFANT Ma.",
"civilite_str": "",
"nom_disp": "LENFANT",
"prenom": "MAXIME",
@ -166,6 +165,7 @@
"moy_sae_14_3": "05.70",
"moy_sae_15_3": "~",
"bac": "",
"sort_key": "lenfant;maxime",
"specialite": "",
"type_admission": "",
"classement": "",
@ -177,7 +177,6 @@
"etudid": 7,
"code_nip": "7",
"rang": "4",
"nom_short": "CUNY Ca.",
"civilite_str": "",
"nom_disp": "CUNY",
"prenom": "CAMILLE",
@ -225,6 +224,7 @@
"moy_sae_15_3": "~",
"bac": "",
"specialite": "",
"sort_key": "cuny;camille",
"type_admission": "",
"classement": "",
"partitions": {
@ -235,7 +235,6 @@
"etudid": 12,
"code_nip": "NIP12",
"rang": "5",
"nom_short": "MOUTON Cl.",
"civilite_str": "M.",
"nom_disp": "MOUTON",
"prenom": "CLAUDE",
@ -282,6 +281,7 @@
"moy_sae_14_3": "11.09",
"moy_sae_15_3": "~",
"bac": "",
"sort_key": "mouton;claude",
"specialite": "",
"type_admission": "",
"classement": "",
@ -293,7 +293,6 @@
"etudid": 3,
"code_nip": "3",
"rang": "6",
"nom_short": "R\u00c9GNIER Pa.",
"civilite_str": "M.",
"nom_disp": "R\u00c9GNIER",
"prenom": "PATRICK",
@ -341,6 +340,7 @@
"moy_sae_15_3": "~",
"bac": "",
"specialite": "",
"sort_key": "regnier;patrick",
"type_admission": "",
"classement": "",
"partitions": {
@ -351,7 +351,6 @@
"etudid": 13,
"code_nip": "13",
"rang": "7",
"nom_short": "ESTEVE Al.",
"civilite_str": "",
"nom_disp": "ESTEVE",
"prenom": "ALIX",
@ -398,6 +397,7 @@
"moy_sae_14_3": "05.17",
"moy_sae_15_3": "~",
"bac": "",
"sort_key": "esteve;alix",
"specialite": "",
"type_admission": "",
"classement": "",
@ -409,7 +409,6 @@
"etudid": 16,
"code_nip": "NIP16",
"rang": "8",
"nom_short": "GILLES Ma.",
"civilite_str": "",
"nom_disp": "GILLES",
"prenom": "MAXIME",
@ -456,6 +455,7 @@
"moy_sae_14_3": "03.32",
"moy_sae_15_3": "~",
"bac": "",
"sort_key": "gilles;maxime",
"specialite": "",
"type_admission": "",
"classement": "",
@ -467,7 +467,6 @@
"etudid": 2,
"code_nip": "NIP2",
"rang": "9",
"nom_short": "NAUDIN Si.",
"civilite_str": "Mme",
"nom_disp": "NAUDIN",
"prenom": "SIMONE",
@ -514,6 +513,7 @@
"moy_sae_14_3": "02.10",
"moy_sae_15_3": "~",
"bac": "",
"sort_key": "naudin;simone",
"specialite": "",
"type_admission": "",
"classement": "",
@ -525,7 +525,6 @@
"etudid": 1,
"code_nip": "1",
"rang": "10",
"nom_short": "COSTA Sa.",
"civilite_str": "",
"nom_disp": "COSTA",
"prenom": "SACHA",
@ -572,6 +571,7 @@
"moy_sae_14_3": "07.17",
"moy_sae_15_3": "~",
"bac": "",
"sort_key": "costa;sacha",
"specialite": "",
"type_admission": "",
"classement": "",
@ -583,7 +583,6 @@
"etudid": 4,
"code_nip": "NIP4",
"rang": "11 ex",
"nom_short": "GAUTIER G\u00e9.",
"civilite_str": "M.",
"nom_disp": "GAUTIER",
"prenom": "G\u00c9RARD",
@ -630,6 +629,7 @@
"moy_sae_14_3": "",
"moy_sae_15_3": "",
"bac": "",
"sort_key": "gautier;gerard",
"specialite": "",
"type_admission": "",
"classement": "",
@ -641,7 +641,6 @@
"etudid": 5,
"code_nip": "5",
"rang": "11 ex",
"nom_short": "VILLENEUVE Fr.",
"civilite_str": "Mme",
"nom_disp": "VILLENEUVE",
"prenom": "FRAN\u00c7OISE",
@ -688,6 +687,7 @@
"moy_sae_14_3": "",
"moy_sae_15_3": "",
"bac": "",
"sort_key": "villeneuve;francoise",
"specialite": "",
"type_admission": "",
"classement": "",
@ -699,7 +699,6 @@
"etudid": 9,
"code_nip": "9",
"rang": "11 ex",
"nom_short": "SCHMITT Em.",
"civilite_str": "M.",
"nom_disp": "SCHMITT",
"prenom": "EMMANUEL",
@ -746,6 +745,7 @@
"moy_sae_14_3": "",
"moy_sae_15_3": "",
"bac": "",
"sort_key": "schmitt;emmanuel",
"specialite": "",
"type_admission": "",
"classement": "",
@ -757,7 +757,6 @@
"etudid": 10,
"code_nip": "NIP10",
"rang": "11 ex",
"nom_short": "BOUTET Ma.",
"civilite_str": "Mme",
"nom_disp": "BOUTET",
"prenom": "MARGUERITE",
@ -804,6 +803,7 @@
"moy_sae_14_3": "",
"moy_sae_15_3": "",
"bac": "",
"sort_key": "boutet;marguerite",
"specialite": "",
"type_admission": "",
"classement": "",
@ -815,7 +815,6 @@
"etudid": 14,
"code_nip": "NIP14",
"rang": "11 ex",
"nom_short": "ROLLIN De.",
"civilite_str": "M.",
"nom_disp": "ROLLIN",
"prenom": "DERC'HEN",
@ -863,6 +862,7 @@
"moy_sae_15_3": "",
"bac": "",
"specialite": "",
"sort_key": "rollin;derchen",
"type_admission": "",
"classement": "",
"partitions": {
@ -873,7 +873,6 @@
"etudid": 15,
"code_nip": "15",
"rang": "11 ex",
"nom_short": "DIOT Ca.",
"civilite_str": "",
"nom_disp": "DIOT",
"prenom": "CAMILLE",
@ -920,6 +919,7 @@
"moy_sae_14_3": "",
"moy_sae_15_3": "",
"bac": "",
"sort_key": "diot;camille",
"specialite": "",
"type_admission": "",
"classement": "",