Compare commits

...

12 Commits

40 changed files with 957 additions and 534 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"
@ -542,8 +540,6 @@ class Identite(models.ScoDocModel):
def inscriptions(self) -> list["FormSemestreInscription"]:
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
from app.models.formsemestre import FormSemestre, FormSemestreInscription
return (
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
.filter(
@ -569,8 +565,6 @@ class Identite(models.ScoDocModel):
(il est rare qu'il y en ai plus d'une, mais c'est possible).
Triées par date de début de semestre décroissante (le plus récent en premier).
"""
from app.models.formsemestre import FormSemestre, FormSemestreInscription
return (
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
.filter(
@ -1099,6 +1093,5 @@ class EtudAnnotation(db.Model):
return e
from app.models.formsemestre import FormSemestre
from app.models.modules import Module
from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.moduleimpls import ModuleImpl, ModuleImplInscription

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

@ -223,9 +223,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 +275,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

@ -57,14 +57,12 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_assiduites
from app.scodoc import codes_cursus
from app.scodoc import sco_cache
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_cursus
from app.scodoc import sco_cursus_dut
from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict
from app.scodoc.sco_permissions import Permission

View File

@ -110,7 +110,7 @@ def formsemestre_recapcomplet(
force_publishing=force_publishing,
)
table_html, table, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
table_html, _, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
formsemestre,
filename=filename,
mode_jury=mode_jury,

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

@ -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

@ -8,21 +8,16 @@
"""
import collections
import time
import numpy as np
from flask import g, url_for
from app.but import cursus_but
from app.but import jury_but
from app.but.jury_but import (
DecisionsProposeesAnnee,
DecisionsProposeesRCUE,
DecisionsProposeesUE,
)
from app.but.jury_but import DecisionsProposeesRCUE
from app.comp.res_compat import NotesTableCompat
from app.models import ApcNiveau, UniteEns
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc.codes_cursus import (
BUT_BARRE_RCUE,
BUT_RCUE_SUFFISANT,
@ -112,9 +107,11 @@ class TableJury(TableRecap):
row.add_cell(
"autorisations_inscription",
"Passage",
", ".join("S" + str(i) for i in sorted(autorisations[etud.id]))
if etud.id in autorisations
else "",
(
", ".join("S" + str(i) for i in sorted(autorisations[etud.id]))
if etud.id in autorisations
else ""
),
group="jury_code_sem",
classes=["recorded_code"],
)
@ -136,6 +133,7 @@ class TableJury(TableRecap):
if not self.read_only else "voir"} décisions""",
group="col_jury_link",
classes=["fontred"] if a_saisir else [],
no_excel=True,
target=url_for(
"notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
@ -278,6 +276,7 @@ class RowJury(RowRecap):
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
self.table.fmt_note(val),
raw_content=val,
raw_title=f"{rcue.ue_1.acronyme}-{rcue.ue_2.acronyme}",
group="rcue",
classes=[note_class],
column_classes={"col_rcue"},
@ -293,6 +292,7 @@ class RowJury(RowRecap):
"empty_code" if not dec_rcue.code_valide else "",
],
column_classes={"col_rcue"},
raw_title=f"{rcue.ue_1.acronyme}-{rcue.ue_2.acronyme}",
)
# # --- Les ECTS validés

View File

@ -613,6 +613,7 @@ class RowRecap(tb.Row):
"etudid": etud.id,
"nomprenom": etud.nomprenom,
},
no_excel=True,
target=url_bulletin,
target_attrs={"class": "etudinfo", "id": str(etud.id)},
)
@ -623,7 +624,11 @@ class RowRecap(tb.Row):
_, nbabsjust, nbabs = self.table.res.formsemestre.get_abs_count(self.etud.id)
self.add_cell("nbabs", "Abs", f"{nbabs:1.0f}", "abs", raw_content=nbabs)
self.add_cell(
"nbabsjust", "Just.", f"{nbabsjust:1.0f}", "abs", raw_content=nbabsjust
"nbabsjust",
"Just.",
f"{nbabsjust:1.0f}",
"abs",
raw_content=nbabsjust,
)
def add_moyennes_cols(

View File

@ -260,12 +260,18 @@ class Table(Element):
self.titles.update(titles)
def add_title(
self, col_id, title: str = None, classes: list[str] = None
self,
col_id,
title: str = None,
classes: list[str] = None,
raw_title: str = None,
) -> tuple["Cell", "Cell"]:
"""Record this title,
and create cells for footer and header if they don't already exist.
If specified, raw_title will be used in excel exports.
"""
title = title or ""
if col_id not in self.titles:
self.titles[col_id] = title
if self.head_title_row:
@ -275,6 +281,7 @@ class Table(Element):
title,
classes=classes,
group=self.column_group.get(col_id),
raw_content=raw_title or title,
)
if self.foot_title_row:
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
@ -359,6 +366,7 @@ class Row(Element):
data: dict[str, str] = None,
elt: str = None,
raw_content=None,
raw_title: str | None = None,
target_attrs: dict = None,
target: str = None,
column_classes: set[str] = None,
@ -384,16 +392,22 @@ class Row(Element):
target_attrs=target_attrs,
)
return self.add_cell_instance(
col_id, cell, column_group=group, title=title, no_excel=no_excel
col_id,
cell,
column_group=group,
title=title,
raw_title=raw_title,
no_excel=no_excel,
)
def add_cell_instance(
self,
col_id: str,
cell: "Cell",
column_group: str = None,
title: str = None,
column_group: str | None = None,
title: str | None = None,
no_excel: bool = False,
raw_title: str | None = None,
) -> "Cell":
"""Add a cell to the row.
Si title est None, il doit avoir été ajouté avec table.add_title().
@ -410,7 +424,9 @@ class Row(Element):
self.table.column_group[col_id] = column_group
if title is not None:
self.table.add_title(col_id, title, classes=cell.classes)
self.table.add_title(
col_id, title, classes=cell.classes, raw_title=raw_title
)
return cell
@ -487,7 +503,7 @@ class Cell(Element):
self.attrs["scope"] = "row"
self.data = data.copy() if data else {}
self.raw_content = raw_content or content
self.raw_content = content if raw_content is None else raw_content
self.target = target
self.target_attrs = target_attrs or {}

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,57 @@ 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;
}
</style>
{# News #}
@ -62,7 +117,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

@ -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,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.953"
SCOVERSION = "9.6.954"
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": "",