forked from ScoDoc/ScoDoc
Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
62e9c02680 | |||
6b49c8472d | |||
841ae1c7ab | |||
187f4721eb | |||
83d538e2a2 | |||
795de44c0c | |||
0d726aa428 | |||
c65689b2a3 | |||
ae0baf8c1a | |||
1e5ef96f8f | |||
0b28583953 | |||
e270ad5520 | |||
95000ed8a8 | |||
eaa7c64e41 |
|
@ -69,7 +69,12 @@ Puis remplacer `/opt/scodoc` par un clone du git.
|
|||
cd /opt
|
||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
mv ScoDoc scodoc # important !
|
||||
|
||||
# Renommer le répertoire:
|
||||
mv ScoDoc scodoc
|
||||
|
||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc.scodoc /opt/scodoc
|
||||
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
|
||||
|
|
1
app/but/__init__.py
Normal file
1
app/but/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -374,7 +374,7 @@ class BulletinBUT:
|
|||
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
etud_etat,
|
||||
self.prefs,
|
||||
decision_sem=d["semestre"].get("decision_sem"),
|
||||
decision_sem=d["semestre"].get("decision"),
|
||||
)
|
||||
if etud_etat == scu.DEMISSION:
|
||||
d["demission"] = "(Démission)"
|
||||
|
|
|
@ -21,7 +21,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
self.infos est le dict issu de BulletinBUT.bulletin_etud_complet()
|
||||
"""
|
||||
|
||||
list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
|
||||
# spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur:
|
||||
list_in_menu = False
|
||||
scale_table_in_page = False # pas de mise à l'échelle pleine page auto
|
||||
multi_pages = True # plusieurs pages par bulletins
|
||||
small_fontsize = "8"
|
||||
|
@ -78,7 +79,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
"coef": 2 * cm,
|
||||
}
|
||||
title_bg = tuple(x / 255.0 for x in title_bg)
|
||||
nota_bene = "La moyenne des ressources et SAÉs dans une UE dépend des poids donnés aux évaluations."
|
||||
nota_bene = """La moyenne des ressources et SAÉs dans une UE
|
||||
dépend des poids donnés aux évaluations."""
|
||||
# elems pour générer table avec gen_table (liste de dicts)
|
||||
rows = [
|
||||
# Ligne de titres
|
||||
|
@ -130,7 +132,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
t = {
|
||||
"titre": f"{ue_acronym} - {ue['titre']}",
|
||||
"moyenne": Paragraph(
|
||||
f"""<para align=right><b>{moy_ue.get("value", "-") if moy_ue is not None else "-"}</b></para>"""
|
||||
f"""<para align=right><b>{moy_ue.get("value", "-")
|
||||
if moy_ue is not None else "-"
|
||||
}</b></para>"""
|
||||
),
|
||||
"_css_row_class": "note_bold",
|
||||
"_pdf_row_markup": ["b"],
|
||||
|
@ -331,7 +335,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
col_idx = 1 # 1ere col. poids
|
||||
for ue_acro in ue_acros:
|
||||
t[ue_acro] = Paragraph(
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{e["poids"].get(ue_acro, "") or ""}</i></para>"""
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
e["poids"].get(ue_acro, "") or ""}</i></para>"""
|
||||
)
|
||||
t["_pdf_style"].append(
|
||||
(
|
||||
|
|
1
app/comp/__init__.py
Normal file
1
app/comp/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -3,11 +3,9 @@
|
|||
|
||||
"""Matrices d'inscription aux modules d'un semestre
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import db
|
||||
from app import models
|
||||
|
||||
#
|
||||
# Le chargement des inscriptions est long: matrice nb_module x nb_etuds
|
||||
|
|
|
@ -16,7 +16,7 @@ from app.scodoc import sco_codes_parcours
|
|||
|
||||
|
||||
class ValidationsSemestre(ResultatsCache):
|
||||
""" """
|
||||
"""Les décisions de jury pour un semestre"""
|
||||
|
||||
_cached_attrs = (
|
||||
"decisions_jury",
|
||||
|
|
|
@ -19,7 +19,6 @@ from app.models.moduleimpls import ModuleImpl
|
|||
from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||
from app.scodoc import sco_preferences
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
@ -33,7 +32,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
|
||||
def __init__(self, formsemestre):
|
||||
super().__init__(formsemestre)
|
||||
"""DataFrame, row UEs(sans bonus), cols modimplid, value coef"""
|
||||
|
||||
self.sem_cube = None
|
||||
"""ndarray (etuds x modimpl x ue)"""
|
||||
|
||||
|
@ -44,7 +43,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
self.store()
|
||||
t2 = time.time()
|
||||
log(
|
||||
f"ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
||||
f"""ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id
|
||||
} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||
)
|
||||
|
||||
def compute(self):
|
||||
|
|
|
@ -11,6 +11,14 @@ from app.models import FormSemestre
|
|||
|
||||
|
||||
class ResultatsCache:
|
||||
"""Résultats cachés (via redis)
|
||||
L'attribut _cached_attrs donne la liste des noms des attributs à cacher
|
||||
(doivent être sérialisables facilement, se limiter à des types simples)
|
||||
|
||||
store() enregistre les attributs dans le cache, et
|
||||
load_cached() les recharge.
|
||||
"""
|
||||
|
||||
_cached_attrs = () # virtual
|
||||
|
||||
def __init__(self, formsemestre: FormSemestre, cache_class=None):
|
||||
|
|
|
@ -50,7 +50,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||
self.store()
|
||||
t2 = time.time()
|
||||
log(
|
||||
f"ResultatsSemestreClassic: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
|
||||
f"""ResultatsSemestreClassic: cached formsemestre_id={
|
||||
formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"""
|
||||
)
|
||||
# recalculé (aussi rapide que de les cacher)
|
||||
self.moy_min = self.etud_moy_gen.min()
|
||||
|
@ -220,36 +221,29 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||
moyenne générale.
|
||||
Coef = somme des coefs des modules de l'UE auxquels il est inscrit
|
||||
"""
|
||||
c = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
||||
if c is not None: # inscrit à au moins un module de cette UE
|
||||
return c
|
||||
coef = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
|
||||
if coef is not None: # inscrit à au moins un module de cette UE
|
||||
return coef
|
||||
# arfff: aucun moyen de déterminer le coefficient de façon sûre
|
||||
log(
|
||||
"* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s"
|
||||
% (self.formsemestre.id, etudid, ue)
|
||||
f"""* oups: calcul coef UE impossible\nformsemestre_id='{self.formsemestre.id
|
||||
}'\netudid='{etudid}'\nue={ue}"""
|
||||
)
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
raise ScoValueError(
|
||||
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
|
||||
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
|
||||
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||
impossible à déterminer pour l'étudiant <a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="discretelink">{etud.nom_disp()}</a></p>
|
||||
<p>Il faut <a href="{
|
||||
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre.id, err_ue_id=ue["ue_id"],
|
||||
)
|
||||
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
</div>
|
||||
"""
|
||||
% (
|
||||
ue.acronyme,
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
etud.nom_disp(),
|
||||
url_for(
|
||||
"notes.formsemestre_edit_uecoefs",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre.id,
|
||||
err_ue_id=ue["ue_id"],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return 0.0 # ?
|
||||
|
||||
|
||||
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
|
||||
"""Calcule la matrice des notes du semestre
|
||||
|
@ -279,7 +273,7 @@ def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
|
|||
(Series rendus par compute_module_moy, index: etud)
|
||||
Resultat: ndarray (etud x module)
|
||||
"""
|
||||
if not len(modimpls_notes):
|
||||
if not modimpls_notes:
|
||||
return np.zeros((0, 0), dtype=float)
|
||||
modimpls_notes_arr = [s.values for s in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Résultats semestre: méthodes communes aux formations classiques et APC
|
||||
"""
|
||||
|
||||
from collections import Counter
|
||||
from functools import cached_property
|
||||
import numpy as np
|
||||
|
@ -20,12 +23,12 @@ from app.models import ModuleImpl, ModuleImplInscription
|
|||
from app.models.ues import UniteEns
|
||||
from app.scodoc.sco_cache import ResultatsSemestreCache
|
||||
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 import sco_groups
|
||||
from app.scodoc import sco_users
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
# Il faut bien distinguer
|
||||
# - ce qui est caché de façon persistente (via redis):
|
||||
# ce sont les attributs listés dans `_cached_attrs`
|
||||
|
@ -41,6 +44,8 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"""
|
||||
|
||||
_cached_attrs = (
|
||||
"bonus",
|
||||
"bonus_ues",
|
||||
"etud_moy_gen_ranks",
|
||||
"etud_moy_gen",
|
||||
"etud_moy_ue",
|
||||
|
@ -55,6 +60,10 @@ class ResultatsSemestre(ResultatsCache):
|
|||
# BUT ou standard ? (apc == "approche par compétences")
|
||||
self.is_apc = formsemestre.formation.is_apc()
|
||||
# Attributs "virtuels", définis dans les sous-classes
|
||||
self.bonus: pd.Series = None # virtuel
|
||||
"Bonus sur moy. gen. Series de float, index etudid"
|
||||
self.bonus_ues: pd.DataFrame = None # virtuel
|
||||
"DataFrame de float, index etudid, columns: ue.id"
|
||||
# ResultatsSemestreBUT ou ResultatsSemestreClassic
|
||||
self.etud_moy_ue = {}
|
||||
"etud_moy_ue: DataFrame columns UE, rows etudid"
|
||||
|
@ -102,6 +111,14 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"dict { etudid : indice dans les inscrits }"
|
||||
return {e.id: idx for idx, e in enumerate(self.etuds)}
|
||||
|
||||
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||
Utile pour stats bottom tableau recap.
|
||||
Résultat: 1d array of float
|
||||
"""
|
||||
# différent en BUT et classique: virtuelle
|
||||
raise NotImplementedError
|
||||
|
||||
@cached_property
|
||||
def etuds_dict(self) -> dict[int, Identite]:
|
||||
"""dict { etudid : Identite } inscrits au semestre,
|
||||
|
@ -230,6 +247,13 @@ class ResultatsSemestre(ResultatsCache):
|
|||
0.0, min(self.etud_moy_gen[etudid], 20.0)
|
||||
)
|
||||
|
||||
def get_etud_etat(self, etudid: int) -> str:
|
||||
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
||||
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
||||
if ins is None:
|
||||
return ""
|
||||
return ins.etat
|
||||
|
||||
def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict:
|
||||
"""Donne les informations sur la capitalisation de l'UE ue pour cet étudiant.
|
||||
Résultat:
|
||||
|
@ -304,11 +328,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||
if coef_ue is None:
|
||||
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
|
||||
raise ScoValueError(
|
||||
f"""L'UE capitalisée {ue_capitalized.acronyme}
|
||||
f"""L'UE capitalisée {ue_capitalized.acronyme}
|
||||
du semestre {orig_sem.titre_annee()}
|
||||
n'a pas d'indication d'ECTS.
|
||||
Corrigez ou faite corriger le programme
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
Corrigez ou faite corriger le programme
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
||||
"""
|
||||
)
|
||||
|
@ -333,6 +357,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None,
|
||||
}
|
||||
|
||||
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
||||
"Détermine le coefficient de l'UE pour cet étudiant."
|
||||
# calcul différent en classqiue et BUT
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_etud_ue_cap_coef(self, etudid, ue, ue_cap):
|
||||
"""Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
|
||||
injectée dans le semestre courant.
|
||||
|
@ -359,7 +388,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||
|
||||
# --- TABLEAU RECAP
|
||||
|
||||
def get_table_recap(self, convert_values=False):
|
||||
def get_table_recap(self, convert_values=False, include_evaluations=False):
|
||||
"""Result: tuple avec
|
||||
- rows: liste de dicts { column_id : value }
|
||||
- titles: { column_id : title }
|
||||
|
@ -391,52 +420,67 @@ class ResultatsSemestre(ResultatsCache):
|
|||
else:
|
||||
fmt_note = lambda x: x
|
||||
|
||||
barre_moy = (
|
||||
self.formsemestre.formation.get_parcours().BARRE_MOY - scu.NOTES_TOLERANCE
|
||||
)
|
||||
barre_valid_ue = self.formsemestre.formation.get_parcours().NOTES_BARRE_VALID_UE
|
||||
parcours = self.formsemestre.formation.get_parcours()
|
||||
barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
|
||||
barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
|
||||
barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING
|
||||
NO_NOTE = "-" # contenu des cellules sans notes
|
||||
rows = []
|
||||
# column_id : title
|
||||
titles = {
|
||||
"rang": "Rg",
|
||||
# ordre des colonnes:
|
||||
"_rang_col_order": 1,
|
||||
"_civilite_str_col_order": 2,
|
||||
"_nom_disp_col_order": 3,
|
||||
"_prenom_col_order": 4,
|
||||
"_nom_short_col_order": 5,
|
||||
"_rang_col_order": 6,
|
||||
}
|
||||
titles = {}
|
||||
# les titres en footer: les mêmes, mais avec des bulles et liens:
|
||||
titles_bot = {}
|
||||
|
||||
def add_cell(
|
||||
row: dict, col_id: str, title: str, content: str, classes: str = ""
|
||||
row: dict,
|
||||
col_id: str,
|
||||
title: str,
|
||||
content: str,
|
||||
classes: str = "",
|
||||
idx: int = 100,
|
||||
):
|
||||
"Add a row to our table. classes is a list of css class names"
|
||||
row[col_id] = content
|
||||
if classes:
|
||||
row[f"_{col_id}_class"] = classes
|
||||
row[f"_{col_id}_class"] = classes + f" c{idx}"
|
||||
if not col_id in titles:
|
||||
titles[col_id] = title
|
||||
titles[f"_{col_id}_col_order"] = idx
|
||||
if classes:
|
||||
titles[f"_{col_id}_class"] = classes
|
||||
return idx + 1
|
||||
|
||||
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
||||
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
||||
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
|
||||
modimpl_ids = set() # modimpl effectivement présents dans la table
|
||||
for etudid in etuds_inscriptions:
|
||||
idx = 0 # index de la colonne
|
||||
etud = Identite.query.get(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
|
||||
add_cell(row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang")
|
||||
idx = add_cell(
|
||||
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
|
||||
)
|
||||
row["_rang_order"] = f"{self.etud_moy_gen_ranks_int[etudid]:05d}"
|
||||
# --- Identité étudiant
|
||||
add_cell(row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||
add_cell(row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
||||
add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail")
|
||||
add_cell(row, "nom_short", "Nom", etud.nom_short, "identite_court")
|
||||
idx = add_cell(
|
||||
row, "civilite_str", "Civ.", etud.civilite_str, "identite_detail", idx
|
||||
)
|
||||
idx = add_cell(
|
||||
row, "nom_disp", "Nom", etud.nom_disp(), "identite_detail", idx
|
||||
)
|
||||
row["_nom_disp_order"] = etud.sort_key
|
||||
idx = add_cell(row, "prenom", "Prénom", etud.prenom, "identite_detail", idx)
|
||||
idx = add_cell(
|
||||
row, "nom_short", "Nom", etud.nom_short, "identite_court", idx
|
||||
)
|
||||
row["_nom_short_order"] = etud.sort_key
|
||||
row["_nom_short_target"] = url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
|
@ -446,25 +490,29 @@ class ResultatsSemestre(ResultatsCache):
|
|||
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
|
||||
row["_nom_disp_target"] = row["_nom_short_target"]
|
||||
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
|
||||
|
||||
idx = 30 # début des colonnes de notes
|
||||
# --- Moyenne générale
|
||||
moy_gen = self.etud_moy_gen.get(etudid, False)
|
||||
note_class = ""
|
||||
if moy_gen is False:
|
||||
moy_gen = NO_NOTE
|
||||
elif isinstance(moy_gen, float) and moy_gen < barre_moy:
|
||||
note_class = " moy_inf"
|
||||
add_cell(
|
||||
note_class = " moy_ue_warning" # en rouge
|
||||
idx = add_cell(
|
||||
row,
|
||||
"moy_gen",
|
||||
"Moy",
|
||||
fmt_note(moy_gen),
|
||||
"col_moy_gen" + note_class,
|
||||
idx,
|
||||
)
|
||||
titles_bot["_moy_gen_target_attrs"] = (
|
||||
'title="moyenne indicative"' if self.is_apc else ""
|
||||
)
|
||||
# --- Moyenne d'UE
|
||||
for ue in [ue for ue in ues if ue.type != UE_SPORT]:
|
||||
nb_ues_validables, nb_ues_warning = 0, 0
|
||||
for ue in ues_sans_bonus:
|
||||
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
||||
if ue_status is not None:
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
|
@ -475,16 +523,36 @@ class ResultatsSemestre(ResultatsCache):
|
|||
note_class = " moy_inf"
|
||||
elif val >= barre_valid_ue:
|
||||
note_class = " moy_ue_valid"
|
||||
add_cell(
|
||||
nb_ues_validables += 1
|
||||
if val < barre_warning_ue:
|
||||
note_class = " moy_ue_warning" # notes très basses
|
||||
nb_ues_warning += 1
|
||||
idx = add_cell(
|
||||
row,
|
||||
col_id,
|
||||
ue.acronyme,
|
||||
fmt_note(val),
|
||||
"col_ue" + note_class,
|
||||
idx,
|
||||
)
|
||||
titles_bot[
|
||||
f"_{col_id}_target_attrs"
|
||||
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
|
||||
# Bonus (sport) dans cette UE ?
|
||||
# Le bonus sport appliqué sur cette UE
|
||||
if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
|
||||
val = self.bonus_ues[ue.id][etud.id] or ""
|
||||
val_fmt = fmt_note(val)
|
||||
if val:
|
||||
val_fmt = f'<span class="green-arrow-up"></span><span class="sp2l">{val_fmt}</span>'
|
||||
idx = add_cell(
|
||||
row,
|
||||
f"bonus_ue_{ue.id}",
|
||||
f"Bonus {ue.acronyme}",
|
||||
val_fmt,
|
||||
"col_ue_bonus",
|
||||
idx,
|
||||
)
|
||||
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
||||
for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False):
|
||||
if ue_status["is_capitalized"]:
|
||||
|
@ -511,13 +579,19 @@ class ResultatsSemestre(ResultatsCache):
|
|||
col_id = (
|
||||
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
||||
)
|
||||
add_cell(
|
||||
val_fmt = fmt_note(val)
|
||||
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
||||
val_fmt = (
|
||||
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
|
||||
)
|
||||
idx = add_cell(
|
||||
row,
|
||||
col_id,
|
||||
modimpl.module.code,
|
||||
fmt_note(val),
|
||||
val_fmt,
|
||||
# class col_res mod_ue_123
|
||||
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
|
||||
idx,
|
||||
)
|
||||
titles_bot[f"_{col_id}_target"] = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
|
@ -530,27 +604,55 @@ class ResultatsSemestre(ResultatsCache):
|
|||
title="{modimpl.module.titre}
|
||||
({sco_users.user_info(modimpl.responsable_id)['nomcomplet']})" """
|
||||
modimpl_ids.add(modimpl.id)
|
||||
|
||||
ue_valid_txt = f"{nb_ues_validables}/{len(ues_sans_bonus)}"
|
||||
if nb_ues_warning:
|
||||
ue_valid_txt += " " + scu.EMO_WARNING
|
||||
add_cell(
|
||||
row,
|
||||
"ues_validables",
|
||||
"UEs",
|
||||
ue_valid_txt,
|
||||
"col_ue col_ues_validables",
|
||||
29, # juste avant moy. gen.
|
||||
)
|
||||
if nb_ues_warning:
|
||||
row["_ues_validables_class"] += " moy_ue_warning"
|
||||
elif nb_ues_validables < len(ues_sans_bonus):
|
||||
row["_ues_validables_class"] += " moy_inf"
|
||||
row["_ues_validables_order"] = nb_ues_validables # pour tri
|
||||
rows.append(row)
|
||||
|
||||
self._recap_add_partitions(rows, titles)
|
||||
self._recap_add_admissions(rows, titles)
|
||||
|
||||
# tri par rang croissant
|
||||
rows.sort(key=lambda e: e["_rang_order"])
|
||||
|
||||
# INFOS POUR FOOTER
|
||||
bottom_infos = self._recap_bottom_infos(
|
||||
[ue for ue in ues if ue.type != UE_SPORT], 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
|
||||
for col_id in titles:
|
||||
c_class = f"_{col_id}_class"
|
||||
if "col_empty" in bottom_infos["moy"].get(c_class, ""):
|
||||
for row in rows:
|
||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
||||
titles[c_class] += " col_empty"
|
||||
for row in bottom_infos.values():
|
||||
row[c_class] = row.get(c_class, "") + " col_empty"
|
||||
|
||||
# --- TABLE FOOTER: ECTS, moyennes, min, max...
|
||||
footer_rows = []
|
||||
for bottom_line in bottom_infos:
|
||||
row = bottom_infos[bottom_line]
|
||||
for (bottom_line, row) in bottom_infos.items():
|
||||
# Cases vides à styler:
|
||||
row["moy_gen"] = row.get("moy_gen", "")
|
||||
row["_moy_gen_class"] = "col_moy_gen"
|
||||
# titre de la ligne:
|
||||
row["prenom"] = row["nom_short"] = bottom_line.capitalize()
|
||||
row["prenom"] = row["nom_short"] = (
|
||||
row.get(f"_title", "") or bottom_line.capitalize()
|
||||
)
|
||||
row["_tr_class"] = bottom_line.lower() + (
|
||||
(" " + row["_tr_class"]) if "_tr_class" in row else ""
|
||||
)
|
||||
|
@ -558,54 +660,65 @@ class ResultatsSemestre(ResultatsCache):
|
|||
titles_bot.update(titles)
|
||||
footer_rows.append(titles_bot)
|
||||
column_ids = [title for title in titles if not title.startswith("_")]
|
||||
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 100))
|
||||
column_ids.sort(
|
||||
key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000)
|
||||
)
|
||||
return (rows, footer_rows, titles, column_ids)
|
||||
|
||||
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
|
||||
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
|
||||
row_min, row_max, row_moy, row_coef, row_ects = (
|
||||
{"_tr_class": "bottom_info"},
|
||||
row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
|
||||
{"_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", "_title": "Code Apogée"},
|
||||
)
|
||||
# --- ECTS
|
||||
for ue in ues:
|
||||
row_ects[f"moy_ue_{ue.id}"] = ue.ects
|
||||
row_ects[f"_moy_ue_{ue.id}_class"] = "col_ue"
|
||||
colid = f"moy_ue_{ue.id}"
|
||||
row_ects[colid] = ue.ects
|
||||
row_ects[f"_{colid}_class"] = "col_ue"
|
||||
# style cases vides pour borders verticales
|
||||
row_coef[f"moy_ue_{ue.id}"] = ""
|
||||
row_coef[f"_moy_ue_{ue.id}_class"] = "col_ue"
|
||||
row_coef[colid] = ""
|
||||
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_class"] = "col_moy_gen"
|
||||
|
||||
# --- MIN, MAX, MOY
|
||||
# --- MIN, MAX, MOY, APO
|
||||
|
||||
row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
|
||||
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
|
||||
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
|
||||
for ue in ues:
|
||||
col_id = f"moy_ue_{ue.id}"
|
||||
row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min())
|
||||
row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max())
|
||||
row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean())
|
||||
row_min[f"_{col_id}_class"] = "col_ue"
|
||||
row_max[f"_{col_id}_class"] = "col_ue"
|
||||
row_moy[f"_{col_id}_class"] = "col_ue"
|
||||
colid = f"moy_ue_{ue.id}"
|
||||
row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min())
|
||||
row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max())
|
||||
row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean())
|
||||
row_min[f"_{colid}_class"] = "col_ue"
|
||||
row_max[f"_{colid}_class"] = "col_ue"
|
||||
row_moy[f"_{colid}_class"] = "col_ue"
|
||||
row_apo[colid] = ue.code_apogee or ""
|
||||
|
||||
for modimpl in self.formsemestre.modimpls_sorted:
|
||||
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:
|
||||
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
|
||||
else:
|
||||
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)
|
||||
row_min[col_id] = fmt_note(np.nanmin(notes))
|
||||
row_max[col_id] = fmt_note(np.nanmax(notes))
|
||||
row_moy[col_id] = fmt_note(np.nanmean(notes))
|
||||
row_min[colid] = fmt_note(np.nanmin(notes))
|
||||
row_max[colid] = fmt_note(np.nanmax(notes))
|
||||
moy = np.nanmean(notes)
|
||||
row_moy[colid] = fmt_note(moy)
|
||||
if np.isnan(moy):
|
||||
# aucune note dans ce module
|
||||
row_moy[f"_{colid}_class"] = "col_empty"
|
||||
row_apo[colid] = modimpl.module.code_apogee or ""
|
||||
|
||||
return { # { key : row } avec key = min, max, moy, coef
|
||||
"min": row_min,
|
||||
|
@ -613,6 +726,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"moy": row_moy,
|
||||
"coef": row_coef,
|
||||
"ects": row_ects,
|
||||
"apo": row_apo,
|
||||
}
|
||||
|
||||
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict):
|
||||
|
@ -621,7 +735,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||
# if dec:
|
||||
# codes_nb[dec["code"]] += 1
|
||||
row_class = ""
|
||||
etud_etat = self.get_etud_etat(etudid) # dans NotesTableCompat, à revoir
|
||||
etud_etat = self.get_etud_etat(etudid)
|
||||
if etud_etat == DEM:
|
||||
gr_name = "Dém."
|
||||
row_class = "dem"
|
||||
|
@ -641,7 +755,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||
|
||||
def _recap_add_admissions(self, rows: list[dict], titles: dict):
|
||||
"""Ajoute les colonnes "admission"
|
||||
rows est une liste de dict avec un clé "etudid"
|
||||
rows est une liste de dict avec une clé "etudid"
|
||||
Les colonnes ont la classe css "admission"
|
||||
"""
|
||||
fields = {
|
||||
|
@ -650,6 +764,14 @@ class ResultatsSemestre(ResultatsCache):
|
|||
"type_admission": "Type Adm.",
|
||||
"classement": "Rg. Adm.",
|
||||
}
|
||||
first = True
|
||||
for i, cid in enumerate(fields):
|
||||
titles[f"_{cid}_col_order"] = 10000 + i # tout à droite
|
||||
if first:
|
||||
titles[f"_{cid}_class"] = "admission admission_first"
|
||||
first = False
|
||||
else:
|
||||
titles[f"_{cid}_class"] = "admission"
|
||||
titles.update(fields)
|
||||
for row in rows:
|
||||
etud = Identite.query.get(row["etudid"])
|
||||
|
@ -662,12 +784,10 @@ class ResultatsSemestre(ResultatsCache):
|
|||
first = False
|
||||
else:
|
||||
row[f"_{cid}_class"] = "admission"
|
||||
titles[f"_{cid}_class"] = row[f"_{cid}_class"]
|
||||
titles[f"_{cid}_col_order"] = 1000 # à la fin
|
||||
|
||||
def _recap_add_partitions(self, rows: list[dict], titles: dict):
|
||||
"""Ajoute les colonnes indiquant les groupes
|
||||
rows est une liste de dict avec un clé "etudid"
|
||||
rows est une liste de dict avec une clé "etudid"
|
||||
Les colonnes ont la classe css "partition"
|
||||
"""
|
||||
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
||||
|
@ -700,3 +820,48 @@ class ResultatsSemestre(ResultatsCache):
|
|||
row[f"{cid}"] = gr_name
|
||||
row[f"_{cid}_class"] = klass
|
||||
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 = True
|
||||
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}
|
||||
klass = "evaluation first" if first else "evaluation"
|
||||
first = False
|
||||
for i, e in enumerate(evals):
|
||||
cid = f"eval_{e.id}"
|
||||
titles[
|
||||
cid
|
||||
] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
|
||||
titles[f"_{cid}_class"] = klass
|
||||
titles[f"_{cid}_col_order"] = 9000 + i # à droite
|
||||
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
|
||||
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 ""
|
||||
|
|
|
@ -32,8 +32,6 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"""
|
||||
|
||||
_cached_attrs = ResultatsSemestre._cached_attrs + (
|
||||
"bonus",
|
||||
"bonus_ues",
|
||||
"malus",
|
||||
"etud_moy_gen_ranks",
|
||||
"etud_moy_gen_ranks_int",
|
||||
|
@ -44,8 +42,6 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
super().__init__(formsemestre)
|
||||
|
||||
nb_etuds = len(self.etuds)
|
||||
self.bonus = None # virtuel
|
||||
self.bonus_ues = None # virtuel
|
||||
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
|
||||
self.mod_rangs = None # sera surchargé en Classic, mais pas en APC
|
||||
"""{ modimpl_id : (rangs, effectif) }"""
|
||||
|
@ -131,8 +127,9 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
Markup(
|
||||
f"""Calcul moyenne générale impossible: ECTS des UE manquants !<br>
|
||||
(dans {' ,'.join([ue.acronyme for ue in ue_sans_ects])}
|
||||
de la formation: <a href="{url_for("notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation.id)}">{formation.get_titre_version()}</a>)
|
||||
de la formation: <a href="{url_for("notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
||||
}">{formation.get_titre_version()}</a>)
|
||||
)
|
||||
"""
|
||||
),
|
||||
|
@ -146,7 +143,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"""
|
||||
modimpls_dict = []
|
||||
for modimpl in self.formsemestre.modimpls_sorted:
|
||||
if ue_id == None or modimpl.module.ue.id == ue_id:
|
||||
if (ue_id is None) or (modimpl.module.ue.id == ue_id):
|
||||
d = modimpl.to_dict()
|
||||
# compat ScoDoc < 9.2: ajoute matières
|
||||
d["mat"] = modimpl.module.matiere.to_dict()
|
||||
|
@ -250,13 +247,6 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
)
|
||||
return self.validations.decisions_jury.get(etudid, None)
|
||||
|
||||
def get_etud_etat(self, etudid: int) -> str:
|
||||
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
||||
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
||||
if ins is None:
|
||||
return ""
|
||||
return ins.etat
|
||||
|
||||
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
|
||||
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
|
||||
if not self.moyennes_matieres:
|
||||
|
@ -285,7 +275,8 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
def get_etud_ects_pot(self, etudid: int) -> dict:
|
||||
"""
|
||||
Un dict avec les champs
|
||||
ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury)
|
||||
ects_pot : (float) nb de crédits ECTS qui seraient validés
|
||||
(sous réserve de validation par le jury)
|
||||
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
|
||||
|
||||
Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non
|
||||
|
@ -307,10 +298,14 @@ class NotesTableCompat(ResultatsSemestre):
|
|||
"ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé)
|
||||
}
|
||||
|
||||
def get_etud_rang(self, etudid: int):
|
||||
def get_etud_rang(self, etudid: int) -> str:
|
||||
"""Le rang (classement) de l'étudiant dans le semestre.
|
||||
Result: "13" ou "12 ex"
|
||||
"""
|
||||
return self.etud_moy_gen_ranks.get(etudid, 99999)
|
||||
|
||||
def get_etud_rang_group(self, etudid: int, group_id: int):
|
||||
"Le rang de l'étudiant dans ce groupe (NON IMPLEMENTE)"
|
||||
return (None, 0) # XXX unimplemented TODO
|
||||
|
||||
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
"""Decorators for permissions, roles and ScoDoc7 Zope compatibility
|
||||
"""
|
||||
import functools
|
||||
from functools import wraps
|
||||
import inspect
|
||||
import types
|
||||
import logging
|
||||
|
||||
|
||||
import werkzeug
|
||||
from werkzeug.exceptions import BadRequest
|
||||
import flask
|
||||
from flask import g, current_app, request
|
||||
from flask import abort, url_for, redirect
|
||||
|
|
|
@ -15,6 +15,7 @@ from app.scodoc import sco_preferences
|
|||
|
||||
|
||||
def send_async_email(app, msg):
|
||||
"Send an email, async"
|
||||
with app.app_context():
|
||||
mail.send(msg)
|
||||
|
||||
|
|
1
app/forms/__init__.py
Normal file
1
app/forms/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
1
app/forms/main/__init__.py
Normal file
1
app/forms/main/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -29,17 +29,13 @@
|
|||
Formulaires configuration Exports Apogée (codes)
|
||||
"""
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField
|
||||
|
||||
from app import models
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import SHORT_STR_LEN
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def _build_code_field(code):
|
||||
|
@ -61,6 +57,7 @@ def _build_code_field(code):
|
|||
|
||||
|
||||
class CodesDecisionsForm(FlaskForm):
|
||||
"Formulaire code décisions Apogée"
|
||||
ADC = _build_code_field("ADC")
|
||||
ADJ = _build_code_field("ADJ")
|
||||
ADM = _build_code_field("ADM")
|
||||
|
|
|
@ -47,8 +47,6 @@ from app.scodoc.sco_config_actions import (
|
|||
LogoInsert,
|
||||
)
|
||||
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
Formulaires création département
|
||||
"""
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField, BooleanField
|
||||
|
|
|
@ -131,7 +131,10 @@ class Identite(db.Model):
|
|||
@cached_property
|
||||
def sort_key(self) -> tuple:
|
||||
"clé pour tris par ordre alphabétique"
|
||||
return (self.nom_usuel or self.nom).lower(), self.prenom.lower()
|
||||
return (
|
||||
scu.suppress_accents(self.nom_usuel or self.nom or "").lower(),
|
||||
scu.suppress_accents(self.prenom or "").lower(),
|
||||
)
|
||||
|
||||
def get_first_email(self, field="email") -> str:
|
||||
"Le mail associé à la première adrese de l'étudiant, ou None"
|
||||
|
|
1
app/pe/__init__.py
Normal file
1
app/pe/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# empty but required for pylint
|
|
@ -51,27 +51,34 @@ from app.pe import pe_avislatex
|
|||
def _pe_view_sem_recap_form(formsemestre_id):
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Avis de poursuite d'études"),
|
||||
"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
||||
f"""<h2 class="formsemestre">Génération des avis de poursuites d'études</h2>
|
||||
<p class="help">
|
||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de poursuites d'études.
|
||||
Cette fonction génère un ensemble de fichiers permettant d'éditer des avis de
|
||||
poursuites d'études.
|
||||
<br/>
|
||||
De nombreux aspects sont paramétrables:
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener noreferrer">
|
||||
De nombreux aspects sont paramétrables:
|
||||
<a href="https://scodoc.org/AvisPoursuiteEtudes" target="_blank" rel="noopener">
|
||||
voir la documentation</a>.
|
||||
</p>
|
||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form" enctype="multipart/form-data">
|
||||
<form method="post" action="pe_view_sem_recap" id="pe_view_sem_recap_form"
|
||||
enctype="multipart/form-data">
|
||||
<div class="pe_template_up">
|
||||
Les templates sont généralement installés sur le serveur ou dans le paramétrage de ScoDoc.<br/>
|
||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template (<tt>un_avis.tex</tt>):
|
||||
<div class="pe_template_upb">Template: <input type="file" size="30" name="avis_tmpl_file"/></div>
|
||||
<div class="pe_template_upb">Pied de page: <input type="file" size="30" name="footer_tmpl_file"/></div>
|
||||
Les templates sont généralement installés sur le serveur ou dans le
|
||||
paramétrage de ScoDoc.
|
||||
<br/>
|
||||
Au besoin, vous pouvez spécifier ici votre propre fichier de template
|
||||
(<tt>un_avis.tex</tt>):
|
||||
<div class="pe_template_upb">Template:
|
||||
<input type="file" size="30" name="avis_tmpl_file"/>
|
||||
</div>
|
||||
<div class="pe_template_upb">Pied de page:
|
||||
<input type="file" size="30" name="footer_tmpl_file"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" value="Générer les documents"/>
|
||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}">
|
||||
</form>
|
||||
""".format(
|
||||
formsemestre_id=formsemestre_id
|
||||
),
|
||||
""",
|
||||
]
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@ from app.scodoc.sco_formsemestre import (
|
|||
from app.scodoc.sco_codes_parcours import (
|
||||
DEF,
|
||||
UE_SPORT,
|
||||
UE_is_fondamentale,
|
||||
UE_is_professionnelle,
|
||||
ue_is_fondamentale,
|
||||
ue_is_professionnelle,
|
||||
)
|
||||
from app.scodoc.sco_parcours_dut import formsemestre_get_etud_capitalisation
|
||||
from app.scodoc import sco_codes_parcours
|
||||
|
@ -826,11 +826,11 @@ class NotesTable:
|
|||
and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
|
||||
):
|
||||
mu["ects_pot"] = ue["ects"] or 0.0
|
||||
if UE_is_fondamentale(ue["type"]):
|
||||
if ue_is_fondamentale(ue["type"]):
|
||||
mu["ects_pot_fond"] = mu["ects_pot"]
|
||||
else:
|
||||
mu["ects_pot_fond"] = 0.0
|
||||
if UE_is_professionnelle(ue["type"]):
|
||||
if ue_is_professionnelle(ue["type"]):
|
||||
mu["ects_pot_pro"] = mu["ects_pot"]
|
||||
else:
|
||||
mu["ects_pot_pro"] = 0.0
|
||||
|
|
|
@ -81,11 +81,11 @@ UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...)
|
|||
UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...)
|
||||
|
||||
|
||||
def UE_is_fondamentale(ue_type):
|
||||
def ue_is_fondamentale(ue_type):
|
||||
return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE)
|
||||
|
||||
|
||||
def UE_is_professionnelle(ue_type):
|
||||
def ue_is_professionnelle(ue_type):
|
||||
return (
|
||||
ue_type == UE_PROFESSIONNELLE
|
||||
) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro
|
||||
|
@ -211,7 +211,7 @@ DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1}
|
|||
|
||||
NO_SEMESTRE_ID = -1 # code semestre si pas de semestres
|
||||
|
||||
# Regles gestion parcours
|
||||
# Règles gestion parcours
|
||||
class DUTRule(object):
|
||||
def __init__(self, rule_id, premise, conclusion):
|
||||
self.rule_id = rule_id
|
||||
|
@ -222,13 +222,13 @@ class DUTRule(object):
|
|||
def match(self, state):
|
||||
"True if state match rule premise"
|
||||
assert len(state) == len(self.premise)
|
||||
for i in range(len(state)):
|
||||
for i, stat in enumerate(state):
|
||||
prem = self.premise[i]
|
||||
if isinstance(prem, (list, tuple)):
|
||||
if not state[i] in prem:
|
||||
if not stat in prem:
|
||||
return False
|
||||
else:
|
||||
if prem != ALL and prem != state[i]:
|
||||
if prem not in (ALL, stat):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
@ -244,6 +244,7 @@ class TypeParcours(object):
|
|||
COMPENSATION_UE = True # inutilisé
|
||||
BARRE_MOY = 10.0
|
||||
BARRE_UE_DEFAULT = 8.0
|
||||
BARRE_UE_DISPLAY_WARNING = 8.0
|
||||
BARRE_UE = {}
|
||||
NOTES_BARRE_VALID_UE_TH = 10.0 # seuil pour valider UE
|
||||
NOTES_BARRE_VALID_UE = NOTES_BARRE_VALID_UE_TH - NOTES_TOLERANCE # barre sur UE
|
||||
|
|
|
@ -1314,7 +1314,7 @@ def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
|
|||
|
||||
def formsemestre_delete(formsemestre_id):
|
||||
"""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]
|
||||
H = [
|
||||
html_sco_header.html_sem_header("Suppression du semestre"),
|
||||
|
|
|
@ -41,6 +41,7 @@ from app.comp.moy_mod import ModuleImplResults
|
|||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -53,7 +54,6 @@ from app.scodoc import sco_formsemestre
|
|||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_users
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
|
@ -320,7 +320,9 @@ def _make_table_notes(
|
|||
for etudid, etat in etudid_etats:
|
||||
css_row_class = None
|
||||
# infos identite etudiant
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
etud = Identite.query.get(etudid)
|
||||
if etud is None:
|
||||
continue
|
||||
|
||||
if etat == "I": # si inscrit, indique groupe
|
||||
groups = sco_groups.get_etud_groups(etudid, modimpl_o["formsemestre_id"])
|
||||
|
@ -332,7 +334,7 @@ def _make_table_notes(
|
|||
else:
|
||||
grc = etat
|
||||
|
||||
code = etud.get(anonymous_lst_key)
|
||||
code = getattr(etud, anonymous_lst_key)
|
||||
if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid
|
||||
code = etudid
|
||||
|
||||
|
@ -341,20 +343,20 @@ def _make_table_notes(
|
|||
"code": str(code), # INE, NIP ou etudid
|
||||
"_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
|
||||
"etudid": etudid,
|
||||
"nom": etud["nom"].upper(),
|
||||
"nom": etud.nom.upper(),
|
||||
"_nomprenom_target": url_for(
|
||||
"notes.formsemestre_bulletinetud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=modimpl_o["formsemestre_id"],
|
||||
etudid=etudid,
|
||||
),
|
||||
"_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{etud.get('nom', '').upper()}" """,
|
||||
"prenom": etud["prenom"].lower().capitalize(),
|
||||
"nomprenom": etud["nomprenom"],
|
||||
"_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{etud.sort_key}" """,
|
||||
"prenom": etud.prenom.lower().capitalize(),
|
||||
"nomprenom": etud.nomprenom,
|
||||
"group": grc,
|
||||
"_group_td_attrs": 'class="group"',
|
||||
"email": etud["email"],
|
||||
"emailperso": etud["emailperso"],
|
||||
"email": etud.get_first_email(),
|
||||
"emailperso": etud.get_first_email("emailperso"),
|
||||
"_css_row_class": css_row_class or "",
|
||||
}
|
||||
)
|
||||
|
|
|
@ -130,12 +130,10 @@ def formsemestre_recapcomplet(
|
|||
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
||||
)
|
||||
for (format, label) in (
|
||||
("html", "HTML"),
|
||||
("xls", "Fichier tableur (Excel)"),
|
||||
("xlsall", "Fichier tableur avec toutes les évals"),
|
||||
("csv", "Fichier tableur (CSV)"),
|
||||
("xml", "Fichier XML"),
|
||||
("json", "JSON"),
|
||||
("html", "Tableau"),
|
||||
("evals", "Avec toutes les évaluations"),
|
||||
("xml", "Bulletins XML (obsolète)"),
|
||||
("json", "Bulletins JSON"),
|
||||
):
|
||||
if format == tabformat:
|
||||
selected = " selected"
|
||||
|
@ -149,7 +147,6 @@ def formsemestre_recapcomplet(
|
|||
href="{url_for('notes.formsemestre_bulletins_pdf',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)}">
|
||||
ici avoir le classeur papier</a>)
|
||||
<div class="warning">Nouvelle version: export excel inachevés. Merci de signaler les problèmes.</div>
|
||||
"""
|
||||
)
|
||||
|
||||
|
@ -221,9 +218,11 @@ def do_formsemestre_recapcomplet(
|
|||
):
|
||||
"""Calcule et renvoie le tableau récapitulatif."""
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if format == "html" and not modejury:
|
||||
if (format == "html" or format == "evals") and not modejury:
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
data, filename = gen_formsemestre_recapcomplet_html(formsemestre, res)
|
||||
data, filename = gen_formsemestre_recapcomplet_html(
|
||||
formsemestre, res, include_evaluations=(format == "evals")
|
||||
)
|
||||
else:
|
||||
data, filename, format = make_formsemestre_recapcomplet(
|
||||
formsemestre_id=formsemestre_id,
|
||||
|
@ -239,7 +238,7 @@ def do_formsemestre_recapcomplet(
|
|||
force_publishing=force_publishing,
|
||||
)
|
||||
# ---
|
||||
if format == "xml" or format == "html":
|
||||
if format == "xml" or format == "html" or format == "evals":
|
||||
return data
|
||||
elif format == "csv":
|
||||
return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE)
|
||||
|
@ -251,12 +250,12 @@ def do_formsemestre_recapcomplet(
|
|||
js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE
|
||||
)
|
||||
else:
|
||||
raise ValueError("unknown format %s" % format)
|
||||
raise ValueError(f"unknown format {format}")
|
||||
|
||||
|
||||
def make_formsemestre_recapcomplet(
|
||||
formsemestre_id=None,
|
||||
format="html", # html, xml, xls, xlsall, json
|
||||
format="html", # html, evals, xml, json
|
||||
hidemodules=False, # ne pas montrer les modules (ignoré en XML)
|
||||
hidebac=False, # pas de colonne Bac (ignoré en XML)
|
||||
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
|
||||
|
@ -643,7 +642,7 @@ def make_formsemestre_recapcomplet(
|
|||
"recap_row_nbeval",
|
||||
"recap_row_ects",
|
||||
)[ir - nblines + 6]
|
||||
cells = '<tr class="%s sortbottom">' % styl
|
||||
cells = f'<tr class="{styl} sortbottom">'
|
||||
else:
|
||||
el = etudlink % {
|
||||
"formsemestre_id": formsemestre_id,
|
||||
|
@ -651,14 +650,14 @@ def make_formsemestre_recapcomplet(
|
|||
"name": l[1],
|
||||
}
|
||||
if ir % 2 == 0:
|
||||
cells = '<tr class="recap_row_even" id="etudid%s">' % etudid
|
||||
cells = f'<tr class="recap_row_even" id="etudid{etudid}">'
|
||||
else:
|
||||
cells = '<tr class="recap_row_odd" id="etudid%s">' % etudid
|
||||
cells = f'<tr class="recap_row_odd" id="etudid{etudid}">'
|
||||
ir += 1
|
||||
# XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ]
|
||||
# notes sans le NA:
|
||||
nsn = l[:-2] # copy
|
||||
for i in range(len(nsn)):
|
||||
for i, _ in enumerate(nsn):
|
||||
if nsn[i] == "NA":
|
||||
nsn[i] = "-"
|
||||
try:
|
||||
|
@ -1029,19 +1028,26 @@ def _gen_row(keys: list[str], row, elt="td"):
|
|||
|
||||
|
||||
def gen_formsemestre_recapcomplet_html(
|
||||
formsemestre: FormSemestre, res: NotesTableCompat
|
||||
formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False
|
||||
):
|
||||
"""Construit table recap pour le BUT
|
||||
Return: data, filename
|
||||
"""
|
||||
rows, footer_rows, titles, column_ids = res.get_table_recap(convert_values=True)
|
||||
rows, footer_rows, titles, column_ids = res.get_table_recap(
|
||||
convert_values=True, include_evaluations=include_evaluations
|
||||
)
|
||||
if not rows:
|
||||
return (
|
||||
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>',
|
||||
"",
|
||||
)
|
||||
filename = scu.sanitize_filename(
|
||||
f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
|
||||
)
|
||||
H = [
|
||||
f"""<div class="table_recap"><table class="table_recap {'apc' if formsemestre.formation.is_apc() else ''}">"""
|
||||
f"""<div class="table_recap"><table class="table_recap {
|
||||
'apc' if formsemestre.formation.is_apc() else 'classic'}"
|
||||
data-filename="{filename}">"""
|
||||
]
|
||||
# header
|
||||
H.append(
|
||||
|
@ -1068,7 +1074,4 @@ def gen_formsemestre_recapcomplet_html(
|
|||
</div>
|
||||
"""
|
||||
)
|
||||
return (
|
||||
"".join(H),
|
||||
f'recap-{formsemestre.titre_num().replace(" ", "_")}-{time.strftime("%d-%m-%Y")}',
|
||||
) # suffix ?
|
||||
return ("".join(H), filename) # suffix ?
|
||||
|
|
|
@ -931,6 +931,10 @@ def icontag(name, file_format="png", no_size=False, **attrs):
|
|||
ICON_PDF = icontag("pdficon16x20_img", title="Version PDF")
|
||||
ICON_XLS = icontag("xlsicon_img", title="Version tableur")
|
||||
|
||||
# HTML emojis
|
||||
EMO_WARNING = "⚠️" # warning /!\
|
||||
EMO_RED_TRIANGLE_DOWN = "🔻" # red triangle pointed down
|
||||
|
||||
|
||||
def sort_dates(L, reverse=False):
|
||||
"""Return sorted list of dates, allowing None items (they are put at the beginning)"""
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -29,15 +29,19 @@ $(function () {
|
|||
action: function (e, dt, node, config) {
|
||||
let visible = dt.columns(".col_res").visible()[0];
|
||||
dt.columns(".col_res").visible(!visible);
|
||||
dt.columns(".col_ue_bonus").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").visible()[0];
|
||||
dt.columns(".col_mod").visible(!visible);
|
||||
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.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");
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@ -62,6 +66,15 @@ $(function () {
|
|||
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(
|
||||
{
|
||||
paging: false,
|
||||
|
@ -77,13 +90,37 @@ $(function () {
|
|||
colReorder: true,
|
||||
"columnDefs": [
|
||||
{
|
||||
// cache le détail de l'identité et les colonnes admission
|
||||
"targets": ["identite_detail", "partition_aux", "admission"],
|
||||
"visible": false,
|
||||
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
|
||||
targets: ["codes", "identite_detail", "partition_aux", "admission", "col_empty"],
|
||||
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',
|
||||
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',
|
||||
text: 'Colonnes affichées',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.1.88"
|
||||
SCOVERSION = "9.1.90"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
|
1
tests/bench/__init__.py
Normal file
1
tests/bench/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Simple benchmarks
|
|
@ -5,6 +5,7 @@ import time
|
|||
|
||||
from flask import g
|
||||
from flask_login import login_user
|
||||
from app.models import FormSemestre
|
||||
|
||||
from config import RunningConfig as BenchConfig
|
||||
import app
|
||||
|
@ -12,10 +13,13 @@ from app import db, create_app
|
|||
from app import clear_scodoc_cache
|
||||
from app.auth.models import get_super_admin
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import notes_table
|
||||
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp import res_sem
|
||||
|
||||
|
||||
def setup_generator(dept: str):
|
||||
"setup app"
|
||||
# Setup
|
||||
apptest = create_app(BenchConfig)
|
||||
# Run tests:
|
||||
|
@ -39,12 +43,14 @@ def setup_generator(dept: str):
|
|||
|
||||
|
||||
def bench_notes_table(dept: str, formsemestre_ids: list[int]) -> float:
|
||||
"benchmark note stable"
|
||||
for client in setup_generator(dept):
|
||||
tot_time = 0.0
|
||||
for formsemestre_id in formsemestre_ids:
|
||||
print(f"building sem {formsemestre_id}...")
|
||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||
t0 = time.time()
|
||||
nt = notes_table.NotesTable(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
tot_time += time.time() - t0
|
||||
print(f"Total time: {tot_time}")
|
||||
return tot_time
|
||||
|
|
0
tools/build_release.sh
Normal file → Executable file
0
tools/build_release.sh
Normal file → Executable file
|
@ -4,4 +4,4 @@ Architecture: amd64
|
|||
Maintainer: Emmanuel Viennet <emmanuel@viennet.net>
|
||||
Description: ScoDoc 9
|
||||
Un logiciel pour le suivi de la scolarité universitaire.
|
||||
Depends: adduser, curl, gcc, graphviz, libpq-dev, postfix|exim4, cracklib-runtime, libcrack2-dev, python3-dev, python3-venv, python3-pip, python3-wheel, nginx, postgresql, redis, ufw
|
||||
Depends: adduser, curl, gcc, graphviz, libpq-dev, postfix|exim4, cracklib-runtime, libcrack2-dev, python3-dev, python3-venv, python3-pip, python3-wheel, nginx, postgresql, libpq-dev, redis, ufw
|
||||
|
|
Loading…
Reference in New Issue
Block a user