Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
7042650fd9 | |||
2745ffd687 | |||
9a882ea41d | |||
ea6003e812 | |||
5c6935337e | |||
60998d2e20 | |||
29b877d9ed | |||
|
6834c19015 | ||
|
f47fc4ba46 | ||
5894c6f952 | |||
af1d1884c7 | |||
|
881bf82000 | ||
|
2ed4516a97 | ||
|
75ce1ccd31 | ||
|
f8d5f6ea11 | ||
|
70995fbd7e | ||
dc095765f2 | |||
|
1cec3fa703 | ||
|
032454aefd | ||
|
e3344cf424 | ||
|
d7acff9d35 | ||
|
decdf59e20 | ||
|
42fc08a3a3 | ||
|
f3770fb5c7 | ||
63b28a3277 | |||
bb23cdcea7 | |||
3ca5636454 | |||
42882154d5 | |||
489acb26d2 | |||
8ee373db7d | |||
8e56dc2418 | |||
b3331bd886 | |||
89afb672af | |||
8f25284038 | |||
f29002a57d | |||
69780b3f24 | |||
fbff151be0 | |||
3b436fa0f3 | |||
8847a1f008 | |||
ac882e9ccd | |||
000e016985 | |||
22d90215a0 | |||
043985bff6 | |||
d20ada1797 | |||
|
778fecabb6 | ||
|
fa6f83722e | ||
baa0412071 | |||
d51a47b71a | |||
f21ef41de6 | |||
2d673e7a5d | |||
3e43495831 | |||
|
a4db8c4ff8 | ||
|
1ac35d04c2 | ||
|
687ac3cf13 | ||
18b1f00586 | |||
|
6b985620e9 | ||
|
4d234ba353 | ||
|
5d45fcf656 | ||
|
0a5919b788 | ||
|
09f4525e66 | ||
0bc57807de |
152
README.md
152
README.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
|
||||
|
||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
|
||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian12>
|
||||
|
||||
Documentation utilisateur: <https://scodoc.org>
|
||||
|
||||
|
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
|||
|
||||
### Lignes de commandes
|
||||
|
||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||
Voir [le guide de configuration](https://scodoc.org/GuideConfig).
|
||||
|
||||
## Organisation des fichiers
|
||||
|
||||
|
@ -41,45 +41,41 @@ Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configu
|
|||
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||
|
||||
Principaux contenus:
|
||||
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
|
||||
```
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
```
|
||||
## Pour les développeurs
|
||||
|
||||
### Installation du code
|
||||
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian12)).
|
||||
|
||||
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||
```bash
|
||||
sudo su
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
apt-get install git # si besoin
|
||||
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
|
||||
sudo su
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
apt-get install git # si besoin
|
||||
cd /opt
|
||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
|
||||
# Renommer le répertoire:
|
||||
mv ScoDoc scodoc
|
||||
|
||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc.scodoc /opt/scodoc
|
||||
|
||||
# Donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc:scodoc /opt/scodoc
|
||||
```
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
|
||||
```bash
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
```
|
||||
Et la config:
|
||||
|
||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||
|
||||
```bash
|
||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||
```
|
||||
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
|
||||
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
||||
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||
|
@ -88,11 +84,11 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
|||
|
||||
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||
Avant le premier lancement, créer cette base ainsi:
|
||||
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
|
||||
```bash
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
```
|
||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||
migrations (changements de schéma) ont eu lieu dans le code.
|
||||
|
@ -100,17 +96,17 @@ migrations (changements de schéma) ont eu lieu dans le code.
|
|||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
|
||||
```bash
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
```
|
||||
Puis dérouler les tests unitaires:
|
||||
|
||||
pytest tests/unit
|
||||
|
||||
```bash
|
||||
pytest tests/unit
|
||||
```
|
||||
Ou avec couverture (`pip install pytest-cov`)
|
||||
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
|
||||
```bash
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
```
|
||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
||||
|
||||
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||
|
@ -119,43 +115,43 @@ développement dans un état connu, par exemple pour éviter de recréer à la m
|
|||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||
par les tests:
|
||||
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
|
||||
```bash
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
```
|
||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||
normalement, par exemple:
|
||||
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
|
||||
```bash
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
```
|
||||
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||
utilisateur:
|
||||
|
||||
flask user-password admin
|
||||
|
||||
```bash
|
||||
flask user-password admin
|
||||
```
|
||||
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
||||
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
||||
|
||||
#### Modification du schéma de la base
|
||||
|
||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
|
||||
```bash
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
```
|
||||
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
||||
|
||||
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
||||
ou variables d'environnement pour interroger la bonne base !).
|
||||
```bash
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
flask db upgrade # créé les tables à partir des migrations
|
||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
flask db upgrade # créé les tables à partir des migrations
|
||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
```
|
||||
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
||||
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||
positionner à la bonne étape.
|
||||
|
@ -163,23 +159,23 @@ positionner à la bonne étape.
|
|||
### Profiling
|
||||
|
||||
Sur une machine de DEV, lancer
|
||||
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
|
||||
```bash
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
```
|
||||
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
||||
|
||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||
|
||||
pip install snakeviz
|
||||
|
||||
```bash
|
||||
pip install snakeviz
|
||||
```
|
||||
puis
|
||||
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
|
||||
```bash
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
```
|
||||
## Paquet Debian 12
|
||||
|
||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||
important est `postinst`qui se charge de configurer le système (install ou
|
||||
important est `postinst` qui se charge de configurer le système (install ou
|
||||
upgrade de scodoc9).
|
||||
|
||||
La préparation d'une release se fait à l'aide du script
|
||||
|
|
|
@ -637,14 +637,12 @@ def critical_error(msg):
|
|||
import app.scodoc.sco_utils as scu
|
||||
|
||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||
subject = f"CRITICAL ERROR: {msg}".strip()[:68]
|
||||
send_scodoc_alarm(subject, msg)
|
||||
clear_scodoc_cache()
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Une erreur est survenue.
|
||||
|
||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||
{scu.SCO_DISCORD_ASSISTANCE}
|
||||
Une erreur est survenue, veuillez ré-essayer.
|
||||
|
||||
{msg}
|
||||
"""
|
||||
|
|
|
@ -3,14 +3,15 @@
|
|||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
"""ScoDoc 9 API : Assiduités"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
|
||||
from app import db, log, set_sco_dept
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
|
@ -858,7 +859,10 @@ def assiduite_edit(assiduite_id: int):
|
|||
msg=f"assiduite: modif {assiduite_unique}",
|
||||
)
|
||||
db.session.commit()
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
try:
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
except ObjectDeletedError:
|
||||
return json_error(404, "Assiduité supprimée / inexistante")
|
||||
|
||||
return {"OK": True}
|
||||
|
||||
|
|
|
@ -124,7 +124,9 @@ def _build_bulletin_but_infos(
|
|||
formsemestre, bulletins_sem.res
|
||||
)
|
||||
if warn_html:
|
||||
raise ScoValueError("<b>Formation mal configurée pour le BUT</b>" + warn_html)
|
||||
raise ScoValueError(
|
||||
"<b>Formation mal configurée pour le BUT</b>" + warn_html, safe=True
|
||||
)
|
||||
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
||||
refcomp, etud
|
||||
)
|
||||
|
|
|
@ -73,6 +73,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="bul-table",
|
||||
)
|
||||
table_objects = table.gen(fmt=fmt)
|
||||
objects += table_objects
|
||||
|
@ -427,12 +428,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
||||
else "*"
|
||||
)
|
||||
note_value = e["note"].get("value", "")
|
||||
t = {
|
||||
"titre": f"{e['description'] or ''}",
|
||||
"moyenne": e["note"]["value"],
|
||||
"_moyenne_pdf": Paragraph(
|
||||
f"""<para align=right>{e["note"]["value"]}</para>"""
|
||||
),
|
||||
"moyenne": note_value,
|
||||
"_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
|
||||
"coef": coef,
|
||||
"_coef_pdf": Paragraph(
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
"""caches pour tables APC
|
||||
"""
|
||||
from flask import g
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
|
@ -47,3 +48,27 @@ class EvaluationsPoidsCache(sco_cache.ScoDocCache):
|
|||
"""
|
||||
|
||||
prefix = "EPC"
|
||||
|
||||
@classmethod
|
||||
def invalidate_all(cls):
|
||||
"delete all cached evaluations poids (in current dept)"
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
moduleimpl_ids = [
|
||||
mi.id
|
||||
for mi in ModuleImpl.query.join(FormSemestre).filter_by(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
]
|
||||
cls.delete_many(moduleimpl_ids)
|
||||
|
||||
@classmethod
|
||||
def invalidate_sem(cls, formsemestre_id):
|
||||
"delete cached evaluations poids for this formsemestre from cache"
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
moduleimpl_ids = [
|
||||
mi.id for mi in ModuleImpl.query.filter_by(formsemestre_id=formsemestre_id)
|
||||
]
|
||||
cls.delete_many(moduleimpl_ids)
|
||||
|
|
|
@ -45,7 +45,6 @@ from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
|||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
|
@ -113,6 +112,8 @@ class ModuleImplResults:
|
|||
"""
|
||||
self.evals_etudids_sans_note = {}
|
||||
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
|
||||
self.evals_type = {}
|
||||
"""Type de chaque eval { evaluation.id : evaluation.evaluation_type }"""
|
||||
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"""
|
||||
|
@ -164,7 +165,10 @@ class ModuleImplResults:
|
|||
self.evaluations_completes = []
|
||||
self.evaluations_completes_dict = {}
|
||||
self.etudids_attente = set() # empty
|
||||
self.evals_type = {}
|
||||
evaluation: Evaluation
|
||||
for evaluation in moduleimpl.evaluations:
|
||||
self.evals_type[evaluation.id] = evaluation.evaluation_type
|
||||
eval_df = self._load_evaluation_notes(evaluation)
|
||||
# is_complete ssi
|
||||
# tous les inscrits (non dem) au module ont une note
|
||||
|
@ -270,6 +274,24 @@ class ModuleImplResults:
|
|||
* self.evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
|
||||
def get_evaluations_special_coefs(
|
||||
self, modimpl: ModuleImpl, evaluation_type=Evaluation.EVALUATION_SESSION2
|
||||
) -> np.array:
|
||||
"""Coefficients des évaluations de session 2 ou rattrapage.
|
||||
Les évals de session 2 et rattrapage sont réputées "complètes": elles sont toujours
|
||||
prises en compte mais seules les notes numériques et ABS sont utilisées.
|
||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||
"""
|
||||
return (
|
||||
np.array(
|
||||
[
|
||||
(e.coefficient if e.evaluation_type == evaluation_type else 0.0)
|
||||
for e in modimpl.evaluations
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
).reshape(-1, 1)
|
||||
|
||||
# was _list_notes_evals_titles
|
||||
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"Liste des évaluations complètes"
|
||||
|
@ -296,32 +318,26 @@ class ModuleImplResults:
|
|||
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
||||
}
|
||||
|
||||
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
||||
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas.
|
||||
def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations de rattrapage de ce module.
|
||||
Rattrapage: la moyenne du module est la meilleure note entre moyenne
|
||||
des autres évals et la note eval rattrapage.
|
||||
des autres évals et la moyenne des notes de rattrapage.
|
||||
"""
|
||||
eval_list = [
|
||||
return [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluation_session2(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
||||
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
Session 2: remplace la note de moyenne des autres évals.
|
||||
def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
La moyenne des notes de Session 2 remplace la note de moyenne des autres évals.
|
||||
"""
|
||||
eval_list = [
|
||||
return [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations bonus de ce module, ou liste vide s'il n'en a pas."""
|
||||
|
@ -370,6 +386,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
if nb_ues == 0:
|
||||
return pd.DataFrame(index=self.evals_notes.index, columns=[])
|
||||
# coefs des évals complètes normales (pas rattr., session 2 ni bonus):
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl)
|
||||
evals_poids = evals_poids_df.values * evals_coefs
|
||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||
|
@ -398,6 +415,44 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
) / np.sum(evals_poids_etuds, axis=1)
|
||||
# etuds_moy_module shape: nb_etuds x nb_ues
|
||||
|
||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||
if evals_session2:
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
# Calcul moyenne notes session2 et remplace (si la note session 2 existe)
|
||||
etuds_moy_module_s2 = self._compute_moy_special(
|
||||
modimpl,
|
||||
evals_notes_stacked,
|
||||
evals_poids_df,
|
||||
Evaluation.EVALUATION_SESSION2,
|
||||
)
|
||||
|
||||
# Vrai si toutes les UEs ont bien une note de session 2 calculée:
|
||||
etuds_use_session2 = np.all(np.isfinite(etuds_moy_module_s2), axis=1)
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2[:, np.newaxis],
|
||||
etuds_moy_module_s2,
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
elif evals_rat:
|
||||
etuds_moy_module_rat = self._compute_moy_special(
|
||||
modimpl,
|
||||
evals_notes_stacked,
|
||||
evals_poids_df,
|
||||
Evaluation.EVALUATION_RATTRAPAGE,
|
||||
)
|
||||
etuds_ue_use_rattrapage = (
|
||||
etuds_moy_module_rat > etuds_moy_module
|
||||
) # etud x UE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_ue_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
np.any(etuds_ue_use_rattrapage, axis=1), index=self.evals_notes.index
|
||||
)
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
|
@ -405,47 +460,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
evals_poids_df,
|
||||
evals_notes_stacked,
|
||||
)
|
||||
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2[:, np.newaxis],
|
||||
np.tile(
|
||||
(notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis],
|
||||
nb_ues,
|
||||
),
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
else:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||
if eval_rat:
|
||||
notes_rat = self.evals_notes[eval_rat.id].values
|
||||
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||
notes_rat = np.where(
|
||||
notes_rat > scu.NOTES_ABSENCE,
|
||||
notes_rat / (eval_rat.note_max / 20.0),
|
||||
np.nan,
|
||||
)
|
||||
# "Étend" le rattrapage sur les UE: la note de rattrapage est la même
|
||||
# pour toutes les UE mais ne remplace que là où elle est supérieure
|
||||
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1)
|
||||
# prend le max
|
||||
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||
)
|
||||
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
||||
)
|
||||
self.etuds_moy_module = pd.DataFrame(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
|
@ -453,6 +467,34 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
|||
)
|
||||
return self.etuds_moy_module
|
||||
|
||||
def _compute_moy_special(
|
||||
self,
|
||||
modimpl: ModuleImpl,
|
||||
evals_notes_stacked: np.array,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
evaluation_type: int,
|
||||
) -> np.array:
|
||||
"""Calcul moyenne APC sur évals rattrapage ou session2"""
|
||||
nb_etuds = self.evals_notes.shape[0]
|
||||
nb_ues = evals_poids_df.shape[1]
|
||||
evals_coefs_s2 = self.get_evaluations_special_coefs(
|
||||
modimpl, evaluation_type=evaluation_type
|
||||
)
|
||||
evals_poids_s2 = evals_poids_df.values * evals_coefs_s2
|
||||
poids_stacked_s2 = np.stack(
|
||||
[evals_poids_s2] * nb_etuds
|
||||
) # nb_etuds, nb_evals, nb_ues
|
||||
evals_poids_etuds_s2 = np.where(
|
||||
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||
poids_stacked_s2,
|
||||
0,
|
||||
)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module_s2 = np.sum(
|
||||
evals_poids_etuds_s2 * evals_notes_stacked, axis=1
|
||||
) / np.sum(evals_poids_etuds_s2, axis=1)
|
||||
return etuds_moy_module_s2
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: pd.DataFrame,
|
||||
|
@ -525,6 +567,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
|||
return evals_poids, ues
|
||||
|
||||
|
||||
# appelé par ModuleImpl.check_apc_conformity()
|
||||
def moduleimpl_is_conforme(
|
||||
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
) -> bool:
|
||||
|
@ -546,12 +589,12 @@ def moduleimpl_is_conforme(
|
|||
if len(modimpl_coefs_df) != nb_ues:
|
||||
# il arrive (#bug) que le cache ne soit pas à jour...
|
||||
sco_cache.invalidate_formsemestre()
|
||||
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
|
||||
return app.critical_error("moduleimpl_is_conforme: err 1")
|
||||
|
||||
if moduleimpl.id not in modimpl_coefs_df:
|
||||
# soupçon de bug cache coef ?
|
||||
sco_cache.invalidate_formsemestre()
|
||||
raise ScoBugCatcher("Erreur 454 - merci de ré-essayer")
|
||||
return app.critical_error("moduleimpl_is_conforme: err 2")
|
||||
|
||||
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||
|
@ -593,46 +636,43 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||
evals_coefs_etuds * evals_notes_20, axis=1
|
||||
) / np.sum(evals_coefs_etuds, axis=1)
|
||||
|
||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||
if evals_session2:
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
# Calcule la moyenne des évaluations de session2
|
||||
etuds_moy_module_s2 = self._compute_moy_special(
|
||||
modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
|
||||
)
|
||||
etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2,
|
||||
etuds_moy_module_s2,
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
elif evals_rat:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
# Calcule la moyenne des évaluations de rattrapage
|
||||
etuds_moy_module_rat = self._compute_moy_special(
|
||||
modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
|
||||
)
|
||||
etuds_use_rattrapage = etuds_moy_module_rat > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage, index=self.evals_notes.index
|
||||
)
|
||||
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
modimpl,
|
||||
evals_notes_20,
|
||||
)
|
||||
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2,
|
||||
notes_session2 / (eval_session2.note_max / 20.0),
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
else:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||
if eval_rat:
|
||||
notes_rat = self.evals_notes[eval_rat.id].values
|
||||
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||
notes_rat = np.where(
|
||||
notes_rat > scu.NOTES_ABSENCE,
|
||||
notes_rat / (eval_rat.note_max / 20.0),
|
||||
np.nan,
|
||||
)
|
||||
# prend le max
|
||||
etuds_use_rattrapage = notes_rat > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage, index=self.evals_notes.index
|
||||
)
|
||||
self.etuds_moy_module = pd.Series(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
|
@ -640,6 +680,28 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
|||
|
||||
return self.etuds_moy_module
|
||||
|
||||
def _compute_moy_special(
|
||||
self, modimpl: ModuleImpl, evals_notes_20: np.array, evaluation_type: int
|
||||
) -> np.array:
|
||||
"""Calcul moyenne sur évals rattrapage ou session2"""
|
||||
# n'utilise que les notes valides et ABS (0).
|
||||
# Même calcul que pour les évals normales, mais avec seulement les
|
||||
# coefs des évals de session 2 ou rattrapage:
|
||||
nb_etuds = self.evals_notes.shape[0]
|
||||
evals_coefs = self.get_evaluations_special_coefs(
|
||||
modimpl, evaluation_type=evaluation_type
|
||||
).reshape(-1)
|
||||
coefs_stacked = np.stack([evals_coefs] * nb_etuds)
|
||||
# zéro partout sauf si une note ou ABS:
|
||||
evals_coefs_etuds = np.where(
|
||||
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0
|
||||
)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module = np.sum(
|
||||
evals_coefs_etuds * evals_notes_20, axis=1
|
||||
) / np.sum(evals_coefs_etuds, axis=1)
|
||||
return etuds_moy_module # array 1d (nb_etuds)
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: np.ndarray,
|
||||
|
|
|
@ -207,7 +207,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
|||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||
for modimpl in formsemestre.modimpls_sorted:
|
||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
||||
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
||||
evals_poids = modimpl.get_evaluations_poids()
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
|
|
|
@ -242,7 +242,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
|||
)
|
||||
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
</div>
|
||||
"""
|
||||
""",
|
||||
safe=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -518,7 +518,8 @@ class ResultatsSemestre(ResultatsCache):
|
|||
Corrigez ou faite corriger le programme
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
||||
"""
|
||||
""",
|
||||
safe=True,
|
||||
)
|
||||
else:
|
||||
# Coefs de l'UE capitalisée en formation classique:
|
||||
|
|
14
app/email.py
14
app/email.py
|
@ -9,9 +9,9 @@ import datetime
|
|||
from threading import Thread
|
||||
|
||||
from flask import current_app, g
|
||||
from flask_mail import Message
|
||||
from flask_mail import BadHeaderError, Message
|
||||
|
||||
from app import mail
|
||||
from app import log, mail
|
||||
from app.models.departements import Departement
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import sco_preferences
|
||||
|
@ -20,7 +20,15 @@ from app.scodoc import sco_preferences
|
|||
def send_async_email(app, msg):
|
||||
"Send an email, async"
|
||||
with app.app_context():
|
||||
mail.send(msg)
|
||||
try:
|
||||
mail.send(msg)
|
||||
except BadHeaderError:
|
||||
log(
|
||||
f"""send_async_email: BadHeaderError
|
||||
msg={msg}
|
||||
"""
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def send_email(
|
||||
|
|
|
@ -338,9 +338,11 @@ def add_entreprise():
|
|||
if form.validate_on_submit():
|
||||
entreprise = Entreprise(
|
||||
nom=form.nom_entreprise.data.strip(),
|
||||
siret=form.siret.data.strip()
|
||||
if form.siret.data.strip()
|
||||
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}", # siret provisoire
|
||||
siret=(
|
||||
form.siret.data.strip()
|
||||
if form.siret.data.strip()
|
||||
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}"
|
||||
), # siret provisoire
|
||||
siret_provisoire=False if form.siret.data.strip() else True,
|
||||
association=form.association.data,
|
||||
adresse=form.adresse.data.strip(),
|
||||
|
@ -352,7 +354,7 @@ def add_entreprise():
|
|||
db.session.add(entreprise)
|
||||
db.session.commit()
|
||||
db.session.refresh(entreprise)
|
||||
except:
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
|
@ -804,9 +806,9 @@ def add_offre(entreprise_id):
|
|||
missions=form.missions.data.strip(),
|
||||
duree=form.duree.data.strip(),
|
||||
expiration_date=form.expiration_date.data,
|
||||
correspondant_id=form.correspondant.data
|
||||
if form.correspondant.data != ""
|
||||
else None,
|
||||
correspondant_id=(
|
||||
form.correspondant.data if form.correspondant.data != "" else None
|
||||
),
|
||||
)
|
||||
db.session.add(offre)
|
||||
db.session.commit()
|
||||
|
@ -1328,9 +1330,11 @@ def add_contact(entreprise_id):
|
|||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||
form = ContactCreationForm(
|
||||
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
|
||||
utilisateur=f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||
if current_user.nom and current_user.prenom
|
||||
else "",
|
||||
utilisateur=(
|
||||
f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||
if current_user.nom and current_user.prenom
|
||||
else ""
|
||||
),
|
||||
)
|
||||
if request.method == "POST" and form.cancel.data:
|
||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise_id))
|
||||
|
@ -1496,9 +1500,9 @@ def add_stage_apprentissage(entreprise_id):
|
|||
date_debut=form.date_debut.data,
|
||||
date_fin=form.date_fin.data,
|
||||
formation_text=formation.formsemestre.titre if formation else None,
|
||||
formation_scodoc=formation.formsemestre.formsemestre_id
|
||||
if formation
|
||||
else None,
|
||||
formation_scodoc=(
|
||||
formation.formsemestre.formsemestre_id if formation else None
|
||||
),
|
||||
notes=form.notes.data.strip(),
|
||||
)
|
||||
db.session.add(stage_apprentissage)
|
||||
|
@ -1802,7 +1806,7 @@ def import_donnees():
|
|||
db.session.add(entreprise)
|
||||
db.session.commit()
|
||||
db.session.refresh(entreprise)
|
||||
except:
|
||||
except Exception:
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
|
|
66
app/forms/main/create_bug_report.py
Normal file
66
app/forms/main/create_bug_report.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaire création de ticket de bug
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField, TextAreaField, BooleanField
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class CreateBugReport(FlaskForm):
|
||||
"""Formulaire permettant la création d'un ticket de bug"""
|
||||
|
||||
title = StringField(
|
||||
label="Titre du ticket",
|
||||
validators=[
|
||||
validators.DataRequired("titre du ticket requis"),
|
||||
],
|
||||
)
|
||||
message = TextAreaField(
|
||||
label="Message",
|
||||
id="ticket_message",
|
||||
validators=[
|
||||
validators.DataRequired("message du ticket requis"),
|
||||
],
|
||||
)
|
||||
etab = StringField(label="Etablissement")
|
||||
include_dump = BooleanField(
|
||||
"""Inclure une copie anonymisée de la base de données ?
|
||||
Ces données faciliteront le traitement du problème et resteront strictement confidentielles.
|
||||
""",
|
||||
default=False,
|
||||
)
|
||||
submit = SubmitField("Envoyer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateBugReport, self).__init__(*args, **kwargs)
|
||||
self.etab.data = sco_preferences.get_preference("InstituteName") or ""
|
|
@ -353,12 +353,12 @@ class Assiduite(ScoDocModel):
|
|||
|
||||
elif self.external_data is not None and "module" in self.external_data:
|
||||
return (
|
||||
"Tout module"
|
||||
"Autre module (pas dans la liste)"
|
||||
if self.external_data["module"] == "Autre"
|
||||
else self.external_data["module"]
|
||||
)
|
||||
|
||||
return "Non spécifié" if traduire else None
|
||||
return "Module non spécifié" if traduire else None
|
||||
|
||||
def get_saisie(self) -> str:
|
||||
"""
|
||||
|
|
|
@ -551,7 +551,7 @@ class Identite(models.ScoDocModel):
|
|||
.all()
|
||||
)
|
||||
|
||||
def inscription_courante(self):
|
||||
def inscription_courante(self) -> "FormSemestreInscription | None":
|
||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||
"""
|
||||
|
|
|
@ -71,6 +71,15 @@ class Evaluation(models.ScoDocModel):
|
|||
EVALUATION_BONUS,
|
||||
}
|
||||
|
||||
def type_abbrev(self) -> str:
|
||||
"Le nom abrégé du type de cette éval."
|
||||
return {
|
||||
self.EVALUATION_NORMALE: "std",
|
||||
self.EVALUATION_RATTRAPAGE: "rattrapage",
|
||||
self.EVALUATION_SESSION2: "session 2",
|
||||
self.EVALUATION_BONUS: "bonus",
|
||||
}.get(self.evaluation_type, "?")
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<Evaluation {self.id} {
|
||||
self.date_debut.isoformat() if self.date_debut else ''} "{
|
||||
|
@ -417,12 +426,13 @@ class Evaluation(models.ScoDocModel):
|
|||
return modified
|
||||
|
||||
def set_ue_poids(self, ue, poids: float) -> None:
|
||||
"""Set poids évaluation vers cette UE"""
|
||||
"""Set poids évaluation vers cette UE. Commit."""
|
||||
self.update_ue_poids_dict({ue.id: poids})
|
||||
|
||||
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
"""set poids vers les UE (remplace existants)
|
||||
ue_poids_dict = { ue_id : poids }
|
||||
Commit session.
|
||||
"""
|
||||
from app.models.ues import UniteEns
|
||||
|
||||
|
@ -432,9 +442,12 @@ class Evaluation(models.ScoDocModel):
|
|||
if ue is None:
|
||||
raise ScoValueError("poids vers une UE inexistante")
|
||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||
L.append(ue_poids)
|
||||
db.session.add(ue_poids)
|
||||
L.append(ue_poids)
|
||||
|
||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||
|
||||
db.session.commit()
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
|
|
|
@ -6,6 +6,7 @@ from flask import abort, g
|
|||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp import df_cache
|
||||
|
@ -78,7 +79,9 @@ class ModuleImpl(ScoDocModel):
|
|||
] or self.module.get_edt_ids()
|
||||
|
||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||
"""Les poids des évaluations vers les UEs (accès via cache redis).
|
||||
Toutes les évaluations sont considérées (normales, bonus, rattr., etc.)
|
||||
"""
|
||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||
if evaluations_poids is None:
|
||||
from app.comp import moy_mod
|
||||
|
@ -108,20 +111,37 @@ class ModuleImpl(ScoDocModel):
|
|||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
||||
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
||||
"""true si les poids des évaluations du module permettent de satisfaire
|
||||
les coefficients du PN.
|
||||
def check_apc_conformity(
|
||||
self, res: "ResultatsSemestreBUT", evaluation_type=Evaluation.EVALUATION_NORMALE
|
||||
) -> bool:
|
||||
"""true si les poids des évaluations du type indiqué (normales par défaut)
|
||||
du module permettent de satisfaire les coefficients du PN.
|
||||
"""
|
||||
# appelé par formsemestre_status, liste notes, et moduleimpl_status
|
||||
if not self.module.formation.get_cursus().APC_SAE or (
|
||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||
and self.module.module_type != scu.ModuleType.SAE
|
||||
self.module.module_type
|
||||
not in {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
|
||||
):
|
||||
return True # Non BUT, toujours conforme
|
||||
from app.comp import moy_mod
|
||||
|
||||
mod_results = res.modimpls_results.get(self.id)
|
||||
if mod_results is None:
|
||||
app.critical_error("check_apc_conformity: err 1")
|
||||
|
||||
selected_evaluations_ids = [
|
||||
eval_id
|
||||
for eval_id, eval_type in mod_results.evals_type.items()
|
||||
if eval_type == evaluation_type
|
||||
]
|
||||
if not selected_evaluations_ids:
|
||||
return True # conforme si pas d'évaluations
|
||||
selected_evaluations_poids = self.get_evaluations_poids().loc[
|
||||
selected_evaluations_ids
|
||||
]
|
||||
return moy_mod.moduleimpl_is_conforme(
|
||||
self,
|
||||
self.get_evaluations_poids(),
|
||||
selected_evaluations_poids,
|
||||
res.modimpl_coefs_df,
|
||||
)
|
||||
|
||||
|
|
|
@ -176,6 +176,7 @@ class GenTable:
|
|||
self.xml_link = xml_link
|
||||
# HTML parameters:
|
||||
if not table_id: # random id
|
||||
log("Warning: GenTable() called without table_id")
|
||||
self.table_id = "gt_" + str(random.randint(0, 1000000))
|
||||
else:
|
||||
self.table_id = table_id
|
||||
|
|
|
@ -25,8 +25,7 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""HTML Header/Footer for ScoDoc pages
|
||||
"""
|
||||
"""HTML Header/Footer for ScoDoc pages"""
|
||||
|
||||
import html
|
||||
|
||||
|
@ -101,7 +100,7 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
|
|||
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
|
||||
</script>
|
||||
|
||||
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||
|
@ -218,7 +217,7 @@ def sco_header(
|
|||
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
|
||||
|
||||
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
|
||||
const SCO_TIMEZONE="{scu.TIME_ZONE}";
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"""
|
||||
Génération de la "sidebar" (marge gauche des pages HTML)
|
||||
"""
|
||||
|
||||
from flask import render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
@ -151,7 +152,7 @@ def sidebar(etudid: int = None):
|
|||
H = [
|
||||
f"""
|
||||
<!-- sidebar py -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
{ sidebar_common() }
|
||||
<div class="box-chercheetud">Chercher étudiant:<br>
|
||||
<form method="get" id="form-chercheetud"
|
||||
|
@ -193,7 +194,7 @@ def sidebar(etudid: int = None):
|
|||
formsemestre.date_debut.strftime(scu.DATE_FMT)
|
||||
} au {
|
||||
formsemestre.date_fin.strftime(scu.DATE_FMT)
|
||||
}">({
|
||||
}" data-tooltip>({
|
||||
sco_preferences.get_preference("assi_metrique", None)})
|
||||
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
|
||||
)
|
||||
|
@ -227,12 +228,9 @@ def sidebar(etudid: int = None):
|
|||
<li><a href="{ url_for('assiduites.calendrier_assi_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Calendrier</a></li>
|
||||
<li><a href="{ url_for('assiduites.liste_assiduites_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Liste</a></li>
|
||||
<li><a href="{ url_for('assiduites.bilan_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Bilan</a></li>
|
||||
}" title="Les pages bilan et liste ont été fusionnées">Liste/Bilan</a></li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -157,5 +157,6 @@ def table_billets(
|
|||
rows=rows,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
table_id="table_billets",
|
||||
)
|
||||
return tab
|
||||
|
|
|
@ -288,6 +288,7 @@ def apo_table_compare_etud_results(A, B):
|
|||
html_class="table_leftalign",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="apo_table_compare_etud_results",
|
||||
)
|
||||
return T
|
||||
|
||||
|
|
|
@ -917,6 +917,7 @@ class ApoData:
|
|||
columns_ids=columns_ids,
|
||||
titles=dict(zip(columns_ids, columns_ids)),
|
||||
rows=rows,
|
||||
table_id="build_cr_table",
|
||||
xls_sheet_name="Decisions ScoDoc",
|
||||
)
|
||||
return T
|
||||
|
@ -969,6 +970,7 @@ class ApoData:
|
|||
"rcue": "RCUE",
|
||||
},
|
||||
rows=rows,
|
||||
table_id="adsup_table",
|
||||
xls_sheet_name="ADSUPs",
|
||||
)
|
||||
|
||||
|
@ -1054,6 +1056,7 @@ def nar_etuds_table(apo_data, nar_etuds):
|
|||
columns_ids=columns_ids,
|
||||
titles=dict(zip(columns_ids, columns_ids)),
|
||||
rows=rows,
|
||||
table_id="nar_etuds_table",
|
||||
xls_sheet_name="NAR ScoDoc",
|
||||
)
|
||||
return table.excel()
|
||||
|
|
102
app/scodoc/sco_bug_report.py
Normal file
102
app/scodoc/sco_bug_report.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Rapport de bug ScoDoc
|
||||
|
||||
Permet de créer un rapport de bug (ticket) sur la plateforme git scodoc.org.
|
||||
|
||||
Le principe est le suivant:
|
||||
1- Si l'utilisateur le demande, on dump la base de données et on l'envoie
|
||||
|
||||
2- ScoDoc envoie une requête POST à scodoc.org pour qu'un ticket git soit créé avec les
|
||||
informations fournies par l'utilisateur + quelques métadonnées.
|
||||
|
||||
"""
|
||||
from flask import g
|
||||
from flask_login import current_user
|
||||
import requests
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import sco_version
|
||||
|
||||
from app import log
|
||||
from app.scodoc.sco_dump_db import sco_dump_and_send_db
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def sco_bug_report(
|
||||
title: str = "", message: str = "", etab: str = "", include_dump: bool = False
|
||||
) -> requests.Response:
|
||||
"""Envoi d'un bug report (ticket)"""
|
||||
dump_id = None
|
||||
|
||||
if include_dump:
|
||||
dump = sco_dump_and_send_db()
|
||||
|
||||
try:
|
||||
dump_id = dump.json()["dump_id"]
|
||||
except (requests.exceptions.JSONDecodeError, KeyError):
|
||||
dump_id = "inconnu (erreur)"
|
||||
|
||||
log(f"sco_bug_report: {scu.SCO_BUG_REPORT_URL} by {current_user.user_name}")
|
||||
try:
|
||||
r = requests.post(
|
||||
scu.SCO_BUG_REPORT_URL,
|
||||
json={
|
||||
"ticket": {
|
||||
"title": title,
|
||||
"message": message,
|
||||
"etab": etab,
|
||||
"dept": getattr(g, "scodoc_dept", "-"),
|
||||
},
|
||||
"user": {
|
||||
"name": current_user.get_nomcomplet(),
|
||||
"email": current_user.email,
|
||||
},
|
||||
"dump": {
|
||||
"included": include_dump,
|
||||
"id": dump_id,
|
||||
},
|
||||
"scodoc": {
|
||||
"version": sco_version.SCOVERSION,
|
||||
},
|
||||
},
|
||||
timeout=scu.SCO_ORG_TIMEOUT,
|
||||
)
|
||||
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc:
|
||||
log("ConnectionError: Impossible de joindre le serveur d'assistance")
|
||||
raise ScoValueError(
|
||||
"""
|
||||
Impossible de joindre le serveur d'assistance (scodoc.org).
|
||||
Veuillez contacter le service informatique de votre établissement pour
|
||||
corriger la configuration de ScoDoc. Dans la plupart des cas, il
|
||||
s'agit d'un proxy mal configuré.
|
||||
"""
|
||||
) from exc
|
||||
|
||||
return r
|
|
@ -122,7 +122,8 @@ def replacement_function(match) -> str:
|
|||
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
||||
raise ScoValueError(
|
||||
'balise "%s": logo "%s" introuvable'
|
||||
% (pydoc.html.escape(balise), pydoc.html.escape(name))
|
||||
% (pydoc.html.escape(balise), pydoc.html.escape(name)),
|
||||
safe=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="std_bul_table",
|
||||
)
|
||||
|
||||
return T.gen(fmt=fmt)
|
||||
|
|
|
@ -274,6 +274,7 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
"""expire cache pour un semestre (ou tous ceux du département si formsemestre_id non spécifié).
|
||||
Si pdfonly, n'expire que les bulletins pdf cachés.
|
||||
"""
|
||||
from app.comp import df_cache
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_cursus
|
||||
|
||||
|
@ -315,12 +316,14 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
and fid in g.formsemestre_results_cache
|
||||
):
|
||||
del g.formsemestre_results_cache[fid]
|
||||
|
||||
df_cache.EvaluationsPoidsCache.invalidate_sem(formsemestre_id)
|
||||
else:
|
||||
# optimization when we invalidate all evaluations:
|
||||
EvaluationCache.invalidate_all_sems()
|
||||
df_cache.EvaluationsPoidsCache.invalidate_all()
|
||||
if hasattr(g, "formsemestre_results_cache"):
|
||||
del g.formsemestre_results_cache
|
||||
|
||||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
||||
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
||||
|
|
|
@ -141,6 +141,7 @@ def formsemestre_table_estim_cost(
|
|||
""",
|
||||
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||
filename=f"EstimCout-S{formsemestre.semestre_id}",
|
||||
table_id="formsemestre_table_estim_cost",
|
||||
)
|
||||
return tab
|
||||
|
||||
|
|
|
@ -350,11 +350,13 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
l'étudiant (quelle que soit la formation), le plus ancien en tête"""
|
||||
return self.sems
|
||||
|
||||
def get_cursus_descr(self, filter_futur=False):
|
||||
def get_cursus_descr(self, filter_futur=False, filter_formation_code=False):
|
||||
"""Description brève du parcours: "S1, S2, ..."
|
||||
Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
|
||||
Si filter_formation_code, restreint aux semestres de même code formation que le courant.
|
||||
"""
|
||||
cur_begin_date = self.sem["dateord"]
|
||||
cur_formation_code = self.sem["formation_code"]
|
||||
p = []
|
||||
for s in self.sems:
|
||||
if s["ins"]["etat"] == scu.DEMISSION:
|
||||
|
@ -363,12 +365,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
|||
dem = ""
|
||||
if filter_futur and s["dateord"] > cur_begin_date:
|
||||
continue # skip semestres demarrant apres le courant
|
||||
SA = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if filter_formation_code and s["formation_code"] != cur_formation_code:
|
||||
continue # restreint aux semestres de la formation courante (pour les PV)
|
||||
session_abbrv = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if s["semestre_id"] < 0:
|
||||
SA = "A" # force, cas des DUT annuels par exemple
|
||||
p.append("%s%d%s" % (SA, -s["semestre_id"], dem))
|
||||
session_abbrv = "A" # force, cas des DUT annuels par exemple
|
||||
p.append("%s%d%s" % (session_abbrv, -s["semestre_id"], dem))
|
||||
else:
|
||||
p.append("%s%d%s" % (SA, s["semestre_id"], dem))
|
||||
p.append("%s%d%s" % (session_abbrv, s["semestre_id"], dem))
|
||||
return ", ".join(p)
|
||||
|
||||
def get_parcours_decisions(self):
|
||||
|
|
|
@ -222,6 +222,7 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
|||
html_sortable=True,
|
||||
html_class="table_leftalign table_listegroupe",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="table_debouche_etudids",
|
||||
)
|
||||
return tab
|
||||
|
||||
|
|
|
@ -198,6 +198,18 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
|||
if current_user.has_permission(Permission.EditApogee):
|
||||
html_class += " apo_editable"
|
||||
tab = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
html_class_ignore_default=True,
|
||||
html_class=html_class,
|
||||
html_sortable=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=sems,
|
||||
titles={
|
||||
"formsemestre_id": "id",
|
||||
"semestre_id_n": "S#",
|
||||
|
@ -211,19 +223,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
|||
"elt_sem_apo": "Elt. sem. Apo.",
|
||||
"formation": "Formation",
|
||||
},
|
||||
columns_ids=columns_ids,
|
||||
rows=sems,
|
||||
table_id="semlist",
|
||||
html_class_ignore_default=True,
|
||||
html_class=html_class,
|
||||
html_sortable=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
||||
return tab
|
||||
|
|
|
@ -67,7 +67,7 @@ SCO_DUMP_LOCK = "/tmp/scodump.lock"
|
|||
|
||||
def sco_dump_and_send_db(
|
||||
message: str = "", request_url: str = "", traceback_str_base64: str = ""
|
||||
):
|
||||
) -> requests.Response:
|
||||
"""Dump base de données et l'envoie anonymisée pour debug"""
|
||||
traceback_str = base64.urlsafe_b64decode(traceback_str_base64).decode(
|
||||
scu.SCO_ENCODING
|
||||
|
@ -97,7 +97,6 @@ def sco_dump_and_send_db(
|
|||
|
||||
# Send
|
||||
r = _send_db(ano_db_name, message, request_url, traceback_str=traceback_str)
|
||||
code = r.status_code
|
||||
|
||||
finally:
|
||||
# Drop anonymized database
|
||||
|
@ -107,7 +106,7 @@ def sco_dump_and_send_db(
|
|||
|
||||
log("sco_dump_and_send_db: done.")
|
||||
|
||||
return code
|
||||
return r
|
||||
|
||||
|
||||
def _duplicate_db(db_name, ano_db_name):
|
||||
|
@ -216,11 +215,11 @@ def _drop_ano_db(ano_db_name):
|
|||
log("_drop_ano_db: no temp db, nothing to drop")
|
||||
return
|
||||
cmd = ["dropdb", ano_db_name]
|
||||
log("sco_dump_and_send_db: {}".format(cmd))
|
||||
log(f"sco_dump_and_send_db: {cmd}")
|
||||
try:
|
||||
_ = subprocess.check_output(cmd)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log("sco_dump_and_send_db: exception dropdb {}".format(e))
|
||||
except subprocess.CalledProcessError as exc:
|
||||
log(f"sco_dump_and_send_db: exception dropdb {exc}")
|
||||
raise ScoValueError(
|
||||
"erreur lors de la suppression de la base {}".format(ano_db_name)
|
||||
)
|
||||
f"erreur lors de la suppression de la base {ano_db_name}"
|
||||
) from exc
|
||||
|
|
|
@ -326,6 +326,7 @@ def do_formation_create(args: dict) -> Formation:
|
|||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation.id,
|
||||
),
|
||||
safe=True,
|
||||
) from exc
|
||||
|
||||
ScolarNews.add(
|
||||
|
|
|
@ -490,6 +490,7 @@ def table_apo_csv_list(semset):
|
|||
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
||||
# caption='Maquettes enregistrées',
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="apo_csv_list",
|
||||
)
|
||||
|
||||
return tab
|
||||
|
@ -582,6 +583,7 @@ def _view_etuds_page(
|
|||
html_class="table_leftalign",
|
||||
filename="students_apo",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="view_etuds_page",
|
||||
)
|
||||
if fmt != "html":
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
@ -798,6 +800,7 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
|
|||
filename="students_" + etape_apo,
|
||||
caption="Étudiants Apogée en " + etape_apo,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="view_apo_csv",
|
||||
)
|
||||
|
||||
if fmt != "html":
|
||||
|
|
|
@ -666,7 +666,9 @@ class EtapeBilan:
|
|||
col_ids,
|
||||
self.titres,
|
||||
html_class="repartition",
|
||||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="apo-repartition",
|
||||
).gen(fmt="html")
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
@ -762,9 +764,9 @@ class EtapeBilan:
|
|||
rows,
|
||||
col_ids,
|
||||
titles,
|
||||
table_id="detail",
|
||||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
table_id="apo-detail",
|
||||
).gen(fmt="html")
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
|
|
@ -367,6 +367,9 @@ def evaluation_create_form(
|
|||
+ "\n".join(H)
|
||||
+ "\n"
|
||||
+ tf[1]
|
||||
+ render_template(
|
||||
"scodoc/forms/evaluation_edit.j2",
|
||||
)
|
||||
+ render_template(
|
||||
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
|
||||
)
|
||||
|
|
|
@ -70,8 +70,8 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||
Colonnes:
|
||||
- code (UE ou module),
|
||||
- titre
|
||||
- type évaluation
|
||||
- complete
|
||||
- publiée
|
||||
- inscrits (non dem. ni def.)
|
||||
- nb notes manquantes
|
||||
- nb ATT
|
||||
|
@ -81,9 +81,10 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||
rows = []
|
||||
titles = {
|
||||
"type": "",
|
||||
"code": "Code",
|
||||
"code": "Module",
|
||||
"titre": "",
|
||||
"date": "Date",
|
||||
"type_evaluation": "Type",
|
||||
"complete": "Comptée",
|
||||
"inscrits": "Inscrits",
|
||||
"manquantes": "Manquantes", # notes eval non entrées
|
||||
|
@ -114,7 +115,9 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||
rows.append(row)
|
||||
line_idx += 1
|
||||
for evaluation_id in modimpl_results.evals_notes:
|
||||
e = db.session.get(Evaluation, evaluation_id)
|
||||
e: Evaluation = db.session.get(Evaluation, evaluation_id)
|
||||
if e is None:
|
||||
continue # ignore errors (rare race conditions?)
|
||||
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||
row = {
|
||||
"type": "",
|
||||
|
@ -128,6 +131,7 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
|||
"_titre_target_attrs": 'class="discretelink"',
|
||||
"date": e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "",
|
||||
"_date_order": e.date_debut.isoformat() if e.date_debut else "",
|
||||
"type_evaluation": e.type_abbrev(),
|
||||
"complete": "oui" if eval_etat.is_complete else "non",
|
||||
"_complete_target": "#",
|
||||
"_complete_target_attrs": (
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Evaluations
|
||||
"""
|
||||
"""Evaluations"""
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import operator
|
||||
|
@ -50,6 +50,7 @@ from app.scodoc import sco_cal
|
|||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_gen_cal
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
|
@ -360,6 +361,105 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
|
|||
return etat
|
||||
|
||||
|
||||
class JourEval(sco_gen_cal.Jour):
|
||||
"""
|
||||
Représentation d'un jour dans un calendrier d'évaluations
|
||||
"""
|
||||
|
||||
COLOR_INCOMPLETE = "#FF6060"
|
||||
COLOR_COMPLETE = "#A0FFA0"
|
||||
COLOR_FUTUR = "#70E0FF"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
date: datetime.date,
|
||||
evaluations: list[Evaluation],
|
||||
parent: "CalendrierEval",
|
||||
):
|
||||
super().__init__(date)
|
||||
|
||||
self.evaluations: list[Evaluation] = evaluations
|
||||
self.evaluations.sort(key=lambda e: e.date_debut)
|
||||
|
||||
self.parent: "CalendrierEval" = parent
|
||||
|
||||
def get_html(self) -> str:
|
||||
htmls = []
|
||||
|
||||
for e in self.evaluations:
|
||||
url: str = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl_id,
|
||||
)
|
||||
title: str = (
|
||||
e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
||||
)
|
||||
htmls.append(
|
||||
f"""<a
|
||||
href="{url}"
|
||||
style="{self._get_eval_style(e)}"
|
||||
title="{self._get_eval_title(e)}"
|
||||
class="stdlink"
|
||||
>{title}</a>"""
|
||||
)
|
||||
|
||||
return ", ".join(htmls)
|
||||
|
||||
def _get_eval_style(self, e: Evaluation) -> str:
|
||||
color: str = ""
|
||||
# Etat (notes completes) de l'évaluation:
|
||||
modimpl_result = self.parent.nt.modimpls_results[e.moduleimpl.id]
|
||||
if modimpl_result.evaluations_etat[e.id].is_complete:
|
||||
color = JourEval.COLOR_COMPLETE
|
||||
else:
|
||||
color = JourEval.COLOR_INCOMPLETE
|
||||
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
||||
color = JourEval.COLOR_FUTUR
|
||||
|
||||
return f"background-color: {color};"
|
||||
|
||||
def _get_eval_title(self, e: Evaluation) -> str:
|
||||
heure_debut_txt, heure_fin_txt = "", ""
|
||||
if e.date_debut != e.date_fin:
|
||||
heure_debut_txt = (
|
||||
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
|
||||
)
|
||||
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
|
||||
|
||||
title = f"{e.description or e.moduleimpl.module.titre_str()}"
|
||||
if heure_debut_txt:
|
||||
title += f" de {heure_debut_txt} à {heure_fin_txt}"
|
||||
|
||||
return title
|
||||
|
||||
|
||||
class CalendrierEval(sco_gen_cal.Calendrier):
|
||||
"""
|
||||
Représentation des évaluations d'un semestre dans un calendrier
|
||||
"""
|
||||
|
||||
def __init__(self, year: int, evals: list[Evaluation], nt: NotesTableCompat):
|
||||
# On prend du 01/09 au 31/08
|
||||
date_debut: datetime.datetime = datetime.datetime(year, 9, 1, 0, 0)
|
||||
date_fin: datetime.datetime = datetime.datetime(year + 1, 8, 31, 23, 59)
|
||||
super().__init__(date_debut, date_fin)
|
||||
|
||||
# évalutions du semestre
|
||||
self.evals: dict[datetime.date, list[Evaluation]] = {}
|
||||
for e in evals:
|
||||
if e.date_debut is not None:
|
||||
day = e.date_debut.date()
|
||||
if day not in self.evals:
|
||||
self.evals[day] = []
|
||||
self.evals[day].append(e)
|
||||
|
||||
self.nt: NotesTableCompat = nt
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> JourEval:
|
||||
return JourEval(date, self.evals.get(date, []), parent=self)
|
||||
|
||||
|
||||
# View
|
||||
def formsemestre_evaluations_cal(formsemestre_id):
|
||||
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
||||
|
@ -369,52 +469,9 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||
evaluations = formsemestre.get_evaluations()
|
||||
nb_evals = len(evaluations)
|
||||
|
||||
color_incomplete = "#FF6060"
|
||||
color_complete = "#A0FFA0"
|
||||
color_futur = "#70E0FF"
|
||||
|
||||
year = formsemestre.annee_scolaire()
|
||||
events_by_day = collections.defaultdict(list) # date_iso : event
|
||||
for e in evaluations:
|
||||
if e.date_debut is None:
|
||||
continue # éval. sans date
|
||||
if e.date_debut == e.date_fin:
|
||||
heure_debut_txt, heure_fin_txt = "", ""
|
||||
else:
|
||||
heure_debut_txt = (
|
||||
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
|
||||
)
|
||||
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
|
||||
|
||||
# Etat (notes completes) de l'évaluation:
|
||||
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
|
||||
if modimpl_result.evaluations_etat[e.id].is_complete:
|
||||
color = color_complete
|
||||
else:
|
||||
color = color_incomplete
|
||||
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
||||
color = color_futur
|
||||
day = e.date_debut.date().isoformat() # yyyy-mm-dd
|
||||
event = {
|
||||
"color": color,
|
||||
"date_iso": day,
|
||||
"title": e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval.",
|
||||
"description": f"""{e.description or e.moduleimpl.module.titre_str()}"""
|
||||
+ (
|
||||
f""" de {heure_debut_txt} à {heure_fin_txt}"""
|
||||
if heure_debut_txt
|
||||
else ""
|
||||
),
|
||||
"href": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl_id,
|
||||
),
|
||||
"modimpl": e.moduleimpl,
|
||||
}
|
||||
events_by_day[day].append(event)
|
||||
|
||||
cal_html = sco_cal.YearTable(year, events_by_day=events_by_day)
|
||||
cal = CalendrierEval(year, evaluations, nt)
|
||||
cal_html = cal.get_html()
|
||||
|
||||
return f"""
|
||||
{
|
||||
|
@ -430,15 +487,15 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||
</p>
|
||||
<ul>
|
||||
<li>en <span style=
|
||||
"background-color: {color_incomplete}">rouge</span>
|
||||
"background-color: {JourEval.COLOR_INCOMPLETE}">rouge</span>
|
||||
les évaluations passées auxquelles il manque des notes
|
||||
</li>
|
||||
<li>en <span style=
|
||||
"background-color: {color_complete}">vert</span>
|
||||
"background-color: {JourEval.COLOR_COMPLETE}">vert</span>
|
||||
les évaluations déjà notées
|
||||
</li>
|
||||
<li>en <span style=
|
||||
"background-color: {color_futur}">bleu</span>
|
||||
"background-color: {JourEval.COLOR_FUTUR}">bleu</span>
|
||||
les évaluations futures
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -576,6 +633,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
|||
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
||||
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
||||
table_id="formsemestre_evaluations_delai_correction",
|
||||
)
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
||||
|
|
|
@ -45,13 +45,17 @@ class ScoInvalidCSRF(ScoException):
|
|||
|
||||
|
||||
class ScoValueError(ScoException):
|
||||
"Exception avec page d'erreur utilisateur, et qui stoque dest_url"
|
||||
"""Exception avec page d'erreur utilisateur
|
||||
- dest_url : url où aller après la page d'erreur
|
||||
- safe (default False): si vrai, affiche le message non html quoté.
|
||||
"""
|
||||
|
||||
# mal nommée: super classe de toutes les exceptions avec page
|
||||
# d'erreur gentille.
|
||||
def __init__(self, msg, dest_url=None):
|
||||
def __init__(self, msg, dest_url=None, safe=False):
|
||||
super().__init__(msg)
|
||||
self.dest_url = dest_url
|
||||
self.safe = safe # utilisé par template sco_value_error.j2
|
||||
|
||||
|
||||
class ScoPermissionDenied(ScoValueError):
|
||||
|
|
|
@ -106,6 +106,7 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
|
|||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="export_result_table",
|
||||
)
|
||||
return tab, semlist
|
||||
|
||||
|
|
|
@ -32,8 +32,7 @@ from flask import url_for, g, request
|
|||
from flask_login import current_user
|
||||
|
||||
import app
|
||||
from app.models import Departement
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models import Departement, Identite
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import html_sco_header
|
||||
|
@ -55,7 +54,9 @@ def form_search_etud(
|
|||
"form recherche par nom"
|
||||
H = []
|
||||
H.append(
|
||||
f"""<form action="{ url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept) }" method="POST">
|
||||
f"""<form action="{
|
||||
url_for("scolar.search_etud_in_dept", scodoc_dept=g.scodoc_dept)
|
||||
}" method="POST">
|
||||
<b>{title}</b>
|
||||
<input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
|
||||
<input type="submit" value="Chercher">
|
||||
|
@ -100,9 +101,9 @@ def form_search_etud(
|
|||
return "\n".join(H)
|
||||
|
||||
|
||||
def search_etuds_infos_from_exp(expnom: str = "") -> list[dict]:
|
||||
def search_etuds_infos_from_exp(expnom: str = "") -> list[Identite]:
|
||||
"""Cherche étudiants, expnom peut être, dans cet ordre:
|
||||
un etudid (int), un code NIP, ou le début d'un nom.
|
||||
un etudid (int), un code NIP, ou une partie d'un nom (case insensitive).
|
||||
"""
|
||||
if not isinstance(expnom, int) and len(expnom) <= 1:
|
||||
return [] # si expnom est trop court, n'affiche rien
|
||||
|
@ -111,13 +112,22 @@ def search_etuds_infos_from_exp(expnom: str = "") -> list[dict]:
|
|||
except ValueError:
|
||||
etudid = None
|
||||
if etudid is not None:
|
||||
etuds = sco_etud.get_etud_info(filled=True, etudid=expnom)
|
||||
if len(etuds) == 1:
|
||||
return etuds
|
||||
etud = Identite.query.filter_by(dept_id=g.scodoc_dept_id, id=etudid).first()
|
||||
if etud:
|
||||
return [etud]
|
||||
expnom_str = str(expnom)
|
||||
if scu.is_valid_code_nip(expnom_str):
|
||||
return search_etuds_infos(code_nip=expnom_str)
|
||||
return search_etuds_infos(expnom=expnom_str)
|
||||
etuds = Identite.query.filter_by(
|
||||
dept_id=g.scodoc_dept_id, code_nip=expnom_str
|
||||
).all()
|
||||
if etuds:
|
||||
return etuds
|
||||
|
||||
return (
|
||||
Identite.query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
.filter(Identite.nom.op("~*")(expnom_str))
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def search_etud_in_dept(expnom=""):
|
||||
|
@ -152,7 +162,7 @@ def search_etud_in_dept(expnom=""):
|
|||
|
||||
if len(etuds) == 1:
|
||||
# va directement a la fiche
|
||||
url_args["etudid"] = etuds[0]["etudid"]
|
||||
url_args["etudid"] = etuds[0].id
|
||||
return flask.redirect(url_for(endpoint, **url_args))
|
||||
|
||||
H = [
|
||||
|
@ -179,14 +189,39 @@ def search_etud_in_dept(expnom=""):
|
|||
)
|
||||
if len(etuds) > 0:
|
||||
# Choix dans la liste des résultats:
|
||||
rows = []
|
||||
e: Identite
|
||||
for e in etuds:
|
||||
url_args["etudid"] = e["etudid"]
|
||||
url_args["etudid"] = e.id
|
||||
target = url_for(endpoint, **url_args)
|
||||
e["_nomprenom_target"] = target
|
||||
e["inscription_target"] = target
|
||||
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
||||
sco_groups.etud_add_group_infos(
|
||||
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
|
||||
cur_inscription = e.inscription_courante()
|
||||
inscription = (
|
||||
e.inscription_descr().get("inscription_str", "")
|
||||
if cur_inscription
|
||||
else ""
|
||||
)
|
||||
groupes = (
|
||||
", ".join(
|
||||
gr.group_name
|
||||
for gr in sco_groups.get_etud_formsemestre_groups(
|
||||
e, cur_inscription.formsemestre
|
||||
)
|
||||
)
|
||||
if cur_inscription
|
||||
else ""
|
||||
)
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"code_nip": e.code_nip or "",
|
||||
"etudid": e.id,
|
||||
"inscription": inscription,
|
||||
"inscription_target": target,
|
||||
"groupes": groupes,
|
||||
"nomprenom": e.nomprenom,
|
||||
"_nomprenom_target": target,
|
||||
"_nomprenom_td_attrs": f'id="{e.id}" class="etudinfo"',
|
||||
}
|
||||
)
|
||||
|
||||
tab = GenTable(
|
||||
|
@ -197,10 +232,11 @@ def search_etud_in_dept(expnom=""):
|
|||
"inscription": "Inscription",
|
||||
"groupes": "Groupes",
|
||||
},
|
||||
rows=etuds,
|
||||
rows=rows,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="search_etud_in_dept",
|
||||
)
|
||||
H.append(tab.html())
|
||||
if len(etuds) > 20: # si la page est grande
|
||||
|
@ -213,15 +249,16 @@ def search_etud_in_dept(expnom=""):
|
|||
)
|
||||
)
|
||||
else:
|
||||
H.append('<h2 style="color: red;">Aucun résultat pour "%s".</h2>' % expnom)
|
||||
H.append(f'<h2 style="color: red;">Aucun résultat pour "{expnom}".</h2>')
|
||||
H.append(
|
||||
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP de l'étudiant. Saisir au moins deux caractères.</p>"""
|
||||
"""<p class="help">La recherche porte sur tout ou partie du NOM ou du NIP
|
||||
de l'étudiant. Saisir au moins deux caractères.</p>"""
|
||||
)
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
# Was chercheEtudsInfo()
|
||||
def search_etuds_infos(expnom=None, code_nip=None):
|
||||
def search_etuds_infos(expnom=None, code_nip=None) -> list[dict]:
|
||||
"""recherche les étudiants correspondants à expnom ou au code_nip
|
||||
et ramene liste de mappings utilisables en DTML.
|
||||
"""
|
||||
|
@ -264,7 +301,7 @@ def search_etud_by_name(term: str) -> list:
|
|||
FROM identite
|
||||
WHERE
|
||||
dept_id = %(dept_id)s
|
||||
AND code_nip LIKE %(beginning)s
|
||||
AND code_nip ILIKE %(beginning)s
|
||||
ORDER BY nom
|
||||
""",
|
||||
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
||||
|
@ -283,7 +320,7 @@ def search_etud_by_name(term: str) -> list:
|
|||
FROM identite
|
||||
WHERE
|
||||
dept_id = %(dept_id)s
|
||||
AND nom LIKE %(beginning)s
|
||||
AND nom ILIKE %(beginning)s
|
||||
ORDER BY nom
|
||||
""",
|
||||
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
||||
|
@ -348,6 +385,7 @@ def table_etud_in_accessible_depts(expnom=None):
|
|||
rows=etuds,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
table_id="etud_in_accessible_depts",
|
||||
)
|
||||
|
||||
H.append('<div class="table_etud_in_dept">')
|
||||
|
@ -383,13 +421,13 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
|
|||
"""
|
||||
result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
|
||||
|
||||
T = []
|
||||
rows = []
|
||||
for etuds in result:
|
||||
if etuds:
|
||||
dept_id = etuds[0]["dept"]
|
||||
for e in etuds:
|
||||
for sem in e["sems"]:
|
||||
T.append(
|
||||
rows.append(
|
||||
{
|
||||
"dept": dept_id,
|
||||
"etudid": e["etudid"],
|
||||
|
@ -414,6 +452,6 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
|
|||
"date_debut_iso",
|
||||
"date_fin_iso",
|
||||
)
|
||||
tab = GenTable(columns_ids=columns_ids, rows=T)
|
||||
tab = GenTable(columns_ids=columns_ids, rows=rows, table_id="inscr_etud_by_nip")
|
||||
|
||||
return tab.make_page(fmt=fmt, with_html_headers=False, publish=True)
|
||||
|
|
|
@ -649,20 +649,20 @@ def formation_list_table(detail: bool) -> GenTable:
|
|||
"semestres_ues": "Semestres avec UEs",
|
||||
}
|
||||
return GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
titles=titles,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
caption=title,
|
||||
html_caption=title,
|
||||
table_id="formation_list_table",
|
||||
html_class="formation_list_table table_leftalign",
|
||||
html_with_td_classes=True,
|
||||
html_sortable=True,
|
||||
base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
|
||||
caption=title,
|
||||
columns_ids=columns_ids,
|
||||
html_caption=title,
|
||||
html_class="formation_list_table table_leftalign",
|
||||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title=title,
|
||||
pdf_title=title,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=rows,
|
||||
table_id="formation_list_table",
|
||||
titles=titles,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -527,15 +527,16 @@ def table_formsemestres(
|
|||
preferences = sco_preferences.SemPreferences()
|
||||
tab = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=sems,
|
||||
titles=titles,
|
||||
html_class="table_leftalign",
|
||||
html_empty_element="<p><em>aucun résultat</em></p>",
|
||||
html_next_section=html_next_section,
|
||||
html_sortable=True,
|
||||
html_title=html_title,
|
||||
html_next_section=html_next_section,
|
||||
html_empty_element="<p><em>aucun résultat</em></p>",
|
||||
page_title="Semestres",
|
||||
preferences=preferences,
|
||||
rows=sems,
|
||||
table_id="table_formsemestres",
|
||||
titles=titles,
|
||||
)
|
||||
return tab
|
||||
|
||||
|
|
|
@ -726,20 +726,21 @@ def formsemestre_description_table(
|
|||
rows.append(sums)
|
||||
|
||||
return GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
titles=titles,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
|
||||
caption=title,
|
||||
columns_ids=columns_ids,
|
||||
html_caption=title,
|
||||
html_class="table_leftalign formsemestre_description",
|
||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
|
||||
page_title=title,
|
||||
html_title=html_sco_header.html_sem_header(
|
||||
"Description du semestre", with_page_header=False
|
||||
),
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title=title,
|
||||
pdf_title=title,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
rows=rows,
|
||||
table_id="formsemestre_description_table",
|
||||
titles=titles,
|
||||
)
|
||||
|
||||
|
||||
|
@ -821,6 +822,35 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
</div>
|
||||
</div>
|
||||
<div class="sem-groups-assi">
|
||||
|
||||
"""
|
||||
)
|
||||
if can_edit_abs:
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<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,
|
||||
)}">
|
||||
Saisir l'assiduité</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.bilan_dept",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Justificatifs en attente</a>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.visu_assi_group",
|
||||
|
@ -833,48 +863,20 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
if can_edit_abs:
|
||||
H.append(
|
||||
f"""
|
||||
<div>
|
||||
<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,
|
||||
)}">
|
||||
Visualiser</a>
|
||||
</div>
|
||||
<div>
|
||||
<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,
|
||||
)}">
|
||||
Saisie journalière</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.signal_assiduites_diff",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Saisie différée</a>
|
||||
)}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisir l'assiduité`">
|
||||
(Saisie différée)</a>
|
||||
</div>
|
||||
<div>
|
||||
<a class="stdlink" href="{
|
||||
url_for("assiduites.bilan_dept",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
Justificatifs en attente</a>
|
||||
</div>
|
||||
"""
|
||||
"""
|
||||
)
|
||||
|
||||
H.append("</div>") # /sem-groups-assi
|
||||
|
|
153
app/scodoc/sco_gen_cal.py
Normal file
153
app/scodoc/sco_gen_cal.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
"""
|
||||
Génération d'un calendrier
|
||||
(Classe abstraite à implémenter dans les classes filles)
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from flask import render_template
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import g
|
||||
|
||||
|
||||
class Jour:
|
||||
"""
|
||||
Représente un jour dans le calendrier
|
||||
Permet d'obtenir les informations sur le jour
|
||||
et générer une représentation html
|
||||
"""
|
||||
|
||||
def __init__(self, date: datetime.date):
|
||||
self.date = date
|
||||
self.class_list: list[str] = []
|
||||
|
||||
if self.is_non_work():
|
||||
self.class_list.append("non-travail")
|
||||
if self.is_current_week():
|
||||
self.class_list.append("sem-courante")
|
||||
|
||||
def get_nom(self, short=True):
|
||||
"""
|
||||
Renvoie le nom du jour
|
||||
"M19" ou "Mer 19"
|
||||
|
||||
par défaut en version courte
|
||||
"""
|
||||
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
|
||||
return (
|
||||
f"{str_jour[0] if short or self.is_non_work() else str_jour[:3]+' '}"
|
||||
+ f"{self.date.day}"
|
||||
)
|
||||
|
||||
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 is_current_week(self):
|
||||
"""
|
||||
Renvoie True si le jour est dans la semaine courante
|
||||
"""
|
||||
return self.date.isocalendar()[0:2] == datetime.date.today().isocalendar()[0:2]
|
||||
|
||||
def get_date(self) -> str:
|
||||
"""
|
||||
Renvoie la date du jour au format "dd/mm/yyyy"
|
||||
"""
|
||||
return self.date.strftime(scu.DATE_FMT)
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
Renvoie le code html du jour
|
||||
à surcharger dans les classes filles
|
||||
|
||||
l'html final ressemblera à :
|
||||
|
||||
<div class="jour {{jour.get_class()}}">
|
||||
<span class="nom">{{jour.get_nom()}}</span>
|
||||
<div class="contenu">
|
||||
{{jour.get_html() | safe}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
"""
|
||||
raise NotImplementedError("Méthode à implémenter dans les classes filles")
|
||||
|
||||
def get_class(self):
|
||||
"""
|
||||
Renvoie la classe css du jour
|
||||
|
||||
utilise self.class_list
|
||||
-> fait un join de la liste
|
||||
|
||||
"""
|
||||
return " ".join(self.class_list)
|
||||
|
||||
|
||||
class Calendrier:
|
||||
"""
|
||||
Représente un calendrier
|
||||
Permet d'obtenir les informations sur les jours
|
||||
et générer une représentation html
|
||||
"""
|
||||
|
||||
def __init__(self, date_debut: datetime.date, date_fin: datetime.date):
|
||||
self.date_debut = date_debut
|
||||
self.date_fin = date_fin
|
||||
self.jours: dict[str, list[Jour]] = {}
|
||||
|
||||
def _get_dates_between(self) -> list[datetime.date]:
|
||||
"""
|
||||
get_dates_between Renvoie la liste des dates entre date_debut et date_fin
|
||||
|
||||
Returns:
|
||||
list[datetime.date]: liste des dates entre date_debut et date_fin
|
||||
"""
|
||||
resultat = []
|
||||
date_actuelle: datetime.date = self.date_debut
|
||||
while date_actuelle <= self.date_fin:
|
||||
if isinstance(date_actuelle, datetime.datetime):
|
||||
resultat.append(date_actuelle.date())
|
||||
elif isinstance(date_actuelle, datetime.date):
|
||||
resultat.append(date_actuelle)
|
||||
date_actuelle += datetime.timedelta(days=1)
|
||||
return resultat
|
||||
|
||||
def organize_by_month(self):
|
||||
"""
|
||||
Organise les jours par mois
|
||||
Instancie un objet Jour pour chaque jour
|
||||
|
||||
met à jour self.jours
|
||||
"""
|
||||
organized = {}
|
||||
for date in self._get_dates_between():
|
||||
# Récupérer le mois en français
|
||||
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
|
||||
# Ajouter le jour à la liste correspondante au mois
|
||||
if month not in organized:
|
||||
organized[month] = []
|
||||
|
||||
jour: Jour = self.instanciate_jour(date)
|
||||
|
||||
organized[month].append(jour)
|
||||
|
||||
self.jours = organized
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> Jour:
|
||||
"""
|
||||
Instancie un objet Jour pour chaque jour
|
||||
A surcharger dans les classes filles si besoin
|
||||
"""
|
||||
raise NotImplementedError("Méthode à implémenter dans les classes filles")
|
||||
|
||||
def get_html(self):
|
||||
"""
|
||||
get_html Renvoie le code html du calendrier
|
||||
"""
|
||||
self.organize_by_month()
|
||||
return render_template("calendrier.j2", calendrier=self.jours)
|
|
@ -92,5 +92,6 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
|
|||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="groups_export_annotations",
|
||||
)
|
||||
return table.make_page(fmt=fmt)
|
||||
|
|
|
@ -661,6 +661,7 @@ def groups_table(
|
|||
text_fields_separator=prefs["moodle_csv_separator"],
|
||||
text_with_titles=prefs["moodle_csv_with_headerline"],
|
||||
preferences=prefs,
|
||||
table_id="groups_table",
|
||||
)
|
||||
#
|
||||
if fmt == "html":
|
||||
|
@ -1028,10 +1029,9 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
|
|||
moodle_sem_name = sem["session_id"]
|
||||
|
||||
columns_ids = ("email", "semestre_groupe")
|
||||
T = []
|
||||
for partition_id in partitions_etud_groups:
|
||||
rows = []
|
||||
for partition_id, members in partitions_etud_groups.items():
|
||||
partition = sco_groups.get_partition(partition_id)
|
||||
members = partitions_etud_groups[partition_id]
|
||||
for etudid in members:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
group_name = members[etudid]["group_name"]
|
||||
|
@ -1040,16 +1040,17 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
|
|||
elts.append(partition["partition_name"])
|
||||
if group_name:
|
||||
elts.append(group_name)
|
||||
T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
|
||||
rows.append({"email": etud["email"], "semestre_groupe": "-".join(elts)})
|
||||
# Make table
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||
tab = GenTable(
|
||||
rows=T,
|
||||
columns_ids=("email", "semestre_groupe"),
|
||||
filename=moodle_sem_name + "-moodle",
|
||||
titles={x: x for x in columns_ids},
|
||||
preferences=prefs,
|
||||
rows=rows,
|
||||
text_fields_separator=prefs["moodle_csv_separator"],
|
||||
text_with_titles=prefs["moodle_csv_with_headerline"],
|
||||
preferences=prefs,
|
||||
table_id="export_groups_as_moodle_csv",
|
||||
titles={x: x for x in columns_ids},
|
||||
)
|
||||
return tab.make_page(fmt="csv")
|
||||
|
|
|
@ -834,11 +834,12 @@ def adm_table_description_format():
|
|||
columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
|
||||
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=list(Fmt.values()),
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=list(Fmt.values()),
|
||||
table_id="adm_table_description_format",
|
||||
titles=titles,
|
||||
)
|
||||
return tab
|
||||
|
|
|
@ -747,10 +747,11 @@ def etuds_select_box_xls(src_cat):
|
|||
else:
|
||||
e["paiementinscription_str"] = "-"
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=etuds,
|
||||
caption="%(title)s. %(help)s" % src_cat["infos"],
|
||||
columns_ids=columns_ids,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=etuds,
|
||||
table_id="etuds_select_box_xls",
|
||||
titles=titles,
|
||||
)
|
||||
return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"])
|
||||
|
|
|
@ -599,20 +599,21 @@ def _make_table_notes(
|
|||
)
|
||||
# display
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
html_sortable=True,
|
||||
base_url=base_url,
|
||||
filename=filename,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
caption=caption,
|
||||
html_next_section=html_next_section,
|
||||
page_title="Notes de " + formsemestre.titre_mois(),
|
||||
html_title=html_title,
|
||||
pdf_title=pdf_title,
|
||||
columns_ids=columns_ids,
|
||||
filename=filename,
|
||||
html_class="notes_evaluation",
|
||||
html_next_section=html_next_section,
|
||||
html_sortable=True,
|
||||
html_title=html_title,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title="Notes de " + formsemestre.titre_mois(),
|
||||
pdf_title=pdf_title,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
||||
rows=rows,
|
||||
table_id="table-liste-notes",
|
||||
titles=titles,
|
||||
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete
|
||||
)
|
||||
if fmt == "bordereau":
|
||||
|
|
|
@ -180,6 +180,7 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
|
|||
html_class="table_leftalign table_listegroupe",
|
||||
bottom_titles=bottom_titles,
|
||||
preferences=preferences,
|
||||
table_id="table_etuds_lycees",
|
||||
)
|
||||
return tab, etuds_by_lycee
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
|
|||
can_edit_notes_ens = modimpl.can_edit_notes(current_user)
|
||||
|
||||
if can_edit_notes and nbnotes != 0:
|
||||
sup_label = "Supprimer évaluation impossible (il y a des notes)"
|
||||
sup_label = "Suppression évaluation impossible (il y a des notes)"
|
||||
else:
|
||||
sup_label = "Supprimer évaluation"
|
||||
|
||||
|
@ -344,9 +344,34 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
#
|
||||
if not modimpl.check_apc_conformity(nt):
|
||||
H.append(
|
||||
"""<div class="warning conformite">Les poids des évaluations de ce module ne sont
|
||||
pas encore conformes au PN.
|
||||
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
|
||||
"""<div class="warning conformite">Les poids des évaluations de ce
|
||||
module ne permettent pas d'évaluer toutes les UEs (compétences)
|
||||
prévues par les coefficients du programme.
|
||||
<b>Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.</b>
|
||||
Vérifiez les poids des évaluations.
|
||||
</div>"""
|
||||
)
|
||||
if not modimpl.check_apc_conformity(
|
||||
nt, evaluation_type=Evaluation.EVALUATION_SESSION2
|
||||
):
|
||||
H.append(
|
||||
"""<div class="warning conformite">
|
||||
Il y a des évaluations de <b>deuxième session</b>
|
||||
mais leurs poids ne permettent pas d'évaluer toutes les UEs (compétences)
|
||||
prévues par les coefficients du programme.
|
||||
La deuxième session ne sera donc <b>pas prise en compte</b>.
|
||||
Vérifiez les poids de ces évaluations.
|
||||
</div>"""
|
||||
)
|
||||
if not modimpl.check_apc_conformity(
|
||||
nt, evaluation_type=Evaluation.EVALUATION_RATTRAPAGE
|
||||
):
|
||||
H.append(
|
||||
"""<div class="warning conformite">
|
||||
Il y a des évaluations de <b>rattrapage</b>
|
||||
mais leurs poids n'évaluent pas toutes les UEs (compétences)
|
||||
prévues par les coefficients du programme.
|
||||
Vérifiez les poids de ces évaluations.
|
||||
</div>"""
|
||||
)
|
||||
|
||||
|
|
|
@ -351,7 +351,7 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
canv.drawString(
|
||||
self.preferences["pdf_footer_x"] * mm,
|
||||
self.preferences["pdf_footer_y"] * mm,
|
||||
content,
|
||||
content + " " + (self.preferences["pdf_footer_extra"] or ""),
|
||||
)
|
||||
canv.restoreState()
|
||||
|
||||
|
@ -382,11 +382,11 @@ class ScoDocPageTemplate(PageTemplate):
|
|||
filigranne = self.filigranne.get(doc.page, None)
|
||||
if filigranne:
|
||||
canv.saveState()
|
||||
canv.translate(9 * cm, 27.6 * cm)
|
||||
canv.rotate(30)
|
||||
canv.scale(4.5, 4.5)
|
||||
canv.translate(10 * cm, 21.0 * cm)
|
||||
canv.rotate(36)
|
||||
canv.scale(7, 7)
|
||||
canv.setFillColorRGB(1.0, 0.65, 0.65, alpha=0.6)
|
||||
canv.drawRightString(0, 0, SU(filigranne))
|
||||
canv.drawCentredString(0, 0, SU(filigranne))
|
||||
canv.restoreState()
|
||||
doc.filigranne = None
|
||||
|
||||
|
|
|
@ -378,6 +378,7 @@ class PlacementRunner:
|
|||
preferences=sco_preferences.SemPreferences(
|
||||
self.moduleimpl_data["formsemestre_id"]
|
||||
),
|
||||
table_id="placement_pdf",
|
||||
)
|
||||
return tab.make_page(fmt="pdf", with_html_headers=False)
|
||||
|
||||
|
|
|
@ -221,6 +221,7 @@ def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
|
|||
html_class="table_leftalign table_listegroupe",
|
||||
pdf_link=False, # pas d'export pdf
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="formsemestre_poursuite_report",
|
||||
)
|
||||
tab.filename = scu.make_filename("poursuite " + sem["titreannee"])
|
||||
|
||||
|
|
|
@ -110,6 +110,7 @@ get_base_preferences(formsemestre_id)
|
|||
Return base preferences for current scodoc_dept (instance BasePreferences)
|
||||
|
||||
"""
|
||||
|
||||
import flask
|
||||
from flask import current_app, flash, g, request, url_for
|
||||
|
||||
|
@ -611,26 +612,15 @@ class BasePreferences:
|
|||
"explanation": "toute saisie d'absence doit indiquer le module concerné",
|
||||
},
|
||||
),
|
||||
# (
|
||||
# "forcer_present",
|
||||
# {
|
||||
# "initvalue": 0,
|
||||
# "title": "Forcer l'appel des présents",
|
||||
# "input_type": "boolcheckbox",
|
||||
# "labels": ["non", "oui"],
|
||||
# "category": "assi",
|
||||
# },
|
||||
# ),
|
||||
(
|
||||
"periode_defaut",
|
||||
"non_present",
|
||||
{
|
||||
"initvalue": 2.0,
|
||||
"size": 10,
|
||||
"title": "Durée par défaut d'un créneau",
|
||||
"type": "float",
|
||||
"initvalue": 0,
|
||||
"title": "Désactiver la saisie des présences",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "assi",
|
||||
"only_global": True,
|
||||
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie",
|
||||
"explanation": "Désactive la saisie et l'affichage des présences",
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -644,18 +634,18 @@ class BasePreferences:
|
|||
"category": "assi",
|
||||
},
|
||||
),
|
||||
(
|
||||
"assi_etat_defaut",
|
||||
{
|
||||
"explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||
"initvalue": "aucun",
|
||||
"input_type": "menu",
|
||||
"labels": ["aucun", "present", "retard", "absent"],
|
||||
"allowed_values": ["aucun", "present", "retard", "absent"],
|
||||
"title": "Définir l'état par défaut",
|
||||
"category": "assi",
|
||||
},
|
||||
),
|
||||
# (
|
||||
# "assi_etat_defaut",
|
||||
# {
|
||||
# "explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||
# "initvalue": "aucun",
|
||||
# "input_type": "menu",
|
||||
# "labels": ["aucun", "present", "retard", "absent"],
|
||||
# "allowed_values": ["aucun", "present", "retard", "absent"],
|
||||
# "title": "Définir l'état par défaut",
|
||||
# "category": "assi",
|
||||
# },
|
||||
# ),
|
||||
(
|
||||
"non_travail",
|
||||
{
|
||||
|
@ -962,6 +952,16 @@ class BasePreferences:
|
|||
"category": "pdf",
|
||||
},
|
||||
),
|
||||
(
|
||||
"pdf_footer_extra",
|
||||
{
|
||||
"initvalue": "",
|
||||
"title": "Texte à ajouter en pied de page",
|
||||
"explanation": "sur tous les documents, par exemple vos coordonnées, ...",
|
||||
"size": 78,
|
||||
"category": "pdf",
|
||||
},
|
||||
),
|
||||
(
|
||||
"pdf_footer_x",
|
||||
{
|
||||
|
@ -2291,9 +2291,7 @@ class BasePreferences:
|
|||
if "explanation" in descr:
|
||||
del descr["explanation"]
|
||||
if formsemestre_id:
|
||||
descr[
|
||||
"explanation"
|
||||
] = f"""ou <span class="spanlink"
|
||||
descr["explanation"] = f"""ou <span class="spanlink"
|
||||
onclick="set_global_pref(this, '{pref_name}');"
|
||||
>utiliser paramètre global</span>"""
|
||||
if formsemestre_id and self.is_global(formsemestre_id, pref_name):
|
||||
|
|
|
@ -42,7 +42,6 @@ from app.models import (
|
|||
but_validations,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_cursus
|
||||
|
@ -81,6 +80,7 @@ def dict_pvjury(
|
|||
},
|
||||
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
||||
'validation_parcours' : True si parcours validé (diplome obtenu)
|
||||
'parcours' : 'S1, S2, S3, S4, A1',
|
||||
'prev_code' : code (calculé slt si with_prev),
|
||||
'mention' : mention (en fct moy gen),
|
||||
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
||||
|
@ -107,10 +107,12 @@ def dict_pvjury(
|
|||
D = {} # même chose que decisions, mais { etudid : dec }
|
||||
for etudid in etudids:
|
||||
etud = Identite.get_etud(etudid)
|
||||
Se = sco_cursus.get_situation_etud_cursus(
|
||||
situation_etud = sco_cursus.get_situation_etud_cursus(
|
||||
etud.to_dict_scodoc7(), formsemestre_id
|
||||
)
|
||||
semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal
|
||||
semestre_non_terminal = (
|
||||
semestre_non_terminal or situation_etud.semestre_non_terminal
|
||||
)
|
||||
d = {}
|
||||
d["identite"] = nt.identdict[etudid]
|
||||
d["etat"] = nt.get_etud_etat(
|
||||
|
@ -120,9 +122,8 @@ def dict_pvjury(
|
|||
d["decisions_ue"] = nt.get_etud_decisions_ue(etudid)
|
||||
if formsemestre.formation.is_apc():
|
||||
d.update(but_validations.dict_decision_jury(etud, formsemestre))
|
||||
d["last_formsemestre_id"] = Se.get_semestres()[
|
||||
-1
|
||||
] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit
|
||||
# id du dernier semestre (chronologiquement) dans lequel il a été inscrit:
|
||||
d["last_formsemestre_id"] = situation_etud.get_semestres()[-1]
|
||||
|
||||
ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
|
||||
d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values())
|
||||
|
@ -162,10 +163,13 @@ def dict_pvjury(
|
|||
d["autorisations"] = [a.to_dict() for a in autorisations]
|
||||
d["autorisations_descr"] = descr_autorisations(autorisations)
|
||||
|
||||
d["validation_parcours"] = Se.parcours_validated()
|
||||
d["parcours"] = Se.get_cursus_descr(filter_futur=True)
|
||||
d["validation_parcours"] = situation_etud.parcours_validated()
|
||||
d["parcours"] = situation_etud.get_cursus_descr(filter_futur=True)
|
||||
d["parcours_in_cur_formation"] = situation_etud.get_cursus_descr(
|
||||
filter_futur=True, filter_formation_code=True
|
||||
)
|
||||
if with_parcours_decisions:
|
||||
d["parcours_decisions"] = Se.get_parcours_decisions()
|
||||
d["parcours_decisions"] = situation_etud.get_parcours_decisions()
|
||||
# Observations sur les compensations:
|
||||
compensators = sco_cursus_dut.scolar_formsemestre_validation_list(
|
||||
cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||
|
@ -206,19 +210,19 @@ def dict_pvjury(
|
|||
if not info:
|
||||
continue # should not occur
|
||||
etud = info[0]
|
||||
if Se.prev and Se.prev_decision:
|
||||
d["prev_decision_sem"] = Se.prev_decision
|
||||
d["prev_code"] = Se.prev_decision["code"]
|
||||
if situation_etud.prev and situation_etud.prev_decision:
|
||||
d["prev_decision_sem"] = situation_etud.prev_decision
|
||||
d["prev_code"] = situation_etud.prev_decision["code"]
|
||||
d["prev_code_descr"] = _descr_decision_sem(
|
||||
scu.INSCRIT, Se.prev_decision
|
||||
scu.INSCRIT, situation_etud.prev_decision
|
||||
)
|
||||
d["prev"] = Se.prev
|
||||
d["prev"] = situation_etud.prev
|
||||
has_prev = True
|
||||
else:
|
||||
d["prev_decision_sem"] = None
|
||||
d["prev_code"] = ""
|
||||
d["prev_code_descr"] = ""
|
||||
d["Se"] = Se
|
||||
d["Se"] = situation_etud
|
||||
|
||||
decisions.append(d)
|
||||
D[etudid] = d
|
||||
|
|
|
@ -149,7 +149,7 @@ def pvjury_table(
|
|||
etudid=e["identite"]["etudid"],
|
||||
),
|
||||
"_nomprenom_td_attrs": f"""id="{e['identite']['etudid']}" class="etudinfo" """,
|
||||
"parcours": e["parcours"],
|
||||
"parcours": e["parcours_in_cur_formation"],
|
||||
"decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]),
|
||||
"ue_cap": e["decisions_ue_descr"],
|
||||
"validation_parcours_code": "ADM" if e["validation_parcours"] else "",
|
||||
|
@ -252,6 +252,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
|
|||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="formsemestre_pvjury",
|
||||
)
|
||||
if fmt != "html":
|
||||
return tab.make_page(
|
||||
|
@ -312,6 +313,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
|
|||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="formsemestre_pvjury_counts",
|
||||
).html()
|
||||
)
|
||||
H.append(
|
||||
|
|
|
@ -198,9 +198,9 @@ def formsemestre_recapcomplet(
|
|||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||
}">Calcul automatique des décisions du jury</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase',
|
||||
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_erase',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
|
||||
}">Effacer <em>toutes</em> les décisions de jury BUT issues de ce semestre</a>
|
||||
}">Effacer <em>toutes</em> les décisions de jury issues de ce semestre</a>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
|
|
|
@ -236,6 +236,7 @@ def _results_by_category(
|
|||
html_col_width="4em",
|
||||
html_sortable=True,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id=f"results_by_category-{category_name}",
|
||||
)
|
||||
|
||||
|
||||
|
@ -695,19 +696,18 @@ def table_suivi_cohorte(
|
|||
if statut:
|
||||
dbac += " statut: %s" % statut
|
||||
tab = GenTable(
|
||||
titles=titles,
|
||||
caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
|
||||
columns_ids=columns_ids,
|
||||
rows=L,
|
||||
filename=scu.make_filename("cohorte " + sem["titreannee"]),
|
||||
html_class="table_cohorte",
|
||||
html_col_width="4em",
|
||||
html_sortable=True,
|
||||
filename=scu.make_filename("cohorte " + sem["titreannee"]),
|
||||
origin="Généré par %s le " % sco_version.SCONAME
|
||||
+ scu.timedate_human_repr()
|
||||
+ "",
|
||||
caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
page_title="Suivi cohorte " + sem["titreannee"],
|
||||
html_class="table_cohorte",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
||||
rows=L,
|
||||
table_id="table_suivi_cohorte",
|
||||
titles=titles,
|
||||
)
|
||||
# Explication: liste des semestres associés à chaque date
|
||||
if not P:
|
||||
|
@ -1304,6 +1304,7 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
|
|||
"code_cursus": len(etuds),
|
||||
},
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="table_suivi_cursus",
|
||||
)
|
||||
return tab
|
||||
|
||||
|
|
|
@ -87,15 +87,16 @@ def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
|
|||
bacs.append("Total")
|
||||
|
||||
tab = GenTable(
|
||||
titles={bac: bac for bac in bacs},
|
||||
columns_ids=["titre_indicateur"] + bacs,
|
||||
rows=rows,
|
||||
html_sortable=False,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"),
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
html_caption="Indicateurs BUT annuels.",
|
||||
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
|
||||
columns_ids=["titre_indicateur"] + bacs,
|
||||
filename=scu.make_filename(f"Indicateurs_BUT_{formsemestre.titre_annee()}"),
|
||||
html_caption="Indicateurs BUT annuels.",
|
||||
html_sortable=False,
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
rows=rows,
|
||||
titles={bac: bac for bac in bacs},
|
||||
table_id="formsemestre_but_indicateurs",
|
||||
)
|
||||
title = "Indicateurs suivi annuel BUT"
|
||||
t = tab.make_page(
|
||||
|
|
|
@ -378,10 +378,9 @@ class SemSet(dict):
|
|||
|
||||
def html_diagnostic(self):
|
||||
"""Affichage de la partie Effectifs et Liste des étudiants
|
||||
(actif seulement si un portail est configuré) XXX pourquoi ??
|
||||
(actif seulement si un portail est configuré)
|
||||
"""
|
||||
if sco_portal_apogee.has_portal():
|
||||
return self.bilan.html_diagnostic()
|
||||
return self.bilan.html_diagnostic()
|
||||
return ""
|
||||
|
||||
|
||||
|
@ -482,10 +481,9 @@ def semset_page(fmt="html"):
|
|||
# (remplacé par n liens vers chacun des semestres)
|
||||
# s['_semtitles_str_target'] = s['_export_link_target']
|
||||
# Experimental:
|
||||
s[
|
||||
"_title_td_attrs"
|
||||
] = 'class="inplace_edit" data-url="edit_semset_set_title" id="%s"' % (
|
||||
s["semset_id"]
|
||||
s["_title_td_attrs"] = (
|
||||
'class="inplace_edit" data-url="edit_semset_set_title" id="%s"'
|
||||
% (s["semset_id"])
|
||||
)
|
||||
|
||||
tab = GenTable(
|
||||
|
@ -513,6 +511,7 @@ def semset_page(fmt="html"):
|
|||
html_class="table_leftalign",
|
||||
filename="semsets",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="table-semsets",
|
||||
)
|
||||
if fmt != "html":
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
|
|
@ -115,7 +115,8 @@ def formsemestre_synchro_etuds(
|
|||
url_for('notes.formsemestre_editwithmodules',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}">Modifier ce semestre</a>)
|
||||
"""
|
||||
""",
|
||||
safe=True,
|
||||
)
|
||||
footer = html_sco_header.sco_footer()
|
||||
base_url = url_for(
|
||||
|
|
|
@ -169,6 +169,7 @@ def evaluation_list_operations(evaluation_id):
|
|||
preferences=sco_preferences.SemPreferences(
|
||||
evaluation.moduleimpl.formsemestre_id
|
||||
),
|
||||
table_id="evaluation_list_operations",
|
||||
)
|
||||
return tab.make_page()
|
||||
|
||||
|
@ -241,6 +242,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
|
|||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
|
||||
origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "",
|
||||
table_id="formsemestre_list_saisies_notes",
|
||||
)
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
||||
|
|
|
@ -239,6 +239,7 @@ def list_users(
|
|||
base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0),
|
||||
pdf_link=False, # table is too wide to fit in a paper page => disable pdf
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="list-users",
|
||||
)
|
||||
|
||||
return tab.make_page(fmt=fmt, with_html_headers=False)
|
||||
|
|
|
@ -719,6 +719,7 @@ SCO_DEV_MAIL = "emmanuel.viennet@gmail.com" # SVP ne pas changer
|
|||
# ne pas changer (ou vous perdez le support)
|
||||
SCO_DUMP_UP_URL = "https://scodoc.org/scodoc-installmgr/upload-dump"
|
||||
SCO_UP2DATE = "https://scodoc.org/scodoc-installmgr/check_version"
|
||||
SCO_BUG_REPORT_URL = "https://scodoc.org/scodoc-installmgr/report"
|
||||
SCO_ORG_TIMEOUT = 180 # contacts scodoc.org
|
||||
SCO_EXT_TIMEOUT = 180 # appels à des ressources extérieures (siret, ...)
|
||||
SCO_TEST_API_TIMEOUT = 5 # pour tests unitaires API
|
||||
|
@ -843,7 +844,7 @@ FORBIDDEN_CHARS_EXP = re.compile(r"[*\|~\(\)\\]")
|
|||
ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE)
|
||||
|
||||
|
||||
def is_valid_code_nip(s: str) -> bool:
|
||||
def is_valid_code_nip(s: str) -> bool | None:
|
||||
"""True si s peut être un code NIP: au moins 6 chiffres décimaux"""
|
||||
if not s:
|
||||
return False
|
||||
|
|
|
@ -302,7 +302,6 @@
|
|||
.rbtn {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -327,9 +326,12 @@
|
|||
background-image: url(../icons/absent.svg);
|
||||
}
|
||||
|
||||
.rbtn.aucun::before {
|
||||
background-image: url(../icons/aucun.svg);
|
||||
background-color: var(--color-defaut-dark);
|
||||
.rbtn.aucun {
|
||||
background-image: url("../icons/delete.svg");
|
||||
background-size: calc(100% - 8px) calc(100% - 8px);
|
||||
/* Adjust size to create "margin" */
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.rbtn.retard::before {
|
||||
|
@ -730,31 +732,11 @@ tr.row-justificatif.non_valide td.assi-type {
|
|||
background-color: var(--color-defaut) !important;
|
||||
}
|
||||
|
||||
.color.est_just.sans_etat::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
background-color: var(--color-justi) !important;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.color.invalide::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
.color.invalide {
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
|
||||
.color.attente::before,
|
||||
.color.modifie::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
.color.attente {
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
|
@ -762,6 +744,10 @@ tr.row-justificatif.non_valide td.assi-type {
|
|||
var(--color-justi-attente) 7px) !important;
|
||||
}
|
||||
|
||||
.color.est_just {
|
||||
background-color: var(--color-justi) !important;
|
||||
}
|
||||
|
||||
#gtrcontent .pdp {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
.day .dayline {
|
||||
.jour .dayline {
|
||||
position: absolute;
|
||||
display: none;
|
||||
top: 100%;
|
||||
z-index: 50;
|
||||
width: max-content;
|
||||
height: 75px;
|
||||
background-color: #dedede;
|
||||
border-radius: 15px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
padding: 5px 5px 15px 5px;
|
||||
transform: translateX(-50%);
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
.day:hover .dayline {
|
||||
.jour:hover .dayline {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.dayline .mini-timeline {
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
|
|
@ -4831,7 +4831,9 @@ table.evaluations_recap th.titre {
|
|||
}
|
||||
|
||||
table.evaluations_recap td.complete,
|
||||
table.evaluations_recap th.complete {
|
||||
table.evaluations_recap th.complete,
|
||||
table.evaluations_recap td.type_evaluation,
|
||||
table.evaluations_recap th.type_evaluation {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
|
1
app/static/icons/delete.svg
Normal file
1
app/static/icons/delete.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Layer_1" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m170.8 14.221a14.21 14.21 0 0 1 14.2-14.207l141.991-.008a14.233 14.233 0 0 1 14.2 14.223v35.117h-170.391zm233.461 477.443a21.75 21.75 0 0 1 -21.856 20.33h-254.451a21.968 21.968 0 0 1 -21.854-20.416l-21.774-318.518h343.174l-23.234 318.6zm56.568-347.452h-409.658v-33a33.035 33.035 0 0 1 33.005-33.012l343.644-.011a33.051 33.051 0 0 1 33 33.02v33zm-270.79 291.851a14.422 14.422 0 1 0 28.844 0v-202.247a14.42 14.42 0 0 0 -28.839-.01v202.257zm102.9 0a14.424 14.424 0 1 0 28.848 0v-202.247a14.422 14.422 0 0 0 -28.843-.01z" fill="#fc3333" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 689 B |
|
@ -53,7 +53,7 @@ async function async_get(path, success, errors) {
|
|||
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
|
||||
*/
|
||||
async function async_post(path, data, success, errors) {
|
||||
console.log("async_post " + path);
|
||||
// console.log("async_post " + path);
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(path, {
|
||||
|
@ -296,7 +296,13 @@ function creerLigneEtudiant(etud, index) {
|
|||
// Création des boutons d'assiduités
|
||||
if (readOnly) {
|
||||
} else if (currentAssiduite.type != "conflit") {
|
||||
["present", "retard", "absent"].forEach((abs) => {
|
||||
const etats = ["retard", "absent"];
|
||||
|
||||
if (!window.nonPresent) {
|
||||
etats.splice(0, 0, "present");
|
||||
}
|
||||
|
||||
etats.forEach((abs) => {
|
||||
const btn = document.createElement("input");
|
||||
btn.type = "checkbox";
|
||||
btn.value = abs;
|
||||
|
@ -395,7 +401,7 @@ async function creerTousLesEtudiants(etuds) {
|
|||
* @returns {String}
|
||||
*/
|
||||
async function getModuleImpl(assiduite) {
|
||||
if (assiduite == null) return "Pas de module";
|
||||
if (assiduite == null) return "Module non spécifié";
|
||||
const id = assiduite.moduleimpl_id;
|
||||
|
||||
if (id == null || id == undefined) {
|
||||
|
@ -408,7 +414,7 @@ async function getModuleImpl(assiduite) {
|
|||
? "Autre module (pas dans la liste)"
|
||||
: assiduite.external_data.module;
|
||||
} else {
|
||||
return "Pas de module";
|
||||
return "Module non spécifié";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -425,7 +431,7 @@ async function getModuleImpl(assiduite) {
|
|||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ''}`;
|
||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ""}`;
|
||||
return moduleimpls[id];
|
||||
})
|
||||
.catch((_) => {
|
||||
|
@ -531,12 +537,7 @@ async function MiseAJourLigneEtud(etud) {
|
|||
|
||||
async function actionAssiduite(etud, etat, type, assiduite = null) {
|
||||
const modimpl_id = $("#moduleimpl_select").val();
|
||||
if (
|
||||
assiduite &&
|
||||
assiduite.etat.toLowerCase() === etat &&
|
||||
assiduite.moduleimpl_id == modimpl_id
|
||||
)
|
||||
type = "suppression";
|
||||
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
|
||||
|
||||
const { deb, fin } = getPeriodAsDate();
|
||||
|
||||
|
@ -642,6 +643,9 @@ function mettreToutLeMonde(etat, el = null) {
|
|||
|
||||
// Suppression des assiduités
|
||||
if (etat == "vide") {
|
||||
if (!confirm("Effacer tout les évènements correspondant à cette plage ?")) {
|
||||
return; // annulation
|
||||
}
|
||||
const assiduites_id = lignesEtuds
|
||||
.filter((e) => e.getAttribute("type") == "edition")
|
||||
.map((e) => Number(e.getAttribute("assiduite_id")));
|
||||
|
@ -758,6 +762,7 @@ function envoiToastEtudiant(etat, etud) {
|
|||
pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5));
|
||||
}
|
||||
|
||||
// TODO commenter toutes les fonctions js
|
||||
function envoiToastTous(etat, count) {
|
||||
const span = document.createElement("span");
|
||||
let etatAffiche = etat;
|
||||
|
@ -797,13 +802,16 @@ function estJourTravail(jour, nonWorkdays) {
|
|||
return !nonWorkdays.includes(d);
|
||||
}
|
||||
|
||||
function retourJourTravail(date) {
|
||||
function retourJourTravail(date, anti = true) {
|
||||
const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms
|
||||
let jour = date;
|
||||
let compte = 0;
|
||||
|
||||
while (!estJourTravail(jour, nonWorkDays) && compte++ < 7) {
|
||||
jour = new Date(jour - jourMiliSecondes);
|
||||
let temps = anti
|
||||
? jour - jourMiliSecondes
|
||||
: jour.valueOf() + jourMiliSecondes;
|
||||
jour = new Date(temps);
|
||||
}
|
||||
return jour;
|
||||
}
|
||||
|
@ -813,9 +821,12 @@ function dateCouranteEstTravaillee() {
|
|||
if (!estJourTravail(date, nonWorkDays)) {
|
||||
const nouvelleDate = retourJourTravail(date);
|
||||
$("#date").datepicker("setDate", nouvelleDate);
|
||||
|
||||
let msg = "Le jour sélectionné";
|
||||
if ((new Date()).format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) {
|
||||
msg = "Aujourd'hui";
|
||||
}
|
||||
const att = document.createTextNode(
|
||||
`Le jour sélectionné (${Date.toFRA(
|
||||
`${msg} (${Date.toFRA(
|
||||
date.format("YYYY-MM-DD")
|
||||
)}) n'est pas un jour travaillé.`
|
||||
);
|
||||
|
@ -836,6 +847,17 @@ function dateCouranteEstTravaillee() {
|
|||
return true;
|
||||
}
|
||||
|
||||
function jourSuivant(anti = false) {
|
||||
let date = $("#date").datepicker("getDate");
|
||||
|
||||
date = anti ? date.add(-1, "days") : date.add(1, "days");
|
||||
|
||||
const nouvelleDate = retourJourTravail(date, anti);
|
||||
|
||||
$("#date").datepicker("setDate", nouvelleDate);
|
||||
creerTousLesEtudiants(etuds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajout de la visualisation des assiduités de la mini timeline
|
||||
* @param {HTMLElement} el l'élément survollé
|
||||
|
@ -875,6 +897,11 @@ function setupAssiduiteBubble(el, assiduite) {
|
|||
actionsDiv.appendChild(infos);
|
||||
bubble.appendChild(actionsDiv);
|
||||
|
||||
const stateDiv = document.createElement("div");
|
||||
stateDiv.className = "assiduite-state";
|
||||
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
|
||||
bubble.appendChild(stateDiv);
|
||||
|
||||
const idDiv = document.createElement("div");
|
||||
idDiv.className = "assiduite-id";
|
||||
getModuleImpl(assiduite).then((modImpl) => {
|
||||
|
@ -882,26 +909,32 @@ function setupAssiduiteBubble(el, assiduite) {
|
|||
});
|
||||
bubble.appendChild(idDiv);
|
||||
|
||||
const periodDivDeb = document.createElement("div");
|
||||
periodDivDeb.className = "assiduite-period";
|
||||
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
|
||||
bubble.appendChild(periodDivDeb);
|
||||
const periodDivFin = document.createElement("div");
|
||||
periodDivFin.className = "assiduite-period";
|
||||
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
|
||||
bubble.appendChild(periodDivFin);
|
||||
// Affichage des dates
|
||||
// si les jours sont les mêmes, on affiche "jour hh:mm - hh:mm"
|
||||
// sinon on affiche "jour hh:mm - jour hh:mm"
|
||||
const periodDiv = document.createElement("div");
|
||||
periodDiv.className = "assiduite-period";
|
||||
const dateDeb = new Date(Date.removeUTC(assiduite.date_debut));
|
||||
const dateFin = new Date(Date.removeUTC(assiduite.date_fin));
|
||||
if (dateDeb.isSame(dateFin, "day")) {
|
||||
const jour = dateDeb.format("DD/MM/YYYY");
|
||||
const deb = dateDeb.format("HH:mm");
|
||||
const fin = dateFin.format("HH:mm");
|
||||
periodDiv.textContent = `${jour} de ${deb} à ${fin}`;
|
||||
} else {
|
||||
const jourDeb = dateDeb.format("DD/MM/YYYY");
|
||||
const jourFin = dateFin.format("DD/MM/YYYY");
|
||||
periodDiv.textContent = `du ${jourDeb} au ${jourFin}`;
|
||||
}
|
||||
|
||||
const stateDiv = document.createElement("div");
|
||||
stateDiv.className = "assiduite-state";
|
||||
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
|
||||
bubble.appendChild(stateDiv);
|
||||
bubble.appendChild(periodDiv);
|
||||
|
||||
const motifDiv = document.createElement("div");
|
||||
stateDiv.className = "assiduite-why";
|
||||
motifDiv.className = "assiduite-why";
|
||||
const motif = ["", null, undefined].includes(assiduite.desc)
|
||||
? "Pas de motif"
|
||||
? "Non spécifié"
|
||||
: assiduite.desc.capitalize();
|
||||
stateDiv.textContent = `Motif: ${motif}`;
|
||||
motifDiv.textContent = `Motif: ${motif}`;
|
||||
bubble.appendChild(motifDiv);
|
||||
|
||||
const userIdDiv = document.createElement("div");
|
||||
|
|
|
@ -155,18 +155,9 @@ function get_query_args() {
|
|||
|
||||
// Tables (gen_tables)
|
||||
$(function () {
|
||||
if ($("table.gt_table").length > 0) {
|
||||
const url = new URL(document.URL);
|
||||
const order_info_key = JSON.stringify(["table_order", url.pathname]);
|
||||
let order_info;
|
||||
const x = localStorage.getItem(order_info_key);
|
||||
if (x) {
|
||||
try {
|
||||
order_info = JSON.parse(x);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
if ($("table.gt_table, table.gt_table_searchable").length > 0) {
|
||||
|
||||
|
||||
var table_options = {
|
||||
paging: false,
|
||||
searching: false,
|
||||
|
@ -178,20 +169,46 @@ $(function () {
|
|||
},
|
||||
orderCellsTop: true, // cellules ligne 1 pour tri
|
||||
aaSorting: [], // Prevent initial sorting
|
||||
order: order_info,
|
||||
order: "",
|
||||
drawCallback: function (settings) {
|
||||
// permet de conserver l'ordre de tri des colonnes
|
||||
let table = $("table.gt_table").DataTable();
|
||||
let order_info = JSON.stringify(table.order());
|
||||
let currentTable = $(settings.nTable);
|
||||
let order_info_key = get_table_order_info_key(currentTable.attr("id"));
|
||||
let dataTableInstance = $(currentTable).DataTable();
|
||||
let order_info = JSON.stringify(dataTableInstance.order());
|
||||
localStorage.setItem(order_info_key, order_info);
|
||||
},
|
||||
};
|
||||
$("table.gt_table").DataTable(table_options);
|
||||
|
||||
$('.gt_table').each(function() {
|
||||
const x = localStorage.getItem(get_table_order_info_key(this.id));
|
||||
if (x) {
|
||||
try {
|
||||
let order_info = JSON.parse(x);
|
||||
table_options.order = order_info;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
delete table_options.order;
|
||||
}
|
||||
} else {
|
||||
delete table_options.order;
|
||||
}
|
||||
|
||||
$(this).DataTable(table_options);
|
||||
});
|
||||
|
||||
table_options["searching"] = true;
|
||||
$("table.gt_table_searchable").DataTable(table_options);
|
||||
$("table.gt_table_searchable").each(function() {
|
||||
$(this).DataTable(table_options);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function get_table_order_info_key(table_id) {
|
||||
const url = new URL(document.URL);
|
||||
return JSON.stringify(["table_order", table_id, url.pathname]);
|
||||
}
|
||||
|
||||
// Show tags (readonly)
|
||||
function readOnlyTags(nodes) {
|
||||
// nodes are textareas, hide them and create a span showing tags
|
||||
|
@ -230,11 +247,6 @@ class ScoFieldEditor {
|
|||
return true; // Aucune modification, pas d'enregistrement mais on continue normalement
|
||||
}
|
||||
obj.classList.add("sco_wait");
|
||||
// DEBUG
|
||||
// console.log(`
|
||||
// data : ${value},
|
||||
// id: ${obj.dataset.oid}
|
||||
// `);
|
||||
|
||||
$.post(
|
||||
this.save_url,
|
||||
|
|
|
@ -11,8 +11,8 @@ h.id="btc";
|
|||
h.setAttribute("id","btc");
|
||||
h.style.position="absolute";
|
||||
document.getElementsByTagName("body")[0].appendChild(h);
|
||||
if(id==null) links=document.getElementsByTagName("a");
|
||||
else links=document.getElementById(id).getElementsByTagName("a");
|
||||
if(id==null) links=document.querySelectorAll("a, [data-tooltip]"); // was document.getElementsByTagName("a")
|
||||
else links=document.getElementById(id).querySelectorAll("a, [data-tooltip]");// was document.getElementById(id).getElementsByTagName("a")
|
||||
for(i=0;i<links.length;i++){
|
||||
Prepare(links[i]);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Liste simple d'étudiants
|
||||
"""
|
||||
"""Liste simple d'étudiants"""
|
||||
|
||||
import datetime
|
||||
from flask import g, url_for
|
||||
from app import log
|
||||
|
@ -140,6 +140,13 @@ class RowAssi(tb.Row):
|
|||
)
|
||||
stats = self._get_etud_stats(etud)
|
||||
for key, value in stats.items():
|
||||
if key == "present" and sco_preferences.get_preference(
|
||||
"non_present",
|
||||
dept_id=g.scodoc_dept_id,
|
||||
formsemestre_id=self.table.formsemestre.id,
|
||||
):
|
||||
continue
|
||||
|
||||
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
|
||||
if key != "present":
|
||||
self.add_cell(
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<h1>Traitement de l'assiduité</h1>
|
||||
<p class="help">
|
||||
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
|
||||
Pour saisir l'assiduité ou consulter les états, passer par
|
||||
le semestre concerné (saisie par jour ou saisie différée).
|
||||
</p>
|
||||
|
||||
|
|
|
@ -86,9 +86,6 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
|||
|
||||
<div class="scobox">
|
||||
<section class="nonvalide">
|
||||
<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>
|
||||
|
@ -99,6 +96,9 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
|||
département)</p>
|
||||
<p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
|
||||
appuyer sur le bouton "Actualiser"</p>
|
||||
|
||||
{% include "assiduites/explication_etats_justifs.j2" %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -23,47 +23,8 @@ Calendrier de l'assiduité
|
|||
for="mode_demi">mode demi journée</label>
|
||||
</div>
|
||||
|
||||
<div class="calendrier">
|
||||
{% for mois,jours in calendrier.items() %}
|
||||
<div class="month">
|
||||
<h3>{{mois}}</h3>
|
||||
<div class="days {{'demi' if mode_demi else ''}}">
|
||||
{% for jour in jours %}
|
||||
{% if jour.is_non_work() %}
|
||||
<div class="day {{jour.get_class()}}">
|
||||
<span>{{jour.get_nom()}}</span>
|
||||
{% else %}
|
||||
<div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}">
|
||||
{% endif %}
|
||||
{% if mode_demi %}
|
||||
{% if not jour.is_non_work() %}
|
||||
<span>{{jour.get_nom()}}</span>
|
||||
<span class="{{jour.get_demi_class(True, show_pres,show_reta)}}"></span>
|
||||
<span class="{{jour.get_demi_class(False, show_pres,show_reta)}}"></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not jour.is_non_work() %}
|
||||
<span>{{jour.get_nom(False)}}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not jour.is_non_work() and jour.has_assiduites()%}
|
||||
|
||||
<div class="dayline">
|
||||
<div class="dayline-title">
|
||||
<span>{{jour.get_date()}}</span>
|
||||
{{jour.generate_minitimeline() | safe}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="cal">
|
||||
{{calendrier|safe}}
|
||||
<div class="annee">
|
||||
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||
année: </span>
|
||||
|
@ -75,36 +36,7 @@ Calendrier de l'assiduité
|
|||
|
||||
<div class="help">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||
</li>
|
||||
|
||||
<li><span title="Quart Bleu" class="est_just demo"></span> → la période est couverte par un
|
||||
justificatif valide</li>
|
||||
<li><span title="Justif. non valide" class="invalide demo"></span> → la période est
|
||||
couverte par un justificatif non valide
|
||||
</li>
|
||||
<li><span title="Justif. en attente" class="attente demo"></span> → la période
|
||||
a un justificatif en attente de validation
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
|
||||
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||
</div>
|
||||
<ul class="couleurs print">
|
||||
<li><span title="Vert" class="present demo"></span> présence
|
||||
|
@ -163,50 +95,27 @@ Calendrier de l'assiduité
|
|||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.month h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day,
|
||||
.demi .day.color.nonwork {
|
||||
text-align: left;
|
||||
margin: 2px;
|
||||
cursor: default;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
min-width: 6em;
|
||||
.assi_case {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demo.invalide {
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
.assi_case > span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.demo.attente {
|
||||
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;
|
||||
.assi_case>span:last-of-type {
|
||||
border-left: #d5d5d5 solid 1px;
|
||||
}
|
||||
.assi_case>span:first-of-type {
|
||||
border-right: #d5d5d5 solid 1px;
|
||||
}
|
||||
|
||||
.demo.est_just {
|
||||
background-color: var(--color-justi) !important;
|
||||
.dayline{
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.demi .day.nonwork>span {
|
||||
flex: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.demi .day {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
@media print {
|
||||
|
||||
.couleurs.print {
|
||||
|
@ -336,7 +245,5 @@ Calendrier de l'assiduité
|
|||
window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock app_content %}
|
|
@ -1,27 +0,0 @@
|
|||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block title %}
|
||||
Assiduité de {{etud.nomprenom}}
|
||||
{% endblock title %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block app_content %}
|
||||
<div class="pageContent">
|
||||
|
||||
<h2>Liste de l'assiduité et des justificatifs de {{sco.etud.html_link_fiche()|safe}}</h2>
|
||||
{{tableau | safe }}
|
||||
</div>
|
||||
|
||||
{% include "assiduites/explication_etats_justifs.j2" %}
|
||||
|
||||
{% endblock app_content %}
|
|
@ -310,8 +310,13 @@ async function nouvellePeriode(period = null) {
|
|||
|
||||
const assi_btns = document.createElement('div');
|
||||
assi_btns.classList.add('assi-btns');
|
||||
const etats = ["retard", "absent"];
|
||||
|
||||
["present", "retard", "absent"].forEach((value) => {
|
||||
if(!window.nonPresent){
|
||||
etats.splice(0,0,"present");
|
||||
}
|
||||
|
||||
etats.forEach((value) => {
|
||||
const cbox = document.createElement("input");
|
||||
cbox.type = "checkbox";
|
||||
cbox.value = value;
|
||||
|
@ -499,6 +504,8 @@ const moduleimpls = new Map();
|
|||
const inscriptionsModules = new Map();
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
||||
|
||||
// Vérification du forçage de module
|
||||
window.forceModule = "{{ forcer_module }}" == "True";
|
||||
if (window.forceModule) {
|
||||
|
@ -518,12 +525,29 @@ if (window.forceModule) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
const defaultPlage = {{ nouv_plage | safe}} || [];
|
||||
|
||||
/**
|
||||
* Fonction exécutée au lancement de la page
|
||||
* - On affiche ou non les photos des étudiants
|
||||
* - On vérifie si la date est un jour travaillé
|
||||
*/
|
||||
async function main() {
|
||||
|
||||
// On initialise les sélecteurs avec les valeurs par défaut (si elles existent)
|
||||
if (defaultPlage.every((e) => e)) {
|
||||
$("#date").datepicker("setDate", defaultPlage[0]);
|
||||
$("#debut").val(defaultPlage[1]);
|
||||
$("#fin").val(defaultPlage[2]);
|
||||
|
||||
// On ajoute la période si la date est un jour travaillé
|
||||
if(dateCouranteEstTravaillee()){
|
||||
await nouvellePeriode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
|
||||
afficherPDP(checked);
|
||||
$("#date").on("change", async function (d) {
|
||||
|
@ -532,7 +556,7 @@ async function main() {
|
|||
});
|
||||
}
|
||||
|
||||
main();
|
||||
window.addEventListener("load", main);
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -546,6 +570,20 @@ main();
|
|||
|
||||
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
|
||||
|
||||
<div class="ue_warning warning">
|
||||
Attention, cette page va prochainement être remplacée par un mode de saisie hebdomadaire.
|
||||
<p>
|
||||
Pour saisir l'assiduité à une seule date quelconque, utiliser la page
|
||||
<a class="stdlink" href="{{
|
||||
url_for('assiduites.signal_assiduites_group', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, group_ids=group_ids)
|
||||
}}" target="_blank">
|
||||
saisir l'assiduité</a>.
|
||||
</p>
|
||||
<p>Ci-dessous le formulaire vous permettant de saisir plusieurs plages à la fois, qui va bientôt être remplacé/simplifié.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="fix">
|
||||
<!-- Nouvelle Plage
|
||||
|
@ -600,7 +638,9 @@ main();
|
|||
Intialiser les étudiants comme :
|
||||
<select name="etatDef" id="etatDef">
|
||||
<option value="">-</option>
|
||||
{% if not non_present %}
|
||||
<option value="present">présents</option>
|
||||
{% endif %}
|
||||
<option value="retard">en retard</option>
|
||||
<option value="absent">absents</option>
|
||||
</select>
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
const readOnly = {{ readonly }};
|
||||
|
||||
window.forceModule = "{{ forcer_module }}" == "True"
|
||||
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
||||
|
||||
const etudsDefDem = {{ defdem | safe }}
|
||||
|
||||
|
@ -104,6 +105,24 @@
|
|||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
|
||||
<style>
|
||||
#retour-haut{
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
font-size: 3em;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
html{
|
||||
scroll-behavior: smooth !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
{% endblock styles %}
|
||||
|
||||
|
||||
|
@ -112,6 +131,10 @@
|
|||
{{ minitimeline|safe }}
|
||||
<section id="content">
|
||||
|
||||
<a id="retour-haut" href="#gtrcontent">
|
||||
⬆️
|
||||
</a>
|
||||
|
||||
<div class="no-display">
|
||||
<span class="formsemestre_id">{{formsemestre_id}}</span>
|
||||
<span id="formsemestre_date_debut">{{formsemestre_date_debut}}</span>
|
||||
|
@ -130,12 +153,22 @@
|
|||
<div class="infos">
|
||||
<div class="infos-button">Groupes : {{grp|safe}}</div>
|
||||
<div>
|
||||
<button class="btn_date" onclick="jourSuivant(true)">
|
||||
⇤
|
||||
</button>
|
||||
<input type="text" name="date" id="date" class="datepicker" value="{{date}}">
|
||||
</div>
|
||||
<button class="btn_date" onclick="jourSuivant(false)">
|
||||
⇥
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div style="display: {{'none' if readonly == 'true' else 'block'}};">
|
||||
{{timeline|safe}}
|
||||
<div>
|
||||
<button onclick="setPeriodValues(t_start, t_mid)">Matin</button>
|
||||
<button onclick="setPeriodValues(t_mid, t_end)">Après-Midi</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if readonly == "false" %}
|
||||
|
@ -159,14 +192,16 @@
|
|||
<div class="mass-selection">
|
||||
<span>Mettre tout le monde :</span>
|
||||
<fieldset class="btns_field mass">
|
||||
{% if not non_present %}
|
||||
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
|
||||
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present">
|
||||
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Indique l'état Présent pour tous les étudiants" data-tooltip>
|
||||
{% endif %}
|
||||
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard"
|
||||
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard">
|
||||
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Indique l'état Retard pour tous les étudiants" data-tooltip>
|
||||
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
|
||||
class="rbtn absent" onclick="mettreToutLeMonde('absent', this)" title="Absent">
|
||||
class="rbtn absent" onclick="mettreToutLeMonde('absent', this)" title="Indique l'état Absent pour tous les étudiants" data-tooltip>
|
||||
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
|
||||
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer">
|
||||
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Retire l'état pour tous les étudiants" data-tooltip>
|
||||
</fieldset>
|
||||
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
|
||||
</div>
|
||||
|
@ -178,6 +213,11 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="help">
|
||||
<h3>Calendrier</h3>
|
||||
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||
</div>
|
||||
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<div class="assiduite-bubble {{etat}}">
|
||||
<div class="assiduite-id">{{moduleimpl}}</div>
|
||||
<div class="assiduite-period">{{date_debut}}</div>
|
||||
<div class="assiduite-period">{{date_fin}}</div>
|
||||
<div class="assiduite-state">État: {{etat}}</div>
|
||||
<div class="assiduite-id">{{moduleimpl}}</div>
|
||||
<div class="assiduite-period">{{date}}</div>
|
||||
<div class="assiduite-why">Motif: {{motif}}</div>
|
||||
<div class="assiduite-user_id">{{saisie}}</div>
|
||||
</div>
|
|
@ -1,12 +1,28 @@
|
|||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la période
|
||||
</li>
|
||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la période
|
||||
</li>
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la période
|
||||
</li>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||
</li>
|
||||
|
||||
<li><span title="Hachure Bleue" class="justified demo"></span> → l'assiduité est justifiée par un
|
||||
justificatif valide</li>
|
||||
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> → l'assiduité est
|
||||
justifiée par un justificatif non valide / en attente de validation
|
||||
</li>
|
||||
<li><span title="Quart Bleu" class="est_just demo color"></span> → la période est couverte par un
|
||||
justificatif valide</li>
|
||||
<li><span title="Justif. non valide" class="invalide demo color "></span> → la période est
|
||||
couverte par un justificatif non valide
|
||||
</li>
|
||||
<li><span title="Justif. en attente" class="attente demo color"></span> → la période
|
||||
a un justificatif en attente de validation
|
||||
</li>
|
||||
</ul>
|
||||
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
|
||||
|
|
|
@ -74,7 +74,13 @@
|
|||
setupAssiduiteBubble(block, assiduité);
|
||||
}
|
||||
|
||||
// TODO: ajout couleur justificatif
|
||||
// ajout couleur justificatif
|
||||
const justificatifs = assiduité.justificatifs || [];
|
||||
const justified = justificatifs.some(
|
||||
(justificatif) => justificatif.etat === "VALIDE"
|
||||
)
|
||||
|
||||
if(justified) block.classList.add("est_just");
|
||||
|
||||
block.classList.add(assiduité.etat.toLowerCase());
|
||||
if(assiduité.etat != "CRENEAU") block.classList.add("color");
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
const timelineContainer = document.querySelector(".timeline-container");
|
||||
const periodTimeLine = document.querySelector(".period");
|
||||
const t_start = {{ t_start }};
|
||||
const t_mid = {{ t_mid }};
|
||||
const t_end = {{ t_end }};
|
||||
|
||||
const tick_time = 60 / {{ tick_time }};
|
||||
const tick_delay = 1 / tick_time;
|
||||
|
||||
const period_default = {{ periode_defaut }};
|
||||
const period_default = 2;
|
||||
|
||||
let handleMoving = false;
|
||||
|
||||
|
@ -133,6 +134,7 @@
|
|||
timelineContainer.removeEventListener("mousemove", onMouseMove);
|
||||
handleMoving = false;
|
||||
func_call();
|
||||
savePeriodInLocalStorage();
|
||||
|
||||
}
|
||||
timelineContainer.addEventListener("mousemove", onMouseMove);
|
||||
|
@ -166,6 +168,7 @@
|
|||
snapHandlesToQuarters();
|
||||
timelineContainer.removeEventListener("mousemove", onMouseMove);
|
||||
func_call();
|
||||
savePeriodInLocalStorage();
|
||||
}
|
||||
timelineContainer.addEventListener("mousemove", onMouseMove);
|
||||
timelineContainer.addEventListener("touchmove", onMouseMove);
|
||||
|
@ -264,6 +267,7 @@
|
|||
snapHandlesToQuarters();
|
||||
updatePeriodTimeLabel()
|
||||
func_call();
|
||||
savePeriodInLocalStorage();
|
||||
}
|
||||
|
||||
function snapHandlesToQuarters() {
|
||||
|
@ -309,9 +313,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
function savePeriodInLocalStorage(){
|
||||
const dates = getPeriodValues();
|
||||
localStorage.setItem("sco-timeline-values", JSON.stringify(dates));
|
||||
}
|
||||
|
||||
function loadPeriodFromLocalStorage(){
|
||||
const dates = JSON.parse(localStorage.getItem("sco-timeline-values"));
|
||||
if(dates){
|
||||
setPeriodValues(...dates);
|
||||
}else{
|
||||
setPeriodValues(t_start, t_start + period_default);
|
||||
}
|
||||
}
|
||||
|
||||
createTicks();
|
||||
|
||||
setPeriodValues(t_start, t_start + period_default);
|
||||
loadPeriodFromLocalStorage();
|
||||
|
||||
{% if heures %}
|
||||
let [heure_deb, heure_fin] = [{{ heures | safe }}]
|
||||
|
|
88
app/templates/calendrier.j2
Normal file
88
app/templates/calendrier.j2
Normal file
|
@ -0,0 +1,88 @@
|
|||
<div class="calendrier">
|
||||
{% for mois,jours in calendrier.items() %}
|
||||
<div class="mois">
|
||||
<h3>{{mois}}</h3>
|
||||
<div class="jours">
|
||||
{% for jour in jours %}
|
||||
<div class="jour {{jour.get_class()}}">
|
||||
<span class="nom">{{jour.get_nom()}}</span>
|
||||
<div class="contenu">
|
||||
{{jour.get_html() | safe}}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.calendrier {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
overflow-x: scroll;
|
||||
border: 1px solid #444;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mois {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mois h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.jour {
|
||||
text-align: left;
|
||||
margin: 2px;
|
||||
cursor: default;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
min-width: 6em;
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.jour>.contenu {
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jour.jour.non-travail>.nom,
|
||||
.jour.jour.non-travail>.contenu {
|
||||
border: 0;
|
||||
background-color: #badfff;
|
||||
}
|
||||
|
||||
.jour>.nom {
|
||||
width: 3em;
|
||||
min-width: 3em;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.jour>.contenu,
|
||||
.jour>.nom {
|
||||
border: 1px solid #d5d5d5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.jour>.contenu a {
|
||||
padding: 0px 2px;
|
||||
}
|
||||
.jour>.nom {
|
||||
text-align: center;
|
||||
}
|
||||
.sem-courante{
|
||||
--couleur : #ee752c;
|
||||
border-left: solid 3px var(--couleur);
|
||||
border-right: solid 3px var(--couleur);
|
||||
}
|
||||
|
||||
</style>
|
|
@ -49,7 +49,7 @@
|
|||
</li>
|
||||
{% if formsemestre_origine is not none %}
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('notes.formsemestre_jury_but_erase',
|
||||
url_for('notes.formsemestre_jury_erase',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_origine.id,
|
||||
etudid=etud.id, only_one_sem=1)
|
||||
}}">
|
||||
|
|
19
app/templates/sco_bug_report.j2
Normal file
19
app/templates/sco_bug_report.j2
Normal file
|
@ -0,0 +1,19 @@
|
|||
{# -*- mode: jinja-html -*- #}
|
||||
{% extends 'base.j2' %}
|
||||
{% import 'wtf.j2' as wtf %}
|
||||
|
||||
{% block app_content %}
|
||||
<h2>Assistance technique</h2>
|
||||
<p class="help">
|
||||
Ce formulaire permet d'effectuer une demande d'assistance technique.<br>
|
||||
Son <b>contenu sera accessible publiquement</b> sur scodoc.org, veuillez donc ne pas y inclure d'informations sensibles.<br>
|
||||
L'adresse email associée à votre compte ScoDoc est automatiquement transmise avec votre demande mais ne sera pas
|
||||
affichée publiquement.<br>
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ wtf.quick_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock app_content %}
|
|
@ -49,7 +49,7 @@
|
|||
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
|
||||
<script>
|
||||
window.onload = function () { enableTooltips("gtrcontent") };
|
||||
window.onload = function () { enableTooltips("gtrcontent"); enableTooltips("sidebar"); };
|
||||
|
||||
const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}";
|
||||
</script>
|
||||
|
|
|
@ -5,15 +5,19 @@
|
|||
|
||||
<h2>Erreur !</h2>
|
||||
|
||||
{{ exc }}
|
||||
{% if exc.safe %}
|
||||
{{ exc | safe }}
|
||||
{% else %}
|
||||
{{ exc }}
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
{% if g.scodoc_dept %}
|
||||
<a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">continuer</a>
|
||||
<a class="stdlink" href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">continuer</a>
|
||||
{% elif exc.dest_url %}
|
||||
<a href="{{ exc.dest_url or url_for('scodoc.index') }}">continuer</a>
|
||||
<a class="stdlink" href="{{ exc.dest_url or url_for('scodoc.index') }}">continuer</a>
|
||||
{% else %}
|
||||
<a href="{{ exc.dest_url or url_for('scodoc.index') }}">retour page d'accueil</a>
|
||||
<a class="stdlink" href="{{ exc.dest_url or url_for('scodoc.index') }}">retour page d'accueil</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
|
22
app/templates/scodoc/forms/evaluation_edit.j2
Normal file
22
app/templates/scodoc/forms/evaluation_edit.j2
Normal file
|
@ -0,0 +1,22 @@
|
|||
{# Interdit de définir des évaluations non normales "immédiates" #}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var evaluationTypeSelect = document.getElementById('tf_evaluation_type');
|
||||
var publishIncompleteCheckbox = document.querySelector('input[type="checkbox"][name="publish_incomplete:list"]');
|
||||
|
||||
function updateCheckboxState() {
|
||||
if (evaluationTypeSelect.value !== '0') {
|
||||
publishIncompleteCheckbox.disabled = true;
|
||||
publishIncompleteCheckbox.checked = false; // Ensure the checkbox is not checked
|
||||
} else {
|
||||
publishIncompleteCheckbox.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener to select element
|
||||
evaluationTypeSelect.addEventListener('change', updateCheckboxState);
|
||||
|
||||
// Initial call to set the correct state when the page loads
|
||||
updateCheckboxState();
|
||||
});
|
||||
</script>
|
|
@ -299,6 +299,9 @@ div.effectif {
|
|||
<li>
|
||||
<a class="stdlink" href="sco_dump_and_send_db">Envoyer données</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="stdlink" href="sco_bug_report">Signaler une erreur ou suggérer une amélioration</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{# Barre marge gauche ScoDoc #}
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
<!-- sidebar -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar" id="sidebar">
|
||||
{# sidebar_common #}
|
||||
<a class="scodoc_title" href="{{
|
||||
url_for('scodoc.index', scodoc_dept=g.scodoc_dept) }}">ScoDoc {{ sco.SCOVERSION }}</a>
|
||||
|
@ -57,7 +57,7 @@
|
|||
<b>Absences</b>
|
||||
{% if sco.etud_cur_sem %}
|
||||
<span title="absences du {{ sco.etud_cur_sem['date_debut'].strftime('%d/%m/%Y') }}
|
||||
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}">({{sco.prefs["assi_metrique"]}})
|
||||
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}" data-tooltip>({{sco.prefs["assi_metrique"]}})
|
||||
<br />{{'%1g'|format(sco.nb_abs_just)}} J., {{'%1g'|format(sco.nb_abs_nj)}} N.J.</span>
|
||||
{% endif %}
|
||||
<ul>
|
||||
|
@ -73,10 +73,8 @@
|
|||
{% endif %}
|
||||
<li><a href="{{ url_for('assiduites.calendrier_assi_etud', scodoc_dept=g.scodoc_dept,
|
||||
etudid=sco.etud.id) }}">Calendrier</a></li>
|
||||
<li><a href="{{ url_for('assiduites.liste_assiduites_etud', scodoc_dept=g.scodoc_dept,
|
||||
etudid=sco.etud.id) }}">Liste</a></li>
|
||||
<li><a href="{{ url_for('assiduites.bilan_etud', scodoc_dept=g.scodoc_dept,
|
||||
etudid=sco.etud.id) }}">Bilan</a></li>
|
||||
etudid=sco.etud.id) }}" title="Les pages bilan et liste ont été fusionnées">Liste/Bilan</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div> {# /etud-insidebar #}
|
||||
|
|
|
@ -63,6 +63,7 @@ from app.models import (
|
|||
Scolog,
|
||||
)
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
|
||||
from app.auth.models import User
|
||||
from app.models.assiduites import get_assiduites_justif
|
||||
from app.tables.list_etuds import RowEtud, TableEtud
|
||||
|
@ -82,6 +83,7 @@ from app.scodoc import sco_find_etud
|
|||
from app.scodoc import sco_assiduites as scass
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_gen_cal
|
||||
|
||||
|
||||
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
|
||||
|
@ -509,51 +511,6 @@ def _record_assiduite_etud(
|
|||
return False
|
||||
|
||||
|
||||
@bp.route("/liste_assiduites_etud")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def liste_assiduites_etud():
|
||||
"""
|
||||
liste_assiduites_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
|
||||
Args:
|
||||
etudid (int): l'identifiant de l'étudiant
|
||||
|
||||
Returns:
|
||||
str: l'html généré
|
||||
"""
|
||||
|
||||
# Récupération de l'étudiant concerné
|
||||
etudid = request.args.get("etudid", -1)
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
if etud.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "étudiant inexistant dans ce département")
|
||||
|
||||
# Gestion d'une assiduité unique (redirigé depuis le calendrier) TODO-Assiduites
|
||||
assiduite_id: int = request.args.get("assiduite_id", -1)
|
||||
|
||||
# Préparation de la page
|
||||
tableau = _prepare_tableau(
|
||||
liste_assi.AssiJustifData.from_etudiants(
|
||||
etud,
|
||||
),
|
||||
filename=f"assiduites-justificatifs-{etud.id}",
|
||||
afficher_etu=False,
|
||||
filtre=liste_assi.AssiFiltre(type_obj=0),
|
||||
options=liste_assi.AssiDisplayOptions(show_module=True),
|
||||
cache_key=f"tableau-etud-{etud.id}",
|
||||
)
|
||||
if not tableau[0]:
|
||||
return tableau[1]
|
||||
# Page HTML:
|
||||
return render_template(
|
||||
"assiduites/pages/liste_assiduites.j2",
|
||||
assi_id=assiduite_id,
|
||||
etud=etud,
|
||||
tableau=tableau[1],
|
||||
sco=ScoData(etud),
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/bilan_etud")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
|
@ -581,28 +538,19 @@ def bilan_etud():
|
|||
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
|
||||
)
|
||||
|
||||
# Récupération des assiduités et justificatifs de l'étudiant
|
||||
data = liste_assi.AssiJustifData(
|
||||
etud.assiduites.filter(
|
||||
Assiduite.etat != scu.EtatAssiduite.PRESENT, Assiduite.est_just == False
|
||||
# Préparation de la page
|
||||
tableau = _prepare_tableau(
|
||||
liste_assi.AssiJustifData.from_etudiants(
|
||||
etud,
|
||||
),
|
||||
etud.justificatifs.filter(
|
||||
Justificatif.etat.in_(
|
||||
[scu.EtatJustificatif.ATTENTE, scu.EtatJustificatif.MODIFIE]
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
table = _prepare_tableau(
|
||||
data,
|
||||
filename=f"assiduites-justificatifs-{etud.id}",
|
||||
afficher_etu=False,
|
||||
filename=f"Bilan assiduité {etud.nomprenom}",
|
||||
titre="Bilan de l'assiduité de l'étudiant",
|
||||
cache_key=f"tableau-etud-{etud.id}-bilan",
|
||||
filtre=liste_assi.AssiFiltre(type_obj=0),
|
||||
options=liste_assi.AssiDisplayOptions(show_module=True),
|
||||
cache_key=f"tableau-etud-{etud.id}",
|
||||
)
|
||||
|
||||
if not table[0]:
|
||||
return table[1]
|
||||
if not tableau[0]:
|
||||
return tableau[1]
|
||||
|
||||
# Génération de la page HTML
|
||||
return render_template(
|
||||
|
@ -612,7 +560,7 @@ def bilan_etud():
|
|||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
sco=ScoData(etud),
|
||||
tableau=table[1],
|
||||
tableau=tableau[1],
|
||||
)
|
||||
|
||||
|
||||
|
@ -652,7 +600,7 @@ def edit_justificatif_etud(justif_id: int):
|
|||
back_url = request.args.get("back_url", None)
|
||||
|
||||
redirect_url = back_url or url_for(
|
||||
"assiduites.liste_assiduites_etud",
|
||||
"assiduites.bilan_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=justif.etudiant.id,
|
||||
)
|
||||
|
@ -757,8 +705,6 @@ def _verif_date_form_justif(
|
|||
deb = deb.replace(hour=0, minute=0)
|
||||
fin = fin.replace(hour=23, minute=59)
|
||||
|
||||
print(f"DEBUG {cas=}")
|
||||
|
||||
return deb, fin
|
||||
|
||||
|
||||
|
@ -925,7 +871,14 @@ def calendrier_assi_etud():
|
|||
# (sera utilisé pour générer le selecteur d'année)
|
||||
annees_str: str = json.dumps(annees)
|
||||
|
||||
calendrier: dict[str, list["Jour"]] = generate_calendar(etud, annee)
|
||||
cal = CalendrierAssi(
|
||||
annee,
|
||||
etud,
|
||||
mode_demi=mode_demi,
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
)
|
||||
calendrier: str = cal.get_html()
|
||||
|
||||
# Peuplement du template jinja
|
||||
return render_template(
|
||||
|
@ -951,8 +904,6 @@ def choix_date() -> str:
|
|||
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)
|
||||
|
@ -982,11 +933,7 @@ def choix_date() -> str:
|
|||
if ok:
|
||||
return redirect(
|
||||
url_for(
|
||||
(
|
||||
"assiduites.signal_assiduites_group"
|
||||
if request.args.get("readonly") is None
|
||||
else "assiduites.visu_assiduites_group"
|
||||
),
|
||||
"assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
|
@ -1058,7 +1005,7 @@ def signal_assiduites_group():
|
|||
)
|
||||
if not groups_infos.members:
|
||||
return (
|
||||
html_sco_header.sco_header(page_title="Saisie journalière de l'assiduité")
|
||||
html_sco_header.sco_header(page_title="Saisie de l'assiduité")
|
||||
+ "<h3>Aucun étudiant ! </h3>"
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
|
@ -1132,6 +1079,11 @@ def signal_assiduites_group():
|
|||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
non_present=sco_preferences.get_preference(
|
||||
"non_present",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
formsemestre_date_debut=str(formsemestre.date_debut),
|
||||
formsemestre_date_fin=str(formsemestre.date_fin),
|
||||
formsemestre_id=formsemestre_id,
|
||||
|
@ -1144,137 +1096,7 @@ def signal_assiduites_group():
|
|||
sco=ScoData(formsemestre=formsemestre),
|
||||
sem=sem["titre_num"],
|
||||
timeline=_timeline(heures=",".join([f"'{s}'" for s in heures])),
|
||||
title="Saisie journalière des assiduités",
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/visu_assiduites_group")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def visu_assiduites_group():
|
||||
"""
|
||||
Visualisation des assiduités des groupes pour le jour donné
|
||||
dans le formsemestre_id et le moduleimpl_id
|
||||
Returns:
|
||||
str: l'html généré
|
||||
"""
|
||||
|
||||
# Récupération des paramètres de la requête
|
||||
formsemestre_id: int = request.args.get("formsemestre_id", -1)
|
||||
moduleimpl_id: int = request.args.get("moduleimpl_id")
|
||||
date: str = request.args.get("jour", datetime.date.today().isoformat())
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
if group_ids is None:
|
||||
group_ids = []
|
||||
else:
|
||||
group_ids = group_ids.split(",")
|
||||
map(str, group_ids)
|
||||
|
||||
# Vérification du moduleimpl_id
|
||||
if moduleimpl_id is not None:
|
||||
try:
|
||||
moduleimpl_id = int(moduleimpl_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ScoValueError("identifiant de moduleimpl invalide") from exc
|
||||
# Vérification du formsemestre_id
|
||||
if formsemestre_id is not None:
|
||||
try:
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ScoValueError("identifiant de formsemestre invalide") from exc
|
||||
|
||||
# Récupérations des/du groupe(s)
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
|
||||
)
|
||||
if not groups_infos.members:
|
||||
return (
|
||||
html_sco_header.sco_header(page_title="Saisie journalière de l'assiduité")
|
||||
+ "<h3>Aucun étudiant ! </h3>"
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
|
||||
# --- Filtrage par formsemestre ---
|
||||
formsemestre_id = groups_infos.formsemestre_id
|
||||
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre.dept_id != g.scodoc_dept_id:
|
||||
abort(404, "groupes inexistants dans ce département")
|
||||
|
||||
# Récupération des étudiants du/des groupe(s)
|
||||
etuds = [
|
||||
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
|
||||
for m in groups_infos.members
|
||||
]
|
||||
|
||||
# --- Vérification de la date ---
|
||||
real_date = scu.is_iso_formated(date, True).date()
|
||||
if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin:
|
||||
# Si le jour est hors semestre, renvoyer vers choix date
|
||||
return redirect(
|
||||
url_for(
|
||||
"assiduites.choix_date",
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
readonly="true",
|
||||
)
|
||||
)
|
||||
|
||||
# --- Restriction en fonction du moduleimpl_id ---
|
||||
if moduleimpl_id:
|
||||
mod_inscrits = {
|
||||
x["etudid"]
|
||||
for x in sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=moduleimpl_id
|
||||
)
|
||||
}
|
||||
etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits]
|
||||
if etuds_inscrits_module:
|
||||
etuds = etuds_inscrits_module
|
||||
else:
|
||||
# Si aucun etudiant n'est inscrit au module choisi...
|
||||
moduleimpl_id = None
|
||||
|
||||
# --- Génération du HTML ---
|
||||
|
||||
if groups_infos.tous_les_etuds_du_sem:
|
||||
gr_tit = "en"
|
||||
else:
|
||||
if len(groups_infos.group_ids) > 1:
|
||||
grp = "des groupes"
|
||||
else:
|
||||
grp = "du groupe"
|
||||
gr_tit = (
|
||||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
||||
)
|
||||
|
||||
# Récupération du semestre en dictionnaire
|
||||
sem = formsemestre.to_dict()
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/signal_assiduites_group.j2",
|
||||
date=_dateiso_to_datefr(date),
|
||||
defdem=_get_etuds_dem_def(formsemestre),
|
||||
forcer_module=sco_preferences.get_preference(
|
||||
"forcer_module",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
formsemestre_date_debut=str(formsemestre.date_debut),
|
||||
formsemestre_date_fin=str(formsemestre.date_fin),
|
||||
formsemestre_id=formsemestre_id,
|
||||
gr_tit=gr_tit,
|
||||
grp=sco_groups_view.menu_groups_choice(groups_infos),
|
||||
minitimeline=_mini_timeline(),
|
||||
moduleimpl_select=_module_selector(formsemestre, moduleimpl_id),
|
||||
nonworkdays=_non_work_days(),
|
||||
sem=sem["titre_num"],
|
||||
timeline=_timeline(),
|
||||
readonly="true",
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
title="Saisie journalière de l'assiduité",
|
||||
title="Saisie de l'assiduité",
|
||||
)
|
||||
|
||||
|
||||
|
@ -1914,8 +1736,29 @@ def _preparer_objet(
|
|||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
def signal_assiduites_diff():
|
||||
"""TODO documenter
|
||||
"""
|
||||
Utilisé notamment par "Saisie différée" sur tableau de bord semetstre"
|
||||
|
||||
Arguments de la requête:
|
||||
|
||||
- group_ids : liste des groupes
|
||||
example : group_ids=1,2,3
|
||||
- formsemestre_id : id du formsemestre
|
||||
example : formsemestre_id=1
|
||||
- moduleimpl_id : id du moduleimpl
|
||||
example : moduleimpl_id=1
|
||||
|
||||
(Permet de pré-générer une plage. Si non renseigné, la plage sera vide)
|
||||
(Les trois valeurs suivantes doivent être renseignées ensemble)
|
||||
- date
|
||||
example : date=01/01/2021
|
||||
- heure_debut
|
||||
example : heure_debut=08:00
|
||||
- heure_fin
|
||||
example : heure_fin=10:00
|
||||
|
||||
Exemple de requête :
|
||||
signal_assiduites_diff?formsemestre_id=67&group_ids=400&moduleimpl_id=1229&date=15/04/2024&heure_debut=12:34&heure_fin=12:55
|
||||
"""
|
||||
# Récupération des paramètres de la requête
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
|
@ -1957,11 +1800,23 @@ def signal_assiduites_diff():
|
|||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
||||
)
|
||||
|
||||
# Pré-remplissage des sélecteurs
|
||||
moduleimpl_id = request.args.get("moduleimpl_id", -1)
|
||||
try:
|
||||
moduleimpl_id = int(moduleimpl_id)
|
||||
except ValueError:
|
||||
moduleimpl_id = -1
|
||||
# date fra (dd/mm/yyyy)
|
||||
date = request.args.get("date", "")
|
||||
# heures (hh:mm)
|
||||
heure_deb = request.args.get("heure_debut", "")
|
||||
heure_fin = request.args.get("heure_fin", "")
|
||||
|
||||
# vérifications des sélecteurs
|
||||
date = date if re.match(r"^\d{2}\/\d{2}\/\d{4}$", date) else ""
|
||||
heure_deb = heure_deb if re.match(r"^[0-2]\d:[0-5]\d$", heure_deb) else ""
|
||||
heure_fin = heure_fin if re.match(r"^[0-2]\d:[0-5]\d$", heure_fin) else ""
|
||||
nouv_plage: list[str] = [date, heure_deb, heure_fin]
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/signal_assiduites_diff.j2",
|
||||
|
@ -1977,6 +1832,14 @@ def signal_assiduites_diff():
|
|||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
non_present=sco_preferences.get_preference(
|
||||
"non_present",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
nouv_plage=nouv_plage,
|
||||
formsemestre_id=formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
)
|
||||
|
||||
|
||||
|
@ -2247,89 +2110,6 @@ def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str:
|
|||
return f'du {deb.strftime("%d/%m/%Y %H:%M")} au {fin.strftime("%d/%m/%Y %H:%M")}'
|
||||
|
||||
|
||||
def _get_days_between_dates(deb: str, fin: str) -> str:
|
||||
"""
|
||||
_get_days_between_dates récupère tous les jours entre deux dates
|
||||
|
||||
Args:
|
||||
deb (str): date de début
|
||||
fin (str): date de fin
|
||||
|
||||
Returns:
|
||||
str: une chaine json représentant une liste des jours
|
||||
['date_iso','date_iso2', ...]
|
||||
"""
|
||||
if deb is None or fin is None:
|
||||
return "null"
|
||||
try:
|
||||
if isinstance(deb, str) and isinstance(fin, str):
|
||||
date_deb: datetime.date = datetime.date.fromisoformat(deb)
|
||||
date_fin: datetime.date = datetime.date.fromisoformat(fin)
|
||||
else:
|
||||
date_deb, date_fin = deb.date(), fin.date()
|
||||
except ValueError:
|
||||
return "null"
|
||||
dates: list[str] = []
|
||||
while date_deb <= date_fin:
|
||||
dates.append(f'"{date_deb.isoformat()}"')
|
||||
date_deb = date_deb + datetime.timedelta(days=1)
|
||||
|
||||
return f"[{','.join(dates)}]"
|
||||
|
||||
|
||||
def _differee(
|
||||
etudiants: list[dict],
|
||||
moduleimpl_select: str,
|
||||
date: str = None,
|
||||
periode: dict[str, str] = None,
|
||||
formsemestre_id: int = None,
|
||||
) -> str:
|
||||
"""
|
||||
_differee Génère un tableau de saisie différé
|
||||
|
||||
Args:
|
||||
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.
|
||||
formsemestre_id (int, optional): l'id du semestre pour le selecteur de module.
|
||||
|
||||
Returns:
|
||||
str: le widget (html/css/js)
|
||||
"""
|
||||
if date is None:
|
||||
date = datetime.date.today().isoformat()
|
||||
|
||||
forcer_module = sco_preferences.get_preference(
|
||||
"forcer_module",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
)
|
||||
|
||||
assi_etat_defaut = sco_preferences.get_preference(
|
||||
"assi_etat_defaut",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
)
|
||||
|
||||
periode_defaut = sco_preferences.get_preference(
|
||||
"periode_defaut",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/widgets/differee.j2",
|
||||
etudiants=etudiants,
|
||||
assi_etat_defaut=assi_etat_defaut,
|
||||
periode_defaut=periode_defaut,
|
||||
forcer_module=forcer_module,
|
||||
moduleimpl_select=moduleimpl_select,
|
||||
date=date,
|
||||
periode=periode,
|
||||
)
|
||||
|
||||
|
||||
def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> str:
|
||||
"""
|
||||
_module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre
|
||||
|
@ -2399,7 +2179,7 @@ def _module_selector_multiple(
|
|||
)
|
||||
|
||||
|
||||
def _timeline(formsemestre_id: int = None, heures=None) -> str:
|
||||
def _timeline(heures=None) -> str:
|
||||
"""
|
||||
_timeline retourne l'html de la timeline
|
||||
|
||||
|
@ -2414,11 +2194,9 @@ def _timeline(formsemestre_id: int = None, heures=None) -> str:
|
|||
return render_template(
|
||||
"assiduites/widgets/timeline.j2",
|
||||
t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"),
|
||||
t_mid=ScoDocSiteConfig.assi_get_rounded_time("assi_lunch_time", "13:00:00"),
|
||||
t_end=ScoDocSiteConfig.assi_get_rounded_time("assi_afternoon_time", "18:00:00"),
|
||||
tick_time=ScoDocSiteConfig.get("assi_tick_time", 15),
|
||||
periode_defaut=sco_preferences.get_preference(
|
||||
"periode_defaut", formsemestre_id
|
||||
),
|
||||
heures=heures,
|
||||
)
|
||||
|
||||
|
@ -2473,79 +2251,74 @@ def _get_etuds_dem_def(formsemestre) -> str:
|
|||
# --- Gestion du calendrier ---
|
||||
|
||||
|
||||
def generate_calendar(
|
||||
etudiant: Identite,
|
||||
annee: int = None,
|
||||
) -> dict[str, list["Jour"]]:
|
||||
class JourAssi(sco_gen_cal.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()
|
||||
|
||||
# On prend du 01/09 au 31/08
|
||||
date_debut: datetime.datetime = datetime.datetime(annee, 9, 1, 0, 0)
|
||||
date_fin: datetime.datetime = datetime.datetime(annee + 1, 8, 31, 23, 59)
|
||||
|
||||
# Filtrage des assiduités et des justificatifs en fonction de la periode / année
|
||||
etud_assiduites: Query = scass.filter_by_date(
|
||||
etudiant.assiduites,
|
||||
Assiduite,
|
||||
date_deb=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
etud_justificatifs: Query = scass.filter_by_date(
|
||||
etudiant.justificatifs,
|
||||
Justificatif,
|
||||
date_deb=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
|
||||
# Récupération des jours de l'année et de leurs assiduités/justificatifs
|
||||
annee_par_mois: dict[str, list[Jour]] = _organize_by_month(
|
||||
_get_dates_between(
|
||||
deb=date_debut.date(),
|
||||
fin=date_fin.date(),
|
||||
),
|
||||
etud_assiduites,
|
||||
etud_justificatifs,
|
||||
)
|
||||
|
||||
return annee_par_mois
|
||||
|
||||
|
||||
class Jour:
|
||||
"""Jour
|
||||
Jour du calendrier
|
||||
get_nom : retourne le numéro et le nom du Jour (ex: M19 / Mer 19)
|
||||
Représente un jour d'assiduité
|
||||
"""
|
||||
|
||||
def __init__(self, date: datetime.date, assiduites: Query, justificatifs: Query):
|
||||
self.date = date
|
||||
def __init__(
|
||||
self,
|
||||
date: datetime.date,
|
||||
assiduites: Query,
|
||||
justificatifs: Query,
|
||||
parent: "CalendrierAssi",
|
||||
):
|
||||
super().__init__(date)
|
||||
|
||||
# assiduités et justificatifs du jour
|
||||
self.assiduites = assiduites
|
||||
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]+' '}"
|
||||
+ f"{self.date.day}"
|
||||
self.parent = parent
|
||||
|
||||
def get_html(self) -> str:
|
||||
# si non travaillé on renvoie une case vide
|
||||
if self.is_non_work():
|
||||
return ""
|
||||
|
||||
html: str = (
|
||||
self._get_html_demi() if self.parent.mode_demi else self._get_html_normal()
|
||||
)
|
||||
html = f'<div class="assi_case">{html}</div>'
|
||||
|
||||
def get_date(self) -> str:
|
||||
"""
|
||||
Renvoie la date du jour au format "dd/mm/yyyy"
|
||||
"""
|
||||
return self.date.strftime(scu.DATE_FMT)
|
||||
if self.has_assiduite():
|
||||
minitimeline: str = f"""
|
||||
<div class="dayline">
|
||||
<div class="dayline-title">
|
||||
<span>{self.get_date()}</span>
|
||||
{self._generate_minitimeline()}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
html += minitimeline
|
||||
|
||||
def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str:
|
||||
return html
|
||||
|
||||
def has_assiduite(self) -> bool:
|
||||
"""Renvoie True si le jour a une assiduité"""
|
||||
return self.assiduites.count() > 0
|
||||
|
||||
def _get_html_normal(self) -> str:
|
||||
"""
|
||||
Retourne la classe css du jour (mode normal)
|
||||
Renvoie l'html de la case du calendrier
|
||||
(version journee normale (donc une couleur))
|
||||
"""
|
||||
class_name = self._get_color_normal()
|
||||
return f'<span class="{class_name}"></span>'
|
||||
|
||||
def _get_html_demi(self) -> str:
|
||||
"""
|
||||
Renvoie l'html de la case du calendrier
|
||||
(version journee divisée en demi-journées (donc 2 couleurs))
|
||||
"""
|
||||
matin = self._get_color_demi(True)
|
||||
aprem = self._get_color_demi(False)
|
||||
return f'<span class="{matin}"></span><span class="{aprem}"></span>'
|
||||
|
||||
def _get_color_normal(self) -> str:
|
||||
"""renvoie la classe css correspondant
|
||||
à la case du calendrier
|
||||
(version journee normale)
|
||||
"""
|
||||
etat = ""
|
||||
est_just = ""
|
||||
|
@ -2555,8 +2328,8 @@ class Jour:
|
|||
|
||||
etat = self._get_color_assiduites_cascade(
|
||||
self._get_etats_from_assiduites(self.assiduites),
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
show_pres=self.parent.show_pres,
|
||||
show_reta=self.parent.show_reta,
|
||||
)
|
||||
|
||||
est_just = self._get_color_justificatifs_cascade(
|
||||
|
@ -2565,13 +2338,11 @@ class Jour:
|
|||
|
||||
return f"color {etat} {est_just}"
|
||||
|
||||
def get_demi_class(
|
||||
self, matin: bool, show_pres: bool = False, show_reta: bool = False
|
||||
) -> str:
|
||||
def _get_color_demi(self, matin: bool) -> str:
|
||||
"""renvoie la classe css correspondant
|
||||
à la case du calendrier
|
||||
(version journee divisée en demi-journees)
|
||||
"""
|
||||
Renvoie la class css de la demi journée
|
||||
"""
|
||||
|
||||
heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
|
||||
|
||||
if matin:
|
||||
|
@ -2603,8 +2374,8 @@ class Jour:
|
|||
|
||||
etat = self._get_color_assiduites_cascade(
|
||||
self._get_etats_from_assiduites(assiduites_matin),
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
show_pres=self.parent.show_pres,
|
||||
show_reta=self.parent.show_reta,
|
||||
)
|
||||
|
||||
est_just = self._get_color_justificatifs_cascade(
|
||||
|
@ -2643,8 +2414,8 @@ class Jour:
|
|||
|
||||
etat = self._get_color_assiduites_cascade(
|
||||
self._get_etats_from_assiduites(assiduites_aprem),
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
show_pres=self.parent.show_pres,
|
||||
show_reta=self.parent.show_reta,
|
||||
)
|
||||
|
||||
est_just = self._get_color_justificatifs_cascade(
|
||||
|
@ -2653,77 +2424,6 @@ 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
|
||||
heure_matin: datetime.timedelta = _time_to_timedelta(
|
||||
scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08: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
|
||||
|
||||
# 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]] = []
|
||||
|
||||
for assi in self.assiduites:
|
||||
deb: datetime.timedelta = _time_to_timedelta(
|
||||
assi.date_debut.time()
|
||||
if assi.date_debut.date() == self.date
|
||||
else heure_matin
|
||||
)
|
||||
fin: datetime.timedelta = _time_to_timedelta(
|
||||
assi.date_fin.time()
|
||||
if assi.date_fin.date() == self.date
|
||||
else heure_soir
|
||||
)
|
||||
|
||||
emplacement: float = max(((deb - heure_matin) / longueur_timeline) * 100, 0)
|
||||
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 ""
|
||||
|
||||
assiduite_blocks.append(
|
||||
{
|
||||
"longueur": longueur,
|
||||
"emplacement": emplacement,
|
||||
"etat": etat,
|
||||
"est_just": est_just,
|
||||
"bubble": _generate_assiduite_bubble(assi),
|
||||
"id": assi.assiduite_id,
|
||||
}
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/widgets/minitimeline_simple.j2",
|
||||
assi_blocks=assiduite_blocks,
|
||||
)
|
||||
|
||||
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))
|
||||
|
||||
|
@ -2762,47 +2462,110 @@ class Jour:
|
|||
|
||||
return ""
|
||||
|
||||
def _generate_minitimeline(self) -> str:
|
||||
"""
|
||||
Génère la minitimeline du jour
|
||||
"""
|
||||
# Récupérer le référenciel de la timeline
|
||||
heure_matin: datetime.timedelta = _time_to_timedelta(
|
||||
scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08: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
|
||||
|
||||
def _get_dates_between(deb: datetime.date, fin: datetime.date) -> list[datetime.date]:
|
||||
resultat = []
|
||||
date_actuelle = deb
|
||||
while date_actuelle <= fin:
|
||||
resultat.append(date_actuelle)
|
||||
date_actuelle += datetime.timedelta(days=1)
|
||||
return resultat
|
||||
# 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]] = []
|
||||
|
||||
for assi in self.assiduites:
|
||||
deb: datetime.timedelta = _time_to_timedelta(
|
||||
assi.date_debut.time()
|
||||
if assi.date_debut.date() == self.date
|
||||
else heure_matin
|
||||
)
|
||||
fin: datetime.timedelta = _time_to_timedelta(
|
||||
assi.date_fin.time()
|
||||
if assi.date_fin.date() == self.date
|
||||
else heure_soir
|
||||
)
|
||||
|
||||
emplacement: float = max(((deb - heure_matin) / longueur_timeline) * 100, 0)
|
||||
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 ""
|
||||
|
||||
assiduite_blocks.append(
|
||||
{
|
||||
"longueur": longueur,
|
||||
"emplacement": emplacement,
|
||||
"etat": etat,
|
||||
"est_just": est_just,
|
||||
"bubble": _generate_assiduite_bubble(assi),
|
||||
"id": assi.assiduite_id,
|
||||
}
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/widgets/minitimeline_simple.j2",
|
||||
assi_blocks=assiduite_blocks,
|
||||
)
|
||||
|
||||
|
||||
def _organize_by_month(days, assiduites, justificatifs) -> dict[str, list[Jour]]:
|
||||
class CalendrierAssi(sco_gen_cal.Calendrier):
|
||||
"""
|
||||
Organiser les dates par mois.
|
||||
Représente un calendrier d'assiduité d'un étudiant
|
||||
"""
|
||||
organized = {}
|
||||
for date in days:
|
||||
# Récupérer le mois en français
|
||||
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
|
||||
# Ajouter le jour à la liste correspondante au mois
|
||||
if month not in organized:
|
||||
organized[month] = []
|
||||
|
||||
date_assiduites: Query = scass.filter_by_date(
|
||||
assiduites,
|
||||
def __init__(self, annee: int, etudiant: Identite, **options):
|
||||
# On prend du 01/09 au 31/08
|
||||
date_debut: datetime.datetime = datetime.datetime(annee, 9, 1, 0, 0)
|
||||
date_fin: datetime.datetime = datetime.datetime(annee + 1, 8, 31, 23, 59)
|
||||
super().__init__(date_debut, date_fin)
|
||||
|
||||
# On récupère les assiduités et les justificatifs
|
||||
self.etud_assiduites: Query = scass.filter_by_date(
|
||||
etudiant.assiduites,
|
||||
Assiduite,
|
||||
date_deb=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
self.etud_justificatifs: Query = scass.filter_by_date(
|
||||
etudiant.justificatifs,
|
||||
Justificatif,
|
||||
date_deb=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
|
||||
# Ajout des options (exemple : mode_demi, show_pres, show_reta, ...)
|
||||
for key, value in options.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> JourAssi:
|
||||
"""
|
||||
Instancie un jour d'assiduité
|
||||
"""
|
||||
assiduites: Query = scass.filter_by_date(
|
||||
self.etud_assiduites,
|
||||
Assiduite,
|
||||
date_deb=datetime.datetime.combine(date, datetime.time(0, 0)),
|
||||
date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)),
|
||||
)
|
||||
|
||||
date_justificatifs: Query = scass.filter_by_date(
|
||||
justificatifs,
|
||||
justificatifs: Query = scass.filter_by_date(
|
||||
self.etud_justificatifs,
|
||||
Justificatif,
|
||||
date_deb=datetime.datetime.combine(date, datetime.time(0, 0)),
|
||||
date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)),
|
||||
)
|
||||
# On génère un `Jour` composé d'une date, et des assiduités/justificatifs du jour
|
||||
jour: Jour = Jour(date, date_assiduites, date_justificatifs)
|
||||
|
||||
organized[month].append(jour)
|
||||
|
||||
return organized
|
||||
return JourAssi(date, assiduites, justificatifs, parent=self)
|
||||
|
||||
|
||||
def _time_to_timedelta(t: datetime.time) -> datetime.timedelta:
|
||||
|
@ -2818,14 +2581,26 @@ def _generate_assiduite_bubble(assiduite: Assiduite) -> str:
|
|||
# Récupérer informations saisie
|
||||
saisie: str = assiduite.get_saisie()
|
||||
|
||||
motif: str = assiduite.description if assiduite.description else ""
|
||||
motif: str = assiduite.description or "Non spécifié"
|
||||
|
||||
# Récupérer date
|
||||
|
||||
if assiduite.date_debut.date() == assiduite.date_fin.date():
|
||||
jour = assiduite.date_debut.strftime("%d/%m/%Y")
|
||||
heure_deb: str = assiduite.date_debut.strftime("%H:%M")
|
||||
heure_fin: str = assiduite.date_fin.strftime("%H:%M")
|
||||
date: str = f"{jour} de {heure_deb} à {heure_fin}"
|
||||
else:
|
||||
date: str = (
|
||||
f"du {assiduite.date_debut.strftime('%d/%m/%Y')} "
|
||||
+ f"au {assiduite.date_fin.strftime('%d/%m/%Y')}"
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/widgets/assiduite_bubble.j2",
|
||||
moduleimpl=moduleimpl_infos,
|
||||
etat=scu.EtatAssiduite(assiduite.etat).name.lower(),
|
||||
date_debut=assiduite.date_debut.strftime("%d/%m/%Y %H:%M"),
|
||||
date_fin=assiduite.date_fin.strftime("%d/%m/%Y %H:%M"),
|
||||
date=date,
|
||||
saisie=saisie,
|
||||
motif=motif,
|
||||
)
|
||||
|
|
|
@ -1248,6 +1248,7 @@ def view_module_abs(moduleimpl_id, fmt="html"):
|
|||
filename="absmodule_" + scu.make_filename(modimpl.module.titre_str()),
|
||||
caption=f"Absences dans le module {modimpl.module.titre_str()}",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="view_module_abs",
|
||||
)
|
||||
|
||||
if fmt != "html":
|
||||
|
@ -1340,7 +1341,7 @@ def formsemestre_enseignants_list(formsemestre_id, fmt="html"):
|
|||
|
||||
# --- Generate page with table
|
||||
title = f"Enseignants de {formsemestre.titre_mois()}"
|
||||
T = GenTable(
|
||||
table = GenTable(
|
||||
columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"],
|
||||
titles={
|
||||
"nom_fmt": "Nom",
|
||||
|
@ -1361,8 +1362,9 @@ def formsemestre_enseignants_list(formsemestre_id, fmt="html"):
|
|||
caption="""Tous les enseignants (responsables ou associés aux modules de
|
||||
ce semestre) apparaissent. Le nombre de saisies d'absences est indicatif.""",
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
table_id="formsemestre_enseignants_list",
|
||||
)
|
||||
return T.make_page(page_title=title, title=title, fmt=fmt)
|
||||
return table.make_page(page_title=title, title=title, fmt=fmt)
|
||||
|
||||
|
||||
@bp.route("/edit_enseignants_form_delete", methods=["GET", "POST"])
|
||||
|
@ -1436,14 +1438,14 @@ def formsemestre_desinscription(etudid, formsemestre_id, dialog_confirmed=False)
|
|||
if nt.etud_has_decision(etudid):
|
||||
raise ScoValueError(
|
||||
f"""Désinscription impossible: l'étudiant a une décision de jury
|
||||
(la supprimer avant si nécessaire:
|
||||
<a href="{
|
||||
(la supprimer avant si nécessaire avec
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_validation_suppress_etud",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||
formsemestre_id=formsemestre_id)
|
||||
}">supprimer décision jury</a>
|
||||
)
|
||||
"""
|
||||
}">supprimer décision jury</a>)
|
||||
""",
|
||||
safe=True,
|
||||
)
|
||||
if not dialog_confirmed:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
|
@ -2893,20 +2895,21 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
|
|||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre_jury_but_erase/<int:formsemestre_id>",
|
||||
"/formsemestre_jury_erase/<int:formsemestre_id>",
|
||||
methods=["GET", "POST"],
|
||||
defaults={"etudid": None},
|
||||
)
|
||||
@bp.route(
|
||||
"/formsemestre_jury_but_erase/<int:formsemestre_id>/<int:etudid>",
|
||||
"/formsemestre_jury_erase/<int:formsemestre_id>/<int:etudid>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
|
||||
"""Supprime la décision de jury BUT pour cette année.
|
||||
def formsemestre_jury_erase(formsemestre_id: int, etudid: int = None):
|
||||
"""Supprime la décision de jury (classique ou BUT) pour cette année.
|
||||
Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits.
|
||||
Si only_one_sem, n'efface que pour le formsemestre indiqué, pas les deux de l'année.
|
||||
En BUT, si only_one_sem n'efface que pour le formsemestre indiqué, pas les deux de l'année.
|
||||
En classique, n'affecte que les décisions issues de ce formsemestre.
|
||||
"""
|
||||
only_one_sem = int(request.args.get("only_one_sem") or False)
|
||||
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
||||
|
@ -2920,8 +2923,7 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
|
|||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
)
|
||||
if not formsemestre.formation.is_apc():
|
||||
raise ScoValueError("semestre non BUT")
|
||||
is_apc = formsemestre.formation.is_apc()
|
||||
if etudid is None:
|
||||
etud = None
|
||||
etuds = formsemestre.get_inscrits(include_demdef=True)
|
||||
|
@ -2934,8 +2936,13 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
|
|||
else:
|
||||
etud = Identite.get_etud(etudid)
|
||||
etuds = [etud]
|
||||
endpoint = (
|
||||
"notes.formsemestre_validation_but"
|
||||
if is_apc
|
||||
else "notes.formsemestre_validation_etud_form"
|
||||
)
|
||||
dest_url = url_for(
|
||||
"notes.formsemestre_validation_but",
|
||||
endpoint,
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
etudid=etudid,
|
||||
|
@ -2943,13 +2950,18 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
|
|||
if request.method == "POST":
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etud in etuds:
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
deca.erase(only_one_sem=only_one_sem)
|
||||
log(f"formsemestre_jury_but_erase({formsemestre_id}, {etudid})")
|
||||
if is_apc:
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
deca.erase(only_one_sem=only_one_sem)
|
||||
else:
|
||||
sco_formsemestre_validation.formsemestre_validation_suppress_etud(
|
||||
formsemestre.id, etud.id
|
||||
)
|
||||
log(f"formsemestre_jury_erase({formsemestre_id}, {etud.id})")
|
||||
flash(
|
||||
(
|
||||
"décisions de jury du semestre effacées"
|
||||
if only_one_sem
|
||||
if (only_one_sem or is_apc)
|
||||
else "décisions de jury des semestres de l'année BUT effacées"
|
||||
)
|
||||
+ f" pour {len(etuds)} étudiant{'s' if len(etuds) > 1 else ''}"
|
||||
|
@ -2964,22 +2976,29 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
|
|||
else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre")
|
||||
} ?""",
|
||||
explanation=(
|
||||
f"""Les validations d'UE et autorisations de passage
|
||||
(
|
||||
f"""Les validations d'UE et autorisations de passage
|
||||
du semestre S{formsemestre.semestre_id} seront effacées."""
|
||||
if only_one_sem
|
||||
else """Les validations de toutes les UE, RCUE (compétences) et année
|
||||
if (only_one_sem or is_apc)
|
||||
else """Les validations de toutes les UE, RCUE (compétences) et année
|
||||
issues de cette année scolaire seront effacées.
|
||||
"""
|
||||
)
|
||||
+ """
|
||||
)
|
||||
+ """
|
||||
<p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p>
|
||||
"""
|
||||
+ """
|
||||
<p>Efface aussi toutes les validations concernant l'année BUT de ce semestre,
|
||||
même si elles ont été acquises ailleurs.
|
||||
</p>
|
||||
</p>"""
|
||||
if is_apc
|
||||
else ""
|
||||
+ """
|
||||
<div class="warning">Cette opération est irréversible !
|
||||
A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite.
|
||||
</div>
|
||||
""",
|
||||
"""
|
||||
),
|
||||
cancel_url=dest_url,
|
||||
)
|
||||
|
||||
|
|
|
@ -128,6 +128,7 @@ def refcomp_table():
|
|||
}
|
||||
for ref in refs
|
||||
],
|
||||
table_id="refcomp_table",
|
||||
)
|
||||
return render_template(
|
||||
"but/refcomp_table.j2",
|
||||
|
|
|
@ -85,6 +85,7 @@ from app.scodoc import (
|
|||
html_sco_header,
|
||||
sco_import_etuds,
|
||||
sco_archives_etud,
|
||||
sco_bug_report,
|
||||
sco_cache,
|
||||
sco_debouche,
|
||||
sco_dept,
|
||||
|
@ -109,6 +110,7 @@ from app.scodoc import (
|
|||
sco_up_to_date,
|
||||
)
|
||||
from app.tables import list_etuds
|
||||
from app.forms.main.create_bug_report import CreateBugReport
|
||||
|
||||
|
||||
def sco_publish(route, function, permission, methods=["GET"]):
|
||||
|
@ -332,6 +334,7 @@ def showEtudLog(etudid, fmt="html"):
|
|||
fiche de {etud['nomprenom']}</a></li>
|
||||
</ul>""",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="showEtudLog",
|
||||
)
|
||||
|
||||
return tab.make_page(fmt=fmt)
|
||||
|
@ -1367,7 +1370,9 @@ def etudident_edit_form():
|
|||
|
||||
|
||||
def _validate_date_naissance(val: str, field) -> bool:
|
||||
"vrai si date saisie valide"
|
||||
"vrai si date saisie valide (peut être vide)"
|
||||
if not val:
|
||||
return True
|
||||
try:
|
||||
date_naissance = scu.convert_fr_date(val)
|
||||
except ScoValueError:
|
||||
|
@ -1788,7 +1793,11 @@ def _etudident_create_or_edit_form(edit):
|
|||
+ homonyms_html
|
||||
+ F
|
||||
)
|
||||
tf[2]["date_naissance"] = scu.convert_fr_date(tf[2]["date_naissance"])
|
||||
tf[2]["date_naissance"] = (
|
||||
scu.convert_fr_date(tf[2]["date_naissance"])
|
||||
if tf[2]["date_naissance"]
|
||||
else None
|
||||
)
|
||||
if not edit:
|
||||
etud = sco_etud.create_etud(cnx, args=tf[2])
|
||||
etudid = etud["etudid"]
|
||||
|
@ -2527,25 +2536,65 @@ def stat_bac(formsemestre_id):
|
|||
def sco_dump_and_send_db(message="", request_url="", traceback_str_base64=""):
|
||||
"Send anonymized data to supervision"
|
||||
|
||||
status_code = sco_dump_db.sco_dump_and_send_db(
|
||||
r = sco_dump_db.sco_dump_and_send_db(
|
||||
message, request_url, traceback_str_base64=traceback_str_base64
|
||||
)
|
||||
|
||||
status_code = r.status_code
|
||||
|
||||
try:
|
||||
r_msg = r.json()["message"]
|
||||
except (requests.exceptions.JSONDecodeError, KeyError):
|
||||
r_msg = "Erreur: code <tt>"
|
||||
+status_code
|
||||
+'</tt> Merci de contacter <a href="mailto:'
|
||||
+scu.SCO_DEV_MAIL
|
||||
+'">'
|
||||
+scu.SCO_DEV_MAIL
|
||||
+"</a>"
|
||||
|
||||
H = [html_sco_header.sco_header(page_title="Assistance technique")]
|
||||
if status_code == requests.codes.INSUFFICIENT_STORAGE: # pylint: disable=no-member
|
||||
H.append(
|
||||
"""<p class="warning">
|
||||
Erreur: espace serveur trop plein.
|
||||
Merci de contacter <a href="mailto:{0}">{0}</a></p>""".format(
|
||||
scu.SCO_DEV_MAIL
|
||||
)
|
||||
)
|
||||
elif status_code == requests.codes.OK: # pylint: disable=no-member
|
||||
H.append("""<p>Opération effectuée.</p>""")
|
||||
if status_code == requests.codes.OK: # pylint: disable=no-member
|
||||
H.append(f"""<p>Opération effectuée.</p><p>{r_msg}</p>""")
|
||||
else:
|
||||
H.append(
|
||||
f"""<p class="warning">
|
||||
Erreur: code <tt>{status_code}</tt>
|
||||
Merci de contacter <a href="mailto:{scu.SCO_DEV_MAIL}">{scu.SCO_DEV_MAIL}</a></p>"""
|
||||
)
|
||||
H.append(f"""<p class="warning">{r_msg}</p>""")
|
||||
flash("Données envoyées au serveur d'assistance")
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
# --- Report form (assistance)
|
||||
@bp.route("/sco_bug_report", methods=["GET", "POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def sco_bug_report_form():
|
||||
"Formulaire de création d'un ticket d'assistance"
|
||||
|
||||
form = CreateBugReport()
|
||||
if request.method == "POST" and form.cancel.data: # cancel button
|
||||
return flask.redirect(url_for("scodoc.index"))
|
||||
if form.validate_on_submit():
|
||||
r = sco_bug_report.sco_bug_report(
|
||||
form.title.data, form.message.data, form.etab.data, form.include_dump.data
|
||||
)
|
||||
|
||||
status_code = r.status_code
|
||||
try:
|
||||
r_msg = r.json()["message"]
|
||||
except (requests.exceptions.JSONDecodeError, KeyError):
|
||||
log(f"sco_bug_report: error {status_code}")
|
||||
r_msg = f"""Erreur: code <tt>{status_code}</tt>
|
||||
Merci de contacter
|
||||
<a href="mailto:{scu.SCO_DEV_MAIL}">{scu.SCO_DEV_MAIL}</a>
|
||||
"""
|
||||
|
||||
H = [html_sco_header.sco_header(page_title="Assistance technique")]
|
||||
if r.status_code >= 200 and r.status_code < 300:
|
||||
H.append(f"""<p>Opération effectuée.</p><p>{r_msg}</p>""")
|
||||
else:
|
||||
H.append(f"""<p class="warning">{r_msg}</p>""")
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
return render_template(
|
||||
"sco_bug_report.j2",
|
||||
form=form,
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.963"
|
||||
SCOVERSION = "9.6.967"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
@ -14,7 +14,7 @@ SCONEWS = """
|
|||
<li>Nouveaux bulletins BUT compacts</li>
|
||||
<li>Nouvelle gestion des absences et assiduité</li>
|
||||
<li>Mise à jour logiciels: Debian 12, Python 3.11, ...</li>
|
||||
<li>Evaluations bonus</li>
|
||||
<li>Évaluations bonus</li>
|
||||
</ul>
|
||||
|
||||
<li>ScoDoc 9.5 (juillet 2023)</li>
|
||||
|
|
72
tests/api/dump_all_results.py
Normal file
72
tests/api/dump_all_results.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Exemple utilisation API ScoDoc 9
|
||||
|
||||
Usage:
|
||||
cd /opt/scodoc/tests/api
|
||||
python -i dump_all_results.py
|
||||
|
||||
Demande les résultats (bulletins JSON) de tous les semestres
|
||||
et les enregistre dans un fichier json
|
||||
(`all_results.json` du rép. courant)
|
||||
|
||||
Sur M1 en 9.6.966, processed 1219 formsemestres in 645.64s
|
||||
Sur M1 en 9.6.967, processed 1219 formsemestres in 626.22s
|
||||
|
||||
"""
|
||||
|
||||
import json
|
||||
from pprint import pprint as pp
|
||||
import sys
|
||||
import time
|
||||
import urllib3
|
||||
from setup_test_api import (
|
||||
API_PASSWORD,
|
||||
API_URL,
|
||||
API_USER,
|
||||
APIError,
|
||||
CHECK_CERTIFICATE,
|
||||
get_auth_headers,
|
||||
GET,
|
||||
POST_JSON,
|
||||
SCODOC_URL,
|
||||
)
|
||||
|
||||
|
||||
if not CHECK_CERTIFICATE:
|
||||
urllib3.disable_warnings()
|
||||
|
||||
print(f"SCODOC_URL={SCODOC_URL}")
|
||||
print(f"API URL={API_URL}")
|
||||
|
||||
|
||||
HEADERS = get_auth_headers(API_USER, API_PASSWORD)
|
||||
|
||||
|
||||
# Liste de tous les formsemestres (de tous les depts)
|
||||
formsemestres = GET("/formsemestres/query", headers=HEADERS)
|
||||
print(f"{len(formsemestres)} formsemestres")
|
||||
|
||||
#
|
||||
all_results = []
|
||||
t0 = time.time()
|
||||
for formsemestre in formsemestres:
|
||||
print(f"{formsemestre['session_id']}\t", end="", flush=True)
|
||||
t = time.time()
|
||||
result = GET(f"/formsemestre/{formsemestre['id']}/resultats", headers=HEADERS)
|
||||
print(f"{(time.time()-t):3.2f}s")
|
||||
all_results.append(
|
||||
f"""
|
||||
{{
|
||||
"formsemestre" : { json.dumps(formsemestre, indent=1, sort_keys=True) },
|
||||
"resultats" : { json.dumps(result, indent=1, sort_keys=True) }
|
||||
}}
|
||||
"""
|
||||
)
|
||||
|
||||
print(f"Processed {len(formsemestres)} formsemestres in {(time.time()-t0):3.2f}s")
|
||||
|
||||
with open("all_results.json", "w", encoding="utf-8") as f:
|
||||
f.write("[\n" + ",\n".join(all_results) + "\n]")
|
|
@ -247,7 +247,7 @@ 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, _ = moy_mod.load_evaluations_poids(moduleimpl_id)
|
||||
evals_poids = modimpl.get_evaluations_poids()
|
||||
assert evals_poids.shape == (nb_evals, nb_ues)
|
||||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
||||
|
|
|
@ -400,10 +400,10 @@ def test_import_etuds_xlsx(test_client):
|
|||
# Test de search_etud_in_dept
|
||||
etuds = sco_find_etud.search_etuds_infos_from_exp("NOM10")
|
||||
assert len(etuds) == 1
|
||||
assert etuds[0]["code_ine"] == "ine10"
|
||||
assert etuds[0].code_ine == "ine10"
|
||||
etuds = sco_find_etud.search_etuds_infos_from_exp("NOM")
|
||||
assert len(etuds) > 1
|
||||
assert all(e["nom"].startswith("NOM") for e in etuds)
|
||||
assert all(e.nom.startswith("NOM") for e in etuds)
|
||||
etuds = sco_find_etud.search_etuds_infos_from_exp("1000010")
|
||||
assert len(etuds) == 1
|
||||
assert etuds[0]["code_ine"] == "ine10"
|
||||
assert etuds[0].code_ine == "ine10"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user