Compare commits
19 Commits
Author | SHA1 | Date |
---|---|---|
Emmanuel Viennet | 043985bff6 | |
Emmanuel Viennet | d20ada1797 | |
Iziram | 778fecabb6 | |
Iziram | fa6f83722e | |
Emmanuel Viennet | baa0412071 | |
Emmanuel Viennet | d51a47b71a | |
Lyanis Souidi | f21ef41de6 | |
Lyanis Souidi | 2d673e7a5d | |
Emmanuel Viennet | 3e43495831 | |
Iziram | a4db8c4ff8 | |
Iziram | 1ac35d04c2 | |
Iziram | 687ac3cf13 | |
Emmanuel Viennet | 18b1f00586 | |
Iziram | 6b985620e9 | |
Iziram | 4d234ba353 | |
Iziram | 5d45fcf656 | |
Iziram | 0a5919b788 | |
Iziram | 09f4525e66 | |
Emmanuel Viennet | 0bc57807de |
152
README.md
152
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,45 +41,41 @@ Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configu
|
||||||
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
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
|
||||||
.../config/logos # Logos de l'établissement
|
.../config/logos # Logos de l'établissement
|
||||||
.../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
|
||||||
|
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||||
|
apt-get install git # si besoin
|
||||||
|
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
|
||||||
|
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||||
|
|
||||||
sudo su
|
# Donner ce répertoire à l'utilisateur scodoc:
|
||||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
chown -R scodoc:scodoc /opt/scodoc
|
||||||
apt-get install git # si besoin
|
```
|
||||||
cd /opt
|
|
||||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
|
||||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
|
||||||
|
|
||||||
# Renommer le répertoire:
|
|
||||||
mv ScoDoc scodoc
|
|
||||||
|
|
||||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
|
||||||
chown -R scodoc.scodoc /opt/scodoc
|
|
||||||
|
|
||||||
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,43 +115,43 @@ développement dans un état connu, par exemple pour éviter de recréer à la m
|
||||||
|
|
||||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
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
|
||||||
|
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||||
|
flask db upgrade # créé les tables à partir des migrations
|
||||||
|
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||||
|
|
||||||
dropdb SCODOC_DEV
|
# puis imports:
|
||||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
flask import-scodoc7-users
|
||||||
flask db upgrade # créé les tables à partir des migrations
|
flask import-scodoc7-dept STID SCOSTID
|
||||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
```
|
||||||
|
|
||||||
# puis imports:
|
|
||||||
flask import-scodoc7-users
|
|
||||||
flask import-scodoc7-dept STID SCOSTID
|
|
||||||
|
|
||||||
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,23 +159,23 @@ 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
|
||||||
important est `postinst`qui se charge de configurer le système (install ou
|
important est `postinst` qui se charge de configurer le système (install ou
|
||||||
upgrade de scodoc9).
|
upgrade de scodoc9).
|
||||||
|
|
||||||
La préparation d'une release se fait à l'aide du script
|
La préparation d'une release se fait à l'aide du script
|
||||||
|
|
|
@ -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()
|
||||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
try:
|
||||||
|
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||||
|
except ObjectDeletedError:
|
||||||
|
return json_error(404, "Assiduité supprimée / inexistante")
|
||||||
|
|
||||||
return {"OK": True}
|
return {"OK": True}
|
||||||
|
|
||||||
|
|
|
@ -427,12 +427,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>{
|
||||||
|
|
|
@ -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,105 @@ 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
|
# 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"""
|
||||||
|
@ -369,52 +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_by_day = collections.defaultdict(list) # date_iso : 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
|
|
||||||
if e.date_debut == e.date_fin:
|
|
||||||
heure_debut_txt, heure_fin_txt = "", ""
|
|
||||||
else:
|
|
||||||
heure_debut_txt = (
|
|
||||||
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
|
|
||||||
)
|
|
||||||
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
|
|
||||||
|
|
||||||
# Etat (notes completes) de l'évaluation:
|
|
||||||
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
|
|
||||||
if modimpl_result.evaluations_etat[e.id].is_complete:
|
|
||||||
color = color_complete
|
|
||||||
else:
|
|
||||||
color = color_incomplete
|
|
||||||
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
|
||||||
color = color_futur
|
|
||||||
day = e.date_debut.date().isoformat() # yyyy-mm-dd
|
|
||||||
event = {
|
|
||||||
"color": color,
|
|
||||||
"date_iso": day,
|
|
||||||
"title": e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval.",
|
|
||||||
"description": f"""{e.description or e.moduleimpl.module.titre_str()}"""
|
|
||||||
+ (
|
|
||||||
f""" de {heure_debut_txt} à {heure_fin_txt}"""
|
|
||||||
if heure_debut_txt
|
|
||||||
else ""
|
|
||||||
),
|
|
||||||
"href": url_for(
|
|
||||||
"notes.moduleimpl_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
moduleimpl_id=e.moduleimpl_id,
|
|
||||||
),
|
|
||||||
"modimpl": e.moduleimpl,
|
|
||||||
}
|
|
||||||
events_by_day[day].append(event)
|
|
||||||
|
|
||||||
cal_html = sco_cal.YearTable(year, events_by_day=events_by_day)
|
|
||||||
|
|
||||||
return f"""
|
return f"""
|
||||||
{
|
{
|
||||||
|
@ -430,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>
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
"""
|
||||||
|
Génération d'un calendrier
|
||||||
|
(Classe abstraite à implémenter dans les classes filles)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from flask import render_template
|
||||||
|
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from app import g
|
||||||
|
|
||||||
|
|
||||||
|
class Jour:
|
||||||
|
"""
|
||||||
|
Représente un jour dans le calendrier
|
||||||
|
Permet d'obtenir les informations sur le jour
|
||||||
|
et générer une représentation html
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, date: datetime.date):
|
||||||
|
self.date = date
|
||||||
|
self.class_list: list[str] = []
|
||||||
|
|
||||||
|
if self.is_non_work():
|
||||||
|
self.class_list.append("non-travail")
|
||||||
|
if self.is_current_week():
|
||||||
|
self.class_list.append("sem-courante")
|
||||||
|
|
||||||
|
def get_nom(self, short=True):
|
||||||
|
"""
|
||||||
|
Renvoie le nom du jour
|
||||||
|
"M19" ou "Mer 19"
|
||||||
|
|
||||||
|
par défaut en version courte
|
||||||
|
"""
|
||||||
|
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
|
||||||
|
return (
|
||||||
|
f"{str_jour[0] if short or self.is_non_work() else str_jour[:3]+' '}"
|
||||||
|
+ f"{self.date.day}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_non_work(self):
|
||||||
|
"""
|
||||||
|
Renvoie True si le jour est un jour non travaillé
|
||||||
|
(en fonction de la préférence du département)
|
||||||
|
"""
|
||||||
|
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
|
||||||
|
dept_id=g.scodoc_dept_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_current_week(self):
|
||||||
|
"""
|
||||||
|
Renvoie True si le jour est dans la semaine courante
|
||||||
|
"""
|
||||||
|
return self.date.isocalendar()[0:2] == datetime.date.today().isocalendar()[0:2]
|
||||||
|
|
||||||
|
def get_date(self) -> str:
|
||||||
|
"""
|
||||||
|
Renvoie la date du jour au format "dd/mm/yyyy"
|
||||||
|
"""
|
||||||
|
return self.date.strftime(scu.DATE_FMT)
|
||||||
|
|
||||||
|
def get_html(self):
|
||||||
|
"""
|
||||||
|
Renvoie le code html du jour
|
||||||
|
à surcharger dans les classes filles
|
||||||
|
|
||||||
|
l'html final ressemblera à :
|
||||||
|
|
||||||
|
<div class="jour {{jour.get_class()}}">
|
||||||
|
<span class="nom">{{jour.get_nom()}}</span>
|
||||||
|
<div class="contenu">
|
||||||
|
{{jour.get_html() | safe}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Méthode à implémenter dans les classes filles")
|
||||||
|
|
||||||
|
def get_class(self):
|
||||||
|
"""
|
||||||
|
Renvoie la classe css du jour
|
||||||
|
|
||||||
|
utilise self.class_list
|
||||||
|
-> fait un join de la liste
|
||||||
|
|
||||||
|
"""
|
||||||
|
return " ".join(self.class_list)
|
||||||
|
|
||||||
|
|
||||||
|
class Calendrier:
|
||||||
|
"""
|
||||||
|
Représente un calendrier
|
||||||
|
Permet d'obtenir les informations sur les jours
|
||||||
|
et générer une représentation html
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, date_debut: datetime.date, date_fin: datetime.date):
|
||||||
|
self.date_debut = date_debut
|
||||||
|
self.date_fin = date_fin
|
||||||
|
self.jours: dict[str, list[Jour]] = {}
|
||||||
|
|
||||||
|
def _get_dates_between(self) -> list[datetime.date]:
|
||||||
|
"""
|
||||||
|
get_dates_between Renvoie la liste des dates entre date_debut et date_fin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[datetime.date]: liste des dates entre date_debut et date_fin
|
||||||
|
"""
|
||||||
|
resultat = []
|
||||||
|
date_actuelle: datetime.date = self.date_debut
|
||||||
|
while date_actuelle <= self.date_fin:
|
||||||
|
if isinstance(date_actuelle, datetime.datetime):
|
||||||
|
resultat.append(date_actuelle.date())
|
||||||
|
elif isinstance(date_actuelle, datetime.date):
|
||||||
|
resultat.append(date_actuelle)
|
||||||
|
date_actuelle += datetime.timedelta(days=1)
|
||||||
|
return resultat
|
||||||
|
|
||||||
|
def organize_by_month(self):
|
||||||
|
"""
|
||||||
|
Organise les jours par mois
|
||||||
|
Instancie un objet Jour pour chaque jour
|
||||||
|
|
||||||
|
met à jour self.jours
|
||||||
|
"""
|
||||||
|
organized = {}
|
||||||
|
for date in self._get_dates_between():
|
||||||
|
# Récupérer le mois en français
|
||||||
|
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
|
||||||
|
# Ajouter le jour à la liste correspondante au mois
|
||||||
|
if month not in organized:
|
||||||
|
organized[month] = []
|
||||||
|
|
||||||
|
jour: Jour = self.instanciate_jour(date)
|
||||||
|
|
||||||
|
organized[month].append(jour)
|
||||||
|
|
||||||
|
self.jours = organized
|
||||||
|
|
||||||
|
def instanciate_jour(self, date: datetime.date) -> Jour:
|
||||||
|
"""
|
||||||
|
Instancie un objet Jour pour chaque jour
|
||||||
|
A surcharger dans les classes filles si besoin
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Méthode à implémenter dans les classes filles")
|
||||||
|
|
||||||
|
def get_html(self):
|
||||||
|
"""
|
||||||
|
get_html Renvoie le code html du calendrier
|
||||||
|
"""
|
||||||
|
self.organize_by_month()
|
||||||
|
return render_template("calendrier.j2", calendrier=self.jours)
|
|
@ -611,16 +611,17 @@ class BasePreferences:
|
||||||
"explanation": "toute saisie d'absence doit indiquer le module concerné",
|
"explanation": "toute saisie d'absence doit indiquer le module concerné",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
# (
|
(
|
||||||
# "forcer_present",
|
"non_present",
|
||||||
# {
|
{
|
||||||
# "initvalue": 0,
|
"initvalue": 0,
|
||||||
# "title": "Forcer l'appel des présents",
|
"title": "Désactiver la saisie des présences",
|
||||||
# "input_type": "boolcheckbox",
|
"input_type": "boolcheckbox",
|
||||||
# "labels": ["non", "oui"],
|
"labels": ["non", "oui"],
|
||||||
# "category": "assi",
|
"category": "assi",
|
||||||
# },
|
"explanation": "Désactive la saisie et l'affichage des présences",
|
||||||
# ),
|
},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"periode_defaut",
|
"periode_defaut",
|
||||||
{
|
{
|
||||||
|
@ -644,18 +645,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",
|
||||||
{
|
{
|
||||||
|
|
|
@ -730,31 +730,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 +742,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%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -296,7 +296,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;
|
||||||
|
@ -425,7 +431,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 +537,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();
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -23,47 +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>{{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>
|
||||||
|
@ -75,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
|
||||||
|
@ -163,50 +95,27 @@ Calendrier de l'assiduité
|
||||||
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 {
|
||||||
|
@ -336,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 %}
|
|
@ -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>
|
||||||
|
|
||||||
|
@ -600,7 +624,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 }}
|
||||||
|
|
||||||
|
@ -159,8 +160,10 @@
|
||||||
<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="Present">
|
||||||
|
{% 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="Retard">
|
||||||
<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"
|
||||||
|
@ -178,6 +181,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" %}
|
||||||
|
|
|
@ -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>
|
||||||
</li>
|
<ul class="couleurs">
|
||||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la période
|
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
||||||
</li>
|
période
|
||||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la période
|
</li>
|
||||||
</li>
|
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||||
|
</li>
|
||||||
|
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||||
|
période
|
||||||
|
</li>
|
||||||
|
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
||||||
|
</li>
|
||||||
|
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
||||||
|
période
|
||||||
|
</li>
|
||||||
|
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||||
|
</li>
|
||||||
|
|
||||||
<li><span title="Hachure Bleue" class="justified demo"></span> → l'assiduité est justifiée par un
|
<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");
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
<div class="calendrier">
|
||||||
|
{% for mois,jours in calendrier.items() %}
|
||||||
|
<div class="mois">
|
||||||
|
<h3>{{mois}}</h3>
|
||||||
|
<div class="jours">
|
||||||
|
{% for jour in jours %}
|
||||||
|
<div class="jour {{jour.get_class()}}">
|
||||||
|
<span class="nom">{{jour.get_nom()}}</span>
|
||||||
|
<div class="contenu">
|
||||||
|
{{jour.get_html() | safe}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.calendrier {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
overflow-x: scroll;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mois {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mois h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jour {
|
||||||
|
text-align: left;
|
||||||
|
margin: 2px;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 13px;
|
||||||
|
position: relative;
|
||||||
|
font-weight: normal;
|
||||||
|
min-width: 6em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jour>.contenu {
|
||||||
|
background-color: white;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jour.jour.non-travail>.nom,
|
||||||
|
.jour.jour.non-travail>.contenu {
|
||||||
|
border: 0;
|
||||||
|
background-color: #badfff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jour>.nom {
|
||||||
|
width: 3em;
|
||||||
|
min-width: 3em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jour>.contenu,
|
||||||
|
.jour>.nom {
|
||||||
|
border: 1px solid #d5d5d5;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jour>.contenu a {
|
||||||
|
padding: 0px 2px;
|
||||||
|
}
|
||||||
|
.jour>.nom {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.sem-courante{
|
||||||
|
--couleur : #ee752c;
|
||||||
|
border-left: solid 3px var(--couleur);
|
||||||
|
border-right: solid 3px var(--couleur);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -63,6 +63,7 @@ from app.models import (
|
||||||
Scolog,
|
Scolog,
|
||||||
)
|
)
|
||||||
from app.scodoc.codes_cursus import UE_STANDARD
|
from app.scodoc.codes_cursus import UE_STANDARD
|
||||||
|
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.models.assiduites import get_assiduites_justif
|
from app.models.assiduites import get_assiduites_justif
|
||||||
from app.tables.list_etuds import RowEtud, TableEtud
|
from app.tables.list_etuds import RowEtud, TableEtud
|
||||||
|
@ -82,6 +83,7 @@ from app.scodoc import sco_find_etud
|
||||||
from app.scodoc import sco_assiduites as scass
|
from app.scodoc import sco_assiduites as scass
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc import sco_gen_cal
|
||||||
|
|
||||||
|
|
||||||
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
|
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
|
||||||
|
@ -757,8 +759,6 @@ def _verif_date_form_justif(
|
||||||
deb = deb.replace(hour=0, minute=0)
|
deb = deb.replace(hour=0, minute=0)
|
||||||
fin = fin.replace(hour=23, minute=59)
|
fin = fin.replace(hour=23, minute=59)
|
||||||
|
|
||||||
print(f"DEBUG {cas=}")
|
|
||||||
|
|
||||||
return deb, fin
|
return deb, fin
|
||||||
|
|
||||||
|
|
||||||
|
@ -925,7 +925,14 @@ def calendrier_assi_etud():
|
||||||
# (sera utilisé pour générer le selecteur d'année)
|
# (sera utilisé pour générer le selecteur d'année)
|
||||||
annees_str: str = json.dumps(annees)
|
annees_str: str = json.dumps(annees)
|
||||||
|
|
||||||
calendrier: dict[str, list["Jour"]] = generate_calendar(etud, annee)
|
cal = CalendrierAssi(
|
||||||
|
annee,
|
||||||
|
etud,
|
||||||
|
mode_demi=mode_demi,
|
||||||
|
show_pres=show_pres,
|
||||||
|
show_reta=show_reta,
|
||||||
|
)
|
||||||
|
calendrier: str = cal.get_html()
|
||||||
|
|
||||||
# Peuplement du template jinja
|
# Peuplement du template jinja
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -1132,6 +1139,11 @@ def signal_assiduites_group():
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
dept_id=g.scodoc_dept_id,
|
dept_id=g.scodoc_dept_id,
|
||||||
),
|
),
|
||||||
|
non_present=sco_preferences.get_preference(
|
||||||
|
"non_present",
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
dept_id=g.scodoc_dept_id,
|
||||||
|
),
|
||||||
formsemestre_date_debut=str(formsemestre.date_debut),
|
formsemestre_date_debut=str(formsemestre.date_debut),
|
||||||
formsemestre_date_fin=str(formsemestre.date_fin),
|
formsemestre_date_fin=str(formsemestre.date_fin),
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
|
@ -1914,8 +1926,29 @@ def _preparer_objet(
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.AbsChange)
|
@permission_required(Permission.AbsChange)
|
||||||
def signal_assiduites_diff():
|
def signal_assiduites_diff():
|
||||||
"""TODO documenter
|
"""
|
||||||
Utilisé notamment par "Saisie différée" sur tableau de bord semetstre"
|
Utilisé notamment par "Saisie différée" sur tableau de bord semetstre"
|
||||||
|
|
||||||
|
Arguments de la requête:
|
||||||
|
|
||||||
|
- group_ids : liste des groupes
|
||||||
|
example : group_ids=1,2,3
|
||||||
|
- formsemestre_id : id du formsemestre
|
||||||
|
example : formsemestre_id=1
|
||||||
|
- moduleimpl_id : id du moduleimpl
|
||||||
|
example : moduleimpl_id=1
|
||||||
|
|
||||||
|
(Permet de pré-générer une plage. Si non renseigné, la plage sera vide)
|
||||||
|
(Les trois valeurs suivantes doivent être renseignées ensemble)
|
||||||
|
- date
|
||||||
|
example : date=01/01/2021
|
||||||
|
- heure_debut
|
||||||
|
example : heure_debut=08:00
|
||||||
|
- heure_fin
|
||||||
|
example : heure_fin=10:00
|
||||||
|
|
||||||
|
Exemple de requête :
|
||||||
|
signal_assiduites_diff?formsemestre_id=67&group_ids=400&moduleimpl_id=1229&date=15/04/2024&heure_debut=12:34&heure_fin=12:55
|
||||||
"""
|
"""
|
||||||
# Récupération des paramètres de la requête
|
# Récupération des paramètres de la requête
|
||||||
group_ids: list[int] = request.args.get("group_ids", None)
|
group_ids: list[int] = request.args.get("group_ids", None)
|
||||||
|
@ -1957,11 +1990,23 @@ def signal_assiduites_diff():
|
||||||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Pré-remplissage des sélecteurs
|
||||||
moduleimpl_id = request.args.get("moduleimpl_id", -1)
|
moduleimpl_id = request.args.get("moduleimpl_id", -1)
|
||||||
try:
|
try:
|
||||||
moduleimpl_id = int(moduleimpl_id)
|
moduleimpl_id = int(moduleimpl_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
moduleimpl_id = -1
|
moduleimpl_id = -1
|
||||||
|
# date fra (dd/mm/yyyy)
|
||||||
|
date = request.args.get("date", "")
|
||||||
|
# heures (hh:mm)
|
||||||
|
heure_deb = request.args.get("heure_debut", "")
|
||||||
|
heure_fin = request.args.get("heure_fin", "")
|
||||||
|
|
||||||
|
# vérifications des sélecteurs
|
||||||
|
date = date if re.match(r"^\d{2}\/\d{2}\/\d{4}$", date) else ""
|
||||||
|
heure_deb = heure_deb if re.match(r"^[0-2]\d:[0-5]\d$", heure_deb) else ""
|
||||||
|
heure_fin = heure_fin if re.match(r"^[0-2]\d:[0-5]\d$", heure_fin) else ""
|
||||||
|
nouv_plage: list[str] = [date, heure_deb, heure_fin]
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"assiduites/pages/signal_assiduites_diff.j2",
|
"assiduites/pages/signal_assiduites_diff.j2",
|
||||||
|
@ -1977,6 +2022,12 @@ def signal_assiduites_diff():
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
dept_id=g.scodoc_dept_id,
|
dept_id=g.scodoc_dept_id,
|
||||||
),
|
),
|
||||||
|
non_present=sco_preferences.get_preference(
|
||||||
|
"non_present",
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
dept_id=g.scodoc_dept_id,
|
||||||
|
),
|
||||||
|
nouv_plage=nouv_plage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2473,79 +2524,74 @@ def _get_etuds_dem_def(formsemestre) -> str:
|
||||||
# --- Gestion du calendrier ---
|
# --- Gestion du calendrier ---
|
||||||
|
|
||||||
|
|
||||||
def generate_calendar(
|
class JourAssi(sco_gen_cal.Jour):
|
||||||
etudiant: Identite,
|
|
||||||
annee: int = None,
|
|
||||||
) -> dict[str, list["Jour"]]:
|
|
||||||
"""
|
"""
|
||||||
Génère le calendrier d'assiduité de l'étudiant pour une année scolaire donnée
|
Représente un jour d'assiduité
|
||||||
"""
|
|
||||||
# Si pas d'année alors on prend l'année scolaire en cours
|
|
||||||
if annee is None:
|
|
||||||
annee = scu.annee_scolaire()
|
|
||||||
|
|
||||||
# On prend du 01/09 au 31/08
|
|
||||||
date_debut: datetime.datetime = datetime.datetime(annee, 9, 1, 0, 0)
|
|
||||||
date_fin: datetime.datetime = datetime.datetime(annee + 1, 8, 31, 23, 59)
|
|
||||||
|
|
||||||
# Filtrage des assiduités et des justificatifs en fonction de la periode / année
|
|
||||||
etud_assiduites: Query = scass.filter_by_date(
|
|
||||||
etudiant.assiduites,
|
|
||||||
Assiduite,
|
|
||||||
date_deb=date_debut,
|
|
||||||
date_fin=date_fin,
|
|
||||||
)
|
|
||||||
etud_justificatifs: Query = scass.filter_by_date(
|
|
||||||
etudiant.justificatifs,
|
|
||||||
Justificatif,
|
|
||||||
date_deb=date_debut,
|
|
||||||
date_fin=date_fin,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Récupération des jours de l'année et de leurs assiduités/justificatifs
|
|
||||||
annee_par_mois: dict[str, list[Jour]] = _organize_by_month(
|
|
||||||
_get_dates_between(
|
|
||||||
deb=date_debut.date(),
|
|
||||||
fin=date_fin.date(),
|
|
||||||
),
|
|
||||||
etud_assiduites,
|
|
||||||
etud_justificatifs,
|
|
||||||
)
|
|
||||||
|
|
||||||
return annee_par_mois
|
|
||||||
|
|
||||||
|
|
||||||
class Jour:
|
|
||||||
"""Jour
|
|
||||||
Jour du calendrier
|
|
||||||
get_nom : retourne le numéro et le nom du Jour (ex: M19 / Mer 19)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, date: datetime.date, assiduites: Query, justificatifs: Query):
|
def __init__(
|
||||||
self.date = date
|
self,
|
||||||
|
date: datetime.date,
|
||||||
|
assiduites: Query,
|
||||||
|
justificatifs: Query,
|
||||||
|
parent: "CalendrierAssi",
|
||||||
|
):
|
||||||
|
super().__init__(date)
|
||||||
|
|
||||||
|
# assiduités et justificatifs du jour
|
||||||
self.assiduites = assiduites
|
self.assiduites = assiduites
|
||||||
self.justificatifs = justificatifs
|
self.justificatifs = justificatifs
|
||||||
|
|
||||||
def get_nom(self, mode_demi: bool = True) -> str:
|
self.parent = parent
|
||||||
"""
|
|
||||||
Renvoie le nom du jour
|
def get_html(self) -> str:
|
||||||
"M19" ou "Mer 19"
|
# si non travaillé on renvoie une case vide
|
||||||
"""
|
if self.is_non_work():
|
||||||
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
|
return ""
|
||||||
return (
|
|
||||||
f"{str_jour[0] if mode_demi or self.is_non_work() else str_jour[:3]+' '}"
|
html: str = (
|
||||||
+ f"{self.date.day}"
|
self._get_html_demi() if self.parent.mode_demi else self._get_html_normal()
|
||||||
)
|
)
|
||||||
|
html = f'<div class="assi_case">{html}</div>'
|
||||||
|
|
||||||
def get_date(self) -> str:
|
if self.has_assiduite():
|
||||||
"""
|
minitimeline: str = f"""
|
||||||
Renvoie la date du jour au format "dd/mm/yyyy"
|
<div class="dayline">
|
||||||
"""
|
<div class="dayline-title">
|
||||||
return self.date.strftime(scu.DATE_FMT)
|
<span>{self.get_date()}</span>
|
||||||
|
{self._generate_minitimeline()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
html += minitimeline
|
||||||
|
|
||||||
def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str:
|
return html
|
||||||
|
|
||||||
|
def has_assiduite(self) -> bool:
|
||||||
|
"""Renvoie True si le jour a une assiduité"""
|
||||||
|
return self.assiduites.count() > 0
|
||||||
|
|
||||||
|
def _get_html_normal(self) -> str:
|
||||||
"""
|
"""
|
||||||
Retourne la classe css du jour (mode normal)
|
Renvoie l'html de la case du calendrier
|
||||||
|
(version journee normale (donc une couleur))
|
||||||
|
"""
|
||||||
|
class_name = self._get_color_normal()
|
||||||
|
return f'<span class="{class_name}"></span>'
|
||||||
|
|
||||||
|
def _get_html_demi(self) -> str:
|
||||||
|
"""
|
||||||
|
Renvoie l'html de la case du calendrier
|
||||||
|
(version journee divisée en demi-journées (donc 2 couleurs))
|
||||||
|
"""
|
||||||
|
matin = self._get_color_demi(True)
|
||||||
|
aprem = self._get_color_demi(False)
|
||||||
|
return f'<span class="{matin}"></span><span class="{aprem}"></span>'
|
||||||
|
|
||||||
|
def _get_color_normal(self) -> str:
|
||||||
|
"""renvoie la classe css correspondant
|
||||||
|
à la case du calendrier
|
||||||
|
(version journee normale)
|
||||||
"""
|
"""
|
||||||
etat = ""
|
etat = ""
|
||||||
est_just = ""
|
est_just = ""
|
||||||
|
@ -2555,8 +2601,8 @@ class Jour:
|
||||||
|
|
||||||
etat = self._get_color_assiduites_cascade(
|
etat = self._get_color_assiduites_cascade(
|
||||||
self._get_etats_from_assiduites(self.assiduites),
|
self._get_etats_from_assiduites(self.assiduites),
|
||||||
show_pres=show_pres,
|
show_pres=self.parent.show_pres,
|
||||||
show_reta=show_reta,
|
show_reta=self.parent.show_reta,
|
||||||
)
|
)
|
||||||
|
|
||||||
est_just = self._get_color_justificatifs_cascade(
|
est_just = self._get_color_justificatifs_cascade(
|
||||||
|
@ -2565,13 +2611,11 @@ class Jour:
|
||||||
|
|
||||||
return f"color {etat} {est_just}"
|
return f"color {etat} {est_just}"
|
||||||
|
|
||||||
def get_demi_class(
|
def _get_color_demi(self, matin: bool) -> str:
|
||||||
self, matin: bool, show_pres: bool = False, show_reta: bool = False
|
"""renvoie la classe css correspondant
|
||||||
) -> str:
|
à la case du calendrier
|
||||||
|
(version journee divisée en demi-journees)
|
||||||
"""
|
"""
|
||||||
Renvoie la class css de la demi journée
|
|
||||||
"""
|
|
||||||
|
|
||||||
heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
|
heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
|
||||||
|
|
||||||
if matin:
|
if matin:
|
||||||
|
@ -2603,8 +2647,8 @@ class Jour:
|
||||||
|
|
||||||
etat = self._get_color_assiduites_cascade(
|
etat = self._get_color_assiduites_cascade(
|
||||||
self._get_etats_from_assiduites(assiduites_matin),
|
self._get_etats_from_assiduites(assiduites_matin),
|
||||||
show_pres=show_pres,
|
show_pres=self.parent.show_pres,
|
||||||
show_reta=show_reta,
|
show_reta=self.parent.show_reta,
|
||||||
)
|
)
|
||||||
|
|
||||||
est_just = self._get_color_justificatifs_cascade(
|
est_just = self._get_color_justificatifs_cascade(
|
||||||
|
@ -2643,8 +2687,8 @@ class Jour:
|
||||||
|
|
||||||
etat = self._get_color_assiduites_cascade(
|
etat = self._get_color_assiduites_cascade(
|
||||||
self._get_etats_from_assiduites(assiduites_aprem),
|
self._get_etats_from_assiduites(assiduites_aprem),
|
||||||
show_pres=show_pres,
|
show_pres=self.parent.show_pres,
|
||||||
show_reta=show_reta,
|
show_reta=self.parent.show_reta,
|
||||||
)
|
)
|
||||||
|
|
||||||
est_just = self._get_color_justificatifs_cascade(
|
est_just = self._get_color_justificatifs_cascade(
|
||||||
|
@ -2653,77 +2697,6 @@ class Jour:
|
||||||
|
|
||||||
return f"color {etat} {est_just}"
|
return f"color {etat} {est_just}"
|
||||||
|
|
||||||
def has_assiduites(self) -> bool:
|
|
||||||
"""
|
|
||||||
Renverra True si le jour a des assiduités
|
|
||||||
"""
|
|
||||||
return self.assiduites.count() > 0
|
|
||||||
|
|
||||||
def generate_minitimeline(self) -> str:
|
|
||||||
"""
|
|
||||||
Génère la minitimeline du jour
|
|
||||||
"""
|
|
||||||
# Récupérer le référenciel de la timeline
|
|
||||||
heure_matin: datetime.timedelta = _time_to_timedelta(
|
|
||||||
scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
|
|
||||||
)
|
|
||||||
heure_soir: datetime.timedelta = _time_to_timedelta(
|
|
||||||
scass.str_to_time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00"))
|
|
||||||
)
|
|
||||||
# longueur_timeline = heure_soir - heure_matin
|
|
||||||
longueur_timeline: datetime.timedelta = heure_soir - heure_matin
|
|
||||||
|
|
||||||
# chaque block d'assiduité est défini par:
|
|
||||||
# longueur = ( (fin-deb) / longueur_timeline ) * 100
|
|
||||||
# emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100
|
|
||||||
# longueur + emplacement = 100% sinon on réduit longueur
|
|
||||||
|
|
||||||
assiduite_blocks: list[dict[str, float | str]] = []
|
|
||||||
|
|
||||||
for assi in self.assiduites:
|
|
||||||
deb: datetime.timedelta = _time_to_timedelta(
|
|
||||||
assi.date_debut.time()
|
|
||||||
if assi.date_debut.date() == self.date
|
|
||||||
else heure_matin
|
|
||||||
)
|
|
||||||
fin: datetime.timedelta = _time_to_timedelta(
|
|
||||||
assi.date_fin.time()
|
|
||||||
if assi.date_fin.date() == self.date
|
|
||||||
else heure_soir
|
|
||||||
)
|
|
||||||
|
|
||||||
emplacement: float = max(((deb - heure_matin) / longueur_timeline) * 100, 0)
|
|
||||||
longueur: float = ((fin - deb) / longueur_timeline) * 100
|
|
||||||
if longueur + emplacement > 100:
|
|
||||||
longueur = 100 - emplacement
|
|
||||||
etat: str = scu.EtatAssiduite(assi.etat).name.lower()
|
|
||||||
est_just: str = "est_just" if assi.est_just else ""
|
|
||||||
|
|
||||||
assiduite_blocks.append(
|
|
||||||
{
|
|
||||||
"longueur": longueur,
|
|
||||||
"emplacement": emplacement,
|
|
||||||
"etat": etat,
|
|
||||||
"est_just": est_just,
|
|
||||||
"bubble": _generate_assiduite_bubble(assi),
|
|
||||||
"id": assi.assiduite_id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
"assiduites/widgets/minitimeline_simple.j2",
|
|
||||||
assi_blocks=assiduite_blocks,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_non_work(self):
|
|
||||||
"""
|
|
||||||
Renvoie True si le jour est un jour non travaillé
|
|
||||||
(en fonction de la préférence du département)
|
|
||||||
"""
|
|
||||||
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
|
|
||||||
dept_id=g.scodoc_dept_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]:
|
def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]:
|
||||||
return list(set(scu.EtatAssiduite(assi.etat) for assi in assiduites))
|
return list(set(scu.EtatAssiduite(assi.etat) for assi in assiduites))
|
||||||
|
|
||||||
|
@ -2762,47 +2735,110 @@ class Jour:
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def _generate_minitimeline(self) -> str:
|
||||||
|
"""
|
||||||
|
Génère la minitimeline du jour
|
||||||
|
"""
|
||||||
|
# Récupérer le référenciel de la timeline
|
||||||
|
heure_matin: datetime.timedelta = _time_to_timedelta(
|
||||||
|
scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
|
||||||
|
)
|
||||||
|
heure_soir: datetime.timedelta = _time_to_timedelta(
|
||||||
|
scass.str_to_time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00"))
|
||||||
|
)
|
||||||
|
# longueur_timeline = heure_soir - heure_matin
|
||||||
|
longueur_timeline: datetime.timedelta = heure_soir - heure_matin
|
||||||
|
|
||||||
def _get_dates_between(deb: datetime.date, fin: datetime.date) -> list[datetime.date]:
|
# chaque block d'assiduité est défini par:
|
||||||
resultat = []
|
# longueur = ( (fin-deb) / longueur_timeline ) * 100
|
||||||
date_actuelle = deb
|
# emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100
|
||||||
while date_actuelle <= fin:
|
# longueur + emplacement = 100% sinon on réduit longueur
|
||||||
resultat.append(date_actuelle)
|
|
||||||
date_actuelle += datetime.timedelta(days=1)
|
assiduite_blocks: list[dict[str, float | str]] = []
|
||||||
return resultat
|
|
||||||
|
for assi in self.assiduites:
|
||||||
|
deb: datetime.timedelta = _time_to_timedelta(
|
||||||
|
assi.date_debut.time()
|
||||||
|
if assi.date_debut.date() == self.date
|
||||||
|
else heure_matin
|
||||||
|
)
|
||||||
|
fin: datetime.timedelta = _time_to_timedelta(
|
||||||
|
assi.date_fin.time()
|
||||||
|
if assi.date_fin.date() == self.date
|
||||||
|
else heure_soir
|
||||||
|
)
|
||||||
|
|
||||||
|
emplacement: float = max(((deb - heure_matin) / longueur_timeline) * 100, 0)
|
||||||
|
longueur: float = ((fin - deb) / longueur_timeline) * 100
|
||||||
|
if longueur + emplacement > 100:
|
||||||
|
longueur = 100 - emplacement
|
||||||
|
|
||||||
|
etat: str = scu.EtatAssiduite(assi.etat).name.lower()
|
||||||
|
est_just: str = "est_just" if assi.est_just else ""
|
||||||
|
|
||||||
|
assiduite_blocks.append(
|
||||||
|
{
|
||||||
|
"longueur": longueur,
|
||||||
|
"emplacement": emplacement,
|
||||||
|
"etat": etat,
|
||||||
|
"est_just": est_just,
|
||||||
|
"bubble": _generate_assiduite_bubble(assi),
|
||||||
|
"id": assi.assiduite_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"assiduites/widgets/minitimeline_simple.j2",
|
||||||
|
assi_blocks=assiduite_blocks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _organize_by_month(days, assiduites, justificatifs) -> dict[str, list[Jour]]:
|
class CalendrierAssi(sco_gen_cal.Calendrier):
|
||||||
"""
|
"""
|
||||||
Organiser les dates par mois.
|
Représente un calendrier d'assiduité d'un étudiant
|
||||||
"""
|
"""
|
||||||
organized = {}
|
|
||||||
for date in days:
|
|
||||||
# Récupérer le mois en français
|
|
||||||
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
|
|
||||||
# Ajouter le jour à la liste correspondante au mois
|
|
||||||
if month not in organized:
|
|
||||||
organized[month] = []
|
|
||||||
|
|
||||||
date_assiduites: Query = scass.filter_by_date(
|
def __init__(self, annee: int, etudiant: Identite, **options):
|
||||||
assiduites,
|
# On prend du 01/09 au 31/08
|
||||||
|
date_debut: datetime.datetime = datetime.datetime(annee, 9, 1, 0, 0)
|
||||||
|
date_fin: datetime.datetime = datetime.datetime(annee + 1, 8, 31, 23, 59)
|
||||||
|
super().__init__(date_debut, date_fin)
|
||||||
|
|
||||||
|
# On récupère les assiduités et les justificatifs
|
||||||
|
self.etud_assiduites: Query = scass.filter_by_date(
|
||||||
|
etudiant.assiduites,
|
||||||
|
Assiduite,
|
||||||
|
date_deb=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
)
|
||||||
|
self.etud_justificatifs: Query = scass.filter_by_date(
|
||||||
|
etudiant.justificatifs,
|
||||||
|
Justificatif,
|
||||||
|
date_deb=date_debut,
|
||||||
|
date_fin=date_fin,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ajout des options (exemple : mode_demi, show_pres, show_reta, ...)
|
||||||
|
for key, value in options.items():
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def instanciate_jour(self, date: datetime.date) -> JourAssi:
|
||||||
|
"""
|
||||||
|
Instancie un jour d'assiduité
|
||||||
|
"""
|
||||||
|
assiduites: Query = scass.filter_by_date(
|
||||||
|
self.etud_assiduites,
|
||||||
Assiduite,
|
Assiduite,
|
||||||
date_deb=datetime.datetime.combine(date, datetime.time(0, 0)),
|
date_deb=datetime.datetime.combine(date, datetime.time(0, 0)),
|
||||||
date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)),
|
date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)),
|
||||||
)
|
)
|
||||||
|
justificatifs: Query = scass.filter_by_date(
|
||||||
date_justificatifs: Query = scass.filter_by_date(
|
self.etud_justificatifs,
|
||||||
justificatifs,
|
|
||||||
Justificatif,
|
Justificatif,
|
||||||
date_deb=datetime.datetime.combine(date, datetime.time(0, 0)),
|
date_deb=datetime.datetime.combine(date, datetime.time(0, 0)),
|
||||||
date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)),
|
date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)),
|
||||||
)
|
)
|
||||||
# On génère un `Jour` composé d'une date, et des assiduités/justificatifs du jour
|
return JourAssi(date, assiduites, justificatifs, parent=self)
|
||||||
jour: Jour = Jour(date, date_assiduites, date_justificatifs)
|
|
||||||
|
|
||||||
organized[month].append(jour)
|
|
||||||
|
|
||||||
return organized
|
|
||||||
|
|
||||||
|
|
||||||
def _time_to_timedelta(t: datetime.time) -> datetime.timedelta:
|
def _time_to_timedelta(t: datetime.time) -> datetime.timedelta:
|
||||||
|
|
|
@ -1367,7 +1367,9 @@ def etudident_edit_form():
|
||||||
|
|
||||||
|
|
||||||
def _validate_date_naissance(val: str, field) -> bool:
|
def _validate_date_naissance(val: str, field) -> bool:
|
||||||
"vrai si date saisie valide"
|
"vrai si date saisie valide (peut être vide)"
|
||||||
|
if not val:
|
||||||
|
return True
|
||||||
try:
|
try:
|
||||||
date_naissance = scu.convert_fr_date(val)
|
date_naissance = scu.convert_fr_date(val)
|
||||||
except ScoValueError:
|
except ScoValueError:
|
||||||
|
@ -1788,7 +1790,11 @@ def _etudident_create_or_edit_form(edit):
|
||||||
+ homonyms_html
|
+ homonyms_html
|
||||||
+ F
|
+ F
|
||||||
)
|
)
|
||||||
tf[2]["date_naissance"] = scu.convert_fr_date(tf[2]["date_naissance"])
|
tf[2]["date_naissance"] = (
|
||||||
|
scu.convert_fr_date(tf[2]["date_naissance"])
|
||||||
|
if tf[2]["date_naissance"]
|
||||||
|
else None
|
||||||
|
)
|
||||||
if not edit:
|
if not edit:
|
||||||
etud = sco_etud.create_etud(cnx, args=tf[2])
|
etud = sco_etud.create_etud(cnx, args=tf[2])
|
||||||
etudid = etud["etudid"]
|
etudid = etud["etudid"]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.6.963"
|
SCOVERSION = "9.6.966"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@ mkdir -p "$optdir" || die "mkdir failure for $optdir"
|
||||||
archive="$FACTORY_DIR"/"$PACKAGE_NAME-$RELEASE_TAG".tar.gz
|
archive="$FACTORY_DIR"/"$PACKAGE_NAME-$RELEASE_TAG".tar.gz
|
||||||
echo "Downloading $GIT_RELEASE_URL ..."
|
echo "Downloading $GIT_RELEASE_URL ..."
|
||||||
# curl -o "$archive" "$GIT_RELEASE_URL" || die "curl failure for $GIT_RELEASE_URL"
|
# curl -o "$archive" "$GIT_RELEASE_URL" || die "curl failure for $GIT_RELEASE_URL"
|
||||||
#wget --progress=dot -O "$archive" "$GIT_RELEASE_URL" || die "wget failure for $GIT_RELEASE_URL"
|
wget --progress=dot -O "$archive" "$GIT_RELEASE_URL" || die "wget failure for $GIT_RELEASE_URL"
|
||||||
# -nv
|
# -nv
|
||||||
|
|
||||||
# On décomprime
|
# On décomprime
|
||||||
|
|
Loading…
Reference in New Issue