Compare commits
90 Commits
Author | SHA1 | Date | |
---|---|---|---|
02a5b00ecf | |||
dcdf6a8012 | |||
912a213dcd | |||
3575e89dc0 | |||
21c0625147 | |||
e18c1d8fd0 | |||
5867d0f430 | |||
9897ccc659 | |||
|
7575959bd4 | ||
|
2aafbad9e2 | ||
50f2cd7a0f | |||
fd8fbb9e02 | |||
|
ebcef76950 | ||
|
13349776af | ||
|
f275286b71 | ||
|
f4f6c13d79 | ||
e7f23efe65 | |||
e44d3fd5dc | |||
fac36fa11c | |||
9289535359 | |||
|
d73b925006 | ||
6749ca70d6 | |||
|
dea403b03d | ||
|
ab9543c310 | ||
|
f94998f66b | ||
|
eb88a8ca83 | ||
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 | |||
87aaf12d27 | |||
c8ab9b9b6c | |||
ad7b48e110 |
80
README.md
80
README.md
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
|
(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>
|
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
|
### Lignes de commandes
|
||||||
|
|
||||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
Voir [le guide de configuration](https://scodoc.org/GuideConfig).
|
||||||
|
|
||||||
## Organisation des fichiers
|
## Organisation des fichiers
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ 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é.
|
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||||
|
|
||||||
Principaux contenus:
|
Principaux contenus:
|
||||||
|
```
|
||||||
/opt/scodoc-data
|
/opt/scodoc-data
|
||||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||||
/opt/scodoc-data/config # Fichiers de configuration
|
/opt/scodoc-data/config # Fichiers de configuration
|
||||||
|
@ -49,37 +49,33 @@ Principaux contenus:
|
||||||
.../config/depts # un fichier par département
|
.../config/depts # un fichier par département
|
||||||
/opt/scodoc-data/photos # Photos des étudiants
|
/opt/scodoc-data/photos # Photos des étudiants
|
||||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||||
|
```
|
||||||
## Pour les développeurs
|
## Pour les développeurs
|
||||||
|
|
||||||
### Installation du code
|
### 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.
|
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||||
|
```bash
|
||||||
sudo su
|
sudo su
|
||||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||||
apt-get install git # si besoin
|
apt-get install git # si besoin
|
||||||
cd /opt
|
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
|
||||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
|
||||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||||
|
|
||||||
# Renommer le répertoire:
|
# Donner ce répertoire à l'utilisateur scodoc:
|
||||||
mv ScoDoc scodoc
|
chown -R scodoc:scodoc /opt/scodoc
|
||||||
|
```
|
||||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
|
||||||
chown -R scodoc.scodoc /opt/scodoc
|
|
||||||
|
|
||||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||||
|
```bash
|
||||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||||
mv /opt/off-scodoc/venv /opt/scodoc
|
mv /opt/off-scodoc/venv /opt/scodoc
|
||||||
|
```
|
||||||
Et la config:
|
Et la config:
|
||||||
|
```bash
|
||||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||||
|
```
|
||||||
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
|
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
|
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
||||||
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
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`.
|
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||||
Avant le premier lancement, créer cette base ainsi:
|
Avant le premier lancement, créer cette base ainsi:
|
||||||
|
```bash
|
||||||
./tools/create_database.sh SCODOC_TEST
|
./tools/create_database.sh SCODOC_TEST
|
||||||
export FLASK_ENV=test
|
export FLASK_ENV=test
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
```
|
||||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
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
|
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.
|
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
|
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||||
scripts de tests:
|
scripts de tests:
|
||||||
Lancer au préalable:
|
Lancer au préalable:
|
||||||
|
```bash
|
||||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||||
|
```
|
||||||
Puis dérouler les tests unitaires:
|
Puis dérouler les tests unitaires:
|
||||||
|
```bash
|
||||||
pytest tests/unit
|
pytest tests/unit
|
||||||
|
```
|
||||||
Ou avec couverture (`pip install pytest-cov`)
|
Ou avec couverture (`pip install pytest-cov`)
|
||||||
|
```bash
|
||||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||||
|
```
|
||||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
#### 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
|
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||||
|
@ -119,34 +115,34 @@ 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
|
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||||
par les tests:
|
par les tests:
|
||||||
|
```bash
|
||||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||||
|
```
|
||||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||||
normalement, par exemple:
|
normalement, par exemple:
|
||||||
|
```bash
|
||||||
pytest tests/unit/test_sco_basic.py
|
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
|
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||||
utilisateur:
|
utilisateur:
|
||||||
|
```bash
|
||||||
flask user-password admin
|
flask user-password admin
|
||||||
|
```
|
||||||
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
**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 !
|
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
||||||
|
|
||||||
#### Modification du schéma de la base
|
#### Modification du schéma de la base
|
||||||
|
|
||||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||||
|
```bash
|
||||||
flask db migrate -m "message explicatif....."
|
flask db migrate -m "message explicatif....."
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
```
|
||||||
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
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`
|
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
||||||
ou variables d'environnement pour interroger la bonne base !).
|
ou variables d'environnement pour interroger la bonne base !).
|
||||||
|
```bash
|
||||||
dropdb SCODOC_DEV
|
dropdb SCODOC_DEV
|
||||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||||
flask db upgrade # créé les tables à partir des migrations
|
flask db upgrade # créé les tables à partir des migrations
|
||||||
|
@ -155,7 +151,7 @@ ou variables d'environnement pour interroger la bonne base !).
|
||||||
# puis imports:
|
# puis imports:
|
||||||
flask import-scodoc7-users
|
flask import-scodoc7-users
|
||||||
flask import-scodoc7-dept STID SCOSTID
|
flask import-scodoc7-dept STID SCOSTID
|
||||||
|
```
|
||||||
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
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
|
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||||
positionner à la bonne étape.
|
positionner à la bonne étape.
|
||||||
|
@ -163,19 +159,19 @@ positionner à la bonne étape.
|
||||||
### Profiling
|
### Profiling
|
||||||
|
|
||||||
Sur une machine de DEV, lancer
|
Sur une machine de DEV, lancer
|
||||||
|
```bash
|
||||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
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`).
|
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:
|
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||||
|
```bash
|
||||||
pip install snakeviz
|
pip install snakeviz
|
||||||
|
```
|
||||||
puis
|
puis
|
||||||
|
```bash
|
||||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||||
|
```
|
||||||
## Paquet Debian 12
|
## Paquet Debian 12
|
||||||
|
|
||||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||||
|
|
|
@ -637,14 +637,12 @@ def critical_error(msg):
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
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()
|
clear_scodoc_cache()
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""
|
f"""
|
||||||
Une erreur est survenue.
|
Une erreur est survenue, veuillez ré-essayer.
|
||||||
|
|
||||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
|
||||||
{scu.SCO_DISCORD_ASSISTANCE}
|
|
||||||
|
|
||||||
{msg}
|
{msg}
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,14 +3,15 @@
|
||||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
"""ScoDoc 9 API : Assiduités
|
"""ScoDoc 9 API : Assiduités"""
|
||||||
"""
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_json import as_json
|
from flask_json import as_json
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||||
|
|
||||||
from app import db, log, set_sco_dept
|
from app import db, log, set_sco_dept
|
||||||
import app.scodoc.sco_assiduites as scass
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
@ -858,7 +859,10 @@ def assiduite_edit(assiduite_id: int):
|
||||||
msg=f"assiduite: modif {assiduite_unique}",
|
msg=f"assiduite: modif {assiduite_unique}",
|
||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
try:
|
||||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||||
|
except ObjectDeletedError:
|
||||||
|
return json_error(404, "Assiduité supprimée / inexistante")
|
||||||
|
|
||||||
return {"OK": True}
|
return {"OK": True}
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,9 @@ def _build_bulletin_but_infos(
|
||||||
formsemestre, bulletins_sem.res
|
formsemestre, bulletins_sem.res
|
||||||
)
|
)
|
||||||
if warn_html:
|
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(
|
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
||||||
refcomp, etud
|
refcomp, etud
|
||||||
)
|
)
|
||||||
|
|
|
@ -73,6 +73,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
html_class="notes_bulletin",
|
html_class="notes_bulletin",
|
||||||
html_class_ignore_default=True,
|
html_class_ignore_default=True,
|
||||||
html_with_td_classes=True,
|
html_with_td_classes=True,
|
||||||
|
table_id="bul-table",
|
||||||
)
|
)
|
||||||
table_objects = table.gen(fmt=fmt)
|
table_objects = table.gen(fmt=fmt)
|
||||||
objects += table_objects
|
objects += table_objects
|
||||||
|
@ -427,12 +428,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||||
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
||||||
else "*"
|
else "*"
|
||||||
)
|
)
|
||||||
|
note_value = e["note"].get("value", "")
|
||||||
t = {
|
t = {
|
||||||
"titre": f"{e['description'] or ''}",
|
"titre": f"{e['description'] or ''}",
|
||||||
"moyenne": e["note"]["value"],
|
"moyenne": note_value,
|
||||||
"_moyenne_pdf": Paragraph(
|
"_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
|
||||||
f"""<para align=right>{e["note"]["value"]}</para>"""
|
|
||||||
),
|
|
||||||
"coef": coef,
|
"coef": coef,
|
||||||
"_coef_pdf": Paragraph(
|
"_coef_pdf": Paragraph(
|
||||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
|
|
||||||
"""caches pour tables APC
|
"""caches pour tables APC
|
||||||
"""
|
"""
|
||||||
|
from flask import g
|
||||||
|
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
|
|
||||||
|
@ -47,3 +48,27 @@ class EvaluationsPoidsCache(sco_cache.ScoDocCache):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
prefix = "EPC"
|
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_cache
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.codes_cursus import UE_SPORT
|
from app.scodoc.codes_cursus import UE_SPORT
|
||||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
|
||||||
|
|
||||||
|
@ -113,6 +112,8 @@ class ModuleImplResults:
|
||||||
"""
|
"""
|
||||||
self.evals_etudids_sans_note = {}
|
self.evals_etudids_sans_note = {}
|
||||||
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
|
"""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.load_notes(etudids, etudids_actifs)
|
||||||
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
|
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"""
|
"""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 = []
|
||||||
self.evaluations_completes_dict = {}
|
self.evaluations_completes_dict = {}
|
||||||
self.etudids_attente = set() # empty
|
self.etudids_attente = set() # empty
|
||||||
|
self.evals_type = {}
|
||||||
|
evaluation: Evaluation
|
||||||
for evaluation in moduleimpl.evaluations:
|
for evaluation in moduleimpl.evaluations:
|
||||||
|
self.evals_type[evaluation.id] = evaluation.evaluation_type
|
||||||
eval_df = self._load_evaluation_notes(evaluation)
|
eval_df = self._load_evaluation_notes(evaluation)
|
||||||
# is_complete ssi
|
# is_complete ssi
|
||||||
# tous les inscrits (non dem) au module ont une note
|
# tous les inscrits (non dem) au module ont une note
|
||||||
|
@ -270,6 +274,24 @@ class ModuleImplResults:
|
||||||
* self.evaluations_completes
|
* self.evaluations_completes
|
||||||
).reshape(-1, 1)
|
).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
|
# was _list_notes_evals_titles
|
||||||
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||||
"Liste des évaluations complètes"
|
"Liste des évaluations complètes"
|
||||||
|
@ -296,32 +318,26 @@ class ModuleImplResults:
|
||||||
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||||
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas.
|
"""Les évaluations de rattrapage de ce module.
|
||||||
Rattrapage: la moyenne du module est la meilleure note entre moyenne
|
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
|
e
|
||||||
for e in moduleimpl.evaluations
|
for e in moduleimpl.evaluations
|
||||||
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
|
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:
|
def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||||
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas.
|
"""Les évaluations 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.
|
La moyenne des notes de Session 2 remplace la note de moyenne des autres évals.
|
||||||
"""
|
"""
|
||||||
eval_list = [
|
return [
|
||||||
e
|
e
|
||||||
for e in moduleimpl.evaluations
|
for e in moduleimpl.evaluations
|
||||||
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
|
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]:
|
def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]:
|
||||||
"""Les évaluations bonus de ce module, ou liste vide s'il n'en a pas."""
|
"""Les évaluations bonus de ce module, ou liste vide s'il n'en a pas."""
|
||||||
|
@ -344,12 +360,13 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||||
"Calcul des moyennes de modules à la mode BUT"
|
"Calcul des moyennes de modules à la mode BUT"
|
||||||
|
|
||||||
def compute_module_moy(
|
def compute_module_moy(
|
||||||
self,
|
self, evals_poids_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||||
evals_poids_df: pd.DataFrame,
|
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
"""Calcule les moyennes des étudiants dans ce module
|
"""Calcule les moyennes des étudiants dans ce module
|
||||||
|
|
||||||
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
Argument:
|
||||||
|
evals_poids: DataFrame, colonnes: UEs, lignes: EVALs
|
||||||
|
modimpl_coefs_df: DataFrame, colonnes: modimpl_id, lignes: ue_id
|
||||||
|
|
||||||
Résultat: DataFrame, colonnes UE, lignes etud
|
Résultat: DataFrame, colonnes UE, lignes etud
|
||||||
= la note de l'étudiant dans chaque UE pour ce module.
|
= la note de l'étudiant dans chaque UE pour ce module.
|
||||||
|
@ -370,6 +387,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||||
if nb_ues == 0:
|
if nb_ues == 0:
|
||||||
return pd.DataFrame(index=self.evals_notes.index, columns=[])
|
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_coefs = self.get_evaluations_coefs(modimpl)
|
||||||
evals_poids = evals_poids_df.values * evals_coefs
|
evals_poids = evals_poids_df.values * evals_coefs
|
||||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||||
|
@ -398,6 +416,47 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||||
) / np.sum(evals_poids_etuds, axis=1)
|
) / np.sum(evals_poids_etuds, axis=1)
|
||||||
# etuds_moy_module shape: nb_etuds x nb_ues
|
# 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 avec coef non nul ont bien une note de session 2 calculée:
|
||||||
|
mod_coefs = modimpl_coefs_df[modimpl.id]
|
||||||
|
etuds_use_session2 = np.all(
|
||||||
|
np.isfinite(etuds_moy_module_s2[:, mod_coefs != 0]), 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:
|
# Application des évaluations bonus:
|
||||||
etuds_moy_module = self.apply_bonus(
|
etuds_moy_module = self.apply_bonus(
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
|
@ -405,47 +464,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||||
evals_poids_df,
|
evals_poids_df,
|
||||||
evals_notes_stacked,
|
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(
|
self.etuds_moy_module = pd.DataFrame(
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
index=self.evals_notes.index,
|
index=self.evals_notes.index,
|
||||||
|
@ -453,6 +471,34 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||||
)
|
)
|
||||||
return self.etuds_moy_module
|
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(
|
def apply_bonus(
|
||||||
self,
|
self,
|
||||||
etuds_moy_module: pd.DataFrame,
|
etuds_moy_module: pd.DataFrame,
|
||||||
|
@ -525,6 +571,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||||
return evals_poids, ues
|
return evals_poids, ues
|
||||||
|
|
||||||
|
|
||||||
|
# appelé par ModuleImpl.check_apc_conformity()
|
||||||
def moduleimpl_is_conforme(
|
def moduleimpl_is_conforme(
|
||||||
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
@ -546,12 +593,12 @@ def moduleimpl_is_conforme(
|
||||||
if len(modimpl_coefs_df) != nb_ues:
|
if len(modimpl_coefs_df) != nb_ues:
|
||||||
# il arrive (#bug) que le cache ne soit pas à jour...
|
# il arrive (#bug) que le cache ne soit pas à jour...
|
||||||
sco_cache.invalidate_formsemestre()
|
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:
|
if moduleimpl.id not in modimpl_coefs_df:
|
||||||
# soupçon de bug cache coef ?
|
# soupçon de bug cache coef ?
|
||||||
sco_cache.invalidate_formsemestre()
|
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
|
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||||
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||||
|
@ -593,46 +640,43 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||||
evals_coefs_etuds * evals_notes_20, axis=1
|
evals_coefs_etuds * evals_notes_20, axis=1
|
||||||
) / np.sum(evals_coefs_etuds, axis=1)
|
) / np.sum(evals_coefs_etuds, axis=1)
|
||||||
|
|
||||||
# Application des évaluations bonus:
|
evals_session2 = self.get_evaluations_session2(modimpl)
|
||||||
etuds_moy_module = self.apply_bonus(
|
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||||
etuds_moy_module,
|
if evals_session2:
|
||||||
modimpl,
|
|
||||||
evals_notes_20,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Session2 : quand elle existe, remplace la note de module
|
# Session2 : quand elle existe, remplace la note de module
|
||||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
# Calcule la moyenne des évaluations de session2
|
||||||
if eval_session2:
|
etuds_moy_module_s2 = self._compute_moy_special(
|
||||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
|
||||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
)
|
||||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
|
||||||
etuds_moy_module = np.where(
|
etuds_moy_module = np.where(
|
||||||
etuds_use_session2,
|
etuds_use_session2,
|
||||||
notes_session2 / (eval_session2.note_max / 20.0),
|
etuds_moy_module_s2,
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
)
|
)
|
||||||
self.etuds_use_session2 = pd.Series(
|
self.etuds_use_session2 = pd.Series(
|
||||||
etuds_use_session2, index=self.evals_notes.index
|
etuds_use_session2, index=self.evals_notes.index
|
||||||
)
|
)
|
||||||
else:
|
elif evals_rat:
|
||||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
# Calcule la moyenne des évaluations de rattrapage
|
||||||
if eval_rat:
|
etuds_moy_module_rat = self._compute_moy_special(
|
||||||
notes_rat = self.evals_notes[eval_rat.id].values
|
modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
|
||||||
# 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 = etuds_moy_module_rat > etuds_moy_module
|
||||||
etuds_use_rattrapage = notes_rat > etuds_moy_module
|
|
||||||
etuds_moy_module = np.where(
|
etuds_moy_module = np.where(
|
||||||
etuds_use_rattrapage, notes_rat, etuds_moy_module
|
etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||||
)
|
)
|
||||||
self.etuds_use_rattrapage = pd.Series(
|
self.etuds_use_rattrapage = pd.Series(
|
||||||
etuds_use_rattrapage, index=self.evals_notes.index
|
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,
|
||||||
|
)
|
||||||
self.etuds_moy_module = pd.Series(
|
self.etuds_moy_module = pd.Series(
|
||||||
etuds_moy_module,
|
etuds_moy_module,
|
||||||
index=self.evals_notes.index,
|
index=self.evals_notes.index,
|
||||||
|
@ -640,6 +684,28 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||||
|
|
||||||
return self.etuds_moy_module
|
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(
|
def apply_bonus(
|
||||||
self,
|
self,
|
||||||
etuds_moy_module: np.ndarray,
|
etuds_moy_module: np.ndarray,
|
||||||
|
|
|
@ -183,7 +183,9 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
||||||
return modimpls_notes.swapaxes(0, 1)
|
return modimpls_notes.swapaxes(0, 1)
|
||||||
|
|
||||||
|
|
||||||
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
def notes_sem_load_cube(
|
||||||
|
formsemestre: FormSemestre, modimpl_coefs_df: pd.DataFrame
|
||||||
|
) -> tuple:
|
||||||
"""Construit le "cube" (tenseur) des notes du semestre.
|
"""Construit le "cube" (tenseur) des notes du semestre.
|
||||||
Charge toutes les notes (sql), calcule les moyennes des modules
|
Charge toutes les notes (sql), calcule les moyennes des modules
|
||||||
et assemble le cube.
|
et assemble le cube.
|
||||||
|
@ -207,8 +209,8 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||||
for modimpl in formsemestre.modimpls_sorted:
|
for modimpl in formsemestre.modimpls_sorted:
|
||||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
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)
|
etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
|
||||||
modimpls_results[modimpl.id] = mod_results
|
modimpls_results[modimpl.id] = mod_results
|
||||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||||
modimpls_notes.append(etuds_moy_module)
|
modimpls_notes.append(etuds_moy_module)
|
||||||
|
|
|
@ -59,16 +59,17 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||||
|
|
||||||
def compute(self):
|
def compute(self):
|
||||||
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
||||||
|
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||||
|
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||||
|
)
|
||||||
(
|
(
|
||||||
self.sem_cube,
|
self.sem_cube,
|
||||||
self.modimpls_evals_poids,
|
self.modimpls_evals_poids,
|
||||||
self.modimpls_results,
|
self.modimpls_results,
|
||||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
) = moy_ue.notes_sem_load_cube(self.formsemestre, self.modimpl_coefs_df)
|
||||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||||
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
||||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
|
||||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
|
||||||
)
|
|
||||||
# l'idx de la colonne du mod modimpl.id est
|
# l'idx de la colonne du mod modimpl.id est
|
||||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||||
|
|
|
@ -242,7 +242,8 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||||
)
|
)
|
||||||
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||||
</div>
|
</div>
|
||||||
"""
|
""",
|
||||||
|
safe=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -518,7 +518,8 @@ class ResultatsSemestre(ResultatsCache):
|
||||||
Corrigez ou faite corriger le programme
|
Corrigez ou faite corriger le programme
|
||||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||||
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
||||||
"""
|
""",
|
||||||
|
safe=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Coefs de l'UE capitalisée en formation classique:
|
# Coefs de l'UE capitalisée en formation classique:
|
||||||
|
|
12
app/email.py
12
app/email.py
|
@ -9,9 +9,9 @@ import datetime
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from flask import current_app, g
|
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.departements import Departement
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
|
@ -20,7 +20,15 @@ from app.scodoc import sco_preferences
|
||||||
def send_async_email(app, msg):
|
def send_async_email(app, msg):
|
||||||
"Send an email, async"
|
"Send an email, async"
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
try:
|
||||||
mail.send(msg)
|
mail.send(msg)
|
||||||
|
except BadHeaderError:
|
||||||
|
log(
|
||||||
|
f"""send_async_email: BadHeaderError
|
||||||
|
msg={msg}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def send_email(
|
def send_email(
|
||||||
|
|
|
@ -338,9 +338,11 @@ def add_entreprise():
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
entreprise = Entreprise(
|
entreprise = Entreprise(
|
||||||
nom=form.nom_entreprise.data.strip(),
|
nom=form.nom_entreprise.data.strip(),
|
||||||
siret=form.siret.data.strip()
|
siret=(
|
||||||
|
form.siret.data.strip()
|
||||||
if 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
|
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,
|
siret_provisoire=False if form.siret.data.strip() else True,
|
||||||
association=form.association.data,
|
association=form.association.data,
|
||||||
adresse=form.adresse.data.strip(),
|
adresse=form.adresse.data.strip(),
|
||||||
|
@ -352,7 +354,7 @@ def add_entreprise():
|
||||||
db.session.add(entreprise)
|
db.session.add(entreprise)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.refresh(entreprise)
|
db.session.refresh(entreprise)
|
||||||
except:
|
except Exception:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
flash("Une erreur est survenue veuillez réessayer.")
|
flash("Une erreur est survenue veuillez réessayer.")
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -804,9 +806,9 @@ def add_offre(entreprise_id):
|
||||||
missions=form.missions.data.strip(),
|
missions=form.missions.data.strip(),
|
||||||
duree=form.duree.data.strip(),
|
duree=form.duree.data.strip(),
|
||||||
expiration_date=form.expiration_date.data,
|
expiration_date=form.expiration_date.data,
|
||||||
correspondant_id=form.correspondant.data
|
correspondant_id=(
|
||||||
if form.correspondant.data != ""
|
form.correspondant.data if form.correspondant.data != "" else None
|
||||||
else None,
|
),
|
||||||
)
|
)
|
||||||
db.session.add(offre)
|
db.session.add(offre)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -1328,9 +1330,11 @@ def add_contact(entreprise_id):
|
||||||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||||
form = ContactCreationForm(
|
form = ContactCreationForm(
|
||||||
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
|
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
|
||||||
utilisateur=f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
utilisateur=(
|
||||||
|
f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||||
if current_user.nom and current_user.prenom
|
if current_user.nom and current_user.prenom
|
||||||
else "",
|
else ""
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if request.method == "POST" and form.cancel.data:
|
if request.method == "POST" and form.cancel.data:
|
||||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise_id))
|
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_debut=form.date_debut.data,
|
||||||
date_fin=form.date_fin.data,
|
date_fin=form.date_fin.data,
|
||||||
formation_text=formation.formsemestre.titre if formation else None,
|
formation_text=formation.formsemestre.titre if formation else None,
|
||||||
formation_scodoc=formation.formsemestre.formsemestre_id
|
formation_scodoc=(
|
||||||
if formation
|
formation.formsemestre.formsemestre_id if formation else None
|
||||||
else None,
|
),
|
||||||
notes=form.notes.data.strip(),
|
notes=form.notes.data.strip(),
|
||||||
)
|
)
|
||||||
db.session.add(stage_apprentissage)
|
db.session.add(stage_apprentissage)
|
||||||
|
@ -1802,7 +1806,7 @@ def import_donnees():
|
||||||
db.session.add(entreprise)
|
db.session.add(entreprise)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
db.session.refresh(entreprise)
|
db.session.refresh(entreprise)
|
||||||
except:
|
except Exception:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
flash("Une erreur est survenue veuillez réessayer.")
|
flash("Une erreur est survenue veuillez réessayer.")
|
||||||
return render_template(
|
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:
|
elif self.external_data is not None and "module" in self.external_data:
|
||||||
return (
|
return (
|
||||||
"Tout module"
|
"Autre module (pas dans la liste)"
|
||||||
if self.external_data["module"] == "Autre"
|
if self.external_data["module"] == "Autre"
|
||||||
else self.external_data["module"]
|
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:
|
def get_saisie(self) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -274,6 +274,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||||
return "type_departement mismatch"
|
return "type_departement mismatch"
|
||||||
# Table d'équivalences entre refs:
|
# Table d'équivalences entre refs:
|
||||||
equiv = self._load_config_equivalences()
|
equiv = self._load_config_equivalences()
|
||||||
|
# Même specialité (ou alias) ?
|
||||||
|
if self.specialite != other.specialite and other.specialite not in equiv.get(
|
||||||
|
"alias", []
|
||||||
|
):
|
||||||
|
return "specialite mismatch"
|
||||||
# mêmes parcours ?
|
# mêmes parcours ?
|
||||||
eq_parcours = equiv.get("parcours", {})
|
eq_parcours = equiv.get("parcours", {})
|
||||||
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
|
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
|
||||||
|
@ -317,6 +322,9 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||||
def _load_config_equivalences(self) -> dict:
|
def _load_config_equivalences(self) -> dict:
|
||||||
"""Load config file ressources/referentiels/equivalences.yaml
|
"""Load config file ressources/referentiels/equivalences.yaml
|
||||||
used to define equivalences between distinct referentiels
|
used to define equivalences between distinct referentiels
|
||||||
|
return a dict, with optional keys:
|
||||||
|
alias: list of equivalent names for speciality (eg SD == STID)
|
||||||
|
parcours: dict with equivalent parcours acronyms
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
|
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
|
||||||
|
|
|
@ -199,6 +199,11 @@ class Identite(models.ScoDocModel):
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_etud(cls, etudid: int) -> "Identite":
|
def get_etud(cls, etudid: int) -> "Identite":
|
||||||
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||||
|
if not isinstance(etudid, int):
|
||||||
|
try:
|
||||||
|
etudid = int(etudid)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
abort(404, "etudid invalide")
|
||||||
if g.scodoc_dept:
|
if g.scodoc_dept:
|
||||||
return cls.query.filter_by(
|
return cls.query.filter_by(
|
||||||
id=etudid, dept_id=g.scodoc_dept_id
|
id=etudid, dept_id=g.scodoc_dept_id
|
||||||
|
@ -299,9 +304,10 @@ class Identite(models.ScoDocModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nomprenom(self, reverse=False) -> str:
|
def nomprenom(self, reverse=False) -> str:
|
||||||
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
"""DEPRECATED
|
||||||
|
Civilité/prénom/nom pour affichages: "M. Pierre Dupont"
|
||||||
Si reverse, "Dupont Pierre", sans civilité.
|
Si reverse, "Dupont Pierre", sans civilité.
|
||||||
Prend l'identité courant et non celle de l'état civile si elles diffèrent.
|
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||||
"""
|
"""
|
||||||
nom = self.nom_usuel or self.nom
|
nom = self.nom_usuel or self.nom
|
||||||
prenom = self.prenom_str
|
prenom = self.prenom_str
|
||||||
|
@ -309,6 +315,12 @@ class Identite(models.ScoDocModel):
|
||||||
return f"{nom} {prenom}".strip()
|
return f"{nom} {prenom}".strip()
|
||||||
return f"{self.civilite_str} {prenom} {nom}".strip()
|
return f"{self.civilite_str} {prenom} {nom}".strip()
|
||||||
|
|
||||||
|
def nom_prenom(self) -> str:
|
||||||
|
"""Civilite NOM Prénom
|
||||||
|
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||||
|
"""
|
||||||
|
return f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()} {self.prenom_str}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prenom_str(self):
|
def prenom_str(self):
|
||||||
"""Prénom à afficher. Par exemple: "Jean-Christophe" """
|
"""Prénom à afficher. Par exemple: "Jean-Christophe" """
|
||||||
|
@ -551,7 +563,7 @@ class Identite(models.ScoDocModel):
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
def inscription_courante(self):
|
def inscription_courante(self) -> "FormSemestreInscription | None":
|
||||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -71,6 +71,15 @@ class Evaluation(models.ScoDocModel):
|
||||||
EVALUATION_BONUS,
|
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):
|
def __repr__(self):
|
||||||
return f"""<Evaluation {self.id} {
|
return f"""<Evaluation {self.id} {
|
||||||
self.date_debut.isoformat() if self.date_debut else ''} "{
|
self.date_debut.isoformat() if self.date_debut else ''} "{
|
||||||
|
@ -417,12 +426,13 @@ class Evaluation(models.ScoDocModel):
|
||||||
return modified
|
return modified
|
||||||
|
|
||||||
def set_ue_poids(self, ue, poids: float) -> None:
|
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})
|
self.update_ue_poids_dict({ue.id: poids})
|
||||||
|
|
||||||
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||||
"""set poids vers les UE (remplace existants)
|
"""set poids vers les UE (remplace existants)
|
||||||
ue_poids_dict = { ue_id : poids }
|
ue_poids_dict = { ue_id : poids }
|
||||||
|
Commit session.
|
||||||
"""
|
"""
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
|
|
||||||
|
@ -432,9 +442,12 @@ class Evaluation(models.ScoDocModel):
|
||||||
if ue is None:
|
if ue is None:
|
||||||
raise ScoValueError("poids vers une UE inexistante")
|
raise ScoValueError("poids vers une UE inexistante")
|
||||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||||
L.append(ue_poids)
|
|
||||||
db.session.add(ue_poids)
|
db.session.add(ue_poids)
|
||||||
|
L.append(ue_poids)
|
||||||
|
|
||||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||||
|
|
||||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
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_login import current_user
|
||||||
from flask_sqlalchemy.query import Query
|
from flask_sqlalchemy.query import Query
|
||||||
|
|
||||||
|
import app
|
||||||
from app import db
|
from app import db
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.comp import df_cache
|
from app.comp import df_cache
|
||||||
|
@ -78,7 +79,9 @@ class ModuleImpl(ScoDocModel):
|
||||||
] or self.module.get_edt_ids()
|
] or self.module.get_edt_ids()
|
||||||
|
|
||||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
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)
|
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||||
if evaluations_poids is None:
|
if evaluations_poids is None:
|
||||||
from app.comp import moy_mod
|
from app.comp import moy_mod
|
||||||
|
@ -108,20 +111,37 @@ class ModuleImpl(ScoDocModel):
|
||||||
"""Invalide poids cachés"""
|
"""Invalide poids cachés"""
|
||||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||||
|
|
||||||
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
def check_apc_conformity(
|
||||||
"""true si les poids des évaluations du module permettent de satisfaire
|
self, res: "ResultatsSemestreBUT", evaluation_type=Evaluation.EVALUATION_NORMALE
|
||||||
les coefficients du PN.
|
) -> 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 (
|
if not self.module.formation.get_cursus().APC_SAE or (
|
||||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
self.module.module_type
|
||||||
and self.module.module_type != scu.ModuleType.SAE
|
not in {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
|
||||||
):
|
):
|
||||||
return True # Non BUT, toujours conforme
|
return True # Non BUT, toujours conforme
|
||||||
from app.comp import moy_mod
|
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(
|
return moy_mod.moduleimpl_is_conforme(
|
||||||
self,
|
self,
|
||||||
self.get_evaluations_poids(),
|
selected_evaluations_poids,
|
||||||
res.modimpl_coefs_df,
|
res.modimpl_coefs_df,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -340,6 +340,21 @@ class Module(models.ScoDocModel):
|
||||||
# Liste seulement les coefs définis:
|
# Liste seulement les coefs définis:
|
||||||
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
||||||
|
|
||||||
|
def get_ue_coefs_descr(self) -> str:
|
||||||
|
"""Description des coefficients vers les UEs (APC)"""
|
||||||
|
coefs_descr = ", ".join(
|
||||||
|
[
|
||||||
|
f"{ue.acronyme}: {co}"
|
||||||
|
for ue, co in self.ue_coefs_list()
|
||||||
|
if isinstance(co, float) and co > 0
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if coefs_descr:
|
||||||
|
descr = "Coefs: " + coefs_descr
|
||||||
|
else:
|
||||||
|
descr = "(pas de coefficients) "
|
||||||
|
return descr
|
||||||
|
|
||||||
def get_codes_apogee(self) -> set[str]:
|
def get_codes_apogee(self) -> set[str]:
|
||||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
||||||
if self.code_apogee:
|
if self.code_apogee:
|
||||||
|
|
|
@ -432,6 +432,7 @@ class UniteEns(models.ScoDocModel):
|
||||||
self.niveau_competence, parcours
|
self.niveau_competence, parcours
|
||||||
)
|
)
|
||||||
if not ok:
|
if not ok:
|
||||||
|
self.formation.invalidate_cached_sems()
|
||||||
self.niveau_competence = prev_niveau # restore
|
self.niveau_competence = prev_niveau # restore
|
||||||
return False, error_message
|
return False, error_message
|
||||||
|
|
||||||
|
|
|
@ -176,6 +176,7 @@ class GenTable:
|
||||||
self.xml_link = xml_link
|
self.xml_link = xml_link
|
||||||
# HTML parameters:
|
# HTML parameters:
|
||||||
if not table_id: # random id
|
if not table_id: # random id
|
||||||
|
log("Warning: GenTable() called without table_id")
|
||||||
self.table_id = "gt_" + str(random.randint(0, 1000000))
|
self.table_id = "gt_" + str(random.randint(0, 1000000))
|
||||||
else:
|
else:
|
||||||
self.table_id = table_id
|
self.table_id = table_id
|
||||||
|
|
|
@ -25,8 +25,7 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""HTML Header/Footer for ScoDoc pages
|
"""HTML Header/Footer for ScoDoc pages"""
|
||||||
"""
|
|
||||||
|
|
||||||
import html
|
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/menu.js"></script>
|
||||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onload=function(){{enableTooltips("gtrcontent")}};
|
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></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/menu.js"></script>
|
||||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||||
<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_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
|
||||||
const SCO_TIMEZONE="{scu.TIME_ZONE}";
|
const SCO_TIMEZONE="{scu.TIME_ZONE}";
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
"""
|
"""
|
||||||
Génération de la "sidebar" (marge gauche des pages HTML)
|
Génération de la "sidebar" (marge gauche des pages HTML)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import render_template, url_for
|
from flask import render_template, url_for
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
@ -151,7 +152,7 @@ def sidebar(etudid: int = None):
|
||||||
H = [
|
H = [
|
||||||
f"""
|
f"""
|
||||||
<!-- sidebar py -->
|
<!-- sidebar py -->
|
||||||
<div class="sidebar">
|
<div class="sidebar" id="sidebar">
|
||||||
{ sidebar_common() }
|
{ sidebar_common() }
|
||||||
<div class="box-chercheetud">Chercher étudiant:<br>
|
<div class="box-chercheetud">Chercher étudiant:<br>
|
||||||
<form method="get" id="form-chercheetud"
|
<form method="get" id="form-chercheetud"
|
||||||
|
@ -193,7 +194,7 @@ def sidebar(etudid: int = None):
|
||||||
formsemestre.date_debut.strftime(scu.DATE_FMT)
|
formsemestre.date_debut.strftime(scu.DATE_FMT)
|
||||||
} au {
|
} au {
|
||||||
formsemestre.date_fin.strftime(scu.DATE_FMT)
|
formsemestre.date_fin.strftime(scu.DATE_FMT)
|
||||||
}">({
|
}" data-tooltip>({
|
||||||
sco_preferences.get_preference("assi_metrique", None)})
|
sco_preferences.get_preference("assi_metrique", None)})
|
||||||
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
|
<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',
|
<li><a href="{ url_for('assiduites.calendrier_assi_etud',
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||||
}">Calendrier</a></li>
|
}">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',
|
<li><a href="{ url_for('assiduites.bilan_etud',
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
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>
|
</ul>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -157,5 +157,6 @@ def table_billets(
|
||||||
rows=rows,
|
rows=rows,
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
|
table_id="table_billets",
|
||||||
)
|
)
|
||||||
return tab
|
return tab
|
||||||
|
|
|
@ -288,6 +288,7 @@ def apo_table_compare_etud_results(A, B):
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
html_with_td_classes=True,
|
html_with_td_classes=True,
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="apo_table_compare_etud_results",
|
||||||
)
|
)
|
||||||
return T
|
return T
|
||||||
|
|
||||||
|
|
|
@ -917,6 +917,7 @@ class ApoData:
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
titles=dict(zip(columns_ids, columns_ids)),
|
titles=dict(zip(columns_ids, columns_ids)),
|
||||||
rows=rows,
|
rows=rows,
|
||||||
|
table_id="build_cr_table",
|
||||||
xls_sheet_name="Decisions ScoDoc",
|
xls_sheet_name="Decisions ScoDoc",
|
||||||
)
|
)
|
||||||
return T
|
return T
|
||||||
|
@ -969,6 +970,7 @@ class ApoData:
|
||||||
"rcue": "RCUE",
|
"rcue": "RCUE",
|
||||||
},
|
},
|
||||||
rows=rows,
|
rows=rows,
|
||||||
|
table_id="adsup_table",
|
||||||
xls_sheet_name="ADSUPs",
|
xls_sheet_name="ADSUPs",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1054,6 +1056,7 @@ def nar_etuds_table(apo_data, nar_etuds):
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
titles=dict(zip(columns_ids, columns_ids)),
|
titles=dict(zip(columns_ids, columns_ids)),
|
||||||
rows=rows,
|
rows=rows,
|
||||||
|
table_id="nar_etuds_table",
|
||||||
xls_sheet_name="NAR ScoDoc",
|
xls_sheet_name="NAR ScoDoc",
|
||||||
)
|
)
|
||||||
return table.excel()
|
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
|
|
@ -226,6 +226,7 @@ class BulletinGenerator:
|
||||||
server_name=self.server_name,
|
server_name=self.server_name,
|
||||||
filigranne=self.filigranne,
|
filigranne=self.filigranne,
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
with_page_numbers=self.multi_pages,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -106,6 +106,7 @@ def assemble_bulletins_pdf(
|
||||||
pagesbookmarks=pagesbookmarks,
|
pagesbookmarks=pagesbookmarks,
|
||||||
filigranne=filigranne,
|
filigranne=filigranne,
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
with_page_numbers=False, # on ne veut pas de no de pages sur les bulletins imprimés en masse
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
document.multiBuild(story)
|
document.multiBuild(story)
|
||||||
|
@ -122,7 +123,8 @@ def replacement_function(match) -> str:
|
||||||
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
'balise "%s": logo "%s" introuvable'
|
'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="notes_bulletin",
|
||||||
html_class_ignore_default=True,
|
html_class_ignore_default=True,
|
||||||
html_with_td_classes=True,
|
html_with_td_classes=True,
|
||||||
|
table_id="std_bul_table",
|
||||||
)
|
)
|
||||||
|
|
||||||
return T.gen(fmt=fmt)
|
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é).
|
"""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.
|
Si pdfonly, n'expire que les bulletins pdf cachés.
|
||||||
"""
|
"""
|
||||||
|
from app.comp import df_cache
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
from app.scodoc import sco_cursus
|
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
|
and fid in g.formsemestre_results_cache
|
||||||
):
|
):
|
||||||
del g.formsemestre_results_cache[fid]
|
del g.formsemestre_results_cache[fid]
|
||||||
|
df_cache.EvaluationsPoidsCache.invalidate_sem(formsemestre_id)
|
||||||
else:
|
else:
|
||||||
# optimization when we invalidate all evaluations:
|
# optimization when we invalidate all evaluations:
|
||||||
EvaluationCache.invalidate_all_sems()
|
EvaluationCache.invalidate_all_sems()
|
||||||
|
df_cache.EvaluationsPoidsCache.invalidate_all()
|
||||||
if hasattr(g, "formsemestre_results_cache"):
|
if hasattr(g, "formsemestre_results_cache"):
|
||||||
del g.formsemestre_results_cache
|
del g.formsemestre_results_cache
|
||||||
|
|
||||||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||||
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
||||||
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
||||||
|
|
|
@ -230,41 +230,41 @@ def next_iso_day(date):
|
||||||
|
|
||||||
def YearTable(
|
def YearTable(
|
||||||
year,
|
year,
|
||||||
events=[],
|
events_by_day: dict[str, list[dict]],
|
||||||
firstmonth=9,
|
firstmonth=9,
|
||||||
lastmonth=7,
|
lastmonth=7,
|
||||||
halfday=0,
|
|
||||||
dayattributes="",
|
dayattributes="",
|
||||||
pad_width=8,
|
|
||||||
):
|
):
|
||||||
|
# Code simplifié en 2024: utilisé seulement pour calendrier évaluations
|
||||||
"""Generate a calendar table
|
"""Generate a calendar table
|
||||||
events = list of tuples (date, text, color, href [,halfday])
|
events = list of tuples (date, text, color, href [,halfday])
|
||||||
where date is a string in ISO format (yyyy-mm-dd)
|
where date is a string in ISO format (yyyy-mm-dd)
|
||||||
halfday is boolean (true: morning, false: afternoon)
|
halfday is boolean (true: morning, false: afternoon)
|
||||||
text = text to put in calendar (must be short, 1-5 cars) (optional)
|
text = text to put in calendar (must be short, 1-5 cars) (optional)
|
||||||
if halfday, generate 2 cells per day (morning, afternoon)
|
|
||||||
"""
|
"""
|
||||||
T = [
|
T = [
|
||||||
'<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">'
|
"""<table id="maincalendar" class="maincalendar"
|
||||||
|
border="3" cellpadding="1" cellspacing="1" frame="box">"""
|
||||||
]
|
]
|
||||||
T.append("<tr>")
|
T.append("<tr>")
|
||||||
month = firstmonth
|
month = firstmonth
|
||||||
while 1:
|
while True:
|
||||||
T.append('<td valign="top">')
|
T.append('<td valign="top">')
|
||||||
T.append(MonthTableHead(month))
|
T.append(_month_table_head(month))
|
||||||
T.append(
|
T.append(
|
||||||
MonthTableBody(
|
_month_table_body(
|
||||||
month,
|
month,
|
||||||
year,
|
year,
|
||||||
events,
|
events_by_day,
|
||||||
halfday,
|
|
||||||
dayattributes,
|
dayattributes,
|
||||||
is_work_saturday(),
|
is_work_saturday(),
|
||||||
pad_width=pad_width,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
T.append(MonthTableTail())
|
T.append(
|
||||||
T.append("</td>")
|
"""
|
||||||
|
</table>
|
||||||
|
</td>"""
|
||||||
|
)
|
||||||
if month == lastmonth:
|
if month == lastmonth:
|
||||||
break
|
break
|
||||||
month = month + 1
|
month = month + 1
|
||||||
|
@ -322,29 +322,32 @@ WEEKDAYCOLOR = GRAY1
|
||||||
WEEKENDCOLOR = GREEN3
|
WEEKENDCOLOR = GREEN3
|
||||||
|
|
||||||
|
|
||||||
def MonthTableHead(month):
|
def _month_table_head(month):
|
||||||
color = WHITE
|
color = WHITE
|
||||||
return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
|
return f"""<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
|
||||||
<tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % (
|
<tr bgcolor="{color}">
|
||||||
color,
|
<td class="calcol" colspan="2" align="center">{MONTHNAMES_ABREV[month - 1]}</td>
|
||||||
MONTHNAMES_ABREV[month - 1],
|
</tr>\n"""
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def MonthTableTail():
|
def _month_table_body(
|
||||||
return "</table>\n"
|
month,
|
||||||
|
year,
|
||||||
|
events_by_day: dict[str, list[dict]],
|
||||||
def MonthTableBody(
|
trattributes="",
|
||||||
month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8
|
work_saturday=False,
|
||||||
):
|
) -> str:
|
||||||
|
"""
|
||||||
|
events : [event]
|
||||||
|
event = [ yyyy-mm-dd, legend, href, color, descr ] XXX
|
||||||
|
"""
|
||||||
firstday, nbdays = calendar.monthrange(year, month)
|
firstday, nbdays = calendar.monthrange(year, month)
|
||||||
localtime = time.localtime()
|
localtime = time.localtime()
|
||||||
current_weeknum = time.strftime("%U", localtime)
|
current_weeknum = time.strftime("%U", localtime)
|
||||||
current_year = localtime[0]
|
current_year = localtime[0]
|
||||||
T = []
|
rows = []
|
||||||
# cherche date du lundi de la 1ere semaine de ce mois
|
# cherche date du lundi de la 1ere semaine de ce mois
|
||||||
monday = ddmmyyyy("1/%d/%d" % (month, year))
|
monday = ddmmyyyy(f"1/{month}/{year}")
|
||||||
while monday.weekday != 0:
|
while monday.weekday != 0:
|
||||||
monday = monday.prev()
|
monday = monday.prev()
|
||||||
|
|
||||||
|
@ -353,7 +356,6 @@ def MonthTableBody(
|
||||||
else:
|
else:
|
||||||
weekend = ("S", "D")
|
weekend = ("S", "D")
|
||||||
|
|
||||||
if not halfday:
|
|
||||||
for d in range(1, nbdays + 1):
|
for d in range(1, nbdays + 1):
|
||||||
weeknum = time.strftime(
|
weeknum = time.strftime(
|
||||||
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
|
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
|
||||||
|
@ -367,144 +369,38 @@ def MonthTableBody(
|
||||||
bgcolor = WEEKDAYCOLOR
|
bgcolor = WEEKDAYCOLOR
|
||||||
weekclass = "wk" + str(monday).replace("/", "_")
|
weekclass = "wk" + str(monday).replace("/", "_")
|
||||||
attrs = trattributes
|
attrs = trattributes
|
||||||
|
# events this day ?
|
||||||
|
events = events_by_day.get(f"{year}-{month:02}-{d:02}", [])
|
||||||
color = None
|
color = None
|
||||||
legend = ""
|
ev_txts = []
|
||||||
href = ""
|
|
||||||
descr = ""
|
|
||||||
# event this day ?
|
|
||||||
# each event is a tuple (date, text, color, href)
|
|
||||||
# where date is a string in ISO format (yyyy-mm-dd)
|
|
||||||
for ev in events:
|
for ev in events:
|
||||||
ev_year = int(ev[0][:4])
|
color = ev.get("color")
|
||||||
ev_month = int(ev[0][5:7])
|
href = ev.get("href", "")
|
||||||
ev_day = int(ev[0][8:10])
|
description = ev.get("description", "")
|
||||||
if year == ev_year and month == ev_month and ev_day == d:
|
if href:
|
||||||
if ev[1]:
|
href = f'href="{href}"'
|
||||||
legend = ev[1]
|
if description:
|
||||||
if ev[2]:
|
description = f"""title="{html.escape(description, quote=True)}" """
|
||||||
color = ev[2]
|
if href or description:
|
||||||
if ev[3]:
|
ev_txts.append(f"""<a {href} {description}>{ev.get("title", "")}</a>""")
|
||||||
href = ev[3]
|
else:
|
||||||
if len(ev) > 4 and ev[4]:
|
ev_txts.append(ev.get("title", " "))
|
||||||
descr = ev[4]
|
|
||||||
#
|
#
|
||||||
cc = []
|
cc = []
|
||||||
if color is not None:
|
if color is not None:
|
||||||
cc.append('<td bgcolor="%s" class="calcell">' % color)
|
cc.append(f'<td bgcolor="{color}" class="calcell">')
|
||||||
else:
|
else:
|
||||||
cc.append('<td class="calcell">')
|
cc.append('<td class="calcell">')
|
||||||
|
|
||||||
if href:
|
cc.append(f"{', '.join(ev_txts)}</td>")
|
||||||
href = 'href="%s"' % href
|
cells = "".join(cc)
|
||||||
if descr:
|
|
||||||
descr = 'title="%s"' % html.escape(descr, quote=True)
|
|
||||||
if href or descr:
|
|
||||||
cc.append("<a %s %s>" % (href, descr))
|
|
||||||
|
|
||||||
if legend or d == 1:
|
|
||||||
if pad_width is not None:
|
|
||||||
n = pad_width - len(legend) # pad to 8 cars
|
|
||||||
if n > 0:
|
|
||||||
legend = (
|
|
||||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
legend = " " # empty cell
|
|
||||||
cc.append(legend)
|
|
||||||
if href or descr:
|
|
||||||
cc.append("</a>")
|
|
||||||
cc.append("</td>")
|
|
||||||
cell = "".join(cc)
|
|
||||||
if day == "D":
|
if day == "D":
|
||||||
monday = monday.next_day(7)
|
monday = monday.next_day(7)
|
||||||
if (
|
if weeknum == current_weeknum and current_year == year and weekclass != "wkend":
|
||||||
weeknum == current_weeknum
|
|
||||||
and current_year == year
|
|
||||||
and weekclass != "wkend"
|
|
||||||
):
|
|
||||||
weekclass += " currentweek"
|
weekclass += " currentweek"
|
||||||
T.append(
|
rows.append(
|
||||||
'<tr bgcolor="%s" class="%s" %s><td class="calday">%d%s</td>%s</tr>'
|
f"""<tr bgcolor="{bgcolor}" class="{weekclass}" {attrs}>
|
||||||
% (bgcolor, weekclass, attrs, d, day, cell)
|
<td class="calday">{d}{day}</td>{cells}</tr>"""
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
# Calendar with 2 cells / day
|
|
||||||
for d in range(1, nbdays + 1):
|
|
||||||
weeknum = time.strftime(
|
|
||||||
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
|
|
||||||
)
|
|
||||||
day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
|
|
||||||
if day in weekend:
|
|
||||||
bgcolor = WEEKENDCOLOR
|
|
||||||
weekclass = "wkend"
|
|
||||||
attrs = ""
|
|
||||||
else:
|
|
||||||
bgcolor = WEEKDAYCOLOR
|
|
||||||
weekclass = "wk" + str(monday).replace("/", "_")
|
|
||||||
attrs = trattributes
|
|
||||||
if (
|
|
||||||
weeknum == current_weeknum
|
|
||||||
and current_year == year
|
|
||||||
and weekclass != "wkend"
|
|
||||||
):
|
|
||||||
weeknum += " currentweek"
|
|
||||||
|
|
||||||
if day == "D":
|
return "\n".join(rows)
|
||||||
monday = monday.next_day(7)
|
|
||||||
T.append(
|
|
||||||
'<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>'
|
|
||||||
% (bgcolor, weekclass, attrs, d, day)
|
|
||||||
)
|
|
||||||
cc = []
|
|
||||||
for morning in (True, False):
|
|
||||||
color = None
|
|
||||||
legend = ""
|
|
||||||
href = ""
|
|
||||||
descr = ""
|
|
||||||
for ev in events:
|
|
||||||
ev_year = int(ev[0][:4])
|
|
||||||
ev_month = int(ev[0][5:7])
|
|
||||||
ev_day = int(ev[0][8:10])
|
|
||||||
if ev[4] is not None:
|
|
||||||
ev_half = int(ev[4])
|
|
||||||
else:
|
|
||||||
ev_half = 0
|
|
||||||
if (
|
|
||||||
year == ev_year
|
|
||||||
and month == ev_month
|
|
||||||
and ev_day == d
|
|
||||||
and morning == ev_half
|
|
||||||
):
|
|
||||||
if ev[1]:
|
|
||||||
legend = ev[1]
|
|
||||||
if ev[2]:
|
|
||||||
color = ev[2]
|
|
||||||
if ev[3]:
|
|
||||||
href = ev[3]
|
|
||||||
if len(ev) > 5 and ev[5]:
|
|
||||||
descr = ev[5]
|
|
||||||
#
|
|
||||||
if color is not None:
|
|
||||||
cc.append('<td bgcolor="%s" class="calcell">' % (color))
|
|
||||||
else:
|
|
||||||
cc.append('<td class="calcell">')
|
|
||||||
if href:
|
|
||||||
href = 'href="%s"' % href
|
|
||||||
if descr:
|
|
||||||
descr = 'title="%s"' % html.escape(descr, quote=True)
|
|
||||||
if href or descr:
|
|
||||||
cc.append("<a %s %s>" % (href, descr))
|
|
||||||
if legend or d == 1:
|
|
||||||
n = 3 - len(legend) # pad to 3 cars
|
|
||||||
if n > 0:
|
|
||||||
legend = (
|
|
||||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
legend = " " # empty cell
|
|
||||||
cc.append(legend)
|
|
||||||
if href or descr:
|
|
||||||
cc.append("</a>")
|
|
||||||
cc.append("</td>\n")
|
|
||||||
T.append("".join(cc) + "</tr>")
|
|
||||||
return "\n".join(T)
|
|
||||||
|
|
|
@ -141,6 +141,7 @@ def formsemestre_table_estim_cost(
|
||||||
""",
|
""",
|
||||||
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||||
filename=f"EstimCout-S{formsemestre.semestre_id}",
|
filename=f"EstimCout-S{formsemestre.semestre_id}",
|
||||||
|
table_id="formsemestre_table_estim_cost",
|
||||||
)
|
)
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
|
@ -350,11 +350,13 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||||
l'étudiant (quelle que soit la formation), le plus ancien en tête"""
|
l'étudiant (quelle que soit la formation), le plus ancien en tête"""
|
||||||
return self.sems
|
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, ..."
|
"""Description brève du parcours: "S1, S2, ..."
|
||||||
Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
|
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_begin_date = self.sem["dateord"]
|
||||||
|
cur_formation_code = self.sem["formation_code"]
|
||||||
p = []
|
p = []
|
||||||
for s in self.sems:
|
for s in self.sems:
|
||||||
if s["ins"]["etat"] == scu.DEMISSION:
|
if s["ins"]["etat"] == scu.DEMISSION:
|
||||||
|
@ -363,12 +365,14 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||||
dem = ""
|
dem = ""
|
||||||
if filter_futur and s["dateord"] > cur_begin_date:
|
if filter_futur and s["dateord"] > cur_begin_date:
|
||||||
continue # skip semestres demarrant apres le courant
|
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:
|
if s["semestre_id"] < 0:
|
||||||
SA = "A" # force, cas des DUT annuels par exemple
|
session_abbrv = "A" # force, cas des DUT annuels par exemple
|
||||||
p.append("%s%d%s" % (SA, -s["semestre_id"], dem))
|
p.append("%s%d%s" % (session_abbrv, -s["semestre_id"], dem))
|
||||||
else:
|
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)
|
return ", ".join(p)
|
||||||
|
|
||||||
def get_parcours_decisions(self):
|
def get_parcours_decisions(self):
|
||||||
|
|
|
@ -222,6 +222,7 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
html_class="table_leftalign table_listegroupe",
|
html_class="table_leftalign table_listegroupe",
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="table_debouche_etudids",
|
||||||
)
|
)
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
|
@ -198,6 +198,18 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
||||||
if current_user.has_permission(Permission.EditApogee):
|
if current_user.has_permission(Permission.EditApogee):
|
||||||
html_class += " apo_editable"
|
html_class += " apo_editable"
|
||||||
tab = GenTable(
|
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={
|
titles={
|
||||||
"formsemestre_id": "id",
|
"formsemestre_id": "id",
|
||||||
"semestre_id_n": "S#",
|
"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.",
|
"elt_sem_apo": "Elt. sem. Apo.",
|
||||||
"formation": "Formation",
|
"formation": "Formation",
|
||||||
},
|
},
|
||||||
columns_ids=columns_ids,
|
|
||||||
rows=sems,
|
|
||||||
table_id="semlist",
|
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
|
return tab
|
||||||
|
|
|
@ -67,7 +67,7 @@ SCO_DUMP_LOCK = "/tmp/scodump.lock"
|
||||||
|
|
||||||
def sco_dump_and_send_db(
|
def sco_dump_and_send_db(
|
||||||
message: str = "", request_url: str = "", traceback_str_base64: str = ""
|
message: str = "", request_url: str = "", traceback_str_base64: str = ""
|
||||||
):
|
) -> requests.Response:
|
||||||
"""Dump base de données et l'envoie anonymisée pour debug"""
|
"""Dump base de données et l'envoie anonymisée pour debug"""
|
||||||
traceback_str = base64.urlsafe_b64decode(traceback_str_base64).decode(
|
traceback_str = base64.urlsafe_b64decode(traceback_str_base64).decode(
|
||||||
scu.SCO_ENCODING
|
scu.SCO_ENCODING
|
||||||
|
@ -97,7 +97,6 @@ def sco_dump_and_send_db(
|
||||||
|
|
||||||
# Send
|
# Send
|
||||||
r = _send_db(ano_db_name, message, request_url, traceback_str=traceback_str)
|
r = _send_db(ano_db_name, message, request_url, traceback_str=traceback_str)
|
||||||
code = r.status_code
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Drop anonymized database
|
# Drop anonymized database
|
||||||
|
@ -107,7 +106,7 @@ def sco_dump_and_send_db(
|
||||||
|
|
||||||
log("sco_dump_and_send_db: done.")
|
log("sco_dump_and_send_db: done.")
|
||||||
|
|
||||||
return code
|
return r
|
||||||
|
|
||||||
|
|
||||||
def _duplicate_db(db_name, ano_db_name):
|
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")
|
log("_drop_ano_db: no temp db, nothing to drop")
|
||||||
return
|
return
|
||||||
cmd = ["dropdb", ano_db_name]
|
cmd = ["dropdb", ano_db_name]
|
||||||
log("sco_dump_and_send_db: {}".format(cmd))
|
log(f"sco_dump_and_send_db: {cmd}")
|
||||||
try:
|
try:
|
||||||
_ = subprocess.check_output(cmd)
|
_ = subprocess.check_output(cmd)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as exc:
|
||||||
log("sco_dump_and_send_db: exception dropdb {}".format(e))
|
log(f"sco_dump_and_send_db: exception dropdb {exc}")
|
||||||
raise ScoValueError(
|
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,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formation_id=formation.id,
|
formation_id=formation.id,
|
||||||
),
|
),
|
||||||
|
safe=True,
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
Lecture et conversion des ics.
|
Lecture et conversion des ics.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
@ -229,7 +230,7 @@ def translate_calendar(
|
||||||
heure_deb=event["heure_deb"],
|
heure_deb=event["heure_deb"],
|
||||||
heure_fin=event["heure_fin"],
|
heure_fin=event["heure_fin"],
|
||||||
moduleimpl_id=modimpl.id,
|
moduleimpl_id=modimpl.id,
|
||||||
jour=event["jour"],
|
day=event["jour"],
|
||||||
)
|
)
|
||||||
if modimpl and group
|
if modimpl and group
|
||||||
else None
|
else None
|
||||||
|
|
|
@ -490,6 +490,7 @@ def table_apo_csv_list(semset):
|
||||||
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
||||||
# caption='Maquettes enregistrées',
|
# caption='Maquettes enregistrées',
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="apo_csv_list",
|
||||||
)
|
)
|
||||||
|
|
||||||
return tab
|
return tab
|
||||||
|
@ -582,6 +583,7 @@ def _view_etuds_page(
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
filename="students_apo",
|
filename="students_apo",
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="view_etuds_page",
|
||||||
)
|
)
|
||||||
if fmt != "html":
|
if fmt != "html":
|
||||||
return tab.make_page(fmt=fmt)
|
return tab.make_page(fmt=fmt)
|
||||||
|
@ -798,6 +800,7 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
|
||||||
filename="students_" + etape_apo,
|
filename="students_" + etape_apo,
|
||||||
caption="Étudiants Apogée en " + etape_apo,
|
caption="Étudiants Apogée en " + etape_apo,
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="view_apo_csv",
|
||||||
)
|
)
|
||||||
|
|
||||||
if fmt != "html":
|
if fmt != "html":
|
||||||
|
|
|
@ -666,7 +666,9 @@ class EtapeBilan:
|
||||||
col_ids,
|
col_ids,
|
||||||
self.titres,
|
self.titres,
|
||||||
html_class="repartition",
|
html_class="repartition",
|
||||||
|
html_sortable=True,
|
||||||
html_with_td_classes=True,
|
html_with_td_classes=True,
|
||||||
|
table_id="apo-repartition",
|
||||||
).gen(fmt="html")
|
).gen(fmt="html")
|
||||||
)
|
)
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
@ -762,9 +764,9 @@ class EtapeBilan:
|
||||||
rows,
|
rows,
|
||||||
col_ids,
|
col_ids,
|
||||||
titles,
|
titles,
|
||||||
table_id="detail",
|
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
|
table_id="apo-detail",
|
||||||
).gen(fmt="html")
|
).gen(fmt="html")
|
||||||
)
|
)
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
|
|
@ -367,6 +367,9 @@ def evaluation_create_form(
|
||||||
+ "\n".join(H)
|
+ "\n".join(H)
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ tf[1]
|
+ tf[1]
|
||||||
|
+ render_template(
|
||||||
|
"scodoc/forms/evaluation_edit.j2",
|
||||||
|
)
|
||||||
+ render_template(
|
+ render_template(
|
||||||
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
|
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
|
||||||
)
|
)
|
||||||
|
|
|
@ -70,8 +70,8 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||||
Colonnes:
|
Colonnes:
|
||||||
- code (UE ou module),
|
- code (UE ou module),
|
||||||
- titre
|
- titre
|
||||||
|
- type évaluation
|
||||||
- complete
|
- complete
|
||||||
- publiée
|
|
||||||
- inscrits (non dem. ni def.)
|
- inscrits (non dem. ni def.)
|
||||||
- nb notes manquantes
|
- nb notes manquantes
|
||||||
- nb ATT
|
- nb ATT
|
||||||
|
@ -81,9 +81,10 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||||
rows = []
|
rows = []
|
||||||
titles = {
|
titles = {
|
||||||
"type": "",
|
"type": "",
|
||||||
"code": "Code",
|
"code": "Module",
|
||||||
"titre": "",
|
"titre": "",
|
||||||
"date": "Date",
|
"date": "Date",
|
||||||
|
"type_evaluation": "Type",
|
||||||
"complete": "Comptée",
|
"complete": "Comptée",
|
||||||
"inscrits": "Inscrits",
|
"inscrits": "Inscrits",
|
||||||
"manquantes": "Manquantes", # notes eval non entrées
|
"manquantes": "Manquantes", # notes eval non entrées
|
||||||
|
@ -114,7 +115,9 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
line_idx += 1
|
line_idx += 1
|
||||||
for evaluation_id in modimpl_results.evals_notes:
|
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]
|
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
||||||
row = {
|
row = {
|
||||||
"type": "",
|
"type": "",
|
||||||
|
@ -128,6 +131,7 @@ def evaluations_recap_table(formsemestre: FormSemestre) -> list[dict]:
|
||||||
"_titre_target_attrs": 'class="discretelink"',
|
"_titre_target_attrs": 'class="discretelink"',
|
||||||
"date": e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "",
|
"date": e.date_debut.strftime(scu.DATE_FMT) if e.date_debut else "",
|
||||||
"_date_order": e.date_debut.isoformat() 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": "oui" if eval_etat.is_complete else "non",
|
||||||
"_complete_target": "#",
|
"_complete_target": "#",
|
||||||
"_complete_target_attrs": (
|
"_complete_target_attrs": (
|
||||||
|
|
|
@ -25,8 +25,8 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Evaluations
|
"""Evaluations"""
|
||||||
"""
|
|
||||||
import collections
|
import collections
|
||||||
import datetime
|
import datetime
|
||||||
import operator
|
import operator
|
||||||
|
@ -50,6 +50,7 @@ from app.scodoc import sco_cal
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_formsemestre_inscriptions
|
from app.scodoc import sco_formsemestre_inscriptions
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
|
from app.scodoc import sco_gen_cal
|
||||||
from app.scodoc import sco_moduleimpl
|
from app.scodoc import sco_moduleimpl
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_users
|
from app.scodoc import sco_users
|
||||||
|
@ -360,6 +361,106 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
|
||||||
return etat
|
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):
|
def formsemestre_evaluations_cal(formsemestre_id):
|
||||||
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
||||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
@ -368,58 +469,9 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
||||||
evaluations = formsemestre.get_evaluations()
|
evaluations = formsemestre.get_evaluations()
|
||||||
nb_evals = len(evaluations)
|
nb_evals = len(evaluations)
|
||||||
|
|
||||||
color_incomplete = "#FF6060"
|
|
||||||
color_complete = "#A0FFA0"
|
|
||||||
color_futur = "#70E0FF"
|
|
||||||
|
|
||||||
year = formsemestre.annee_scolaire()
|
year = formsemestre.annee_scolaire()
|
||||||
events = {} # (day, halfday) : event
|
cal = CalendrierEval(year, evaluations, nt)
|
||||||
for e in evaluations:
|
cal_html = cal.get_html()
|
||||||
if e.date_debut is None:
|
|
||||||
continue # éval. sans date
|
|
||||||
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
|
||||||
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 "?"
|
|
||||||
|
|
||||||
description = f"""{
|
|
||||||
e.moduleimpl.module.titre
|
|
||||||
}, de {heure_debut_txt} à {heure_fin_txt}"""
|
|
||||||
|
|
||||||
# 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
|
|
||||||
href = url_for(
|
|
||||||
"notes.moduleimpl_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
moduleimpl_id=e.moduleimpl_id,
|
|
||||||
)
|
|
||||||
day = e.date_debut.date().isoformat() # yyyy-mm-dd
|
|
||||||
event = events.get(day)
|
|
||||||
if not event:
|
|
||||||
events[day] = [day, txt, color, href, description, e.moduleimpl]
|
|
||||||
else:
|
|
||||||
if event[-1].id != e.moduleimpl.id:
|
|
||||||
# plusieurs evals de modules differents a la meme date
|
|
||||||
event[1] += ", " + txt
|
|
||||||
event[4] += ", " + description
|
|
||||||
if color == color_incomplete:
|
|
||||||
event[2] = color_incomplete
|
|
||||||
if color == color_futur:
|
|
||||||
event[2] = color_futur
|
|
||||||
|
|
||||||
cal_html = sco_cal.YearTable(
|
|
||||||
year, events=list(events.values()), halfday=False, pad_width=None
|
|
||||||
)
|
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
{
|
{
|
||||||
|
@ -435,15 +487,15 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>en <span style=
|
<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
|
les évaluations passées auxquelles il manque des notes
|
||||||
</li>
|
</li>
|
||||||
<li>en <span style=
|
<li>en <span style=
|
||||||
"background-color: {color_complete}">vert</span>
|
"background-color: {JourEval.COLOR_COMPLETE}">vert</span>
|
||||||
les évaluations déjà notées
|
les évaluations déjà notées
|
||||||
</li>
|
</li>
|
||||||
<li>en <span style=
|
<li>en <span style=
|
||||||
"background-color: {color_futur}">bleu</span>
|
"background-color: {JourEval.COLOR_FUTUR}">bleu</span>
|
||||||
les évaluations futures
|
les évaluations futures
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -581,6 +633,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
|
||||||
base_url="%s?formsemestre_id=%s" % (request.base_url, 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()}""",
|
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||||
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
|
||||||
|
table_id="formsemestre_evaluations_delai_correction",
|
||||||
)
|
)
|
||||||
return tab.make_page(fmt=fmt)
|
return tab.make_page(fmt=fmt)
|
||||||
|
|
||||||
|
|
|
@ -45,13 +45,17 @@ class ScoInvalidCSRF(ScoException):
|
||||||
|
|
||||||
|
|
||||||
class ScoValueError(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
|
# mal nommée: super classe de toutes les exceptions avec page
|
||||||
# d'erreur gentille.
|
# d'erreur gentille.
|
||||||
def __init__(self, msg, dest_url=None):
|
def __init__(self, msg, dest_url=None, safe=False):
|
||||||
super().__init__(msg)
|
super().__init__(msg)
|
||||||
self.dest_url = dest_url
|
self.dest_url = dest_url
|
||||||
|
self.safe = safe # utilisé par template sco_value_error.j2
|
||||||
|
|
||||||
|
|
||||||
class ScoPermissionDenied(ScoValueError):
|
class ScoPermissionDenied(ScoValueError):
|
||||||
|
|
|
@ -106,6 +106,7 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="export_result_table",
|
||||||
)
|
)
|
||||||
return tab, semlist
|
return tab, semlist
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,7 @@ from flask import url_for, g, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app.models import Departement
|
from app.models import Departement, Identite
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
|
@ -55,7 +54,9 @@ def form_search_etud(
|
||||||
"form recherche par nom"
|
"form recherche par nom"
|
||||||
H = []
|
H = []
|
||||||
H.append(
|
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>
|
<b>{title}</b>
|
||||||
<input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
|
<input type="text" name="expnom" class="in-expnom" width="12" spellcheck="false" value="">
|
||||||
<input type="submit" value="Chercher">
|
<input type="submit" value="Chercher">
|
||||||
|
@ -100,9 +101,9 @@ def form_search_etud(
|
||||||
return "\n".join(H)
|
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:
|
"""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:
|
if not isinstance(expnom, int) and len(expnom) <= 1:
|
||||||
return [] # si expnom est trop court, n'affiche rien
|
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:
|
except ValueError:
|
||||||
etudid = None
|
etudid = None
|
||||||
if etudid is not None:
|
if etudid is not None:
|
||||||
etuds = sco_etud.get_etud_info(filled=True, etudid=expnom)
|
etud = Identite.query.filter_by(dept_id=g.scodoc_dept_id, id=etudid).first()
|
||||||
if len(etuds) == 1:
|
if etud:
|
||||||
return etuds
|
return [etud]
|
||||||
expnom_str = str(expnom)
|
expnom_str = str(expnom)
|
||||||
if scu.is_valid_code_nip(expnom_str):
|
if scu.is_valid_code_nip(expnom_str):
|
||||||
return search_etuds_infos(code_nip=expnom_str)
|
etuds = Identite.query.filter_by(
|
||||||
return search_etuds_infos(expnom=expnom_str)
|
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=""):
|
def search_etud_in_dept(expnom=""):
|
||||||
|
@ -152,7 +162,7 @@ def search_etud_in_dept(expnom=""):
|
||||||
|
|
||||||
if len(etuds) == 1:
|
if len(etuds) == 1:
|
||||||
# va directement a la fiche
|
# 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))
|
return flask.redirect(url_for(endpoint, **url_args))
|
||||||
|
|
||||||
H = [
|
H = [
|
||||||
|
@ -179,14 +189,39 @@ def search_etud_in_dept(expnom=""):
|
||||||
)
|
)
|
||||||
if len(etuds) > 0:
|
if len(etuds) > 0:
|
||||||
# Choix dans la liste des résultats:
|
# Choix dans la liste des résultats:
|
||||||
|
rows = []
|
||||||
|
e: Identite
|
||||||
for e in etuds:
|
for e in etuds:
|
||||||
url_args["etudid"] = e["etudid"]
|
url_args["etudid"] = e.id
|
||||||
target = url_for(endpoint, **url_args)
|
target = url_for(endpoint, **url_args)
|
||||||
e["_nomprenom_target"] = target
|
cur_inscription = e.inscription_courante()
|
||||||
e["inscription_target"] = target
|
inscription = (
|
||||||
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
e.inscription_descr().get("inscription_str", "")
|
||||||
sco_groups.etud_add_group_infos(
|
if cur_inscription
|
||||||
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
|
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(
|
tab = GenTable(
|
||||||
|
@ -197,10 +232,11 @@ def search_etud_in_dept(expnom=""):
|
||||||
"inscription": "Inscription",
|
"inscription": "Inscription",
|
||||||
"groupes": "Groupes",
|
"groupes": "Groupes",
|
||||||
},
|
},
|
||||||
rows=etuds,
|
rows=rows,
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="search_etud_in_dept",
|
||||||
)
|
)
|
||||||
H.append(tab.html())
|
H.append(tab.html())
|
||||||
if len(etuds) > 20: # si la page est grande
|
if len(etuds) > 20: # si la page est grande
|
||||||
|
@ -213,15 +249,16 @@ def search_etud_in_dept(expnom=""):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
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(
|
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()
|
return "\n".join(H) + html_sco_header.sco_footer()
|
||||||
|
|
||||||
|
|
||||||
# Was chercheEtudsInfo()
|
# 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
|
"""recherche les étudiants correspondants à expnom ou au code_nip
|
||||||
et ramene liste de mappings utilisables en DTML.
|
et ramene liste de mappings utilisables en DTML.
|
||||||
"""
|
"""
|
||||||
|
@ -264,7 +301,7 @@ def search_etud_by_name(term: str) -> list:
|
||||||
FROM identite
|
FROM identite
|
||||||
WHERE
|
WHERE
|
||||||
dept_id = %(dept_id)s
|
dept_id = %(dept_id)s
|
||||||
AND code_nip LIKE %(beginning)s
|
AND code_nip ILIKE %(beginning)s
|
||||||
ORDER BY nom
|
ORDER BY nom
|
||||||
""",
|
""",
|
||||||
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
||||||
|
@ -283,7 +320,7 @@ def search_etud_by_name(term: str) -> list:
|
||||||
FROM identite
|
FROM identite
|
||||||
WHERE
|
WHERE
|
||||||
dept_id = %(dept_id)s
|
dept_id = %(dept_id)s
|
||||||
AND nom LIKE %(beginning)s
|
AND nom ILIKE %(beginning)s
|
||||||
ORDER BY nom
|
ORDER BY nom
|
||||||
""",
|
""",
|
||||||
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
||||||
|
@ -348,6 +385,7 @@ def table_etud_in_accessible_depts(expnom=None):
|
||||||
rows=etuds,
|
rows=etuds,
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
|
table_id="etud_in_accessible_depts",
|
||||||
)
|
)
|
||||||
|
|
||||||
H.append('<div class="table_etud_in_dept">')
|
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)
|
result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
|
||||||
|
|
||||||
T = []
|
rows = []
|
||||||
for etuds in result:
|
for etuds in result:
|
||||||
if etuds:
|
if etuds:
|
||||||
dept_id = etuds[0]["dept"]
|
dept_id = etuds[0]["dept"]
|
||||||
for e in etuds:
|
for e in etuds:
|
||||||
for sem in e["sems"]:
|
for sem in e["sems"]:
|
||||||
T.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"dept": dept_id,
|
"dept": dept_id,
|
||||||
"etudid": e["etudid"],
|
"etudid": e["etudid"],
|
||||||
|
@ -414,6 +452,6 @@ def search_inscr_etud_by_nip(code_nip, fmt="json"):
|
||||||
"date_debut_iso",
|
"date_debut_iso",
|
||||||
"date_fin_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)
|
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",
|
"semestres_ues": "Semestres avec UEs",
|
||||||
}
|
}
|
||||||
return GenTable(
|
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 ""),
|
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,
|
page_title=title,
|
||||||
pdf_title=title,
|
pdf_title=title,
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
rows=rows,
|
||||||
|
table_id="formation_list_table",
|
||||||
|
titles=titles,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -527,15 +527,16 @@ def table_formsemestres(
|
||||||
preferences = sco_preferences.SemPreferences()
|
preferences = sco_preferences.SemPreferences()
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
rows=sems,
|
|
||||||
titles=titles,
|
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
|
html_empty_element="<p><em>aucun résultat</em></p>",
|
||||||
|
html_next_section=html_next_section,
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
html_title=html_title,
|
html_title=html_title,
|
||||||
html_next_section=html_next_section,
|
|
||||||
html_empty_element="<p><em>aucun résultat</em></p>",
|
|
||||||
page_title="Semestres",
|
page_title="Semestres",
|
||||||
preferences=preferences,
|
preferences=preferences,
|
||||||
|
rows=sems,
|
||||||
|
table_id="table_formsemestres",
|
||||||
|
titles=titles,
|
||||||
)
|
)
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
|
@ -726,20 +726,21 @@ def formsemestre_description_table(
|
||||||
rows.append(sums)
|
rows.append(sums)
|
||||||
|
|
||||||
return GenTable(
|
return GenTable(
|
||||||
columns_ids=columns_ids,
|
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
|
||||||
rows=rows,
|
|
||||||
titles=titles,
|
|
||||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
|
||||||
caption=title,
|
caption=title,
|
||||||
|
columns_ids=columns_ids,
|
||||||
html_caption=title,
|
html_caption=title,
|
||||||
html_class="table_leftalign formsemestre_description",
|
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(
|
html_title=html_sco_header.html_sem_header(
|
||||||
"Description du semestre", with_page_header=False
|
"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,
|
pdf_title=title,
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
rows=rows,
|
||||||
|
table_id="formsemestre_description_table",
|
||||||
|
titles=titles,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -821,6 +822,55 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sem-groups-assi">
|
<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,
|
||||||
|
day=datetime.date.today().isoformat(),
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
group_ids=group.id,
|
||||||
|
)}">
|
||||||
|
Saisir l'assiduité</a>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
# YYYY-Www (ISO 8601) :
|
||||||
|
current_week: str = datetime.datetime.now().strftime("%G-W%V")
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div>
|
||||||
|
<a class="stdlink" href="{
|
||||||
|
url_for("assiduites.signal_assiduites_hebdo",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
group_ids=group.id,
|
||||||
|
week=current_week,
|
||||||
|
)}">Saisie hebdomadaire</a>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
if can_edit_abs:
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<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>
|
<div>
|
||||||
<a class="stdlink" href="{
|
<a class="stdlink" href="{
|
||||||
url_for("assiduites.visu_assi_group",
|
url_for("assiduites.visu_assi_group",
|
||||||
|
@ -833,49 +883,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||||
</div>
|
</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>
|
|
||||||
</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
|
H.append("</div>") # /sem-groups-assi
|
||||||
if partition_is_empty:
|
if partition_is_empty:
|
||||||
|
@ -1186,17 +1193,7 @@ def formsemestre_tableau_modules(
|
||||||
mod_descr = "Module " + (mod.titre or "")
|
mod_descr = "Module " + (mod.titre or "")
|
||||||
is_apc = mod.is_apc() # SAE ou ressource
|
is_apc = mod.is_apc() # SAE ou ressource
|
||||||
if is_apc:
|
if is_apc:
|
||||||
coef_descr = ", ".join(
|
mod_descr += " " + mod.get_ue_coefs_descr()
|
||||||
[
|
|
||||||
f"{ue.acronyme}: {co}"
|
|
||||||
for ue, co in mod.ue_coefs_list()
|
|
||||||
if isinstance(co, float) and co > 0
|
|
||||||
]
|
|
||||||
)
|
|
||||||
if coef_descr:
|
|
||||||
mod_descr += " Coefs: " + coef_descr
|
|
||||||
else:
|
|
||||||
mod_descr += " (pas de coefficients) "
|
|
||||||
else:
|
else:
|
||||||
mod_descr += ", coef. " + str(mod.coefficient)
|
mod_descr += ", coef. " + str(mod.coefficient)
|
||||||
mod_ens = sco_users.user_info(modimpl.responsable_id)["nomcomplet"]
|
mod_ens = sco_users.user_info(modimpl.responsable_id)["nomcomplet"]
|
||||||
|
|
214
app/scodoc/sco_gen_cal.py
Normal file
214
app/scodoc/sco_gen_cal.py
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
"""
|
||||||
|
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, fmt=scu.DATE_FMT) -> str:
|
||||||
|
"""
|
||||||
|
Renvoie la date du jour au format fmt ou "dd/mm/yyyy" par défaut
|
||||||
|
"""
|
||||||
|
return self.date.strftime(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
|
||||||
|
|
||||||
|
highlight: str
|
||||||
|
-> ["jour", "semaine", "mois"]
|
||||||
|
permet de mettre en valeur lors du passage de la souris
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
date_debut: datetime.date,
|
||||||
|
date_fin: datetime.date,
|
||||||
|
highlight: str = None,
|
||||||
|
):
|
||||||
|
self.date_debut = date_debut
|
||||||
|
self.date_fin = date_fin
|
||||||
|
self.jours: dict[str, list[Jour]] = {}
|
||||||
|
self.highlight: str = highlight
|
||||||
|
|
||||||
|
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] = {} # semaine {22: []}
|
||||||
|
|
||||||
|
jour: Jour = self.instanciate_jour(date)
|
||||||
|
semaine = date.strftime("%G-W%V")
|
||||||
|
if semaine not in organized[month]:
|
||||||
|
organized[month][semaine] = []
|
||||||
|
organized[month][semaine].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, highlight=self.highlight
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class JourChoix(Jour):
|
||||||
|
"""
|
||||||
|
Représente un jour dans le calendrier pour choisir une date
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_html(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class CalendrierChoix(Calendrier):
|
||||||
|
"""
|
||||||
|
Représente un calendrier pour choisir une date
|
||||||
|
"""
|
||||||
|
|
||||||
|
def instanciate_jour(self, date: datetime.date) -> Jour:
|
||||||
|
return JourChoix(date)
|
||||||
|
|
||||||
|
|
||||||
|
def calendrier_choix_date(
|
||||||
|
date_debut: datetime.date,
|
||||||
|
date_fin: datetime.date,
|
||||||
|
url: str,
|
||||||
|
mode: str = "jour",
|
||||||
|
titre: str = "Choisir une date",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Permet d'afficher un calendrier pour choisir une date et renvoyer sur une url.
|
||||||
|
|
||||||
|
mode : str
|
||||||
|
- "jour" -> ajoutera "&day=yyyy-mm-dd" à l'url (ex: 2024-05-30)
|
||||||
|
- "semaine" -> ajoutera "&week=yyyy-Www" à l'url (ex : 2024-W22)
|
||||||
|
|
||||||
|
titre : str
|
||||||
|
- texte à afficher au dessus du calendrier
|
||||||
|
"""
|
||||||
|
|
||||||
|
calendrier: CalendrierChoix = CalendrierChoix(date_debut, date_fin, highlight=mode)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"choix_date.j2",
|
||||||
|
calendrier=calendrier.get_html(),
|
||||||
|
url=url,
|
||||||
|
titre=titre,
|
||||||
|
mode=mode,
|
||||||
|
)
|
|
@ -92,5 +92,6 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
table_id="groups_export_annotations",
|
||||||
)
|
)
|
||||||
return table.make_page(fmt=fmt)
|
return table.make_page(fmt=fmt)
|
||||||
|
|
|
@ -585,8 +585,8 @@ def groups_table(
|
||||||
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
|
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
|
||||||
etud_info["_prenom_target"] = fiche_url
|
etud_info["_prenom_target"] = fiche_url
|
||||||
|
|
||||||
etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (
|
etud_info["_nom_disp_td_attrs"] = (
|
||||||
etud_info["etudid"]
|
'id="%s" class="etudinfo"' % (etud_info["etudid"])
|
||||||
)
|
)
|
||||||
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
|
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
|
||||||
if etud_info["etat"] == "D":
|
if etud_info["etat"] == "D":
|
||||||
|
@ -661,6 +661,7 @@ def groups_table(
|
||||||
text_fields_separator=prefs["moodle_csv_separator"],
|
text_fields_separator=prefs["moodle_csv_separator"],
|
||||||
text_with_titles=prefs["moodle_csv_with_headerline"],
|
text_with_titles=prefs["moodle_csv_with_headerline"],
|
||||||
preferences=prefs,
|
preferences=prefs,
|
||||||
|
table_id="groups_table",
|
||||||
)
|
)
|
||||||
#
|
#
|
||||||
if fmt == "html":
|
if fmt == "html":
|
||||||
|
@ -982,7 +983,7 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
|
||||||
"assiduites.signal_assiduites_group",
|
"assiduites.signal_assiduites_group",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
group_ids=",".join(map(str,groups_infos.group_ids)),
|
group_ids=",".join(map(str,groups_infos.group_ids)),
|
||||||
jour=datetime.date.today().isoformat(),
|
day=datetime.date.today().isoformat(),
|
||||||
formsemestre_id=groups_infos.formsemestre_id,
|
formsemestre_id=groups_infos.formsemestre_id,
|
||||||
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
||||||
)
|
)
|
||||||
|
@ -997,12 +998,12 @@ def form_choix_saisie_semaine(groups_infos):
|
||||||
return ""
|
return ""
|
||||||
query_args = parse_qs(request.query_string)
|
query_args = parse_qs(request.query_string)
|
||||||
moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
|
moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
|
||||||
semaine = datetime.date.today().isocalendar().week
|
semaine = datetime.datetime.now().strftime("%G-W%V")
|
||||||
return f"""
|
return f"""
|
||||||
<button onclick="window.location='{url_for(
|
<button onclick="window.location='{url_for(
|
||||||
"assiduites.signal_assiduites_diff",
|
"assiduites.signal_assiduites_hebdo",
|
||||||
group_ids=",".join(map(str,groups_infos.group_ids)),
|
group_ids=",".join(map(str,groups_infos.group_ids)),
|
||||||
semaine=semaine,
|
week=semaine,
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=groups_infos.formsemestre_id,
|
formsemestre_id=groups_infos.formsemestre_id,
|
||||||
moduleimpl_id=moduleimpl_id
|
moduleimpl_id=moduleimpl_id
|
||||||
|
@ -1028,10 +1029,9 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
|
||||||
moodle_sem_name = sem["session_id"]
|
moodle_sem_name = sem["session_id"]
|
||||||
|
|
||||||
columns_ids = ("email", "semestre_groupe")
|
columns_ids = ("email", "semestre_groupe")
|
||||||
T = []
|
rows = []
|
||||||
for partition_id in partitions_etud_groups:
|
for partition_id, members in partitions_etud_groups.items():
|
||||||
partition = sco_groups.get_partition(partition_id)
|
partition = sco_groups.get_partition(partition_id)
|
||||||
members = partitions_etud_groups[partition_id]
|
|
||||||
for etudid in members:
|
for etudid in members:
|
||||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||||
group_name = members[etudid]["group_name"]
|
group_name = members[etudid]["group_name"]
|
||||||
|
@ -1040,16 +1040,17 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
|
||||||
elts.append(partition["partition_name"])
|
elts.append(partition["partition_name"])
|
||||||
if group_name:
|
if group_name:
|
||||||
elts.append(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
|
# Make table
|
||||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
rows=T,
|
|
||||||
columns_ids=("email", "semestre_groupe"),
|
columns_ids=("email", "semestre_groupe"),
|
||||||
filename=moodle_sem_name + "-moodle",
|
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_fields_separator=prefs["moodle_csv_separator"],
|
||||||
text_with_titles=prefs["moodle_csv_with_headerline"],
|
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")
|
return tab.make_page(fmt="csv")
|
||||||
|
|
|
@ -834,11 +834,12 @@ def adm_table_description_format():
|
||||||
columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
|
columns_ids = ("attribute", "type", "writable", "description", "aliases_str")
|
||||||
|
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
titles=titles,
|
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
rows=list(Fmt.values()),
|
|
||||||
html_sortable=True,
|
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
|
html_sortable=True,
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
rows=list(Fmt.values()),
|
||||||
|
table_id="adm_table_description_format",
|
||||||
|
titles=titles,
|
||||||
)
|
)
|
||||||
return tab
|
return tab
|
||||||
|
|
|
@ -747,10 +747,11 @@ def etuds_select_box_xls(src_cat):
|
||||||
else:
|
else:
|
||||||
e["paiementinscription_str"] = "-"
|
e["paiementinscription_str"] = "-"
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
titles=titles,
|
|
||||||
columns_ids=columns_ids,
|
|
||||||
rows=etuds,
|
|
||||||
caption="%(title)s. %(help)s" % src_cat["infos"],
|
caption="%(title)s. %(help)s" % src_cat["infos"],
|
||||||
|
columns_ids=columns_ids,
|
||||||
preferences=sco_preferences.SemPreferences(),
|
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"])
|
return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"])
|
||||||
|
|
|
@ -599,20 +599,21 @@ def _make_table_notes(
|
||||||
)
|
)
|
||||||
# display
|
# display
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
titles=titles,
|
|
||||||
columns_ids=columns_ids,
|
|
||||||
rows=rows,
|
|
||||||
html_sortable=True,
|
|
||||||
base_url=base_url,
|
base_url=base_url,
|
||||||
filename=filename,
|
|
||||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
|
||||||
caption=caption,
|
caption=caption,
|
||||||
html_next_section=html_next_section,
|
columns_ids=columns_ids,
|
||||||
page_title="Notes de " + formsemestre.titre_mois(),
|
filename=filename,
|
||||||
html_title=html_title,
|
|
||||||
pdf_title=pdf_title,
|
|
||||||
html_class="notes_evaluation",
|
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),
|
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
|
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete
|
||||||
)
|
)
|
||||||
if fmt == "bordereau":
|
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",
|
html_class="table_leftalign table_listegroupe",
|
||||||
bottom_titles=bottom_titles,
|
bottom_titles=bottom_titles,
|
||||||
preferences=preferences,
|
preferences=preferences,
|
||||||
|
table_id="table_etuds_lycees",
|
||||||
)
|
)
|
||||||
return tab, etuds_by_lycee
|
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)
|
can_edit_notes_ens = modimpl.can_edit_notes(current_user)
|
||||||
|
|
||||||
if can_edit_notes and nbnotes != 0:
|
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:
|
else:
|
||||||
sup_label = "Supprimer évaluation"
|
sup_label = "Supprimer évaluation"
|
||||||
|
|
||||||
|
@ -146,29 +146,48 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
|
||||||
return htmlutils.make_menu("actions", menu_eval, alone=True)
|
return htmlutils.make_menu("actions", menu_eval, alone=True)
|
||||||
|
|
||||||
|
|
||||||
def _ue_coefs_html(coefs_lst) -> str:
|
def _ue_coefs_html(modimpl: ModuleImpl) -> str:
|
||||||
""" """
|
""" """
|
||||||
max_coef = max([x[1] for x in coefs_lst]) if coefs_lst else 1.0
|
coefs_lst = modimpl.module.ue_coefs_list()
|
||||||
H = """
|
max_coef = max(x[1] for x in coefs_lst) if coefs_lst else 1.0
|
||||||
|
H = f"""
|
||||||
<div id="modimpl_coefs">
|
<div id="modimpl_coefs">
|
||||||
<div>Coefficients vers les UE</div>
|
<div>Coefficients vers les UEs
|
||||||
|
<span><a class="stdlink" href="{
|
||||||
|
url_for(
|
||||||
|
"notes.edit_modules_ue_coefs",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formation_id=modimpl.module.formation.id,
|
||||||
|
semestre_idx=modimpl.formsemestre.semestre_id,
|
||||||
|
)
|
||||||
|
}">détail</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if coefs_lst:
|
if coefs_lst:
|
||||||
H += (
|
H += _html_hinton_map(
|
||||||
f"""
|
colors=(uc[0].color for uc in coefs_lst),
|
||||||
<div class="coefs_histo" style="--max:{max_coef}">
|
max_val=max_coef,
|
||||||
"""
|
size=36,
|
||||||
+ "\n".join(
|
title=modimpl.module.get_ue_coefs_descr(),
|
||||||
[
|
values=(uc[1] for uc in coefs_lst),
|
||||||
f"""<div style="--coef:{coef};
|
|
||||||
{'background-color: ' + ue.color + ';' if ue.color else ''}
|
|
||||||
"><div>{coef}</div>{ue.acronyme}</div>"""
|
|
||||||
for ue, coef in coefs_lst
|
|
||||||
if coef > 0
|
|
||||||
]
|
|
||||||
)
|
|
||||||
+ "</div>"
|
|
||||||
)
|
)
|
||||||
|
# (
|
||||||
|
# f"""
|
||||||
|
# <div class="coefs_histo" style="--max:{max_coef}">
|
||||||
|
# """
|
||||||
|
# + "\n".join(
|
||||||
|
# [
|
||||||
|
# f"""<div style="--coef:{coef};
|
||||||
|
# {'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||||
|
# "><div>{coef}</div>{ue.acronyme}</div>"""
|
||||||
|
# for ue, coef in coefs_lst
|
||||||
|
# if coef > 0
|
||||||
|
# ]
|
||||||
|
# )
|
||||||
|
# + "</div>"
|
||||||
|
# )
|
||||||
else:
|
else:
|
||||||
H += """<div class="missing_value">non définis</div>"""
|
H += """<div class="missing_value">non définis</div>"""
|
||||||
H += "</div>"
|
H += "</div>"
|
||||||
|
@ -195,12 +214,21 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||||
Evaluation.date_debut.desc(),
|
Evaluation.date_debut.desc(),
|
||||||
).all()
|
).all()
|
||||||
nb_evaluations = len(evaluations)
|
nb_evaluations = len(evaluations)
|
||||||
max_poids = max(
|
# Le poids max pour chaque catégorie d'évaluation
|
||||||
|
max_poids_by_type: dict[int, float] = {}
|
||||||
|
for eval_type in (
|
||||||
|
Evaluation.EVALUATION_NORMALE,
|
||||||
|
Evaluation.EVALUATION_RATTRAPAGE,
|
||||||
|
Evaluation.EVALUATION_SESSION2,
|
||||||
|
Evaluation.EVALUATION_BONUS,
|
||||||
|
):
|
||||||
|
max_poids_by_type[eval_type] = max(
|
||||||
[
|
[
|
||||||
max([p.poids for p in e.ue_poids] or [0]) * (e.coefficient or 0.0)
|
max([p.poids for p in e.ue_poids] or [0]) * (e.coefficient or 0.0)
|
||||||
for e in evaluations
|
for e in evaluations
|
||||||
|
if e.evaluation_type == eval_type
|
||||||
]
|
]
|
||||||
or [0]
|
or [0.0]
|
||||||
)
|
)
|
||||||
#
|
#
|
||||||
sem_locked = not formsemestre.etat
|
sem_locked = not formsemestre.etat
|
||||||
|
@ -265,7 +293,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||||
H.append(scu.icontag("lock32_img", title="verrouillé"))
|
H.append(scu.icontag("lock32_img", title="verrouillé"))
|
||||||
H.append("""</td><td class="fichetitre2">""")
|
H.append("""</td><td class="fichetitre2">""")
|
||||||
if modimpl.module.is_apc():
|
if modimpl.module.is_apc():
|
||||||
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list()))
|
H.append(_ue_coefs_html(modimpl))
|
||||||
else:
|
else:
|
||||||
H.append(
|
H.append(
|
||||||
f"""Coef. dans le semestre: {
|
f"""Coef. dans le semestre: {
|
||||||
|
@ -318,12 +346,28 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||||
f"""
|
f"""
|
||||||
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
||||||
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
|
||||||
}?group_ids={group_id}&jour={
|
}?group_ids={group_id}&day={
|
||||||
datetime.date.today().isoformat()
|
datetime.date.today().isoformat()
|
||||||
}&formsemestre_id={formsemestre.id}
|
}&formsemestre_id={formsemestre.id}
|
||||||
&moduleimpl_id={moduleimpl_id}
|
&moduleimpl_id={moduleimpl_id}
|
||||||
"
|
"
|
||||||
>Saisie Absences journée</a></span>
|
>Saisie Absences</a></span>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
current_week: str = datetime.datetime.now().strftime("%G-W%V")
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
|
||||||
|
url_for("assiduites.signal_assiduites_hebdo",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
group_ids=group_id,
|
||||||
|
week=current_week,
|
||||||
|
moduleimpl_id=moduleimpl_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>Saisie Absences (Hebdo)</a></span>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
|
@ -335,8 +379,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||||
group_ids=group_id,
|
group_ids=group_id,
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
|
||||||
)}"
|
)}" title="Page en cours de fusion et sera prochainement supprimée. Veuillez utiliser la page `Saisie Absences`"
|
||||||
>Saisie Absences Différée</a></span>
|
>(Saisie Absences Différée)</a></span>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -344,9 +388,34 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||||
#
|
#
|
||||||
if not modimpl.check_apc_conformity(nt):
|
if not modimpl.check_apc_conformity(nt):
|
||||||
H.append(
|
H.append(
|
||||||
"""<div class="warning conformite">Les poids des évaluations de ce module ne sont
|
"""<div class="warning conformite">Les poids des évaluations de ce
|
||||||
pas encore conformes au PN.
|
module ne permettent pas d'évaluer toutes les UEs (compétences)
|
||||||
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
|
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>"""
|
</div>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -437,7 +506,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||||
eval_index=eval_index,
|
eval_index=eval_index,
|
||||||
nb_evals=nb_evaluations,
|
nb_evals=nb_evaluations,
|
||||||
is_apc=nt.is_apc,
|
is_apc=nt.is_apc,
|
||||||
max_poids=max_poids,
|
max_poids=max_poids_by_type.get(evaluation.evaluation_type, 10000.0),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
eval_index -= 1
|
eval_index -= 1
|
||||||
|
@ -756,27 +825,27 @@ def _ligne_evaluation(
|
||||||
#
|
#
|
||||||
if etat["nb_notes"] == 0:
|
if etat["nb_notes"] == 0:
|
||||||
H.append(f"""<tr class="{tr_class}"><td></td>""")
|
H.append(f"""<tr class="{tr_class}"><td></td>""")
|
||||||
if modimpl.module.is_apc():
|
# if modimpl.module.is_apc():
|
||||||
H.append(
|
# H.append(
|
||||||
f"""<td colspan="8" class="eval_poids">{
|
# f"""<td colspan="8" class="eval_poids">{
|
||||||
evaluation.get_ue_poids_str()}</td>"""
|
# evaluation.get_ue_poids_str()}</td>"""
|
||||||
)
|
# )
|
||||||
else:
|
# else:
|
||||||
H.append('<td colspan="8"></td>')
|
# H.append('<td colspan="8"></td>')
|
||||||
H.append("""</tr>""")
|
H.append("""</tr>""")
|
||||||
else: # il y a deja des notes saisies
|
else: # il y a deja des notes saisies
|
||||||
gr_moyennes = etat["gr_moyennes"]
|
gr_moyennes = etat["gr_moyennes"]
|
||||||
first_group = True
|
# first_group = True
|
||||||
for gr_moyenne in gr_moyennes:
|
for gr_moyenne in gr_moyennes:
|
||||||
H.append(f"""<tr class="{tr_class}"><td> </td>""")
|
H.append(f"""<tr class="{tr_class}"><td> </td>""")
|
||||||
if first_group and modimpl.module.is_apc():
|
# if first_group and modimpl.module.is_apc():
|
||||||
H.append(
|
# H.append(
|
||||||
f"""<td class="eval_poids" colspan="4">{
|
# f"""<td class="eval_poids" colspan="4">{
|
||||||
evaluation.get_ue_poids_str()}</td>"""
|
# evaluation.get_ue_poids_str()}</td>"""
|
||||||
)
|
# )
|
||||||
else:
|
# else:
|
||||||
H.append("""<td colspan="4"></td>""")
|
H.append("""<td colspan="4"></td>""")
|
||||||
first_group = False
|
# first_group = False
|
||||||
if gr_moyenne["group_name"] is None:
|
if gr_moyenne["group_name"] is None:
|
||||||
name = "Tous" # tous
|
name = "Tous" # tous
|
||||||
else:
|
else:
|
||||||
|
@ -832,26 +901,47 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
|
||||||
ue_poids = evaluation.get_ue_poids_dict(sort=True) # { ue_id : poids }
|
ue_poids = evaluation.get_ue_poids_dict(sort=True) # { ue_id : poids }
|
||||||
if not ue_poids:
|
if not ue_poids:
|
||||||
return ""
|
return ""
|
||||||
if max_poids < scu.NOTES_PRECISION:
|
values = [poids * (evaluation.coefficient) for poids in ue_poids.values()]
|
||||||
|
colors = [db.session.get(UniteEns, ue_id).color for ue_id in ue_poids]
|
||||||
|
return _html_hinton_map(
|
||||||
|
classes=("evaluation_poids",),
|
||||||
|
colors=colors,
|
||||||
|
max_val=max_poids,
|
||||||
|
title=f"Poids de l'évaluation vers les UEs: {evaluation.get_ue_poids_str()}",
|
||||||
|
values=values,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _html_hinton_map(
|
||||||
|
classes=(),
|
||||||
|
colors=(),
|
||||||
|
max_val: float | None = None,
|
||||||
|
size=12,
|
||||||
|
title: str = "",
|
||||||
|
values=(),
|
||||||
|
) -> str:
|
||||||
|
"""Représente une liste de nombres sous forme de carrés"""
|
||||||
|
if max_val is None:
|
||||||
|
max_val = max(values)
|
||||||
|
if max_val < scu.NOTES_PRECISION:
|
||||||
return ""
|
return ""
|
||||||
H = (
|
return (
|
||||||
"""<div class="evaluation_poids">"""
|
f"""<div class="hinton_map {" ".join(classes)}"
|
||||||
|
style="--size:{size}px;"
|
||||||
|
title="{title}"
|
||||||
|
data-tooltip>"""
|
||||||
+ "\n".join(
|
+ "\n".join(
|
||||||
[
|
[
|
||||||
f"""<div title="poids vers {ue.acronyme}: {poids:g}">
|
f"""<div>
|
||||||
<div style="--size:{math.sqrt(poids*(evaluation.coefficient)/max_poids*144)}px;
|
<div style="--boxsize:{size*math.sqrt(value/max_val)}px;
|
||||||
{'background-color: ' + ue.color + ';' if ue.color else ''}
|
{'background-color: ' + color + ';' if color else ''}
|
||||||
"></div>
|
"></div>
|
||||||
</div>"""
|
</div>"""
|
||||||
for ue, poids in (
|
for value, color in zip(values, colors)
|
||||||
(db.session.get(UniteEns, ue_id), poids)
|
|
||||||
for ue_id, poids in ue_poids.items()
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
+ "</div>"
|
+ "</div>"
|
||||||
)
|
)
|
||||||
return H
|
|
||||||
|
|
||||||
|
|
||||||
def _html_modimpl_etuds_attente(res: ResultatsSemestre, modimpl: ModuleImpl) -> str:
|
def _html_modimpl_etuds_attente(res: ResultatsSemestre, modimpl: ModuleImpl) -> str:
|
||||||
|
|
|
@ -247,6 +247,7 @@ class ScoDocPageTemplate(PageTemplate):
|
||||||
footer_template=DEFAULT_PDF_FOOTER_TEMPLATE,
|
footer_template=DEFAULT_PDF_FOOTER_TEMPLATE,
|
||||||
filigranne=None,
|
filigranne=None,
|
||||||
preferences=None, # dictionnary with preferences, required
|
preferences=None, # dictionnary with preferences, required
|
||||||
|
with_page_numbers=False,
|
||||||
):
|
):
|
||||||
"""Initialise our page template."""
|
"""Initialise our page template."""
|
||||||
# defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
|
# defered import (solve circular dependency ->sco_logo ->scodoc, ->sco_pdf
|
||||||
|
@ -259,8 +260,9 @@ class ScoDocPageTemplate(PageTemplate):
|
||||||
self.pdfmeta_subject = subject
|
self.pdfmeta_subject = subject
|
||||||
self.server_name = server_name
|
self.server_name = server_name
|
||||||
self.filigranne = filigranne
|
self.filigranne = filigranne
|
||||||
self.page_number = 1
|
|
||||||
self.footer_template = footer_template
|
self.footer_template = footer_template
|
||||||
|
self.with_page_numbers = with_page_numbers
|
||||||
|
self.page_number = 1
|
||||||
if self.preferences:
|
if self.preferences:
|
||||||
self.with_page_background = self.preferences["bul_pdf_with_background"]
|
self.with_page_background = self.preferences["bul_pdf_with_background"]
|
||||||
else:
|
else:
|
||||||
|
@ -337,6 +339,7 @@ class ScoDocPageTemplate(PageTemplate):
|
||||||
|
|
||||||
def draw_footer(self, canv, content):
|
def draw_footer(self, canv, content):
|
||||||
"""Print the footer"""
|
"""Print the footer"""
|
||||||
|
# called 1/page
|
||||||
try:
|
try:
|
||||||
canv.setFont(
|
canv.setFont(
|
||||||
self.preferences["SCOLAR_FONT"],
|
self.preferences["SCOLAR_FONT"],
|
||||||
|
@ -351,8 +354,11 @@ class ScoDocPageTemplate(PageTemplate):
|
||||||
canv.drawString(
|
canv.drawString(
|
||||||
self.preferences["pdf_footer_x"] * mm,
|
self.preferences["pdf_footer_x"] * mm,
|
||||||
self.preferences["pdf_footer_y"] * mm,
|
self.preferences["pdf_footer_y"] * mm,
|
||||||
content,
|
content + " " + (self.preferences["pdf_footer_extra"] or ""),
|
||||||
)
|
)
|
||||||
|
if self.with_page_numbers:
|
||||||
|
canv.drawString(190.0 * mm, 6 * mm, f"Page {self.page_number}")
|
||||||
|
|
||||||
canv.restoreState()
|
canv.restoreState()
|
||||||
|
|
||||||
def footer_string(self) -> str:
|
def footer_string(self) -> str:
|
||||||
|
@ -382,18 +388,14 @@ class ScoDocPageTemplate(PageTemplate):
|
||||||
filigranne = self.filigranne.get(doc.page, None)
|
filigranne = self.filigranne.get(doc.page, None)
|
||||||
if filigranne:
|
if filigranne:
|
||||||
canv.saveState()
|
canv.saveState()
|
||||||
canv.translate(9 * cm, 27.6 * cm)
|
canv.translate(10 * cm, 21.0 * cm)
|
||||||
canv.rotate(30)
|
canv.rotate(36)
|
||||||
canv.scale(4.5, 4.5)
|
canv.scale(7, 7)
|
||||||
canv.setFillColorRGB(1.0, 0.65, 0.65, alpha=0.6)
|
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()
|
canv.restoreState()
|
||||||
doc.filigranne = None
|
doc.filigranne = None
|
||||||
|
# Increment page number
|
||||||
def afterPage(self):
|
|
||||||
"""Called after all flowables have been drawn on a page.
|
|
||||||
Increment pageNum since the page has been completed.
|
|
||||||
"""
|
|
||||||
self.page_number += 1
|
self.page_number += 1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -96,13 +96,16 @@ def photo_portal_url(code_nip: str):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_etud_photo_url(etudid, size="small"):
|
def get_etud_photo_url(etudid, size="small", seed=None):
|
||||||
|
"L'URL scodoc vers la photo de l'étudiant"
|
||||||
|
kwargs = {"seed": seed} if seed else {}
|
||||||
return (
|
return (
|
||||||
url_for(
|
url_for(
|
||||||
"scolar.get_photo_image",
|
"scolar.get_photo_image",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
etudid=etudid,
|
etudid=etudid,
|
||||||
size=size,
|
size=size,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
if has_request_context()
|
if has_request_context()
|
||||||
else ""
|
else ""
|
||||||
|
@ -114,9 +117,11 @@ def etud_photo_url(etud: dict, size="small", fast=False) -> str:
|
||||||
If ScoDoc doesn't have an image and a portal is configured, link to it.
|
If ScoDoc doesn't have an image and a portal is configured, link to it.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
photo_url = get_etud_photo_url(etud["etudid"], size=size)
|
|
||||||
if fast:
|
if fast:
|
||||||
return photo_url
|
return get_etud_photo_url(etud["etudid"], size=size)
|
||||||
|
photo_url = get_etud_photo_url(
|
||||||
|
etud["etudid"], size=size, seed=hash(etud.get("photo_filename"))
|
||||||
|
)
|
||||||
path = photo_pathname(etud["photo_filename"], size=size)
|
path = photo_pathname(etud["photo_filename"], size=size)
|
||||||
if not path:
|
if not path:
|
||||||
# Portail ?
|
# Portail ?
|
||||||
|
@ -374,7 +379,15 @@ def copy_portal_photo_to_fs(etudid: int):
|
||||||
portal_timeout = sco_preferences.get_preference("portal_timeout")
|
portal_timeout = sco_preferences.get_preference("portal_timeout")
|
||||||
error_message = None
|
error_message = None
|
||||||
try:
|
try:
|
||||||
r = requests.get(url, timeout=portal_timeout)
|
r = requests.get(
|
||||||
|
url,
|
||||||
|
timeout=portal_timeout,
|
||||||
|
params={
|
||||||
|
"nom": etud.nom or "",
|
||||||
|
"prenom": etud.prenom or "",
|
||||||
|
"civilite": etud.civilite,
|
||||||
|
},
|
||||||
|
)
|
||||||
except requests.ConnectionError:
|
except requests.ConnectionError:
|
||||||
error_message = "ConnectionError"
|
error_message = "ConnectionError"
|
||||||
except requests.Timeout:
|
except requests.Timeout:
|
||||||
|
|
|
@ -378,6 +378,7 @@ class PlacementRunner:
|
||||||
preferences=sco_preferences.SemPreferences(
|
preferences=sco_preferences.SemPreferences(
|
||||||
self.moduleimpl_data["formsemestre_id"]
|
self.moduleimpl_data["formsemestre_id"]
|
||||||
),
|
),
|
||||||
|
table_id="placement_pdf",
|
||||||
)
|
)
|
||||||
return tab.make_page(fmt="pdf", with_html_headers=False)
|
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",
|
html_class="table_leftalign table_listegroupe",
|
||||||
pdf_link=False, # pas d'export pdf
|
pdf_link=False, # pas d'export pdf
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
table_id="formsemestre_poursuite_report",
|
||||||
)
|
)
|
||||||
tab.filename = scu.make_filename("poursuite " + sem["titreannee"])
|
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)
|
Return base preferences for current scodoc_dept (instance BasePreferences)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import current_app, flash, g, request, url_for
|
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é",
|
"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,
|
"initvalue": 0,
|
||||||
"size": 10,
|
"title": "Désactiver la saisie des présences",
|
||||||
"title": "Durée par défaut d'un créneau",
|
"input_type": "boolcheckbox",
|
||||||
"type": "float",
|
"labels": ["non", "oui"],
|
||||||
"category": "assi",
|
"category": "assi",
|
||||||
"only_global": True,
|
"explanation": "Désactive la saisie et l'affichage des présences",
|
||||||
"explanation": "Durée d'un créneau en heure. Utilisé dans les pages de saisie",
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -644,18 +634,18 @@ class BasePreferences:
|
||||||
"category": "assi",
|
"category": "assi",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
# (
|
||||||
"assi_etat_defaut",
|
# "assi_etat_defaut",
|
||||||
{
|
# {
|
||||||
"explanation": "⚠ non fonctionnel, travaux en cours !",
|
# "explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||||
"initvalue": "aucun",
|
# "initvalue": "aucun",
|
||||||
"input_type": "menu",
|
# "input_type": "menu",
|
||||||
"labels": ["aucun", "present", "retard", "absent"],
|
# "labels": ["aucun", "present", "retard", "absent"],
|
||||||
"allowed_values": ["aucun", "present", "retard", "absent"],
|
# "allowed_values": ["aucun", "present", "retard", "absent"],
|
||||||
"title": "Définir l'état par défaut",
|
# "title": "Définir l'état par défaut",
|
||||||
"category": "assi",
|
# "category": "assi",
|
||||||
},
|
# },
|
||||||
),
|
# ),
|
||||||
(
|
(
|
||||||
"non_travail",
|
"non_travail",
|
||||||
{
|
{
|
||||||
|
@ -962,6 +952,16 @@ class BasePreferences:
|
||||||
"category": "pdf",
|
"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",
|
"pdf_footer_x",
|
||||||
{
|
{
|
||||||
|
@ -2291,9 +2291,7 @@ class BasePreferences:
|
||||||
if "explanation" in descr:
|
if "explanation" in descr:
|
||||||
del descr["explanation"]
|
del descr["explanation"]
|
||||||
if formsemestre_id:
|
if formsemestre_id:
|
||||||
descr[
|
descr["explanation"] = f"""ou <span class="spanlink"
|
||||||
"explanation"
|
|
||||||
] = f"""ou <span class="spanlink"
|
|
||||||
onclick="set_global_pref(this, '{pref_name}');"
|
onclick="set_global_pref(this, '{pref_name}');"
|
||||||
>utiliser paramètre global</span>"""
|
>utiliser paramètre global</span>"""
|
||||||
if formsemestre_id and self.is_global(formsemestre_id, pref_name):
|
if formsemestre_id and self.is_global(formsemestre_id, pref_name):
|
||||||
|
|
|
@ -42,7 +42,6 @@ from app.models import (
|
||||||
but_validations,
|
but_validations,
|
||||||
)
|
)
|
||||||
from app.scodoc import codes_cursus
|
from app.scodoc import codes_cursus
|
||||||
from app.scodoc import sco_edit_ue
|
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_cursus
|
from app.scodoc import sco_cursus
|
||||||
|
@ -81,6 +80,7 @@ def dict_pvjury(
|
||||||
},
|
},
|
||||||
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
'autorisations' : [ { 'semestre_id' : { ... } } ],
|
||||||
'validation_parcours' : True si parcours validé (diplome obtenu)
|
'validation_parcours' : True si parcours validé (diplome obtenu)
|
||||||
|
'parcours' : 'S1, S2, S3, S4, A1',
|
||||||
'prev_code' : code (calculé slt si with_prev),
|
'prev_code' : code (calculé slt si with_prev),
|
||||||
'mention' : mention (en fct moy gen),
|
'mention' : mention (en fct moy gen),
|
||||||
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
|
'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 }
|
D = {} # même chose que decisions, mais { etudid : dec }
|
||||||
for etudid in etudids:
|
for etudid in etudids:
|
||||||
etud = Identite.get_etud(etudid)
|
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
|
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 = {}
|
||||||
d["identite"] = nt.identdict[etudid]
|
d["identite"] = nt.identdict[etudid]
|
||||||
d["etat"] = nt.get_etud_etat(
|
d["etat"] = nt.get_etud_etat(
|
||||||
|
@ -120,9 +122,8 @@ def dict_pvjury(
|
||||||
d["decisions_ue"] = nt.get_etud_decisions_ue(etudid)
|
d["decisions_ue"] = nt.get_etud_decisions_ue(etudid)
|
||||||
if formsemestre.formation.is_apc():
|
if formsemestre.formation.is_apc():
|
||||||
d.update(but_validations.dict_decision_jury(etud, formsemestre))
|
d.update(but_validations.dict_decision_jury(etud, formsemestre))
|
||||||
d["last_formsemestre_id"] = Se.get_semestres()[
|
# id du dernier semestre (chronologiquement) dans lequel il a été inscrit:
|
||||||
-1
|
d["last_formsemestre_id"] = situation_etud.get_semestres()[-1]
|
||||||
] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit
|
|
||||||
|
|
||||||
ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
|
ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
|
||||||
d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values())
|
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"] = [a.to_dict() for a in autorisations]
|
||||||
d["autorisations_descr"] = descr_autorisations(autorisations)
|
d["autorisations_descr"] = descr_autorisations(autorisations)
|
||||||
|
|
||||||
d["validation_parcours"] = Se.parcours_validated()
|
d["validation_parcours"] = situation_etud.parcours_validated()
|
||||||
d["parcours"] = Se.get_cursus_descr(filter_futur=True)
|
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:
|
if with_parcours_decisions:
|
||||||
d["parcours_decisions"] = Se.get_parcours_decisions()
|
d["parcours_decisions"] = situation_etud.get_parcours_decisions()
|
||||||
# Observations sur les compensations:
|
# Observations sur les compensations:
|
||||||
compensators = sco_cursus_dut.scolar_formsemestre_validation_list(
|
compensators = sco_cursus_dut.scolar_formsemestre_validation_list(
|
||||||
cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
|
cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||||
|
@ -206,19 +210,19 @@ def dict_pvjury(
|
||||||
if not info:
|
if not info:
|
||||||
continue # should not occur
|
continue # should not occur
|
||||||
etud = info[0]
|
etud = info[0]
|
||||||
if Se.prev and Se.prev_decision:
|
if situation_etud.prev and situation_etud.prev_decision:
|
||||||
d["prev_decision_sem"] = Se.prev_decision
|
d["prev_decision_sem"] = situation_etud.prev_decision
|
||||||
d["prev_code"] = Se.prev_decision["code"]
|
d["prev_code"] = situation_etud.prev_decision["code"]
|
||||||
d["prev_code_descr"] = _descr_decision_sem(
|
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
|
has_prev = True
|
||||||
else:
|
else:
|
||||||
d["prev_decision_sem"] = None
|
d["prev_decision_sem"] = None
|
||||||
d["prev_code"] = ""
|
d["prev_code"] = ""
|
||||||
d["prev_code_descr"] = ""
|
d["prev_code_descr"] = ""
|
||||||
d["Se"] = Se
|
d["Se"] = situation_etud
|
||||||
|
|
||||||
decisions.append(d)
|
decisions.append(d)
|
||||||
D[etudid] = d
|
D[etudid] = d
|
||||||
|
|
|
@ -149,7 +149,7 @@ def pvjury_table(
|
||||||
etudid=e["identite"]["etudid"],
|
etudid=e["identite"]["etudid"],
|
||||||
),
|
),
|
||||||
"_nomprenom_td_attrs": f"""id="{e['identite']['etudid']}" class="etudinfo" """,
|
"_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"]),
|
"decision": _descr_decision_sem_abbrev(e["etat"], e["decision_sem"]),
|
||||||
"ue_cap": e["decisions_ue_descr"],
|
"ue_cap": e["decisions_ue_descr"],
|
||||||
"validation_parcours_code": "ADM" if e["validation_parcours"] else "",
|
"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_class="table_leftalign",
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
table_id="formsemestre_pvjury",
|
||||||
)
|
)
|
||||||
if fmt != "html":
|
if fmt != "html":
|
||||||
return tab.make_page(
|
return tab.make_page(
|
||||||
|
@ -312,6 +313,7 @@ def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
html_with_td_classes=True,
|
html_with_td_classes=True,
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
table_id="formsemestre_pvjury_counts",
|
||||||
).html()
|
).html()
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
|
|
|
@ -198,9 +198,9 @@ def formsemestre_recapcomplet(
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||||
}">Calcul automatique des décisions du jury</a>
|
}">Calcul automatique des décisions du jury</a>
|
||||||
</li>
|
</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)
|
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>
|
</li>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -236,6 +236,7 @@ def _results_by_category(
|
||||||
html_col_width="4em",
|
html_col_width="4em",
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
table_id=f"results_by_category-{category_name}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -350,6 +351,7 @@ def formsemestre_report_counts(
|
||||||
"statut",
|
"statut",
|
||||||
"annee_admission",
|
"annee_admission",
|
||||||
"type_admission",
|
"type_admission",
|
||||||
|
"boursier",
|
||||||
"boursier_prec",
|
"boursier_prec",
|
||||||
]
|
]
|
||||||
if jury_but_mode:
|
if jury_but_mode:
|
||||||
|
@ -695,19 +697,18 @@ def table_suivi_cohorte(
|
||||||
if statut:
|
if statut:
|
||||||
dbac += " statut: %s" % statut
|
dbac += " statut: %s" % statut
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
titles=titles,
|
caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
rows=L,
|
filename=scu.make_filename("cohorte " + sem["titreannee"]),
|
||||||
|
html_class="table_cohorte",
|
||||||
html_col_width="4em",
|
html_col_width="4em",
|
||||||
html_sortable=True,
|
html_sortable=True,
|
||||||
filename=scu.make_filename("cohorte " + sem["titreannee"]),
|
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||||
origin="Généré par %s le " % sco_version.SCONAME
|
|
||||||
+ scu.timedate_human_repr()
|
|
||||||
+ "",
|
|
||||||
caption="Suivi cohorte " + pp + sem["titreannee"] + dbac,
|
|
||||||
page_title="Suivi cohorte " + sem["titreannee"],
|
page_title="Suivi cohorte " + sem["titreannee"],
|
||||||
html_class="table_cohorte",
|
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
||||||
|
rows=L,
|
||||||
|
table_id="table_suivi_cohorte",
|
||||||
|
titles=titles,
|
||||||
)
|
)
|
||||||
# Explication: liste des semestres associés à chaque date
|
# Explication: liste des semestres associés à chaque date
|
||||||
if not P:
|
if not P:
|
||||||
|
@ -1304,6 +1305,7 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
|
||||||
"code_cursus": len(etuds),
|
"code_cursus": len(etuds),
|
||||||
},
|
},
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
|
table_id="table_suivi_cursus",
|
||||||
)
|
)
|
||||||
return tab
|
return tab
|
||||||
|
|
||||||
|
|
|
@ -87,15 +87,16 @@ def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
|
||||||
bacs.append("Total")
|
bacs.append("Total")
|
||||||
|
|
||||||
tab = GenTable(
|
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}",
|
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"
|
title = "Indicateurs suivi annuel BUT"
|
||||||
t = tab.make_page(
|
t = tab.make_page(
|
||||||
|
|
|
@ -378,9 +378,8 @@ class SemSet(dict):
|
||||||
|
|
||||||
def html_diagnostic(self):
|
def html_diagnostic(self):
|
||||||
"""Affichage de la partie Effectifs et Liste des étudiants
|
"""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 ""
|
return ""
|
||||||
|
|
||||||
|
@ -482,10 +481,9 @@ def semset_page(fmt="html"):
|
||||||
# (remplacé par n liens vers chacun des semestres)
|
# (remplacé par n liens vers chacun des semestres)
|
||||||
# s['_semtitles_str_target'] = s['_export_link_target']
|
# s['_semtitles_str_target'] = s['_export_link_target']
|
||||||
# Experimental:
|
# Experimental:
|
||||||
s[
|
s["_title_td_attrs"] = (
|
||||||
"_title_td_attrs"
|
'class="inplace_edit" data-url="edit_semset_set_title" id="%s"'
|
||||||
] = 'class="inplace_edit" data-url="edit_semset_set_title" id="%s"' % (
|
% (s["semset_id"])
|
||||||
s["semset_id"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
|
@ -513,6 +511,7 @@ def semset_page(fmt="html"):
|
||||||
html_class="table_leftalign",
|
html_class="table_leftalign",
|
||||||
filename="semsets",
|
filename="semsets",
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="table-semsets",
|
||||||
)
|
)
|
||||||
if fmt != "html":
|
if fmt != "html":
|
||||||
return tab.make_page(fmt=fmt)
|
return tab.make_page(fmt=fmt)
|
||||||
|
|
|
@ -115,7 +115,8 @@ def formsemestre_synchro_etuds(
|
||||||
url_for('notes.formsemestre_editwithmodules',
|
url_for('notes.formsemestre_editwithmodules',
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||||
}">Modifier ce semestre</a>)
|
}">Modifier ce semestre</a>)
|
||||||
"""
|
""",
|
||||||
|
safe=True,
|
||||||
)
|
)
|
||||||
footer = html_sco_header.sco_footer()
|
footer = html_sco_header.sco_footer()
|
||||||
base_url = url_for(
|
base_url = url_for(
|
||||||
|
|
|
@ -169,6 +169,7 @@ def evaluation_list_operations(evaluation_id):
|
||||||
preferences=sco_preferences.SemPreferences(
|
preferences=sco_preferences.SemPreferences(
|
||||||
evaluation.moduleimpl.formsemestre_id
|
evaluation.moduleimpl.formsemestre_id
|
||||||
),
|
),
|
||||||
|
table_id="evaluation_list_operations",
|
||||||
)
|
)
|
||||||
return tab.make_page()
|
return tab.make_page()
|
||||||
|
|
||||||
|
@ -241,6 +242,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
|
||||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||||
base_url="%s?formsemestre_id=%s" % (request.base_url, 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() + "",
|
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)
|
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),
|
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
|
pdf_link=False, # table is too wide to fit in a paper page => disable pdf
|
||||||
preferences=sco_preferences.SemPreferences(),
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
table_id="list-users",
|
||||||
)
|
)
|
||||||
|
|
||||||
return tab.make_page(fmt=fmt, with_html_headers=False)
|
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)
|
# ne pas changer (ou vous perdez le support)
|
||||||
SCO_DUMP_UP_URL = "https://scodoc.org/scodoc-installmgr/upload-dump"
|
SCO_DUMP_UP_URL = "https://scodoc.org/scodoc-installmgr/upload-dump"
|
||||||
SCO_UP2DATE = "https://scodoc.org/scodoc-installmgr/check_version"
|
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_ORG_TIMEOUT = 180 # contacts scodoc.org
|
||||||
SCO_EXT_TIMEOUT = 180 # appels à des ressources extérieures (siret, ...)
|
SCO_EXT_TIMEOUT = 180 # appels à des ressources extérieures (siret, ...)
|
||||||
SCO_TEST_API_TIMEOUT = 5 # pour tests unitaires API
|
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)
|
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"""
|
"""True si s peut être un code NIP: au moins 6 chiffres décimaux"""
|
||||||
if not s:
|
if not s:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -302,7 +302,6 @@
|
||||||
.rbtn {
|
.rbtn {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,9 +326,12 @@
|
||||||
background-image: url(../icons/absent.svg);
|
background-image: url(../icons/absent.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rbtn.aucun::before {
|
.rbtn.aucun {
|
||||||
background-image: url(../icons/aucun.svg);
|
background-image: url("../icons/delete.svg");
|
||||||
background-color: var(--color-defaut-dark);
|
background-size: calc(100% - 8px) calc(100% - 8px);
|
||||||
|
/* Adjust size to create "margin" */
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rbtn.retard::before {
|
.rbtn.retard::before {
|
||||||
|
@ -730,31 +732,11 @@ tr.row-justificatif.non_valide td.assi-type {
|
||||||
background-color: var(--color-defaut) !important;
|
background-color: var(--color-defaut) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color.est_just.sans_etat::before {
|
.color.invalide {
|
||||||
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;
|
|
||||||
background-color: var(--color-justi-invalide) !important;
|
background-color: var(--color-justi-invalide) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color.attente::before,
|
.color.attente {
|
||||||
.color.modifie::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 25%;
|
|
||||||
height: 100%;
|
|
||||||
right: 0;
|
|
||||||
background: repeating-linear-gradient(to bottom,
|
background: repeating-linear-gradient(to bottom,
|
||||||
var(--color-justi-attente-stripe) 0px,
|
var(--color-justi-attente-stripe) 0px,
|
||||||
var(--color-justi-attente-stripe) 4px,
|
var(--color-justi-attente-stripe) 4px,
|
||||||
|
@ -762,6 +744,10 @@ tr.row-justificatif.non_valide td.assi-type {
|
||||||
var(--color-justi-attente) 7px) !important;
|
var(--color-justi-attente) 7px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.color.est_just {
|
||||||
|
background-color: var(--color-justi) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#gtrcontent .pdp {
|
#gtrcontent .pdp {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
.day .dayline {
|
.jour .dayline {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: 75px;
|
|
||||||
background-color: #dedede;
|
background-color: #dedede;
|
||||||
border-radius: 15px;
|
border-radius: 8px;
|
||||||
padding: 5px;
|
padding: 5px 5px 15px 5px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 2px solid #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day:hover .dayline {
|
.jour:hover .dayline {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dayline .mini-timeline {
|
.dayline .mini-timeline {
|
||||||
margin-top: 10%;
|
margin-top: 10%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1085,18 +1085,35 @@ span.spanlink:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.trombi_box {
|
.trombi_box {
|
||||||
display: inline-block;
|
|
||||||
width: 110px;
|
|
||||||
vertical-align: top;
|
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
width: 140px;
|
||||||
|
/* Constant width for the box */
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* Ensures trombi-photo is above trombi_legend */
|
||||||
|
align-items: center;
|
||||||
|
/* Centers content horizontally */
|
||||||
}
|
}
|
||||||
|
|
||||||
span.trombi_legend {
|
.trombi-photo {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
/* Centers image horizontally within the photo container */
|
||||||
|
margin-bottom: 10px;
|
||||||
|
/* Adds some space between the photo and the legend */
|
||||||
}
|
}
|
||||||
|
|
||||||
span.trombi-photo {
|
.trombi-photo img {
|
||||||
|
width: auto;
|
||||||
|
/* Maintains aspect ratio */
|
||||||
|
height: 120px;
|
||||||
|
/* Sets the height to 90px */
|
||||||
|
max-width: 100%;
|
||||||
|
/* Ensures the image doesn't exceed the container's width */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* span.trombi_legend {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1106,7 +1123,9 @@ span.trombi_box a {
|
||||||
|
|
||||||
span.trombi_box a img {
|
span.trombi_box a img {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
height: 128px;
|
||||||
|
width: auto;
|
||||||
|
} */
|
||||||
|
|
||||||
.trombi_nom {
|
.trombi_nom {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -2096,8 +2115,9 @@ div.evaluation_titre {
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* visualisation poids évaluations */
|
/* visualisation poids évaluations */
|
||||||
.evaluation_poids {
|
.hinton_map {
|
||||||
height: 12px;
|
height: 12px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -2105,10 +2125,10 @@ div.evaluation_titre {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.evaluation_poids>div {
|
.hinton_map>div {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
height: 12px;
|
height: var(--size);
|
||||||
width: 12px;
|
width: var(--size);
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
border: 1px solid rgb(180, 180, 180);
|
border: 1px solid rgb(180, 180, 180);
|
||||||
|
@ -2116,9 +2136,9 @@ div.evaluation_titre {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.evaluation_poids>div>div {
|
.hinton_map>div>div {
|
||||||
height: var(--size);
|
height: var(--boxsize);
|
||||||
width: var(--size);
|
width: var(--boxsize);
|
||||||
background: #09c;
|
background: #09c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4831,7 +4851,9 @@ table.evaluations_recap th.titre {
|
||||||
}
|
}
|
||||||
|
|
||||||
table.evaluations_recap td.complete,
|
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;
|
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
|
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
|
||||||
*/
|
*/
|
||||||
async function async_post(path, data, success, errors) {
|
async function async_post(path, data, success, errors) {
|
||||||
console.log("async_post " + path);
|
// console.log("async_post " + path);
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(path, {
|
response = await fetch(path, {
|
||||||
|
@ -67,9 +67,15 @@ async function async_post(path, data, success, errors) {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
success(responseData);
|
success(responseData);
|
||||||
|
} else {
|
||||||
|
if (response.status == 404) {
|
||||||
|
response.json().then((data) => {
|
||||||
|
if (errors) errors(data);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Network response was not ok.");
|
throw new Error("Network response was not ok.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (errors) errors(error);
|
if (errors) errors(error);
|
||||||
|
@ -296,7 +302,13 @@ function creerLigneEtudiant(etud, index) {
|
||||||
// Création des boutons d'assiduités
|
// Création des boutons d'assiduités
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
} else if (currentAssiduite.type != "conflit") {
|
} 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");
|
const btn = document.createElement("input");
|
||||||
btn.type = "checkbox";
|
btn.type = "checkbox";
|
||||||
btn.value = abs;
|
btn.value = abs;
|
||||||
|
@ -395,7 +407,7 @@ async function creerTousLesEtudiants(etuds) {
|
||||||
* @returns {String}
|
* @returns {String}
|
||||||
*/
|
*/
|
||||||
async function getModuleImpl(assiduite) {
|
async function getModuleImpl(assiduite) {
|
||||||
if (assiduite == null) return "Pas de module";
|
if (assiduite == null) return "Module non spécifié";
|
||||||
const id = assiduite.moduleimpl_id;
|
const id = assiduite.moduleimpl_id;
|
||||||
|
|
||||||
if (id == null || id == undefined) {
|
if (id == null || id == undefined) {
|
||||||
|
@ -408,7 +420,7 @@ async function getModuleImpl(assiduite) {
|
||||||
? "Autre module (pas dans la liste)"
|
? "Autre module (pas dans la liste)"
|
||||||
: assiduite.external_data.module;
|
: assiduite.external_data.module;
|
||||||
} else {
|
} else {
|
||||||
return "Pas de module";
|
return "Module non spécifié";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,7 +437,7 @@ async function getModuleImpl(assiduite) {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ''}`;
|
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ""}`;
|
||||||
return moduleimpls[id];
|
return moduleimpls[id];
|
||||||
})
|
})
|
||||||
.catch((_) => {
|
.catch((_) => {
|
||||||
|
@ -531,12 +543,7 @@ async function MiseAJourLigneEtud(etud) {
|
||||||
|
|
||||||
async function actionAssiduite(etud, etat, type, assiduite = null) {
|
async function actionAssiduite(etud, etat, type, assiduite = null) {
|
||||||
const modimpl_id = $("#moduleimpl_select").val();
|
const modimpl_id = $("#moduleimpl_select").val();
|
||||||
if (
|
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
|
||||||
assiduite &&
|
|
||||||
assiduite.etat.toLowerCase() === etat &&
|
|
||||||
assiduite.moduleimpl_id == modimpl_id
|
|
||||||
)
|
|
||||||
type = "suppression";
|
|
||||||
|
|
||||||
const { deb, fin } = getPeriodAsDate();
|
const { deb, fin } = getPeriodAsDate();
|
||||||
|
|
||||||
|
@ -614,7 +621,10 @@ function erreurModuleImpl(message) {
|
||||||
|
|
||||||
openAlertModal("Sélection du module", content);
|
openAlertModal("Sélection du module", content);
|
||||||
}
|
}
|
||||||
if (message == "L'étudiant n'est pas inscrit au module") {
|
if (
|
||||||
|
message == "L'étudiant n'est pas inscrit au module" ||
|
||||||
|
message == "param 'moduleimpl_id': etud non inscrit"
|
||||||
|
) {
|
||||||
const HTML = `
|
const HTML = `
|
||||||
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
|
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
|
||||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||||
|
@ -642,6 +652,9 @@ function mettreToutLeMonde(etat, el = null) {
|
||||||
|
|
||||||
// Suppression des assiduités
|
// Suppression des assiduités
|
||||||
if (etat == "vide") {
|
if (etat == "vide") {
|
||||||
|
if (!confirm("Effacer tout les évènements correspondant à cette plage ?")) {
|
||||||
|
return; // annulation
|
||||||
|
}
|
||||||
const assiduites_id = lignesEtuds
|
const assiduites_id = lignesEtuds
|
||||||
.filter((e) => e.getAttribute("type") == "edition")
|
.filter((e) => e.getAttribute("type") == "edition")
|
||||||
.map((e) => Number(e.getAttribute("assiduite_id")));
|
.map((e) => Number(e.getAttribute("assiduite_id")));
|
||||||
|
@ -758,6 +771,7 @@ function envoiToastEtudiant(etat, etud) {
|
||||||
pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5));
|
pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO commenter toutes les fonctions js
|
||||||
function envoiToastTous(etat, count) {
|
function envoiToastTous(etat, count) {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
let etatAffiche = etat;
|
let etatAffiche = etat;
|
||||||
|
@ -797,13 +811,16 @@ function estJourTravail(jour, nonWorkdays) {
|
||||||
return !nonWorkdays.includes(d);
|
return !nonWorkdays.includes(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
function retourJourTravail(date) {
|
function retourJourTravail(date, anti = true) {
|
||||||
const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms
|
const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms
|
||||||
let jour = date;
|
let jour = date;
|
||||||
let compte = 0;
|
let compte = 0;
|
||||||
|
|
||||||
while (!estJourTravail(jour, nonWorkDays) && compte++ < 7) {
|
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;
|
return jour;
|
||||||
}
|
}
|
||||||
|
@ -813,9 +830,12 @@ function dateCouranteEstTravaillee() {
|
||||||
if (!estJourTravail(date, nonWorkDays)) {
|
if (!estJourTravail(date, nonWorkDays)) {
|
||||||
const nouvelleDate = retourJourTravail(date);
|
const nouvelleDate = retourJourTravail(date);
|
||||||
$("#date").datepicker("setDate", nouvelleDate);
|
$("#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(
|
const att = document.createTextNode(
|
||||||
`Le jour sélectionné (${Date.toFRA(
|
`${msg} (${Date.toFRA(
|
||||||
date.format("YYYY-MM-DD")
|
date.format("YYYY-MM-DD")
|
||||||
)}) n'est pas un jour travaillé.`
|
)}) n'est pas un jour travaillé.`
|
||||||
);
|
);
|
||||||
|
@ -836,6 +856,17 @@ function dateCouranteEstTravaillee() {
|
||||||
return true;
|
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
|
* Ajout de la visualisation des assiduités de la mini timeline
|
||||||
* @param {HTMLElement} el l'élément survollé
|
* @param {HTMLElement} el l'élément survollé
|
||||||
|
@ -875,6 +906,11 @@ function setupAssiduiteBubble(el, assiduite) {
|
||||||
actionsDiv.appendChild(infos);
|
actionsDiv.appendChild(infos);
|
||||||
bubble.appendChild(actionsDiv);
|
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");
|
const idDiv = document.createElement("div");
|
||||||
idDiv.className = "assiduite-id";
|
idDiv.className = "assiduite-id";
|
||||||
getModuleImpl(assiduite).then((modImpl) => {
|
getModuleImpl(assiduite).then((modImpl) => {
|
||||||
|
@ -882,26 +918,32 @@ function setupAssiduiteBubble(el, assiduite) {
|
||||||
});
|
});
|
||||||
bubble.appendChild(idDiv);
|
bubble.appendChild(idDiv);
|
||||||
|
|
||||||
const periodDivDeb = document.createElement("div");
|
// Affichage des dates
|
||||||
periodDivDeb.className = "assiduite-period";
|
// si les jours sont les mêmes, on affiche "jour hh:mm - hh:mm"
|
||||||
periodDivDeb.textContent = `${formatDateModal(assiduite.date_debut)}`;
|
// sinon on affiche "jour hh:mm - jour hh:mm"
|
||||||
bubble.appendChild(periodDivDeb);
|
const periodDiv = document.createElement("div");
|
||||||
const periodDivFin = document.createElement("div");
|
periodDiv.className = "assiduite-period";
|
||||||
periodDivFin.className = "assiduite-period";
|
const dateDeb = new Date(Date.removeUTC(assiduite.date_debut));
|
||||||
periodDivFin.textContent = `${formatDateModal(assiduite.date_fin)}`;
|
const dateFin = new Date(Date.removeUTC(assiduite.date_fin));
|
||||||
bubble.appendChild(periodDivFin);
|
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");
|
bubble.appendChild(periodDiv);
|
||||||
stateDiv.className = "assiduite-state";
|
|
||||||
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
|
|
||||||
bubble.appendChild(stateDiv);
|
|
||||||
|
|
||||||
const motifDiv = document.createElement("div");
|
const motifDiv = document.createElement("div");
|
||||||
stateDiv.className = "assiduite-why";
|
motifDiv.className = "assiduite-why";
|
||||||
const motif = ["", null, undefined].includes(assiduite.desc)
|
const motif = ["", null, undefined].includes(assiduite.desc)
|
||||||
? "Pas de motif"
|
? "Non spécifié"
|
||||||
: assiduite.desc.capitalize();
|
: assiduite.desc.capitalize();
|
||||||
stateDiv.textContent = `Motif: ${motif}`;
|
motifDiv.textContent = `Motif: ${motif}`;
|
||||||
bubble.appendChild(motifDiv);
|
bubble.appendChild(motifDiv);
|
||||||
|
|
||||||
const userIdDiv = document.createElement("div");
|
const userIdDiv = document.createElement("div");
|
||||||
|
|
|
@ -430,3 +430,23 @@ class Duration {
|
||||||
function hasTimeConflict(period, interval) {
|
function hasTimeConflict(period, interval) {
|
||||||
return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb);
|
return period.deb.isBefore(interval.fin) && period.fin.isAfter(interval.deb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fonction auxiliaire pour obtenir le numéro de semaine ISO d'une date donnée
|
||||||
|
function getISOWeek(date) {
|
||||||
|
const target = new Date(date.valueOf());
|
||||||
|
const dayNr = (date.getUTCDay() + 6) % 7;
|
||||||
|
target.setUTCDate(target.getUTCDate() - dayNr + 3);
|
||||||
|
const firstThursday = target.valueOf();
|
||||||
|
target.setUTCMonth(0, 1);
|
||||||
|
if (target.getUTCDay() !== 4) {
|
||||||
|
target.setUTCMonth(0, 1 + ((4 - target.getUTCDay() + 7) % 7));
|
||||||
|
}
|
||||||
|
return 1 + Math.ceil((firstThursday - target) / 604800000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction auxiliaire pour obtenir le nombre de semaines ISO dans une année donnée
|
||||||
|
function getISOWeeksInYear(year) {
|
||||||
|
const date = new Date(year, 11, 31);
|
||||||
|
const week = getISOWeek(date);
|
||||||
|
return week === 1 ? getISOWeek(new Date(year, 11, 24)) : week;
|
||||||
|
}
|
||||||
|
|
|
@ -155,18 +155,9 @@ function get_query_args() {
|
||||||
|
|
||||||
// Tables (gen_tables)
|
// Tables (gen_tables)
|
||||||
$(function () {
|
$(function () {
|
||||||
if ($("table.gt_table").length > 0) {
|
if ($("table.gt_table, table.gt_table_searchable").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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var table_options = {
|
var table_options = {
|
||||||
paging: false,
|
paging: false,
|
||||||
searching: false,
|
searching: false,
|
||||||
|
@ -178,20 +169,46 @@ $(function () {
|
||||||
},
|
},
|
||||||
orderCellsTop: true, // cellules ligne 1 pour tri
|
orderCellsTop: true, // cellules ligne 1 pour tri
|
||||||
aaSorting: [], // Prevent initial sorting
|
aaSorting: [], // Prevent initial sorting
|
||||||
order: order_info,
|
order: "",
|
||||||
drawCallback: function (settings) {
|
drawCallback: function (settings) {
|
||||||
// permet de conserver l'ordre de tri des colonnes
|
// permet de conserver l'ordre de tri des colonnes
|
||||||
let table = $("table.gt_table").DataTable();
|
let currentTable = $(settings.nTable);
|
||||||
let order_info = JSON.stringify(table.order());
|
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);
|
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_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)
|
// Show tags (readonly)
|
||||||
function readOnlyTags(nodes) {
|
function readOnlyTags(nodes) {
|
||||||
// nodes are textareas, hide them and create a span showing tags
|
// 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
|
return true; // Aucune modification, pas d'enregistrement mais on continue normalement
|
||||||
}
|
}
|
||||||
obj.classList.add("sco_wait");
|
obj.classList.add("sco_wait");
|
||||||
// DEBUG
|
|
||||||
// console.log(`
|
|
||||||
// data : ${value},
|
|
||||||
// id: ${obj.dataset.oid}
|
|
||||||
// `);
|
|
||||||
|
|
||||||
$.post(
|
$.post(
|
||||||
this.save_url,
|
this.save_url,
|
||||||
|
|
|
@ -11,8 +11,8 @@ h.id="btc";
|
||||||
h.setAttribute("id","btc");
|
h.setAttribute("id","btc");
|
||||||
h.style.position="absolute";
|
h.style.position="absolute";
|
||||||
document.getElementsByTagName("body")[0].appendChild(h);
|
document.getElementsByTagName("body")[0].appendChild(h);
|
||||||
if(id==null) links=document.getElementsByTagName("a");
|
if(id==null) links=document.querySelectorAll("a, [data-tooltip]"); // was document.getElementsByTagName("a")
|
||||||
else links=document.getElementById(id).getElementsByTagName("a");
|
else links=document.getElementById(id).querySelectorAll("a, [data-tooltip]");// was document.getElementById(id).getElementsByTagName("a")
|
||||||
for(i=0;i<links.length;i++){
|
for(i=0;i<links.length;i++){
|
||||||
Prepare(links[i]);
|
Prepare(links[i]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
"""Liste simple d'étudiants
|
"""Liste simple d'étudiants"""
|
||||||
"""
|
|
||||||
import datetime
|
import datetime
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
from app import log
|
from app import log
|
||||||
|
@ -140,6 +140,13 @@ class RowAssi(tb.Row):
|
||||||
)
|
)
|
||||||
stats = self._get_etud_stats(etud)
|
stats = self._get_etud_stats(etud)
|
||||||
for key, value in stats.items():
|
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")
|
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
|
||||||
if key != "present":
|
if key != "present":
|
||||||
self.add_cell(
|
self.add_cell(
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
<h1>Traitement de l'assiduité</h1>
|
<h1>Traitement de l'assiduité</h1>
|
||||||
<p class="help">
|
<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).
|
le semestre concerné (saisie par jour ou saisie différée).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -86,9 +86,6 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
||||||
|
|
||||||
<div class="scobox">
|
<div class="scobox">
|
||||||
<section class="nonvalide">
|
<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 }}
|
{{tableau | safe }}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -99,6 +96,9 @@ Bilan assiduité de {{sco.etud.nomprenom}}
|
||||||
département)</p>
|
département)</p>
|
||||||
<p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
|
<p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
|
||||||
appuyer sur le bouton "Actualiser"</p>
|
appuyer sur le bouton "Actualiser"</p>
|
||||||
|
|
||||||
|
{% include "assiduites/explication_etats_justifs.j2" %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,49 +23,8 @@ Calendrier de l'assiduité
|
||||||
for="mode_demi">mode demi journée</label>
|
for="mode_demi">mode demi journée</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendrier">
|
<div class="cal">
|
||||||
{% for mois,jours in calendrier.items() %}
|
{{calendrier|safe}}
|
||||||
<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>Assiduité du</span>
|
|
||||||
<br>
|
|
||||||
<span>{{jour.get_date()}}</span>
|
|
||||||
{{jour.generate_minitimeline() | safe}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="annee">
|
<div class="annee">
|
||||||
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||||
année: </span>
|
année: </span>
|
||||||
|
@ -77,36 +36,7 @@ Calendrier de l'assiduité
|
||||||
|
|
||||||
<div class="help">
|
<div class="help">
|
||||||
<h3>Calendrier</h3>
|
<h3>Calendrier</h3>
|
||||||
<p>Code couleur</p>
|
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="couleurs print">
|
<ul class="couleurs print">
|
||||||
<li><span title="Vert" class="present demo"></span> présence
|
<li><span title="Vert" class="present demo"></span> présence
|
||||||
|
@ -158,57 +88,34 @@ Calendrier de l'assiduité
|
||||||
|
|
||||||
.calendrier {
|
.calendrier {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: start;
|
justify-content: center;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.month h3 {
|
.assi_case {
|
||||||
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;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: start;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo.invalide {
|
.assi_case > span {
|
||||||
background-color: var(--color-justi-invalide) !important;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo.attente {
|
.assi_case>span:last-of-type {
|
||||||
background: repeating-linear-gradient(to bottom,
|
border-left: #d5d5d5 solid 1px;
|
||||||
var(--color-justi-attente-stripe) 0px,
|
}
|
||||||
var(--color-justi-attente-stripe) 4px,
|
.assi_case>span:first-of-type {
|
||||||
var(--color-justi-attente) 4px,
|
border-right: #d5d5d5 solid 1px;
|
||||||
var(--color-justi-attente) 7px) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo.est_just {
|
.dayline{
|
||||||
background-color: var(--color-justi) !important;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.demi .day.nonwork>span {
|
|
||||||
flex: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demi .day {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
|
|
||||||
.couleurs.print {
|
.couleurs.print {
|
||||||
|
@ -338,7 +245,5 @@ Calendrier de l'assiduité
|
||||||
window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
|
window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock app_content %}
|
{% 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');
|
const assi_btns = document.createElement('div');
|
||||||
assi_btns.classList.add('assi-btns');
|
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");
|
const cbox = document.createElement("input");
|
||||||
cbox.type = "checkbox";
|
cbox.type = "checkbox";
|
||||||
cbox.value = value;
|
cbox.value = value;
|
||||||
|
@ -499,6 +504,8 @@ const moduleimpls = new Map();
|
||||||
const inscriptionsModules = new Map();
|
const inscriptionsModules = new Map();
|
||||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||||
|
|
||||||
|
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
||||||
|
|
||||||
// Vérification du forçage de module
|
// Vérification du forçage de module
|
||||||
window.forceModule = "{{ forcer_module }}" == "True";
|
window.forceModule = "{{ forcer_module }}" == "True";
|
||||||
if (window.forceModule) {
|
if (window.forceModule) {
|
||||||
|
@ -518,12 +525,29 @@ if (window.forceModule) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultPlage = {{ nouv_plage | safe}} || [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fonction exécutée au lancement de la page
|
* Fonction exécutée au lancement de la page
|
||||||
* - On affiche ou non les photos des étudiants
|
* - On affiche ou non les photos des étudiants
|
||||||
* - On vérifie si la date est un jour travaillé
|
* - On vérifie si la date est un jour travaillé
|
||||||
*/
|
*/
|
||||||
async function main() {
|
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";
|
const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
|
||||||
afficherPDP(checked);
|
afficherPDP(checked);
|
||||||
$("#date").on("change", async function (d) {
|
$("#date").on("change", async function (d) {
|
||||||
|
@ -532,7 +556,7 @@ async function main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
window.addEventListener("load", main);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -546,6 +570,36 @@ main();
|
||||||
|
|
||||||
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
|
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
|
||||||
|
|
||||||
|
<div class="ue_warning warning">
|
||||||
|
Attention, cette page va prochainement être supprimée, car il est plus facile d'utiliser
|
||||||
|
<ul>
|
||||||
|
la page
|
||||||
|
<li><a class="stdlink" href="{{
|
||||||
|
url_for('assiduites.signal_assiduites_group',
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
group_ids=group_ids)
|
||||||
|
}}">
|
||||||
|
saisie de l'assiduité</a> pour saisir à une seule date quelconque
|
||||||
|
</li>
|
||||||
|
<li>ou <a class="stdlink" href="{{
|
||||||
|
url_for('assiduites.signal_assiduites_hebdo',
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
group_ids=group_ids,
|
||||||
|
)
|
||||||
|
}}">saisie hebdomadaire</a> pour saisir sur une semaine.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>Ci-dessous le formulaire vous permettant de saisir plusieurs plages à la fois,
|
||||||
|
qui va bientôt être retiré.
|
||||||
|
</p>
|
||||||
|
<p>N'hésitez pas à commenter sur le <a href="{{scu.SCO_DISCORD_ASSISTANCE}}">salon Discord</a>
|
||||||
|
si vous avez d'autres besoins.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="fix">
|
<div id="fix">
|
||||||
<!-- Nouvelle Plage
|
<!-- Nouvelle Plage
|
||||||
|
@ -600,7 +654,9 @@ main();
|
||||||
Intialiser les étudiants comme :
|
Intialiser les étudiants comme :
|
||||||
<select name="etatDef" id="etatDef">
|
<select name="etatDef" id="etatDef">
|
||||||
<option value="">-</option>
|
<option value="">-</option>
|
||||||
|
{% if not non_present %}
|
||||||
<option value="present">présents</option>
|
<option value="present">présents</option>
|
||||||
|
{% endif %}
|
||||||
<option value="retard">en retard</option>
|
<option value="retard">en retard</option>
|
||||||
<option value="absent">absents</option>
|
<option value="absent">absents</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
const readOnly = {{ readonly }};
|
const readOnly = {{ readonly }};
|
||||||
|
|
||||||
window.forceModule = "{{ forcer_module }}" == "True"
|
window.forceModule = "{{ forcer_module }}" == "True"
|
||||||
|
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
||||||
|
|
||||||
const etudsDefDem = {{ defdem | safe }}
|
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}}/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/assiduites.css">
|
||||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.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 %}
|
{% endblock styles %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,6 +131,10 @@
|
||||||
{{ minitimeline|safe }}
|
{{ minitimeline|safe }}
|
||||||
<section id="content">
|
<section id="content">
|
||||||
|
|
||||||
|
<a id="retour-haut" href="#gtrcontent">
|
||||||
|
⬆️
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="no-display">
|
<div class="no-display">
|
||||||
<span class="formsemestre_id">{{formsemestre_id}}</span>
|
<span class="formsemestre_id">{{formsemestre_id}}</span>
|
||||||
<span id="formsemestre_date_debut">{{formsemestre_date_debut}}</span>
|
<span id="formsemestre_date_debut">{{formsemestre_date_debut}}</span>
|
||||||
|
@ -130,12 +153,22 @@
|
||||||
<div class="infos">
|
<div class="infos">
|
||||||
<div class="infos-button">Groupes : {{grp|safe}}</div>
|
<div class="infos-button">Groupes : {{grp|safe}}</div>
|
||||||
<div>
|
<div>
|
||||||
|
<button class="btn_date" onclick="jourSuivant(true)">
|
||||||
|
⇤
|
||||||
|
</button>
|
||||||
<input type="text" name="date" id="date" class="datepicker" value="{{date}}">
|
<input type="text" name="date" id="date" class="datepicker" value="{{date}}">
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn_date" onclick="jourSuivant(false)">
|
||||||
|
⇥
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div style="display: {{'none' if readonly == 'true' else 'block'}};">
|
<div style="display: {{'none' if readonly == 'true' else 'block'}};">
|
||||||
{{timeline|safe}}
|
{{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>
|
</div>
|
||||||
|
|
||||||
{% if readonly == "false" %}
|
{% if readonly == "false" %}
|
||||||
|
@ -159,14 +192,16 @@
|
||||||
<div class="mass-selection">
|
<div class="mass-selection">
|
||||||
<span>Mettre tout le monde :</span>
|
<span>Mettre tout le monde :</span>
|
||||||
<fieldset class="btns_field mass">
|
<fieldset class="btns_field mass">
|
||||||
|
{% if not non_present %}
|
||||||
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_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"
|
<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"
|
<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"
|
<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>
|
</fieldset>
|
||||||
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
|
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
|
||||||
</div>
|
</div>
|
||||||
|
@ -178,6 +213,11 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="help">
|
||||||
|
<h3>Calendrier</h3>
|
||||||
|
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "assiduites/widgets/toast.j2" %}
|
{% include "assiduites/widgets/toast.j2" %}
|
||||||
{% include "assiduites/widgets/alert.j2" %}
|
{% include "assiduites/widgets/alert.j2" %}
|
||||||
{% include "assiduites/widgets/prompt.j2" %}
|
{% include "assiduites/widgets/prompt.j2" %}
|
||||||
|
|
899
app/templates/assiduites/pages/signal_assiduites_hebdo.j2
Normal file
899
app/templates/assiduites/pages/signal_assiduites_hebdo.j2
Normal file
|
@ -0,0 +1,899 @@
|
||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||||
|
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rbtn::before {
|
||||||
|
--size: 1.5em;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-timepicker-container,
|
||||||
|
#ui-datepicker-div {
|
||||||
|
z-index: 5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#new_periode,
|
||||||
|
#actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: fit-content;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#actions {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#actions label {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fix {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1em;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fix>.box {
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timepicker {
|
||||||
|
width: 5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#moduleimpl_select {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1600px;
|
||||||
|
position: relative;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.premier th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.second th {
|
||||||
|
position: sticky;
|
||||||
|
top: 38px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sticky-col {
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rbtn:not(:checked)::before {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grayed {
|
||||||
|
filter: brightness(0.5);
|
||||||
|
}
|
||||||
|
.conflit {
|
||||||
|
background-color: var(--color-conflit);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflit_calendar{
|
||||||
|
font-size: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.timePicker-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timePicker-modal.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timePicker-modal-content {
|
||||||
|
background-color: white;
|
||||||
|
margin: 15% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 300px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timePicker-close {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timePicker-close:hover,
|
||||||
|
.timePicker-close:focus {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-picker-container {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmButton {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmButton:hover {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.etudinfo{
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock styles %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||||
|
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||||
|
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||||
|
{% include "sco_timepicker.j2" %}
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const readonly = "{{readonly | safe}}" == "True";
|
||||||
|
const non_present = "{{non_present | safe}}" == "True";
|
||||||
|
|
||||||
|
const etuds = [
|
||||||
|
{% for etud in etudiants %}
|
||||||
|
{
|
||||||
|
id: {{etud.etudid}},
|
||||||
|
nom: "{{etud.nom}}",
|
||||||
|
prenom: "{{etud.prenom}}"
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
|
||||||
|
let days = [
|
||||||
|
{% for jour in hebdo_jours %}
|
||||||
|
{
|
||||||
|
date : new Date(Date.fromFRA("{{jour[1][1]}}")),
|
||||||
|
visible : "{{not jour[0]}}" == "True",
|
||||||
|
nom : "{{jour[1][0]}}",
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
] // [0]=Lundi ... [6]=Dimanche -> à 00h00
|
||||||
|
|
||||||
|
//Une fonction d'action quand un bouton est cliqué
|
||||||
|
// 3 possibilités :
|
||||||
|
// - assiduite_id = null -> créer nv assi avec état du bouton
|
||||||
|
// - assiduite_id non null et bouton coché == etat assi -> suppression de l'assiduité
|
||||||
|
// - assiduite_id non null et bouton coché != etat assi -> modification de l'assiduité
|
||||||
|
async function actionButton(btn, same = false) {
|
||||||
|
let td = btn.parentElement;
|
||||||
|
let tr = td.parentElement;
|
||||||
|
let etudid = tr.getAttribute("etudid");
|
||||||
|
let etud = etuds.find((etud) => etud.id == etudid);
|
||||||
|
let etat = btn.value;
|
||||||
|
let assiduite_id = td.getAttribute("assiduite_id");
|
||||||
|
let dayInfo = [td.getAttribute("day"), td.getAttribute("time")]// [0]=[0..6] [1]=am/pm
|
||||||
|
let day = days[dayInfo[0]].date;
|
||||||
|
dayInfo[1] = dayInfo[1] == "am" ? "matin" : "apresmidi";
|
||||||
|
let deb = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].debut);
|
||||||
|
let fin = new Date(day.format('YYYY-MM-DD') + "T" + temps[dayInfo[1]].fin);
|
||||||
|
|
||||||
|
const assi = {
|
||||||
|
etudid: etudid,
|
||||||
|
etat: etat,
|
||||||
|
moduleimpl_id: document.getElementById("moduleimpl_select").value,
|
||||||
|
date_debut: deb.toFakeIso(),
|
||||||
|
date_fin: fin.toFakeIso(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelEvent = false;
|
||||||
|
|
||||||
|
if (assiduite_id != "") {
|
||||||
|
if (same) {
|
||||||
|
// Suppression
|
||||||
|
await async_post(
|
||||||
|
`../../api/assiduite/delete`,
|
||||||
|
[assiduite_id],
|
||||||
|
(data) => {
|
||||||
|
if (data.success.length > 0) {
|
||||||
|
envoiToastEtudiant("remove", etud);
|
||||||
|
td.setAttribute("assiduite_id", "");
|
||||||
|
} else {
|
||||||
|
console.error(data.errors["0"].message);
|
||||||
|
cancelEvent = true;
|
||||||
|
erreurModuleImpl(data.errors["0"].message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error("Erreur lors de la suppression de l'assiduité", error);
|
||||||
|
cancelEvent = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Modification
|
||||||
|
await async_post(
|
||||||
|
`../../api/assiduite/${assiduite_id}/edit`,
|
||||||
|
assi,
|
||||||
|
(data) => {
|
||||||
|
envoiToastEtudiant(etat, etud);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error("Erreur lors de la modification de l'assiduité", error);
|
||||||
|
cancelEvent = true;
|
||||||
|
erreurModuleImpl(error.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Création
|
||||||
|
await async_post(
|
||||||
|
`../../api/assiduite/${etud.id}/create`,
|
||||||
|
[assi],
|
||||||
|
(data) => {
|
||||||
|
if (data.success.length > 0) {
|
||||||
|
envoiToastEtudiant(etat, etud);
|
||||||
|
//mise à jour de l'assiduité_id dans le td
|
||||||
|
td.setAttribute("assiduite_id", data.success["0"].message.assiduite_id);
|
||||||
|
} else {
|
||||||
|
console.error(data.errors["0"].message);
|
||||||
|
erreurModuleImpl(data.errors["0"].message);
|
||||||
|
cancelEvent = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error("Erreur lors de la création de l'assiduité", error);
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cancelEvent;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recupAssiduitesHebdo(callback) {
|
||||||
|
const etudIds = etuds.map((etud) => etud.id).join(",");
|
||||||
|
const date_debut = days[0].date.startOf("day").format("YYYY-MM-DDTHH:mm");
|
||||||
|
const date_fin = days[6].date.endOf("day").format("YYYY-MM-DDTHH:mm");
|
||||||
|
|
||||||
|
url =
|
||||||
|
`../../api/assiduites/group/query?date_debut=${date_debut}` +
|
||||||
|
`&date_fin=${date_fin}&etudids=${etudIds}&with_justifs`;
|
||||||
|
|
||||||
|
await fetch(url)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
let assiduites = []
|
||||||
|
Object.keys(data).forEach((etudid) => {
|
||||||
|
assiduites.push(...data[etudid]);
|
||||||
|
});
|
||||||
|
callback(assiduites);
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
console.error(
|
||||||
|
"There has been a problem with your fetch operation:",
|
||||||
|
error
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateTable(assiduites) {
|
||||||
|
|
||||||
|
const img_conflit = `
|
||||||
|
<a
|
||||||
|
class="conflit_calendar"
|
||||||
|
title="Des assiduités existent déjà pour cette période. Cliquez ici pour voir le calendrier de l'assiduité de l'étudiant"
|
||||||
|
data-tooltip
|
||||||
|
target="_blank"
|
||||||
|
>📅</a>`
|
||||||
|
|
||||||
|
// Suppression existant
|
||||||
|
document.querySelectorAll("td.btns").forEach((el) => {
|
||||||
|
el.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < days.length; i++) {
|
||||||
|
let day = days[i].date;
|
||||||
|
|
||||||
|
let morningPeriod = {
|
||||||
|
deb: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.debut),
|
||||||
|
fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.matin.fin),
|
||||||
|
}
|
||||||
|
let afternoonPeriod = {
|
||||||
|
deb: (new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.debut)),
|
||||||
|
fin: new Date(day.format('YYYY-MM-DD') + "T" + temps.apresmidi.fin),
|
||||||
|
}
|
||||||
|
const assiduitesByDay = {
|
||||||
|
matin: assiduites.filter((assi) => {
|
||||||
|
const period = {
|
||||||
|
deb: new Date(assi.date_debut),
|
||||||
|
fin: new Date(assi.date_fin)
|
||||||
|
}
|
||||||
|
return hasTimeConflict(period, morningPeriod);
|
||||||
|
}),
|
||||||
|
apresmidi: assiduites.filter((assi) => {
|
||||||
|
const period = {
|
||||||
|
deb: new Date(assi.date_debut),
|
||||||
|
fin: new Date(assi.date_fin)
|
||||||
|
}
|
||||||
|
return hasTimeConflict(period, afternoonPeriod);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Récupération des tr étudiants
|
||||||
|
let trs = document.querySelectorAll("tr[etudid]");
|
||||||
|
|
||||||
|
trs.forEach((tr) => {
|
||||||
|
let etudid = tr.getAttribute("etudid");
|
||||||
|
|
||||||
|
if (!days[i].visible && i >= 5) {
|
||||||
|
return;
|
||||||
|
} else if (!days[i].visible) {
|
||||||
|
tr.insertAdjacentHTML("beforeend", "<td class='grayed btns' colspan='2'></td>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let etudAssiMorning = assiduitesByDay.matin.filter((a) => {
|
||||||
|
return a.etudid == etudid;
|
||||||
|
});
|
||||||
|
let etudAssiAfternoon = assiduitesByDay.apresmidi.filter((a) => {
|
||||||
|
return a.etudid == etudid;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Créations des boutons
|
||||||
|
// matin
|
||||||
|
let tdMatin = document.createElement("td");
|
||||||
|
tdMatin.classList.add("btns");
|
||||||
|
tdMatin.setAttribute("day", i);
|
||||||
|
tdMatin.setAttribute("time", "am");
|
||||||
|
|
||||||
|
tr.appendChild(tdMatin);
|
||||||
|
|
||||||
|
// après-midi
|
||||||
|
let tdApresmidi = document.createElement("td");
|
||||||
|
tdApresmidi.classList.add("btns");
|
||||||
|
tdApresmidi.setAttribute("day", i);
|
||||||
|
tdApresmidi.setAttribute("time", "pm");
|
||||||
|
tr.appendChild(tdApresmidi);
|
||||||
|
|
||||||
|
|
||||||
|
// Peuplement des boutons en fonction des assiduités
|
||||||
|
let boutons = `
|
||||||
|
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
|
||||||
|
class="rbtn retard" value="retard">
|
||||||
|
<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
|
||||||
|
class="rbtn absent" value="absent">
|
||||||
|
`
|
||||||
|
|
||||||
|
if (!non_present) {
|
||||||
|
boutons = `<input type="checkbox" name="matin-${etudid}" id="matin-${etudid}"
|
||||||
|
class="rbtn present" value="present">`+boutons;
|
||||||
|
}
|
||||||
|
|
||||||
|
// matin
|
||||||
|
tdMatin.innerHTML = boutons
|
||||||
|
tdMatin.setAttribute("assiduite_id", "")
|
||||||
|
if (etudAssiMorning.length != 0) {
|
||||||
|
let assi = etudAssiMorning[0];
|
||||||
|
const deb = new Date(assi.date_debut);
|
||||||
|
const fin = new Date(assi.date_fin);
|
||||||
|
|
||||||
|
// si dates == periode -> cocher bouton correspondant
|
||||||
|
// Sinon supprimer boutons et mettre case "rouge" + tooltip
|
||||||
|
|
||||||
|
if (deb.isSame(morningPeriod.deb, "minutes") && fin.isSame(morningPeriod.fin, "minutes")) {
|
||||||
|
let etat = assi.etat.toLowerCase();
|
||||||
|
const input = tdMatin.querySelector(`[value="${etat}"]`)
|
||||||
|
if (input) {
|
||||||
|
input.checked = true;
|
||||||
|
}
|
||||||
|
tdMatin.setAttribute("assiduite_id", assi.assiduite_id);
|
||||||
|
let saisie = new Date(assi.entry_date).format("DD/MM/Y HH:mm");
|
||||||
|
saisie = saisie.split(" ").join(" à ");
|
||||||
|
let text = `noté ${etat} le ${saisie} par ${assi.user_nom_complet}`;
|
||||||
|
tdMatin.setAttribute("title", text);
|
||||||
|
tdMatin.setAttribute("data-tooltip", "");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
tdMatin.innerHTML = img_conflit;
|
||||||
|
tdMatin.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`;
|
||||||
|
tdMatin.classList.add("conflit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// après-midi
|
||||||
|
tdApresmidi.innerHTML = boutons
|
||||||
|
tdApresmidi.setAttribute("assiduite_id", "")
|
||||||
|
if (etudAssiAfternoon.length != 0) {
|
||||||
|
let assi = etudAssiAfternoon[0];
|
||||||
|
const deb = new Date(assi.date_debut);
|
||||||
|
const fin = new Date(assi.date_fin);
|
||||||
|
|
||||||
|
// si dates == periode -> cocher bouton correspondant
|
||||||
|
// Sinon supprimer boutons et mettre case "rouge" + tooltip
|
||||||
|
|
||||||
|
if (deb.isSame(afternoonPeriod.deb, "minutes") && fin.isSame(afternoonPeriod.fin, "minutes")) {
|
||||||
|
let etat = assi.etat.toLowerCase();
|
||||||
|
const input = tdApresmidi.querySelector(`[value="${etat}"]`)
|
||||||
|
if (input) {
|
||||||
|
input.checked = true;
|
||||||
|
}
|
||||||
|
tdApresmidi.setAttribute("assiduite_id", assi.assiduite_id);
|
||||||
|
|
||||||
|
let saisie = new Date(assi.entry_date).format("DD/MM/Y HH:mm");
|
||||||
|
saisie = saisie.split(" ").join(" à ");
|
||||||
|
let text = `noté ${etat} le ${saisie} par ${assi.user_nom_complet}`;
|
||||||
|
tdApresmidi.setAttribute("title", text);
|
||||||
|
tdApresmidi.setAttribute("data-tooltip", "");
|
||||||
|
} else {
|
||||||
|
tdApresmidi.innerHTML = img_conflit;
|
||||||
|
tdApresmidi.querySelector(".conflit_calendar").href = `calendrier_assi_etud?etudid=${etudid}`;
|
||||||
|
tdApresmidi.classList.add("conflit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("td .rbtn").forEach((el) => {
|
||||||
|
el.addEventListener("click", async (e) => {
|
||||||
|
|
||||||
|
if (readonly) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = e.target;
|
||||||
|
let parent = target.parentElement;
|
||||||
|
|
||||||
|
let isCancelled = await actionButton(target, !target.checked);
|
||||||
|
if (isCancelled) {
|
||||||
|
e.preventDefault();
|
||||||
|
target.checked = !target.checked;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputs = parent.querySelectorAll(".rbtn");
|
||||||
|
inputs.forEach((input) => {
|
||||||
|
if (input != target) {
|
||||||
|
input.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
enableTooltips("table");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Une fonction pour changer de semaine (précédente ou suivante)
|
||||||
|
// fait juste un location.href avec les bons paramètres
|
||||||
|
function changeWeek(prev = false) {
|
||||||
|
const currentUrl = new URL(window.location.href); // Récupère l'URL actuelle
|
||||||
|
const params = new URLSearchParams(currentUrl.search); // Récupère les paramètres de l'URL
|
||||||
|
let currentWeekParam = params.get('week');
|
||||||
|
|
||||||
|
// Extraire l'année et le numéro de semaine du paramètre de la semaine actuelle
|
||||||
|
const [year, week] = currentWeekParam.split('-W').map(Number);
|
||||||
|
|
||||||
|
// Calculer la nouvelle semaine et l'année
|
||||||
|
let newYear = year;
|
||||||
|
let newWeek = week + (prev ? -1 : 1);
|
||||||
|
|
||||||
|
if (newWeek < 1) {
|
||||||
|
newYear -= 1; // Passer à l'année précédente
|
||||||
|
newWeek = getISOWeeksInYear(newYear); // Dernière semaine de l'année précédente
|
||||||
|
} else if (newWeek > getISOWeeksInYear(newYear)) {
|
||||||
|
newYear += 1; // Passer à l'année suivante
|
||||||
|
newWeek = 1; // Première semaine de l'année suivante
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formater le nouveau paramètre de semaine
|
||||||
|
const newWeekParam = `${newYear}-W${String(newWeek).padStart(2, '0')}`;
|
||||||
|
params.set('week', newWeekParam); // Mettre à jour le paramètre 'week'
|
||||||
|
currentUrl.search = params.toString(); // Mettre à jour les paramètres de l'URL
|
||||||
|
window.location.href = currentUrl.toString(); // Rediriger vers la nouvelle URL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Une fonction pour gérer le bouton "tout le monde présent"
|
||||||
|
// coche tous les boutons de la colonne
|
||||||
|
function allPresent(day, time) {
|
||||||
|
// Version naive : coche tous les boutons de la colonne
|
||||||
|
// TODO - Optimiser avec une seule requête API
|
||||||
|
let tds = document.querySelectorAll(`td[day="${day}"][time="${time}"]`);
|
||||||
|
const real_time = time == "am" ? "matin" : "apresmidi";
|
||||||
|
const assi = {
|
||||||
|
etat: "present",
|
||||||
|
moduleimpl_id: document.getElementById("moduleimpl_select").value,
|
||||||
|
date_debut: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].debut).toFakeIso(),
|
||||||
|
date_fin: new Date(days[day].date.format('YYYY-MM-DD') + "T" + temps[real_time].fin).toFakeIso(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let toCreate = []; // [{etudid:<int>}]
|
||||||
|
let toEdit = [];// [{etudid:<int>, assiduite_id:<int>}]
|
||||||
|
|
||||||
|
tds.forEach((td) => {
|
||||||
|
// on ne touche pas aux conflits
|
||||||
|
if (td.classList.contains("conflit")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tr = td.parentElement;
|
||||||
|
const etudid = Number(tr.getAttribute("etudid"));
|
||||||
|
|
||||||
|
const assiduite_id = td.getAttribute("assiduite_id");
|
||||||
|
if (assiduite_id == "") {
|
||||||
|
toCreate.push({ etudid: etudid });
|
||||||
|
} else {
|
||||||
|
toEdit.push({ etudid: etudid, assiduite_id: Number(assiduite_id) });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Création
|
||||||
|
toCreate = toCreate.map((el) => {
|
||||||
|
return {
|
||||||
|
...assi,
|
||||||
|
etudid: el.etudid,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modification
|
||||||
|
toEdit = toEdit.map((el) => {
|
||||||
|
return {
|
||||||
|
...assi,
|
||||||
|
etudid: el.etudid,
|
||||||
|
assiduite_id: el.assiduite_id,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Appel API
|
||||||
|
let counts = {
|
||||||
|
create: toCreate.length,
|
||||||
|
edit: toEdit.length
|
||||||
|
}
|
||||||
|
const promiseCreate = async_post(
|
||||||
|
`../../api/assiduites/create`,
|
||||||
|
toCreate,
|
||||||
|
async (data) => {
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
console.error(data.errors);
|
||||||
|
data.errors.forEach((err) => {
|
||||||
|
let obj = toCreate[err.indice];
|
||||||
|
let etu = etuds.find((el) => el.id == obj.etudid);
|
||||||
|
|
||||||
|
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
|
||||||
|
const toast = generateToast(text, "var(--color-error)", 10);
|
||||||
|
pushToast(toast);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
counts.create = data.success.length;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error("Erreur lors de la création de l'assiduité", error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const promiseEdit = async_post(
|
||||||
|
`../../api/assiduites/edit`,
|
||||||
|
toEdit,
|
||||||
|
async (data) => {
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
console.error(data.errors);
|
||||||
|
data.errors.forEach((err) => {
|
||||||
|
let obj = toEdit[err.indice];
|
||||||
|
let etu = etuds.find((el) => el.id == obj.etudid);
|
||||||
|
|
||||||
|
const text = document.createTextNode(`Erreur pour ${etu.nom} ${etu.prenom} : ${err.message}`);
|
||||||
|
const toast = generateToast(text, "var(--color-error)");
|
||||||
|
pushToast(toast);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
counts.edit = data.success.length;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error("Erreur lors de l'édition de l'assiduité", error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Affiche un loader
|
||||||
|
afficheLoader();
|
||||||
|
|
||||||
|
Promise.all([promiseCreate, promiseEdit]).then(async () => {
|
||||||
|
retirerLoader();
|
||||||
|
await recupAssiduitesHebdo(updateTable);
|
||||||
|
envoiToastTous("present", counts.create + counts.edit);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function updateTemps(temps){
|
||||||
|
let matin = document.getElementById("text-matin");
|
||||||
|
let apresmidi = document.getElementById("text-apresmidi");
|
||||||
|
matin.textContent = `${temps.matin.debut} à ${temps.matin.fin}`;
|
||||||
|
apresmidi.textContent = `${temps.apresmidi.debut} à ${temps.apresmidi.fin}`;
|
||||||
|
|
||||||
|
recupAssiduitesHebdo(updateTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
const temps = {
|
||||||
|
matin: {
|
||||||
|
debut: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||||
|
fin: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}"
|
||||||
|
},
|
||||||
|
apresmidi: {
|
||||||
|
debut: "{{ scu.get_assiduites_time_config("assi_lunch_time") }}",
|
||||||
|
fin: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById("text-matin").addEventListener("click", (e)=>{
|
||||||
|
e.preventDefault();
|
||||||
|
openModal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("text-apresmidi").addEventListener("click", (e)=>{
|
||||||
|
e.preventDefault();
|
||||||
|
openModal(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTemps(temps);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
function openModal(morning = true){
|
||||||
|
|
||||||
|
let text = morning ? "du matin" : "de l'après-midi";
|
||||||
|
const modal = document.getElementById("timePickerModal");
|
||||||
|
modal.querySelector("#timePicker-modal-text").textContent = text;
|
||||||
|
|
||||||
|
let time1 = $("#time1");
|
||||||
|
let time2 = $("#time2");
|
||||||
|
|
||||||
|
// Réinitialiser les champs
|
||||||
|
time1.val(morning ? temps.matin.debut : temps.apresmidi.debut);
|
||||||
|
time2.val(morning ? temps.matin.fin : temps.apresmidi.fin);
|
||||||
|
|
||||||
|
// Définir l'action du bouton de confirmation
|
||||||
|
|
||||||
|
document.getElementById("confirmButton").onclick = function(){
|
||||||
|
let debut = time1.val();
|
||||||
|
let fin = time2.val();
|
||||||
|
|
||||||
|
if (debut == "" || fin == ""){
|
||||||
|
alert("Veuillez remplir les deux champs");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debut >= fin){
|
||||||
|
alert("L'heure de début doit être inférieure à l'heure de fin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (morning){
|
||||||
|
if (fin > temps.apresmidi.debut){
|
||||||
|
alert("L'heure de fin du matin doit être inférieure à l'heure de début de l'après-midi");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
temps.matin.debut = debut;
|
||||||
|
temps.matin.fin = fin;
|
||||||
|
} else {
|
||||||
|
if (debut < temps.matin.fin){
|
||||||
|
alert("L'heure de début de l'après-midi doit être supérieure à l'heure de fin du matin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
temps.apresmidi.debut = debut;
|
||||||
|
temps.apresmidi.fin = fin;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTemps(temps);
|
||||||
|
modal.classList.remove("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
modal.classList.add("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", ()=>{
|
||||||
|
const modal = document.getElementById("timePickerModal");
|
||||||
|
modal.querySelector(".timePicker-close").onclick = function() {
|
||||||
|
modal.classList.remove("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keyup', function(e) {
|
||||||
|
if (e.key === "Escape" && modal.classList.contains("show")) {
|
||||||
|
modal.classList.remove("show");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
document.querySelectorAll("th .rbtn").forEach((el)=>{
|
||||||
|
el.addEventListener("click", (e)=>{
|
||||||
|
allPresent(...el.id.split("-"));
|
||||||
|
e.preventDefault();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock scripts %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ title }}
|
||||||
|
{% endblock title %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
|
||||||
|
<h2>Signalement hebdomadaire de l'assiduité {{ gr | safe }}</h2>
|
||||||
|
<br>
|
||||||
|
<div id="actions" class="flex">
|
||||||
|
<button onclick="changeWeek(true)">Semaine précédente</button>
|
||||||
|
<label for="moduleimpl_select">
|
||||||
|
Module:
|
||||||
|
{{moduleimpl_select | safe}}
|
||||||
|
</label>
|
||||||
|
<button onclick="changeWeek(false)">Semaine suivante</button>
|
||||||
|
<span><a href="{{url_choix_semaine}}" class="stdlink">autre semaine<a></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 id="tableau-dates">
|
||||||
|
Le matin <a href="#" id="text-matin" title="Cliquer pour modifier les horaires">9h à 12h</a> et l'après-midi de <a href="#" id="text-apresmidi" title="Cliquer pour modifier les horaires">13h à 17h</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{% if readonly %}
|
||||||
|
<h4
|
||||||
|
title="Vous n'avez pas les permissions nécessaires afin de modifier les assiduités"
|
||||||
|
data-tooltip
|
||||||
|
>
|
||||||
|
Ouvert en mode <span class="rouge">lecture seule</span>.
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
<table id="table">
|
||||||
|
<thead>
|
||||||
|
<tr class="premier">
|
||||||
|
<th rowspan="2">Étudiants</th>
|
||||||
|
|
||||||
|
{% for jour in hebdo_jours %}
|
||||||
|
|
||||||
|
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
|
||||||
|
<th colspan="2" class="{{'grayed' if jour[0] else ''}}" >{{ jour[1][0] }} {{jour[1][1] }}</th>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
<tr class="second">
|
||||||
|
{% for jour in hebdo_jours %}
|
||||||
|
|
||||||
|
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
|
||||||
|
<th class="{{'grayed' if jour[0] else ''}}">Matin</th>
|
||||||
|
<th class="{{'grayed' if jour[0] else ''}}">Après-midi</th>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% if not readonly and not non_present %}
|
||||||
|
<tr>
|
||||||
|
{# Ne pas afficher si preference "non presences" / "readonly" #}
|
||||||
|
<th></th>
|
||||||
|
{% for jour in hebdo_jours %}
|
||||||
|
{% if not jour[0] or jour[1][0] not in ['Samedi', 'Dimanche'] %}
|
||||||
|
<th class="{{'grayed' if jour[0] else ''}}">
|
||||||
|
<input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-am" class="rbtn present" {{'disabled' if jour[0] else ''}}>
|
||||||
|
</th>
|
||||||
|
<th class="{{'grayed' if jour[0] else ''}}">
|
||||||
|
<input title="Mettre tout le monde présent" data-tooltip type="checkbox" name="" id="{{loop.index - 1}}-pm" class="rbtn present" {{'disabled' if jour[0] else ''}}>
|
||||||
|
</th>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for etud in etudiants %}
|
||||||
|
<tr etudid="{{etud.etudid}}" id="row-{{etud.etudid}}">
|
||||||
|
<td class="etudinfo" id="etud-{{etud.etudid}}">{{ etud.nom_prenom() }}</td>
|
||||||
|
{# Sera rempli en JS #}
|
||||||
|
{# Ne pas afficher bouton présent si pref "non présences" #}
|
||||||
|
{# <td>
|
||||||
|
<input type="checkbox" name="" id="" class="rbtn present">
|
||||||
|
<input type="checkbox" name="" id="" class="rbtn retard">
|
||||||
|
<input type="checkbox" name="" id="" class="rbtn absent">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="" id="" class="rbtn present">
|
||||||
|
<input type="checkbox" name="" id="" class="rbtn retard">
|
||||||
|
<input type="checkbox" name="" id="" class="rbtn absent">
|
||||||
|
</td> #}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div id="timePickerModal" class="timePicker-modal">
|
||||||
|
<div class="timePicker-modal-content">
|
||||||
|
<span class="timePicker-close">×</span>
|
||||||
|
<h2>Choisissez les horaires <span id="timePicker-modal-text"></span></h2>
|
||||||
|
<div class="time-picker-container">
|
||||||
|
<label for="time1">Début</label>
|
||||||
|
<input type="text" id="time1" name="time1" class="timepicker" placeholder="hh:mm">
|
||||||
|
</div>
|
||||||
|
<div class="time-picker-container">
|
||||||
|
<label for="time2">Fin</label>
|
||||||
|
<input type="text" id="time2" name="time2" class="timepicker" placeholder="hh:mm">
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<button id="confirmButton">Confirmer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "assiduites/widgets/alert.j2" %}
|
||||||
|
{% include "assiduites/widgets/toast.j2" %}
|
||||||
|
{% endblock app_content %}
|
|
@ -1,8 +1,7 @@
|
||||||
<div class="assiduite-bubble {{etat}}">
|
<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-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-why">Motif: {{motif}}</div>
|
||||||
<div class="assiduite-user_id">{{saisie}}</div>
|
<div class="assiduite-user_id">{{saisie}}</div>
|
||||||
</div>
|
</div>
|
|
@ -1,12 +1,28 @@
|
||||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la période
|
<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>
|
||||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la période
|
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||||
</li>
|
</li>
|
||||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la période
|
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||||
|
période
|
||||||
|
</li>
|
||||||
|
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
||||||
|
</li>
|
||||||
|
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
||||||
|
période
|
||||||
|
</li>
|
||||||
|
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li><span title="Hachure Bleue" class="justified demo"></span> → l'assiduité est justifiée par un
|
<li><span title="Quart Bleu" class="est_just demo color"></span> → la période est couverte par un
|
||||||
justificatif valide</li>
|
justificatif valide</li>
|
||||||
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> → l'assiduité est
|
<li><span title="Justif. non valide" class="invalide demo color "></span> → la période est
|
||||||
justifiée par un justificatif non valide / en attente de validation
|
couverte par un justificatif non valide
|
||||||
</li>
|
</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é);
|
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());
|
block.classList.add(assiduité.etat.toLowerCase());
|
||||||
if(assiduité.etat != "CRENEAU") block.classList.add("color");
|
if(assiduité.etat != "CRENEAU") block.classList.add("color");
|
||||||
|
|
|
@ -17,12 +17,13 @@
|
||||||
const timelineContainer = document.querySelector(".timeline-container");
|
const timelineContainer = document.querySelector(".timeline-container");
|
||||||
const periodTimeLine = document.querySelector(".period");
|
const periodTimeLine = document.querySelector(".period");
|
||||||
const t_start = {{ t_start }};
|
const t_start = {{ t_start }};
|
||||||
|
const t_mid = {{ t_mid }};
|
||||||
const t_end = {{ t_end }};
|
const t_end = {{ t_end }};
|
||||||
|
|
||||||
const tick_time = 60 / {{ tick_time }};
|
const tick_time = 60 / {{ tick_time }};
|
||||||
const tick_delay = 1 / tick_time;
|
const tick_delay = 1 / tick_time;
|
||||||
|
|
||||||
const period_default = {{ periode_defaut }};
|
const period_default = 2;
|
||||||
|
|
||||||
let handleMoving = false;
|
let handleMoving = false;
|
||||||
|
|
||||||
|
@ -133,6 +134,7 @@
|
||||||
timelineContainer.removeEventListener("mousemove", onMouseMove);
|
timelineContainer.removeEventListener("mousemove", onMouseMove);
|
||||||
handleMoving = false;
|
handleMoving = false;
|
||||||
func_call();
|
func_call();
|
||||||
|
savePeriodInLocalStorage();
|
||||||
|
|
||||||
}
|
}
|
||||||
timelineContainer.addEventListener("mousemove", onMouseMove);
|
timelineContainer.addEventListener("mousemove", onMouseMove);
|
||||||
|
@ -166,6 +168,7 @@
|
||||||
snapHandlesToQuarters();
|
snapHandlesToQuarters();
|
||||||
timelineContainer.removeEventListener("mousemove", onMouseMove);
|
timelineContainer.removeEventListener("mousemove", onMouseMove);
|
||||||
func_call();
|
func_call();
|
||||||
|
savePeriodInLocalStorage();
|
||||||
}
|
}
|
||||||
timelineContainer.addEventListener("mousemove", onMouseMove);
|
timelineContainer.addEventListener("mousemove", onMouseMove);
|
||||||
timelineContainer.addEventListener("touchmove", onMouseMove);
|
timelineContainer.addEventListener("touchmove", onMouseMove);
|
||||||
|
@ -264,6 +267,7 @@
|
||||||
snapHandlesToQuarters();
|
snapHandlesToQuarters();
|
||||||
updatePeriodTimeLabel()
|
updatePeriodTimeLabel()
|
||||||
func_call();
|
func_call();
|
||||||
|
savePeriodInLocalStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapHandlesToQuarters() {
|
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();
|
createTicks();
|
||||||
|
|
||||||
setPeriodValues(t_start, t_start + period_default);
|
loadPeriodFromLocalStorage();
|
||||||
|
|
||||||
{% if heures %}
|
{% if heures %}
|
||||||
let [heure_deb, heure_fin] = [{{ heures | safe }}]
|
let [heure_deb, heure_fin] = [{{ heures | safe }}]
|
||||||
|
|
93
app/templates/calendrier.j2
Normal file
93
app/templates/calendrier.j2
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<div class="calendrier">
|
||||||
|
{% for mois,semaines in calendrier.items() %}
|
||||||
|
<div class="mois {{'highlight' if highlight=='mois'}}">
|
||||||
|
<h3>{{mois}}</h3>
|
||||||
|
{% for semaine in semaines %}
|
||||||
|
<div class="jours {{'highlight' if highlight=='semaine'}}" week_index="{{semaine}}">
|
||||||
|
{% for jour in semaines[semaine] %}
|
||||||
|
<div class="jour {{jour.get_class()}} {{'highlight' if highlight=='jour'}}" date="{{jour.get_date('%Y-%m-%d')}}">
|
||||||
|
<span class="nom">{{jour.get_nom()}}</span>
|
||||||
|
<div class="contenu">
|
||||||
|
{{jour.get_html() | safe}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
.highlight:hover{
|
||||||
|
border: solid 3px yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
62
app/templates/choix_date.j2
Normal file
62
app/templates/choix_date.j2
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.highlight {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
.highlight * {
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gtrcontent h2.titre {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.content{
|
||||||
|
width: 90%;
|
||||||
|
max-width: 1600px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="titre">{{titre}}</h2>
|
||||||
|
{{calendrier | safe}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock app_content %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const mode = "{{mode}}";
|
||||||
|
const url = new URL(window.location.origin + "{{url | safe}}");
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", ()=>{
|
||||||
|
const highlight = document.querySelectorAll(".highlight");
|
||||||
|
highlight.forEach((el)=>{
|
||||||
|
el.addEventListener("click", (e)=>{
|
||||||
|
if (mode == "jour"){
|
||||||
|
const date = el.getAttribute("date");
|
||||||
|
url.searchParams.set("day", date);
|
||||||
|
}
|
||||||
|
if (mode == "semaine"){
|
||||||
|
const date = el.getAttribute("week_index");
|
||||||
|
url.searchParams.set("week", date);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock scripts %}
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<h2>Accès non autorisé</h2>
|
<h2>Accès non autorisé</h2>
|
||||||
|
|
||||||
{{ exc | safe }}
|
{{ exc }}
|
||||||
|
|
||||||
<p class="footer">
|
<p class="footer">
|
||||||
{% if g.scodoc_dept %}
|
{% if g.scodoc_dept %}
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
</li>
|
</li>
|
||||||
{% if formsemestre_origine is not none %}
|
{% if formsemestre_origine is not none %}
|
||||||
<li><a class="stdlink" href="{{
|
<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,
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_origine.id,
|
||||||
etudid=etud.id, only_one_sem=1)
|
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}}/js/scodoc.js"></script>
|
||||||
<script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
|
<script src="{{scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
|
||||||
<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_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}";
|
||||||
</script>
|
</script>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user