Compare commits

...

13 Commits

Author SHA1 Message Date
Emmanuel Viennet 635269ff36 Modifie FormSemestre.etudids_actifs: retire @cached_property. Tests OK. 2024-03-22 11:49:51 +01:00
Emmanuel Viennet 4aa30a40bd Fix: front&back saisie note sur DEM 2024-03-21 16:42:28 +01:00
Emmanuel Viennet 03c03f3725 Fix form recherche par étape 2024-03-21 15:54:56 +01:00
Emmanuel Viennet 29eb8c297b Améliore page accueil dept.: formation, cosmétique, export excel 2024-03-21 13:21:25 +01:00
Emmanuel Viennet 38032a8c09 Ré-écriture de la page d'accueil de département. Template. 2024-03-21 12:06:34 +01:00
Emmanuel Viennet 2f2d98954c Maquette: introduit scobox, reprend certaines pages. WIP 2024-03-20 18:13:19 +01:00
Emmanuel Viennet 2e5d94f048 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into iziram-main96 2024-03-19 20:59:28 +01:00
Emmanuel Viennet 1b1b8ebdc4 Merge branch 'main96' of https://scodoc.org/git/iziram/ScoDoc into iziram-main96 2024-03-19 20:59:13 +01:00
Emmanuel Viennet 9c6db169f3 Restreint accès aux bulletins PDF si formsemestre.bul_hide_xml (sémantique changée) + WIP tests unitaires API 2024-03-19 18:22:02 +01:00
Emmanuel Viennet 763f60fb3d Fix: /etud_info_html si pas de données admission 2024-03-19 09:34:03 +01:00
Emmanuel Viennet dece9a82d1 Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into iziram-main96 2024-03-18 14:24:27 +01:00
Emmanuel Viennet 0262b6e2ac Affichage nb évaluations en attente su rtableau de bord. Ne consière plus les évaluations bloquées comme en attente. 2024-03-18 14:20:34 +01:00
Emmanuel Viennet f8f47e05ff Fix: formsemestre_table_estim_cost: error handling 2024-03-17 13:28:49 +01:00
38 changed files with 803 additions and 501 deletions

View File

@ -414,9 +414,16 @@ def bulletin(
if version == "pdf":
version = "long"
pdf = True
if version not in scu.BULLETINS_VERSIONS_BUT:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if version not in (
scu.BULLETINS_VERSIONS_BUT
if formsemestre.formation.is_apc()
else scu.BULLETINS_VERSIONS
):
return json_error(404, "version invalide")
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
if formsemestre.bul_hide_xml and pdf:
return json_error(403, "bulletin non disponible")
# note: la version json est réduite si bul_hide_xml
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
return json_error(404, "formsemestre inexistant")

View File

@ -52,7 +52,8 @@ def formations():
@as_json
def formations_ids():
"""
Retourne la liste de toutes les id de formations (tous départements)
Retourne la liste de toutes les id de formations
(tous départements, ou du département indiqué dans la route)
Exemple de résultat : [ 17, 99, 32 ]
"""

View File

@ -12,7 +12,7 @@ from operator import attrgetter, itemgetter
from flask import g, make_response, request
from flask_json import as_json
from flask_login import current_user, login_required
import sqlalchemy as sa
import app
from app import db
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
@ -171,6 +171,44 @@ def formsemestres_query():
]
@bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
@scodoc
@permission_required(Permission.EditFormSemestre)
@as_json
def formsemestre_edit(formsemestre_id: int):
"""Modifie les champs d'un formsemestre."""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
args = request.get_json(force=True) # may raise 400 Bad Request
editable_keys = {
"semestre_id",
"titre",
"date_debut",
"date_fin",
"edt_id",
"etat",
"modalite",
"gestion_compensation",
"bul_hide_xml",
"block_moyennes",
"block_moyenne_generale",
"mode_calcul_moyennes",
"gestion_semestrielle",
"bul_bgcolor",
"resp_can_edit",
"resp_can_change_ens",
"ens_can_edit_eval",
"elt_sem_apo",
"elt_annee_apo",
}
formsemestre.from_dict({k: v for (k, v) in args.items() if k in editable_keys})
try:
db.session.commit()
except sa.exc.StatementError as exc:
return json_error(404, f"invalid argument(s): {exc.args[0]}")
return formsemestre.to_dict_api()
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@ -468,13 +506,13 @@ def etat_evals(formsemestre_id: int):
date_mediane = notes_sorted[len(notes_sorted) // 2].date
eval_dict["saisie_notes"] = {
"datetime_debut": date_debut.isoformat()
if date_debut is not None
else None,
"datetime_debut": (
date_debut.isoformat() if date_debut is not None else None
),
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
"datetime_mediane": date_mediane.isoformat()
if date_mediane is not None
else None,
"datetime_mediane": (
date_mediane.isoformat() if date_mediane is not None else None
),
}
list_eval.append(eval_dict)

View File

@ -23,9 +23,12 @@ from app.models.but_refcomp import (
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
def orebut_import_refcomp(
xml_data: str, dept_id: int, orig_filename=None
) -> ApcReferentielCompetences:
"""Importation XML Orébut
peut lever TypeError ou ScoFormatError
L'objet créé est ajouté et commité.
Résultat: instance de ApcReferentielCompetences
"""
# Vérifie que le même fichier n'a pas déjà été chargé:
@ -41,7 +44,7 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
try:
root = ElementTree.XML(xml_data)
except ElementTree.ParseError as exc:
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}")
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}") from exc
if root.tag != "referentiel_competence":
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
@ -60,7 +63,8 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
db.session.rollback()
raise ScoValueError(
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({
competence.attrib["id"]})
"""
) from exc
ref.competences.append(c)

View File

@ -72,7 +72,15 @@ class ModuleImplResults:
les caches sont gérés par ResultatsSemestre.
"""
def __init__(self, moduleimpl: ModuleImpl):
def __init__(
self, moduleimpl: ModuleImpl, etudids: list[int], etudids_actifs: set[int]
):
"""
Args:
- etudids : liste des etudids, qui donne l'index du dataframe
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
"""
self.moduleimpl_id = moduleimpl.id
self.module_id = moduleimpl.module.id
self.etudids = None
@ -105,14 +113,21 @@ class ModuleImplResults:
"""
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(etudids, etudids_actifs)
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"""
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
def load_notes(self): # ré-écriture de df_load_modimpl_notes
def load_notes(
self, etudids: list[int], etudids_actifs: set[int]
): # ré-écriture de df_load_modimpl_notes
"""Charge toutes les notes de toutes les évaluations du module.
Args:
- etudids : liste des etudids, qui donne l'index du dataframe
(doit être tous les étudiants inscrits au semestre incluant les DEM et DEF)
- etudids_actifs l'ensemble des étudiants inscrits au semestre, non DEM/DEF.
Dataframe evals_notes
colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int)
@ -135,12 +150,12 @@ class ModuleImplResults:
qui ont des notes ATT.
"""
moduleimpl = db.session.get(ModuleImpl, self.moduleimpl_id)
self.etudids = self._etudids()
self.etudids = etudids
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
moduleimpl.formsemestre.etudids_actifs
etudids_actifs
)
self.nb_inscrits_module = len(inscrits_module)
@ -148,6 +163,7 @@ class ModuleImplResults:
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = []
self.evaluations_completes_dict = {}
self.etudids_attente = set() # empty
for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi
@ -155,13 +171,13 @@ class ModuleImplResults:
# ou évaluation déclarée "à prise en compte immédiate"
# ou rattrapage, 2eme session, bonus
# ET pas bloquée par date (is_blocked)
is_blocked = evaluation.is_blocked()
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = (
(evaluation.evaluation_type != Evaluation.EVALUATION_NORMALE)
or (evaluation.publish_incomplete)
or (not etudids_sans_note)
) and not evaluation.is_blocked()
) and not is_blocked
self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete
self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
@ -178,16 +194,21 @@ class ModuleImplResults:
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
nb_notes = eval_notes_inscr.notna().sum()
# Etudiants avec notes en attente:
# = ceux avec note ATT
eval_etudids_attente = set(
eval_notes_inscr.iloc[
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
].index
)
if evaluation.publish_incomplete:
# et en "immédiat", tous ceux sans note
eval_etudids_attente |= etudids_sans_note
if is_blocked:
eval_etudids_attente = set()
else:
# Etudiants avec notes en attente:
# = ceux avec note ATT
eval_etudids_attente = set(
eval_notes_inscr.iloc[
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
].index
)
if evaluation.publish_incomplete:
# et en "immédiat", tous ceux sans note
eval_etudids_attente |= etudids_sans_note
# Synthèse pour état du module:
self.etudids_attente |= eval_etudids_attente
self.evaluations_etat[evaluation.id] = EvaluationEtat(
@ -229,17 +250,6 @@ class ModuleImplResults:
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
return eval_df
def _etudids(self):
"""L'index du dataframe est la liste de tous les étudiants inscrits au semestre
(incluant les DEM et DEF)
"""
return [
inscr.etudid
for inscr in db.session.get(
ModuleImpl, self.moduleimpl_id
).formsemestre.inscriptions
]
def get_evaluations_coefs(self, modimpl: ModuleImpl) -> np.array:
"""Coefficients des évaluations.
Les coefs des évals incomplètes, rattrapage, session 2, bonus sont forcés à zéro.

View File

@ -99,9 +99,11 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
default_poids = {
mod.id: 1.0
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
else 0.0
mod.id: (
1.0
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
else 0.0
)
for mod in modules
}
@ -148,10 +150,12 @@ def df_load_modimpl_coefs(
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
default_poids = {
modimpl.id: 1.0
if (modimpl.module.module_type == ModuleType.STANDARD)
and (modimpl.module.ue.type == UE_SPORT)
else 0.0
modimpl.id: (
1.0
if (modimpl.module.module_type == ModuleType.STANDARD)
and (modimpl.module.ue.type == UE_SPORT)
else 0.0
)
for modimpl in formsemestre.modimpls_sorted
}
@ -200,8 +204,9 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
modimpls_results = {}
modimpls_evals_poids = {}
modimpls_notes = []
etudids, etudids_actifs = formsemestre.etudids_actifs()
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
modimpls_results[modimpl.id] = mod_results

View File

@ -256,8 +256,9 @@ def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]
"""
modimpls_results = {}
modimpls_notes = []
etudids, etudids_actifs = formsemestre.etudids_actifs()
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsClassic(modimpl)
mod_results = moy_mod.ModuleImplResultsClassic(modimpl, etudids, etudids_actifs)
etuds_moy_module = mod_results.compute_module_moy()
modimpls_results[modimpl.id] = mod_results
modimpls_notes.append(etuds_moy_module)

View File

@ -209,6 +209,7 @@ class ResultatsSemestre(ResultatsCache):
"evalcomplete" : bool,
"last_modif" : datetime.datetime | None, # saisie de note la plus récente
"nb_notes" : int, # nb notes d'étudiants inscrits
"nb_attente" : int, # nb de notes en ATTente (même si bloquée)
},
"evaluation_id" : int,
"jour" : datetime.datetime, # e.date_debut or datetime.datetime(1900, 1, 1)
@ -236,6 +237,7 @@ class ResultatsSemestre(ResultatsCache):
"etat": {
"blocked": evaluation.is_blocked(),
"evalcomplete": etat.is_complete,
"nb_attente": etat.nb_attente,
"nb_notes": etat.nb_notes,
"last_modif": last_modif,
},

View File

@ -249,11 +249,12 @@ class ScolarNews(db.Model):
news_list = cls.last_news(n=n)
if not news_list:
return ""
dept_news_url = url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
H = [
f"""<div class="news"><span class="newstitle"><a href="{
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
f"""<div class="scobox news"><div class="scobox-title"><a href="{
dept_news_url
}">Dernières opérations</a>
</span><ul class="newslist">"""
</div><ul class="newslist">"""
]
for news in news_list:
@ -261,16 +262,22 @@ class ScolarNews(db.Model):
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
class="newstext">{news}</span></li>"""
)
H.append(
f"""<li class="newslist">
<span class="newstext"><a href="{dept_news_url}" class="stdlink">...</a>
</span>
</li>"""
)
H.append("</ul>")
H.append("</ul></div>")
# Informations générales
H.append(
f"""<div><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}">
Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>.
f"""<div>
Pour en savoir plus sur ScoDoc voir
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">scodoc.org</a>
</div>
"""
)
H.append("</div>")
return "\n".join(H)

View File

@ -25,6 +25,7 @@ from sqlalchemy import func
import app.scodoc.sco_utils as scu
from app import db, log
from app.auth.models import User
from app import models
from app.models import APO_CODE_STR_LEN, CODE_STR_LEN, SHORT_STR_LEN
from app.models.but_refcomp import (
ApcParcours,
@ -54,7 +55,7 @@ from app.scodoc.sco_vdi import ApoEtapeVDI
GROUPS_AUTO_ASSIGNMENT_DATA_MAX = 1024 * 1024 # bytes
class FormSemestre(db.Model):
class FormSemestre(models.ScoDocModel):
"""Mise en oeuvre d'un semestre de formation"""
__tablename__ = "notes_formsemestre"
@ -84,7 +85,7 @@ class FormSemestre(db.Model):
bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
"ne publie pas le bulletin XML ou JSON"
"ne publie pas le bulletin sur l'API"
block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
@ -191,7 +192,8 @@ class FormSemestre(db.Model):
def get_formsemestre(
cls, formsemestre_id: int | str, dept_id: int = None
) -> "FormSemestre":
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
"""FormSemestre ou 404, cherche uniquement dans le département spécifié
ou le courant (g.scodoc_dept)"""
if not isinstance(formsemestre_id, int):
try:
formsemestre_id = int(formsemestre_id)
@ -245,12 +247,13 @@ class FormSemestre(db.Model):
def to_dict_api(self):
"""
Un dict avec les informations sur le semestre destiné à l'api
Un dict avec les informations sur le semestre destinées à l'api
"""
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
d.pop("groups_auto_assignment_data", None)
d["annee_scolaire"] = self.annee_scolaire()
d["bul_hide_xml"] = self.bul_hide_xml
if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
d["date_debut_iso"] = self.date_debut.isoformat()
@ -873,7 +876,7 @@ class FormSemestre(db.Model):
descr_sem += " " + self.modalite
return descr_sem
def get_abs_count(self, etudid):
def get_abs_count(self, etudid) -> tuple[int, int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non just, nb abs justifiées, nb abs total)
Utilise un cache.
@ -933,10 +936,14 @@ class FormSemestre(db.Model):
partitions += [p for p in self.partitions if p.partition_name is None]
return partitions
@cached_property
def etudids_actifs(self) -> set:
"Set des etudids inscrits non démissionnaires et non défaillants"
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
def etudids_actifs(self) -> tuple[list[int], set[int]]:
"""Liste les etudids inscrits (incluant DEM et DEF),
qui ser al'index des dataframes de notes
et donne l'ensemble des inscrits non DEM ni DEF.
"""
return [inscr.etudid for inscr in self.inscriptions], {
ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT
}
@cached_property
def etuds_inscriptions(self) -> dict:

View File

@ -303,13 +303,16 @@ def sco_header(
# div pour affichage messages temporaires
H.append('<div id="sco_msg" class="head_message"></div>')
#
H.append('<div class="sco-app-content">')
return "".join(H)
def sco_footer():
"""Main HTMl pages footer"""
return (
"""</div><!-- /gtrcontent -->""" + scu.CUSTOM_HTML_FOOTER + """</body></html>"""
"""</div></div><!-- /gtrcontent -->"""
+ scu.CUSTOM_HTML_FOOTER
+ """</body></html>"""
)

View File

@ -114,10 +114,8 @@ def formsemestre_bulletinetud_published_dict(
if etudid not in nt.identdict:
abort(404, "etudiant non inscrit dans ce semestre")
d = {"type": "classic", "version": "0"}
if (not sem["bul_hide_xml"]) or force_publishing:
published = True
else:
published = False
published = (not formsemestre.bul_hide_xml) or force_publishing
if xml_nodate:
docdate = ""
else:

View File

@ -30,17 +30,18 @@
(coût théorique en heures équivalent TD)
"""
from flask import request
from flask import request, Response
from app.models import FormSemestre
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
import sco_version
def formsemestre_table_estim_cost(
formsemestre_id,
formsemestre: FormSemestre,
n_group_td=1,
n_group_tp=1,
coef_tp=1,
@ -55,8 +56,6 @@ def formsemestre_table_estim_cost(
peut conduire à une sur-estimation du coût s'il y a des modules optionnels
(dans ce cas, retoucher le tableau excel exporté).
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
rows = []
for modimpl in formsemestre.modimpls:
rows.append(
@ -76,14 +75,14 @@ def formsemestre_table_estim_cost(
+ coef_cours * row["heures_cours"]
+ coef_tp * row["heures_tp"]
)
sum_cours = sum([t["heures_cours"] for t in rows])
sum_td = sum([t["heures_td"] for t in rows])
sum_tp = sum([t["heures_tp"] for t in rows])
sum_cours = sum(t["heures_cours"] for t in rows)
sum_td = sum(t["heures_td"] for t in rows)
sum_tp = sum(t["heures_tp"] for t in rows)
sum_heqtd = sum_td + coef_cours * sum_cours + coef_tp * sum_tp
assert abs(sum([t["HeqTD"] for t in rows]) - sum_heqtd) < 0.01, "%s != %s" % (
sum([t["HeqTD"] for t in rows]),
sum_heqtd,
)
# assert abs(sum(t["HeqTD"] for t in rows) - sum_heqtd) < 0.01, "%s != %s" % (
# sum(t["HeqTD"] for t in rows),
# sum_heqtd,
# )
rows.append(
{
@ -117,7 +116,7 @@ def formsemestre_table_estim_cost(
),
rows=rows,
html_sortable=True,
preferences=sco_preferences.SemPreferences(formsemestre_id),
preferences=sco_preferences.SemPreferences(formsemestre.id),
html_class="table_leftalign table_listegroupe",
xls_before_table=[
[formsemestre.titre_annee()],
@ -146,47 +145,45 @@ def formsemestre_table_estim_cost(
return tab
# view
def formsemestre_estim_cost(
formsemestre_id,
n_group_td=1,
n_group_tp=1,
coef_tp=1,
coef_cours=1.5,
formsemestre_id: int,
n_group_td: int | str = 1,
n_group_tp: int | str = 1,
coef_tp: float | str = 1.0,
coef_cours: float | str = 1.5,
fmt="html",
):
) -> str | Response:
"""Page (formulaire) estimation coûts"""
try:
n_group_td = int(n_group_td)
n_group_tp = int(n_group_tp)
coef_tp = float(coef_tp)
coef_cours = float(coef_cours)
except ValueError as exc:
raise ScoValueError("paramètre invalide: utiliser des nombres") from exc
n_group_td = int(n_group_td)
n_group_tp = int(n_group_tp)
coef_tp = float(coef_tp)
coef_cours = float(coef_cours)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
tab = formsemestre_table_estim_cost(
formsemestre_id,
formsemestre,
n_group_td=n_group_td,
n_group_tp=n_group_tp,
coef_tp=coef_tp,
coef_cours=coef_cours,
)
h = """
<form name="f" method="get" action="%s">
<input type="hidden" name="formsemestre_id" value="%s"></input>
Nombre de groupes de TD: <input type="text" name="n_group_td" value="%s" onchange="document.f.submit()"/><br>
Nombre de groupes de TP: <input type="text" name="n_group_tp" value="%s" onchange="document.f.submit()"/>
&nbsp;Coefficient heures TP: <input type="text" name="coef_tp" value="%s" onchange="document.f.submit()"/>
tab.html_before_table = f"""
<form name="f" method="get" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}"></input>
Nombre de groupes de TD: <input type="text" name="n_group_td" value="{n_group_td}" onchange="document.f.submit()"/><br>
Nombre de groupes de TP: <input type="text" name="n_group_tp" value="{n_group_tp}" onchange="document.f.submit()"/>
&nbsp;Coefficient heures TP: <input type="text" name="coef_tp" value="{coef_tp}" onchange="document.f.submit()"/>
<br>
</form>
""" % (
request.base_url,
formsemestre_id,
n_group_td,
n_group_tp,
coef_tp,
)
tab.html_before_table = h
"""
tab.base_url = "%s?formsemestre_id=%s&n_group_td=%s&n_group_tp=%s&coef_tp=%s" % (
request.base_url,
formsemestre_id,
formsemestre.id,
n_group_td,
n_group_tp,
coef_tp,

View File

@ -3,7 +3,7 @@
##############################################################################
#
# Gestion scolarite IUT
# ScoDoc
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
@ -28,208 +28,158 @@
"""Page accueil département (liste des semestres, etc)
"""
from flask import g
from flask import url_for
from sqlalchemy import desc
from flask import g, url_for, render_template
from flask_login import current_user
from flask_sqlalchemy.query import Query
import app
from app import log
from app.models import ScolarNews
from app.models import FormSemestre, ScolarNews
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
import app.scodoc.notesdb as ndb
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_modalites
from app.scodoc import sco_preferences
from app.scodoc import sco_users
from app.views import ScoData
def index_html(showcodes=0, showsemtable=0):
def index_html(showcodes=0, showsemtable=0, export_table_formsemestres=False):
"Page accueil département (liste des semestres)"
showcodes = int(showcodes)
showsemtable = int(showsemtable)
H = []
showsemtable = int(showsemtable) or export_table_formsemestres
# News:
H.append(ScolarNews.scolar_news_summary_html())
# Avertissement de mise à jour:
H.append("""<div id="update_warning"></div>""")
# Liste de toutes les sessions:
sems = sco_formsemestre.do_formsemestre_list()
cursems = [] # semestres "courants"
othersems = [] # autres (verrouillés)
# icon image:
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
emptygroupicon = scu.icontag(
"emptygroupicon_img", title="Pas d'inscrits", border="0"
# Liste tous les formsemestres du dept, le plus récent d'abord
current_formsemestres = (
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id, etat=True)
.filter(FormSemestre.modalite != "EXT")
.order_by(desc(FormSemestre.date_debut))
)
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
# Sélection sur l'etat du semestre
for sem in sems:
if sem["etat"] and sem["modalite"] != "EXT":
sem["lockimg"] = ""
cursems.append(sem)
else:
sem["lockimg"] = lockicon
othersems.append(sem)
# Responsable de formation:
sco_formsemestre.sem_set_responsable_name(sem)
if showcodes:
sem["tmpcode"] = f"<td><tt>{sem['formsemestre_id']}</tt></td>"
else:
sem["tmpcode"] = ""
# Nombre d'inscrits:
args = {"formsemestre_id": sem["formsemestre_id"]}
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args)
nb = len(ins) # nb etudiants
sem["nb_inscrits"] = nb
if nb > 0:
sem["groupicon"] = groupicon
else:
sem["groupicon"] = emptygroupicon
# S'il n'y a pas d'utilisateurs dans la base, affiche message
if not sco_users.get_users_count(dept=g.scodoc_dept):
H.append(
"""<h2>Aucun utilisateur défini !</h2><p>Pour définir des utilisateurs
<a href="Users">passez par la page Utilisateurs</a>.
<br>
Définissez au moins un utilisateur avec le rôle AdminXXX
(le responsable du département XXX).
</p>
"""
locked_formsemestres = (
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id, etat=False)
.filter(FormSemestre.modalite != "EXT")
.order_by(desc(FormSemestre.date_debut))
)
formsemestres = (
FormSemestre.query.filter_by(dept_id=g.scodoc_dept_id)
.filter(FormSemestre.modalite != "EXT")
.order_by(desc(FormSemestre.date_debut))
)
if showsemtable: # table de tous les formsemestres
table = _sem_table_gt(
formsemestres,
showcodes=showcodes,
fmt="xlsx" if export_table_formsemestres else "html",
)
if export_table_formsemestres:
return table # cas spécial: on renvoie juste cette table
html_table_formsemestres = table.html()
else:
html_table_formsemestres = None
# Liste des formsemestres "courants"
if cursems:
H.append('<h2 class="listesems">Sessions en cours</h2>')
H.append(_sem_table(cursems))
return render_template(
"scolar/index.j2",
current_user=current_user,
dept_name=sco_preferences.get_preference("DeptName"),
formsemestres=formsemestres,
html_current_formsemestres=_show_current_formsemestres(
current_formsemestres, showcodes
),
html_table_formsemestres=html_table_formsemestres,
locked_formsemestres=locked_formsemestres,
nb_locked=locked_formsemestres.count(),
nb_user_accounts=sco_users.get_users_count(dept=g.scodoc_dept),
page_title=f"ScoDoc {g.scodoc_dept}",
Permission=Permission,
scolar_news_summary=ScolarNews.scolar_news_summary_html(),
showsemtable=showsemtable,
sco=ScoData(),
)
def _convert_formsemestres_to_dicts(
formsemestres: Query, showcodes: bool, fmt: str = "html"
) -> list[dict]:
""" """
if fmt == "html":
# icon images:
groupicon = scu.icontag("groupicon_img", title="Inscrits", border="0")
emptygroupicon = scu.icontag(
"emptygroupicon_img", title="Pas d'inscrits", border="0"
)
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
else:
groupicon = "X"
emptygroupicon = ""
lockicon = "X"
# génère liste de dict
sems = []
for formsemestre in formsemestres:
nb_inscrits = len(formsemestre.inscriptions)
formation = formsemestre.formation
sem = {
"anneescolaire": formsemestre.annee_scolaire(),
"anneescolaire_str": formsemestre.annee_scolaire_str(),
"bul_hide_xml": formsemestre.bul_hide_xml,
"dateord": formsemestre.date_debut,
"elt_annee_apo": formsemestre.elt_annee_apo,
"elt_sem_apo": formsemestre.elt_sem_apo,
"etapes_apo_str": formsemestre.etapes_apo_str(),
"formation": f"{formation.acronyme} v{formation.version}",
"_formation_target": url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=formation.id,
semestre_idx=formsemestre.semestre_id,
),
"formsemestre_id": formsemestre.id,
"groupicon": groupicon if nb_inscrits > 0 else emptygroupicon,
"lockimg": lockicon,
"modalite": formsemestre.modalite,
"mois_debut": formsemestre.mois_debut(),
"mois_fin": formsemestre.mois_fin(),
"nb_inscrits": nb_inscrits,
"responsable_name": formsemestre.responsables_str(),
"semestre_id": formsemestre.semestre_id,
"session_id": formsemestre.session_id(),
"titre_num": formsemestre.titre_num(),
"tmpcode": (f"<td><tt>{formsemestre.id}</tt></td>" if showcodes else ""),
}
sems.append(sem)
return sems
def _show_current_formsemestres(formsemestres: Query, showcodes: bool) -> str:
"""html div avec les formsemestres courants de la page d'accueil"""
H = []
if formsemestres.count():
H.append("""<div class="scobox-title">Sessions en cours</div>""")
H.append(_sem_table(_convert_formsemestres_to_dicts(formsemestres, showcodes)))
else:
# aucun semestre courant: affiche aide
H.append(
"""<h2 class="listesems">Aucune session en cours !</h2>
"""
<div class="scobox-title">Aucune session en cours !</div>
<p>Pour ajouter une session, aller dans <a href="Notes" id="link-programmes">Formations</a>,
choisissez une formation, puis suivez le lien "<em>UE, modules, semestres</em>".
</p><p>
, en bas de page, suivez le lien
</p>
<p>, en bas de page, suivez le lien
"<em>Mettre en place un nouveau semestre de formation...</em>"
</p>"""
)
if showsemtable:
H.append(
f"""<hr>
<h2>Semestres de {sco_preferences.get_preference("DeptName")}</h2>
"""
)
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>")
if not showsemtable:
H.append(
f"""<hr>
<p><a class="stdlink" href="{url_for('scolar.index_html',
scodoc_dept=g.scodoc_dept, showsemtable=1)
}">Voir table des semestres (dont {len(othersems)}
verrouillé{'s' if len(othersems) else ''})</a>
</p>"""
)
H.append(
f"""<p>
<form action="{url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)}">
Chercher étape courante:
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form>
</p>"""
)
#
H.append(
"""<hr>
<h3>Gestion des étudiants</h3>
<ul>
"""
)
if current_user.has_permission(Permission.EtudInscrit):
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.etudident_create_form", scodoc_dept=g.scodoc_dept)
}">créer <em>un</em> nouvel étudiant</a>
</li>
<li><a class="stdlink" href="{
url_for("scolar.form_students_import_excel", scodoc_dept=g.scodoc_dept)
}">importer de nouveaux étudiants</a>
(<em>ne pas utiliser</em> sauf cas particulier&nbsp;: utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)
</li>
"""
)
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.export_etudiants_courants", scodoc_dept=g.scodoc_dept)
}">exporter tableau des étudiants des semestres en cours</a>
</li>
"""
)
if current_user.has_permission(
Permission.EtudInscrit
) and sco_preferences.get_preference("portal_url"):
H.append(
f"""
<li><a class="stdlink" href="{
url_for("scolar.formsemestre_import_etud_admission",
scodoc_dept=g.scodoc_dept, tous_courants=1)
}">resynchroniser les données étudiants des semestres en cours depuis le portail</a>
</li>
"""
)
H.append("</ul>")
#
if current_user.has_permission(Permission.EditApogee):
H.append(
f"""<hr>
<h3>Exports Apogée</h3>
<ul>
<li><a class="stdlink" href="{url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
}">Années scolaires / exports Apogée</a></li>
</ul>
"""
)
#
H.append(
"""<hr>
<h3>Assistance</h3>
<ul>
<li><a class="stdlink" href="https://scodoc.org/Contact" target="_blank"
rel="noopener noreferrer">Contact (Discord)</a></li>
<li><a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a></li>
</ul>
"""
)
#
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()
)
return "\n".join(H)
def _sem_table(sems):
def _sem_table(sems: list[dict]) -> str:
"""Affiche liste des semestres, utilisée pour semestres en cours"""
tmpl = """<tr class="%(trclass)s">%(tmpcode)s
<td class="semicon">%(lockimg)s <a href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
tmpl = f"""<tr class="%(trclass)s">%(tmpcode)s
<td class="semicon">%(lockimg)s <a href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept)}?formsemestre_id=%(formsemestre_id)s#groupes">%(groupicon)s</a></td>
<td class="datesem">%(mois_debut)s <a title="%(session_id)s">-</a> %(mois_fin)s</td>
<td><a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<td class="titresem"><a class="stdlink" href="{url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept)}?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>
</td>
</tr>
@ -251,25 +201,34 @@ def _sem_table(sems):
cur_idx = sem["semestre_id"]
else:
sem["trclass"] = ""
sem["notes_url"] = scu.NotesURL()
H.append(tmpl % sem)
H.append("</table>")
return "\n".join(H)
def _sem_table_gt(sems, showcodes=False):
"""Nouvelle version de la table des semestres
def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable:
"""Table des semestres
Utilise une datatables.
"""
_style_sems(sems)
sems = _style_sems(
_convert_formsemestres_to_dicts(formsemestres, showcodes, fmt=fmt), fmt=fmt
)
sems.sort(
key=lambda s: (
-s["anneescolaire"],
s["semestre_id"] if s["semestre_id"] > 0 else -s["semestre_id"] * 1000,
s["modalite"],
)
)
columns_ids = (
"lockimg",
"published",
"semestre_id_n",
"modalite",
#'mois_debut',
"dash_mois_fin",
"titre_resp",
"nb_inscrits",
"formation",
"etapes_apo_str",
"elt_annee_apo",
"elt_sem_apo",
@ -284,7 +243,7 @@ def _sem_table_gt(sems, showcodes=False):
titles={
"formsemestre_id": "id",
"semestre_id_n": "S#",
"modalite": "",
"modalite": "" if fmt == "html" else "Modalité",
"mois_debut": "Début",
"dash_mois_fin": "Année",
"titre_resp": "Semestre",
@ -292,6 +251,7 @@ def _sem_table_gt(sems, showcodes=False):
"etapes_apo_str": "Étape Apo.",
"elt_annee_apo": "Elt. année Apo.",
"elt_sem_apo": "Elt. sem. Apo.",
"formation": "Formation",
},
columns_ids=columns_ids,
rows=sems,
@ -311,22 +271,47 @@ def _sem_table_gt(sems, showcodes=False):
return tab
def _style_sems(sems):
def _style_sems(sems: list[dict], fmt="html") -> list[dict]:
"""ajoute quelques attributs de présentation pour la table"""
for sem in sems:
sem["notes_url"] = scu.NotesURL()
sem["_groupicon_target"] = (
"%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s"
% sem
is_h = fmt == "html"
if is_h:
icon_published = scu.icontag(
"eye_img",
border="0",
title="Bulletins publiés sur la passerelle étudiants",
)
icon_hidden = scu.icontag(
"hide_img",
border="0",
title="Bulletins NON publiés sur la passerelle étudiants",
)
else:
icon_published = "publié"
icon_hidden = "non publié"
for sem in sems:
status_url = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=sem["formsemestre_id"],
)
sem["_groupicon_target"] = status_url
sem["_formsemestre_id_class"] = "blacktt"
sem["dash_mois_fin"] = '<a title="%(session_id)s"></a> %(anneescolaire)s' % sem
sem["dash_mois_fin"] = (
(f"""<a title="{sem['session_id']}">{sem['anneescolaire_str']}</a>""")
if is_h
else sem["anneescolaire_str"]
)
sem["_dash_mois_fin_class"] = "datesem"
sem["titre_resp"] = (
"""<a class="stdlink" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a>
<span class="respsem">(%(responsable_name)s)</span>"""
% sem
(
f"""<a class="stdlink" href="{status_url}">{sem['titre_num']}</a>
<span class="respsem">({sem['responsable_name']})</span>"""
)
if is_h
else f"""{sem['titre_num']} ({sem["responsable_name"]})"""
)
sem["published"] = icon_hidden if sem["bul_hide_xml"] else icon_published
sem["_css_row_class"] = "css_S%d css_M%s" % (
sem["semestre_id"],
sem["modalite"],
@ -347,6 +332,7 @@ def _style_sems(sems):
sem["_elt_sem_apo_td_attrs"] = (
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
)
return sems
def delete_dept(dept_id: int) -> str:

View File

@ -279,11 +279,18 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
nb_eval_completes (= prises en compte)
nb_evals_en_cours (= avec des notes, mais pas complete)
nb_evals_vides (= sans aucune note)
nb_evals_attente (= avec des notes en ATTente et pas bloquée)
date derniere modif
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
"""
nb_evals_completes, nb_evals_en_cours, nb_evals_vides, nb_evals_blocked = 0, 0, 0, 0
(
nb_evals_completes,
nb_evals_en_cours,
nb_evals_vides,
nb_evals_blocked,
nb_evals_attente,
) = (0, 0, 0, 0, 0)
dates = []
for e in etat_evals:
if e["etat"]["blocked"]:
@ -294,6 +301,8 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
nb_evals_vides += 1
elif not e["etat"]["blocked"]:
nb_evals_en_cours += 1
if e["etat"]["nb_attente"] and not e["etat"]["blocked"]:
nb_evals_attente += 1
last_modif = e["etat"]["last_modif"]
if last_modif is not None:
dates.append(e["etat"]["last_modif"])
@ -303,6 +312,7 @@ def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
return {
"nb_evals": len(etat_evals),
"nb_evals_attente": nb_evals_attente,
"nb_evals_blocked": nb_evals_blocked,
"nb_evals_completes": nb_evals_completes,
"nb_evals_en_cours": nb_evals_en_cours,

View File

@ -494,7 +494,7 @@ def table_formsemestres(
):
"""Une table presentant des semestres"""
for sem in sems:
sem_set_responsable_name(sem)
sem_set_responsable_name(sem) # TODO utiliser formsemestre.responsables_str()
sem["_titre_num_target"] = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,

View File

@ -798,7 +798,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
'Tous les étudiants'}
</div>
<div class="sem-groups-partition-titre">{
"Gestion de l'assiduité" if not partition_is_empty else ""
"Assiduité" if not partition_is_empty else ""
}</div>
"""
)
@ -824,14 +824,14 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
</div>
<div class="sem-groups-assi">
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.visu_assi_group",
scodoc_dept=g.scodoc_dept,
date_debut=formsemestre.date_debut.isoformat(),
date_fin=formsemestre.date_fin.isoformat(),
group_ids=group.id,
)}">
<button>Bilan assiduité</button></a>
Bilan</a>
</div>
"""
)
@ -839,42 +839,42 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
H.append(
f"""
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.visu_assiduites_group",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
jour = datetime.date.today().isoformat(),
group_ids=group.id,
)}">
<button>Visualiser</button></a>
Visualiser</a>
</div>
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
jour=datetime.date.today().isoformat(),
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Saisie journalière</button></a>
Saisie journalière</a>
</div>
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Saisie différée</button></a>
Saisie différée</a>
</div>
<div>
<a class="btn" href="{
<a class="stdlink" href="{
url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Justificatifs en attente</button></a>
Justificatifs en attente</a>
</div>
"""
)
@ -1312,7 +1312,9 @@ def formsemestre_tableau_modules(
if etat["attente"]:
H.append(
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
title="Il y a des notes en attente"><span class="evals_attente">en attente</span></a></span>"""
title="Il y a des notes en attente"><span class="evals_attente">{
etat["nb_evals_attente"]
} en attente</span></a></span>"""
)
if not mod_is_conforme:
H.append(

View File

@ -1210,7 +1210,9 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
<em>dans un semestre hors ScoDoc</em>.</p>
<p class="expl"><b>Les UE validées dans ScoDoc sont
<div class="scobox explanation">
<p><b>Les UE validées dans ScoDoc sont
automatiquement prises en compte</b>.
</p>
<p>Cette page est surtout utile pour les étudiants ayant
@ -1227,11 +1229,12 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
l'attribution des ECTS si le code jury est validant (ADM).
</p>
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
</div>
{_get_etud_ue_cap_html(etud, formsemestre)}
<div class="sco_box">
<div class="sco_box_title">
<div class="scobox">
<div class="scobox-title">
Enregistrer une UE antérieure
</div>
{tf[1]}

View File

@ -705,7 +705,8 @@ def _add_eval_columns(
nb_att = 0
sum_notes = 0
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
inscrits = evaluation.moduleimpl.formsemestre.etudids_actifs # set d'etudids
# actifs == inscrit au semestre, non DEM ni DEF:
_, etudids_actifs = evaluation.moduleimpl.formsemestre.etudids_actifs()
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
if evaluation.date_debut:
@ -734,7 +735,7 @@ def _add_eval_columns(
nb_att += 1
# calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES
if (
(etudid in inscrits)
(etudid in etudids_actifs)
and val is not None
and val != scu.NOTES_NEUTRALISE
and val != scu.NOTES_ATTENTE
@ -758,7 +759,7 @@ def _add_eval_columns(
comment,
)
else:
if (etudid in inscrits) and evaluation.publish_incomplete:
if (etudid in etudids_actifs) and evaluation.publish_incomplete:
# Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE
val_fmt = "ATT"
@ -875,8 +876,7 @@ def _add_moymod_column(
col_id = "moymod"
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
inscrits = formsemestre.etudids_actifs
_, etudids_actifs = formsemestre.etudids_actifs()
nb_notes = 0
sum_notes = 0
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
@ -885,7 +885,7 @@ def _add_moymod_column(
val = nt.get_etud_mod_moy(moduleimpl_id, etudid) # note sur 20, ou 'NA','NI'
row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric)
row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
if etudid in inscrits and not isinstance(val, str):
if etudid in etudids_actifs and not isinstance(val, str):
notes.append(val)
if not np.isnan(val):
nb_notes = nb_notes + 1
@ -928,7 +928,7 @@ def _add_apc_columns(
# on va y ajouter une clé par UE du semestre
nt: ResultatsSemestreBUT = res_sem.load_formsemestre_results(modimpl.formsemestre)
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
inscrits = modimpl.formsemestre.etudids_actifs
_, etudids_actifs = modimpl.formsemestre.etudids_actifs()
# les UE dans lesquelles ce module a un coef non nul:
ues_with_coef = nt.modimpl_coefs_df[modimpl.id][
nt.modimpl_coefs_df[modimpl.id] > 0
@ -946,7 +946,7 @@ def _add_apc_columns(
if (
isinstance(moy_ue, float)
and not np.isnan(moy_ue)
and row["etudid"] in inscrits
and row["etudid"] in etudids_actifs
):
sum_by_ue[ue.id] += moy_ue
nb_notes_by_ue[ue.id] += 1

View File

@ -50,8 +50,8 @@ def list_formsemestres_modalites(sems):
return modalites
def group_sems_by_modalite(sems):
"""Given the list of fromsemestre, group them by modalite,
def group_sems_by_modalite(sems: list[dict]):
"""Given the list of formsemestre, group them by modalite,
sorted in each one by semestre id and date
"""
sems_by_mod = collections.defaultdict(list)

View File

@ -332,28 +332,29 @@ def fiche_etud(etudid=None):
)
# fiche admission
infos_admission = _infos_admission(etud, restrict_etud_data)
has_adm_notes = any(
infos_admission[k] for k in ("math", "physique", "anglais", "francais")
)
has_bac_info = any(
infos_admission[k]
for k in (
"bac_specialite",
"annee_bac",
"rapporteur",
"commentaire",
"classement",
"type_admission",
"rap",
if etud.admission:
infos_admission = _infos_admission(etud, restrict_etud_data)
has_adm_notes = any(
infos_admission[k] for k in ("math", "physique", "anglais", "francais")
)
)
if has_bac_info or has_adm_notes:
adm_tmpl = """<!-- Donnees admission -->
<div class="fichetitre">Informations admission</div>
"""
if has_adm_notes:
adm_tmpl += """
has_bac_info = any(
infos_admission[k]
for k in (
"bac_specialite",
"annee_bac",
"rapporteur",
"commentaire",
"classement",
"type_admission",
"rap",
)
)
if has_bac_info or has_adm_notes:
adm_tmpl = """<!-- Donnees admission -->
<div class="fichetitre">Informations admission</div>
"""
if has_adm_notes:
adm_tmpl += """
<table>
<tr><th>Bac</th><th>Année</th><th>Rg</th>
<th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr>
@ -364,24 +365,26 @@ def fiche_etud(etudid=None):
<td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td>
</tr>
</table>
"""
adm_tmpl += """
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="info_lycee">%(info_lycee)s</div>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += """<div class="vadmission">"""
if infos_admission["type_admission"]:
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
if infos_admission["classement"]:
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += "</div>"
if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>"""
"""
adm_tmpl += """
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
<div class="info_lycee">%(info_lycee)s</div>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += """<div class="vadmission">"""
if infos_admission["type_admission"]:
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
if infos_admission["classement"]:
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
if infos_admission["type_admission"] or infos_admission["classement"]:
adm_tmpl += "</div>"
if infos_admission["rap"]:
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
adm_tmpl += """</div>"""
else:
adm_tmpl = "" # pas de boite "info admission"
info["adm_data"] = adm_tmpl % infos_admission
else:
adm_tmpl = "" # pas de boite "info admission"
info["adm_data"] = adm_tmpl % infos_admission
info["adm_data"] = ""
# Fichiers archivés:
info["fichiers_archive_htm"] = (
@ -654,7 +657,7 @@ def _format_adresse(adresse: Adresse | None) -> dict:
def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
"""dict with adminission data, restricted or not"""
"""dict with admission data, restricted or not"""
# info sur rapporteur et son commentaire
rap = ""
if not restrict_etud_data:
@ -799,8 +802,11 @@ def etud_info_html(etudid, with_photo="1", debug=False):
code_cursus, _ = sco_report.get_code_cursus_etud(
etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", "
)
bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite)
bac_abbrev = bac.abbrev()
if etud.admission:
bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite)
bac_abbrev = bac.abbrev()
else:
bac_abbrev = "-"
H = f"""<div class="etud_info_div">
<div class="eid_left">
<div class="eid_nom"><div><a class="stdlink" target="_blank" href="{

View File

@ -68,7 +68,6 @@ from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes
import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
@ -531,7 +530,7 @@ def notes_add(
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
"""
assert evaluation_id is not None
evaluation = Evaluation.get_evaluation(evaluation_id)
now = psycopg2.Timestamp(*time.localtime()[:6])
# Vérifie inscription et valeur note
@ -541,10 +540,16 @@ def notes_add(
evaluation_id, getallstudents=True, include_demdef=True
)
}
# Les étudiants inscrits au semestre ni DEM ni DEF
_, etudids_actifs = evaluation.moduleimpl.formsemestre.etudids_actifs()
for etudid, value in notes:
if check_inscription and (etudid not in inscrits):
if check_inscription and (
(etudid not in inscrits) or (etudid not in etudids_actifs)
):
log(f"notes_add: {etudid} non inscrit ou DEM/DEF: aborting")
raise NoteProcessError(f"étudiant {etudid} non inscrit dans ce module")
if (value is not None) and not isinstance(value, float):
log(f"notes_add: {etudid} valeur de note invalide ({value}): aborting")
raise NoteProcessError(
f"etudiant {etudid}: valeur de note invalide ({value})"
)
@ -555,7 +560,6 @@ def notes_add(
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
etudids_changed = []
nb_suppress = 0
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# etudids pour lesquels il y a une decision de jury et que la note change:

View File

@ -1442,7 +1442,7 @@ 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")
ICON_XLS = icontag("xlsicon_img", title="Export tableur (xlsx)")
# HTML emojis
EMO_WARNING = "&#9888;&#65039;" # warning /!\

View File

@ -577,13 +577,6 @@
border: solid 1px #333;
}
.assi-liste {
border: 1px solid gray;
border-radius: 12px;
margin-right: 24px;
padding: 12px;
}
#options-tableau label {
font-weight: normal;
margin-right: 12px;
@ -651,7 +644,7 @@ tr.row-justificatif.non_valide td.assi-type {
background-color: var(--color-justi-invalide);
}
/*
/*
<== Loader ==>

View File

@ -6,6 +6,7 @@
--sco-content-max-width: 1024px;
--sco-color-explication: rgb(10, 58, 140);
--sco-color-background: rgb(242, 242, 238);
--sco-color-box-bg: rgb(243, 240, 228);
--sco-color-mod-std: #afafc2;
--sco-color-ressources: #f8c844;
--sco-color-saes: #c6ffab;
@ -28,10 +29,6 @@ body {
}
}
div.container {
margin-bottom: 24px;
}
h1,
h2,
h3 {
@ -43,6 +40,51 @@ h3 {
font-weight: bold;
}
details > summary:first-of-type {
display: list-item!important;
}
div.container {
margin-bottom: 24px;
}
div.sco-app-content {
display: flex;
flex-direction: column;
}
div.scobox {
flex: 1 0 0; /* Equal width for all boxes */
max-width: var(--sco-content-max-width);
/* margin: 5px; Optional: Add margin between boxes */
background-color: var(--sco-color-box-bg);
margin-top: 12px;
margin-bottom: 12px;
margin-right: 12px;
padding: 8px;
border: 1px solid #c5b4b2;
border-radius: 8px;
}
div.scobox.explanation {
background-color: var(--sco-color-background);
}
div.scobox div.scobox-title {
font-size: 120%;
font-weight: bold;
margin-bottom: 8px;
}
div.scobox-buttons {
margin-top: 16px;
margin-bottom: 4px;
}
div.scobox-buttons input {
font-size: 110%;
}
div.scobox-etud {
background-color: var(--sco-color-background);
}
/* customization of multiselect style */
.multiselect-container.dropdown-menu {
background-color: #e9e9e9;
@ -554,10 +596,6 @@ table.listesems tr.firstsem td {
padding-top: 0.8em;
}
td.datesem {
font-size: 80%;
white-space: nowrap;
}
h2.listesems {
padding-top: 10px;
@ -645,60 +683,51 @@ table.semlist tbody tr td.modalite {
}
div#gtrcontent table.semlist tbody tr.css_S-1 td {
background-color: rgb(251, 250, 216);
background-color: rgb(211, 213, 255);
}
div#gtrcontent table.semlist tbody tr.css_S1 td {
background-color: rgb(92%, 95%, 94%);
background-color:#e9efef;
}
div#gtrcontent table.semlist tbody tr.css_S2 td {
background-color: rgb(214, 223, 236);
background-color: #d4ebd7;
}
div#gtrcontent table.semlist tbody tr.css_S3 td {
background-color: rgb(167, 216, 201);
background-color: #bedebe;
}
div#gtrcontent table.semlist tbody tr.css_S4 td {
background-color: rgb(131, 225, 140);
background-color: #afd7ad;
}
div#gtrcontent table.semlist tbody tr.css_S5 td {
background-color: #a0cd9a;
}
div#gtrcontent table.semlist tbody tr.css_S6 td {
background-color: #7dcf78;
}
div#gtrcontent table.semlist tbody tr.css_MEXT td {
color: #0b6e08;
color: #fefcdf;
}
/* ----- Liste des news ----- */
div.news {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
div.scobox.news {
font-size: 10pt;
margin-top: 1em;
margin-bottom: 0px;
margin-right: 16px;
margin-left: 16px;
padding: 0.5em;
background-color: rgb(255, 235, 170);
-moz-border-radius: 8px;
-khtml-border-radius: 8px;
border-radius: 8px;
}
div.news a,
div.news a.stdlink {
color: black;
text-decoration: none;
}
div.news a:hover {
color: rgb(153, 51, 51);
text-decoration: underline;
}
span.newstitle {
font-weight: bold;
}
ul.newslist {
padding-left: 1em;
padding-bottom: 0em;
@ -713,6 +742,21 @@ span.newsdate {
span.newstext {
font-style: normal;
}
/* div.news {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 10pt;
margin-top: 1em;
margin-bottom: 0px;
margin-right: 16px;
margin-left: 16px;
padding: 0.5em;
background-color: rgb(255, 235, 170);
-moz-border-radius: 8px;
-khtml-border-radius: 8px;
border-radius: 8px;
} */
span.gt_export_icons {
margin-left: 1.5em;
@ -1184,7 +1228,6 @@ a.discretelink:hover {
text-align: center;
}
.expl,
.help {
max-width: var(--sco-content-max-width);
}
@ -1980,9 +2023,18 @@ ul.ue_inscr_list li.etud {
grid-template-columns: 240px auto;
}
.sem-groups-partition .stdlink, .sem-groups-partition .stdlink:visited {
color: black;
text-decoration-style: dotted;
text-underline-offset: 3px;
}
.sem-groups-list .stdlink, .sem-groups-list .stdlink:visited {
color:rgb(0, 0, 192);
}
.sem-groups-list,
.sem-groups-assi {
background-color: white;
background-color: #ebebeb;
border-radius: 6px;
margin: 4px;
}
@ -4102,22 +4154,18 @@ div.othersemlist input {
margin-left: 20px;
}
div#update_warning {
div.scobox.update_warning {
display: none;
border: 1px solid red;
background-color: rgb(250, 220, 220);
margin: 3ex;
padding-left: 1ex;
padding-right: 1ex;
padding-bottom: 1ex;
}
div#update_warning > div:first-child:before {
div.scobox.update_warning > div:first-child:before {
content: url(/ScoDoc/static/icons/warning_img.png);
vertical-align: -80%;
}
div#update_warning > div:nth-child(2) {
div.scobox.update_warning > div:nth-child(2) {
font-size: 80%;
padding-left: 8ex;
}

View File

@ -114,7 +114,9 @@ function paste_text(e) {
.classList.contains("masquer_DEM");
for (var i = 0; i < list.length; i++) {
currentInput.value = list[i];
if (!currentInput.disabled) { // skip DEM
currentInput.value = list[i];
}
var evt = document.createEvent("HTMLEvents");
evt.initEvent("blur", false, true);
currentInput.dispatchEvent(evt);

View File

@ -110,9 +110,9 @@ div.submit > input {
</div>
</form>
<section class="assi-liste">
<div class="scobox assi-liste">
{{tableau | safe }}
</section>
</div>
</div>

View File

@ -137,9 +137,9 @@ div.submit > input {
</form>
</section>
{% if tableau %}
<section class="assi-liste">
<div class="scobox assi-liste">
{{tableau | safe }}
</section>
</div>
{% endif %}
</div>

View File

@ -7,24 +7,29 @@
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% endblock scripts %}
{% block app_content %}
<h2>Traitement de l'assiduité</h2>
<h1>Traitement de l'assiduité</h1>
<p class="help">
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
le semestre concerné (saisie par jour ou saisie différée).
</p>
<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
choisissez d'abord la personne concernée&nbsp;:</p>
<br>
{{search_etud | safe}}
<br>
{{billets | safe}}
<br>
<div class="scobox scobox-etud">
<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
choisissez d'abord la personne concernée&nbsp;:</p>
{{search_etud | safe}}
</div>
<div>
<h3>Télécharger l'assiduité</h3>
{{billets | safe}}
<div>
<div class="scobox">
<div class="scobox-title">Télécharger tous les enregistrements d'assiduité</div>
<form action="{{url_for('assiduites.recup_assiduites_plage', scodoc_dept=g.scodoc_dept)}}" method="post">
<label for="datedeb">
Du&nbsp;:
@ -36,7 +41,7 @@ le semestre concerné (saisie par jour ou saisie différée).
<input type="text" class="datepicker" id="datefin" name="datefin">
</label>
<br>
<label for="formsemestre_id">Télécharger l'assiduité de </label>
<label for="formsemestre_id">Origine :</label>
<select name="formsemestre_id" id="formsemestre_id">
<option value="">Tout le département</option>
{% for id, titre in formsemestres.items() %}
@ -47,14 +52,17 @@ le semestre concerné (saisie par jour ou saisie différée).
{% endif %}
{% endfor %}
</select>
<br>
<input type="submit" value="Télécharger" name="telecharger">
<div class="scobox-buttons">
<input type="submit" value="Télécharger" name="telecharger">
</div>
</form>
</div>
<br>
<section class="nonvalide">
{{tableau | safe }}
<div class="scobox">
{{tableau | safe }}
</div>
</section>
{% endblock app_content %}

View File

@ -37,16 +37,10 @@ Bilan assiduité de {{sco.etud.nomprenom}}
flex-direction: column;
}
.alerte {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
margin: 5px 0;
.scobox.alerte {
text-align: center;
border-radius: 7px;
background-color: var(--color-error);
}
.alerte.invisible {
@ -70,11 +64,11 @@ Bilan assiduité de {{sco.etud.nomprenom}}
<h2>Bilan de l'assiduité de {{sco.etud.html_link_fiche()|safe}}</span></h2>
<section class="alerte invisible">
<div class="scobox alerte invisible">
<p>Attention, cet étudiant a trop d'absences</p>
</section>
</div>
<section class="stats">
<div class="scobox">
<!-- Statistiques d'assiduité (nb pres, nb retard, nb absence) + nb justifié -->
<h4>Statistiques d'assiduité</h4>
<div class="stats-inputs">
@ -88,13 +82,16 @@ Bilan assiduité de {{sco.etud.nomprenom}}
<div class="stats-values">
</div>
</section>
<br>
</div>
<div class="scobox">
<section class="nonvalide">
<div>Le tableau n'affiche que les assiduités non justifiées et les justificatifs soumis / modifiés</div>
<div class="help">Le tableau n'affiche que les assiduités non justifiées
et les justificatifs soumis / modifiés
</div>
{{tableau | safe }}
</section>
</div>
<div class="legende">
<h3>Statistiques</h3>

View File

@ -1,5 +1,5 @@
<div>
<div class="sco_box_title">{{ titre }}</div>
<div class="assi-tableau">
<div class="scobox-title">{{ titre }}</div>
<div class="options-tableau">
{% if afficher_options != false %}
<input type="checkbox" id="show_pres" name="show_pres"
@ -17,7 +17,7 @@
<a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
<br>
{% endif %}
<label for="nb_ligne_page">Nombre de lignes par page :</label>
<label for="nb_ligne_page">Nombre de lignes par page :</label>
<select name="nb_ligne_page" id="nb_ligne_page" onchange="updateTableau()">
{% for i in [25,50,100,1000] %}
{% if i == options.nb_ligne_page %}
@ -182,14 +182,14 @@
// récupération de la colonne à ordonner
// il faut avoir une classe `external-type:<NOM COL>`
let order_col = e.className.split(" ").find((e)=>e.indexOf("external-type:") != -1);
//Création de la nouvelle url avec le tri
const url = new URL(location.href);
url.searchParams.set("order", order);
url.searchParams.set("order_col", order_col.split(":")[1]);
location.href = url.href
}));
});

View File

@ -33,9 +33,11 @@
{% endblock %}
{% endif %}
{% block app_content %}
page vide
{% endblock %}
<div class="sco-app-content">
{% block app_content %}
page vide
{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,149 @@
{# page accueil département #}
{% extends "sco_page.j2" %}
{% block app_content %}
<style>
table.listesems tr td.datesem {
white-space: nowrap;
padding-left: 8px;
}
table.listesems tr td.titresem {
padding-left: 6px;
}
.table-formsemestres-titre {
font-weight: bold;
font-size: 110%;
}
table.semlist tr td.datesem {
font-size: 80%;
text-align: center;
}
table.semlist tr td.semestre_id_n {
text-align: center;
}
table.semlist tr td.nb_inscrits {
text-align: center;
}
</style>
{# News #}
{{scolar_news_summary|safe}}
{# Avertissement de mise à jour: #}
<div id="update_warning" class="scobox update_warning"></div>
{% if nb_user_accounts == 0 %}
<h2>Aucun utilisateur défini !</h2>
<p>Pour définir des utilisateurs <a href="{{
url_for('users.index_html', scodoc_dept=g.scodoc_dept)
}}">passez par la page Utilisateurs</a>.<br>
Définissez au moins un utilisateur avec le rôle <tt>AdminXXX</tt>
(le responsable du département <tt>XXX</tt>).
</p>
{% endif %}
{# Les semestres courants (cad non verrouillés) #}
<div class="scobox">
{{html_current_formsemestres|safe}}
</div>
{# Table de tous les semestres #}
{% if html_table_formsemestres %}
<details open>
<summary class="table-formsemestres-titre">
Les {{formsemestres.count()}} semestres de {{dept_name}}
&nbsp;
<a href="{{
url_for('scolar.export_table_dept_formsemestres', scodoc_dept=g.scodoc_dept)
}}">{{scu.ICON_XLS|safe}}</a>
</summary>
{{ html_table_formsemestres|safe }}
</details>
{% else %}
<p><a class="stdlink" href="{{
url_for('scolar.index_html', scodoc_dept=g.scodoc_dept, showsemtable=1)
}}">Voir table des {{formsemestres.count()}} semestres
{% if nb_locked %}
(dont {{nb_locked}} verrouillé{{'s' if nb_locked > 1 else ''}})
{%endif%}
</a>
</p>
{% endif %}
</div>
{# Recherche d'un semestre par code Apogée #}
<form action="{{
url_for('notes.view_formsemestre_by_etape', scodoc_dept=g.scodoc_dept)
}}">
Chercher étape courante:
<input name="etape_apo" type="text" size="8" spellcheck="false"></input>
</form>
{# Gestion des étudiants #}
<div class="scobox">
<div class="scobox-title">Gestion des étudiants</div>
<ul>
{% if current_user.has_permission(Permission.EtudInscrit) %}
<li><a class="stdlink" href="{{
url_for('scolar.etudident_create_form', scodoc_dept=g.scodoc_dept)
}}">créer <em>un</em> nouvel étudiant</a>
</li>
<li><a class="stdlink" href="{{
url_for('scolar.form_students_import_excel', scodoc_dept=g.scodoc_dept)
}}">importer de nouveaux étudiants</a>
(<em>ne pas utiliser</em> sauf cas particulier&nbsp;: utilisez plutôt le lien dans
le tableau de bord semestre si vous souhaitez inscrire les
étudiants importés à un semestre)
</li>
{% endif %}
<li><a class="stdlink" href="{{
url_for('scolar.export_etudiants_courants', scodoc_dept=g.scodoc_dept)
}}">exporter tableau des étudiants des semestres en cours</a>
</li>
{% if current_user.has_permission(Permission.EtudInscrit) and sco.prefs["portal_url"] %}
<li><a class="stdlink" href="{{
url_for('scolar.formsemestre_import_etud_admission', scodoc_dept=g.scodoc_dept, tous_courants=1)
}}">resynchroniser les données étudiants des semestres en cours depuis le portail</a>
</li>
{% endif %}
</ul>
</div>
{# Apogée #}
{% if current_user.has_permission(Permission.EditApogee) %}
<div class="scobox">
<div class="scobox-title">Exports Apogée</div>
<ul>
<li><a class="stdlink" href="{{
url_for('notes.semset_page', scodoc_dept=g.scodoc_dept)
}}">Années scolaires / exports Apogée</a>
</li>
</ul>
</div>
{% endif %}
{# Assistance #}
<div class="scobox">
<div class="scobox-title">Assistance</div>
<ul>
<li>
<a class="stdlink" href="https://scodoc.org/Contact" target="_blank"
rel="noopener noreferrer">Contact (Discord)</a>
</li>
<li>
<a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a>
</li>
</ul>
</div>
{% endblock app_content %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/scolar_index.js"></script>
{% endblock scripts %}

View File

@ -349,6 +349,20 @@ def index_html(showcodes=0, showsemtable=0):
return sco_dept.index_html(showcodes=showcodes, showsemtable=showsemtable)
@bp.route("/export_table_dept_formsemestres")
@scodoc
@permission_required(Permission.ScoView)
def export_table_dept_formsemestres():
"""La table de tous les semestres non EXt du département, en excel"""
table = sco_dept.index_html(showcodes=True, export_table_formsemestres=True)
return scu.send_file(
table.excel(),
f"semestres_{g.scodoc_dept}",
suffix=scu.XLSX_SUFFIX,
mime=scu.XLSX_MIMETYPE,
)
@bp.route("/install_info")
@scodoc
@permission_required(Permission.ScoView)

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.951"
SCOVERSION = "9.6.953"
SCONAME = "ScoDoc"

View File

@ -79,10 +79,11 @@ if pytest:
return get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
def GET(path: str, headers: dict = None, errmsg=None, dept=None):
"""Get and returns as JSON
def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
"""Get and optionaly returns as JSON
Special case for non json result (image or pdf):
return Content-Disposition string (inline or attachment)
If raw, return a requests.Response
"""
if dept:
url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path
@ -101,10 +102,11 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None):
raise APIError(
errmsg or f"""erreur status={reply.status_code} !""", reply.json()
)
if raw:
return reply
if reply.headers.get("Content-Type", None) == "application/json":
return reply.json() # decode la reponse JSON
elif reply.headers.get("Content-Type", None) in [
if reply.headers.get("Content-Type", None) in [
"image/jpg",
"image/png",
"application/pdf",

View File

@ -823,16 +823,13 @@ def test_etudiant_bulletin_semestre(api_headers):
assert r.content[:4] == b"%PDF"
######## Bulletin BUT format intermédiaire en pdf #########
r = requests.get(
API_URL
+ "/etudiant/ine/"
+ str(INE)
+ "/formsemestre/1/bulletin/selectedevals/pdf",
r = GET(
f"/etudiant/ine/{INE}/formsemestre/1/bulletin/selectedevals/pdf",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
raw=True, # get response, do not convert to json
)
assert r.status_code == 200
assert r.headers.get("Content-Type", None) == "application/pdf"
assert r.content[:4] == b"%PDF"
################### LONG + PDF #####################
@ -869,37 +866,17 @@ def test_etudiant_bulletin_semestre(api_headers):
################### SHORT #####################
######### Test etudid #########
r = requests.get(
API_URL + "/etudiant/etudid/" + str(ETUDID) + "/formsemestre/1/bulletin/short",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
bul = GET(
f"/etudiant/etudid/{ETUDID}/formsemestre/1/bulletin/short", headers=api_headers
)
assert r.status_code == 200
bul = r.json()
assert len(bul) == 14 # HARDCODED
######### Test code nip #########
r = requests.get(
API_URL + "/etudiant/nip/" + str(NIP) + "/formsemestre/1/bulletin/short",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
bul = r.json()
bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin/short", headers=api_headers)
assert len(bul) == 14 # HARDCODED
######### Test code ine #########
r = requests.get(
API_URL + "/etudiant/ine/" + str(INE) + "/formsemestre/1/bulletin/short",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
bul = r.json()
bul = GET(f"/etudiant/ine/{INE}/formsemestre/1/bulletin/short", headers=api_headers)
assert len(bul) == 14 # HARDCODED
################### SHORT + PDF #####################
@ -941,6 +918,23 @@ def test_etudiant_bulletin_semestre(api_headers):
)
assert r.status_code == 404
### -------- Modifie publication bulletins
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
formsemestre = POST_JSON(
f"/formsemestre/{1}/edit", {"bul_hide_xml": True}, headers=admin_header
)
assert formsemestre["bul_hide_xml"] is True
# La forme utilisée par la passerelle:
bul = GET(f"/etudiant/nip/{NIP}/formsemestre/1/bulletin", headers=api_headers)
assert len(bul) == 9 # version raccourcie, longueur HARDCODED
# TODO forme utilisée par la passerelle pour les PDF
# /ScoDoc/api/etudiant/nip/12345/formsemestre/123/bulletin/long/pdf/nosi
# TODO voir forme utilisée par ScoDoc en interne:
# formsemestre_bulletinetud?formsemestre_id=1263&etudid=16387
formsemestre = POST_JSON(
f"/formsemestre/{1}/edit", {"bul_hide_xml": False}, headers=admin_header
)
def test_etudiant_groups(api_headers):
"""

View File

@ -2,6 +2,7 @@
Test modèles évaluations avec poids BUT
et calcul moyennes modules
"""
import datetime
import numpy as np
import pandas as pd
@ -215,7 +216,8 @@ def test_module_moy(test_client):
etud = G.create_etud(nom="test")
G.inscrit_etudiant(formsemestre_id, etud)
etudid = etud["etudid"]
evaluation1 = db.session.get(Evaluation, evaluation1_ids[0])
evaluation1: Evaluation = db.session.get(Evaluation, evaluation1_ids[0])
formsemestre = evaluation1.moduleimpl.formsemestre
# Crée une deuxième évaluation dans le même moduleimpl:
evaluation2_id = G.create_evaluation(
moduleimpl_id=evaluation1.moduleimpl_id,
@ -245,10 +247,10 @@ def test_module_moy(test_client):
_ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)])
_ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)])
# Calcul de la moyenne du module
evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id)
evals_poids, _ = moy_mod.load_evaluations_poids(moduleimpl_id)
assert evals_poids.shape == (nb_evals, nb_ues)
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
etudids, etudids_actifs = formsemestre.etudids_actifs()
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
evals_notes = mod_results.evals_notes
assert evals_notes[evaluation1.id].dtype == np.float64