Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into entreprises

This commit is contained in:
Emmanuel Viennet 2022-04-18 17:35:03 +02:00
commit 24b6d1f4cc
67 changed files with 2296 additions and 1753 deletions

View File

@ -228,7 +228,7 @@ class BonusSportAdditif(BonusSport):
axis=1, axis=1,
) )
# Seuil: bonus dans [min, max] (défaut [0,20]) # Seuil: bonus dans [min, max] (défaut [0,20])
bonus_max = self.bonus_max or 0.0 bonus_max = self.bonus_max or 20.0
np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr) np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr)
self.bonus_additif(bonus_moy_arr) self.bonus_additif(bonus_moy_arr)
@ -418,17 +418,46 @@ class BonusAmiens(BonusSportAdditif):
class BonusBethune(BonusSportMultiplicatif): class BonusBethune(BonusSportMultiplicatif):
"""Calcul bonus modules optionnels (sport), règle IUT de Béthune. """
Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre. <p>
Ce bonus est égal au nombre de points divisé par 200 et multiplié par la <b>Pour le BUT :</b>
moyenne générale du semestre de l'étudiant. La note de sport est sur 20, et on calcule une bonification (en %)
qui va s'appliquer à <b>la moyenne de chaque UE</b> du semestre en appliquant
la formule : bonification (en %) = max(note-10, 0)*(1/<b>500</b>).
</p><p>
<em>La bonification ne s'applique que si la note est supérieure à 10.</em>
</p><p>
(Une note de 10 donne donc 0% de bonif,
1 point au dessus de 10 augmente la moyenne des UE de 0.2%)
</p>
<p>
<b>Pour le DUT/LP :</b>
La note de sport est sur 20, et on calcule une bonification (en %)
qui va s'appliquer à <b>la moyenne générale</b> du semestre en appliquant
la formule : bonification (en %) = max(note-10, 0)*(1/<b>200</b>).
</p><p>
<em>La bonification ne s'applique que si la note est supérieure à 10.</em>
</p><p>
(Une note de 10 donne donc 0% de bonif,
1 point au dessus de 10 augmente la moyenne des UE de 0.5%)
</p>
""" """
name = "bonus_iutbethune" name = "bonus_iutbethune"
displayed_name = "IUT de Béthune" displayed_name = "IUT de Béthune"
seuil_moy_gen = 10.0 seuil_moy_gen = 10.0 # points comptés au dessus de 10.
amplitude = 0.005
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
if self.formsemestre.formation.is_apc():
self.amplitude = 0.002
else:
self.amplitude = 0.005
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
class BonusBezier(BonusSportAdditif): class BonusBezier(BonusSportAdditif):
@ -502,10 +531,11 @@ class BonusCachan1(BonusSportAdditif):
<ul> <ul>
<li> DUT/LP : la meilleure note d'option, si elle est supérieure à 10, <li> DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
bonifie les moyennes d'UE (<b>sauf l'UE41 dont le code est UE41_E</b>) à raison bonifie les moyennes d'UE (uniquement UE13_E pour le semestre 1, UE23_E
pour le semestre 2, UE33_E pour le semestre 3 et UE43_E pour le semestre
4) à raison
de <em>bonus = (option - 10)/10</em>. de <em>bonus = (option - 10)/10</em>.
</li> </li>
<li> BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie <li> BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
les moyennes d'UE à raison de <em>bonus = (option - 10) * 3%</em>.</li> les moyennes d'UE à raison de <em>bonus = (option - 10) * 3%</em>.</li>
</ul> </ul>
@ -516,6 +546,7 @@ class BonusCachan1(BonusSportAdditif):
seuil_moy_gen = 10.0 # tous les points sont comptés seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.03 proportion_point = 0.03
classic_use_bonus_ues = True classic_use_bonus_ues = True
ues_bonifiables_cachan = {"UE13_E", "UE23_E", "UE33_E", "UE43_E"}
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus, avec réglage différent suivant le type de formation""" """calcul du bonus, avec réglage différent suivant le type de formation"""
@ -540,7 +571,7 @@ class BonusCachan1(BonusSportAdditif):
dtype=float, dtype=float,
) )
else: # --- DUT else: # --- DUT
# pareil mais proportion différente et exclusion d'une UE # pareil mais proportion différente et application à certaines UEs
proportion_point = 0.1 proportion_point = 0.1
bonus_moy_arr = np.where( bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen, note_bonus_max > self.seuil_moy_gen,
@ -553,10 +584,10 @@ class BonusCachan1(BonusSportAdditif):
columns=ues_idx, columns=ues_idx,
dtype=float, dtype=float,
) )
# Pas de bonus sur la ou les ue de code "UE41_E" # Applique bonus seulement sur certaines UE de code connu:
ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"] for ue in ues:
for ue in ue_exclues: if ue.ue_code not in self.ues_bonifiables_cachan:
self.bonus_ues[ue.id] = 0.0 self.bonus_ues[ue.id] = 0.0 # annule
class BonusCalais(BonusSportAdditif): class BonusCalais(BonusSportAdditif):
@ -982,7 +1013,7 @@ class BonusTarbes(BonusSportAdditif):
""" """
name = "bonus_tarbes" name = "bonus_tarbes"
displayed_name = "IUT de Tazrbes" displayed_name = "IUT de Tarbes"
seuil_moy_gen = 10.0 seuil_moy_gen = 10.0
proportion_point = 1 / 30.0 proportion_point = 1 / 30.0
classic_use_bonus_ues = True classic_use_bonus_ues = True
@ -1041,6 +1072,29 @@ class BonusTours(BonusDirect):
) )
class BonusIUTvannes(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Vannes
<p><b>Ne concerne actuellement que les DUT et LP</b></p>
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'U.B.S. (sports, musique, deuxième langue, culture, etc) non
rattachés à une unité d'enseignement.
</p><p>
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés.
</p><p>
3% de ces points cumulés s'ajoutent à la moyenne générale du semestre
déjà obtenue par l'étudiant.
</p>
"""
name = "bonus_iutvannes"
displayed_name = "IUT de Vannes"
seuil_moy_gen = 10.0
proportion_point = 0.03 # 3%
classic_use_bonus_ues = False # seulement sur moy gen.
class BonusVilleAvray(BonusSport): class BonusVilleAvray(BonusSport):
"""Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray. """Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.

View File

@ -92,6 +92,8 @@ class ModuleImplResults:
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
ne donnent pas de coef vers cette UE. ne donnent pas de coef vers cette UE.
""" """
self.evals_etudids_sans_note = {}
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
self.load_notes() self.load_notes()
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index) self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module vient de la session2""" """1 bool par etud, indique si sa moyenne de module vient de la session2"""
@ -142,12 +144,13 @@ class ModuleImplResults:
# ou évaluation déclarée "à prise en compte immédiate" # ou évaluation déclarée "à prise en compte immédiate"
# Les évaluations de rattrapage et 2eme session sont toujours incomplètes # Les évaluations de rattrapage et 2eme session sont toujours incomplètes
# car on calcule leur moyenne à part. # car on calcule leur moyenne à part.
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and ( is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and (
evaluation.publish_incomplete evaluation.publish_incomplete or (not etudids_sans_note)
or (not (inscrits_module - set(eval_df.index)))
) )
self.evaluations_completes.append(is_complete) self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete self.evaluations_completes_dict[evaluation.id] = is_complete
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
# NULL en base => ABS (= -999) # NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
@ -193,7 +196,9 @@ class ModuleImplResults:
return eval_df return eval_df
def _etudids(self): def _etudids(self):
"""L'index du dataframe est la liste de tous les étudiants inscrits au semestre""" """L'index du dataframe est la liste de tous les étudiants inscrits au semestre
(incluant les DEM et DEF)
"""
return [ return [
inscr.etudid inscr.etudid
for inscr in ModuleImpl.query.get( for inscr in ModuleImpl.query.get(

View File

@ -100,8 +100,9 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
if (notes is None) or (len(notes) == 0): if (notes is None) or (len(notes) == 0):
return (pd.Series([], dtype=object), pd.Series([], dtype=int)) return (pd.Series([], dtype=object), pd.Series([], dtype=int))
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
rangs_str = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne rangs_str = pd.Series("", index=notes.index, dtype=str) # le rang est une chaîne
rangs_int = pd.Series(index=notes.index, dtype=int) # le rang numérique pour tris # le rang numérique pour tris:
rangs_int = pd.Series(0, index=notes.index, dtype=int)
N = len(notes) N = len(notes)
nb_ex = 0 # nb d'ex-aequo consécutifs en cours nb_ex = 0 # nb d'ex-aequo consécutifs en cours
notes_i = notes.iat notes_i = notes.iat
@ -128,4 +129,5 @@ def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
rangs_int[etudid] = i + 1 rangs_int[etudid] = i + 1
srang = "%d" % (i + 1) srang = "%d" % (i + 1)
rangs_str[etudid] = srang rangs_str[etudid] = srang
assert rangs_int.dtype == int
return rangs_str, rangs_int return rangs_str, rangs_int

View File

@ -14,18 +14,19 @@ import pandas as pd
from flask import g, url_for from flask import g, url_for
from app.auth.models import User
from app.comp.res_cache import ResultatsCache from app.comp.res_cache import ResultatsCache
from app.comp import res_sem from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, FormSemestreUECoef from app.models import FormSemestre, FormSemestreUECoef, formsemestre
from app.models import Identite from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM
from app.scodoc import sco_evaluation_db
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_users
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
# Il faut bien distinguer # Il faut bien distinguer
@ -387,7 +388,9 @@ class ResultatsSemestre(ResultatsCache):
# --- TABLEAU RECAP # --- TABLEAU RECAP
def get_table_recap(self, convert_values=False): def get_table_recap(
self, convert_values=False, include_evaluations=False, modejury=False
):
"""Result: tuple avec """Result: tuple avec
- rows: liste de dicts { column_id : value } - rows: liste de dicts { column_id : value }
- titles: { column_id : title } - titles: { column_id : title }
@ -413,7 +416,6 @@ class ResultatsSemestre(ResultatsCache):
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id> - les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
_<column_id>_order : clé de tri _<column_id>_order : clé de tri
""" """
if convert_values: if convert_values:
fmt_note = scu.fmt_note fmt_note = scu.fmt_note
else: else:
@ -429,6 +431,7 @@ class ResultatsSemestre(ResultatsCache):
titles = {} titles = {}
# les titres en footer: les mêmes, mais avec des bulles et liens: # les titres en footer: les mêmes, mais avec des bulles et liens:
titles_bot = {} titles_bot = {}
dict_nom_res = {} # cache uid : nomcomplet
def add_cell( def add_cell(
row: dict, row: dict,
@ -457,6 +460,11 @@ class ResultatsSemestre(ResultatsCache):
idx = 0 # index de la colonne idx = 0 # index de la colonne
etud = Identite.query.get(etudid) etud = Identite.query.get(etudid)
row = {"etudid": etudid} row = {"etudid": etudid}
# --- Codes (seront cachés, mais exportés en excel)
idx = add_cell(row, "etudid", "etudid", etudid, "codes", idx)
idx = add_cell(
row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
)
# --- Rang # --- Rang
idx = add_cell( idx = add_cell(
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
@ -532,22 +540,28 @@ class ResultatsSemestre(ResultatsCache):
titles_bot[ titles_bot[
f"_{col_id}_target_attrs" f"_{col_id}_target_attrs"
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
if modejury:
# pas d'autre colonnes de résultats
continue
# Bonus (sport) dans cette UE ? # Bonus (sport) dans cette UE ?
# Le bonus sport appliqué sur cette UE # Le bonus sport appliqué sur cette UE
if (self.bonus_ues is not None) and (ue.id in self.bonus_ues): if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
val = self.bonus_ues[ue.id][etud.id] or "" val = self.bonus_ues[ue.id][etud.id] or ""
val_fmt = fmt_note(val) val_fmt = val_fmt_html = fmt_note(val)
if val: if val:
val_fmt = f'<span class="green-arrow-up"></span><span class="sp2l">{val_fmt}</span>' val_fmt_html = f'<span class="green-arrow-up"></span><span class="sp2l">{val_fmt}</span>'
idx = add_cell( idx = add_cell(
row, row,
f"bonus_ue_{ue.id}", f"bonus_ue_{ue.id}",
f"Bonus {ue.acronyme}", f"Bonus {ue.acronyme}",
val_fmt, val_fmt_html,
"col_ue_bonus", "col_ue_bonus",
idx, idx,
) )
row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE # Les moyennes des modules (ou ressources et SAÉs) dans cette UE
idx_malus = idx # place pour colonne malus à gauche des modules
idx += 1
for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False): for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False):
if ue_status["is_capitalized"]: if ue_status["is_capitalized"]:
val = "-c-" val = "-c-"
@ -573,63 +587,87 @@ class ResultatsSemestre(ResultatsCache):
col_id = ( col_id = (
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
) )
val_fmt = fmt_note(val) val_fmt = val_fmt_html = fmt_note(val)
if modimpl.module.module_type == scu.ModuleType.MALUS: if modimpl.module.module_type == scu.ModuleType.MALUS:
val_fmt = ( val_fmt_html = (
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else "" (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
) )
idx = add_cell( idx = add_cell(
row, row,
col_id, col_id,
modimpl.module.code, modimpl.module.code,
val_fmt, val_fmt_html,
# class col_res mod_ue_123 # class col_res mod_ue_123
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}", f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
idx, idx,
) )
row[f"_{col_id}_xls"] = val_fmt
if modimpl.module.module_type == scu.ModuleType.MALUS:
titles[f"_{col_id}_col_order"] = idx_malus
titles_bot[f"_{col_id}_target"] = url_for( titles_bot[f"_{col_id}_target"] = url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id, moduleimpl_id=modimpl.id,
) )
nom_resp = dict_nom_res.get(modimpl.responsable_id)
if nom_resp is None:
user = User.query.get(modimpl.responsable_id)
nom_resp = user.get_nomcomplet() if user else ""
dict_nom_res[modimpl.responsable_id] = nom_resp
titles_bot[ titles_bot[
f"_{col_id}_target_attrs" f"_{col_id}_target_attrs"
] = f""" ] = f""" title="{modimpl.module.titre} ({nom_resp})" """
title="{modimpl.module.titre}
({sco_users.user_info(modimpl.responsable_id)['nomcomplet']})" """
modimpl_ids.add(modimpl.id) modimpl_ids.add(modimpl.id)
ue_valid_txt = f"{nb_ues_validables}/{len(ues_sans_bonus)}" ue_valid_txt = (
ue_valid_txt_html
) = f"{nb_ues_validables}/{len(ues_sans_bonus)}"
if nb_ues_warning: if nb_ues_warning:
ue_valid_txt += " " + scu.EMO_WARNING ue_valid_txt_html += " " + scu.EMO_WARNING
add_cell( add_cell(
row, row,
"ues_validables", "ues_validables",
"UEs", "UEs",
ue_valid_txt, ue_valid_txt_html,
"col_ue col_ues_validables", "col_ue col_ues_validables",
29, # juste avant moy. gen. 29, # juste avant moy. gen.
) )
row["_ues_validables_xls"] = ue_valid_txt
if nb_ues_warning: if nb_ues_warning:
row["_ues_validables_class"] += " moy_ue_warning" row["_ues_validables_class"] += " moy_ue_warning"
elif nb_ues_validables < len(ues_sans_bonus): elif nb_ues_validables < len(ues_sans_bonus):
row["_ues_validables_class"] += " moy_inf" row["_ues_validables_class"] += " moy_inf"
row["_ues_validables_order"] = nb_ues_validables # pour tri row["_ues_validables_order"] = nb_ues_validables # pour tri
if modejury:
idx = add_cell(
row,
"jury_link",
"",
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
)
}">saisir décision</a>""",
"col_jury_link",
1000,
)
rows.append(row) rows.append(row)
self._recap_add_partitions(rows, titles) self._recap_add_partitions(rows, titles)
self._recap_add_admissions(rows, titles) self._recap_add_admissions(rows, titles)
# tri par rang croissant # tri par rang croissant
rows.sort(key=lambda e: e["_rang_order"]) rows.sort(key=lambda e: e["_rang_order"])
# INFOS POUR FOOTER # INFOS POUR FOOTER
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note) bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
if include_evaluations:
self._recap_add_evaluations(rows, titles, bottom_infos)
# Ajoute style "col_empty" aux colonnes de modules vides # Ajoute style "col_empty" aux colonnes de modules vides
for col_id in titles: for col_id in titles:
c_class = f"_{col_id}_class" c_class = f"_{col_id}_class"
if "col_empty" in bottom_infos["moy"].get(c_class, ""): if "col_empty" in bottom_infos["moy"].get(c_class, ""):
for row in rows: for row in rows:
row[c_class] += " col_empty" row[c_class] = row.get(c_class, "") + " col_empty"
titles[c_class] += " col_empty" titles[c_class] += " col_empty"
for row in bottom_infos.values(): for row in bottom_infos.values():
row[c_class] = row.get(c_class, "") + " col_empty" row[c_class] = row.get(c_class, "") + " col_empty"
@ -641,7 +679,9 @@ class ResultatsSemestre(ResultatsCache):
row["moy_gen"] = row.get("moy_gen", "") row["moy_gen"] = row.get("moy_gen", "")
row["_moy_gen_class"] = "col_moy_gen" row["_moy_gen_class"] = "col_moy_gen"
# titre de la ligne: # titre de la ligne:
row["prenom"] = row["nom_short"] = bottom_line.capitalize() row["prenom"] = row["nom_short"] = (
row.get("_title", "") or bottom_line.capitalize()
)
row["_tr_class"] = bottom_line.lower() + ( row["_tr_class"] = bottom_line.lower() + (
(" " + row["_tr_class"]) if "_tr_class" in row else "" (" " + row["_tr_class"]) if "_tr_class" in row else ""
) )
@ -656,53 +696,58 @@ class ResultatsSemestre(ResultatsCache):
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict: def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
"""Les informations à mettre en bas de la table: min, max, moy, ECTS""" """Les informations à mettre en bas de la table: min, max, moy, ECTS"""
row_min, row_max, row_moy, row_coef, row_ects = ( row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
{"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info", "_title": "Min."},
{"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info", "_title": "Code Apogée"},
) )
# --- ECTS # --- ECTS
for ue in ues: for ue in ues:
row_ects[f"moy_ue_{ue.id}"] = ue.ects colid = f"moy_ue_{ue.id}"
row_ects[f"_moy_ue_{ue.id}_class"] = "col_ue" row_ects[colid] = ue.ects
row_ects[f"_{colid}_class"] = "col_ue"
# style cases vides pour borders verticales # style cases vides pour borders verticales
row_coef[f"moy_ue_{ue.id}"] = "" row_coef[colid] = ""
row_coef[f"_moy_ue_{ue.id}_class"] = "col_ue" row_coef[f"_{colid}_class"] = "col_ue"
# row_apo[colid] = ue.code_apogee or ""
row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]) row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
row_ects["_moy_gen_class"] = "col_moy_gen" row_ects["_moy_gen_class"] = "col_moy_gen"
# --- MIN, MAX, MOY # --- MIN, MAX, MOY, APO
row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min()) row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max()) row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean()) row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
for ue in ues: for ue in ues:
col_id = f"moy_ue_{ue.id}" colid = f"moy_ue_{ue.id}"
row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min()) row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min())
row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max()) row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max())
row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean()) row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean())
row_min[f"_{col_id}_class"] = "col_ue" row_min[f"_{colid}_class"] = "col_ue"
row_max[f"_{col_id}_class"] = "col_ue" row_max[f"_{colid}_class"] = "col_ue"
row_moy[f"_{col_id}_class"] = "col_ue" row_moy[f"_{colid}_class"] = "col_ue"
row_apo[colid] = ue.code_apogee or ""
for modimpl in self.formsemestre.modimpls_sorted: for modimpl in self.formsemestre.modimpls_sorted:
if modimpl.id in modimpl_ids: if modimpl.id in modimpl_ids:
col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" colid = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
if self.is_apc: if self.is_apc:
coef = self.modimpl_coefs_df[modimpl.id][ue.id] coef = self.modimpl_coefs_df[modimpl.id][ue.id]
else: else:
coef = modimpl.module.coefficient or 0 coef = modimpl.module.coefficient or 0
row_coef[col_id] = fmt_note(coef) row_coef[colid] = fmt_note(coef)
notes = self.modimpl_notes(modimpl.id, ue.id) notes = self.modimpl_notes(modimpl.id, ue.id)
row_min[col_id] = fmt_note(np.nanmin(notes)) row_min[colid] = fmt_note(np.nanmin(notes))
row_max[col_id] = fmt_note(np.nanmax(notes)) row_max[colid] = fmt_note(np.nanmax(notes))
moy = np.nanmean(notes) moy = np.nanmean(notes)
row_moy[col_id] = fmt_note(moy) row_moy[colid] = fmt_note(moy)
if np.isnan(moy): if np.isnan(moy):
# aucune note dans ce module # aucune note dans ce module
row_moy[f"_{col_id}_class"] = "col_empty" row_moy[f"_{colid}_class"] = "col_empty"
row_apo[colid] = modimpl.module.code_apogee or ""
return { # { key : row } avec key = min, max, moy, coef return { # { key : row } avec key = min, max, moy, coef
"min": row_min, "min": row_min,
@ -710,6 +755,7 @@ class ResultatsSemestre(ResultatsCache):
"moy": row_moy, "moy": row_moy,
"coef": row_coef, "coef": row_coef,
"ects": row_ects, "ects": row_ects,
"apo": row_apo,
} }
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict): def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict):
@ -803,3 +849,68 @@ class ResultatsSemestre(ResultatsCache):
row[f"{cid}"] = gr_name row[f"{cid}"] = gr_name
row[f"_{cid}_class"] = klass row[f"_{cid}_class"] = klass
first_partition = False first_partition = False
def _recap_add_evaluations(
self, rows: list[dict], titles: dict, bottom_infos: dict
):
"""Ajoute les colonnes avec les notes aux évaluations
rows est une liste de dict avec une clé "etudid"
Les colonnes ont la classe css "evaluation"
"""
# nouvelle ligne pour description évaluations:
bottom_infos["descr_evaluation"] = {
"_tr_class": "bottom_info",
"_title": "Description évaluation",
}
first_eval = True
index_col = 9000 # à droite
for modimpl in self.formsemestre.modimpls_sorted:
evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
eval_index = len(evals) - 1
inscrits = {i.etudid for i in modimpl.inscriptions}
first_eval_of_mod = True
for e in evals:
cid = f"eval_{e.id}"
titles[
cid
] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
klass = "evaluation"
if first_eval:
klass += " first"
elif first_eval_of_mod:
klass += " first_of_mod"
titles[f"_{cid}_class"] = klass
first_eval_of_mod = first_eval = False
titles[f"_{cid}_col_order"] = index_col
index_col += 1
eval_index -= 1
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
e.evaluation_id
)
for row in rows:
etudid = row["etudid"]
if etudid in inscrits:
if etudid in notes_db:
val = notes_db[etudid]["value"]
else:
# Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE
row[cid] = scu.fmt_note(val)
row[f"_{cid}_class"] = klass + {
"ABS": " abs",
"ATT": " att",
"EXC": " exc",
}.get(row[cid], "")
else:
row[cid] = "ni"
row[f"_{cid}_class"] = klass + " non_inscrit"
bottom_infos["coef"][cid] = e.coefficient
bottom_infos["min"][cid] = "0"
bottom_infos["max"][cid] = scu.fmt_note(e.note_max)
bottom_infos["descr_evaluation"][cid] = e.description or ""
bottom_infos["descr_evaluation"][f"_{cid}_target"] = url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=e.id,
)

View File

@ -70,5 +70,16 @@ class CodesDecisionsForm(FlaskForm):
DEM = _build_code_field("DEM") DEM = _build_code_field("DEM")
NAR = _build_code_field("NAR") NAR = _build_code_field("NAR")
RAT = _build_code_field("RAT") RAT = _build_code_field("RAT")
NOTES_FMT = StringField(
label="Format notes exportées",
description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
validators=[
validators.Length(
max=SHORT_STR_LEN,
message=f"Le format ne doit pas dépasser {SHORT_STR_LEN} caractères",
),
validators.DataRequired("format requis"),
],
)
submit = SubmitField("Valider") submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -151,7 +151,7 @@ class AddLogoForm(FlaskForm):
dept_id = dept_key_to_id(self.dept_key.data) dept_id = dept_key_to_id(self.dept_key.data)
if dept_id == GLOBAL: if dept_id == GLOBAL:
dept_id = None dept_id = None
if find_logo(logoname=name.data, dept_id=dept_id) is not None: if find_logo(logoname=name.data, dept_id=dept_id, strict=True) is not None:
raise validators.ValidationError("Un logo de même nom existe déjà") raise validators.ValidationError("Un logo de même nom existe déjà")
def select_action(self): def select_action(self):
@ -160,6 +160,14 @@ class AddLogoForm(FlaskForm):
return LogoInsert.build_action(self.data) return LogoInsert.build_action(self.data)
return None return None
def errors(self):
if self.do_insert.data:
if self.name.errors:
return True
if self.upload.errors:
return True
return False
class LogoForm(FlaskForm): class LogoForm(FlaskForm):
"""Embed both presentation of a logo (cf. template file configuration.html) """Embed both presentation of a logo (cf. template file configuration.html)
@ -211,6 +219,11 @@ class LogoForm(FlaskForm):
return LogoUpdate.build_action(self.data) return LogoUpdate.build_action(self.data)
return None return None
def errors(self):
if self.upload.data and self.upload.errors:
return True
return False
class DeptForm(FlaskForm): class DeptForm(FlaskForm):
dept_key = HiddenField() dept_key = HiddenField()
@ -244,6 +257,14 @@ class DeptForm(FlaskForm):
return self return self
return self.index.get(logoname, None) return self.index.get(logoname, None)
def errors(self):
if self.add_logo.errors():
return True
for logo_form in self.logos:
if logo_form.errors():
return True
return False
def _make_dept_id_name(): def _make_dept_id_name():
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ) """Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)

View File

@ -36,6 +36,7 @@ CODES_SCODOC_TO_APO = {
DEM: "NAR", DEM: "NAR",
NAR: "NAR", NAR: "NAR",
RAT: "ATT", RAT: "ATT",
"NOTES_FMT": "%3.2f",
} }
@ -157,32 +158,6 @@ class ScoDocSiteConfig(db.Model):
class_list.sort(key=lambda x: x[1].replace(" du ", " de ")) class_list.sort(key=lambda x: x[1].replace(" du ", " de "))
return [("", "")] + class_list return [("", "")] + class_list
@classmethod
def get_bonus_sport_func(cls):
"""Fonction bonus_sport ScoDoc 7 XXX
Transitoire pour les tests durant la transition #sco92
"""
"""returns bonus func with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
Raises ScoValueError if func_name not found in module bonus_sport.
"""
from app.scodoc import bonus_sport
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
func_name = c.value
if func_name == "": # pas de bonus défini
return None
try:
return getattr(bonus_sport, func_name)
except AttributeError:
raise ScoValueError(
f"""Fonction de calcul de l'UE bonus inexistante: "{func_name}".
(contacter votre administrateur local)."""
)
@classmethod @classmethod
def get_code_apo(cls, code: str) -> str: def get_code_apo(cls, code: str) -> str:
"""La représentation d'un code pour les exports Apogée. """La représentation d'un code pour les exports Apogée.

View File

@ -16,6 +16,7 @@ from app import models
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc.sco_bac import Baccalaureat from app.scodoc.sco_bac import Baccalaureat
from app.scodoc.sco_exceptions import ScoInvalidParamError
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -354,7 +355,10 @@ def make_etud_args(
""" """
args = None args = None
if etudid: if etudid:
args = {"etudid": etudid} try:
args = {"etudid": int(etudid)}
except ValueError as exc:
raise ScoInvalidParamError() from exc
elif code_nip: elif code_nip:
args = {"code_nip": code_nip} args = {"code_nip": code_nip}
elif use_request: # use form from current request (Flask global) elif use_request: # use form from current request (Flask global)

View File

@ -2,9 +2,21 @@
"""Evenements et logs divers """Evenements et logs divers
""" """
import datetime
import re
from flask import g, url_for
from flask_login import current_user
from app import db from app import db
from app import email
from app import log
from app.auth.models import User
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
class Scolog(db.Model): class Scolog(db.Model):
@ -24,13 +36,219 @@ class Scolog(db.Model):
class ScolarNews(db.Model): class ScolarNews(db.Model):
"""Nouvelles pour page d'accueil""" """Nouvelles pour page d'accueil"""
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_ABS = "ABS" # saisie absence
NEWS_MISC = "MISC" # unused
NEWS_MAP = {
NEWS_INSCR: "inscription d'étudiants",
NEWS_NOTE: "saisie note",
NEWS_FORM: "modification formation",
NEWS_SEM: "création semestre",
NEWS_MISC: "opération", # unused
}
NEWS_TYPES = list(NEWS_MAP.keys())
__tablename__ = "scolar_news" __tablename__ = "scolar_news"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) date = db.Column(
authenticated_user = db.Column(db.Text) # login, sans contrainte db.DateTime(timezone=True), server_default=db.func.now(), index=True
)
authenticated_user = db.Column(db.Text, index=True) # login, sans contrainte
# type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC' # type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
type = db.Column(db.String(SHORT_STR_LEN)) type = db.Column(db.String(SHORT_STR_LEN), index=True)
object = db.Column(db.Integer) # moduleimpl_id, formation_id, formsemestre_id object = db.Column(
db.Integer, index=True
) # moduleimpl_id, formation_id, formsemestre_id
text = db.Column(db.Text) text = db.Column(db.Text)
url = db.Column(db.Text) url = db.Column(db.Text)
def __repr__(self):
return (
f"<{self.__class__.__name__}(id={self.id}, date='{self.date.isoformat()}')>"
)
def __str__(self):
"'Chargement notes dans Stage (S3 FI) par Aurélie Dupont'"
formsemestre = self.get_news_formsemestre()
user = User.query.filter_by(user_name=self.authenticated_user).first()
sem_text = (
f"""(<a href="{url_for('notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">{formsemestre.sem_modalite()}</a>)"""
if formsemestre
else ""
)
author = f"par {user.get_nomcomplet()}" if user else ""
return f"{self.text} {sem_text} {author}"
def formatted_date(self) -> str:
"06 Avr 14h23"
mois = scu.MONTH_NAMES_ABBREV[self.date.month - 1]
return f"{self.date.day} {mois} {self.date.hour:02d}h{self.date.minute:02d}"
def to_dict(self):
return {
"date": {
"display": self.date.strftime("%d/%m/%Y %H:%M"),
"timestamp": self.date.timestamp(),
},
"type": self.NEWS_MAP.get(self.type, "?"),
"authenticated_user": self.authenticated_user,
"text": self.text,
}
@classmethod
def last_news(cls, n=1, dept_id=None, filter_dept=True) -> list:
"The most recent n news. Returns list of ScolarNews instances."
query = cls.query
if filter_dept:
if dept_id is None:
dept_id = g.scodoc_dept_id
query = query.filter_by(dept_id=dept_id)
return query.order_by(cls.date.desc()).limit(n).all()
@classmethod
def add(cls, typ, obj=None, text="", url=None, max_frequency=0):
"""Enregistre une nouvelle
Si max_frequency, ne génère pas 2 nouvelles "identiques"
à moins de max_frequency secondes d'intervalle.
Deux nouvelles sont considérées comme "identiques" si elles ont
même (obj, typ, user).
La nouvelle enregistrée est aussi envoyée par mail.
"""
if max_frequency:
last_news = (
cls.query.filter_by(
dept_id=g.scodoc_dept_id,
authenticated_user=current_user.user_name,
type=typ,
object=obj,
)
.order_by(cls.date.desc())
.limit(1)
.first()
)
if last_news:
now = datetime.datetime.now(tz=last_news.date.tzinfo)
if (now - last_news.date) < datetime.timedelta(seconds=max_frequency):
# on n'enregistre pas
return
news = ScolarNews(
dept_id=g.scodoc_dept_id,
authenticated_user=current_user.user_name,
type=typ,
object=obj,
text=text,
url=url,
)
db.session.add(news)
db.session.commit()
log(f"news: {news}")
news.notify_by_mail()
def get_news_formsemestre(self) -> FormSemestre:
"""formsemestre concerné par la nouvelle
None si inexistant
"""
formsemestre_id = None
if self.type == self.NEWS_INSCR:
formsemestre_id = self.object
elif self.type == self.NEWS_NOTE:
moduleimpl_id = self.object
if moduleimpl_id:
modimpl = ModuleImpl.query.get(moduleimpl_id)
if modimpl is None:
return None # module does not exists anymore
formsemestre_id = modimpl.formsemestre_id
if not formsemestre_id:
return None
formsemestre = FormSemestre.query.get(formsemestre_id)
return formsemestre
def notify_by_mail(self):
"""Notify by email"""
formsemestre = self.get_news_formsemestre()
prefs = sco_preferences.SemPreferences(
formsemestre_id=formsemestre.id if formsemestre else None
)
destinations = prefs["emails_notifications"] or ""
destinations = [x.strip() for x in destinations.split(",")]
destinations = [x for x in destinations if x]
if not destinations:
return
#
txt = self.text
if formsemestre:
txt += f"""\n\nSemestre {formsemestre.titre_mois()}\n\n"""
txt += f"""<a href="{url_for("notes.formsemestre_status", _external=True,
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">{formsemestre.sem_modalite()}</a>
"""
user = User.query.filter_by(user_name=self.authenticated_user).first()
if user:
txt += f"\n\nEffectué par: {user.get_nomcomplet()}\n"
txt = (
"\n"
+ txt
+ """\n
--- Ceci est un message de notification automatique issu de ScoDoc
--- vous recevez ce message car votre adresse est indiquée dans les paramètres de ScoDoc.
"""
)
# Transforme les URL en URL absolues
base = scu.ScoURL()
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
# (si on veut des messages non html)
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?")
sender = prefs["email_from_addr"]
email.send_email(subject, sender, destinations, txt)
@classmethod
def scolar_news_summary_html(cls, n=5) -> str:
"""News summary, formated in HTML"""
news_list = cls.last_news(n=n)
if not news_list:
return ""
H = [
f"""<div class="news"><span class="newstitle"><a href="{
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
}">Dernières opérations</a>
</span><ul class="newslist">"""
]
for news in news_list:
H.append(
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
class="newstext">{news}</span></li>"""
)
H.append("</ul>")
# Informations générales
H.append(
f"""<div>
Pour être informé des évolutions de ScoDoc,
vous pouvez vous
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">
abonner à la liste de diffusion</a>.
</div>
"""
)
H.append("</div>")
return "\n".join(H)

View File

@ -146,7 +146,8 @@ class Formation(db.Model):
db.session.add(ue) db.session.add(ue)
db.session.commit() db.session.commit()
app.clear_scodoc_cache() if change:
app.clear_scodoc_cache()
class Matiere(db.Model): class Matiere(db.Model):

View File

@ -286,7 +286,7 @@ class FormSemestre(db.Model):
""" """
if not self.etapes: if not self.etapes:
return "" return ""
return ", ".join([str(x.etape_apo) for x in self.etapes]) return ", ".join(sorted([str(x.etape_apo) for x in self.etapes]))
def responsables_str(self, abbrev_prenom=True) -> str: def responsables_str(self, abbrev_prenom=True) -> str:
"""chaîne "J. Dupond, X. Martin" """chaîne "J. Dupond, X. Martin"
@ -374,6 +374,16 @@ class FormSemestre(db.Model):
return self.titre return self.titre
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
def sem_modalite(self) -> str:
"""Le semestre et la modialité, ex "S2 FI" ou "S3 APP" """
if self.semestre_id > 0:
descr_sem = f"S{self.semestre_id}"
else:
descr_sem = ""
if self.modalite:
descr_sem += " " + self.modalite
return descr_sem
def get_abs_count(self, etudid): def get_abs_count(self, etudid):
"""Les comptes d'absences de cet étudiant dans ce semestre: """Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs, nb abs justifiées) tuple (nb abs, nb abs justifiées)
@ -423,7 +433,7 @@ notes_formsemestre_responsables = db.Table(
class FormSemestreEtape(db.Model): class FormSemestreEtape(db.Model):
"""Étape Apogée associées au semestre""" """Étape Apogée associée au semestre"""
__tablename__ = "notes_formsemestre_etapes" __tablename__ = "notes_formsemestre_etapes"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)

View File

@ -74,6 +74,10 @@ class GroupDescr(db.Model):
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">""" f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
) )
def get_nom_with_part(self) -> str:
"Nom avec partition: 'TD A'"
return f"{self.partition.partition_name or ''} {self.group_name or '-'}"
group_membership = db.Table( group_membership = db.Table(
"group_membership", "group_membership",

View File

@ -9,7 +9,6 @@ from app.comp import df_cache
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.modules import Module from app.models.modules import Module
import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu

View File

@ -121,6 +121,7 @@ class GenTable(object):
html_with_td_classes=False, # put class=column_id in each <td> html_with_td_classes=False, # put class=column_id in each <td>
html_before_table="", # html snippet to put before the <table> in the page html_before_table="", # html snippet to put before the <table> in the page
html_empty_element="", # replace table when empty html_empty_element="", # replace table when empty
html_table_attrs="", # for html
base_url=None, base_url=None,
origin=None, # string added to excel and xml versions origin=None, # string added to excel and xml versions
filename="table", # filename, without extension filename="table", # filename, without extension
@ -146,6 +147,7 @@ class GenTable(object):
self.html_header = html_header self.html_header = html_header
self.html_before_table = html_before_table self.html_before_table = html_before_table
self.html_empty_element = html_empty_element self.html_empty_element = html_empty_element
self.html_table_attrs = html_table_attrs
self.page_title = page_title self.page_title = page_title
self.pdf_link = pdf_link self.pdf_link = pdf_link
self.xls_link = xls_link self.xls_link = xls_link
@ -209,7 +211,8 @@ class GenTable(object):
omit_hidden_lines=False, omit_hidden_lines=False,
pdf_mode=False, # apply special pdf reportlab processing pdf_mode=False, # apply special pdf reportlab processing
pdf_style_list=[], # modified: list of platypus table style commands pdf_style_list=[], # modified: list of platypus table style commands
): xls_mode=False, # get xls content if available
) -> list:
"table data as a list of lists (rows)" "table data as a list of lists (rows)"
T = [] T = []
line_num = 0 # line number in input data line_num = 0 # line number in input data
@ -237,9 +240,14 @@ class GenTable(object):
# if colspan_count > 0: # if colspan_count > 0:
# continue # skip cells after a span # continue # skip cells after a span
if pdf_mode: if pdf_mode:
content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or "" content = row.get(f"_{cid}_pdf", False) or row.get(cid, "")
elif xls_mode:
content = row.get(f"_{cid}_xls", False) or row.get(cid, "")
else: else:
content = row.get(cid, "") or "" # nota: None converted to '' content = row.get(cid, "")
# Convert None to empty string ""
content = "" if content is None else content
colspan = row.get("_%s_colspan" % cid, 0) colspan = row.get("_%s_colspan" % cid, 0)
if colspan > 1: if colspan > 1:
pdf_style_list.append( pdf_style_list.append(
@ -299,7 +307,7 @@ class GenTable(object):
return self.xml() return self.xml()
elif format == "json": elif format == "json":
return self.json() return self.json()
raise ValueError("GenTable: invalid format: %s" % format) raise ValueError(f"GenTable: invalid format: {format}")
def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""): def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""):
"row is a dict, returns a string <tr...>...</tr>" "row is a dict, returns a string <tr...>...</tr>"
@ -407,8 +415,7 @@ class GenTable(object):
cls = ' class="%s"' % " ".join(tablclasses) cls = ' class="%s"' % " ".join(tablclasses)
else: else:
cls = "" cls = ""
H = [self.html_before_table, f"<table{hid}{cls} {self.html_table_attrs}>"]
H = [self.html_before_table, "<table%s%s>" % (hid, cls)]
line_num = 0 line_num = 0
# thead # thead
@ -479,23 +486,23 @@ class GenTable(object):
def excel(self, wb=None): def excel(self, wb=None):
"""Simple Excel representation of the table""" """Simple Excel representation of the table"""
if wb is None: if wb is None:
ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
else: else:
ses = wb.create_sheet(sheet_name=self.xls_sheet_name) sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
ses.rows += self.xls_before_table sheet.rows += self.xls_before_table
style_bold = sco_excel.excel_make_style(bold=True) style_bold = sco_excel.excel_make_style(bold=True)
style_base = sco_excel.excel_make_style() style_base = sco_excel.excel_make_style()
ses.append_row(ses.make_row(self.get_titles_list(), style_bold)) sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold))
for line in self.get_data_list(): for line in self.get_data_list(xls_mode=True):
ses.append_row(ses.make_row(line, style_base)) sheet.append_row(sheet.make_row(line, style_base))
if self.caption: if self.caption:
ses.append_blank_row() # empty line sheet.append_blank_row() # empty line
ses.append_single_cell_row(self.caption, style_base) sheet.append_single_cell_row(self.caption, style_base)
if self.origin: if self.origin:
ses.append_blank_row() # empty line sheet.append_blank_row() # empty line
ses.append_single_cell_row(self.origin, style_base) sheet.append_single_cell_row(self.origin, style_base)
if wb is None: if wb is None:
return ses.generate() return sheet.generate()
def text(self): def text(self):
"raw text representation of the table" "raw text representation of the table"

View File

@ -57,7 +57,6 @@ def sidebar_common():
<a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br/> <a href="{scu.AbsencesURL()}" class="sidebar">Absences</a> <br/>
""" """
] ]
if current_user.has_permission( if current_user.has_permission(
Permission.ScoUsersAdmin Permission.ScoUsersAdmin
) or current_user.has_permission(Permission.ScoUsersView): ) or current_user.has_permission(Permission.ScoUsersView):
@ -86,9 +85,9 @@ def sidebar():
f"""<div class="sidebar"> f"""<div class="sidebar">
{ sidebar_common() } { sidebar_common() }
<div class="box-chercheetud">Chercher étudiant:<br/> <div class="box-chercheetud">Chercher étudiant:<br/>
<form method="get" id="form-chercheetud" <form method="get" id="form-chercheetud"
action="{ url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }"> action="{url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }">
<div><input type="text" size="12" id="in-expnom" name="expnom" spellcheck="false"></input></div> <div><input type="text" size="12" class="in-expnom" name="expnom" spellcheck="false"></input></div>
</form></div> </form></div>
<div class="etud-insidebar"> <div class="etud-insidebar">
""" """

View File

@ -31,9 +31,7 @@
import calendar import calendar
import datetime import datetime
import html import html
import string
import time import time
import types
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app import log from app import log
@ -42,9 +40,9 @@ from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError
from app.scodoc import sco_abs_notification from app.scodoc import sco_abs_notification
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
import app.scodoc.sco_utils as scu
# --- Misc tools.... ------------------ # --- Misc tools.... ------------------
@ -247,9 +245,9 @@ def day_names():
If work_saturday property is set, include saturday If work_saturday property is set, include saturday
""" """
if is_work_saturday(): if is_work_saturday():
return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"] return scu.DAY_NAMES[:-1]
else: else:
return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"] return scu.DAY_NAMES[:-2]
def next_iso_day(date): def next_iso_day(date):

View File

@ -83,6 +83,7 @@ XXX A vérifier:
import collections import collections
import datetime import datetime
from functools import reduce from functools import reduce
import functools
import io import io
import os import os
import pprint import pprint
@ -125,7 +126,7 @@ APO_SEP = "\t"
APO_NEWLINE = "\r\n" APO_NEWLINE = "\r\n"
def _apo_fmt_note(note): def _apo_fmt_note(note, fmt="%3.2f"):
"Formatte une note pour Apogée (séparateur décimal: ',')" "Formatte une note pour Apogée (séparateur décimal: ',')"
# if not note and isinstance(note, float): changé le 31/1/2022, étrange ? # if not note and isinstance(note, float): changé le 31/1/2022, étrange ?
# return "" # return ""
@ -133,7 +134,7 @@ def _apo_fmt_note(note):
val = float(note) val = float(note)
except ValueError: except ValueError:
return "" return ""
return ("%3.2f" % val).replace(".", APO_DECIMAL_SEP) return (fmt % val).replace(".", APO_DECIMAL_SEP)
def guess_data_encoding(text, threshold=0.6): def guess_data_encoding(text, threshold=0.6):
@ -270,6 +271,9 @@ class ApoEtud(dict):
self.export_res_modules = export_res_modules self.export_res_modules = export_res_modules
self.export_res_sdj = export_res_sdj # export meme si pas de decision de jury self.export_res_sdj = export_res_sdj # export meme si pas de decision de jury
self.export_res_rat = export_res_rat self.export_res_rat = export_res_rat
self.fmt_note = functools.partial(
_apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
)
def __repr__(self): def __repr__(self):
return "ApoEtud( nom='%s', nip='%s' )" % (self["nom"], self["nip"]) return "ApoEtud( nom='%s', nip='%s' )" % (self["nom"], self["nip"])
@ -423,7 +427,7 @@ class ApoEtud(dict):
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
code_decision_ue = decisions_ue[ue["ue_id"]]["code"] code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
return dict( return dict(
N=_apo_fmt_note(ue_status["moy"] if ue_status else ""), N=self.fmt_note(ue_status["moy"] if ue_status else ""),
B=20, B=20,
J="", J="",
R=ScoDocSiteConfig.get_code_apo(code_decision_ue), R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
@ -443,7 +447,7 @@ class ApoEtud(dict):
].split(","): ].split(","):
n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid) n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
if n != "NI" and self.export_res_modules: if n != "NI" and self.export_res_modules:
return dict(N=_apo_fmt_note(n), B=20, J="", R="") return dict(N=self.fmt_note(n), B=20, J="", R="")
else: else:
module_code_found = True module_code_found = True
if module_code_found: if module_code_found:
@ -465,7 +469,7 @@ class ApoEtud(dict):
if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF: if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF:
note_str = "0,01" # note non nulle pour les démissionnaires note_str = "0,01" # note non nulle pour les démissionnaires
else: else:
note_str = _apo_fmt_note(note) note_str = self.fmt_note(note)
return dict(N=note_str, B=20, J="", R=decision_apo, M="") return dict(N=note_str, B=20, J="", R=decision_apo, M="")
def comp_elt_annuel(self, etudid, cur_sem, autre_sem): def comp_elt_annuel(self, etudid, cur_sem, autre_sem):
@ -531,7 +535,7 @@ class ApoEtud(dict):
moy_annuelle = (note + autre_note) / 2 moy_annuelle = (note + autre_note) / 2
except TypeError: except TypeError:
moy_annuelle = "" moy_annuelle = ""
note_str = _apo_fmt_note(moy_annuelle) note_str = self.fmt_note(moy_annuelle)
if code_semestre_validant(autre_decision["code"]): if code_semestre_validant(autre_decision["code"]):
decision_apo_annuelle = decision_apo decision_apo_annuelle = decision_apo

View File

@ -47,14 +47,15 @@
qui est une description (humaine, format libre) de l'archive. qui est une description (humaine, format libre) de l'archive.
""" """
import chardet
import datetime import datetime
import glob import glob
import json
import mimetypes import mimetypes
import os import os
import re import re
import shutil import shutil
import time import time
import chardet
import flask import flask
from flask import g, request from flask import g, request
@ -63,7 +64,9 @@ from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from config import Config from config import Config
from app import log from app import log
from app.models import Departement from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import Departement, FormSemestre
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
AccessDenied, AccessDenied,
@ -95,8 +98,8 @@ class BaseArchiver(object):
self.root = os.path.join(*dirs) self.root = os.path.join(*dirs)
log("initialized archiver, path=" + self.root) log("initialized archiver, path=" + self.root)
path = dirs[0] path = dirs[0]
for dir in dirs[1:]: for directory in dirs[1:]:
path = os.path.join(path, dir) path = os.path.join(path, directory)
try: try:
scu.GSL.acquire() scu.GSL.acquire()
if not os.path.isdir(path): if not os.path.isdir(path):
@ -117,11 +120,11 @@ class BaseArchiver(object):
try: try:
scu.GSL.acquire() scu.GSL.acquire()
if not os.path.isdir(dept_dir): if not os.path.isdir(dept_dir):
log("creating directory %s" % dept_dir) log(f"creating directory {dept_dir}")
os.mkdir(dept_dir) os.mkdir(dept_dir)
obj_dir = os.path.join(dept_dir, str(oid)) obj_dir = os.path.join(dept_dir, str(oid))
if not os.path.isdir(obj_dir): if not os.path.isdir(obj_dir):
log("creating directory %s" % obj_dir) log(f"creating directory {obj_dir}")
os.mkdir(obj_dir) os.mkdir(obj_dir)
finally: finally:
scu.GSL.release() scu.GSL.release()
@ -163,8 +166,9 @@ class BaseArchiver(object):
def get_archive_date(self, archive_id): def get_archive_date(self, archive_id):
"""Returns date (as a DateTime object) of an archive""" """Returns date (as a DateTime object) of an archive"""
dt = [int(x) for x in os.path.split(archive_id)[1].split("-")] return datetime.datetime(
return datetime.datetime(*dt) *[int(x) for x in os.path.split(archive_id)[1].split("-")]
)
def list_archive(self, archive_id: str) -> str: def list_archive(self, archive_id: str) -> str:
"""Return list of filenames (without path) in archive""" """Return list of filenames (without path) in archive"""
@ -195,8 +199,7 @@ class BaseArchiver(object):
archive_id = os.path.join(self.get_obj_dir(oid), archive_name) archive_id = os.path.join(self.get_obj_dir(oid), archive_name)
if not os.path.isdir(archive_id): if not os.path.isdir(archive_id):
log( log(
"invalid archive name: %s, oid=%s, archive_id=%s" f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}"
% (archive_name, oid, archive_id)
) )
raise ValueError("invalid archive name") raise ValueError("invalid archive name")
return archive_id return archive_id
@ -223,7 +226,7 @@ class BaseArchiver(object):
+ os.path.sep + os.path.sep
+ "-".join(["%02d" % x for x in time.localtime()[:6]]) + "-".join(["%02d" % x for x in time.localtime()[:6]])
) )
log("creating archive: %s" % archive_id) log(f"creating archive: {archive_id}")
try: try:
scu.GSL.acquire() scu.GSL.acquire()
os.mkdir(archive_id) # if exists, raises an OSError os.mkdir(archive_id) # if exists, raises an OSError
@ -302,9 +305,14 @@ def do_formsemestre_archive(
Store: Store:
- tableau recap (xls), pv jury (xls et pdf), bulletins (xml et pdf), lettres individuelles (pdf) - tableau recap (xls), pv jury (xls et pdf), bulletins (xml et pdf), lettres individuelles (pdf)
""" """
from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet from app.scodoc.sco_recapcomplet import (
gen_formsemestre_recapcomplet_excel,
gen_formsemestre_recapcomplet_html,
gen_formsemestre_recapcomplet_json,
)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre_id
archive_id = PVArchive.create_obj_archive(sem_archive_id, description) archive_id = PVArchive.create_obj_archive(sem_archive_id, description)
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
@ -319,37 +327,38 @@ def do_formsemestre_archive(
etudids = [m["etudid"] for m in groups_infos.members] etudids = [m["etudid"] for m in groups_infos.members]
# Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes) # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes)
data, _, _ = make_formsemestre_recapcomplet(formsemestre_id, format="xls") data, _ = gen_formsemestre_recapcomplet_excel(
formsemestre, res, include_evaluations=True, format="xls"
)
if data: if data:
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data)
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
data, _, _ = make_formsemestre_recapcomplet( table_html = gen_formsemestre_recapcomplet_html(
formsemestre_id, format="html", disable_etudlink=True formsemestre, res, include_evaluations=True
) )
if data: if table_html:
data = "\n".join( data = "\n".join(
[ [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Moyennes archivées le %s" % date, page_title=f"Moyennes archivées le {date}",
head_message="Moyennes archivées le %s" % date, head_message=f"Moyennes archivées le {date}",
no_side_bar=True, no_side_bar=True,
), ),
'<h2 class="fontorange">Valeurs archivées le %s</h2>' % date, f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
'<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }</style>', '<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }</style>',
data, table_html,
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
) )
data = data.encode(scu.SCO_ENCODING) data = data.encode(scu.SCO_ENCODING)
PVArchive.store(archive_id, "Tableau_moyennes.html", data) PVArchive.store(archive_id, "Tableau_moyennes.html", data)
# Bulletins en XML (pour tous les etudiants, n'utilise pas les groupes) # Bulletins en JSON
data, _, _ = make_formsemestre_recapcomplet( data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
formsemestre_id, format="xml", xml_with_decisions=True data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
) data_js = data_js.encode(scu.SCO_ENCODING)
if data: if data:
data = data.encode(scu.SCO_ENCODING) PVArchive.store(archive_id, "Bulletins.json", data_js)
PVArchive.store(archive_id, "Bulletins.xml", data)
# Decisions de jury, en XLS # Decisions de jury, en XLS
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False) data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
if data: if data:

View File

@ -49,7 +49,6 @@
# sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id), # sco_cache.EvaluationCache.get(evaluation_id), set(evaluation_id, value), delete(evaluation_id),
# #
import time
import traceback import traceback
from flask import g from flask import g
@ -198,6 +197,26 @@ class SemInscriptionsCache(ScoDocCache):
duration = 12 * 60 * 60 # ttl 12h duration = 12 * 60 * 60 # ttl 12h
class TableRecapCache(ScoDocCache):
"""Cache table recap (pour get_table_recap)
Clé: formsemestre_id
Valeur: le html (<div class="table_recap">...</div>)
"""
prefix = "RECAP"
duration = 12 * 60 * 60 # ttl 12h
class TableRecapWithEvalsCache(ScoDocCache):
"""Cache table recap (pour get_table_recap)
Clé: formsemestre_id
Valeur: le html (<div class="table_recap">...</div>)
"""
prefix = "RECAPWITHEVALS"
duration = 12 * 60 * 60 # ttl 12h
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False) def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
formsemestre_id=None, pdfonly=False formsemestre_id=None, pdfonly=False
): ):
@ -209,7 +228,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if getattr(g, "defer_cache_invalidation", False): if getattr(g, "defer_cache_invalidation", False):
g.sem_to_invalidate.add(formsemestre_id) g.sem_to_invalidate.add(formsemestre_id)
return return
log("inval_cache, formsemestre_id=%s pdfonly=%s" % (formsemestre_id, pdfonly)) log("inval_cache, formsemestre_id={formsemestre_id} pdfonly={pdfonly}")
if formsemestre_id is None: if formsemestre_id is None:
# clear all caches # clear all caches
log("----- invalidate_formsemestre: clearing all caches -----") log("----- invalidate_formsemestre: clearing all caches -----")
@ -247,6 +266,9 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
SemInscriptionsCache.delete_many(formsemestre_ids) SemInscriptionsCache.delete_many(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids) ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids) ValidationsSemestreCache.delete_many(formsemestre_ids)
TableRecapCache.delete_many(formsemestre_ids)
TableRecapWithEvalsCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids) SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)

View File

@ -136,7 +136,7 @@ class LogoInsert(Action):
parameters["dept_id"] = None parameters["dept_id"] = None
if parameters["upload"] and parameters["name"]: if parameters["upload"] and parameters["name"]:
logo = find_logo( logo = find_logo(
logoname=parameters["name"], dept_id=parameters["dept_key"] logoname=parameters["name"], dept_id=parameters["dept_key"], strict=True
) )
if logo is None: if logo is None:
return LogoInsert(parameters) return LogoInsert(parameters)

View File

@ -29,9 +29,11 @@
""" """
from flask import g, request from flask import g, request
from flask import url_for
from flask_login import current_user from flask_login import current_user
import app import app
from app.models import ScolarNews
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -40,9 +42,7 @@ import app.scodoc.notesdb as ndb
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_modalites from app.scodoc import sco_modalites
from app.scodoc import sco_news
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_up_to_date
from app.scodoc import sco_users from app.scodoc import sco_users
@ -53,7 +53,7 @@ def index_html(showcodes=0, showsemtable=0):
H = [] H = []
# News: # News:
H.append(sco_news.scolar_news_summary_html()) H.append(ScolarNews.scolar_news_summary_html())
# Avertissement de mise à jour: # Avertissement de mise à jour:
H.append("""<div id="update_warning"></div>""") H.append("""<div id="update_warning"></div>""")
@ -80,7 +80,7 @@ def index_html(showcodes=0, showsemtable=0):
sco_formsemestre.sem_set_responsable_name(sem) sco_formsemestre.sem_set_responsable_name(sem)
if showcodes: if showcodes:
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"] sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
else: else:
sem["tmpcode"] = "" sem["tmpcode"] = ""
# Nombre d'inscrits: # Nombre d'inscrits:
@ -122,26 +122,27 @@ def index_html(showcodes=0, showsemtable=0):
if showsemtable: if showsemtable:
H.append( H.append(
"""<hr/> f"""<hr>
<h2>Semestres de %s</h2> <h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
""" """
% sco_preferences.get_preference("DeptName")
) )
H.append(_sem_table_gt(sems, showcodes=showcodes).html()) H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>") H.append("</table>")
if not showsemtable: if not showsemtable:
H.append( H.append(
'<hr/><p><a href="%s?showsemtable=1">Voir tous les semestres</a></p>' f"""<hr>
% request.base_url <p><a class="stdlink" href="{url_for('scolar.index_html', scodoc_dept=g.scodoc_dept, showsemtable=1)
}">Voir tous les semestres ({len(othersems)} verrouillés)</a>
</p>"""
) )
H.append( H.append(
"""<p><form action="%s/view_formsemestre_by_etape"> f"""<p>
Chercher étape courante: <input name="etape_apo" type="text" size="8" spellcheck="false"></input> <form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
</form Chercher étape courante:
</p> <input name="etape_apo" type="text" size="8" spellcheck="false"></input>
""" </form>
% scu.NotesURL() </p>"""
) )
# #
if current_user.has_permission(Permission.ScoEtudInscrit): if current_user.has_permission(Permission.ScoEtudInscrit):
@ -149,23 +150,26 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
"""<hr> """<hr>
<h3>Gestion des étudiants</h3> <h3>Gestion des étudiants</h3>
<ul> <ul>
<li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a></li> <li><a class="stdlink" href="etudident_create_form">créer <em>un</em> nouvel étudiant</a>
<li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a> (ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans </li>
le tableau de bord semestre si vous souhaitez inscrire les <li><a class="stdlink" href="form_students_import_excel">importer de nouveaux étudiants</a>
étudiants importés à un semestre)</li> (ne pas utiliser sauf cas particulier, utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)
</li>
</ul> </ul>
""" """
) )
# #
if current_user.has_permission(Permission.ScoEditApo): if current_user.has_permission(Permission.ScoEditApo):
H.append( H.append(
"""<hr> f"""<hr>
<h3>Exports Apogée</h3> <h3>Exports Apogée</h3>
<ul> <ul>
<li><a class="stdlink" href="%s/semset_page">Années scolaires / exports Apogée</a></li> <li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
}">Années scolaires / exports Apogée</a></li>
</ul> </ul>
""" """
% scu.NotesURL()
) )
# #
H.append( H.append(
@ -177,7 +181,13 @@ Chercher étape courante: <input name="etape_apo" type="text" size="8" spellchec
""" """
) )
# #
return html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() return (
html_sco_header.sco_header(
page_title=f"ScoDoc {g.scodoc_dept}", javascripts=["js/scolar_index.js"]
)
+ "\n".join(H)
+ html_sco_header.sco_footer()
)
def _sem_table(sems): def _sem_table(sems):
@ -214,7 +224,9 @@ def _sem_table(sems):
def _sem_table_gt(sems, showcodes=False): def _sem_table_gt(sems, showcodes=False):
"""Nouvelle version de la table des semestres""" """Nouvelle version de la table des semestres
Utilise une datatables.
"""
_style_sems(sems) _style_sems(sems)
columns_ids = ( columns_ids = (
"lockimg", "lockimg",
@ -229,6 +241,9 @@ def _sem_table_gt(sems, showcodes=False):
if showcodes: if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids columns_ids = ("formsemestre_id",) + columns_ids
html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
if current_user.has_permission(Permission.ScoEditApo):
html_class += " apo_editable"
tab = GenTable( tab = GenTable(
titles={ titles={
"formsemestre_id": "id", "formsemestre_id": "id",
@ -237,14 +252,16 @@ def _sem_table_gt(sems, showcodes=False):
"mois_debut": "Début", "mois_debut": "Début",
"dash_mois_fin": "Année", "dash_mois_fin": "Année",
"titre_resp": "Semestre", "titre_resp": "Semestre",
"nb_inscrits": "N", # groupicon, "nb_inscrits": "N",
}, },
columns_ids=columns_ids, columns_ids=columns_ids,
rows=sems, rows=sems,
html_class="table_leftalign semlist", table_id="semlist",
html_class_ignore_default=True,
html_class=html_class,
html_sortable=True, html_sortable=True,
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id), html_table_attrs=f"""data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}" """,
# caption='Maquettes enregistrées', html_with_td_classes=True,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
@ -277,6 +294,10 @@ def _style_sems(sems):
sem["semestre_id_n"] = "" sem["semestre_id_n"] = ""
else: else:
sem["semestre_id_n"] = sem["semestre_id"] sem["semestre_id_n"] = sem["semestre_id"]
# pour édition codes Apogée:
sem[
"_etapes_apo_str_td_attrs"
] = f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['etapes_apo_str']}" """
def delete_dept(dept_id: int): def delete_dept(dept_id: int):

View File

@ -37,6 +37,7 @@ from app.models import SHORT_STR_LEN
from app.models.formations import Formation from app.models.formations import Formation
from app.models.modules import Module from app.models.modules import Module
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.models import ScolarNews
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -44,13 +45,10 @@ from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_news
def formation_delete(formation_id=None, dialog_confirmed=False): def formation_delete(formation_id=None, dialog_confirmed=False):
@ -117,11 +115,10 @@ def do_formation_delete(oid):
sco_formations._formationEditor.delete(cnx, oid) sco_formations._formationEditor.delete(cnx, oid)
# news # news
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=oid, obj=oid,
text="Suppression de la formation %(acronyme)s" % F, text=f"Suppression de la formation {F['acronyme']}",
max_frequency=3,
) )
@ -281,10 +278,9 @@ def do_formation_create(args):
# #
r = sco_formations._formationEditor.create(cnx, args) r = sco_formations._formationEditor.create(cnx, args)
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
text="Création de la formation %(titre)s (%(acronyme)s)" % args, text="Création de la formation %(titre)s (%(acronyme)s)" % args,
max_frequency=3,
) )
return r return r

View File

@ -30,6 +30,7 @@
""" """
import flask import flask
from flask import g, url_for, request from flask import g, url_for, request
from app.models.events import ScolarNews
from app.models.formations import Matiere from app.models.formations import Matiere
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -78,8 +79,7 @@ def do_matiere_edit(*args, **kw):
def do_matiere_create(args): def do_matiere_create(args):
"create a matiere" "create a matiere"
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations from app.models import ScolarNews
from app.scodoc import sco_news
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # check
@ -89,11 +89,11 @@ def do_matiere_create(args):
# news # news
formation = Formation.query.get(ue["formation_id"]) formation = Formation.query.get(ue["formation_id"])
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=ue["formation_id"], obj=ue["formation_id"],
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=3, max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()
return r return r
@ -174,10 +174,8 @@ def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
def do_matiere_delete(oid): def do_matiere_delete(oid):
"delete matiere and attached modules" "delete matiere and attached modules"
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_news
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # check
@ -197,11 +195,11 @@ def do_matiere_delete(oid):
# news # news
formation = Formation.query.get(ue["formation_id"]) formation = Formation.query.get(ue["formation_id"])
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=ue["formation_id"], obj=ue["formation_id"],
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=3, max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()

View File

@ -38,6 +38,7 @@ from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import Formation, Matiere, Module, UniteEns from app.models import Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl from app.models import FormSemestre, ModuleImpl
from app.models import ScolarNews
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -53,7 +54,6 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
_moduleEditor = ndb.EditableTable( _moduleEditor = ndb.EditableTable(
"notes_modules", "notes_modules",
@ -98,18 +98,16 @@ def module_list(*args, **kw):
def do_module_create(args) -> int: def do_module_create(args) -> int:
"Create a module. Returns id of new object." "Create a module. Returns id of new object."
# create # create
from app.scodoc import sco_formations
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
r = _moduleEditor.create(cnx, args) r = _moduleEditor.create(cnx, args)
# news # news
formation = Formation.query.get(args["formation_id"]) formation = Formation.query.get(args["formation_id"])
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=formation.id, obj=formation.id,
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=3, max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()
return r return r
@ -396,11 +394,11 @@ def do_module_delete(oid):
# news # news
formation = module.formation formation = module.formation
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=mod["formation_id"], obj=mod["formation_id"],
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=3, max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()

View File

@ -37,6 +37,7 @@ from app import db
from app import log from app import log
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import Formation, UniteEns, ModuleImpl, Module from app.models import Formation, UniteEns, ModuleImpl, Module
from app.models import ScolarNews
from app.models.formations import Matiere from app.models.formations import Matiere
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -55,15 +56,11 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_apc from app.scodoc import sco_edit_apc
from app.scodoc import sco_edit_formation
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
@ -138,11 +135,11 @@ def do_ue_create(args):
ue = UniteEns.query.get(ue_id) ue = UniteEns.query.get(ue_id)
flash(f"UE créée (code {ue.ue_code})") flash(f"UE créée (code {ue.ue_code})")
formation = Formation.query.get(args["formation_id"]) formation = Formation.query.get(args["formation_id"])
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=args["formation_id"], obj=args["formation_id"],
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=3, max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()
return ue_id return ue_id
@ -222,11 +219,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
# news # news
F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0] F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0]
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=ue.formation_id, obj=ue.formation_id,
text="Modification de la formation %(acronyme)s" % F, text=f"Modification de la formation {F['acronyme']}",
max_frequency=3, max_frequency=10 * 60,
) )
# #
if not force: if not force:

View File

@ -34,8 +34,6 @@ from zipfile import ZipFile
import flask import flask
from flask import url_for, g, send_file, request from flask import url_for, g, send_file, request
# from werkzeug.utils import send_file
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc import html_sco_header from app.scodoc import html_sco_header

View File

@ -637,7 +637,7 @@ def create_etud(cnx, args={}):
Returns: Returns:
etud, l'étudiant créé. etud, l'étudiant créé.
""" """
from app.scodoc import sco_news from app.models import ScolarNews
# creation d'un etudiant # creation d'un etudiant
etudid = etudident_create(cnx, args) etudid = etudident_create(cnx, args)
@ -671,9 +671,8 @@ def create_etud(cnx, args={}):
etud = etudident_list(cnx, {"etudid": etudid})[0] etud = etudident_list(cnx, {"etudid": etudid})[0]
fill_etuds_info([etud]) fill_etuds_info([etud])
etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
object=None, # pas d'object pour ne montrer qu'un etudiant
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud, text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
url=etud["url"], url=etud["url"],
) )

View File

@ -25,7 +25,7 @@
# #
############################################################################## ##############################################################################
"""Vérification des abasneces à une évaluation """Vérification des absences à une évaluation
""" """
from flask import url_for, g from flask import url_for, g

View File

@ -28,7 +28,6 @@
"""Gestion evaluations (ScoDoc7, sans SQlAlchemy) """Gestion evaluations (ScoDoc7, sans SQlAlchemy)
""" """
import datetime
import pprint import pprint
import flask import flask
@ -37,6 +36,7 @@ from flask_login import current_user
from app import log from app import log
from app.models import ScolarNews
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -44,9 +44,7 @@ from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
@ -179,9 +177,9 @@ def do_evaluation_create(
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"] mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=moduleimpl_id, obj=moduleimpl_id,
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod, text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"], url=mod["url"],
) )
@ -240,9 +238,9 @@ def do_evaluation_delete(evaluation_id):
mod["url"] = ( mod["url"] = (
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
) )
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=moduleimpl_id, obj=moduleimpl_id,
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod, text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"], url=mod["url"],
) )

View File

@ -0,0 +1,148 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Tableau recap. de toutes les évaluations d'un semestre
avec leur état.
Sur une idée de Pascal Bouron, de Lyon.
"""
import time
from flask import g, url_for
from app.models import Evaluation, FormSemestre
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.comp.moy_mod import ModuleImplResults
from app.scodoc import html_sco_header
import app.scodoc.sco_utils as scu
def evaluations_recap(formsemestre_id: int) -> str:
"""Page récap. de toutes les évaluations d'un semestre"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
rows, titles = evaluations_recap_table(formsemestre)
column_ids = titles.keys()
filename = scu.sanitize_filename(
f"""evaluations-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
if not rows:
return '<div class="evaluations_recap"><div class="message">aucune évaluation</div></div>'
H = [
html_sco_header.sco_header(
page_title="Évaluations du semestre",
javascripts=["js/evaluations_recap.js"],
),
f"""<div class="evaluations_recap"><table class="evaluations_recap compact {
'apc' if formsemestre.formation.is_apc() else 'classic'
}"
data-filename="{filename}">""",
]
# header
H.append(
f"""
<thead>
{scu.gen_row(column_ids, titles, "th", with_col_classes=True)}
</thead>
"""
)
# body
H.append("<tbody>")
for row in rows:
H.append(f"{scu.gen_row(column_ids, row, with_col_classes=True)}\n")
H.append(
"""</tbody></table></div>
<div class="help">Les étudiants démissionnaires ou défaillants ne sont pas pris en compte dans cette table.</div>
"""
)
H.append(
html_sco_header.sco_footer(),
)
return "".join(H)
def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
"""Tableau recap. de toutes les évaluations d'un semestre
Colonnes:
- code (UE ou module),
- titre
- complete
- publiée
- inscrits (non dem. ni def.)
- nb notes manquantes
- nb ATT
- nb ABS
- nb EXC
"""
rows = []
titles = {
"type": "",
"code": "Code",
"titre": "",
"date": "Date",
"complete": "Comptée",
"inscrits": "Inscrits",
"manquantes": "Manquantes", # notes eval non entrées
"nb_abs": "ABS",
"nb_att": "ATT",
"nb_exc": "EXC",
}
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
line_idx = 0
for modimpl in nt.formsemestre.modimpls_sorted:
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
row = {
"type": modimpl.module.type_abbrv().upper(),
"_type_order": f"{line_idx:04d}",
"code": modimpl.module.code,
"_code_target": url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
),
"titre": modimpl.module.titre,
"_titre_class": "titre",
"inscrits": modimpl_results.nb_inscrits_module,
"date": "-",
"_date_order": "",
"_tr_class": f"module {modimpl.module.type_abbrv()}",
}
rows.append(row)
line_idx += 1
for evaluation_id in modimpl_results.evals_notes:
e = Evaluation.query.get(evaluation_id)
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
row = {
"type": "",
"_type_order": f"{line_idx:04d}",
"titre": e.description or "sans titre",
"_titre_target": url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=evaluation_id,
),
"_titre_target_attrs": 'class="discretelink"',
"date": e.jour.strftime("%d/%m/%Y") if e.jour else "",
"_date_order": e.jour.isoformat() if e.jour else "",
"complete": "oui" if eval_etat.is_complete else "non",
"_complete_target": "#",
"_complete_target_attrs": 'class="bull_link" title="prise en compte dans les moyennes"'
if eval_etat.is_complete
else 'class="bull_link incomplete" title="il manque des notes"',
"manquantes": len(modimpl_results.evals_etudids_sans_note[e.id]),
"inscrits": modimpl_results.nb_inscrits_module,
"nb_abs": sum(modimpl_results.evals_notes[e.id] == scu.NOTES_ABSENCE),
"nb_att": eval_etat.nb_attente,
"nb_exc": sum(
modimpl_results.evals_notes[e.id] == scu.NOTES_NEUTRALISE
),
"_tr_class": "evaluation"
+ (" incomplete" if not eval_etat.is_complete else ""),
}
rows.append(row)
line_idx += 1
return rows, titles

View File

@ -36,32 +36,27 @@ from flask import g
from flask_login import current_user from flask_login import current_user
from flask import request from flask import request
from app import log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.models import ScolarNews
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
import sco_version
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -633,13 +628,16 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>' '<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
% moduleimpl_id % moduleimpl_id
) )
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' % ( mod_descr = (
moduleimpl_id, '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s'
Mod["code"] or "", % (
Mod["titre"] or "?", moduleimpl_id,
nomcomplet, Mod["code"] or "",
resp, Mod["titre"] or "?",
link, nomcomplet,
resp,
link,
)
) )
etit = E["description"] or "" etit = E["description"] or ""

View File

@ -185,13 +185,13 @@ def excel_make_style(
class ScoExcelSheet: class ScoExcelSheet:
"""Représente une feuille qui peut être indépendante ou intégrée dans un SCoExcelBook. """Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook.
En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations
est imposé: est imposé:
* instructions globales (largeur/maquage des colonnes et ligne, ...) * instructions globales (largeur/maquage des colonnes et ligne, ...)
* construction et ajout des cellules et ligne selon le sens de lecture (occidental) * construction et ajout des cellules et ligne selon le sens de lecture (occidental)
ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..) ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..)
* pour finit appel de la méthode de génération * pour finir appel de la méthode de génération
""" """
def __init__(self, sheet_name="feuille", default_style=None, wb=None): def __init__(self, sheet_name="feuille", default_style=None, wb=None):
@ -260,7 +260,7 @@ class ScoExcelSheet:
for i, val in enumerate(value): for i, val in enumerate(value):
self.ws.column_dimensions[self.i2col(i)].width = val self.ws.column_dimensions[self.i2col(i)].width = val
# No keys: value is a list of widths # No keys: value is a list of widths
elif type(cle) == str: # accepts set_column_with("D", ...) elif isinstance(cle, str): # accepts set_column_with("D", ...)
self.ws.column_dimensions[cle].width = value self.ws.column_dimensions[cle].width = value
else: else:
self.ws.column_dimensions[self.i2col(cle)].width = value self.ws.column_dimensions[self.i2col(cle)].width = value
@ -337,7 +337,8 @@ class ScoExcelSheet:
return cell return cell
def make_row(self, values: list, style=None, comments=None): def make_row(self, values: list, style=None, comments=None) -> list:
"build a row"
# TODO make possible differents styles in a row # TODO make possible differents styles in a row
if comments is None: if comments is None:
comments = [None] * len(values) comments = [None] * len(values)

View File

@ -63,6 +63,17 @@ class ScoFormatError(ScoValueError):
pass pass
class ScoInvalidParamError(ScoValueError):
"""Paramètres requete invalides.
A utilisée lorsqu'une route est appelée avec des paramètres invalides
(id strings, ...)
"""
def __init__(self, msg=None, dest_url=None):
msg = msg or "Adresse invalide. Vérifiez vos signets."
super().__init__(msg, dest_url=dest_url)
class ScoPDFFormatError(ScoValueError): class ScoPDFFormatError(ScoValueError):
"erreur génération PDF (templates platypus, ...)" "erreur génération PDF (templates platypus, ...)"

View File

@ -53,12 +53,10 @@ def form_search_etud(
): ):
"form recherche par nom" "form recherche par nom"
H = [] H = []
if title:
H.append("<h2>%s</h2>" % title)
H.append( H.append(
f"""<form action="{ url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept) }" method="POST"> f"""<form action="{ url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept) }" method="POST">
<b>{title}</b> <b>{title}</b>
<input type="text" name="expnom" width="12" spellcheck="false" value=""> <input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
<input type="submit" value="Chercher"> <input type="submit" value="Chercher">
<br/>(entrer une partie du nom) <br/>(entrer une partie du nom)
""" """

View File

@ -39,12 +39,12 @@ import app.scodoc.notesdb as ndb
from app import db from app import db
from app import log from app import log
from app.models import Formation, Module from app.models import Formation, Module
from app.models import ScolarNews
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_news
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
from app.scodoc import sco_xml from app.scodoc import sco_xml
@ -351,10 +351,13 @@ def formation_list_table(formation_id=None, args={}):
else: else:
but_locked = '<span class="but_placeholder"></span>' but_locked = '<span class="but_placeholder"></span>'
if editable and not locked: if editable and not locked:
but_suppr = '<a class="stdlink" href="formation_delete?formation_id=%s" id="delete-formation-%s">%s</a>' % ( but_suppr = (
f["formation_id"], '<a class="stdlink" href="formation_delete?formation_id=%s" id="delete-formation-%s">%s</a>'
f["acronyme"].lower().replace(" ", "-"), % (
suppricon, f["formation_id"],
f["acronyme"].lower().replace(" ", "-"),
suppricon,
)
) )
else: else:
but_suppr = '<span class="but_placeholder"></span>' but_suppr = '<span class="but_placeholder"></span>'
@ -422,9 +425,9 @@ def formation_create_new_version(formation_id, redirect=True):
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data) new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
# news # news
F = formation_list(args={"formation_id": new_id})[0] F = formation_list(args={"formation_id": new_id})[0]
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=new_id, obj=new_id,
text="Nouvelle version de la formation %(acronyme)s" % F, text="Nouvelle version de la formation %(acronyme)s" % F,
) )
if redirect: if redirect:

View File

@ -141,7 +141,6 @@ def _formsemestre_enrich(sem):
"""Ajoute champs souvent utiles: titre + annee et dateord (pour tris)""" """Ajoute champs souvent utiles: titre + annee et dateord (pour tris)"""
# imports ici pour eviter refs circulaires # imports ici pour eviter refs circulaires
from app.scodoc import sco_formsemestre_edit from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_etud
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
@ -229,7 +228,7 @@ def etapes_apo_str(etapes):
def do_formsemestre_create(args, silent=False): def do_formsemestre_create(args, silent=False):
"create a formsemestre" "create a formsemestre"
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_news from app.models import ScolarNews
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
formsemestre_id = _formsemestreEditor.create(cnx, args) formsemestre_id = _formsemestreEditor.create(cnx, args)
@ -254,8 +253,8 @@ def do_formsemestre_create(args, silent=False):
args["formsemestre_id"] = formsemestre_id args["formsemestre_id"] = formsemestre_id
args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args
if not silent: if not silent:
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_SEM, typ=ScolarNews.NEWS_SEM,
text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args, text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
url=args["url"], url=args["url"],
) )
@ -350,6 +349,7 @@ def read_formsemestre_etapes(formsemestre_id): # OBSOLETE
"""SELECT etape_apo """SELECT etape_apo
FROM notes_formsemestre_etapes FROM notes_formsemestre_etapes
WHERE formsemestre_id = %(formsemestre_id)s WHERE formsemestre_id = %(formsemestre_id)s
ORDER BY etape_apo
""", """,
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
) )

View File

@ -36,6 +36,7 @@ from app import db
from app.auth.models import User from app.auth.models import User
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteEns from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteEns
from app.models import ScolarNews
from app.models.formations import Formation from app.models.formations import Formation
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -191,10 +192,10 @@ def do_formsemestre_createwithmodules(edit=False):
modimpl.responsable_id, modimpl.responsable_id,
f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !", f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !",
) )
if sem["responsables"]:
initvalues["responsable_id"] = uid2display.get( initvalues["responsable_id"] = uid2display.get(
sem["responsables"][0], sem["responsables"][0] sem["responsables"][0], sem["responsables"][0]
) )
if len(sem["responsables"]) > 1: if len(sem["responsables"]) > 1:
initvalues["responsable_id2"] = uid2display.get( initvalues["responsable_id2"] = uid2display.get(
sem["responsables"][1], sem["responsables"][1] sem["responsables"][1], sem["responsables"][1]
@ -1314,7 +1315,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
def formsemestre_delete(formsemestre_id): def formsemestre_delete(formsemestre_id):
"""Delete a formsemestre (affiche avertissements)""" """Delete a formsemestre (affiche avertissements)"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
H = [ H = [
html_sco_header.html_sem_header("Suppression du semestre"), html_sco_header.html_sem_header("Suppression du semestre"),
@ -1493,11 +1494,9 @@ def do_formsemestre_delete(formsemestre_id):
sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id) sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
# news # news
from app.scodoc import sco_news ScolarNews.add(
typ=ScolarNews.NEWS_SEM,
sco_news.add( obj=formsemestre_id,
typ=sco_news.NEWS_SEM,
object=formsemestre_id,
text="Suppression du semestre %(titre)s" % sem, text="Suppression du semestre %(titre)s" % sem,
) )

View File

@ -357,6 +357,11 @@ def formsemestre_status_menubar(sem):
"endpoint": "notes.formsemestre_recapcomplet", "endpoint": "notes.formsemestre_recapcomplet",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
}, },
{
"title": "État des évaluations",
"endpoint": "notes.evaluations_recap",
"args": {"formsemestre_id": formsemestre_id},
},
{ {
"title": "Saisie des notes", "title": "Saisie des notes",
"endpoint": "notes.formsemestre_status", "endpoint": "notes.formsemestre_status",
@ -404,9 +409,6 @@ def formsemestre_status_menubar(sem):
"args": { "args": {
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"modejury": 1, "modejury": 1,
"hidemodules": 1,
"hidebac": 1,
"pref_override": 0,
}, },
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id), "enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
}, },
@ -764,22 +766,29 @@ def _make_listes_sem(sem, with_absences=True):
if with_absences: if with_absences:
first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday()
form_abs_tmpl = f""" form_abs_tmpl = f"""
<td><form action="{url_for( <td>
<a href="%(url_etat)s">absences</a>
</td>
<td>
<form action="{url_for(
"absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept "absences.SignaleAbsenceGrSemestre", scodoc_dept=g.scodoc_dept
)}" method="get"> )}" method="get">
<input type="hidden" name="datefin" value="{sem['date_fin']}"/> <input type="hidden" name="datefin" value="{sem['date_fin']}"/>
<input type="hidden" name="group_ids" value="%(group_id)s"/> <input type="hidden" name="group_ids" value="%(group_id)s"/>
<input type="hidden" name="destination" value="{destination}"/> <input type="hidden" name="destination" value="{destination}"/>
<input type="submit" value="Saisir absences du" /> <input type="submit" value="Saisir abs des" />
<select name="datedebut" class="noprint"> <select name="datedebut" class="noprint">
""" """
date = first_monday date = first_monday
for jour in sco_abs.day_names(): for jour in sco_abs.day_names():
form_abs_tmpl += '<option value="%s">%s</option>' % (date, jour) form_abs_tmpl += f'<option value="{date}">{jour}s</option>'
date = date.next_day() date = date.next_day()
form_abs_tmpl += """ form_abs_tmpl += f"""
</select> </select>
<a href="%(url_etat)s">état</a>
<a href="{
url_for("absences.choix_semaine", scodoc_dept=g.scodoc_dept)
}?group_id=%(group_id)s">saisie par semaine</a>
</form></td> </form></td>
""" """
else: else:
@ -791,7 +800,7 @@ def _make_listes_sem(sem, with_absences=True):
# Genere liste pour chaque partition (categorie de groupes) # Genere liste pour chaque partition (categorie de groupes)
for partition in sco_groups.get_partitions_list(sem["formsemestre_id"]): for partition in sco_groups.get_partitions_list(sem["formsemestre_id"]):
if not partition["partition_name"]: if not partition["partition_name"]:
H.append("<h4>Tous les étudiants</h4>" % partition) H.append("<h4>Tous les étudiants</h4>")
else: else:
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition) H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
groups = sco_groups.get_partition_groups(partition) groups = sco_groups.get_partition_groups(partition)
@ -821,20 +830,6 @@ def _make_listes_sem(sem, with_absences=True):
) )
}">{group["label"]}</a> }">{group["label"]}</a>
</td><td> </td><td>
(<a href="{
url_for("scolar.groups_view",
group_ids=group["group_id"],
format="xls",
scodoc_dept=g.scodoc_dept,
)
}">tableur</a>)
<a href="{
url_for("scolar.groups_view",
curtab="tab-photos",
group_ids=group["group_id"],
scodoc_dept=g.scodoc_dept,
)
}">Photos</a>
</td> </td>
<td>({n_members} étudiants)</td> <td>({n_members} étudiants)</td>
""" """
@ -1191,6 +1186,7 @@ def formsemestre_tableau_modules(
H.append("<td>") H.append("<td>")
if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE): if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
coefs = mod.ue_coefs_list() coefs = mod.ue_coefs_list()
H.append(f'<a class="invisible_link" href="#" title="{mod_descr}">')
for coef in coefs: for coef in coefs:
if coef[1] > 0: if coef[1] > 0:
H.append( H.append(
@ -1202,6 +1198,7 @@ def formsemestre_tableau_modules(
) )
else: else:
H.append(f"""<span class="mod_coef_indicator_zero"></span>""") H.append(f"""<span class="mod_coef_indicator_zero"></span>""")
H.append("</a>")
H.append("</td>") H.append("</td>")
if mod.module_type in ( if mod.module_type in (
None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs

View File

@ -31,6 +31,7 @@ import time
import flask import flask
from flask import url_for, g, request from flask import url_for, g, request
from app.models.etudiants import Identite
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -107,29 +108,57 @@ def formsemestre_validation_etud_form(
if not Se.sem["etat"]: if not Se.sem["etat"]:
raise ScoValueError("validation: semestre verrouille") raise ScoValueError("validation: semestre verrouille")
url_tableau = url_for(
"notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept,
modejury=1,
formsemestre_id=formsemestre_id,
selected_etudid=etudid, # va a la bonne ligne
)
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Parcours %(nomprenom)s" % etud, page_title=f"Parcours {etud['nomprenom']}",
javascripts=["js/recap_parcours.js"], javascripts=["js/recap_parcours.js"],
) )
] ]
Footer = ["<p>"]
# Navigation suivant/precedent # Navigation suivant/precedent
if etud_index_prev != None: if etud_index_prev is not None:
etud_p = sco_etud.get_etud_info(etudid=T[etud_index_prev][-1], filled=True)[0] etud_prev = Identite.query.get(T[etud_index_prev][-1])
Footer.append( url_prev = url_for(
'<span><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etud_index=%s">Etud. précédent (%s)</a></span>' "notes.formsemestre_validation_etud_form",
% (formsemestre_id, etud_index_prev, etud_p["nomprenom"]) scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etud_index=etud_index_prev,
) )
if etud_index_next != None: else:
etud_n = sco_etud.get_etud_info(etudid=T[etud_index_next][-1], filled=True)[0] url_prev = None
Footer.append( if etud_index_next is not None:
'<span style="padding-left: 50px;"><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etud_index=%s">Etud. suivant (%s)</a></span>' etud_next = Identite.query.get(T[etud_index_next][-1])
% (formsemestre_id, etud_index_next, etud_n["nomprenom"]) url_next = url_for(
"notes.formsemestre_validation_etud_form",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
etud_index=etud_index_next,
) )
Footer.append("</p>") else:
Footer.append(html_sco_header.sco_footer()) url_next = None
footer = ["""<div class="jury_footer"><span>"""]
if url_prev:
footer.append(
f'< <a class="stdlink" href="{url_prev}">{etud_prev.nomprenom}</a>'
)
footer.append(
f"""</span><span><a class="stdlink" href="{url_tableau}">retour à la liste</a></span><span>"""
)
if url_next:
footer.append(
f'<a class="stdlink" href="{url_next}">{etud_next.nomprenom}</a> >'
)
footer.append("</span></div>")
footer.append(html_sco_header.sco_footer())
H.append('<table style="width: 100%"><tr><td>') H.append('<table style="width: 100%"><tr><td>')
if not check: if not check:
@ -171,7 +200,7 @@ def formsemestre_validation_etud_form(
""" """
) )
) )
return "\n".join(H + Footer) return "\n".join(H + footer)
H.append( H.append(
formsemestre_recap_parcours_table( formsemestre_recap_parcours_table(
@ -180,21 +209,10 @@ def formsemestre_validation_etud_form(
) )
if check: if check:
if not desturl: if not desturl:
desturl = url_for( desturl = url_tableau
"notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept,
modejury=1,
hidemodules=1,
hidebac=1,
pref_override=0,
formsemestre_id=formsemestre_id,
sortcol=sortcol
or None, # pour refaire tri sorttable du tableau de notes
_anchor="etudid%s" % etudid, # va a la bonne ligne
)
H.append(f'<ul><li><a href="{desturl}">Continuer</a></li></ul>') H.append(f'<ul><li><a href="{desturl}">Continuer</a></li></ul>')
return "\n".join(H + Footer) return "\n".join(H + footer)
decision_jury = Se.nt.get_etud_decision_sem(etudid) decision_jury = Se.nt.get_etud_decision_sem(etudid)
@ -210,7 +228,7 @@ def formsemestre_validation_etud_form(
""" """
) )
) )
return "\n".join(H + Footer) return "\n".join(H + footer)
# Infos si pas de semestre précédent # Infos si pas de semestre précédent
if not Se.prev: if not Se.prev:
@ -348,7 +366,7 @@ def formsemestre_validation_etud_form(
else: else:
H.append("sans semestres décalés</p>") H.append("sans semestres décalés</p>")
return "".join(H + Footer) return "".join(H + footer)
def formsemestre_validation_etud( def formsemestre_validation_etud(
@ -437,14 +455,6 @@ def _redirect_valid_choice(formsemestre_id, etudid, Se, choice, desturl, sortcol
# sinon renvoie au listing general, # sinon renvoie au listing general,
# if choice.new_code_prev:
# flask.redirect( 'formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1&desturl=%s' % (formsemestre_id, etudid, desturl) )
# else:
# if not desturl:
# desturl = 'formsemestre_recapcomplet?modejury=1&hidemodules=1&formsemestre_id=' + str(formsemestre_id)
# flask.redirect(desturl)
def _dispcode(c): def _dispcode(c):
if not c: if not c:
return "" return ""
@ -948,19 +958,23 @@ def do_formsemestre_validation_auto(formsemestre_id):
) )
if conflicts: if conflicts:
H.append( H.append(
"""<p><b>Attention:</b> %d étudiants non modifiés car décisions différentes f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
déja saisies :<ul>""" car décisions différentes déja saisies :
% len(conflicts) <ul>"""
) )
for etud in conflicts: for etud in conflicts:
H.append( H.append(
'<li><a href="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1">%s</li>' f"""<li><a href="{
% (formsemestre_id, etud["etudid"], etud["nomprenom"]) url_for('notes.formsemestre_validation_etud_form',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
etudid=etud["etudid"], check=1)
}">{etud["nomprenom"]}</li>"""
) )
H.append("</ul>") H.append("</ul>")
H.append( H.append(
'<a href="formsemestre_recapcomplet?formsemestre_id=%s&modejury=1&hidemodules=1&hidebac=1&pref_override=0">continuer</a>' f"""<a href="{url_for('notes.formsemestre_recapcomplet',
% formsemestre_id scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, modejury=1)
}">continuer</a>"""
) )
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)

View File

@ -40,6 +40,8 @@ from flask import g, url_for
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.models import ScolarNews
from app.scodoc.sco_excel import COLORS from app.scodoc.sco_excel import COLORS
from app.scodoc.sco_formsemestre_inscriptions import ( from app.scodoc.sco_formsemestre_inscriptions import (
do_formsemestre_inscription_with_modules, do_formsemestre_inscription_with_modules,
@ -54,14 +56,13 @@ from app.scodoc.sco_exceptions import (
ScoLockedFormError, ScoLockedFormError,
ScoGenError, ScoGenError,
) )
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_news
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
# format description (in tools/) # format description (in tools/)
@ -472,11 +473,11 @@ def scolars_import_excel_file(
diag.append("Import et inscription de %s étudiants" % len(created_etudids)) diag.append("Import et inscription de %s étudiants" % len(created_etudids))
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
% len(created_etudids), % len(created_etudids),
object=formsemestre_id, obj=formsemestre_id,
) )
log("scolars_import_excel_file: completing transaction") log("scolars_import_excel_file: completing transaction")

View File

@ -575,9 +575,7 @@ def _make_table_notes(
html_sortable=True, html_sortable=True,
base_url=base_url, base_url=base_url,
filename=filename, filename=filename,
origin="Généré par %s le " % sco_version.SCONAME origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
+ scu.timedate_human_repr()
+ "",
caption=caption, caption=caption,
html_next_section=html_next_section, html_next_section=html_next_section,
page_title="Notes de " + sem["titremois"], page_title="Notes de " + sem["titremois"],
@ -674,6 +672,21 @@ def _add_eval_columns(
e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture
inscrits = e_o.moduleimpl.formsemestre.etudids_actifs # set d'etudids inscrits = e_o.moduleimpl.formsemestre.etudids_actifs # set d'etudids
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
if len(e["jour"]) > 0:
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
else:
titles[evaluation_id] = "%(description)s " % e
if e["eval_state"]["evalcomplete"]:
klass = "eval_complete"
elif e["eval_state"]["evalattente"]:
klass = "eval_attente"
else:
klass = "eval_incomplete"
titles[evaluation_id] += " (non prise en compte)"
titles[f"_{evaluation_id}_td_attrs"] = f'class="{klass}"'
for row in rows: for row in rows:
etudid = row["etudid"] etudid = row["etudid"]
if etudid in notes_db: if etudid in notes_db:
@ -685,7 +698,7 @@ def _add_eval_columns(
# calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES # calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES
if ( if (
(etudid in inscrits) (etudid in inscrits)
and val != None and val is not None
and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_NEUTRALISE
and val != scu.NOTES_ATTENTE and val != scu.NOTES_ATTENTE
): ):
@ -708,14 +721,26 @@ def _add_eval_columns(
comment, comment,
) )
else: else:
explanation = "" if (etudid in inscrits) and e["publish_incomplete"]:
val_fmt = "" # Note manquante mais prise en compte immédiate: affiche ATT
val = None val = scu.NOTES_ATTENTE
val_fmt = "ATT"
explanation = "non saisie mais prise en compte immédiate"
else:
explanation = ""
val_fmt = ""
val = None
cell_class = klass + {"ATT": " att", "ABS": " abs", "EXC": " exc"}.get(
val_fmt, ""
)
if val is None: if val is None:
row["_" + str(evaluation_id) + "_td_attrs"] = 'class="etudabs" ' row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {cell_class}" '
if not row.get("_css_row_class", ""): if not row.get("_css_row_class", ""):
row["_css_row_class"] = "etudabs" row["_css_row_class"] = "etudabs"
else:
row[f"_{evaluation_id}_td_attrs"] = f'class="{cell_class}" '
# regroupe les commentaires # regroupe les commentaires
if explanation: if explanation:
if explanation in K: if explanation in K:
@ -768,18 +793,6 @@ def _add_eval_columns(
else: else:
row_moys[evaluation_id] = "" row_moys[evaluation_id] = ""
if len(e["jour"]) > 0:
titles[evaluation_id] = "%(description)s (%(jour)s)" % e
else:
titles[evaluation_id] = "%(description)s " % e
if e["eval_state"]["evalcomplete"]:
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_complete"'
elif e["eval_state"]["evalattente"]:
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_attente"'
else:
titles["_" + str(evaluation_id) + "_td_attrs"] = 'class="eval_incomplete"'
return notes, nb_abs, nb_att # pour histogramme return notes, nb_abs, nb_att # pour histogramme

View File

@ -1,276 +0,0 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Gestion des "nouvelles"
"""
import re
import time
from operator import itemgetter
from flask import g
from flask_login import current_user
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app import email
_scolar_news_editor = ndb.EditableTable(
"scolar_news",
"news_id",
("date", "authenticated_user", "type", "object", "text", "url"),
filter_dept=True,
sortkey="date desc",
output_formators={"date": ndb.DateISOtoDMY},
input_formators={"date": ndb.DateDMYtoISO},
html_quote=False, # no user supplied data, needed to store html links
)
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_MISC = "MISC" # unused
NEWS_MAP = {
NEWS_INSCR: "inscription d'étudiants",
NEWS_NOTE: "saisie note",
NEWS_FORM: "modification formation",
NEWS_SEM: "création semestre",
NEWS_MISC: "opération", # unused
}
NEWS_TYPES = list(NEWS_MAP.keys())
scolar_news_create = _scolar_news_editor.create
scolar_news_list = _scolar_news_editor.list
_LAST_NEWS = {} # { (authuser_name, type, object) : time }
def add(typ, object=None, text="", url=None, max_frequency=False):
"""Ajoute une nouvelle.
Si max_frequency, ne genere pas 2 nouvelles identiques à moins de max_frequency
secondes d'intervalle.
"""
from app.scodoc import sco_users
authuser_name = current_user.user_name
cnx = ndb.GetDBConnexion()
args = {
"authenticated_user": authuser_name,
"user_info": sco_users.user_info(authuser_name),
"type": typ,
"object": object,
"text": text,
"url": url,
}
t = time.time()
if max_frequency:
last_news_time = _LAST_NEWS.get((authuser_name, typ, object), False)
if last_news_time and (t - last_news_time < max_frequency):
# log("not recording")
return
log("news: %s" % args)
_LAST_NEWS[(authuser_name, typ, object)] = t
_send_news_by_mail(args)
return scolar_news_create(cnx, args)
def scolar_news_summary(n=5):
"""Return last n news.
News are "compressed", ie redondant events are joined.
"""
from app.scodoc import sco_etud
from app.scodoc import sco_users
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT id AS news_id, *
FROM scolar_news
WHERE dept_id=%(dept_id)s
ORDER BY date DESC LIMIT 100
""",
{"dept_id": g.scodoc_dept_id},
)
selected_news = {} # (type,object) : news dict
news = cursor.dictfetchall() # la plus récente d'abord
for r in reversed(news): # la plus ancienne d'abord
# si on a deja une news avec meme (type,object)
# et du meme jour, on la remplace
dmy = ndb.DateISOtoDMY(r["date"]) # round
key = (r["type"], r["object"], dmy)
selected_news[key] = r
news = list(selected_news.values())
# sort by date, descending
news.sort(key=itemgetter("date"), reverse=True)
news = news[:n]
# mimic EditableTable.list output formatting:
for n in news:
n["date822"] = n["date"].strftime("%a, %d %b %Y %H:%M:%S %z")
# heure
n["hm"] = n["date"].strftime("%Hh%M")
for k in n.keys():
if n[k] is None:
n[k] = ""
if k in _scolar_news_editor.output_formators:
n[k] = _scolar_news_editor.output_formators[k](n[k])
# date resumee
j, m = n["date"].split("/")[:2]
mois = scu.MONTH_NAMES_ABBREV[int(m) - 1]
n["formatted_date"] = "%s %s %s" % (j, mois, n["hm"])
# indication semestre si ajout notes:
infos = _get_formsemestre_infos_from_news(n)
if infos:
n["text"] += (
' (<a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>)'
% infos
)
n["text"] += (
" par " + sco_users.user_info(n["authenticated_user"])["nomcomplet"]
)
return news
def _get_formsemestre_infos_from_news(n):
"""Informations sur le semestre concerné par la nouvelle n
{} si inexistant
"""
formsemestre_id = None
if n["type"] == NEWS_INSCR:
formsemestre_id = n["object"]
elif n["type"] == NEWS_NOTE:
moduleimpl_id = n["object"]
if n["object"]:
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if not mods:
return {} # module does not exists anymore
return {} # pas d'indication du module
mod = mods[0]
formsemestre_id = mod["formsemestre_id"]
if not formsemestre_id:
return {}
try:
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
except:
# semestre n'existe plus
return {}
if sem["semestre_id"] > 0:
descr_sem = "S%d" % sem["semestre_id"]
else:
descr_sem = ""
if sem["modalite"]:
descr_sem += " " + sem["modalite"]
return {"formsemestre_id": formsemestre_id, "sem": sem, "descr_sem": descr_sem}
def scolar_news_summary_html(n=5):
"""News summary, formated in HTML"""
news = scolar_news_summary(n=n)
if not news:
return ""
H = ['<div class="news"><span class="newstitle">Dernières opérations']
H.append('</span><ul class="newslist">')
for n in news:
H.append(
'<li class="newslist"><span class="newsdate">%(formatted_date)s</span><span class="newstext">%(text)s</span></li>'
% n
)
H.append("</ul>")
# Informations générales
H.append(
"""<div>
Pour être informé des évolutions de ScoDoc,
vous pouvez vous
<a class="stdlink" href="%s">
abonner à la liste de diffusion</a>.
</div>
"""
% scu.SCO_ANNONCES_WEBSITE
)
H.append("</div>")
return "\n".join(H)
def _send_news_by_mail(n):
"""Notify by email"""
infos = _get_formsemestre_infos_from_news(n)
formsemestre_id = infos.get("formsemestre_id", None)
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
destinations = prefs["emails_notifications"] or ""
destinations = [x.strip() for x in destinations.split(",")]
destinations = [x for x in destinations if x]
if not destinations:
return
#
txt = n["text"]
if infos:
txt += "\n\nSemestre %(titremois)s\n\n" % infos["sem"]
txt += (
"""<a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>
"""
% infos
)
txt += "\n\nEffectué par: %(nomcomplet)s\n" % n["user_info"]
txt = (
"\n"
+ txt
+ """\n
--- Ceci est un message de notification automatique issu de ScoDoc
--- vous recevez ce message car votre adresse est indiquée dans les paramètres de ScoDoc.
"""
)
# Transforme les URL en URL absolue
base = scu.ScoURL()
txt = re.sub('href=.*?"', 'href="' + base + "/", txt)
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
# (si on veut des messages non html)
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
subject = "[ScoDoc] " + NEWS_MAP.get(n["type"], "?")
sender = prefs["email_from_addr"]
email.send_email(subject, sender, destinations, txt)

View File

@ -350,7 +350,7 @@ class BasePreferences(object):
"initvalue": "", "initvalue": "",
"title": "e-mails à qui notifier les opérations", "title": "e-mails à qui notifier les opérations",
"size": 70, "size": 70,
"explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc). (vous pouvez préférer utiliser le flux rss)", "explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc).",
"category": "general", "category": "general",
"only_global": False, # peut être spécifique à un semestre "only_global": False, # peut être spécifique à un semestre
}, },
@ -382,18 +382,6 @@ class BasePreferences(object):
"only_global": False, "only_global": False,
}, },
), ),
(
"recap_hidebac",
{
"initvalue": 0,
"title": "Cacher la colonne Bac",
"explanation": "sur la table récapitulative",
"input_type": "boolcheckbox",
"category": "misc",
"labels": ["non", "oui"],
"only_global": False,
},
),
# ------------------ Absences # ------------------ Absences
( (
"email_chefdpt", "email_chefdpt",

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,7 @@ from flask_login import current_user
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.models import ScolarNews
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -48,6 +49,7 @@ from app.scodoc.sco_exceptions import (
InvalidNoteValue, InvalidNoteValue,
NoteProcessError, NoteProcessError,
ScoGenError, ScoGenError,
ScoInvalidParamError,
ScoValueError, ScoValueError,
) )
from app.scodoc.TrivialFormulator import TrivialFormulator, TF from app.scodoc.TrivialFormulator import TrivialFormulator, TF
@ -64,7 +66,6 @@ from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes from app.scodoc import sco_undo_notes
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -274,11 +275,12 @@ def do_evaluation_upload_xls():
moduleimpl_id=mod["moduleimpl_id"], moduleimpl_id=mod["moduleimpl_id"],
_external=True, _external=True,
) )
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=M["moduleimpl_id"], obj=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod, text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"], url=mod["url"],
max_frequency=30 * 60, # 30 minutes
) )
msg = ( msg = (
@ -359,11 +361,12 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=mod["moduleimpl_id"], moduleimpl_id=mod["moduleimpl_id"],
) )
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=M["moduleimpl_id"], obj=M["moduleimpl_id"],
text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod, text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"], url=mod["url"],
max_frequency=30 * 60,
) )
return ( return (
html_sco_header.sco_header() html_sco_header.sco_header()
@ -451,9 +454,9 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"] mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=M["moduleimpl_id"], obj=M["moduleimpl_id"],
text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
% mod, % mod,
url=mod["url"], url=mod["url"],
@ -893,10 +896,12 @@ def has_existing_decision(M, E, etudid):
def saisie_notes(evaluation_id, group_ids=[]): def saisie_notes(evaluation_id, group_ids=[]):
"""Formulaire saisie notes d'une évaluation pour un groupe""" """Formulaire saisie notes d'une évaluation pour un groupe"""
if not isinstance(evaluation_id, int):
raise ScoInvalidParamError()
group_ids = [int(group_id) for group_id in group_ids] group_ids = [int(group_id) for group_id in group_ids]
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals: if not evals:
raise ScoValueError("invalid evaluation_id") raise ScoValueError("évaluation inexistante")
E = evals[0] E = evals[0]
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
@ -1283,9 +1288,9 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
nbchanged, _, existing_decisions = notes_add( nbchanged, _, existing_decisions = notes_add(
authuser, evaluation_id, L, comment=comment, do_it=True authuser, evaluation_id, L, comment=comment, do_it=True
) )
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=M["moduleimpl_id"], obj=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod, text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
url=Mod["url"], url=Mod["url"],
max_frequency=30 * 60, # 30 minutes max_frequency=30 * 60, # 30 minutes

View File

@ -29,12 +29,14 @@
""" """
import time import time
import pprint
from operator import itemgetter from operator import itemgetter
from flask import g, url_for from flask import g, url_for
from flask_login import current_user from flask_login import current_user
from app import log
from app.models import ScolarNews
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
@ -43,11 +45,8 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_inscr_passage from app.scodoc import sco_inscr_passage
from app.scodoc import sco_news
from app.scodoc import sco_excel
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app import log
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -701,10 +700,10 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
raise raise
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
text="Import Apogée de %d étudiants en " % len(created_etudids), text="Import Apogée de %d étudiants en " % len(created_etudids),
object=sem["formsemestre_id"], obj=sem["formsemestre_id"],
) )

View File

@ -55,19 +55,18 @@ from app.scodoc.sco_pdf import SU
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_import_etuds from app.scodoc import sco_import_etuds
from app.scodoc import sco_etud
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_photos from app.scodoc import sco_photos
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_trombino_doc
def trombino( def trombino(
group_ids=[], # liste des groupes à afficher group_ids=(), # liste des groupes à afficher
formsemestre_id=None, # utilisé si pas de groupes selectionné formsemestre_id=None, # utilisé si pas de groupes selectionné
etat=None, etat=None,
format="html", format="html",
@ -93,6 +92,8 @@ def trombino(
return _trombino_pdf(groups_infos) return _trombino_pdf(groups_infos)
elif format == "pdflist": elif format == "pdflist":
return _listeappel_photos_pdf(groups_infos) return _listeappel_photos_pdf(groups_infos)
elif format == "doc":
return sco_trombino_doc.trombino_doc(groups_infos)
else: else:
raise Exception("invalid format") raise Exception("invalid format")
# return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer() # return _trombino_html_header() + trombino_html( group, members) + html_sco_header.sco_footer()
@ -176,8 +177,13 @@ def trombino_html(groups_infos):
H.append("</div>") H.append("</div>")
H.append( H.append(
'<div style="margin-bottom:15px;"><a class="stdlink" href="trombino?format=pdf&%s">Version PDF</a></div>' f"""<div style="margin-bottom:15px;">
% groups_infos.groups_query_args <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
format='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a>
&nbsp;&nbsp;
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
format='doc', group_ids=groups_infos.group_ids)}">Version doc</a>
</div>"""
) )
return "\n".join(H) return "\n".join(H)
@ -234,7 +240,7 @@ def _trombino_zip(groups_infos):
Z.writestr(filename, img) Z.writestr(filename, img)
Z.close() Z.close()
size = data.tell() size = data.tell()
log("trombino_zip: %d bytes" % size) log(f"trombino_zip: {size} bytes")
data.seek(0) data.seek(0)
return send_file( return send_file(
data, data,
@ -470,7 +476,7 @@ def _listeappel_photos_pdf(groups_infos):
# --------------------- Upload des photos de tout un groupe # --------------------- Upload des photos de tout un groupe
def photos_generate_excel_sample(group_ids=[]): def photos_generate_excel_sample(group_ids=()):
"""Feuille excel pour import fichiers photos""" """Feuille excel pour import fichiers photos"""
fmt = sco_import_etuds.sco_import_format() fmt = sco_import_etuds.sco_import_format()
data = sco_import_etuds.sco_import_generate_excel_sample( data = sco_import_etuds.sco_import_generate_excel_sample(
@ -492,31 +498,33 @@ def photos_generate_excel_sample(group_ids=[]):
# return sco_excel.send_excel_file(data, "ImportPhotos" + scu.XLSX_SUFFIX) # return sco_excel.send_excel_file(data, "ImportPhotos" + scu.XLSX_SUFFIX)
def photos_import_files_form(group_ids=[]): def photos_import_files_form(group_ids=()):
"""Formulaire pour importation photos""" """Formulaire pour importation photos"""
if not group_ids: if not group_ids:
raise ScoValueError("paramètre manquant !") raise ScoValueError("paramètre manquant !")
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args back_url = f"groups_view?{groups_infos.groups_query_args}&curtab=tab-photos"
H = [ H = [
html_sco_header.sco_header(page_title="Import des photos des étudiants"), html_sco_header.sco_header(page_title="Import des photos des étudiants"),
"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2> f"""<h2 class="formsemestre">Téléchargement des photos des étudiants</h2>
<p><b>Vous pouvez aussi charger les photos individuellement via la fiche de chaque étudiant (menu "Etudiant" / "Changer la photo").</b></p> <p><b>Vous pouvez aussi charger les photos individuellement via la fiche
<p class="help">Cette page permet de charger en une seule fois les photos de plusieurs étudiants.<br/> de chaque étudiant (menu "Etudiant" / "Changer la photo").</b>
Il faut d'abord remplir une feuille excel donnant les noms </p>
<p class="help">Cette page permet de charger en une seule fois les photos
de plusieurs étudiants.<br/>
Il faut d'abord remplir une feuille excel donnant les noms
des fichiers images (une image par étudiant). des fichiers images (une image par étudiant).
</p> </p>
<p class="help">Ensuite, réunir vos images dans un fichier zip, puis télécharger <p class="help">Ensuite, réunir vos images dans un fichier zip, puis télécharger
simultanément le fichier excel et le fichier zip. simultanément le fichier excel et le fichier zip.
</p> </p>
<ol> <ol>
<li><a class="stdlink" href="photos_generate_excel_sample?%s"> <li><a class="stdlink" href="photos_generate_excel_sample?{groups_infos.groups_query_args}">
Obtenir la feuille excel à remplir</a> Obtenir la feuille excel à remplir</a>
</li> </li>
<li style="padding-top: 2em;"> <li style="padding-top: 2em;">
""" """,
% groups_infos.groups_query_args,
] ]
F = html_sco_header.sco_footer() F = html_sco_header.sco_footer()
vals = scu.get_request_args() vals = scu.get_request_args()

View File

@ -0,0 +1,76 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Génération d'un trombinoscope en doc
"""
import docx
from docx.shared import Mm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from app.scodoc import sco_etud
from app.scodoc import sco_photos
import app.scodoc.sco_utils as scu
import sco_version
def trombino_doc(groups_infos):
"Send photos as docx document"
filename = f"trombino_{groups_infos.groups_filename}.docx"
sem = groups_infos.formsemestre # suppose 1 seul semestre
PHOTO_WIDTH = Mm(25)
N_PER_ROW = 5 # XXX should be in ScoDoc preferences
document = docx.Document()
document.add_heading(
f"Trombinoscope {sem['titreannee']} {groups_infos.groups_titles}", 1
)
section = document.sections[0]
footer = section.footer
footer.paragraphs[
0
].text = f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}"
nb_images = len(groups_infos.members)
table = document.add_table(rows=2 * (nb_images // N_PER_ROW + 1), cols=N_PER_ROW)
table.allow_autofit = False
for i, t in enumerate(groups_infos.members):
li = i // N_PER_ROW
co = i % N_PER_ROW
img_path = (
sco_photos.photo_pathname(t["photo_filename"], size="small")
or sco_photos.UNKNOWN_IMAGE_PATH
)
cell = table.rows[2 * li].cells[co]
cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
cell_p, cell_f, cell_r = _paragraph_format_run(cell)
cell_r.add_picture(img_path, width=PHOTO_WIDTH)
# le nom de l'étudiant: cellules de lignes impaires
cell = table.rows[2 * li + 1].cells[co]
cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
cell_p, cell_f, cell_r = _paragraph_format_run(cell)
cell_r.add_text(sco_etud.format_nomprenom(t))
cell_f.space_after = Mm(8)
return scu.send_docx(document, filename)
def _paragraph_format_run(cell):
"parag. dans cellule tableau"
# inspired by https://stackoverflow.com/questions/64218305/problem-with-python-docx-putting-pictures-in-a-table
paragraph = cell.paragraphs[0]
fmt = paragraph.paragraph_format
run = paragraph.add_run()
fmt.space_before = Mm(0)
fmt.space_after = Mm(0)
fmt.line_spacing = 1.0
fmt.alignment = WD_ALIGN_PARAGRAPH.CENTER
return paragraph, fmt, run

View File

@ -227,7 +227,7 @@ def _user_list(user_name):
@cache.memoize(timeout=50) # seconds @cache.memoize(timeout=50) # seconds
def user_info(user_name_or_id=None, user=None): def user_info(user_name_or_id=None, user: User = None):
"""Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base). """Dict avec infos sur l'utilisateur (qui peut ne pas etre dans notre base).
Si user_name est specifie (string ou id), interroge la BD. Sinon, user doit etre une instance Si user_name est specifie (string ou id), interroge la BD. Sinon, user doit etre une instance
de User. de User.

View File

@ -33,6 +33,7 @@ import bisect
import copy import copy
import datetime import datetime
from enum import IntEnum from enum import IntEnum
import io
import json import json
from hashlib import md5 from hashlib import md5
import numbers import numbers
@ -49,6 +50,7 @@ from PIL import Image as PILImage
import pydot import pydot
import requests import requests
import flask
from flask import g, request from flask import g, request
from flask import flash, url_for, make_response, jsonify from flask import flash, url_for, make_response, jsonify
@ -176,6 +178,7 @@ MONTH_NAMES = (
"novembre", "novembre",
"décembre", "décembre",
) )
DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche")
def fmt_note(val, note_max=None, keep_numeric=False): def fmt_note(val, note_max=None, keep_numeric=False):
@ -191,7 +194,7 @@ def fmt_note(val, note_max=None, keep_numeric=False):
if isinstance(val, float) or isinstance(val, int): if isinstance(val, float) or isinstance(val, int):
if np.isnan(val): if np.isnan(val):
return "~" return "~"
if note_max != None and note_max > 0: if (note_max is not None) and note_max > 0:
val = val * 20.0 / note_max val = val * 20.0 / note_max
if keep_numeric: if keep_numeric:
return val return val
@ -379,6 +382,10 @@ CSV_FIELDSEP = ";"
CSV_LINESEP = "\n" CSV_LINESEP = "\n"
CSV_MIMETYPE = "text/comma-separated-values" CSV_MIMETYPE = "text/comma-separated-values"
CSV_SUFFIX = ".csv" CSV_SUFFIX = ".csv"
DOCX_MIMETYPE = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
DOCX_SUFFIX = ".docx"
JSON_MIMETYPE = "application/json" JSON_MIMETYPE = "application/json"
JSON_SUFFIX = ".json" JSON_SUFFIX = ".json"
PDF_MIMETYPE = "application/pdf" PDF_MIMETYPE = "application/pdf"
@ -398,6 +405,7 @@ def get_mime_suffix(format_code: str) -> tuple[str, str]:
""" """
d = { d = {
"csv": (CSV_MIMETYPE, CSV_SUFFIX), "csv": (CSV_MIMETYPE, CSV_SUFFIX),
"docx": (DOCX_MIMETYPE, DOCX_SUFFIX),
"xls": (XLSX_MIMETYPE, XLSX_SUFFIX), "xls": (XLSX_MIMETYPE, XLSX_SUFFIX),
"xlsx": (XLSX_MIMETYPE, XLSX_SUFFIX), "xlsx": (XLSX_MIMETYPE, XLSX_SUFFIX),
"pdf": (PDF_MIMETYPE, PDF_SUFFIX), "pdf": (PDF_MIMETYPE, PDF_SUFFIX),
@ -720,15 +728,13 @@ def sendResult(
def send_file(data, filename="", suffix="", mime=None, attached=None): def send_file(data, filename="", suffix="", mime=None, attached=None):
"""Build Flask Response for file download of given type """Build Flask Response for file download of given type
By default (attached is None), json and xml are inlined and otrher types are attached. By default (attached is None), json and xml are inlined and other types are attached.
""" """
if attached is None: if attached is None:
if mime == XML_MIMETYPE or mime == JSON_MIMETYPE: if mime == XML_MIMETYPE or mime == JSON_MIMETYPE:
attached = False attached = False
else: else:
attached = True attached = True
# if attached and not filename:
# raise ValueError("send_file: missing attachement filename")
if filename: if filename:
if suffix: if suffix:
filename += suffix filename += suffix
@ -740,6 +746,18 @@ def send_file(data, filename="", suffix="", mime=None, attached=None):
return response return response
def send_docx(document, filename):
"Send a python-docx document"
buffer = io.BytesIO() # in-memory document, no disk file
document.save(buffer)
buffer.seek(0)
return flask.send_file(
buffer,
download_name=sanitize_filename(filename),
mimetype=DOCX_MIMETYPE,
)
def get_request_args(): def get_request_args():
"""returns a dict with request (POST or GET) arguments """returns a dict with request (POST or GET) arguments
converted to suit legacy Zope style (scodoc7) functions. converted to suit legacy Zope style (scodoc7) functions.
@ -1057,6 +1075,36 @@ def objects_renumber(db, obj_list) -> None:
db.session.commit() db.session.commit()
def gen_cell(key: str, row: dict, elt="td", with_col_class=False):
"html table cell"
klass = row.get(f"_{key}_class", "")
if with_col_class:
klass = key + " " + klass
attrs = f'class="{klass}"' if klass else ""
order = row.get(f"_{key}_order")
if order:
attrs += f' data-order="{order}"'
content = row.get(key, "")
target = row.get(f"_{key}_target")
target_attrs = row.get(f"_{key}_target_attrs", "")
if target or target_attrs: # avec lien
href = f'href="{target}"' if target else ""
content = f"<a {href} {target_attrs}>{content}</a>"
return f"<{elt} {attrs}>{content}</{elt}>"
def gen_row(
keys: list[str], row, elt="td", selected_etudid=None, with_col_classes=False
):
"html table row"
klass = row.get("_tr_class")
tr_class = f'class="{klass}"' if klass else ""
tr_id = (
f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else ""
)
return f"""<tr {tr_id} {tr_class}>{"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) for key in keys if not key.startswith('_')])}</tr>"""
# Pour accès depuis les templates jinja # Pour accès depuis les templates jinja
def is_entreprises_enabled(): def is_entreprises_enabled():
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig

View File

@ -427,8 +427,8 @@ table.semlist tr td {
border: none; border: none;
} }
table.semlist tr a.stdlink, table.semlist tbody tr a.stdlink,
table.semlist tr a.stdlink:visited { table.semlist tbody tr a.stdlink:visited {
color: navy; color: navy;
text-decoration: none; text-decoration: none;
} }
@ -442,32 +442,86 @@ table.semlist tr td.semestre_id {
text-align: right; text-align: right;
} }
table.semlist tr td.modalite { table.semlist tbody tr td.modalite {
text-align: left; text-align: left;
padding-right: 1em; padding-right: 1em;
} }
div#gtrcontent table.semlist tr.css_S-1 { /***************************/
/* Statut des cellules */
/***************************/
.sco_selected {
outline: 1px solid #c09;
}
.sco_modifying {
outline: 2px dashed #c09;
background-color: white !important;
}
.sco_wait {
outline: 2px solid #c90;
}
.sco_good {
outline: 2px solid #9c0;
}
.sco_modified {
font-weight: bold;
color: indigo
}
/***************************/
/* Message */
/***************************/
.message {
position: fixed;
bottom: 100%;
left: 50%;
z-index: 10;
padding: 20px;
border-radius: 0 0 10px 10px;
background: #ec7068;
background: #90c;
color: #FFF;
font-size: 24px;
animation: message 3s;
transform: translate(-50%, 0);
}
@keyframes message {
20% {
transform: translate(-50%, 100%)
}
80% {
transform: translate(-50%, 100%)
}
}
div#gtrcontent table.semlist tbody tr.css_S-1 td {
background-color: rgb(251, 250, 216); background-color: rgb(251, 250, 216);
} }
div#gtrcontent table.semlist tr.css_S1 { div#gtrcontent table.semlist tbody tr.css_S1 td {
background-color: rgb(92%, 95%, 94%); background-color: rgb(92%, 95%, 94%);
} }
div#gtrcontent table.semlist tr.css_S2 { div#gtrcontent table.semlist tbody tr.css_S2 td {
background-color: rgb(214, 223, 236); background-color: rgb(214, 223, 236);
} }
div#gtrcontent table.semlist tr.css_S3 { div#gtrcontent table.semlist tbody tr.css_S3 td {
background-color: rgb(167, 216, 201); background-color: rgb(167, 216, 201);
} }
div#gtrcontent table.semlist tr.css_S4 { div#gtrcontent table.semlist tbody tr.css_S4 td {
background-color: rgb(131, 225, 140); background-color: rgb(131, 225, 140);
} }
div#gtrcontent table.semlist tr.css_MEXT { div#gtrcontent table.semlist tbody tr.css_MEXT td {
color: #0b6e08; color: #0b6e08;
} }
@ -487,6 +541,16 @@ div.news {
border-radius: 8px; border-radius: 8px;
} }
div.news a {
color: black;
text-decoration: none;
}
div.news a:hover {
color: rgb(153, 51, 51);
text-decoration: underline;
}
span.newstitle { span.newstitle {
font-weight: bold; font-weight: bold;
} }
@ -987,9 +1051,34 @@ span.wtf-field ul.errors li {
font-weight: bold; font-weight: bold;
} }
.configuration_logo div.img {} .configuration_logo summary {
display: list-item !important;
}
.configuration_logo div.img-container { .configuration_logo h1 {
display: inline-block;
}
.configuration_logo h2 {
display: inline-block;
}
.configuration_logo h3 {
display: inline-block;
}
.configuration_logo details>*:not(summary) {
margin-left: 32px;
}
.configuration_logo .content {
display: grid;
grid-template-columns: auto auto 1fr;
}
.configuration_logo .image_logo {
vertical-align: top;
grid-column: 1/2;
width: 256px; width: 256px;
} }
@ -997,8 +1086,27 @@ span.wtf-field ul.errors li {
max-width: 100%; max-width: 100%;
} }
.configuration_logo div.img-data { .configuration_logo .infos_logo {
vertical-align: top; grid-column: 2/3;
}
.configuration_logo .actions_logo {
grid-column: 3/5;
display: grid;
grid-template-columns: auto auto;
grid-column-gap: 10px;
align-self: start;
grid-row-gap: 10px;
}
.configuration_logo .actions_logo .action_label {
grid-column: 1/2;
grid-template-columns: auto auto;
}
.configuration_logo .actions_logo .action_button {
grid-column: 2/3;
align-self: start;
} }
.configuration_logo logo-edit titre { .configuration_logo logo-edit titre {
@ -1076,7 +1184,7 @@ tr.etuddem td {
td.etudabs, td.etudabs,
td.etudabs a.discretelink, td.etudabs a.discretelink,
tr.etudabs td.moyenne a.discretelink { tr.etudabs td.moyenne a.discretelink {
color: rgb(180, 0, 0); color: rgb(195, 0, 0);
} }
tr.moyenne td { tr.moyenne td {
@ -1089,13 +1197,31 @@ table.notes_evaluation th.eval_complete {
table.notes_evaluation th.eval_incomplete { table.notes_evaluation th.eval_incomplete {
color: red; color: red;
width: 80px;
font-size: 80%;
}
table.notes_evaluation td.eval_incomplete>a {
font-size: 80%;
color: rgb(166, 50, 159);
} }
table.notes_evaluation th.eval_attente { table.notes_evaluation th.eval_attente {
color: rgb(215, 90, 0); color: rgb(215, 90, 0);
; width: 80px;
} }
table.notes_evaluation td.att a {
color: rgb(255, 0, 217);
font-weight: bold;
}
table.notes_evaluation td.exc a {
font-style: italic;
color: rgb(0, 131, 0);
}
table.notes_evaluation tr td a.discretelink:hover { table.notes_evaluation tr td a.discretelink:hover {
text-decoration: none; text-decoration: none;
} }
@ -1139,6 +1265,18 @@ span.jurylink a {
text-decoration: underline; text-decoration: underline;
} }
div.jury_footer {
display: flex;
justify-content: space-evenly;
}
div.jury_footer>span {
border: 2px solid rgb(90, 90, 90);
border-radius: 4px;
padding: 4px;
background-color: rgb(230, 242, 230);
}
.eval_description p { .eval_description p {
margin-left: 15px; margin-left: 15px;
margin-bottom: 2px; margin-bottom: 2px;
@ -2402,6 +2540,12 @@ span.bul_minmax:before {
content: " "; content: " ";
} }
a.invisible_link,
a.invisible_link:hover {
text-decoration: none;
color: rgb(20, 30, 30);
}
a.bull_link { a.bull_link {
text-decoration: none; text-decoration: none;
color: rgb(20, 30, 30); color: rgb(20, 30, 30);
@ -3535,7 +3679,7 @@ table.dataTable td.group {
text-align: left; text-align: left;
} }
/* Nouveau tableau recap */ /* ------------- Nouveau tableau recap ------------ */
div.table_recap { div.table_recap {
margin-top: 6px; margin-top: 6px;
} }
@ -3703,4 +3847,120 @@ table.table_recap tr.dem td {
table.table_recap tr.def td { table.table_recap tr.def td {
color: rgb(121, 74, 74); color: rgb(121, 74, 74);
font-style: italic; font-style: italic;
}
table.table_recap td.evaluation,
table.table_recap tr.descr_evaluation {
font-size: 90%;
color: rgb(4, 16, 159);
}
table.table_recap tr.descr_evaluation a {
color: rgb(4, 16, 159);
text-decoration: none;
}
table.table_recap tr.descr_evaluation a:hover {
color: red;
}
table.table_recap tr.descr_evaluation {
vertical-align: top;
}
table.table_recap tr.apo {
font-size: 75%;
font-family: monospace;
}
table.table_recap tr.apo td {
border: 1px solid gray;
background-color: #d8f5fe;
}
table.table_recap td.evaluation.first,
table.table_recap th.evaluation.first {
border-left: 2px solid rgb(4, 16, 159);
}
table.table_recap td.evaluation.first_of_mod,
table.table_recap th.evaluation.first_of_mod {
border-left: 1px dashed rgb(4, 16, 159);
}
table.table_recap td.evaluation.att {
color: rgb(255, 0, 217);
font-weight: bold;
}
table.table_recap td.evaluation.abs {
color: rgb(231, 0, 0);
font-weight: bold;
}
table.table_recap td.evaluation.exc {
font-style: italic;
color: rgb(0, 131, 0);
}
table.table_recap td.evaluation.non_inscrit {
font-style: italic;
color: rgb(101, 101, 101);
}
/* ------------- Tableau etat evals ------------ */
div.evaluations_recap table.evaluations_recap {
width: auto !important;
border: 1px solid black;
}
table.evaluations_recap tr.odd td {
background-color: #fff4e4;
}
table.evaluations_recap tr.res td {
background-color: #f7d372;
}
table.evaluations_recap tr.sae td {
background-color: #d8fcc8;
}
table.evaluations_recap tr.module td {
font-weight: bold;
}
table.evaluations_recap tr.evaluation td.titre {
font-style: italic;
padding-left: 2em;
}
table.evaluations_recap td.titre,
table.evaluations_recap th.titre {
max-width: 350px;
}
table.evaluations_recap td.complete,
table.evaluations_recap th.complete {
text-align: center;
}
table.evaluations_recap tr.evaluation.incomplete td,
table.evaluations_recap tr.evaluation.incomplete td a {
color: red;
}
table.evaluations_recap tr.evaluation.incomplete td a.incomplete {
font-weight: bold;
}
table.evaluations_recap td.inscrits,
table.evaluations_recap td.manquantes,
table.evaluations_recap td.nb_abs,
table.evaluations_recap td.nb_att,
table.evaluations_recap td.nb_exc {
text-align: center;
} }

View File

@ -0,0 +1,38 @@
// Tableau recap evaluations du semestre
$(function () {
$('table.evaluations_recap').DataTable(
{
paging: false,
searching: true,
info: false,
autoWidth: false,
fixedHeader: {
header: true,
footer: false
},
orderCellsTop: true, // cellules ligne 1 pour tri
aaSorting: [], // Prevent initial sorting
colReorder: true,
"columnDefs": [
{
// colonne date, triable (XXX ne fonctionne pas)
targets: ["date"],
"type": "string",
},
],
dom: 'Bfrtip',
buttons: [
{
extend: 'copyHtml5',
text: 'Copier',
exportOptions: { orthogonal: 'export' }
},
{
extend: 'excelHtml5',
exportOptions: { orthogonal: 'export' },
title: document.querySelector('table.evaluations_recap').dataset.filename
},
],
})
});

View File

@ -3,14 +3,14 @@
$(function () { $(function () {
// Autocomplete recherche etudiants par nom // Autocomplete recherche etudiants par nom
$("#in-expnom").autocomplete( $(".in-expnom").autocomplete(
{ {
delay: 300, // wait 300ms before suggestions delay: 300, // wait 300ms before suggestions
minLength: 2, // min nb of chars before suggest minLength: 2, // min nb of chars before suggest
position: { collision: 'flip' }, // automatic menu position up/down position: { collision: 'flip' }, // automatic menu position up/down
source: "search_etud_by_name", source: SCO_URL + "/search_etud_by_name",
select: function (event, ui) { select: function (event, ui) {
$("#in-expnom").val(ui.item.value); $(".in-expnom").val(ui.item.value);
$("#form-chercheetud").submit(); $("#form-chercheetud").submit();
} }
}); });
@ -133,3 +133,134 @@ function readOnlyTags(nodes) {
node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>'); node.after('<span class="ro_tags"><span class="ro_tag">' + tags.join('</span><span class="ro_tag">') + '</span></span>');
} }
} }
/* Editeur pour champs
* Usage: créer un élément avec data-oid (object id)
* La méthode d'URL save sera appelée en POST avec deux arguments: oid et value,
* value contenant la valeur du champs.
* Inspiré par les codes et conseils de Seb. L.
*/
class ScoFieldEditor {
constructor(selector, save_url, read_only) {
this.save_url = save_url;
this.read_only = read_only;
this.selector = selector;
this.installListeners();
}
// Enregistre l'élément obj
save(obj) {
var value = obj.innerText.trim();
if (value.length == 0) {
value = "";
}
if (value == obj.dataset.value) {
return true; // Aucune modification, pas d'enregistrement mais on continue normalement
}
obj.classList.add("sco_wait");
// DEBUG
// console.log(`
// data : ${value},
// id: ${obj.dataset.oid}
// `);
$.post(this.save_url,
{
oid: obj.dataset.oid,
value: value,
},
function (result) {
obj.classList.remove("sco_wait");
obj.classList.add("sco_modified");
}
);
return true;
}
/*****************************/
/* Gestion des évènements */
/*****************************/
installListeners() {
if (this.read_only) {
return;
}
document.body.addEventListener("keydown", this.key);
let editor = this;
this.handleSelectCell = (event) => { editor.selectCell(event) };
this.handleModifCell = (event) => { editor.modifCell(event) };
this.handleBlur = (event) => { editor.blurCell(event) };
this.handleKeyCell = (event) => { editor.keyCell(event) };
document.querySelectorAll(this.selector).forEach(cellule => {
cellule.addEventListener("click", this.handleSelectCell);
cellule.addEventListener("dblclick", this.handleModifCell);
cellule.addEventListener("blur", this.handleBlur);
});
}
/*********************************/
/* Interaction avec les cellules */
/*********************************/
blurCell(event) {
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
}
selectCell(event) {
let obj = event.currentTarget;
if (obj) {
if (obj.classList.contains("sco_modifying")) {
return; // Cellule en cours de modification, ne pas sélectionner.
}
let currentModif = document.querySelector(".sco_modifying");
if (currentModif) {
if (!this.save(currentModif)) {
return;
}
}
this.unselectCell();
obj.classList.add("sco_selected");
}
}
unselectCell() {
document.querySelectorAll(".sco_selected, .sco_modifying").forEach(cellule => {
cellule.classList.remove("sco_selected", "sco_modifying");
cellule.removeAttribute("contentEditable");
cellule.removeEventListener("keydown", this.handleKeyCell);
});
}
modifCell(event) {
let obj = event.currentTarget;
if (obj) {
obj.classList.add("sco_modifying");
obj.contentEditable = true;
obj.addEventListener("keydown", this.handleKeyCell);
obj.focus();
}
}
key(event) {
switch (event.key) {
case "Enter":
this.modifCell(document.querySelector(".sco_selected"));
event.preventDefault();
break;
}
}
keyCell(event) {
let obj = event.currentTarget;
if (obj) {
if (event.key == "Enter") {
event.preventDefault();
event.stopPropagation();
if (!this.save(obj)) {
return
}
obj.classList.remove("sco_modifying");
// ArrowMove(0, 1);
// modifCell(document.querySelector(".sco_selected"));
this.unselectCell();
}
}
}
}

View File

@ -0,0 +1,22 @@
/* Page accueil département */
var apo_editor = null;
$(document).ready(function () {
var table_options = {
"paging": false,
"searching": false,
"info": false,
/* "autoWidth" : false, */
"fixedHeader": {
"header": true,
"footer": true
},
"orderCellsTop": true, // cellules ligne 1 pour tri
"aaSorting": [], // Prevent initial sorting
};
$('table.semlist').DataTable(table_options);
let apo_save_url = document.querySelector("table#semlist.apo_editable").dataset.apo_save_url;
apo_editor = new ScoFieldEditor(".etapes_apo_str", apo_save_url, false);
});

View File

@ -21,41 +21,54 @@ $(function () {
dt.columns(".partition_aux").visible(!visible); dt.columns(".partition_aux").visible(!visible);
dt.buttons('toggle_partitions:name').text(visible ? "Toutes les partitions" : "Cacher les partitions"); dt.buttons('toggle_partitions:name').text(visible ? "Toutes les partitions" : "Cacher les partitions");
} }
}, }];
$('table.table_recap').hasClass("apc") ? if (!$('table.table_recap').hasClass("jury")) {
{ buttons.push(
name: "toggle_res", $('table.table_recap').hasClass("apc") ?
text: "Cacher les ressources", {
action: function (e, dt, node, config) { name: "toggle_res",
let visible = dt.columns(".col_res").visible()[0]; text: "Cacher les ressources",
dt.columns(".col_res").visible(!visible); action: function (e, dt, node, config) {
dt.columns(".col_ue_bonus").visible(!visible); let visible = dt.columns(".col_res").visible()[0];
dt.buttons('toggle_res:name').text(visible ? "Montrer les ressources" : "Cacher les ressources"); dt.columns(".col_res").visible(!visible);
dt.columns(".col_ue_bonus").visible(!visible);
dt.columns(".col_malus").visible(!visible);
dt.buttons('toggle_res:name').text(visible ? "Montrer les ressources" : "Cacher les ressources");
}
} : {
name: "toggle_mod",
text: "Cacher les modules",
action: function (e, dt, node, config) {
let visible = dt.columns(".col_mod:not(.col_empty)").visible()[0];
dt.columns(".col_mod:not(.col_empty)").visible(!visible);
dt.columns(".col_ue_bonus").visible(!visible);
dt.columns(".col_malus").visible(!visible);
dt.buttons('toggle_mod:name').text(visible ? "Montrer les modules" : "Cacher les modules");
visible = dt.columns(".col_empty").visible()[0];
dt.buttons('toggle_col_empty:name').text(visible ? "Cacher mod. vides" : "Montrer mod. vides");
}
} }
} : { );
name: "toggle_mod", if ($('table.table_recap').hasClass("apc")) {
text: "Cacher les modules", buttons.push({
name: "toggle_sae",
text: "Cacher les SAÉs",
action: function (e, dt, node, config) { action: function (e, dt, node, config) {
let visible = dt.columns(".col_mod:not(.col_empty)").visible()[0]; let visible = dt.columns(".col_sae").visible()[0];
dt.columns(".col_mod:not(.col_empty)").visible(!visible); dt.columns(".col_sae").visible(!visible);
dt.columns(".col_ue_bonus").visible(!visible); dt.buttons('toggle_sae:name').text(visible ? "Montrer les SAÉs" : "Cacher les SAÉs");
dt.buttons('toggle_mod:name').text(visible ? "Montrer les modules" : "Cacher les modules");
visible = dt.columns(".col_empty").visible()[0];
dt.buttons('toggle_col_empty:name').text(visible ? "Cacher mod. vides" : "Montrer mod. vides");
} }
} })
]; }
if ($('table.table_recap').hasClass("apc")) {
buttons.push({ buttons.push({
name: "toggle_sae", name: "toggle_col_empty",
text: "Cacher les SAÉs", text: "Montrer mod. vides",
action: function (e, dt, node, config) { action: function (e, dt, node, config) {
let visible = dt.columns(".col_sae").visible()[0]; let visible = dt.columns(".col_empty").visible()[0];
dt.columns(".col_sae").visible(!visible); dt.columns(".col_empty").visible(!visible);
dt.buttons('toggle_sae:name').text(visible ? "Montrer les SAÉs" : "Cacher les SAÉs"); dt.buttons('toggle_col_empty:name').text(visible ? "Montrer mod. vides" : "Cacher mod. vides");
} }
}) })
} }
buttons.push({ buttons.push({
name: "toggle_admission", name: "toggle_admission",
@ -66,15 +79,6 @@ $(function () {
dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission"); dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission");
} }
}) })
buttons.push({
name: "toggle_col_empty",
text: "Montrer mod. vides",
action: function (e, dt, node, config) {
let visible = dt.columns(".col_empty").visible()[0];
dt.columns(".col_empty").visible(!visible);
dt.buttons('toggle_col_empty:name').text(visible ? "Montrer mod. vides" : "Cacher mod. vides");
}
})
$('table.table_recap').DataTable( $('table.table_recap').DataTable(
{ {
paging: false, paging: false,
@ -90,13 +94,37 @@ $(function () {
colReorder: true, colReorder: true,
"columnDefs": [ "columnDefs": [
{ {
// cache le détail de l'identité, les groupes, les colonnes admission et les vides // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
"targets": ["identite_detail", "partition_aux", "admission", "col_empty"], targets: ["codes", "identite_detail", "partition_aux", "admission", "col_empty"],
"visible": false, visible: false,
},
{
// Elimine les 0 à gauche pour les exports excel et les "copy"
targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae"],
render: function (data, type, row) {
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
}
},
{
// Elimine les décorations (fleches bonus/malus) pour les exports
targets: ["col_ue_bonus", "col_malus"],
render: function (data, type, row) {
return type === 'export' ? data.replace(/.*(\d\d\.\d\d)/, '$1').replace(/0(\d\..*)/, '$1') : data;
}
}, },
], ],
dom: 'Bfrtip', dom: 'Bfrtip',
buttons: ['copy', 'excel', 'pdf', buttons: [
{
extend: 'copyHtml5',
text: 'Copier',
exportOptions: { orthogonal: 'export' }
},
{
extend: 'excelHtml5',
exportOptions: { orthogonal: 'export' },
title: document.querySelector('table.table_recap').dataset.filename
},
{ {
extend: 'collection', extend: 'collection',
text: 'Colonnes affichées', text: 'Colonnes affichées',
@ -117,4 +145,10 @@ $(function () {
$(this).addClass('selected'); $(this).addClass('selected');
} }
}); });
// Pour montrer et highlihter l'étudiant sélectionné:
$(function () {
document.querySelector("#row_selected").scrollIntoView();
window.scrollBy(0, -50);
document.querySelector("#row_selected").classList.add("selected");
});
}); });

View File

@ -32,7 +32,7 @@
{% if can_edit_appreciations %} {% if can_edit_appreciations %}
<p><a class="stdlink" href="{{url_for( <p><a class="stdlink" href="{{url_for(
'notes.appreciation_add_form', scodoc_dept=g.scodoc_dept, 'notes.appreciation_add_form', scodoc_dept=g.scodoc_dept,
etudid=etud.id, formsemestre_id=formsemestre_id) etudid=etud.id, formsemestre_id=formsemestre.id)
}}">Ajouter une appréciation</a> }}">Ajouter une appréciation</a>
</p> </p>
{% endif %} {% endif %}

View File

@ -20,73 +20,78 @@
{% endmacro %} {% endmacro %}
{% macro render_add_logo(add_logo_form) %} {% macro render_add_logo(add_logo_form) %}
<div class="logo-add"> {% if add_logo_form.errors() %}
<h3>Ajouter un logo</h3> <details open>
{{ add_logo_form.hidden_tag() }} {% else %}
{{ render_field(add_logo_form.name) }} <details>
{{ render_field(add_logo_form.upload) }} {% endif %}
{{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }} <summary>
</div> <h3>Ajouter un logo</h3>
</summary>
<div>
{{ render_field(add_logo_form.name) }}
{{ render_field(add_logo_form.upload) }}
{{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }}
</div>
</details>
{% endmacro %} {% endmacro %}
{% macro render_logo(dept_form, logo_form) %} {% macro render_logo(dept_form, logo_form) %}
<div class="logo-edit"> {% if logo_form.errors() %}
{{ logo_form.hidden_tag() }} <details open>
{% if logo_form.titre %}
<tr class="logo-edit">
<td colspan="3" class="titre">
<div class="nom">
<h3>{{ logo_form.titre }}</h3>
</div>
<div class="description">{{ logo_form.description or "" }}</div>
</td>
</tr>
{% else %} {% else %}
<tr class="logo-edit"> <details>
<td colspan="3" class="titre">
<span class="nom">
<h3>Logo personalisé: {{ logo_form.logo_id.data }}</h3>
</span>
<span class="description">{{ logo_form.description or "" }}</span>
</td>
</tr>
{% endif %} {% endif %}
<tr> {{ logo_form.hidden_tag() }}
<td style="padding-right: 20px; "> <summary>
<div class="img-container"> {% if logo_form.titre %}
<h3 class="titre_logo">{{ logo_form.titre }}</h3>
{% if logo_form.description %}
<div class="sco_help">{{ logo_form.description }}</div>
{% endif %}
{% else %}
<h3 class="titre_logo">Logo personalisé: {{ logo_form.logo_id.data }}</h3>
{% if logo_form.description %}
<div class="sco_help">{{ logo_form.description }}</div>
{% endif %}
{% endif %}
</summary>
<div class="content">
<div class="image_logo">
<img src="{{ logo_form.logo.get_url_small() }}" alt="pas de logo chargé" /> <img src="{{ logo_form.logo.get_url_small() }}" alt="pas de logo chargé" />
</div> </div>
</td> <div class="infos_logo">
<td class="img-data"> <h4>{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})</h4>
<h3>{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})</h3> Taille: {{ logo_form.logo.size }} px
Taille: {{ logo_form.logo.size }} px {% if logo_form.logo.mm %} &nbsp; / &nbsp; {{ logo_form.logo.mm }} mm {% endif %}<br />
{% if logo_form.logo.mm %} &nbsp; / &nbsp; {{ logo_form.logo.mm }} mm {% endif %}<br /> Aspect ratio: {{ logo_form.logo.aspect_ratio }}<br />
Aspect ratio: {{ logo_form.logo.aspect_ratio }}<br /> Usage: <span style="font-family: system-ui">{{ logo_form.logo.get_usage() }}</span>
Usage: <span style="font-family: system-ui">{{ logo_form.logo.get_usage() }}</span> </div>
</td> <div class="actions_logo">
<td class="" img-action"> <div class="action_label">Modifier l'image</div>
<p>Modifier l'image</p> <div class="action_button">
<span class="wtf-field">{{ render_field(logo_form.upload, False, onchange="submit_form()") }}</span> <span class="wtf-field">{{ render_field(logo_form.upload, False, onchange="submit_form()") }}</span>
{% if logo_form.can_delete %} </div>
<p>Supprimer l'image</p> {% if logo_form.can_delete %}
{{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }} <div class="action_label">Supprimer l'image</div>
{% endif %} <div class="action_button">
</td> {{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }}
</tr> </div>
</div> {% endif %}
</div>
</div>
</details>
{% endmacro %} {% endmacro %}
{% macro render_logos(dept_form) %} {% macro render_logos(dept_form) %}
<table>
{% for logo_entry in dept_form.logos.entries %} {% for logo_entry in dept_form.logos.entries %}
{% set logo_form = logo_entry.form %} {% set logo_form = logo_entry.form %}
{{ render_logo(dept_form, logo_form) }} {{ render_logo(dept_form, logo_form) }}
{% else %} {% else %}
<p class="logo-edit"> <p class="logo-titre_logo">
<h3>Aucun logo défini en propre à ce département</h3> <h3 class="titre_logo">Aucun logo défini en propre à ce département</h3>
</p> </p>
{% endfor %} {% endfor %}
</table>
{% endmacro %} {% endmacro %}
{% block app_content %} {% block app_content %}
@ -100,25 +105,30 @@
<div class="configuration_logo"> <div class="configuration_logo">
<h1>Bibliothèque de logos</h1> <h1>Bibliothèque de logos</h1>
{% for dept_entry in form.depts.entries %} {% for dept_entry in form.depts.entries %}
{% set dept_form = dept_entry.form %} {% set dept_form = dept_entry.form %}
{{ dept_entry.form.hidden_tag() }} {{ dept_entry.form.hidden_tag() }}
{% if dept_entry.form.is_local() %} {% if dept_form.errors() %}
<div class="departement"> <details open>
<h2>Département {{ dept_form.dept_name.data }}</h2>
<h3>Logos locaux</h3>
<div class="sco_help">Les paramètres donnés sont spécifiques à ce département.<br />
Les logos du département se substituent aux logos de même nom définis globalement:</div>
</div>
{% else %} {% else %}
<div class="departement"> <details>
<h2>Logos généraux</h2>
<div class="sco_help">Les images de cette section sont utilisé pour tous les départements,
mais peuvent être redéfinies localement au niveau de chaque département
(il suffit de définir un logo local de même nom)</div>
</div>
{% endif %} {% endif %}
{{ render_logos(dept_form) }} <summary>
{{ render_add_logo(dept_form.add_logo.form) }} {% if dept_entry.form.is_local() %}
<h2>Département {{ dept_form.dept_name.data }}</h2>
<div class="sco_help">Les paramètres donnés sont spécifiques à ce département.<br />
Les logos du département se substituent aux logos de même nom définis globalement:</div>
{% else %}
<h2>Logos généraux</h2>
<div class="sco_help">Les images de cette section sont utilisé pour tous les départements,
mais peuvent être redéfinies localement au niveau de chaque département
(il suffit de définir un logo local de même nom)</div>
{% endif %}
</summary>
<div>
{{ render_logos(dept_form) }}
{{ render_add_logo(dept_form.add_logo.form) }}
</div>
</details>
{% endfor %} {% endfor %}
</div> </div>
</form> </form>

View File

@ -0,0 +1,47 @@
{# -*- mode: jinja-html -*- #}
{% extends "sco_page.html" %}
{% block styles %}
{{super()}}
{% endblock %}
{% block app_content %}
<h2>Opérations dans le département {{g.scodoc_dept}}</h2>
<table id="dept_news" class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Auteur</th>
<th>Détail</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block scripts %}
{{super()}}
<script>
$(document).ready(function () {
$('#dept_news').DataTable({
ajax: '{{url_for("scolar.dept_news_json", scodoc_dept=g.scodoc_dept)}}',
serverSide: true,
columns: [
{
data: {
_: "date.display",
sort: "date.timestamp"
}
},
{data: 'type', searchable: false},
{data: 'authenticated_user', orderable: false, searchable: true},
{data: 'text', orderable: false, searchable: true}
],
"order": [[ 0, "desc" ]]
});
});
</script>
{% endblock %}

View File

@ -15,7 +15,7 @@
<h2 class="insidebar">Dépt. {{ sco.prefs["DeptName"] }}</h2> <h2 class="insidebar">Dépt. {{ sco.prefs["DeptName"] }}</h2>
<a href="{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}" class="sidebar">Accueil</a> <br /> <a href="{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}" class="sidebar">Accueil</a> <br />
{% if sco.prefs["DeptIntranetURL"] %} {% if sco.prefs["DeptIntranetURL"] %}
<a href="{{ sco.prefs["DeptIntranetURL"] }}" class="sidebar"> <a href="{{ sco.prefs[" DeptIntranetURL"] }}" class="sidebar">
{{ sco.prefs["DeptIntranetTitle"] }}</a> {{ sco.prefs["DeptIntranetTitle"] }}</a>
{% endif %} {% endif %}
<br> <br>
@ -41,7 +41,7 @@
<form method="get" id="form-chercheetud" <form method="get" id="form-chercheetud"
action="{{ url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }}"> action="{{ url_for('scolar.search_etud_in_dept', scodoc_dept=g.scodoc_dept) }}">
<div> <div>
<input type="text" size="12" id="in-expnom" name="expnom" spellcheck="false" /> <input type="text" size="12" class="in-expnom" name="expnom" spellcheck="false" />
</div> </div>
</form> </form>
</div> </div>
@ -49,7 +49,7 @@
<div class="etud-insidebar"> <div class="etud-insidebar">
{% if sco.etud %} {% if sco.etud %}
<h2 id="insidebar-etud"><a href="{{url_for( <h2 id="insidebar-etud"><a href="{{url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=sco.etud.id )}}" class="sidebar"> 'scolar.ficheEtud', scodoc_dept=g.scodoc_dept, etudid=sco.etud.id )}}" class="sidebar">
<span class="fontred">{{sco.etud.nomprenom}}</span></a> <span class="fontred">{{sco.etud.nomprenom}}</span></a>
</h2> </h2>
<b>Absences</b> <b>Absences</b>

View File

@ -69,7 +69,7 @@ from app.decorators import (
permission_required, permission_required,
permission_required_compat_scodoc7, permission_required_compat_scodoc7,
) )
from app.models import FormSemestre from app.models import FormSemestre, GroupDescr
from app.models.absences import BilletAbsence from app.models.absences import BilletAbsence
from app.views import absences_bp as bp from app.views import absences_bp as bp
@ -119,63 +119,71 @@ def sco_publish(route, function, permission, methods=["GET"]):
@scodoc7func @scodoc7func
def index_html(): def index_html():
"""Gestionnaire absences, page principale""" """Gestionnaire absences, page principale"""
# crude portage from 1999 DTML
sems = sco_formsemestre.do_formsemestre_list()
authuser = current_user
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Gestion des absences", page_title="Saisie des absences",
cssstyles=["css/calabs.css"], cssstyles=["css/calabs.css"],
javascripts=["js/calabs.js"], javascripts=["js/calabs.js"],
), ),
"""<h2>Gestion des Absences</h2>""", """<h2>Traitement des absences</h2>
<p class="help">
Pour saisir des absences ou consulter les états, il est recommandé par passer par
le semestre concerné (saisie par jours nommés ou par semaines).
</p>
""",
] ]
if not sems: H.append(
"""<p class="help">Pour signaler, annuler ou justifier une absence pour un seul étudiant,
choisissez d'abord concerné:</p>"""
)
H.append(sco_find_etud.form_search_etud())
if current_user.has_permission(
Permission.ScoAbsChange
) and sco_preferences.get_preference("handle_billets_abs"):
H.append( H.append(
"""<p class="warning">Aucun semestre défini (ou aucun groupe d'étudiant)</p>"""
)
else:
H.append(
"""<ul><li><a href="EtatAbsences">Afficher l'état des absences (pour tout un groupe)</a></li>"""
)
if sco_preferences.get_preference("handle_billets_abs"):
H.append(
"""<li><a href="listeBillets">Traitement des billets d'absence en attente</a></li>"""
)
H.append(
"""<p>Pour signaler, annuler ou justifier une absence, choisissez d'abord l'étudiant concerné:</p>"""
)
H.append(sco_find_etud.form_search_etud())
if authuser.has_permission(Permission.ScoAbsChange):
H.extend(
(
"""<hr/>
<form action="SignaleAbsenceGrHebdo" id="formw">
<input type="hidden" name="destination" value="%s"/>
<p>
<span style="font-weight: bold; font-size:120%%;">
Saisie par semaine </span> - Choix du groupe:
<input name="datelundi" type="hidden" value="x"/>
""" """
% request.base_url, <h2 style="margin-top: 30px;">Billets d'absence</h2>
sco_abs_views.formChoixSemestreGroupe(), <ul><li><a href="listeBillets">Traitement des billets d'absence en attente</a></li></ul>
"</p>", """
cal_select_week(), )
"""<p class="help">Sélectionner le groupe d'étudiants, puis cliquez sur une semaine pour
saisir les absences de toute cette semaine.</p>
</form>""",
)
)
else:
H.append(
"""<p class="scoinfo">Vous n'avez pas l'autorisation d'ajouter, justifier ou supprimer des absences.</p>"""
)
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)
@bp.route("/choix_semaine")
@scodoc
@permission_required(Permission.ScoAbsChange)
@scodoc7func
def choix_semaine(group_id):
"""Page choix semaine sur calendrier pour saisie absences d'un groupe"""
group = GroupDescr.query.get_or_404(group_id)
H = [
html_sco_header.sco_header(
page_title="Saisie des absences",
cssstyles=["css/calabs.css"],
javascripts=["js/calabs.js"],
),
f"""
<h2>Saisie des Absences</h2>
<form action="SignaleAbsenceGrHebdo" id="formw">
<p>
<span style="font-weight: bold; font-size:120%;">
Saisie par semaine </span> - Groupe: {group.get_nom_with_part()}
<input name="datelundi" type="hidden" value="x"/>
<input name="group_ids" type="hidden" value="{group_id}"/>
</p>
""",
cal_select_week(),
"""<p class="help">Sélectionner le groupe d'étudiants, puis cliquez sur une semaine pour
saisir les absences de toute cette semaine.</p>
</form>
""",
html_sco_header.sco_footer(),
]
return "\n".join(H)
def cal_select_week(year=None): def cal_select_week(year=None):
"display calendar allowing week selection" "display calendar allowing week selection"
if not year: if not year:
@ -479,7 +487,7 @@ def SignaleAbsenceGrSemestre(
datedebut, datedebut,
datefin, datefin,
destination="", destination="",
group_ids=[], # list of groups to display group_ids=(), # list of groups to display
nbweeks=4, # ne montre que les nbweeks dernieres semaines nbweeks=4, # ne montre que les nbweeks dernieres semaines
moduleimpl_id=None, moduleimpl_id=None,
): ):
@ -566,7 +574,7 @@ def SignaleAbsenceGrSemestre(
url_link_semaines += "&moduleimpl_id=" + str(moduleimpl_id) url_link_semaines += "&moduleimpl_id=" + str(moduleimpl_id)
# #
dates = [x.ISO() for x in dates] dates = [x.ISO() for x in dates]
dayname = sco_abs.day_names()[jourdebut.weekday] day_name = sco_abs.day_names()[jourdebut.weekday]
if groups_infos.tous_les_etuds_du_sem: if groups_infos.tous_les_etuds_du_sem:
gr_tit = "en" gr_tit = "en"
@ -579,19 +587,18 @@ def SignaleAbsenceGrSemestre(
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Saisie des absences", page_title=f"Saisie des absences du {day_name}",
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js", "js/abs_ajax.js"], javascripts=["js/etud_info.js", "js/abs_ajax.js"],
no_side_bar=1, no_side_bar=1,
), ),
"""<table border="0" cellspacing="16"><tr><td> f"""<table border="0" cellspacing="16"><tr><td>
<h2>Saisie des absences %s %s, <h2>Saisie des absences {gr_tit} {sem["titre_num"]},
les <span class="fontred">%s</span></h2> les <span class="fontred">{day_name}s</span></h2>
<p> <p>
<a href="%s">%s</a> <a href="{url_link_semaines}">{msg}</a>
<form id="abs_form" action="doSignaleAbsenceGrSemestre" method="post"> <form id="abs_form" action="doSignaleAbsenceGrSemestre" method="post">
""" """,
% (gr_tit, sem["titre_num"], dayname, url_link_semaines, msg),
] ]
# #
if etuds: if etuds:
@ -820,7 +827,6 @@ def _gen_form_saisie_groupe(
# version pour formulaire avec AJAX (Yann LB) # version pour formulaire avec AJAX (Yann LB)
H.append( H.append(
""" """
<p><input type="button" value="Retour" onClick="window.location='%s'"/>
</p> </p>
</form> </form>
</p> </p>
@ -831,8 +837,7 @@ def _gen_form_saisie_groupe(
</p><p class="help">Si vous "décochez" une case, l'absence correspondante sera supprimée. </p><p class="help">Si vous "décochez" une case, l'absence correspondante sera supprimée.
Attention, les modifications sont automatiquement entregistrées au fur et à mesure. Attention, les modifications sont automatiquement entregistrées au fur et à mesure.
</p> </p>
""" """
% destination
) )
return H return H

View File

@ -100,6 +100,7 @@ from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_check_abs from app.scodoc import sco_evaluation_check_abs
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_evaluation_edit from app.scodoc import sco_evaluation_edit
from app.scodoc import sco_evaluation_recap
from app.scodoc import sco_export_results from app.scodoc import sco_export_results
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -109,22 +110,18 @@ from app.scodoc import sco_formsemestre_exterieurs
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_groups
from app.scodoc import sco_inscr_passage from app.scodoc import sco_inscr_passage
from app.scodoc import sco_liste_notes from app.scodoc import sco_liste_notes
from app.scodoc import sco_lycee from app.scodoc import sco_lycee
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_moduleimpl_inscriptions from app.scodoc import sco_moduleimpl_inscriptions
from app.scodoc import sco_moduleimpl_status from app.scodoc import sco_moduleimpl_status
from app.scodoc import sco_news
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_placement from app.scodoc import sco_placement
from app.scodoc import sco_poursuite_dut from app.scodoc import sco_poursuite_dut
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_prepajury from app.scodoc import sco_prepajury
from app.scodoc import sco_pvjury from app.scodoc import sco_pvjury
from app.scodoc import sco_pvpdf
from app.scodoc import sco_recapcomplet from app.scodoc import sco_recapcomplet
from app.scodoc import sco_report from app.scodoc import sco_report
from app.scodoc import sco_saisie_notes from app.scodoc import sco_saisie_notes
@ -136,7 +133,6 @@ from app.scodoc import sco_undo_notes
from app.scodoc import sco_users from app.scodoc import sco_users
from app.scodoc import sco_xml from app.scodoc import sco_xml
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_pdf import PDFLOCK
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.views import ScoData from app.views import ScoData
@ -235,6 +231,11 @@ sco_publish(
sco_recapcomplet.formsemestre_recapcomplet, sco_recapcomplet.formsemestre_recapcomplet,
Permission.ScoView, Permission.ScoView,
) )
sco_publish(
"/evaluations_recap",
sco_evaluation_recap.evaluations_recap,
Permission.ScoView,
)
sco_publish( sco_publish(
"/formsemestres_bulletins", "/formsemestres_bulletins",
sco_recapcomplet.formsemestres_bulletins, sco_recapcomplet.formsemestres_bulletins,
@ -1737,12 +1738,13 @@ def evaluation_listenotes():
evaluation_id = None evaluation_id = None
moduleimpl_id = None moduleimpl_id = None
vals = scu.get_request_args() vals = scu.get_request_args()
if "evaluation_id" in vals: try:
evaluation_id = int(vals["evaluation_id"]) if "evaluation_id" in vals:
mode = "eval" evaluation_id = int(vals["evaluation_id"])
if "moduleimpl_id" in vals and vals["moduleimpl_id"]: if "moduleimpl_id" in vals and vals["moduleimpl_id"]:
moduleimpl_id = int(vals["moduleimpl_id"]) moduleimpl_id = int(vals["moduleimpl_id"])
mode = "module" except ValueError as exc:
raise ScoValueError("adresse invalide !") from exc
format = vals.get("format", "html") format = vals.get("format", "html")
html_content, page_title = sco_liste_notes.do_evaluation_listenotes( html_content, page_title = sco_liste_notes.do_evaluation_listenotes(
@ -2408,6 +2410,33 @@ sco_publish(
Permission.ScoEditApo, Permission.ScoEditApo,
) )
@bp.route("/formsemestre_set_apo_etapes", methods=["POST"])
@scodoc
@permission_required(Permission.ScoEditApo)
def formsemestre_set_apo_etapes():
"""Change les codes étapes du semestre indiqué.
Args: oid=formsemestre_id, value=chaine "V1RT, V1RT2", codes séparés par des virgules
"""
formsemestre_id = int(request.form.get("oid"))
etapes_apo_str = request.form.get("value")
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
current_etapes = {e.etape_apo for e in formsemestre.etapes}
new_etapes = {s.strip() for s in etapes_apo_str.split(",")}
if new_etapes != current_etapes:
formsemestre.etapes = []
for etape_apo in new_etapes:
etape = models.FormSemestreEtape(
formsemestre_id=formsemestre_id, etape_apo=etape_apo
)
formsemestre.etapes.append(etape)
db.session.add(formsemestre)
db.session.commit()
return ("", 204)
# sco_semset # sco_semset
sco_publish("/semset_page", sco_semset.semset_page, Permission.ScoEditApo) sco_publish("/semset_page", sco_semset.semset_page, Permission.ScoEditApo)
sco_publish( sco_publish(

View File

@ -144,11 +144,12 @@ def config_codes_decisions():
if form.validate_on_submit(): if form.validate_on_submit():
for code in models.config.CODES_SCODOC_TO_APO: for code in models.config.CODES_SCODOC_TO_APO:
ScoDocSiteConfig.set_code_apo(code, getattr(form, code).data) ScoDocSiteConfig.set_code_apo(code, getattr(form, code).data)
flash(f"Codes décisions enregistrés.") flash("Codes décisions enregistrés.")
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
elif request.method == "GET": elif request.method == "GET":
for code in models.config.CODES_SCODOC_TO_APO: for code in models.config.CODES_SCODOC_TO_APO:
getattr(form, code).data = ScoDocSiteConfig.get_code_apo(code) getattr(form, code).data = ScoDocSiteConfig.get_code_apo(code)
return render_template( return render_template(
"config_codes_decisions.html", "config_codes_decisions.html",
form=form, form=form,

View File

@ -41,6 +41,7 @@ from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed from flask_wtf.file import FileField, FileAllowed
from wtforms import SubmitField from wtforms import SubmitField
from app import db
from app import log from app import log
from app.decorators import ( from app.decorators import (
scodoc, scodoc,
@ -52,8 +53,10 @@ from app.decorators import (
) )
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.etudiants import make_etud_args from app.models.etudiants import make_etud_args
from app.models.events import ScolarNews
from app.views import scolar_bp as bp from app.views import scolar_bp as bp
from app.views import ScoData
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -322,7 +325,7 @@ def showEtudLog(etudid, format="html"):
# ---------- PAGE ACCUEIL (listes) -------------- # ---------- PAGE ACCUEIL (listes) --------------
@bp.route("/") @bp.route("/", alias=True)
@bp.route("/index_html") @bp.route("/index_html")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@ -339,6 +342,67 @@ def install_info():
return sco_up_to_date.is_up_to_date() return sco_up_to_date.is_up_to_date()
@bp.route("/dept_news")
@scodoc
@permission_required(Permission.ScoView)
def dept_news():
"Affiche table des dernières opérations"
return render_template(
"dept_news.html", title=f"Opérations {g.scodoc_dept}", sco=ScoData()
)
@bp.route("/dept_news_json")
@scodoc
@permission_required(Permission.ScoView)
def dept_news_json():
"Table des news du département"
start = request.args.get("start", type=int)
length = request.args.get("length", type=int)
log(f"dept_news_json( start={start}, length={length})")
query = ScolarNews.query.filter_by(dept_id=g.scodoc_dept_id)
# search
search = request.args.get("search[value]")
if search:
query = query.filter(
db.or_(
ScolarNews.authenticated_user.like(f"%{search}%"),
ScolarNews.text.like(f"%{search}%"),
)
)
total_filtered = query.count()
# sorting
order = []
i = 0
while True:
col_index = request.args.get(f"order[{i}][column]")
if col_index is None:
break
col_name = request.args.get(f"columns[{col_index}][data]")
if col_name not in ["date", "type", "authenticated_user"]:
col_name = "date"
descending = request.args.get(f"order[{i}][dir]") == "desc"
col = getattr(ScolarNews, col_name)
if descending:
col = col.desc()
order.append(col)
i += 1
if order:
query = query.order_by(*order)
# pagination
query = query.offset(start).limit(length)
data = [news.to_dict() for news in query]
# response
return {
"data": data,
"recordsFiltered": total_filtered,
"recordsTotal": ScolarNews.query.count(),
"draw": request.args.get("draw", type=int),
}
sco_publish( sco_publish(
"/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"] "/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"]
) )

View File

@ -1,5 +1,5 @@
alembic==1.7.5 alembic==1.7.5
astroid==2.9.1 astroid==2.11.2
attrs==21.4.0 attrs==21.4.0
Babel==2.9.1 Babel==2.9.1
blinker==1.4 blinker==1.4
@ -35,6 +35,7 @@ isort==5.10.1
itsdangerous==2.0.1 itsdangerous==2.0.1
Jinja2==3.0.3 Jinja2==3.0.3
lazy-object-proxy==1.7.1 lazy-object-proxy==1.7.1
lxml==4.8.0
Mako==1.1.6 Mako==1.1.6
MarkupSafe==2.0.1 MarkupSafe==2.0.1
mccabe==0.6.1 mccabe==0.6.1
@ -53,6 +54,7 @@ pyOpenSSL==21.0.0
pyparsing==3.0.6 pyparsing==3.0.6
pytest==6.2.5 pytest==6.2.5
python-dateutil==2.8.2 python-dateutil==2.8.2
python-docx==0.8.11
python-dotenv==0.19.2 python-dotenv==0.19.2
python-editor==1.0.4 python-editor==1.0.4
pytz==2021.3 pytz==2021.3

View File

@ -1,13 +1,21 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.1.90" SCOVERSION = "9.2.4"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"
SCONEWS = """ SCONEWS = """
<h4>Année 2021</h4> <h4>Année 2021</h4>
<ul> <ul>
<li>ScoDoc 9.2:
<ul>
<li>Tableau récap. complet pour BUT et autres formations.</li>
<li>Tableau état évaluations</li>
<li>Version alpha du module "relations entreprises"</li>
<li>Export des trombinoscope en document docx</li>
<li>Très nombreux correctifs</li>
</ul>
<li>ScoDoc 9.1.75: bulletins BUT pdf</li> <li>ScoDoc 9.1.75: bulletins BUT pdf</li>
<li>ScoDoc 9.1.50: nombreuses amélioration gestion BUT</li> <li>ScoDoc 9.1.50: nombreuses amélioration gestion BUT</li>
<li>ScoDoc 9.1: gestion des formations par compétences, type BUT.</li> <li>ScoDoc 9.1: gestion des formations par compétences, type BUT.</li>

View File

@ -77,6 +77,7 @@ def make_shell_context():
"pp": pp, "pp": pp,
"Role": Role, "Role": Role,
"scolar": scolar, "scolar": scolar,
"ScolarNews": models.ScolarNews,
"scu": scu, "scu": scu,
"UniteEns": UniteEns, "UniteEns": UniteEns,
"User": User, "User": User,

View File

@ -35,7 +35,7 @@ cnx = psycopg2.connect(DBCNXSTRING)
cursor = cnx.cursor() cursor = cnx.cursor()
cursor.execute( cursor.execute(
"select count(*) from notes_formsemestre where formsemestre_id=%(formsemestre_id)s", "select count(*) from notes_formsemestre where id=%(formsemestre_id)s",
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
) )
nsem = cursor.fetchone()[0] nsem = cursor.fetchone()[0]
@ -44,9 +44,9 @@ if nsem != 1:
sys.exit(2) sys.exit(2)
cursor.execute( cursor.execute(
"""select i.etudid """select i.id
from identite i, notes_formsemestre_inscription ins from identite i, notes_formsemestre_inscription ins
where i.etudid=ins.etudid and ins.formsemestre_id=%(formsemestre_id)s where i.id=ins.etudid and ins.formsemestre_id=%(formsemestre_id)s
""", """,
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
) )
@ -64,7 +64,7 @@ for (etudid,) in cursor:
"etudid": etudid, "etudid": etudid,
"code_nip": random.randrange(10000000, 99999999), "code_nip": random.randrange(10000000, 99999999),
} }
req = "update identite set nom=%(nom)s, prenom=%(prenom)s, civilite=%(civilite)s where etudid=%(etudid)s" req = "update identite set nom=%(nom)s, prenom=%(prenom)s, civilite=%(civilite)s where id=%(etudid)s"
# print( req % args) # print( req % args)
wcursor.execute(req, args) wcursor.execute(req, args)
n += 1 n += 1