forked from ScoDoc/ScoDoc
Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into entreprises
This commit is contained in:
commit
24b6d1f4cc
@ -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.
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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})
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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)
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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">
|
||||||
"""
|
"""
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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"],
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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"],
|
||||||
)
|
)
|
||||||
|
148
app/scodoc/sco_evaluation_recap.py
Normal file
148
app/scodoc/sco_evaluation_recap.py
Normal 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
|
@ -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 ""
|
||||||
|
@ -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)
|
||||||
|
@ -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, ...)"
|
||||||
|
|
||||||
|
@ -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)
|
||||||
"""
|
"""
|
||||||
|
@ -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:
|
||||||
|
@ -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},
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
|
@ -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
@ -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
|
||||||
|
@ -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"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
<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()
|
||||||
|
76
app/scodoc/sco_trombino_doc.py
Normal file
76
app/scodoc/sco_trombino_doc.py
Normal 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
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
38
app/static/js/evaluations_recap.js
Normal file
38
app/static/js/evaluations_recap.js
Normal 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
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
})
|
||||||
|
});
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
22
app/static/js/scolar_index.js
Normal file
22
app/static/js/scolar_index.js
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %} / {{ logo_form.logo.mm }} mm {% endif %}<br />
|
||||||
{% if logo_form.logo.mm %} / {{ 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>
|
||||||
|
47
app/templates/dept_news.html
Normal file
47
app/templates/dept_news.html
Normal 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 %}
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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"]
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user