Compare commits

...

6 Commits

13 changed files with 261 additions and 105 deletions

View File

@ -269,13 +269,7 @@ def validation_rcue_record(etudid: int):
validation.parcours_id = parcours_id
validation.ue1_id = ue1_id
validation.ue2_id = ue2_id
log(f"updating {validation}")
Scolog.logdb(
method="validation_rcue_record",
etudid=etudid,
msg=f"Mise à jour {validation}",
commit=False,
)
operation = "update"
else:
validation = ApcValidationRCUE(
code=code,
@ -286,15 +280,16 @@ def validation_rcue_record(etudid: int):
ue1_id=ue1_id,
ue2_id=ue2_id,
)
log(f"recording {validation}")
operation = "record"
db.session.add(validation)
db.session.commit()
Scolog.logdb(
method="validation_rcue_record",
etudid=etudid,
msg=f"Enregistrement {validation}",
commit=False,
commit=True,
)
db.session.add(validation)
db.session.commit()
log(f"{operation} {validation}")
return validation.to_dict()

View File

@ -212,6 +212,34 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
else:
self.ue_std_rows(rows, ue, title_bg)
@staticmethod
def affichage_bonus_malus(ue: dict) -> list:
fields_bmr = []
# lecture des bonus sport culture et malus (ou bonus autre) (0 si valeur non numérique)
try:
bonus_sc = float(ue.get("bonus", 0.0)) or 0
except ValueError:
bonus_sc = 0
try:
malus = float(ue.get("malus", 0.0)) or 0
except ValueError:
malus = 0
# Calcul de l affichage
if malus < 0:
if bonus_sc > 0:
fields_bmr.append(f"Bonus sport/culture: {bonus_sc}")
fields_bmr.append(f"Bonus autres: {-malus}")
else:
fields_bmr.append(f"Bonus: {-malus}")
elif malus > 0:
if bonus_sc > 0:
fields_bmr.append(f"Bonus: {bonus_sc}")
fields_bmr.append(f"Malus: {malus}")
else:
if bonus_sc > 0:
fields_bmr.append(f"Bonus: {bonus_sc}")
return fields_bmr
def ue_std_rows(self, rows: list, ue: dict, title_bg: tuple):
"Lignes décrivant une UE standard dans la table de synthèse"
# 2eme ligne titre UE (bonus/malus/ects)
@ -220,20 +248,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
else:
ects_txt = ""
# case Bonus/Malus/Rang "bmr"
fields_bmr = []
try:
value = float(ue.get("bonus", 0.0))
if value != 0:
fields_bmr.append(f"Bonus: {ue['bonus']}")
except ValueError:
pass
try:
value = float(ue.get("malus", 0.0))
if value != 0:
fields_bmr.append(f"Malus: {ue['malus']}")
except ValueError:
pass
fields_bmr = BulletinGeneratorStandardBUT.affichage_bonus_malus(ue)
moy_ue = ue.get("moyenne", "-")
if isinstance(moy_ue, dict): # UE non capitalisées
if self.preferences["bul_show_ue_rangs"]:

View File

@ -82,6 +82,10 @@ class ApcValidationRCUE(db.Model):
"as a dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
d["etud"] = self.etud.to_dict_short()
d["ue1"] = self.ue1.to_dict()
d["ue2"] = self.ue2.to_dict()
return d
def to_dict_bul(self) -> dict:

View File

@ -33,7 +33,6 @@
import collections
import datetime
import operator
import urllib
from urllib.parse import parse_qs
import time
@ -42,6 +41,8 @@ import time
from flask import url_for, g, request
from flask_login import current_user
from app import db
from app.models import FormSemestre
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_abs
@ -65,6 +66,7 @@ JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
# view:
def groups_view(
group_ids=(),
@ -422,6 +424,13 @@ class DisplayedGroupsInfos(object):
H.append(f'<input type="hidden" name="group_ids" value="{group_id}"/>')
return "\n".join(H)
def get_formsemestre(self) -> FormSemestre:
return (
db.session.get(FormSemestre, self.formsemestre_id)
if self.formsemestre_id
else None
)
# Ancien ZScolar.group_list renommé ici en group_table
def groups_table(

View File

@ -517,7 +517,7 @@ def ficheEtud(etudid=None):
cursus=but_cursus,
scu=scu,
)}
<div>
<div class="link_validation_rcues">
<a href="{url_for("notes.validation_rcues",
scodoc_dept=g.scodoc_dept, etudid=etudid,
formsemestre_id=last_formsemestre.id)}"

View File

@ -8,7 +8,7 @@
"""
from flask import g, url_for
from app.models import Identite, Assiduite, Justificatif
from app.models import Identite, Justificatif
from app.tables import table_builder as tb
import app.scodoc.sco_assiduites as scass
from app.scodoc import sco_preferences
@ -65,31 +65,35 @@ class RowAssi(tb.Row):
}
)
bilan_etud = f"{url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept)}?etudid={etud.id}"
bilan_etud = url_for(
"assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
)
self.add_cell(
"nom_disp",
"Nom",
etud.nom_disp(),
"identite_detail",
"etudinfo",
attrs={"id": str(etud.id)},
data={"order": etud.sort_key},
target=bilan_etud,
target_attrs={"class": "discretelink", "id": str(etud.id)},
target_attrs={"class": "discretelink"},
)
self.add_cell(
"prenom",
"Prénom",
etud.prenom,
"identite_detail",
etud.prenom_str,
"etudinfo",
attrs={"id": str(etud.id)},
data={"order": etud.sort_key},
target=bilan_etud,
target_attrs={"class": "discretelink", "id": str(etud.id)},
target_attrs={"class": "discretelink"},
)
stats = self._get_etud_stats(etud)
for key, value in stats.items():
self.add_cell(key, value[0], f"{value[1] - value[2]}", "assi_stats")
self.add_cell(
key + "_justi",
value[0] + " Justifiée(s)",
value[0] + " Justifiées",
f"{value[2]}",
"assi_stats",
)
@ -102,9 +106,9 @@ class RowAssi(tb.Row):
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
retour: dict[str, tuple[str, float, float]] = {
"present": ["Présence(s)", 0.0, 0.0],
"retard": ["Retard(s)", 0.0, 0.0],
"absent": ["Absence(s)", 0.0, 0.0],
"present": ["Présences", 0.0, 0.0],
"retard": ["Retards", 0.0, 0.0],
"absent": ["Absences", 0.0, 0.0],
}
assi_metric = {

View File

@ -1,3 +1,12 @@
{% extends "sco_page.j2" %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
{% endblock %}
{% block app_content %}
<h2>Visualisation de l'assiduité {{gr_tit|safe}}</h2>
<div class="stats-inputs">
@ -5,7 +14,9 @@
value="{{date_debut}}"></label>
<label class="stats-label"> Date de fin <input type="date" name="stats_date_fin" id="stats_date_fin"
value="{{date_fin}}"></label>
<button onclick="stats()">Changer la période</button>
<button onclick="stats()">Changer</button>
<a style="margin-left:32px;" href="{{request.url}}&format=xlsx">{{scu.ICON_XLS|safe}}</a>
</div>
{{tableau | safe}}
@ -27,3 +38,5 @@
})
</script>
{% endblock %}

View File

@ -100,6 +100,7 @@
data-ue2_id="{{niv['ue_pair'].id}}"
data-code="{{validation.code if validation else ''}}"
>
<option value="" disabled {{"selected" if not validation else ""}}>-</option>
{% for code in rcue_codes %}
<option value="{{code}}"
{% if validation and validation.code == code -%}

View File

@ -649,6 +649,7 @@ def visu_assi_group():
"debut": request.args.get("date_debut"),
"fin": request.args.get("date_fin"),
}
fmt = request.args.get("format", "html")
group_ids: list[int] = request.args.get("group_ids", None)
etudiants: list[dict] = []
@ -662,14 +663,16 @@ def visu_assi_group():
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
etuds = etuds_sorted_from_ids([m["etudid"] for m in groups_infos.members])
header: str = html_sco_header.sco_header(
page_title="Visualisation des assiduités",
init_qtip=True,
)
table: TableAssi = TableAssi(etuds=etuds, dates=list(dates.values()))
if fmt.startswith("xls"):
return scu.send_file(
table.excel(),
filename=f"assiduite-{groups_infos.groups_filename}",
mime=scu.XLSX_MIMETYPE,
suffix=scu.XLSX_SUFFIX,
)
if groups_infos.tous_les_etuds_du_sem:
gr_tit = "en"
else:
@ -681,18 +684,16 @@ def visu_assi_group():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
)
return HTMLBuilder(
header,
render_template(
return render_template(
"assiduites/pages/visu_assi.j2",
tableau=table.html(),
gr_tit=gr_tit,
date_debut=dates["debut"],
date_fin=dates["fin"],
group_ids=request.args.get("group_ids", None),
),
html_sco_header.sco_footer(),
).build()
sco=ScoData(formsemestre=groups_infos.get_formsemestre()),
title=f"Assiduité {grp} {groups_infos.groups_titles}",
)
@bp.route("/SignalAssiduiteDifferee")

View File

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

View File

@ -0,0 +1,82 @@
"""Tests unitaires : bulletins de notes
Utiliser comme:
pytest tests/unit/test_bulletin_bonus.py
"""
from app.but.bulletin_but_pdf import BulletinGeneratorStandardBUT
def test_nobonus():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus({}) == []
def test_bonus_sport_nul():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus({"bonus": 0}) == []
def test_malus_nul():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus({"malus": 0}) == []
def test_bonus_et_malus_nuls():
assert (
BulletinGeneratorStandardBUT.affichage_bonus_malus({"bonus": 0, "malus": 0})
== []
)
def test_vrai_malus():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus({"malus": 0.1}) == [
"Malus: 0.1"
]
def test_bonus_sport_et_vrai_malus():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus(
{"malus": 0.12, "bonus": 0.23}
) == [
"Bonus: 0.23",
"Malus: 0.12",
]
def test_bonus_sport_seul():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus({"bonus": 0.5}) == [
"Bonus: 0.5"
]
def test_bonus_sport_nul_et_vrai_malus():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus(
{"bonus": 0, "malus": 0.5}
) == ["Malus: 0.5"]
def test_bonus_sport_et_malus_nul():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus(
{"bonus": 0.5, "malus": 0}
) == [
"Bonus: 0.5",
]
def test_faux_malus():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus({"malus": -0.6}) == [
"Bonus: 0.6"
]
def test_sport_nul_faux_malus():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus(
{"bonus": 0, "malus": -0.6}
) == ["Bonus: 0.6"]
def test_bonus_sport_et_faux_malus():
assert BulletinGeneratorStandardBUT.affichage_bonus_malus(
{"bonus": 0.3, "malus": -0.6}
) == [
"Bonus sport/culture: 0.3",
"Bonus autres: 0.6",
]

View File

@ -75,9 +75,9 @@ fi
#echo "Creating python3 virtualenv..."
su -c "(cd $SCODOC_DIR && python3 -m venv venv)" "$SCODOC_USER" || die "Error creating Python 3 virtualenv"
# ------------ INSTALL DES PAQUETS PYTHON (3.9)
# ------------ INSTALL DES PAQUETS PYTHON (3.11)
# pip in our env, as user "scodoc"
su -c "(cd $SCODOC_DIR && source venv/bin/activate && pip install wheel && pip install -r requirements-3.9.txt)" "$SCODOC_USER" || die "Error installing python packages"
su -c "(cd $SCODOC_DIR && source venv/bin/activate && pip install wheel && pip install -r requirements-3.11.txt)" "$SCODOC_USER" || die "Error installing python packages"
# --- NGINX
# Evite d'écraser: il faudrait ici présenter un dialogue "fichier local modifié, ..."

View File

@ -7,6 +7,8 @@ from datetime import date, datetime, time, timedelta
from json import dump, dumps
from sqlalchemy import not_
from flask import g
from app import db
from app.models import (
Absence,
@ -30,6 +32,7 @@ from app.scodoc.sco_utils import (
localize_datetime,
print_progress_bar,
)
from app.scodoc import notesdb as ndb
class _glob:
@ -38,7 +41,6 @@ class _glob:
DEBUG: bool = False
PROBLEMS: dict[int, list[str]] = {}
DEPT_ETUDIDS: dict[int, Identite] = {}
MODULES: dict[tuple[int, int]] = {}
COMPTE: list[int, int] = []
ERR_ETU: list[int] = []
MERGER_ASSI: "_Merger" = None
@ -97,42 +99,70 @@ class _Merger:
date_deb = _Merger._tuple_to_date(self.deb)
date_fin = _Merger._tuple_to_date(self.fin, end=True)
retour = Justificatif.fast_create_justificatif(
etudid=self.etudid,
date_debut=date_deb,
date_fin=date_fin,
etat=EtatJustificatif.VALIDE,
raison=self.raison,
entry_date=self.entry_date,
_glob.cursor.execute(
"""INSERT INTO justificatifs
(etudid,date_debut,date_fin,etat,raison,entry_date)
VALUES (%(etudid)s,%(date_debut)s,%(date_fin)s,%(etat)s,%(raison)s,%(entry_date)s)
""",
{
"etudid": self.etudid,
"date_debut": date_deb,
"date_fin": date_fin,
"etat": EtatJustificatif.VALIDE,
"raison": self.raison,
"entry_date": self.entry_date,
},
)
return retour
# retour = Justificatif.fast_create_justificatif(
# etudid=self.etudid,
# date_debut=date_deb,
# date_fin=date_fin,
# etat=EtatJustificatif.VALIDE,
# raison=self.raison,
# entry_date=self.entry_date,
# )
# return retour
def _to_assi(self):
date_deb = _Merger._tuple_to_date(self.deb)
date_fin = _Merger._tuple_to_date(self.fin, end=True)
retour = Assiduite.fast_create_assiduite(
etudid=self.etudid,
date_debut=date_deb,
date_fin=date_fin,
etat=EtatAssiduite.ABSENT,
moduleimpl_id=self.moduleimpl,
description=self.raison,
entry_date=self.entry_date,
_glob.cursor.execute(
"""INSERT INTO assiduites
(etudid,date_debut,date_fin,etat,moduleimpl_id,"desc",entry_date)
VALUES (%(etudid)s,%(date_debut)s,%(date_fin)s,%(etat)s,%(moduleimpl_id)s,%(desc)s,%(entry_date)s)
""",
{
"etudid": self.etudid,
"date_debut": date_deb,
"date_fin": date_fin,
"etat": EtatAssiduite.ABSENT,
"moduleimpl_id": self.moduleimpl,
"desc": self.raison,
"entry_date": self.entry_date,
},
)
return retour
# retour = Assiduite.fast_create_assiduite(
# etudid=self.etudid,
# date_debut=date_deb,
# date_fin=date_fin,
# etat=EtatAssiduite.ABSENT,
# moduleimpl_id=self.moduleimpl,
# description=self.raison,
# entry_date=self.entry_date,
# )
# return retour
def export(self):
"""Génère un nouvel objet Assiduité ou Justificatif"""
obj: Assiduite or Justificatif = None
if self.est_abs:
_glob.COMPTE[0] += 1
obj = self._to_assi()
self._to_assi()
else:
_glob.COMPTE[1] += 1
obj = self._to_justif()
db.session.add(obj)
self._to_justif()
class _Statistics:
@ -243,6 +273,10 @@ def migrate_abs_to_assiduites(
evening: list[str] = str(evening).split(":")
_glob.EVENING = time(int(evening[0]), int(evening[1]))
ndb.open_db_connection()
_glob.cnx = g.db_conn
_glob.cursor = _glob.cnx.cursor()
if dept is None:
prof_total = Profiler("MigrationTotal")
prof_total.start()
@ -287,7 +321,6 @@ def migrate_dept(dept_name: str, stats: _Statistics, time_elapsed: Profiler):
return
_glob.DEPT_ETUDIDS = {e.id for e in Identite.query.filter_by(dept_id=dept.id)}
_glob.MODULES = {}
_glob.COMPTE = [0, 0]
_glob.ERR_ETU = []
_glob.MERGER_ASSI = None
@ -299,9 +332,17 @@ def migrate_dept(dept_name: str, stats: _Statistics, time_elapsed: Profiler):
print_progress_bar(0, absences_len, "Progression", "effectué", autosize=True)
etuds_modimpl_ids = {}
for i, abs_ in enumerate(absences):
etud_modimpl_ids = etuds_modimpl_ids.get(abs_.etudid)
if etud_modimpl_ids is None:
etud_modimpl_ids = {
ins.moduleimpl_id
for ins in ModuleImplInscription.query.filter_by(etudid=abs_.etudid)
}
etuds_modimpl_ids[abs_.etudid] = etud_modimpl_ids
try:
_from_abs_to_assiduite_justificatif(abs_)
_from_abs_to_assiduite_justificatif(abs_, etud_modimpl_ids)
except ValueError as e:
stats.add_problem(abs_, e.args[0])
@ -322,14 +363,14 @@ def migrate_dept(dept_name: str, stats: _Statistics, time_elapsed: Profiler):
"effectué",
autosize=True,
)
db.session.commit()
_glob.cnx.commit()
if _glob.MERGER_ASSI is not None:
_glob.MERGER_ASSI.export()
if _glob.MERGER_JUST is not None:
_glob.MERGER_JUST.export()
db.session.commit()
_glob.cnx.commit()
print_progress_bar(
absences_len,
@ -379,24 +420,15 @@ def migrate_dept(dept_name: str, stats: _Statistics, time_elapsed: Profiler):
print(dumps(statistiques, indent=2))
def _from_abs_to_assiduite_justificatif(_abs: Absence):
def _from_abs_to_assiduite_justificatif(_abs: Absence, etud_modimpl_ids: set[int]):
if _abs.etudid not in _glob.DEPT_ETUDIDS:
raise ValueError("Etudiant inexistant")
if _abs.estabs:
moduleimpl_id: int = _abs.moduleimpl_id
if (
moduleimpl_id is not None
and (_abs.etudid, _abs.moduleimpl_id) not in _glob.MODULES
if (_abs.moduleimpl_id is not None) and (
_abs.moduleimpl_id not in etud_modimpl_ids
):
moduleimpl_inscription: ModuleImplInscription = (
ModuleImplInscription.query.filter_by(
moduleimpl_id=_abs.moduleimpl_id, etudid=_abs.etudid
).first()
)
if moduleimpl_inscription is None:
raise ValueError("Moduleimpl_id incorrect ou étudiant non inscrit")
_glob.MODULES[(_abs.etudid, _abs.moduleimpl_id)] = True
if _glob.MERGER_ASSI is None:
_glob.MERGER_ASSI = _Merger(_abs, True)