forked from ScoDoc/ScoDoc
Update opolka/ScoDoc from ScoDoc/ScoDoc #2
@ -187,7 +187,7 @@ def dept_etudiants(acronym: str):
|
||||
]
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return [etud.to_dict_short() for etud in dept.etats_civils]
|
||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/etudiants")
|
||||
@ -200,7 +200,7 @@ def dept_etudiants_by_id(dept_id: int):
|
||||
Retourne la liste des étudiants d'un département d'id donné.
|
||||
"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
return [etud.to_dict_short() for etud in dept.etats_civils]
|
||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/formsemestres_ids")
|
||||
|
@ -114,10 +114,13 @@ class EtudCursusBUT:
|
||||
validation_rcue: ApcValidationRCUE
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if (
|
||||
niveau is None
|
||||
or not niveau.competence.id in self.validation_par_competence_et_annee
|
||||
):
|
||||
if niveau is None:
|
||||
raise ScoValueError(
|
||||
"""UE d'un RCUE non associée à un niveau de compétence.
|
||||
Vérifiez la formation et les associations de ses UEs.
|
||||
"""
|
||||
)
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
|
@ -126,6 +126,7 @@ class AjoutAssiOrJustForm(FlaskForm):
|
||||
|
||||
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
||||
"Formulaire de saisie d'une assiduité pour un étudiant"
|
||||
|
||||
description = TextAreaField(
|
||||
"Description",
|
||||
render_kw={
|
||||
@ -152,6 +153,7 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
||||
|
||||
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
"Formulaire de saisie d'un justificatif pour un étudiant"
|
||||
|
||||
raison = TextAreaField(
|
||||
"Raison",
|
||||
render_kw={
|
||||
@ -176,6 +178,12 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
|
||||
|
||||
class ChoixDateForm(FlaskForm):
|
||||
"""
|
||||
Formulaire de choix de date
|
||||
(utilisé par la page de choix de date
|
||||
si la date courante n'est pas dans le semestre)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -5,7 +5,6 @@ from datetime import datetime
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.exc import DataError
|
||||
|
||||
from app import db, log, g, set_sco_dept
|
||||
from app.models import (
|
||||
@ -89,6 +88,8 @@ class Assiduite(ScoDocModel):
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
# Argument "restrict" obligatoire car on override la fonction "to_dict" de ScoDocModel
|
||||
# pylint: disable-next=unused-argument
|
||||
def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité
|
||||
restrict n'est pas utilisé ici.
|
||||
@ -307,6 +308,9 @@ class Assiduite(ScoDocModel):
|
||||
|
||||
def supprime(self):
|
||||
"Supprime l'assiduité. Log et commit."
|
||||
|
||||
# Obligatoire car import circulaire sinon
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from app.scodoc import sco_assiduites as scass
|
||||
|
||||
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
||||
@ -356,7 +360,7 @@ class Assiduite(ScoDocModel):
|
||||
|
||||
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
|
||||
utilisateur: str = ""
|
||||
if self.user != None:
|
||||
if self.user is not None:
|
||||
self.user: User
|
||||
utilisateur = f"par {self.user.get_prenomnom()}"
|
||||
|
||||
@ -515,6 +519,8 @@ class Justificatif(ScoDocModel):
|
||||
def create_justificatif(
|
||||
cls,
|
||||
etudiant: Identite,
|
||||
# On a besoin des arguments mais on utilise "locals" pour les récupérer
|
||||
# pylint: disable=unused-argument
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etat: EtatJustificatif,
|
||||
@ -538,8 +544,10 @@ class Justificatif(ScoDocModel):
|
||||
|
||||
def supprime(self):
|
||||
"Supprime le justificatif. Log et commit."
|
||||
|
||||
# Obligatoire car import circulaire sinon
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from app.scodoc import sco_assiduites as scass
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
|
||||
# Récupération de l'archive du justificatif
|
||||
archive_name: str = self.fichier
|
||||
|
@ -20,8 +20,11 @@ class Trace:
|
||||
|
||||
Role des fichiers traces :
|
||||
- Sauvegarder la date de dépôt du fichier
|
||||
- Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif)
|
||||
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView)
|
||||
- Sauvegarder la date de suppression du fichier
|
||||
(dans le cas de plusieurs fichiers pour un même justif)
|
||||
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier
|
||||
(=> permet de montrer les fichiers qu'aux personnes
|
||||
qui l'on déposé / qui ont le rôle AssiJustifView)
|
||||
|
||||
_trace.csv :
|
||||
nom_fichier_srv,datetime_depot,datetime_suppr,user_id
|
||||
|
@ -37,21 +37,34 @@ class CountCalculator:
|
||||
------------
|
||||
1. Initialisation : La classe peut être initialisée avec des horaires personnalisés
|
||||
pour le matin, le midi et le soir, ainsi qu'une durée de pause déjeuner.
|
||||
Si non spécifiés, les valeurs par défaut seront chargées depuis la configuration `ScoDocSiteConfig`.
|
||||
Si non spécifiés, les valeurs par défaut seront
|
||||
chargées depuis la configuration `ScoDocSiteConfig`.
|
||||
Exemple d'initialisation :
|
||||
calculator = CountCalculator(morning="08:00", noon="13:00", evening="18:00", nb_heures_par_jour=8)
|
||||
calculator = CountCalculator(
|
||||
morning="08:00",
|
||||
noon="13:00",
|
||||
evening="18:00",
|
||||
nb_heures_par_jour=8
|
||||
)
|
||||
|
||||
2. Ajout d'assiduités :
|
||||
Exemple d'ajout d'assiduité :
|
||||
- calculator.compute_assiduites(etudiant.assiduites)
|
||||
- calculator.compute_assiduites([<Assiduite>, <Assiduite>, <Assiduite>, <Assiduite>])
|
||||
- calculator.compute_assiduites([
|
||||
<Assiduite>,
|
||||
<Assiduite>,
|
||||
<Assiduite>,
|
||||
<Assiduite>
|
||||
])
|
||||
|
||||
3. Accès aux métriques : Après l'ajout des assiduités, on peut accéder aux métriques telles que :
|
||||
3. Accès aux métriques : Après l'ajout des assiduités,
|
||||
on peut accéder aux métriques telles que :
|
||||
le nombre total de jours, de demi-journées et d'heures calculées.
|
||||
Exemple d'accès aux métriques :
|
||||
metrics = calculator.to_dict()
|
||||
|
||||
4.Réinitialisation du comptage: Si besoin on peut réinitialisé le compteur sans perdre la configuration
|
||||
4.Réinitialisation du comptage: Si besoin on peut réinitialiser
|
||||
le compteur sans perdre la configuration
|
||||
(horaires personnalisés)
|
||||
Exemple de réinitialisation :
|
||||
calculator.reset()
|
||||
@ -61,8 +74,10 @@ class CountCalculator:
|
||||
- reset() : Réinitialise les compteurs de la classe.
|
||||
- add_half_day(day: date, is_morning: bool) : Ajoute une demi-journée au comptage.
|
||||
- add_day(day: date) : Ajoute un jour complet au comptage.
|
||||
- compute_long_assiduite(assi: Assiduite) : Traite les assiduités s'étendant sur plus d'un jour.
|
||||
- compute_assiduites(assiduites: Query | list) : Calcule les métriques pour une collection d'assiduités.
|
||||
- compute_long_assiduite(assi: Assiduite) : Traite les assiduités
|
||||
s'étendant sur plus d'un jour.
|
||||
- compute_assiduites(assiduites: Query | list) : Calcule les métriques pour
|
||||
une collection d'assiduités.
|
||||
- to_dict() : Retourne les métriques sous forme de dictionnaire.
|
||||
|
||||
Notes :
|
||||
@ -85,17 +100,14 @@ class CountCalculator:
|
||||
evening: str = None,
|
||||
nb_heures_par_jour: int = None,
|
||||
) -> None:
|
||||
# Transformation d'une heure "HH:MM" en time(h,m)
|
||||
STR_TIME = lambda x: time(*list(map(int, x.split(":"))))
|
||||
|
||||
self.morning: time = STR_TIME(
|
||||
self.morning: time = str_to_time(
|
||||
morning if morning else ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||
)
|
||||
# Date pivot pour déterminer les demi-journées
|
||||
self.noon: time = STR_TIME(
|
||||
self.noon: time = str_to_time(
|
||||
noon if noon else ScoDocSiteConfig.get("assi_lunch_time", "13:00")
|
||||
)
|
||||
self.evening: time = STR_TIME(
|
||||
self.evening: time = str_to_time(
|
||||
evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00")
|
||||
)
|
||||
|
||||
@ -103,10 +115,6 @@ class CountCalculator:
|
||||
scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
|
||||
delta_total: timedelta = datetime.combine(
|
||||
date.min, self.evening
|
||||
) - datetime.combine(date.min, self.morning)
|
||||
|
||||
# Sera utilisé pour les assiduités longues (> 1 journée)
|
||||
self.nb_heures_par_jour = (
|
||||
nb_heures_par_jour
|
||||
@ -340,17 +348,27 @@ class CountCalculator:
|
||||
|
||||
def setup_data(self):
|
||||
"""Met en forme les données
|
||||
pour les journées et les demi-journées : au lieu d'avoir list[str] on a le nombre (len(list[str]))
|
||||
pour les journées et les demi-journées :
|
||||
au lieu d'avoir list[str] on a le nombre (len(list[str]))
|
||||
"""
|
||||
for key in self.data:
|
||||
self.data[key]["journee"] = len(self.data[key]["journee"])
|
||||
self.data[key]["demi"] = len(self.data[key]["demi"])
|
||||
for value in self.data.values():
|
||||
value["journee"] = len(value["journee"])
|
||||
value["demi"] = len(value["demi"])
|
||||
|
||||
def to_dict(self, only_total: bool = True) -> dict[str, int | float]:
|
||||
"""Retourne les métriques sous la forme d'un dictionnaire"""
|
||||
return self.data["total"] if only_total else self.data
|
||||
|
||||
|
||||
def str_to_time(time_str: str) -> time:
|
||||
"""Convertit une chaîne de caractères représentant une heure en objet time
|
||||
exemples :
|
||||
- "08:00" -> time(8, 0)
|
||||
- "18:00:00" -> time(18, 0, 0)
|
||||
"""
|
||||
return time(*list(map(int, time_str.split(":"))))
|
||||
|
||||
|
||||
def get_assiduites_stats(
|
||||
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
||||
) -> dict[str, int | float]:
|
||||
@ -756,7 +774,6 @@ def invalidate_assiduites_etud_date(etudid: int, the_date: datetime):
|
||||
pour cet étudiant et cette date.
|
||||
Invalide cache absence et caches semestre
|
||||
"""
|
||||
from app.scodoc import sco_compute_moy
|
||||
|
||||
# Semestres a cette date:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||
@ -776,17 +793,9 @@ def invalidate_assiduites_etud_date(etudid: int, the_date: datetime):
|
||||
# Invalide les PDF et les absences:
|
||||
for sem in sems:
|
||||
# Inval cache bulletin et/ou note_table
|
||||
if sco_compute_moy.formsemestre_expressions_use_abscounts(
|
||||
sem["formsemestre_id"]
|
||||
):
|
||||
# certaines formules utilisent les absences
|
||||
pdfonly = False
|
||||
else:
|
||||
# efface toujours le PDF car il affiche en général les absences
|
||||
pdfonly = True
|
||||
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=sem["formsemestre_id"], pdfonly=pdfonly
|
||||
formsemestre_id=sem["formsemestre_id"], pdfonly=True
|
||||
)
|
||||
|
||||
# Inval cache compteurs absences:
|
||||
@ -818,4 +827,4 @@ def simple_invalidate_cache(obj: dict, etudid: str | int = None):
|
||||
pattern=f"tableau-etud-{etudid}*"
|
||||
)
|
||||
# Invalide les tableaux "bilan dept"
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern=f"tableau-dept*")
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern="tableau-dept*")
|
||||
|
@ -70,9 +70,9 @@ def evaluation_check_absences(evaluation: Evaluation):
|
||||
deb <= Assiduite.date_fin,
|
||||
)
|
||||
|
||||
abs_etudids = set(assi.etudid for assi in assiduites)
|
||||
abs_nj_etudids = set(assi.etudid for assi in assiduites if assi.est_just is False)
|
||||
just_etudids = set(assi.etudid for assi in assiduites if assi.est_just is True)
|
||||
abs_etudids = {assi.etudid for assi in assiduites}
|
||||
abs_nj_etudids = {assi.etudid for assi in assiduites if assi.est_just is False}
|
||||
just_etudids = {assi.etudid for assi in assiduites if assi.est_just is True}
|
||||
|
||||
# Les notes:
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
|
||||
|
@ -1337,21 +1337,12 @@ def do_formsemestre_clone(
|
||||
% (pname, pvalue, formsemestre_id)
|
||||
)
|
||||
|
||||
# 5- Copy formules utilisateur
|
||||
objs = sco_compute_moy.formsemestre_ue_computation_expr_list(
|
||||
cnx, args={"formsemestre_id": orig_formsemestre_id}
|
||||
)
|
||||
for obj in objs:
|
||||
args = obj.copy()
|
||||
args["formsemestre_id"] = formsemestre_id
|
||||
_ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
|
||||
|
||||
# 6- Copie les parcours
|
||||
# 5- Copie les parcours
|
||||
formsemestre.parcours = formsemestre_orig.parcours
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
|
||||
# 7- Copy partitions and groups
|
||||
# 6- Copy partitions and groups
|
||||
if clone_partitions:
|
||||
sco_groups_copy.clone_partitions_and_groups(
|
||||
orig_formsemestre_id, formsemestre_id
|
||||
|
@ -1216,7 +1216,7 @@ def formsemestre_tableau_modules(
|
||||
if expr:
|
||||
H.append(
|
||||
f""" <span class="formula" title="mode de calcul de la moyenne d'UE">{expr}</span>
|
||||
<span class="warning">formule inutilisée en 9.2: <a href="{
|
||||
<span class="warning">formule inutilisée en ScoDoc 9: <a href="{
|
||||
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ue_id=ue.id )
|
||||
}
|
||||
">supprimer</a></span>"""
|
||||
|
@ -358,11 +358,11 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
|
||||
if formsemestre_has_decisions(formsemestre_id):
|
||||
H.append(
|
||||
"""<ul class="tf-msg">
|
||||
<li class="tf-msg warning">Décisions de jury saisies: seul le ou la responsable du
|
||||
"""<div class="formsemestre-warning-box">
|
||||
<div class="warning">Décisions de jury saisies: seul le ou la responsable du
|
||||
semestre peut saisir des notes (elle devra modifier les décisions de jury).
|
||||
</li>
|
||||
</ul>"""
|
||||
</div>
|
||||
</div>"""
|
||||
)
|
||||
#
|
||||
H.append(
|
||||
|
@ -130,7 +130,8 @@ def print_progress_bar(
|
||||
decimals - Optional : nombres de chiffres après la virgule (Int)
|
||||
length - Optional : taille de la barre en nombre de caractères (Int)
|
||||
fill - Optional : charactère de remplissange de la barre (Str)
|
||||
autosize - Optional : Choisir automatiquement la taille de la barre en fonction du terminal (Bool)
|
||||
autosize - Optional : Choisir automatiquement la taille de la barre
|
||||
en fonction du terminal (Bool)
|
||||
"""
|
||||
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||
color = TerminalColor.RED
|
||||
@ -174,11 +175,15 @@ class BiDirectionalEnum(Enum):
|
||||
@classmethod
|
||||
def contains(cls, attr: str):
|
||||
"""Vérifie sur un attribut existe dans l'enum"""
|
||||
|
||||
# Existe dans la classe parent de Enum (EnumType)
|
||||
# pylint: disable-next=no-member
|
||||
return attr.upper() in cls._member_names_
|
||||
|
||||
@classmethod
|
||||
def all(cls, keys=True):
|
||||
"""Retourne toutes les clés de l'enum"""
|
||||
# pylint: disable-next=no-member
|
||||
return cls._member_names_ if keys else list(cls._value2member_map_.keys())
|
||||
|
||||
@classmethod
|
||||
@ -207,6 +212,9 @@ class EtatAssiduite(int, BiDirectionalEnum):
|
||||
ABSENT = 2
|
||||
|
||||
def version_lisible(self) -> str:
|
||||
"""Retourne une version lisible des états d'assiduités
|
||||
Est utilisé pour les vues.
|
||||
"""
|
||||
return {
|
||||
EtatAssiduite.PRESENT: "Présence",
|
||||
EtatAssiduite.ABSENT: "Absence",
|
||||
@ -225,6 +233,9 @@ class EtatJustificatif(int, BiDirectionalEnum):
|
||||
MODIFIE = 3
|
||||
|
||||
def version_lisible(self) -> str:
|
||||
"""Retourne une version lisible des états de justificatifs
|
||||
Est utilisé pour les vues.
|
||||
"""
|
||||
return {
|
||||
EtatJustificatif.VALIDE: "valide",
|
||||
EtatJustificatif.ATTENTE: "soumis",
|
||||
@ -254,11 +265,13 @@ class NonWorkDays(int, BiDirectionalEnum):
|
||||
cls, formsemestre_id: int = None, dept_id: int = None
|
||||
) -> list["NonWorkDays"]:
|
||||
"""
|
||||
get_all_non_work_days Récupère la liste des non workdays (str) depuis les préférences
|
||||
get_all_non_work_days Récupère la liste des non workdays
|
||||
(str) depuis les préférences
|
||||
puis renvoie une liste BiDirectionnalEnum<int> NonWorkDays
|
||||
|
||||
Example:
|
||||
non_work_days : list[NonWorkDays] = NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
non_work_days : list[NonWorkDays] =
|
||||
NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id)
|
||||
if datetime.datetime.now().weekday() in non_work_days:
|
||||
print("Aujourd'hui est un jour non travaillé")
|
||||
|
||||
@ -269,6 +282,8 @@ class NonWorkDays(int, BiDirectionalEnum):
|
||||
Returns:
|
||||
list[NonWorkDays]: La liste des NonWorkDays en version BiDirectionnalEnum<int>
|
||||
"""
|
||||
# Import circulaire
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
return [
|
||||
|
@ -3407,6 +3407,17 @@ li.tf-msg {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
div.formsemestre-warning-box {
|
||||
background-color: yellow;
|
||||
border-radius: 4px;
|
||||
margin-top: 12px;
|
||||
margin-left: 0px;
|
||||
padding-left: 0px;
|
||||
padding-right: 4px;
|
||||
padding-top: 2px;
|
||||
/* padding-bottom: 1px; */
|
||||
}
|
||||
|
||||
.warning, .warning-bloquant {
|
||||
color: red;
|
||||
margin-left: 16px;
|
||||
|
@ -1,3 +1,8 @@
|
||||
"""
|
||||
Gestion des listes d'assiduités et justificatifs
|
||||
(affichage, pagination, filtrage, options d'affichage, tableaux)
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import url_for
|
||||
@ -8,10 +13,18 @@ from sqlalchemy import desc, literal, union, asc
|
||||
from app import db, g
|
||||
from app.auth.models import User
|
||||
from app.models import Assiduite, Identite, Justificatif
|
||||
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
EtatJustificatif,
|
||||
to_bool,
|
||||
date_debut_annee_scolaire,
|
||||
date_fin_annee_scolaire,
|
||||
localize_datetime,
|
||||
)
|
||||
from app.tables import table_builder as tb
|
||||
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_preferences import get_preference
|
||||
|
||||
|
||||
class Pagination:
|
||||
@ -26,9 +39,11 @@ class Pagination:
|
||||
On peut ensuite récupérer les éléments de la page courante avec la méthode `items()`
|
||||
|
||||
Cette classe ne permet pas de changer de page.
|
||||
(Pour cela, il faut créer une nouvelle instance, avec la collection originelle et la nouvelle page)
|
||||
(Pour cela, il faut créer une nouvelle instance,
|
||||
avec la collection originelle et la nouvelle page)
|
||||
|
||||
l'intéret est de ne pas garder en mémoire toute la collection, mais seulement la page courante
|
||||
l'intéret est de ne pas garder en mémoire toute la collection,
|
||||
mais seulement la page courante
|
||||
|
||||
"""
|
||||
|
||||
@ -37,9 +52,11 @@ class Pagination:
|
||||
__init__ Instancie un nouvel objet Pagination
|
||||
|
||||
Args:
|
||||
collection (list): La collection à paginer. Il s'agit par exemple d'une requête
|
||||
collection (list): La collection à paginer.
|
||||
Il s'agit par exemple d'une requête
|
||||
page (int, optional): le numéro de la page à voir. Defaults to 1.
|
||||
per_page (int, optional): le nombre d'éléments par page. Defaults to -1. (-1 = pas de pagination/tout afficher)
|
||||
per_page (int, optional): le nombre d'éléments par page.
|
||||
Defaults to -1. (-1 = pas de pagination/tout afficher)
|
||||
"""
|
||||
# par défaut le total des pages est 1 (même si la collection est vide)
|
||||
self.total_pages = 1
|
||||
@ -195,6 +212,17 @@ class ListeAssiJusti(tb.Table):
|
||||
r = query_finale.all()
|
||||
RequeteTableauAssiduiteCache.set(cle_cache, r)
|
||||
|
||||
# Filtrer Si préférence "Limiter les assiduités à l'année courante"
|
||||
if get_preference("assi_limit_annee"):
|
||||
annee_debut = localize_datetime(date_debut_annee_scolaire())
|
||||
annee_fin = localize_datetime(date_fin_annee_scolaire())
|
||||
r = [
|
||||
obj
|
||||
for obj in r
|
||||
if obj._asdict()["date_debut"] >= annee_debut
|
||||
and obj._asdict()["date_fin"] <= annee_fin
|
||||
]
|
||||
|
||||
# Paginer la requête pour ne pas envoyer trop d'informations au client
|
||||
pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
|
||||
self.total_pages = pagination.total_pages
|
||||
@ -212,15 +240,17 @@ class ListeAssiJusti(tb.Table):
|
||||
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
||||
|
||||
Args:
|
||||
collection (list): La collection à paginer. Il s'agit par exemple d'une requête qui a déjà
|
||||
collection (list): La collection à paginer.
|
||||
Il s'agit par exemple d'une requête qui a déjà
|
||||
été construite et qui est prête à être exécutée.
|
||||
|
||||
Returns:
|
||||
Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée.
|
||||
Pagination: Un objet Pagination qui encapsule les résultats de
|
||||
la requête paginée.
|
||||
|
||||
Note:
|
||||
Cette méthode ne modifie pas la collection originelle; elle renvoie plutôt un nouvel
|
||||
objet qui contient les résultats paginés.
|
||||
Cette méthode ne modifie pas la collection originelle;
|
||||
elle renvoie plutôt un nouvel objet qui contient les résultats paginés.
|
||||
"""
|
||||
return Pagination(
|
||||
collection,
|
||||
@ -232,29 +262,35 @@ class ListeAssiJusti(tb.Table):
|
||||
"""
|
||||
Combine les requêtes d'assiduités et de justificatifs en une seule requête.
|
||||
|
||||
Cette fonction prend en entrée deux requêtes optionnelles, une pour les assiduités
|
||||
et une pour les justificatifs, et renvoie une requête combinée qui sélectionne
|
||||
un ensemble spécifique de colonnes pour chaque type d'objet.
|
||||
Cette fonction prend en entrée deux requêtes optionnelles,
|
||||
une pour les assiduités et une pour les justificatifs,
|
||||
et renvoie une requête combinée qui sélectionne un ensemble
|
||||
spécifique de colonnes pour chaque type d'objet.
|
||||
|
||||
Les colonnes sélectionnées sont:
|
||||
- obj_id: l'identifiant de l'objet (assiduite_id pour les assiduités, justif_id pour les justificatifs)
|
||||
- obj_id: l'identifiant de l'objet
|
||||
(assiduite_id pour les assiduités, justif_id pour les justificatifs)
|
||||
- etudid: l'identifiant de l'étudiant
|
||||
- entry_date: la date de saisie de l'objet
|
||||
- date_debut: la date de début de l'objet
|
||||
- date_fin: la date de fin de l'objet
|
||||
- etat: l'état de l'objet
|
||||
- type: le type de l'objet ("assiduite" pour les assiduités, "justificatif" pour les justificatifs)
|
||||
- type: le type de l'objet
|
||||
("assiduite" pour les assiduités, "justificatif" pour les justificatifs)
|
||||
- est_just : si l'assiduité est justifié (booléen) None pour les justificatifs
|
||||
- user_id : l'identifiant de l'utilisateur qui a signalé l'assiduité ou le justificatif
|
||||
- user_id : l'identifiant de l'utilisateur qui a
|
||||
signalé l'assiduité ou le justificatif
|
||||
|
||||
Args:
|
||||
query_assiduite (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||
pour les assiduités.
|
||||
Si None (default), aucune assiduité ne sera incluse dans la requête combinée.
|
||||
Si None (default), aucune assiduité ne sera incluse
|
||||
dans la requête combinée.
|
||||
|
||||
query_justificatif (sqlalchemy.orm.Query, optional): Une requête SQLAlchemy
|
||||
pour les justificatifs.
|
||||
Si None (default), aucun justificatif ne sera inclus dans la requête combinée.
|
||||
Si None (default), aucun justificatif ne sera
|
||||
inclus dans la requête combinée.
|
||||
|
||||
Returns:
|
||||
sqlalchemy.orm.Query: Une requête combinée qui peut être exécutée pour
|
||||
@ -599,10 +635,15 @@ class AssiFiltre:
|
||||
|
||||
Args:
|
||||
type_obj (int, optional): type d'objet (0:Tout, 1: Assi, 2:Justi). Defaults to 0.
|
||||
entry_date (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_debut (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_fin (tuple[int, datetime], optional): (0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
etats (list[int | EtatJustificatif | EtatAssiduite], optional): liste d'états valides (int | EtatJustificatif | EtatAssiduite). Defaults to None.
|
||||
entry_date (tuple[int, datetime], optional):
|
||||
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_debut (tuple[int, datetime], optional):
|
||||
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
date_fin (tuple[int, datetime], optional):
|
||||
(0: egal, 1: avant, 2: après) + datetime(avec TZ). Defaults to None.
|
||||
etats (list[int | EtatJustificatif | EtatAssiduite], optional):
|
||||
liste d'états valides (int | EtatJustificatif | EtatAssiduite).
|
||||
Defaults to None.
|
||||
"""
|
||||
|
||||
self.filtres = {"type_obj": type_obj}
|
||||
@ -637,7 +678,7 @@ class AssiFiltre:
|
||||
|
||||
type_filtrage, date = val_filtre
|
||||
|
||||
match (type_filtrage):
|
||||
match type_filtrage:
|
||||
# On garde uniquement les dates supérieures au filtre
|
||||
case 2:
|
||||
query_filtree = query_filtree.filter(
|
||||
@ -734,6 +775,10 @@ class AssiJustifData:
|
||||
|
||||
@staticmethod
|
||||
def from_etudiants(*etudiants: Identite) -> "AssiJustifData":
|
||||
"""
|
||||
Génère un object AssiJustifData à partir d'une liste d'étudiants
|
||||
(Récupère les assiduités et justificatifs des étudiants)
|
||||
"""
|
||||
data = AssiJustifData()
|
||||
data.assiduites_query = Assiduite.query.filter(
|
||||
Assiduite.etudid.in_([e.etudid for e in etudiants])
|
||||
@ -745,4 +790,5 @@ class AssiJustifData:
|
||||
return data
|
||||
|
||||
def get(self) -> tuple[Query, Query]:
|
||||
"Renvoi les requêtes d'assiduités et justificatifs"
|
||||
return self.assiduites_query, self.justificatifs_query
|
||||
|
@ -37,7 +37,7 @@ class TableAssi(tb.Table):
|
||||
convert_values=False,
|
||||
**kwargs,
|
||||
):
|
||||
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
|
||||
classes = ["gt_table"]
|
||||
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"]
|
||||
self.formsemestre = formsemestre
|
||||
|
@ -3,9 +3,9 @@
|
||||
Calendrier de l'assiduité
|
||||
{% endblock title %}
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block app_content %}
|
||||
@ -15,9 +15,12 @@ Calendrier de l'assiduité
|
||||
<h2>Assiduité de {{sco.etud.html_link_fiche()|safe}}</h2>
|
||||
|
||||
<div class="options">
|
||||
<input type="checkbox" id="show_pres" name="show_pres" class="memo" {{'checked' if show_pres else ''}}><label for="show_pres">afficher les présences</label>
|
||||
<input type="checkbox" name="show_reta" id="show_reta" class="memo" {{'checked' if show_reta else ''}}><label for="show_reta">afficher les retards</label>
|
||||
<input type="checkbox" name="mode_demi" id="mode_demi" class="memo" {{'checked' if mode_demi else ''}}><label for="mode_demi">mode demi journée</label>
|
||||
<input type="checkbox" id="show_pres" name="show_pres" class="memo" {{'checked' if show_pres else '' }}><label
|
||||
for="show_pres">afficher les présences</label>
|
||||
<input type="checkbox" name="show_reta" id="show_reta" class="memo" {{'checked' if show_reta else '' }}><label
|
||||
for="show_reta">afficher les retards</label>
|
||||
<input type="checkbox" name="mode_demi" id="mode_demi" class="memo" {{'checked' if mode_demi else '' }}><label
|
||||
for="mode_demi">mode demi journée</label>
|
||||
</div>
|
||||
|
||||
<div class="calendrier">
|
||||
@ -64,7 +67,7 @@ Calendrier de l'assiduité
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="annee">
|
||||
<span id="label-annee">Année scolaire 2022-2023</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||
année: </span>
|
||||
<select name="" id="annee">
|
||||
</select>
|
||||
@ -76,15 +79,18 @@ Calendrier de l'assiduité
|
||||
<h3>Calendrier</h3>
|
||||
<p>Code couleur</p>
|
||||
<ul class="couleurs">
|
||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la période
|
||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||
</li>
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la période
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
||||
</li>
|
||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la période
|
||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||
</li>
|
||||
@ -120,9 +126,9 @@ Calendrier de l'assiduité
|
||||
<li><span title="Quart Violet" class="invalide demo"></span> justificatif non valide
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style>
|
||||
.help .couleurs {
|
||||
grid-template-columns: 2;
|
||||
grid-template-rows: auto;
|
||||
@ -154,6 +160,7 @@ Calendrier de l'assiduité
|
||||
.color.absent.est_just {
|
||||
background-color: var(--color-absent-justi) !important;
|
||||
}
|
||||
|
||||
.color.retard {
|
||||
background-color: var(--color-retard) !important;
|
||||
}
|
||||
@ -218,31 +225,31 @@ Calendrier de l'assiduité
|
||||
right: 0;
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
.color.attente::before, .color.modifie::before {
|
||||
|
||||
.color.attente::before,
|
||||
.color.modifie::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
var(--color-justi-attente) 4px,
|
||||
var(--color-justi-attente) 7px
|
||||
)!important;
|
||||
var(--color-justi-attente) 7px) !important;
|
||||
}
|
||||
|
||||
.demo.invalide {
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
|
||||
.demo.attente {
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
var(--color-justi-attente) 4px,
|
||||
var(--color-justi-attente) 7px
|
||||
)!important;
|
||||
var(--color-justi-attente) 7px) !important;
|
||||
}
|
||||
|
||||
.demo.est_just {
|
||||
@ -333,9 +340,9 @@ Calendrier de l'assiduité
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
function getOptions() {
|
||||
return {
|
||||
"show_pres": document.getElementById("show_pres").checked,
|
||||
@ -345,7 +352,7 @@ Calendrier de l'assiduité
|
||||
}
|
||||
|
||||
|
||||
function updatePage(){
|
||||
function updatePage() {
|
||||
const url = new URL(location.href);
|
||||
const options = getOptions();
|
||||
url.searchParams.set("annee", document.getElementById('annee').value);
|
||||
@ -353,12 +360,12 @@ Calendrier de l'assiduité
|
||||
url.searchParams.set("show_pres", options.show_pres);
|
||||
url.searchParams.set("show_reta", options.show_reta);
|
||||
|
||||
if (location.href != url.href){
|
||||
if (location.href != url.href) {
|
||||
location.href = url.href
|
||||
}
|
||||
}
|
||||
|
||||
const defAnnee = {{ annee }}
|
||||
const defAnnee = "{{ annee | safe}}"
|
||||
let annees = {{ annees | safe }}
|
||||
annees = annees.filter((x, i) => annees.indexOf(x) === i)
|
||||
const etudid = {{ sco.etud.id }};
|
||||
@ -366,28 +373,30 @@ Calendrier de l'assiduité
|
||||
const select = document.querySelector('#annee');
|
||||
annees.forEach((a) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = a + "",
|
||||
opt.textContent = `${a} - ${a + 1}`;
|
||||
if (a === defAnnee) {
|
||||
let a_1 = a.substring(0, 4)
|
||||
opt.value = a_1 + "",
|
||||
opt.textContent = a
|
||||
if (a_1 === defAnnee) {
|
||||
opt.selected = true;
|
||||
document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}-${a + 1}`
|
||||
document.querySelector('.annee #label-annee').textContent = `Année scolaire ${a}`
|
||||
|
||||
}
|
||||
select.appendChild(opt)
|
||||
})
|
||||
|
||||
document.querySelectorAll('input[type="checkbox"].memo, #annee').forEach(el => {
|
||||
el.addEventListener('change', function() {
|
||||
el.addEventListener('change', function () {
|
||||
updatePage();
|
||||
})});
|
||||
})
|
||||
});
|
||||
|
||||
document.querySelectorAll('[assi_id]').forEach((el,i) => {
|
||||
el.addEventListener('click', ()=>{
|
||||
document.querySelectorAll('[assi_id]').forEach((el, i) => {
|
||||
el.addEventListener('click', () => {
|
||||
const assi_id = el.getAttribute('assi_id');
|
||||
window.open(`${SCO_URL}/Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock app_content %}
|
||||
</script>
|
||||
{% endblock app_content %}
|
@ -25,8 +25,10 @@
|
||||
##############################################################################
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from flask import g, request, render_template, flash
|
||||
from flask import abort, url_for, redirect, Response
|
||||
@ -121,7 +123,6 @@ def bilan_dept():
|
||||
if formsemestre_id:
|
||||
try:
|
||||
formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
annee = formsemestre.annee_scolaire()
|
||||
except AttributeError:
|
||||
formsemestre_id = ""
|
||||
|
||||
@ -230,17 +231,22 @@ def ajout_assiduite_etud() -> str | Response:
|
||||
# On dresse la liste des modules de l'année scolaire en cours
|
||||
# auxquels est inscrit l'étudiant pour peupler le menu "module"
|
||||
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
||||
choices = {
|
||||
"": [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
|
||||
}
|
||||
choices: OrderedDict = OrderedDict()
|
||||
choices[""] = [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")]
|
||||
for formsemestre_id in modimpls_by_formsemestre:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
|
||||
# indique le nom du semestre dans le menu (optgroup)
|
||||
choices[formsemestre.titre_annee()] = [
|
||||
group_name: str = formsemestre.titre_annee()
|
||||
choices[group_name] = [
|
||||
(m.id, f"{m.module.code} {m.module.abbrev or m.module.titre or ''}")
|
||||
for m in modimpls_by_formsemestre[formsemestre_id]
|
||||
if m.module.ue.type == UE_STANDARD
|
||||
]
|
||||
|
||||
if formsemestre.est_courant():
|
||||
choices.move_to_end(group_name, last=False)
|
||||
choices.move_to_end("", last=False)
|
||||
form.modimpl.choices = choices
|
||||
|
||||
if form.validate_on_submit():
|
||||
@ -825,17 +831,19 @@ def calendrier_assi_etud():
|
||||
# Récupération des années d'étude de l'étudiant
|
||||
annees: list[int] = []
|
||||
for ins in etud.formsemestre_inscriptions:
|
||||
date_deb = ins.formsemestre.date_debut
|
||||
date_fin = ins.formsemestre.date_fin
|
||||
annees.extend(
|
||||
(ins.formsemestre.date_debut.year, ins.formsemestre.date_fin.year)
|
||||
[
|
||||
scu.annee_scolaire_repr(date_deb.year, date_deb.month),
|
||||
scu.annee_scolaire_repr(date_fin.year, date_fin.month),
|
||||
]
|
||||
)
|
||||
annees = sorted(annees, reverse=True)
|
||||
|
||||
# Transformation en une liste "json"
|
||||
# (sera utilisé pour générer le selecteur d'année)
|
||||
annees_str: str = "["
|
||||
for ann in annees:
|
||||
annees_str += f"{ann},"
|
||||
annees_str += "]"
|
||||
annees_str: str = json.dumps(annees)
|
||||
|
||||
calendrier: dict[str, list["Jour"]] = generate_calendar(etud, annee)
|
||||
|
||||
@ -857,6 +865,15 @@ def calendrier_assi_etud():
|
||||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
def choix_date() -> str:
|
||||
"""
|
||||
choix_date Choix de la date pour la saisie des assiduités
|
||||
|
||||
Route utilisée uniquement si la date courante n'est pas dans le semestre
|
||||
concerné par la requête vers une des pages suivantes :
|
||||
- saisie_assiduites_group
|
||||
- visu_assiduites_group
|
||||
|
||||
"""
|
||||
formsemestre_id = request.args.get("formsemestre_id")
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
|
||||
@ -973,9 +990,6 @@ def signal_assiduites_group():
|
||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "groupes inexistants dans ce département")
|
||||
|
||||
# Vérification du forçage du module
|
||||
require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)
|
||||
|
||||
# Récupération des étudiants des groupes
|
||||
etuds = [
|
||||
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||
@ -1107,9 +1121,6 @@ def visu_assiduites_group():
|
||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "groupes inexistants dans ce département")
|
||||
|
||||
# Vérfication du forçage du module
|
||||
require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)
|
||||
|
||||
# Récupération des étudiants du/des groupe(s)
|
||||
etuds = [
|
||||
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||
@ -1781,22 +1792,6 @@ def signal_assiduites_diff():
|
||||
)
|
||||
etudiants = list(sorted(etudiants, key=lambda etud: etud.sort_key))
|
||||
|
||||
# Génération de l'HTML
|
||||
|
||||
header: str = html_sco_header.sco_header(
|
||||
page_title="Assiduité: saisie différée",
|
||||
init_qtip=True,
|
||||
cssstyles=[
|
||||
"css/assiduites.css",
|
||||
],
|
||||
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
|
||||
+ [
|
||||
"js/assiduites.js",
|
||||
"js/date_utils.js",
|
||||
"js/etud_info.js",
|
||||
],
|
||||
)
|
||||
|
||||
if groups_infos.tous_les_etuds_du_sem:
|
||||
gr_tit = "en"
|
||||
else:
|
||||
@ -2075,8 +2070,8 @@ def _differee(
|
||||
etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires)
|
||||
moduleimpl_select (str): l'html représentant le selecteur de module
|
||||
date (str, optional): la première date à afficher. Defaults to None.
|
||||
periode (dict[str, str], optional):La période par défaut de la première colonne. Defaults to None.
|
||||
formsemestre_id (int, optional): l'id du semestre pour le selecteur de module. Defaults to None.
|
||||
periode (dict[str, str], optional):La période par défaut de la première colonne.
|
||||
formsemestre_id (int, optional): l'id du semestre pour le selecteur de module.
|
||||
|
||||
Returns:
|
||||
str: le widget (html/css/js)
|
||||
@ -2162,7 +2157,7 @@ def _module_selector_multiple(
|
||||
Prend les semestres de l'année, sauf si only_form est indiqué.
|
||||
"""
|
||||
modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire())
|
||||
choices = {}
|
||||
choices = OrderedDict()
|
||||
for formsemestre_id in modimpls_by_formsemestre:
|
||||
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
|
||||
if only_form is not None and formsemestre != only_form:
|
||||
@ -2177,6 +2172,9 @@ def _module_selector_multiple(
|
||||
if m.module.ue.type == UE_STANDARD
|
||||
]
|
||||
|
||||
if formsemestre.est_courant():
|
||||
choices.move_to_end(formsemestre.titre_annee(), last=False)
|
||||
|
||||
return render_template(
|
||||
"assiduites/widgets/moduleimpl_selector_multiple.j2",
|
||||
choices=choices,
|
||||
@ -2262,6 +2260,9 @@ def generate_calendar(
|
||||
etudiant: Identite,
|
||||
annee: int = None,
|
||||
) -> dict[str, list["Jour"]]:
|
||||
"""
|
||||
Génère le calendrier d'assiduité de l'étudiant pour une année scolaire donnée
|
||||
"""
|
||||
# Si pas d'année alors on prend l'année scolaire en cours
|
||||
if annee is None:
|
||||
annee = scu.annee_scolaire()
|
||||
@ -2309,13 +2310,26 @@ class Jour:
|
||||
self.justificatifs = justificatifs
|
||||
|
||||
def get_nom(self, mode_demi: bool = True) -> str:
|
||||
"""
|
||||
Renvoie le nom du jour
|
||||
"M19" ou "Mer 19"
|
||||
"""
|
||||
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
|
||||
return f"{str_jour[0] if mode_demi or self.is_non_work() else str_jour[:3]+' '}{self.date.day}"
|
||||
return (
|
||||
f"{str_jour[0] if mode_demi or self.is_non_work() else str_jour[:3]+' '}"
|
||||
+ f"{self.date.day}"
|
||||
)
|
||||
|
||||
def get_date(self) -> str:
|
||||
"""
|
||||
Renvoie la date du jour au format "dd/mm/yyyy"
|
||||
"""
|
||||
return self.date.strftime("%d/%m/%Y")
|
||||
|
||||
def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str:
|
||||
"""
|
||||
Retourne la classe css du jour (mode normal)
|
||||
"""
|
||||
etat = ""
|
||||
est_just = ""
|
||||
|
||||
@ -2337,13 +2351,19 @@ class Jour:
|
||||
def get_demi_class(
|
||||
self, matin: bool, show_pres: bool = False, show_reta: bool = False
|
||||
) -> str:
|
||||
# Transformation d'une heure "HH:MM" en time(h,m)
|
||||
str2time = lambda x: datetime.time(*list(map(int, x.split(":"))))
|
||||
"""
|
||||
Renvoie la class css de la demi journée
|
||||
"""
|
||||
|
||||
heure_midi = str2time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
|
||||
heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
|
||||
|
||||
if matin:
|
||||
heure_matin = str2time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
|
||||
heure_matin = scass.str_to_time(
|
||||
ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||
)
|
||||
log(
|
||||
f'{ScoDocSiteConfig.get("assi_morning_time", "08:00")=}{heure_matin=} {type(heure_matin)=}'
|
||||
)
|
||||
matin = (
|
||||
# date debut
|
||||
scu.localize_datetime(
|
||||
@ -2355,12 +2375,16 @@ class Jour:
|
||||
assiduites_matin = [
|
||||
assi
|
||||
for assi in self.assiduites
|
||||
if scu.is_period_overlapping((assi.date_debut, assi.date_fin), matin)
|
||||
if scu.is_period_overlapping(
|
||||
(assi.date_debut, assi.date_fin), matin, bornes=False
|
||||
)
|
||||
]
|
||||
justificatifs_matin = [
|
||||
justi
|
||||
for justi in self.justificatifs
|
||||
if scu.is_period_overlapping((justi.date_debut, justi.date_fin), matin)
|
||||
if scu.is_period_overlapping(
|
||||
(justi.date_debut, justi.date_fin), matin, bornes=False
|
||||
)
|
||||
]
|
||||
|
||||
etat = self._get_color_assiduites_cascade(
|
||||
@ -2375,7 +2399,9 @@ class Jour:
|
||||
|
||||
return f"color {etat} {est_just}"
|
||||
|
||||
heure_soir = str2time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00"))
|
||||
heure_soir = scass.str_to_time(
|
||||
ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
|
||||
)
|
||||
|
||||
# séparation en demi journées
|
||||
aprem = (
|
||||
@ -2388,13 +2414,17 @@ class Jour:
|
||||
assiduites_aprem = [
|
||||
assi
|
||||
for assi in self.assiduites
|
||||
if scu.is_period_overlapping((assi.date_debut, assi.date_fin), aprem)
|
||||
if scu.is_period_overlapping(
|
||||
(assi.date_debut, assi.date_fin), aprem, bornes=False
|
||||
)
|
||||
]
|
||||
|
||||
justificatifs_aprem = [
|
||||
justi
|
||||
for justi in self.justificatifs
|
||||
if scu.is_period_overlapping((justi.date_debut, justi.date_fin), aprem)
|
||||
if scu.is_period_overlapping(
|
||||
(justi.date_debut, justi.date_fin), aprem, bornes=False
|
||||
)
|
||||
]
|
||||
|
||||
etat = self._get_color_assiduites_cascade(
|
||||
@ -2410,22 +2440,21 @@ class Jour:
|
||||
return f"color {etat} {est_just}"
|
||||
|
||||
def has_assiduites(self) -> bool:
|
||||
"""
|
||||
Renverra True si le jour a des assiduités
|
||||
"""
|
||||
return self.assiduites.count() > 0
|
||||
|
||||
def generate_minitimeline(self) -> str:
|
||||
"""
|
||||
Génère la minitimeline du jour
|
||||
"""
|
||||
# Récupérer le référenciel de la timeline
|
||||
str2time = lambda x: _time_to_timedelta(
|
||||
datetime.time(*list(map(int, x.split(":"))))
|
||||
heure_matin: datetime.timedelta = _time_to_timedelta(
|
||||
scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
|
||||
)
|
||||
|
||||
heure_matin: datetime.timedelta = str2time(
|
||||
ScoDocSiteConfig.get("assi_morning_time", "08:00")
|
||||
)
|
||||
heure_midi: datetime.timedelta = str2time(
|
||||
ScoDocSiteConfig.get("assi_lun_time", "13:00")
|
||||
)
|
||||
heure_soir: datetime.timedelta = str2time(
|
||||
ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
|
||||
heure_soir: datetime.timedelta = _time_to_timedelta(
|
||||
scass.str_to_time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00"))
|
||||
)
|
||||
# longueur_timeline = heure_soir - heure_matin
|
||||
longueur_timeline: datetime.timedelta = heure_soir - heure_matin
|
||||
@ -2433,6 +2462,7 @@ class Jour:
|
||||
# chaque block d'assiduité est défini par:
|
||||
# longueur = ( (fin-deb) / longueur_timeline ) * 100
|
||||
# emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100
|
||||
# longueur + emplacement = 100% sinon on réduit longueur
|
||||
|
||||
assiduite_blocks: list[dict[str, float | str]] = []
|
||||
|
||||
@ -2448,8 +2478,10 @@ class Jour:
|
||||
else heure_soir
|
||||
)
|
||||
|
||||
longueur: float = ((fin - deb) / longueur_timeline) * 100
|
||||
emplacement: float = ((deb - heure_matin) / longueur_timeline) * 100
|
||||
longueur: float = ((fin - deb) / longueur_timeline) * 100
|
||||
if longueur + emplacement > 100:
|
||||
longueur = 100 - emplacement
|
||||
etat: str = scu.EtatAssiduite(assi.etat).name.lower()
|
||||
est_just: str = "est_just" if assi.est_just else ""
|
||||
|
||||
@ -2470,17 +2502,21 @@ class Jour:
|
||||
)
|
||||
|
||||
def is_non_work(self):
|
||||
"""
|
||||
Renvoie True si le jour est un jour non travaillé
|
||||
(en fonction de la préférence du département)
|
||||
"""
|
||||
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
|
||||
def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]:
|
||||
return list(set([scu.EtatAssiduite(assi.etat) for assi in assiduites]))
|
||||
return list(set(scu.EtatAssiduite(assi.etat) for assi in assiduites))
|
||||
|
||||
def _get_etats_from_justificatifs(
|
||||
self, justificatifs: Query
|
||||
) -> list[scu.EtatJustificatif]:
|
||||
return list(set([scu.EtatJustificatif(justi.etat) for justi in justificatifs]))
|
||||
return list(set(scu.EtatJustificatif(justi.etat) for justi in justificatifs))
|
||||
|
||||
def _get_color_assiduites_cascade(
|
||||
self,
|
||||
|
@ -61,11 +61,11 @@ class DevConfig(Config):
|
||||
DEBUG = True
|
||||
TESTING = False
|
||||
SQLALCHEMY_DATABASE_URI = (
|
||||
os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC"
|
||||
os.environ.get("SCODOC_DATABASE_URI") or "postgresql:///SCODOC_DEV"
|
||||
)
|
||||
SECRET_KEY = os.environ.get("DEV_SECRET_KEY") or "bb3faec7d9a34eb68a8e3e710087d87a"
|
||||
# pour le avoir url_for dans le shell:
|
||||
SERVER_NAME = "http://localhost:8080"
|
||||
# SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "localhost"
|
||||
|
||||
|
||||
class TestConfig(DevConfig):
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.946"
|
||||
SCOVERSION = "9.6.948"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
||||
#
|
||||
# E. Viennet, Fev 2023
|
||||
|
||||
cd /opt/scodoc
|
||||
cd /opt/scodoc || exit 1
|
||||
# suppose que le virtual env est bien configuré
|
||||
|
||||
# Utilise un port spécifique pour pouvoir lancer ce test sans couper
|
||||
|
Loading…
Reference in New Issue
Block a user