Compare commits
23 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 | |
Emmanuel Viennet | 87aaf12d27 | |
Emmanuel Viennet | c8ab9b9b6c | |
Emmanuel Viennet | ad7b48e110 | |
Emmanuel Viennet | f2ce16f161 |
152
README.md
152
README.md
|
@ -2,7 +2,7 @@
|
|||
|
||||
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
|
||||
|
||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
|
||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian12>
|
||||
|
||||
Documentation utilisateur: <https://scodoc.org>
|
||||
|
||||
|
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
|||
|
||||
### Lignes de commandes
|
||||
|
||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||
Voir [le guide de configuration](https://scodoc.org/GuideConfig).
|
||||
|
||||
## Organisation des fichiers
|
||||
|
||||
|
@ -41,45 +41,41 @@ Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configu
|
|||
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||
|
||||
Principaux contenus:
|
||||
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
|
||||
```
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
```
|
||||
## Pour les développeurs
|
||||
|
||||
### Installation du code
|
||||
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian12)).
|
||||
|
||||
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||
```bash
|
||||
sudo su
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
apt-get install git # si besoin
|
||||
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
|
||||
sudo su
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
apt-get install git # si besoin
|
||||
cd /opt
|
||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
|
||||
# Renommer le répertoire:
|
||||
mv ScoDoc scodoc
|
||||
|
||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc.scodoc /opt/scodoc
|
||||
|
||||
# Donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc:scodoc /opt/scodoc
|
||||
```
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
|
||||
```bash
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
```
|
||||
Et la config:
|
||||
|
||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||
|
||||
```bash
|
||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||
```
|
||||
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
|
||||
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
||||
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||
|
@ -88,11 +84,11 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
|||
|
||||
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||
Avant le premier lancement, créer cette base ainsi:
|
||||
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
|
||||
```bash
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
```
|
||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||
migrations (changements de schéma) ont eu lieu dans le code.
|
||||
|
@ -100,17 +96,17 @@ migrations (changements de schéma) ont eu lieu dans le code.
|
|||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
|
||||
```bash
|
||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||
```
|
||||
Puis dérouler les tests unitaires:
|
||||
|
||||
pytest tests/unit
|
||||
|
||||
```bash
|
||||
pytest tests/unit
|
||||
```
|
||||
Ou avec couverture (`pip install pytest-cov`)
|
||||
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
|
||||
```bash
|
||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||
```
|
||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
||||
|
||||
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||
|
@ -119,43 +115,43 @@ développement dans un état connu, par exemple pour éviter de recréer à la m
|
|||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||
par les tests:
|
||||
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
|
||||
```bash
|
||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||
```
|
||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||
normalement, par exemple:
|
||||
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
|
||||
```bash
|
||||
pytest tests/unit/test_sco_basic.py
|
||||
```
|
||||
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||
utilisateur:
|
||||
|
||||
flask user-password admin
|
||||
|
||||
```bash
|
||||
flask user-password admin
|
||||
```
|
||||
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
||||
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
||||
|
||||
#### Modification du schéma de la base
|
||||
|
||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
|
||||
```bash
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
```
|
||||
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
||||
|
||||
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
||||
ou variables d'environnement pour interroger la bonne base !).
|
||||
```bash
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
flask db upgrade # créé les tables à partir des migrations
|
||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
flask db upgrade # créé les tables à partir des migrations
|
||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
```
|
||||
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
||||
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||
positionner à la bonne étape.
|
||||
|
@ -163,23 +159,23 @@ positionner à la bonne étape.
|
|||
### Profiling
|
||||
|
||||
Sur une machine de DEV, lancer
|
||||
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
|
||||
```bash
|
||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||
```
|
||||
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
||||
|
||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||
|
||||
pip install snakeviz
|
||||
|
||||
```bash
|
||||
pip install snakeviz
|
||||
```
|
||||
puis
|
||||
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
|
||||
```bash
|
||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||
```
|
||||
## Paquet Debian 12
|
||||
|
||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||
important est `postinst`qui se charge de configurer le système (install ou
|
||||
important est `postinst` qui se charge de configurer le système (install ou
|
||||
upgrade de scodoc9).
|
||||
|
||||
La préparation d'une release se fait à l'aide du script
|
||||
|
|
|
@ -3,14 +3,15 @@
|
|||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Assiduités
|
||||
"""
|
||||
"""ScoDoc 9 API : Assiduités"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
|
||||
from app import db, log, set_sco_dept
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
|
@ -858,7 +859,10 @@ def assiduite_edit(assiduite_id: int):
|
|||
msg=f"assiduite: modif {assiduite_unique}",
|
||||
)
|
||||
db.session.commit()
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
try:
|
||||
scass.simple_invalidate_cache(assiduite_unique.to_dict())
|
||||
except ObjectDeletedError:
|
||||
return json_error(404, "Assiduité supprimée / inexistante")
|
||||
|
||||
return {"OK": True}
|
||||
|
||||
|
|
|
@ -427,12 +427,11 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
|||
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
||||
else "*"
|
||||
)
|
||||
note_value = e["note"].get("value", "")
|
||||
t = {
|
||||
"titre": f"{e['description'] or ''}",
|
||||
"moyenne": e["note"]["value"],
|
||||
"_moyenne_pdf": Paragraph(
|
||||
f"""<para align=right>{e["note"]["value"]}</para>"""
|
||||
),
|
||||
"moyenne": note_value,
|
||||
"_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
|
||||
"coef": coef,
|
||||
"_coef_pdf": Paragraph(
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
|
|
|
@ -432,6 +432,7 @@ class UniteEns(models.ScoDocModel):
|
|||
self.niveau_competence, parcours
|
||||
)
|
||||
if not ok:
|
||||
self.formation.invalidate_cached_sems()
|
||||
self.niveau_competence = prev_niveau # restore
|
||||
return False, error_message
|
||||
|
||||
|
|
|
@ -49,11 +49,13 @@
|
|||
"""
|
||||
import datetime
|
||||
import glob
|
||||
import gzip
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import zlib
|
||||
|
||||
import chardet
|
||||
|
||||
|
@ -241,11 +243,13 @@ class BaseArchiver:
|
|||
filename: str,
|
||||
data: str | bytes,
|
||||
dept_id: int = None,
|
||||
compress=False,
|
||||
):
|
||||
"""Store data in archive, under given filename.
|
||||
Filename may be modified (sanitized): return used filename
|
||||
The file is created or replaced.
|
||||
data may be str or bytes
|
||||
If compress, data is gziped and filename suffix ".gz" added.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
data = data.encode(scu.SCO_ENCODING)
|
||||
|
@ -255,8 +259,14 @@ class BaseArchiver:
|
|||
try:
|
||||
scu.GSL.acquire()
|
||||
fname = os.path.join(archive_id, filename)
|
||||
with open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
if compress:
|
||||
if not fname.endswith(".gz"):
|
||||
fname += ".gz"
|
||||
with gzip.open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
else:
|
||||
with open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
except FileNotFoundError as exc:
|
||||
raise ScoValueError(
|
||||
f"Erreur stockage archive (dossier inexistant, chemin {fname})"
|
||||
|
@ -274,8 +284,17 @@ class BaseArchiver:
|
|||
fname = os.path.join(archive_id, filename)
|
||||
log(f"reading archive file {fname}")
|
||||
try:
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
if fname.endswith(".gz"):
|
||||
try:
|
||||
with gzip.open(fname) as f:
|
||||
data = f.read()
|
||||
except (OSError, EOFError, zlib.error) as exc:
|
||||
raise ScoValueError(
|
||||
f"Erreur lecture archive ({fname} invalide)"
|
||||
) from exc
|
||||
else:
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
except FileNotFoundError as exc:
|
||||
raise ScoValueError(
|
||||
f"Erreur lecture archive (inexistant, chemin {fname})"
|
||||
|
@ -288,6 +307,8 @@ class BaseArchiver:
|
|||
"""
|
||||
archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
|
||||
data = self.get(archive_id, filename)
|
||||
if filename.endswith(".gz"):
|
||||
filename = filename[:-3]
|
||||
mime = mimetypes.guess_type(filename)[0]
|
||||
if mime is None:
|
||||
mime = "application/octet-stream"
|
||||
|
|
|
@ -68,7 +68,7 @@ PV_ARCHIVER = SemsArchiver()
|
|||
|
||||
|
||||
def do_formsemestre_archive(
|
||||
formsemestre_id,
|
||||
formsemestre: FormSemestre,
|
||||
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
|
||||
description="",
|
||||
date_jury="",
|
||||
|
@ -92,9 +92,8 @@ def do_formsemestre_archive(
|
|||
raise ScoValueError(
|
||||
"do_formsemestre_archive: version de bulletin demandée invalide"
|
||||
)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
sem_archive_id = formsemestre_id
|
||||
sem_archive_id = formsemestre.id
|
||||
archive_id = PV_ARCHIVER.create_obj_archive(
|
||||
sem_archive_id, description, formsemestre.dept_id
|
||||
)
|
||||
|
@ -102,9 +101,9 @@ def do_formsemestre_archive(
|
|||
|
||||
if not group_ids:
|
||||
# tous les inscrits du semestre
|
||||
group_ids = [sco_groups.get_default_group(formsemestre_id)]
|
||||
group_ids = [sco_groups.get_default_group(formsemestre.id)]
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(
|
||||
group_ids, formsemestre_id=formsemestre_id
|
||||
group_ids, formsemestre_id=formsemestre.id
|
||||
)
|
||||
groups_filename = "-" + groups_infos.groups_filename
|
||||
etudids = [m["etudid"] for m in groups_infos.members]
|
||||
|
@ -142,19 +141,23 @@ def do_formsemestre_archive(
|
|||
)
|
||||
|
||||
# Bulletins en JSON
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
||||
data = gen_formsemestre_recapcomplet_json(formsemestre.id, xml_with_decisions=True)
|
||||
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id
|
||||
archive_id,
|
||||
"Bulletins.json",
|
||||
data_js,
|
||||
dept_id=formsemestre.dept_id,
|
||||
compress=True,
|
||||
)
|
||||
# Décisions de jury, en XLS
|
||||
if formsemestre.formation.is_apc():
|
||||
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
|
||||
response = jury_but_pv.pvjury_page_but(formsemestre.id, fmt="xls")
|
||||
data = response.get_data()
|
||||
else: # formations classiques
|
||||
data = sco_pv_forms.formsemestre_pvjury(
|
||||
formsemestre_id, fmt="xls", publish=False
|
||||
formsemestre.id, fmt="xls", publish=False
|
||||
)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
|
@ -165,7 +168,7 @@ def do_formsemestre_archive(
|
|||
)
|
||||
# Classeur bulletins (PDF)
|
||||
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
|
||||
formsemestre_id, version=bul_version
|
||||
formsemestre.id, version=bul_version
|
||||
)
|
||||
if data:
|
||||
PV_ARCHIVER.store(
|
||||
|
@ -173,10 +176,11 @@ def do_formsemestre_archive(
|
|||
"Bulletins.pdf",
|
||||
data,
|
||||
dept_id=formsemestre.dept_id,
|
||||
compress=True,
|
||||
)
|
||||
# Lettres individuelles (PDF):
|
||||
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
|
||||
formsemestre_id,
|
||||
formsemestre.id,
|
||||
etudids=etudids,
|
||||
date_jury=date_jury,
|
||||
date_commission=date_commission,
|
||||
|
@ -217,7 +221,7 @@ def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
|
|||
"""Make and store new archive for this formsemestre.
|
||||
(all students or only selected groups)
|
||||
"""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
if not formsemestre.can_edit_pv():
|
||||
raise ScoPermissionDenied(
|
||||
dest_url=url_for(
|
||||
|
@ -320,7 +324,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
|
|||
else:
|
||||
tf[2]["anonymous"] = False
|
||||
do_formsemestre_archive(
|
||||
formsemestre_id,
|
||||
formsemestre,
|
||||
group_ids=group_ids,
|
||||
description=tf[2]["description"],
|
||||
date_jury=tf[2]["date_jury"],
|
||||
|
@ -352,7 +356,7 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
"""Page listing archives"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = formsemestre_id
|
||||
L = []
|
||||
archives_descr = []
|
||||
for archive_id in PV_ARCHIVER.list_obj_archives(
|
||||
sem_archive_id, dept_id=formsemestre.dept_id
|
||||
):
|
||||
|
@ -366,28 +370,30 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
archive_id, dept_id=formsemestre.dept_id
|
||||
),
|
||||
}
|
||||
L.append(a)
|
||||
archives_descr.append(a)
|
||||
|
||||
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
|
||||
if not L:
|
||||
if not archives_descr:
|
||||
H.append("<p>aucune archive enregistrée</p>")
|
||||
else:
|
||||
H.append("<ul>")
|
||||
for a in L:
|
||||
for a in archives_descr:
|
||||
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
|
||||
H.append(
|
||||
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
|
||||
% (
|
||||
a["date"].strftime("%d/%m/%Y %H:%M"),
|
||||
a["description"],
|
||||
formsemestre_id,
|
||||
archive_name,
|
||||
)
|
||||
f"""<li>{a["date"].strftime("%d/%m/%Y %H:%M")} : <em>{a["description"]}</em>
|
||||
(<a href="{ url_for( "notes.formsemestre_delete_archive", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id, archive_name=archive_name
|
||||
)}">supprimer</a>)
|
||||
<ul>"""
|
||||
)
|
||||
for filename in a["content"]:
|
||||
H.append(
|
||||
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
|
||||
% (formsemestre_id, archive_name, filename, filename)
|
||||
f"""<li><a href="{
|
||||
url_for( "notes.formsemestre_get_archived_file", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
archive_name=archive_name,
|
||||
filename=filename
|
||||
)}">{filename[:-3] if filename.endswith(".gz") else filename}</a></li>"""
|
||||
)
|
||||
if not a["content"]:
|
||||
H.append("<li><em>aucun fichier !</em></li>")
|
||||
|
@ -399,7 +405,7 @@ def formsemestre_list_archives(formsemestre_id):
|
|||
|
||||
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
|
||||
"""Send file to client."""
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
sem_archive_id = formsemestre.id
|
||||
return PV_ARCHIVER.get_archived_file(
|
||||
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id
|
||||
|
|
|
@ -230,41 +230,41 @@ def next_iso_day(date):
|
|||
|
||||
def YearTable(
|
||||
year,
|
||||
events=[],
|
||||
events_by_day: dict[str, list[dict]],
|
||||
firstmonth=9,
|
||||
lastmonth=7,
|
||||
halfday=0,
|
||||
dayattributes="",
|
||||
pad_width=8,
|
||||
):
|
||||
# Code simplifié en 2024: utilisé seulement pour calendrier évaluations
|
||||
"""Generate a calendar table
|
||||
events = list of tuples (date, text, color, href [,halfday])
|
||||
where date is a string in ISO format (yyyy-mm-dd)
|
||||
halfday is boolean (true: morning, false: afternoon)
|
||||
text = text to put in calendar (must be short, 1-5 cars) (optional)
|
||||
if halfday, generate 2 cells per day (morning, afternoon)
|
||||
"""
|
||||
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>")
|
||||
month = firstmonth
|
||||
while 1:
|
||||
while True:
|
||||
T.append('<td valign="top">')
|
||||
T.append(MonthTableHead(month))
|
||||
T.append(_month_table_head(month))
|
||||
T.append(
|
||||
MonthTableBody(
|
||||
_month_table_body(
|
||||
month,
|
||||
year,
|
||||
events,
|
||||
halfday,
|
||||
events_by_day,
|
||||
dayattributes,
|
||||
is_work_saturday(),
|
||||
pad_width=pad_width,
|
||||
)
|
||||
)
|
||||
T.append(MonthTableTail())
|
||||
T.append("</td>")
|
||||
T.append(
|
||||
"""
|
||||
</table>
|
||||
</td>"""
|
||||
)
|
||||
if month == lastmonth:
|
||||
break
|
||||
month = month + 1
|
||||
|
@ -322,29 +322,32 @@ WEEKDAYCOLOR = GRAY1
|
|||
WEEKENDCOLOR = GREEN3
|
||||
|
||||
|
||||
def MonthTableHead(month):
|
||||
def _month_table_head(month):
|
||||
color = WHITE
|
||||
return """<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""" % (
|
||||
color,
|
||||
MONTHNAMES_ABREV[month - 1],
|
||||
)
|
||||
return f"""<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
|
||||
<tr bgcolor="{color}">
|
||||
<td class="calcol" colspan="2" align="center">{MONTHNAMES_ABREV[month - 1]}</td>
|
||||
</tr>\n"""
|
||||
|
||||
|
||||
def MonthTableTail():
|
||||
return "</table>\n"
|
||||
|
||||
|
||||
def MonthTableBody(
|
||||
month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8
|
||||
):
|
||||
def _month_table_body(
|
||||
month,
|
||||
year,
|
||||
events_by_day: dict[str, list[dict]],
|
||||
trattributes="",
|
||||
work_saturday=False,
|
||||
) -> str:
|
||||
"""
|
||||
events : [event]
|
||||
event = [ yyyy-mm-dd, legend, href, color, descr ] XXX
|
||||
"""
|
||||
firstday, nbdays = calendar.monthrange(year, month)
|
||||
localtime = time.localtime()
|
||||
current_weeknum = time.strftime("%U", localtime)
|
||||
current_year = localtime[0]
|
||||
T = []
|
||||
rows = []
|
||||
# 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:
|
||||
monday = monday.prev()
|
||||
|
||||
|
@ -353,158 +356,51 @@ def MonthTableBody(
|
|||
else:
|
||||
weekend = ("S", "D")
|
||||
|
||||
if not halfday:
|
||||
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
|
||||
color = None
|
||||
legend = ""
|
||||
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:
|
||||
ev_year = int(ev[0][:4])
|
||||
ev_month = int(ev[0][5:7])
|
||||
ev_day = int(ev[0][8:10])
|
||||
if year == ev_year and month == ev_month and ev_day == d:
|
||||
if ev[1]:
|
||||
legend = ev[1]
|
||||
if ev[2]:
|
||||
color = ev[2]
|
||||
if ev[3]:
|
||||
href = ev[3]
|
||||
if len(ev) > 4 and ev[4]:
|
||||
descr = ev[4]
|
||||
#
|
||||
cc = []
|
||||
if color is not None:
|
||||
cc.append('<td bgcolor="%s" class="calcell">' % color)
|
||||
else:
|
||||
cc.append('<td class="calcell">')
|
||||
|
||||
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
|
||||
# events this day ?
|
||||
events = events_by_day.get(f"{year}-{month:02}-{d:02}", [])
|
||||
color = None
|
||||
ev_txts = []
|
||||
for ev in events:
|
||||
color = ev.get("color")
|
||||
href = ev.get("href", "")
|
||||
description = ev.get("description", "")
|
||||
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:
|
||||
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)
|
||||
)
|
||||
href = f'href="{href}"'
|
||||
if description:
|
||||
description = f"""title="{html.escape(description, quote=True)}" """
|
||||
if href or description:
|
||||
ev_txts.append(f"""<a {href} {description}>{ev.get("title", "")}</a>""")
|
||||
else:
|
||||
legend = " " # empty cell
|
||||
cc.append(legend)
|
||||
if href or descr:
|
||||
cc.append("</a>")
|
||||
cc.append("</td>")
|
||||
cell = "".join(cc)
|
||||
if day == "D":
|
||||
monday = monday.next_day(7)
|
||||
if (
|
||||
weeknum == current_weeknum
|
||||
and current_year == year
|
||||
and weekclass != "wkend"
|
||||
):
|
||||
weekclass += " currentweek"
|
||||
T.append(
|
||||
'<tr bgcolor="%s" class="%s" %s><td class="calday">%d%s</td>%s</tr>'
|
||||
% (bgcolor, weekclass, attrs, d, day, cell)
|
||||
)
|
||||
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"
|
||||
ev_txts.append(ev.get("title", " "))
|
||||
#
|
||||
cc = []
|
||||
if color is not None:
|
||||
cc.append(f'<td bgcolor="{color}" class="calcell">')
|
||||
else:
|
||||
cc.append('<td class="calcell">')
|
||||
|
||||
if day == "D":
|
||||
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)
|
||||
cc.append(f"{', '.join(ev_txts)}</td>")
|
||||
cells = "".join(cc)
|
||||
if day == "D":
|
||||
monday = monday.next_day(7)
|
||||
if weeknum == current_weeknum and current_year == year and weekclass != "wkend":
|
||||
weekclass += " currentweek"
|
||||
rows.append(
|
||||
f"""<tr bgcolor="{bgcolor}" class="{weekclass}" {attrs}>
|
||||
<td class="calday">{d}{day}</td>{cells}</tr>"""
|
||||
)
|
||||
|
||||
return "\n".join(rows)
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
"""Evaluations
|
||||
"""
|
||||
"""Evaluations"""
|
||||
|
||||
import collections
|
||||
import datetime
|
||||
import operator
|
||||
|
@ -50,6 +50,7 @@ from app.scodoc import sco_cal
|
|||
from app.scodoc import sco_evaluation_db
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_gen_cal
|
||||
from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
|
@ -360,6 +361,106 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
|
|||
return etat
|
||||
|
||||
|
||||
class JourEval(sco_gen_cal.Jour):
|
||||
"""
|
||||
Représentation d'un jour dans un calendrier d'évaluations
|
||||
"""
|
||||
|
||||
COLOR_INCOMPLETE = "#FF6060"
|
||||
COLOR_COMPLETE = "#A0FFA0"
|
||||
COLOR_FUTUR = "#70E0FF"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
date: datetime.date,
|
||||
evaluations: list[Evaluation],
|
||||
parent: "CalendrierEval",
|
||||
):
|
||||
super().__init__(date)
|
||||
|
||||
self.evaluations: list[Evaluation] = evaluations
|
||||
self.evaluations.sort(key=lambda e: e.date_debut)
|
||||
|
||||
self.parent: "CalendrierEval" = parent
|
||||
|
||||
def get_html(self) -> str:
|
||||
htmls = []
|
||||
|
||||
for e in self.evaluations:
|
||||
url: str = url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=e.moduleimpl_id,
|
||||
)
|
||||
title: str = (
|
||||
e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
|
||||
)
|
||||
htmls.append(
|
||||
f"""<a
|
||||
href="{url}"
|
||||
style="{self._get_eval_style(e)}"
|
||||
title="{self._get_eval_title(e)}"
|
||||
class="stdlink"
|
||||
>{title}</a>"""
|
||||
)
|
||||
|
||||
return ", ".join(htmls)
|
||||
|
||||
def _get_eval_style(self, e: Evaluation) -> str:
|
||||
color: str = ""
|
||||
# Etat (notes completes) de l'évaluation:
|
||||
modimpl_result = self.parent.nt.modimpls_results[e.moduleimpl.id]
|
||||
if modimpl_result.evaluations_etat[e.id].is_complete:
|
||||
color = JourEval.COLOR_COMPLETE
|
||||
else:
|
||||
color = JourEval.COLOR_INCOMPLETE
|
||||
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
|
||||
color = JourEval.COLOR_FUTUR
|
||||
|
||||
return f"background-color: {color};"
|
||||
|
||||
def _get_eval_title(self, e: Evaluation) -> str:
|
||||
heure_debut_txt, heure_fin_txt = "", ""
|
||||
if e.date_debut != e.date_fin:
|
||||
heure_debut_txt = (
|
||||
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
|
||||
)
|
||||
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
|
||||
|
||||
title = f"{e.description or e.moduleimpl.module.titre_str()}"
|
||||
if heure_debut_txt:
|
||||
title += f" de {heure_debut_txt} à {heure_fin_txt}"
|
||||
|
||||
return title
|
||||
|
||||
|
||||
class CalendrierEval(sco_gen_cal.Calendrier):
|
||||
"""
|
||||
Représentation des évaluations d'un semestre dans un calendrier
|
||||
"""
|
||||
|
||||
def __init__(self, year: int, evals: list[Evaluation], nt: NotesTableCompat):
|
||||
# On prend du 01/09 au 31/08
|
||||
date_debut: datetime.datetime = datetime.datetime(year, 9, 1, 0, 0)
|
||||
date_fin: datetime.datetime = datetime.datetime(year + 1, 8, 31, 23, 59)
|
||||
super().__init__(date_debut, date_fin)
|
||||
|
||||
# évalutions du semestre
|
||||
self.evals: dict[datetime.date, list[Evaluation]] = {}
|
||||
for e in evals:
|
||||
if e.date_debut is not None:
|
||||
day = e.date_debut.date()
|
||||
if day not in self.evals:
|
||||
self.evals[day] = []
|
||||
self.evals[day].append(e)
|
||||
|
||||
self.nt: NotesTableCompat = nt
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> JourEval:
|
||||
return JourEval(date, self.evals.get(date, []), parent=self)
|
||||
|
||||
|
||||
# View
|
||||
def formsemestre_evaluations_cal(formsemestre_id):
|
||||
"""Page avec calendrier de toutes les evaluations de ce semestre"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
@ -368,58 +469,9 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||
evaluations = formsemestre.get_evaluations()
|
||||
nb_evals = len(evaluations)
|
||||
|
||||
color_incomplete = "#FF6060"
|
||||
color_complete = "#A0FFA0"
|
||||
color_futur = "#70E0FF"
|
||||
|
||||
year = formsemestre.annee_scolaire()
|
||||
events = {} # (day, halfday) : event
|
||||
for e in evaluations:
|
||||
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
|
||||
)
|
||||
cal = CalendrierEval(year, evaluations, nt)
|
||||
cal_html = cal.get_html()
|
||||
|
||||
return f"""
|
||||
{
|
||||
|
@ -435,15 +487,15 @@ def formsemestre_evaluations_cal(formsemestre_id):
|
|||
</p>
|
||||
<ul>
|
||||
<li>en <span style=
|
||||
"background-color: {color_incomplete}">rouge</span>
|
||||
"background-color: {JourEval.COLOR_INCOMPLETE}">rouge</span>
|
||||
les évaluations passées auxquelles il manque des notes
|
||||
</li>
|
||||
<li>en <span style=
|
||||
"background-color: {color_complete}">vert</span>
|
||||
"background-color: {JourEval.COLOR_COMPLETE}">vert</span>
|
||||
les évaluations déjà notées
|
||||
</li>
|
||||
<li>en <span style=
|
||||
"background-color: {color_futur}">bleu</span>
|
||||
"background-color: {JourEval.COLOR_FUTUR}">bleu</span>
|
||||
les évaluations futures
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -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é",
|
||||
},
|
||||
),
|
||||
# (
|
||||
# "forcer_present",
|
||||
# {
|
||||
# "initvalue": 0,
|
||||
# "title": "Forcer l'appel des présents",
|
||||
# "input_type": "boolcheckbox",
|
||||
# "labels": ["non", "oui"],
|
||||
# "category": "assi",
|
||||
# },
|
||||
# ),
|
||||
(
|
||||
"non_present",
|
||||
{
|
||||
"initvalue": 0,
|
||||
"title": "Désactiver la saisie des présences",
|
||||
"input_type": "boolcheckbox",
|
||||
"labels": ["non", "oui"],
|
||||
"category": "assi",
|
||||
"explanation": "Désactive la saisie et l'affichage des présences",
|
||||
},
|
||||
),
|
||||
(
|
||||
"periode_defaut",
|
||||
{
|
||||
|
@ -644,18 +645,18 @@ class BasePreferences:
|
|||
"category": "assi",
|
||||
},
|
||||
),
|
||||
(
|
||||
"assi_etat_defaut",
|
||||
{
|
||||
"explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||
"initvalue": "aucun",
|
||||
"input_type": "menu",
|
||||
"labels": ["aucun", "present", "retard", "absent"],
|
||||
"allowed_values": ["aucun", "present", "retard", "absent"],
|
||||
"title": "Définir l'état par défaut",
|
||||
"category": "assi",
|
||||
},
|
||||
),
|
||||
# (
|
||||
# "assi_etat_defaut",
|
||||
# {
|
||||
# "explanation": "⚠ non fonctionnel, travaux en cours !",
|
||||
# "initvalue": "aucun",
|
||||
# "input_type": "menu",
|
||||
# "labels": ["aucun", "present", "retard", "absent"],
|
||||
# "allowed_values": ["aucun", "present", "retard", "absent"],
|
||||
# "title": "Définir l'état par défaut",
|
||||
# "category": "assi",
|
||||
# },
|
||||
# ),
|
||||
(
|
||||
"non_travail",
|
||||
{
|
||||
|
|
|
@ -730,31 +730,11 @@ tr.row-justificatif.non_valide td.assi-type {
|
|||
background-color: var(--color-defaut) !important;
|
||||
}
|
||||
|
||||
.color.est_just.sans_etat::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
background-color: var(--color-justi) !important;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.color.invalide::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
.color.invalide {
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
}
|
||||
|
||||
.color.attente::before,
|
||||
.color.modifie::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
.color.attente {
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
|
@ -762,6 +742,10 @@ tr.row-justificatif.non_valide td.assi-type {
|
|||
var(--color-justi-attente) 7px) !important;
|
||||
}
|
||||
|
||||
.color.est_just {
|
||||
background-color: var(--color-justi) !important;
|
||||
}
|
||||
|
||||
#gtrcontent .pdp {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
.day .dayline {
|
||||
.jour .dayline {
|
||||
position: absolute;
|
||||
display: none;
|
||||
top: 100%;
|
||||
z-index: 50;
|
||||
width: max-content;
|
||||
height: 75px;
|
||||
background-color: #dedede;
|
||||
border-radius: 15px;
|
||||
padding: 5px;
|
||||
border-radius: 8px;
|
||||
padding: 5px 5px 15px 5px;
|
||||
transform: translateX(-50%);
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
.day:hover .dayline {
|
||||
.jour:hover .dayline {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.dayline .mini-timeline {
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
|
|
@ -296,7 +296,13 @@ function creerLigneEtudiant(etud, index) {
|
|||
// Création des boutons d'assiduités
|
||||
if (readOnly) {
|
||||
} else if (currentAssiduite.type != "conflit") {
|
||||
["present", "retard", "absent"].forEach((abs) => {
|
||||
const etats = ["retard", "absent"];
|
||||
|
||||
if (!window.nonPresent) {
|
||||
etats.splice(0, 0, "present");
|
||||
}
|
||||
|
||||
etats.forEach((abs) => {
|
||||
const btn = document.createElement("input");
|
||||
btn.type = "checkbox";
|
||||
btn.value = abs;
|
||||
|
@ -425,7 +431,7 @@ async function getModuleImpl(assiduite) {
|
|||
return res.json();
|
||||
})
|
||||
.then((data) => {
|
||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ''}`;
|
||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ""}`;
|
||||
return moduleimpls[id];
|
||||
})
|
||||
.catch((_) => {
|
||||
|
@ -531,12 +537,7 @@ async function MiseAJourLigneEtud(etud) {
|
|||
|
||||
async function actionAssiduite(etud, etat, type, assiduite = null) {
|
||||
const modimpl_id = $("#moduleimpl_select").val();
|
||||
if (
|
||||
assiduite &&
|
||||
assiduite.etat.toLowerCase() === etat &&
|
||||
assiduite.moduleimpl_id == modimpl_id
|
||||
)
|
||||
type = "suppression";
|
||||
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
|
||||
|
||||
const { deb, fin } = getPeriodAsDate();
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Liste simple d'étudiants
|
||||
"""
|
||||
"""Liste simple d'étudiants"""
|
||||
|
||||
import datetime
|
||||
from flask import g, url_for
|
||||
from app import log
|
||||
|
@ -140,6 +140,13 @@ class RowAssi(tb.Row):
|
|||
)
|
||||
stats = self._get_etud_stats(etud)
|
||||
for key, value in stats.items():
|
||||
if key == "present" and sco_preferences.get_preference(
|
||||
"non_present",
|
||||
dept_id=g.scodoc_dept_id,
|
||||
formsemestre_id=self.table.formsemestre.id,
|
||||
):
|
||||
continue
|
||||
|
||||
self.add_cell(key, value[0], fmt_num(value[1] - value[2]), "assi_stats")
|
||||
if key != "present":
|
||||
self.add_cell(
|
||||
|
|
|
@ -23,49 +23,8 @@ Calendrier de l'assiduité
|
|||
for="mode_demi">mode demi journée</label>
|
||||
</div>
|
||||
|
||||
<div class="calendrier">
|
||||
{% for mois,jours in calendrier.items() %}
|
||||
<div class="month">
|
||||
<h3>{{mois}}</h3>
|
||||
<div class="days {{'demi' if mode_demi else ''}}">
|
||||
{% for jour in jours %}
|
||||
{% if jour.is_non_work() %}
|
||||
<div class="day {{jour.get_class()}}">
|
||||
<span>{{jour.get_nom()}}</span>
|
||||
{% else %}
|
||||
<div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}">
|
||||
{% endif %}
|
||||
{% if mode_demi %}
|
||||
{% if not jour.is_non_work() %}
|
||||
<span>{{jour.get_nom()}}</span>
|
||||
<span class="{{jour.get_demi_class(True, show_pres,show_reta)}}"></span>
|
||||
<span class="{{jour.get_demi_class(False, show_pres,show_reta)}}"></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if not jour.is_non_work() %}
|
||||
<span>{{jour.get_nom(False)}}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not jour.is_non_work() and jour.has_assiduites()%}
|
||||
|
||||
<div class="dayline">
|
||||
<div class="dayline-title">
|
||||
<span>Assiduité du</span>
|
||||
<br>
|
||||
<span>{{jour.get_date()}}</span>
|
||||
{{jour.generate_minitimeline() | safe}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="cal">
|
||||
{{calendrier|safe}}
|
||||
<div class="annee">
|
||||
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||
année: </span>
|
||||
|
@ -77,36 +36,7 @@ Calendrier de l'assiduité
|
|||
|
||||
<div class="help">
|
||||
<h3>Calendrier</h3>
|
||||
<p>Code couleur</p>
|
||||
<ul class="couleurs">
|
||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||
</li>
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
||||
</li>
|
||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||
</li>
|
||||
|
||||
<li><span title="Quart Bleu" class="est_just demo"></span> → la période est couverte par un
|
||||
justificatif valide</li>
|
||||
<li><span title="Justif. non valide" class="invalide demo"></span> → la période est
|
||||
couverte par un justificatif non valide
|
||||
</li>
|
||||
<li><span title="Justif. en attente" class="attente demo"></span> → la période
|
||||
a un justificatif en attente de validation
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
|
||||
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||
</div>
|
||||
<ul class="couleurs print">
|
||||
<li><span title="Vert" class="present demo"></span> présence
|
||||
|
@ -158,57 +88,34 @@ Calendrier de l'assiduité
|
|||
|
||||
.calendrier {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
justify-content: center;
|
||||
overflow-x: scroll;
|
||||
border: 1px solid #444;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.month h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.day,
|
||||
.demi .day.color.nonwork {
|
||||
text-align: left;
|
||||
margin: 2px;
|
||||
cursor: default;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
min-width: 6em;
|
||||
.assi_case {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demo.invalide {
|
||||
background-color: var(--color-justi-invalide) !important;
|
||||
.assi_case > span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.demo.attente {
|
||||
background: repeating-linear-gradient(to bottom,
|
||||
var(--color-justi-attente-stripe) 0px,
|
||||
var(--color-justi-attente-stripe) 4px,
|
||||
var(--color-justi-attente) 4px,
|
||||
var(--color-justi-attente) 7px) !important;
|
||||
.assi_case>span:last-of-type {
|
||||
border-left: #d5d5d5 solid 1px;
|
||||
}
|
||||
.assi_case>span:first-of-type {
|
||||
border-right: #d5d5d5 solid 1px;
|
||||
}
|
||||
|
||||
.demo.est_just {
|
||||
background-color: var(--color-justi) !important;
|
||||
.dayline{
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.demi .day.nonwork>span {
|
||||
flex: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.demi .day {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
|
||||
@media print {
|
||||
|
||||
.couleurs.print {
|
||||
|
@ -338,7 +245,5 @@ Calendrier de l'assiduité
|
|||
window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock app_content %}
|
|
@ -310,8 +310,13 @@ async function nouvellePeriode(period = null) {
|
|||
|
||||
const assi_btns = document.createElement('div');
|
||||
assi_btns.classList.add('assi-btns');
|
||||
const etats = ["retard", "absent"];
|
||||
|
||||
["present", "retard", "absent"].forEach((value) => {
|
||||
if(!window.nonPresent){
|
||||
etats.splice(0,0,"present");
|
||||
}
|
||||
|
||||
etats.forEach((value) => {
|
||||
const cbox = document.createElement("input");
|
||||
cbox.type = "checkbox";
|
||||
cbox.value = value;
|
||||
|
@ -499,6 +504,8 @@ const moduleimpls = new Map();
|
|||
const inscriptionsModules = new Map();
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
||||
|
||||
// Vérification du forçage de module
|
||||
window.forceModule = "{{ forcer_module }}" == "True";
|
||||
if (window.forceModule) {
|
||||
|
@ -518,12 +525,29 @@ if (window.forceModule) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
const defaultPlage = {{ nouv_plage | safe}} || [];
|
||||
|
||||
/**
|
||||
* Fonction exécutée au lancement de la page
|
||||
* - On affiche ou non les photos des étudiants
|
||||
* - On vérifie si la date est un jour travaillé
|
||||
*/
|
||||
async function main() {
|
||||
|
||||
// On initialise les sélecteurs avec les valeurs par défaut (si elles existent)
|
||||
if (defaultPlage.every((e) => e)) {
|
||||
$("#date").datepicker("setDate", defaultPlage[0]);
|
||||
$("#debut").val(defaultPlage[1]);
|
||||
$("#fin").val(defaultPlage[2]);
|
||||
|
||||
// On ajoute la période si la date est un jour travaillé
|
||||
if(dateCouranteEstTravaillee()){
|
||||
await nouvellePeriode();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const checked = localStorage.getItem("scodoc-etud-pdp") == "true";
|
||||
afficherPDP(checked);
|
||||
$("#date").on("change", async function (d) {
|
||||
|
@ -532,7 +556,7 @@ async function main() {
|
|||
});
|
||||
}
|
||||
|
||||
main();
|
||||
window.addEventListener("load", main);
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -600,7 +624,9 @@ main();
|
|||
Intialiser les étudiants comme :
|
||||
<select name="etatDef" id="etatDef">
|
||||
<option value="">-</option>
|
||||
{% if not non_present %}
|
||||
<option value="present">présents</option>
|
||||
{% endif %}
|
||||
<option value="retard">en retard</option>
|
||||
<option value="absent">absents</option>
|
||||
</select>
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
const readOnly = {{ readonly }};
|
||||
|
||||
window.forceModule = "{{ forcer_module }}" == "True"
|
||||
window.nonPresent = {{ 'true' if non_present else 'false' }};
|
||||
|
||||
const etudsDefDem = {{ defdem | safe }}
|
||||
|
||||
|
@ -159,8 +160,10 @@
|
|||
<div class="mass-selection">
|
||||
<span>Mettre tout le monde :</span>
|
||||
<fieldset class="btns_field mass">
|
||||
{% if not non_present %}
|
||||
<input type="checkbox" value="present" name="mass_btn_assiduites" id="mass_rbtn_present"
|
||||
class="rbtn present" onclick="mettreToutLeMonde('present', this)" title="Present">
|
||||
{% endif %}
|
||||
<input type="checkbox" value="retard" name="mass_btn_assiduites" id="mass_rbtn_retard"
|
||||
class="rbtn retard" onclick="mettreToutLeMonde('retard', this)" title="Retard">
|
||||
<input type="checkbox" value="absent" name="mass_btn_assiduites" id="mass_rbtn_absent"
|
||||
|
@ -178,6 +181,11 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="help">
|
||||
<h3>Calendrier</h3>
|
||||
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||
</div>
|
||||
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
|
|
|
@ -1,12 +1,28 @@
|
|||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la période
|
||||
</li>
|
||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la période
|
||||
</li>
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la période
|
||||
</li>
|
||||
<p>Code couleur</p>
|
||||
<ul class="couleurs">
|
||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||
</li>
|
||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
||||
</li>
|
||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
||||
période
|
||||
</li>
|
||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||
</li>
|
||||
|
||||
<li><span title="Hachure Bleue" class="justified demo"></span> → l'assiduité est justifiée par un
|
||||
justificatif valide</li>
|
||||
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> → l'assiduité est
|
||||
justifiée par un justificatif non valide / en attente de validation
|
||||
</li>
|
||||
<li><span title="Quart Bleu" class="est_just demo color"></span> → la période est couverte par un
|
||||
justificatif valide</li>
|
||||
<li><span title="Justif. non valide" class="invalide demo color "></span> → la période est
|
||||
couverte par un justificatif non valide
|
||||
</li>
|
||||
<li><span title="Justif. en attente" class="attente demo color"></span> → la période
|
||||
a un justificatif en attente de validation
|
||||
</li>
|
||||
</ul>
|
||||
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
|
||||
|
|
|
@ -74,7 +74,13 @@
|
|||
setupAssiduiteBubble(block, assiduité);
|
||||
}
|
||||
|
||||
// TODO: ajout couleur justificatif
|
||||
// ajout couleur justificatif
|
||||
const justificatifs = assiduité.justificatifs || [];
|
||||
const justified = justificatifs.some(
|
||||
(justificatif) => justificatif.etat === "VALIDE"
|
||||
)
|
||||
|
||||
if(justified) block.classList.add("est_just");
|
||||
|
||||
block.classList.add(assiduité.etat.toLowerCase());
|
||||
if(assiduité.etat != "CRENEAU") block.classList.add("color");
|
||||
|
|
|
@ -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>
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
<h2>Accès non autorisé</h2>
|
||||
|
||||
{{ exc | safe }}
|
||||
{{ exc }}
|
||||
|
||||
<p class="footer">
|
||||
{% if g.scodoc_dept %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<h2>Erreur !</h2>
|
||||
|
||||
{{ exc | safe }}
|
||||
{{ exc }}
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
{% if g.scodoc_dept %}
|
||||
|
|
|
@ -63,6 +63,7 @@ from app.models import (
|
|||
Scolog,
|
||||
)
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
|
||||
from app.auth.models import User
|
||||
from app.models.assiduites import get_assiduites_justif
|
||||
from app.tables.list_etuds import RowEtud, TableEtud
|
||||
|
@ -82,6 +83,7 @@ from app.scodoc import sco_find_etud
|
|||
from app.scodoc import sco_assiduites as scass
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_gen_cal
|
||||
|
||||
|
||||
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
|
||||
|
@ -757,8 +759,6 @@ def _verif_date_form_justif(
|
|||
deb = deb.replace(hour=0, minute=0)
|
||||
fin = fin.replace(hour=23, minute=59)
|
||||
|
||||
print(f"DEBUG {cas=}")
|
||||
|
||||
return deb, fin
|
||||
|
||||
|
||||
|
@ -925,7 +925,14 @@ def calendrier_assi_etud():
|
|||
# (sera utilisé pour générer le selecteur d'année)
|
||||
annees_str: str = json.dumps(annees)
|
||||
|
||||
calendrier: dict[str, list["Jour"]] = generate_calendar(etud, annee)
|
||||
cal = CalendrierAssi(
|
||||
annee,
|
||||
etud,
|
||||
mode_demi=mode_demi,
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
)
|
||||
calendrier: str = cal.get_html()
|
||||
|
||||
# Peuplement du template jinja
|
||||
return render_template(
|
||||
|
@ -1132,6 +1139,11 @@ def signal_assiduites_group():
|
|||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
non_present=sco_preferences.get_preference(
|
||||
"non_present",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
formsemestre_date_debut=str(formsemestre.date_debut),
|
||||
formsemestre_date_fin=str(formsemestre.date_fin),
|
||||
formsemestre_id=formsemestre_id,
|
||||
|
@ -1914,8 +1926,29 @@ def _preparer_objet(
|
|||
@scodoc
|
||||
@permission_required(Permission.AbsChange)
|
||||
def signal_assiduites_diff():
|
||||
"""TODO documenter
|
||||
"""
|
||||
Utilisé notamment par "Saisie différée" sur tableau de bord semetstre"
|
||||
|
||||
Arguments de la requête:
|
||||
|
||||
- group_ids : liste des groupes
|
||||
example : group_ids=1,2,3
|
||||
- formsemestre_id : id du formsemestre
|
||||
example : formsemestre_id=1
|
||||
- moduleimpl_id : id du moduleimpl
|
||||
example : moduleimpl_id=1
|
||||
|
||||
(Permet de pré-générer une plage. Si non renseigné, la plage sera vide)
|
||||
(Les trois valeurs suivantes doivent être renseignées ensemble)
|
||||
- date
|
||||
example : date=01/01/2021
|
||||
- heure_debut
|
||||
example : heure_debut=08:00
|
||||
- heure_fin
|
||||
example : heure_fin=10:00
|
||||
|
||||
Exemple de requête :
|
||||
signal_assiduites_diff?formsemestre_id=67&group_ids=400&moduleimpl_id=1229&date=15/04/2024&heure_debut=12:34&heure_fin=12:55
|
||||
"""
|
||||
# Récupération des paramètres de la requête
|
||||
group_ids: list[int] = request.args.get("group_ids", None)
|
||||
|
@ -1957,11 +1990,23 @@ def signal_assiduites_diff():
|
|||
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
|
||||
)
|
||||
|
||||
# Pré-remplissage des sélecteurs
|
||||
moduleimpl_id = request.args.get("moduleimpl_id", -1)
|
||||
try:
|
||||
moduleimpl_id = int(moduleimpl_id)
|
||||
except ValueError:
|
||||
moduleimpl_id = -1
|
||||
# date fra (dd/mm/yyyy)
|
||||
date = request.args.get("date", "")
|
||||
# heures (hh:mm)
|
||||
heure_deb = request.args.get("heure_debut", "")
|
||||
heure_fin = request.args.get("heure_fin", "")
|
||||
|
||||
# vérifications des sélecteurs
|
||||
date = date if re.match(r"^\d{2}\/\d{2}\/\d{4}$", date) else ""
|
||||
heure_deb = heure_deb if re.match(r"^[0-2]\d:[0-5]\d$", heure_deb) else ""
|
||||
heure_fin = heure_fin if re.match(r"^[0-2]\d:[0-5]\d$", heure_fin) else ""
|
||||
nouv_plage: list[str] = [date, heure_deb, heure_fin]
|
||||
|
||||
return render_template(
|
||||
"assiduites/pages/signal_assiduites_diff.j2",
|
||||
|
@ -1977,6 +2022,12 @@ def signal_assiduites_diff():
|
|||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
non_present=sco_preferences.get_preference(
|
||||
"non_present",
|
||||
formsemestre_id=formsemestre_id,
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
nouv_plage=nouv_plage,
|
||||
)
|
||||
|
||||
|
||||
|
@ -2473,79 +2524,74 @@ def _get_etuds_dem_def(formsemestre) -> str:
|
|||
# --- Gestion du calendrier ---
|
||||
|
||||
|
||||
def generate_calendar(
|
||||
etudiant: Identite,
|
||||
annee: int = None,
|
||||
) -> dict[str, list["Jour"]]:
|
||||
class JourAssi(sco_gen_cal.Jour):
|
||||
"""
|
||||
Génère le calendrier d'assiduité de l'étudiant pour une année scolaire donnée
|
||||
"""
|
||||
# Si pas d'année alors on prend l'année scolaire en cours
|
||||
if annee is None:
|
||||
annee = scu.annee_scolaire()
|
||||
|
||||
# On prend du 01/09 au 31/08
|
||||
date_debut: datetime.datetime = datetime.datetime(annee, 9, 1, 0, 0)
|
||||
date_fin: datetime.datetime = datetime.datetime(annee + 1, 8, 31, 23, 59)
|
||||
|
||||
# Filtrage des assiduités et des justificatifs en fonction de la periode / année
|
||||
etud_assiduites: Query = scass.filter_by_date(
|
||||
etudiant.assiduites,
|
||||
Assiduite,
|
||||
date_deb=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
etud_justificatifs: Query = scass.filter_by_date(
|
||||
etudiant.justificatifs,
|
||||
Justificatif,
|
||||
date_deb=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
|
||||
# Récupération des jours de l'année et de leurs assiduités/justificatifs
|
||||
annee_par_mois: dict[str, list[Jour]] = _organize_by_month(
|
||||
_get_dates_between(
|
||||
deb=date_debut.date(),
|
||||
fin=date_fin.date(),
|
||||
),
|
||||
etud_assiduites,
|
||||
etud_justificatifs,
|
||||
)
|
||||
|
||||
return annee_par_mois
|
||||
|
||||
|
||||
class Jour:
|
||||
"""Jour
|
||||
Jour du calendrier
|
||||
get_nom : retourne le numéro et le nom du Jour (ex: M19 / Mer 19)
|
||||
Représente un jour d'assiduité
|
||||
"""
|
||||
|
||||
def __init__(self, date: datetime.date, assiduites: Query, justificatifs: Query):
|
||||
self.date = date
|
||||
def __init__(
|
||||
self,
|
||||
date: datetime.date,
|
||||
assiduites: Query,
|
||||
justificatifs: Query,
|
||||
parent: "CalendrierAssi",
|
||||
):
|
||||
super().__init__(date)
|
||||
|
||||
# assiduités et justificatifs du jour
|
||||
self.assiduites = assiduites
|
||||
self.justificatifs = justificatifs
|
||||
|
||||
def get_nom(self, mode_demi: bool = True) -> str:
|
||||
"""
|
||||
Renvoie le nom du jour
|
||||
"M19" ou "Mer 19"
|
||||
"""
|
||||
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
|
||||
return (
|
||||
f"{str_jour[0] if mode_demi or self.is_non_work() else str_jour[:3]+' '}"
|
||||
+ f"{self.date.day}"
|
||||
self.parent = parent
|
||||
|
||||
def get_html(self) -> str:
|
||||
# si non travaillé on renvoie une case vide
|
||||
if self.is_non_work():
|
||||
return ""
|
||||
|
||||
html: str = (
|
||||
self._get_html_demi() if self.parent.mode_demi else self._get_html_normal()
|
||||
)
|
||||
html = f'<div class="assi_case">{html}</div>'
|
||||
|
||||
def get_date(self) -> str:
|
||||
"""
|
||||
Renvoie la date du jour au format "dd/mm/yyyy"
|
||||
"""
|
||||
return self.date.strftime(scu.DATE_FMT)
|
||||
if self.has_assiduite():
|
||||
minitimeline: str = f"""
|
||||
<div class="dayline">
|
||||
<div class="dayline-title">
|
||||
<span>{self.get_date()}</span>
|
||||
{self._generate_minitimeline()}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
html += minitimeline
|
||||
|
||||
def get_class(self, show_pres: bool = False, show_reta: bool = False) -> str:
|
||||
return html
|
||||
|
||||
def has_assiduite(self) -> bool:
|
||||
"""Renvoie True si le jour a une assiduité"""
|
||||
return self.assiduites.count() > 0
|
||||
|
||||
def _get_html_normal(self) -> str:
|
||||
"""
|
||||
Retourne la classe css du jour (mode normal)
|
||||
Renvoie l'html de la case du calendrier
|
||||
(version journee normale (donc une couleur))
|
||||
"""
|
||||
class_name = self._get_color_normal()
|
||||
return f'<span class="{class_name}"></span>'
|
||||
|
||||
def _get_html_demi(self) -> str:
|
||||
"""
|
||||
Renvoie l'html de la case du calendrier
|
||||
(version journee divisée en demi-journées (donc 2 couleurs))
|
||||
"""
|
||||
matin = self._get_color_demi(True)
|
||||
aprem = self._get_color_demi(False)
|
||||
return f'<span class="{matin}"></span><span class="{aprem}"></span>'
|
||||
|
||||
def _get_color_normal(self) -> str:
|
||||
"""renvoie la classe css correspondant
|
||||
à la case du calendrier
|
||||
(version journee normale)
|
||||
"""
|
||||
etat = ""
|
||||
est_just = ""
|
||||
|
@ -2555,8 +2601,8 @@ class Jour:
|
|||
|
||||
etat = self._get_color_assiduites_cascade(
|
||||
self._get_etats_from_assiduites(self.assiduites),
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
show_pres=self.parent.show_pres,
|
||||
show_reta=self.parent.show_reta,
|
||||
)
|
||||
|
||||
est_just = self._get_color_justificatifs_cascade(
|
||||
|
@ -2565,13 +2611,11 @@ class Jour:
|
|||
|
||||
return f"color {etat} {est_just}"
|
||||
|
||||
def get_demi_class(
|
||||
self, matin: bool, show_pres: bool = False, show_reta: bool = False
|
||||
) -> str:
|
||||
def _get_color_demi(self, matin: bool) -> str:
|
||||
"""renvoie la classe css correspondant
|
||||
à la case du calendrier
|
||||
(version journee divisée en demi-journees)
|
||||
"""
|
||||
Renvoie la class css de la demi journée
|
||||
"""
|
||||
|
||||
heure_midi = scass.str_to_time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
|
||||
|
||||
if matin:
|
||||
|
@ -2603,8 +2647,8 @@ class Jour:
|
|||
|
||||
etat = self._get_color_assiduites_cascade(
|
||||
self._get_etats_from_assiduites(assiduites_matin),
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
show_pres=self.parent.show_pres,
|
||||
show_reta=self.parent.show_reta,
|
||||
)
|
||||
|
||||
est_just = self._get_color_justificatifs_cascade(
|
||||
|
@ -2643,8 +2687,8 @@ class Jour:
|
|||
|
||||
etat = self._get_color_assiduites_cascade(
|
||||
self._get_etats_from_assiduites(assiduites_aprem),
|
||||
show_pres=show_pres,
|
||||
show_reta=show_reta,
|
||||
show_pres=self.parent.show_pres,
|
||||
show_reta=self.parent.show_reta,
|
||||
)
|
||||
|
||||
est_just = self._get_color_justificatifs_cascade(
|
||||
|
@ -2653,77 +2697,6 @@ class Jour:
|
|||
|
||||
return f"color {etat} {est_just}"
|
||||
|
||||
def has_assiduites(self) -> bool:
|
||||
"""
|
||||
Renverra True si le jour a des assiduités
|
||||
"""
|
||||
return self.assiduites.count() > 0
|
||||
|
||||
def generate_minitimeline(self) -> str:
|
||||
"""
|
||||
Génère la minitimeline du jour
|
||||
"""
|
||||
# Récupérer le référenciel de la timeline
|
||||
heure_matin: datetime.timedelta = _time_to_timedelta(
|
||||
scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
|
||||
)
|
||||
heure_soir: datetime.timedelta = _time_to_timedelta(
|
||||
scass.str_to_time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00"))
|
||||
)
|
||||
# longueur_timeline = heure_soir - heure_matin
|
||||
longueur_timeline: datetime.timedelta = heure_soir - heure_matin
|
||||
|
||||
# chaque block d'assiduité est défini par:
|
||||
# longueur = ( (fin-deb) / longueur_timeline ) * 100
|
||||
# emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100
|
||||
# longueur + emplacement = 100% sinon on réduit longueur
|
||||
|
||||
assiduite_blocks: list[dict[str, float | str]] = []
|
||||
|
||||
for assi in self.assiduites:
|
||||
deb: datetime.timedelta = _time_to_timedelta(
|
||||
assi.date_debut.time()
|
||||
if assi.date_debut.date() == self.date
|
||||
else heure_matin
|
||||
)
|
||||
fin: datetime.timedelta = _time_to_timedelta(
|
||||
assi.date_fin.time()
|
||||
if assi.date_fin.date() == self.date
|
||||
else heure_soir
|
||||
)
|
||||
|
||||
emplacement: float = max(((deb - heure_matin) / longueur_timeline) * 100, 0)
|
||||
longueur: float = ((fin - deb) / longueur_timeline) * 100
|
||||
if longueur + emplacement > 100:
|
||||
longueur = 100 - emplacement
|
||||
etat: str = scu.EtatAssiduite(assi.etat).name.lower()
|
||||
est_just: str = "est_just" if assi.est_just else ""
|
||||
|
||||
assiduite_blocks.append(
|
||||
{
|
||||
"longueur": longueur,
|
||||
"emplacement": emplacement,
|
||||
"etat": etat,
|
||||
"est_just": est_just,
|
||||
"bubble": _generate_assiduite_bubble(assi),
|
||||
"id": assi.assiduite_id,
|
||||
}
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/widgets/minitimeline_simple.j2",
|
||||
assi_blocks=assiduite_blocks,
|
||||
)
|
||||
|
||||
def is_non_work(self):
|
||||
"""
|
||||
Renvoie True si le jour est un jour non travaillé
|
||||
(en fonction de la préférence du département)
|
||||
"""
|
||||
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
|
||||
def _get_etats_from_assiduites(self, assiduites: Query) -> list[scu.EtatAssiduite]:
|
||||
return list(set(scu.EtatAssiduite(assi.etat) for assi in assiduites))
|
||||
|
||||
|
@ -2762,47 +2735,110 @@ class Jour:
|
|||
|
||||
return ""
|
||||
|
||||
def _generate_minitimeline(self) -> str:
|
||||
"""
|
||||
Génère la minitimeline du jour
|
||||
"""
|
||||
# Récupérer le référenciel de la timeline
|
||||
heure_matin: datetime.timedelta = _time_to_timedelta(
|
||||
scass.str_to_time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
|
||||
)
|
||||
heure_soir: datetime.timedelta = _time_to_timedelta(
|
||||
scass.str_to_time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00"))
|
||||
)
|
||||
# longueur_timeline = heure_soir - heure_matin
|
||||
longueur_timeline: datetime.timedelta = heure_soir - heure_matin
|
||||
|
||||
def _get_dates_between(deb: datetime.date, fin: datetime.date) -> list[datetime.date]:
|
||||
resultat = []
|
||||
date_actuelle = deb
|
||||
while date_actuelle <= fin:
|
||||
resultat.append(date_actuelle)
|
||||
date_actuelle += datetime.timedelta(days=1)
|
||||
return resultat
|
||||
# chaque block d'assiduité est défini par:
|
||||
# longueur = ( (fin-deb) / longueur_timeline ) * 100
|
||||
# emplacement = ( (deb - heure_matin) / longueur_timeline ) * 100
|
||||
# longueur + emplacement = 100% sinon on réduit longueur
|
||||
|
||||
assiduite_blocks: list[dict[str, float | str]] = []
|
||||
|
||||
for assi in self.assiduites:
|
||||
deb: datetime.timedelta = _time_to_timedelta(
|
||||
assi.date_debut.time()
|
||||
if assi.date_debut.date() == self.date
|
||||
else heure_matin
|
||||
)
|
||||
fin: datetime.timedelta = _time_to_timedelta(
|
||||
assi.date_fin.time()
|
||||
if assi.date_fin.date() == self.date
|
||||
else heure_soir
|
||||
)
|
||||
|
||||
emplacement: float = max(((deb - heure_matin) / longueur_timeline) * 100, 0)
|
||||
longueur: float = ((fin - deb) / longueur_timeline) * 100
|
||||
if longueur + emplacement > 100:
|
||||
longueur = 100 - emplacement
|
||||
|
||||
etat: str = scu.EtatAssiduite(assi.etat).name.lower()
|
||||
est_just: str = "est_just" if assi.est_just else ""
|
||||
|
||||
assiduite_blocks.append(
|
||||
{
|
||||
"longueur": longueur,
|
||||
"emplacement": emplacement,
|
||||
"etat": etat,
|
||||
"est_just": est_just,
|
||||
"bubble": _generate_assiduite_bubble(assi),
|
||||
"id": assi.assiduite_id,
|
||||
}
|
||||
)
|
||||
|
||||
return render_template(
|
||||
"assiduites/widgets/minitimeline_simple.j2",
|
||||
assi_blocks=assiduite_blocks,
|
||||
)
|
||||
|
||||
|
||||
def _organize_by_month(days, assiduites, justificatifs) -> dict[str, list[Jour]]:
|
||||
class CalendrierAssi(sco_gen_cal.Calendrier):
|
||||
"""
|
||||
Organiser les dates par mois.
|
||||
Représente un calendrier d'assiduité d'un étudiant
|
||||
"""
|
||||
organized = {}
|
||||
for date in days:
|
||||
# Récupérer le mois en français
|
||||
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
|
||||
# Ajouter le jour à la liste correspondante au mois
|
||||
if month not in organized:
|
||||
organized[month] = []
|
||||
|
||||
date_assiduites: Query = scass.filter_by_date(
|
||||
assiduites,
|
||||
def __init__(self, annee: int, etudiant: Identite, **options):
|
||||
# On prend du 01/09 au 31/08
|
||||
date_debut: datetime.datetime = datetime.datetime(annee, 9, 1, 0, 0)
|
||||
date_fin: datetime.datetime = datetime.datetime(annee + 1, 8, 31, 23, 59)
|
||||
super().__init__(date_debut, date_fin)
|
||||
|
||||
# On récupère les assiduités et les justificatifs
|
||||
self.etud_assiduites: Query = scass.filter_by_date(
|
||||
etudiant.assiduites,
|
||||
Assiduite,
|
||||
date_deb=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
self.etud_justificatifs: Query = scass.filter_by_date(
|
||||
etudiant.justificatifs,
|
||||
Justificatif,
|
||||
date_deb=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
|
||||
# Ajout des options (exemple : mode_demi, show_pres, show_reta, ...)
|
||||
for key, value in options.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def instanciate_jour(self, date: datetime.date) -> JourAssi:
|
||||
"""
|
||||
Instancie un jour d'assiduité
|
||||
"""
|
||||
assiduites: Query = scass.filter_by_date(
|
||||
self.etud_assiduites,
|
||||
Assiduite,
|
||||
date_deb=datetime.datetime.combine(date, datetime.time(0, 0)),
|
||||
date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)),
|
||||
)
|
||||
|
||||
date_justificatifs: Query = scass.filter_by_date(
|
||||
justificatifs,
|
||||
justificatifs: Query = scass.filter_by_date(
|
||||
self.etud_justificatifs,
|
||||
Justificatif,
|
||||
date_deb=datetime.datetime.combine(date, datetime.time(0, 0)),
|
||||
date_fin=datetime.datetime.combine(date, datetime.time(23, 59, 59)),
|
||||
)
|
||||
# On génère un `Jour` composé d'une date, et des assiduités/justificatifs du jour
|
||||
jour: Jour = Jour(date, date_assiduites, date_justificatifs)
|
||||
|
||||
organized[month].append(jour)
|
||||
|
||||
return organized
|
||||
return JourAssi(date, assiduites, justificatifs, parent=self)
|
||||
|
||||
|
||||
def _time_to_timedelta(t: datetime.time) -> datetime.timedelta:
|
||||
|
|
|
@ -1367,7 +1367,9 @@ def etudident_edit_form():
|
|||
|
||||
|
||||
def _validate_date_naissance(val: str, field) -> bool:
|
||||
"vrai si date saisie valide"
|
||||
"vrai si date saisie valide (peut être vide)"
|
||||
if not val:
|
||||
return True
|
||||
try:
|
||||
date_naissance = scu.convert_fr_date(val)
|
||||
except ScoValueError:
|
||||
|
@ -1788,7 +1790,11 @@ def _etudident_create_or_edit_form(edit):
|
|||
+ homonyms_html
|
||||
+ F
|
||||
)
|
||||
tf[2]["date_naissance"] = scu.convert_fr_date(tf[2]["date_naissance"])
|
||||
tf[2]["date_naissance"] = (
|
||||
scu.convert_fr_date(tf[2]["date_naissance"])
|
||||
if tf[2]["date_naissance"]
|
||||
else None
|
||||
)
|
||||
if not edit:
|
||||
etud = sco_etud.create_etud(cnx, args=tf[2])
|
||||
etudid = etud["etudid"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.961"
|
||||
SCOVERSION = "9.6.966"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ mkdir -p "$optdir" || die "mkdir failure for $optdir"
|
|||
archive="$FACTORY_DIR"/"$PACKAGE_NAME-$RELEASE_TAG".tar.gz
|
||||
echo "Downloading $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
|
||||
|
||||
# On décomprime
|
||||
|
|
Loading…
Reference in New Issue