Compare commits

...

47 Commits

Author SHA1 Message Date
Emmanuel Viennet 043985bff6 cosmetic: calendrier evaluations 2024-05-17 15:23:29 +02:00
Emmanuel Viennet d20ada1797 Merge branch 'gen_cal' of https://scodoc.org/git/iziram/ScoDoc into gen_cal 2024-05-17 12:02:23 +02:00
Iziram 778fecabb6 sco_gen_cal : correction affichage semaine/année courante 2024-05-15 14:16:11 +02:00
Iziram fa6f83722e sco_gen_cal : ajout style semaine courante 2024-05-15 13:35:44 +02:00
Emmanuel Viennet baa0412071 Merge pull request 'Mise à jour du README' (#881) from lyanis/ScoDoc:readme into master
Reviewed-on: #881
2024-05-13 18:23:34 +02:00
Emmanuel Viennet d51a47b71a Fix: formulaire creation étudiant (date naissance vide) 2024-05-13 17:31:54 +02:00
Lyanis Souidi f21ef41de6 README: Mise en forme des blocs de code 2024-05-13 14:54:52 +02:00
Lyanis Souidi 2d673e7a5d Mise à jour du README 2024-05-13 11:16:10 +02:00
Emmanuel Viennet 3e43495831 Fix: bulletins pdf, notes évaluations sans notes. 2024-05-07 18:17:13 +02:00
Iziram a4db8c4ff8 utilisation sco_gen_cal pour calendrier evaluations #875 2024-05-07 16:47:08 +02:00
Iziram 1ac35d04c2 Assiduité : utilisation sco_gen_cal closes #877 2024-05-07 16:45:03 +02:00
Iziram 687ac3cf13 Assiduité : Généralisation du Calendrier WIP 2024-05-06 17:29:21 +02:00
Emmanuel Viennet 18b1f00586 version 9.6.964 2024-04-24 20:58:02 +02:00
Iziram 6b985620e9 Assiduité : signal_assiduites_diff : plage depuis args url closes #741 2024-04-24 20:55:21 +02:00
Iziram 4d234ba353 Assiduité : désactiver saisie présence closes #793 2024-04-24 20:55:21 +02:00
Iziram 5d45fcf656 Assiduité : signal_assiduites_group : bug fix suppr assi autre 2024-04-24 20:55:21 +02:00
Iziram 0a5919b788 Assiduité : pseudo-fix (catch + http error) #872 2024-04-24 20:55:21 +02:00
Iziram 09f4525e66 Assiduité : maj couleurs minitimeline + legende 2024-04-24 20:55:21 +02:00
Emmanuel Viennet 0bc57807de release build script using gitea again 2024-04-24 20:37:03 +02:00
Emmanuel Viennet 87aaf12d27 Protect against Reflected XSS on home page (and other exception-handling pages) 2024-04-23 18:28:00 +02:00
Emmanuel Viennet c8ab9b9b6c Invalidation cache lors d'une erreur sur association UE/Niveau. Peut-être cause de #874. 2024-04-15 18:06:26 +02:00
Emmanuel Viennet ad7b48e110 Calendrier évaluations: fix #875 2024-04-15 17:53:02 +02:00
Emmanuel Viennet f2ce16f161 Archive PV: gzip large files 2024-04-15 03:21:32 +02:00
Emmanuel Viennet 1ddf9b6ab8 Fix: création utilisateur si un seul département 2024-04-12 15:50:53 +02:00
Emmanuel Viennet 0a2e39cae1 Ajoute aide sur édition parcours UEs 2024-04-12 01:10:42 +02:00
Emmanuel Viennet a194b4b6e0 Edition parcours UE: si tous cochés, tronc commun 2024-04-12 01:05:02 +02:00
Emmanuel Viennet cbe85dfb7d anonymize_users: ignore admin 2024-04-12 01:04:27 +02:00
Emmanuel Viennet beba69bfe4 Améliore/met à jour tests unitaires API 2024-04-11 06:00:00 +02:00
Emmanuel Viennet 41fec29452 Bulletin BUT: ne mentionne pas les évaluations rattrapage/session2 sans notes. (c'est déjà le cas en classic) 2024-04-11 01:45:25 +02:00
Emmanuel Viennet 9bd05ea241 Modify SCO_URL in all js: no trailing slash. 2024-04-11 01:44:17 +02:00
Emmanuel Viennet 58b831513d Améliore traitement des erreurs lors de la génération des PDF 2024-04-10 15:29:30 +02:00
Emmanuel Viennet b861aba6a3 Tableaux génériques: possibilité de déclarer un colonne seulement pour excel. Assiduité: ajout etudid et NIP a visu_assi_group: closes #873. 2024-04-10 15:09:32 +02:00
Emmanuel Viennet c2443c361f Améliore page activation module entreprises. Implements #634 2024-04-09 00:36:46 +02:00
Emmanuel Viennet ab4731bd43 Suppression des anciennes fonctions ScoDoc7 donnant les URLs de base. 2024-04-08 18:57:00 +02:00
Emmanuel Viennet c17bc8b61b Fix: liste semestres avec code 2024-04-08 16:26:38 +02:00
Emmanuel Viennet e44a5ee55d Corrige templates formsemestre 2024-04-07 19:52:22 +02:00
Emmanuel Viennet a747ed22e2 Ajoute équivalences pour ref. comp. QLIO 2024-04-07 19:51:34 +02:00
Emmanuel Viennet 5d0a932634 Bulletins BUT: utilisation de l'abbréviation du titre module si présente. 2024-04-06 12:33:07 +02:00
Emmanuel Viennet 2b150cf521 Modif config Jina2. Refonte ScoData, fournit par défaut à tous les templates. 2024-04-06 12:16:53 +02:00
Emmanuel Viennet 5a5ddcacd7 Associer une formation BUT à un nouveau référentiel 'équivalent'. 2024-04-05 23:41:34 +02:00
Emmanuel Viennet 3f6e65b9da Elimine @cached_property sur Identite, pourrait provoquer incohérences temporaires en multithread 2024-04-05 11:00:01 +02:00
Emmanuel Viennet 5eba6170a5 Fix: typo bloquant affichage formations avec UEs sans semestre_idx 2024-04-05 10:11:34 +02:00
Emmanuel Viennet bd9bf87112 Enrichissement du tableau des formations (coche 'détails') 2024-04-05 00:23:29 +02:00
Emmanuel Viennet a0e2af481f Fonction expérimentale pour changer le ref. de compétences d'une formation 2024-04-05 00:22:14 +02:00
Emmanuel Viennet 42e8f97441 Fix: missing exception 2024-04-04 11:23:26 +02:00
Emmanuel Viennet 8ec0171ca0 Script préparation démos: renommage de tous les étudiants 2024-04-03 19:02:40 +02:00
Emmanuel Viennet 6dfab2d843 Fix typo affichage heures 2024-04-03 18:47:44 +02:00
105 changed files with 3310 additions and 2350 deletions

152
README.md
View File

@ -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

View File

@ -315,12 +315,6 @@ def create_app(config_class=DevConfig):
app.register_error_handler(503, postgresql_server_error) app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage) app.register_error_handler(APIInvalidParams, handle_invalid_usage)
# Add some globals
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
from app.auth import bp as auth_bp from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix="/auth") app.register_blueprint(auth_bp, url_prefix="/auth")
@ -338,8 +332,15 @@ def create_app(config_class=DevConfig):
from app.api import api_bp from app.api import api_bp
from app.api import api_web_bp from app.api import api_web_bp
# Jinja2 configuration
# Enable autoescaping of all templates, including .j2 # Enable autoescaping of all templates, including .j2
app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True) app.jinja_env.autoescape = select_autoescape(default_for_string=True, default=True)
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# previously in Flask-Bootstrap:
app.jinja_env.globals["bootstrap_is_hidden_field"] = lambda field: isinstance(
field, HiddenField
)
# https://scodoc.fr/ScoDoc # https://scodoc.fr/ScoDoc
app.register_blueprint(scodoc_bp) app.register_blueprint(scodoc_bp)

View File

@ -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}

View File

@ -603,8 +603,19 @@ class Role(db.Model):
"""Create default roles if missing, then, if reset_permissions, """Create default roles if missing, then, if reset_permissions,
reset their permissions to default values. reset their permissions to default values.
""" """
Role.reset_roles_permissions(
SCO_ROLES_DEFAULTS, reset_permissions=reset_permissions
)
@staticmethod
def reset_roles_permissions(roles_perms: dict[str, tuple], reset_permissions=True):
"""Ajoute les permissions aux roles
roles_perms : { "role_name" : (permission, ...) }
reset_permissions : si vrai efface permissions déja existantes
Si le role n'existe pas, il est (re) créé.
"""
default_role = "Observateur" default_role = "Observateur"
for role_name, permissions in SCO_ROLES_DEFAULTS.items(): for role_name, permissions in roles_perms.items():
role = Role.query.filter_by(name=role_name).first() role = Role.query.filter_by(name=role_name).first()
if role is None: if role is None:
role = Role(name=role_name) role = Role(name=role_name)

View File

@ -37,7 +37,17 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
] ]
# Choix des parcours # Choix des parcours
ue_pids = [p.id for p in ue.parcours] ue_pids = [p.id for p in ue.parcours]
H.append("""<form id="choix_parcours">""") H.append(
"""
<div class="help">
Cocher tous les parcours dans lesquels cette UE est utilisée,
même si vous n'offrez pas ce parcours dans votre département.
Sans quoi, les UEs de Tronc Commun ne seront pas reconnues.
Ne cocher aucun parcours est équivalent à tous les cocher.
</div>
<form id="choix_parcours" style="margin-top: 12px;">
"""
)
ects_differents = { ects_differents = {
ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours ue.get_ects(parcour, only_parcours=True) for parcour in ref_comp.parcours

View File

@ -9,12 +9,14 @@
import collections import collections
import datetime import datetime
import pandas as pd
import numpy as np import numpy as np
from flask import g, has_request_context, url_for from flask import g, has_request_context, url_for
from app import db from app import db
from app.comp.moy_mod import ModuleImplResults
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import Evaluation, FormSemestre, Identite from app.models import Evaluation, FormSemestre, Identite, ModuleImpl
from app.models.groups import GroupDescr from app.models.groups import GroupDescr
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins, sco_utils as scu
@ -229,7 +231,7 @@ class BulletinBUT:
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
d[modimpl.module.code] = { d[modimpl.module.code] = {
"id": modimpl.id, "id": modimpl.id,
"titre": modimpl.module.titre, "titre": modimpl.module.titre_str(),
"code_apogee": modimpl.module.code_apogee, "code_apogee": modimpl.module.code_apogee,
"url": ( "url": (
url_for( url_for(
@ -249,59 +251,88 @@ class BulletinBUT:
# "moy": fmt_note(moyennes_etuds.mean()), # "moy": fmt_note(moyennes_etuds.mean()),
}, },
"evaluations": ( "evaluations": (
[ self.etud_list_modimpl_evaluations(
self.etud_eval_results(etud, e) etud, modimpl, modimpl_results, version
for e in modimpl.evaluations )
if (e.visibulletin or version == "long")
and (e.id in modimpl_results.evaluations_etat)
and (
modimpl_results.evaluations_etat[e.id].is_complete
or self.prefs["bul_show_all_evals"]
)
]
if version != "short" if version != "short"
else [] else []
), ),
} }
return d return d
def etud_eval_results(self, etud, e: Evaluation) -> dict: def etud_list_modimpl_evaluations(
self,
etud: Identite,
modimpl: ModuleImpl,
modimpl_results: ModuleImplResults,
version: str,
) -> list[dict]:
"""Liste des résultats aux évaluations de ce modimpl à montrer pour cet étudiant"""
evaluation: Evaluation
eval_results = []
for evaluation in modimpl.evaluations:
if (
(evaluation.visibulletin or version == "long")
and (evaluation.id in modimpl_results.evaluations_etat)
and (
modimpl_results.evaluations_etat[evaluation.id].is_complete
or self.prefs["bul_show_all_evals"]
)
):
eval_notes = self.res.modimpls_results[modimpl.id].evals_notes[
evaluation.id
]
if (evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE) or (
not np.isnan(eval_notes[etud.id])
):
eval_results.append(
self.etud_eval_results(etud, evaluation, eval_notes)
)
return eval_results
def etud_eval_results(
self, etud: Identite, evaluation: Evaluation, eval_notes: pd.DataFrame
) -> dict:
"dict resultats d'un étudiant à une évaluation" "dict resultats d'un étudiant à une évaluation"
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits # eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna() notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id] modimpls_evals_poids = self.res.modimpls_evals_poids[evaluation.moduleimpl_id]
try: try:
etud_ues_ids = self.res.etud_ues_ids(etud.id) etud_ues_ids = self.res.etud_ues_ids(etud.id)
poids = { poids = {
ue.acronyme: modimpls_evals_poids[ue.id][e.id] ue.acronyme: modimpls_evals_poids[ue.id][evaluation.id]
for ue in self.res.ues for ue in self.res.ues
if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids) if (ue.type != UE_SPORT) and (ue.id in etud_ues_ids)
} }
except KeyError: except KeyError:
poids = collections.defaultdict(lambda: 0.0) poids = collections.defaultdict(lambda: 0.0)
d = { d = {
"id": e.id, "id": evaluation.id,
"coef": ( "coef": (
fmt_note(e.coefficient) fmt_note(evaluation.coefficient)
if e.evaluation_type == Evaluation.EVALUATION_NORMALE if evaluation.evaluation_type == Evaluation.EVALUATION_NORMALE
else None else None
), ),
"date_debut": e.date_debut.isoformat() if e.date_debut else None, "date_debut": (
"date_fin": e.date_fin.isoformat() if e.date_fin else None, evaluation.date_debut.isoformat() if evaluation.date_debut else None
"description": e.description, ),
"evaluation_type": e.evaluation_type, "date_fin": (
evaluation.date_fin.isoformat() if evaluation.date_fin else None
),
"description": evaluation.description,
"evaluation_type": evaluation.evaluation_type,
"note": ( "note": (
{ {
"value": fmt_note( "value": fmt_note(
eval_notes[etud.id], eval_notes[etud.id],
note_max=e.note_max, note_max=evaluation.note_max,
), ),
"min": fmt_note(notes_ok.min(), note_max=e.note_max), "min": fmt_note(notes_ok.min(), note_max=evaluation.note_max),
"max": fmt_note(notes_ok.max(), note_max=e.note_max), "max": fmt_note(notes_ok.max(), note_max=evaluation.note_max),
"moy": fmt_note(notes_ok.mean(), note_max=e.note_max), "moy": fmt_note(notes_ok.mean(), note_max=evaluation.note_max),
} }
if not e.is_blocked() if not evaluation.is_blocked()
else {} else {}
), ),
"poids": poids, "poids": poids,
@ -309,17 +340,25 @@ class BulletinBUT:
url_for( url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
evaluation_id=e.id, evaluation_id=evaluation.id,
) )
if has_request_context() if has_request_context()
else "na" else "na"
), ),
# deprecated (supprimer avant #sco9.7) # deprecated (supprimer avant #sco9.7)
"date": e.date_debut.isoformat() if e.date_debut else None, "date": (
"heure_debut": ( evaluation.date_debut.isoformat() if evaluation.date_debut else None
e.date_debut.time().isoformat("minutes") if e.date_debut else None ),
"heure_debut": (
evaluation.date_debut.time().isoformat("minutes")
if evaluation.date_debut
else None
),
"heure_fin": (
evaluation.date_fin.time().isoformat("minutes")
if evaluation.date_fin
else None
), ),
"heure_fin": e.date_fin.time().isoformat("minutes") if e.date_fin else None,
} }
return d return d
@ -540,9 +579,9 @@ class BulletinBUT:
d.update(infos) d.update(infos)
# --- Rangs # --- Rangs
d[ d["rang_nt"] = (
"rang_nt" f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" )
d["rang_txt"] = "Rang " + d["rang_nt"] d["rang_txt"] = "Rang " + d["rang_nt"]
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"])) d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))

View File

@ -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>{

92
app/but/change_refcomp.py Normal file
View File

@ -0,0 +1,92 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Code expérimental: si deux référentiel sont presques identiques
(mêmes compétences, niveaux, parcours)
essaie de changer une formation de référentiel.
"""
from app import clear_scodoc_cache, db
from app.models import (
ApcParcours,
ApcReferentielCompetences,
ApcValidationRCUE,
Formation,
FormSemestreInscription,
UniteEns,
)
from app.scodoc.sco_exceptions import ScoValueError
def formation_change_referentiel(
formation: Formation, new_ref: ApcReferentielCompetences
):
"""Try to change ref."""
if not formation.referentiel_competence:
raise ScoValueError("formation non associée à un référentiel")
if not isinstance(new_ref, ApcReferentielCompetences):
raise ScoValueError("nouveau référentiel invalide")
r = formation.referentiel_competence.map_to_other_referentiel(new_ref)
if isinstance(r, str):
raise ScoValueError(f"référentiels incompatibles: {r}")
parcours_map, competences_map, niveaux_map = r
formation.referentiel_competence = new_ref
db.session.add(formation)
# UEs - Niveaux et UEs - parcours
for ue in formation.ues:
if ue.niveau_competence:
ue.niveau_competence_id = niveaux_map[ue.niveau_competence_id]
db.session.add(ue)
if ue.parcours:
new_list = [ApcParcours.query.get(parcours_map[p.id]) for p in ue.parcours]
ue.parcours.clear()
ue.parcours.extend(new_list)
db.session.add(ue)
# Modules / parcours et app_critiques
for module in formation.modules:
if module.parcours:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in module.parcours
]
module.parcours.clear()
module.parcours.extend(new_list)
db.session.add(module)
if module.app_critiques: # efface les apprentissages critiques
module.app_critiques.clear()
db.session.add(module)
# ApcValidationRCUE
for valid_rcue in ApcValidationRCUE.query.join(
UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id
).filter_by(formation_id=formation.id):
if valid_rcue.parcour:
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
db.session.add(valid_rcue)
for valid_rcue in ApcValidationRCUE.query.join(
UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id
).filter_by(formation_id=formation.id):
if valid_rcue.parcour:
valid_rcue.parcour_id = parcours_map[valid_rcue.parcour.id]
db.session.add(valid_rcue)
# FormSemestre / parcours_formsemestre
for formsemestre in formation.formsemestres:
new_list = [
ApcParcours.query.get(parcours_map[p.id]) for p in formsemestre.parcours
]
formsemestre.parcours.clear()
formsemestre.parcours.extend(new_list)
db.session.add(formsemestre)
# FormSemestreInscription.parcour_id
for inscr in FormSemestreInscription.query.filter_by(
formsemestre_id=formsemestre.id
).filter(FormSemestreInscription.parcour_id != None):
if inscr.parcour_id is not None:
inscr.parcour_id = parcours_map[inscr.parcour_id]
#
db.session.commit()
clear_scodoc_cache()

View File

@ -10,9 +10,11 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField from wtforms import SelectField, SubmitField
from wtforms.validators import DataRequired
class FormationRefCompForm(FlaskForm): class FormationRefCompForm(FlaskForm):
"Choix d'un référentiel"
referentiel_competence = SelectField( referentiel_competence = SelectField(
"Choisir parmi les référentiels déjà chargés :" "Choisir parmi les référentiels déjà chargés :"
) )
@ -21,6 +23,7 @@ class FormationRefCompForm(FlaskForm):
class RefCompLoadForm(FlaskForm): class RefCompLoadForm(FlaskForm):
"Upload d'un référentiel"
referentiel_standard = SelectField( referentiel_standard = SelectField(
"Choisir un référentiel de compétences officiel BUT" "Choisir un référentiel de compétences officiel BUT"
) )
@ -47,3 +50,12 @@ class RefCompLoadForm(FlaskForm):
) )
return False return False
return True return True
class FormationChangeRefCompForm(FlaskForm):
"choix d'un nouveau ref. comp. pour une formation"
object_select = SelectField(
"Choisir le nouveau référentiel", validators=[DataRequired()]
)
submit = SubmitField("Changer le référentiel de la formation")
cancel = SubmitField("Annuler")

View File

@ -59,3 +59,4 @@ def check_taxe_now(taxes):
from app.entreprises import routes from app.entreprises import routes
from app.entreprises.activate import activate_module

View File

@ -0,0 +1,31 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Activation du module entreprises
L'affichage du module est contrôlé par la config ScoDocConfig.enable_entreprises
Au moment de l'activation, il est en général utile de proposer de configurer les
permissions de rôles standards: AdminEntreprise UtilisateurEntreprise ObservateurEntreprise
Voir associations dans sco_roles_default
"""
from app.auth.models import Role
from app.models import ScoDocSiteConfig
from app.scodoc.sco_roles_default import SCO_ROLES_ENTREPRISES_DEFAULT
def activate_module(
enable: bool = True, set_default_roles_permission: bool = False
) -> bool:
"""Active le module et en option donne les permissions aux rôles standards.
True si l'état d'activation a changé.
"""
change = ScoDocSiteConfig.enable_entreprises(enable)
if enable and set_default_roles_permission:
Role.reset_roles_permissions(SCO_ROLES_ENTREPRISES_DEFAULT)
return change

View File

@ -0,0 +1,17 @@
"""
Formulaire activation module entreprises
"""
from flask_wtf import FlaskForm
from wtforms.fields.simple import BooleanField, SubmitField
from app.models import ScoDocSiteConfig
class ActivateEntreprisesForm(FlaskForm):
"Formulaire activation module entreprises"
set_default_roles_permission = BooleanField(
"(re)mettre les rôles 'Entreprise' à leurs valeurs par défaut"
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -54,7 +54,6 @@ class BonusConfigurationForm(FlaskForm):
class ScoDocConfigurationForm(FlaskForm): class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration avancée" "Panneau de configuration avancée"
enable_entreprises = BooleanField("activer le module <em>entreprises</em>")
disable_passerelle = BooleanField( # disable car par défaut activée disable_passerelle = BooleanField( # disable car par défaut activée
"""cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation.""" """cacher les fonctions liées à une passerelle de publication des résultats vers les étudiants ("œil"). N'affecte pas l'API, juste la présentation."""
) )
@ -127,13 +126,6 @@ def configuration():
flash("Fonction bonus inchangée.") flash("Fonction bonus inchangée.")
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
elif form_scodoc.submit_scodoc.data and form_scodoc.validate(): elif form_scodoc.submit_scodoc.data and form_scodoc.validate():
if ScoDocSiteConfig.enable_entreprises(
enabled=form_scodoc.data["enable_entreprises"]
):
flash(
"Module entreprise "
+ ("activé" if form_scodoc.data["enable_entreprises"] else "désactivé")
)
if ScoDocSiteConfig.disable_passerelle( if ScoDocSiteConfig.disable_passerelle(
disabled=form_scodoc.data["disable_passerelle"] disabled=form_scodoc.data["disable_passerelle"]
): ):
@ -182,6 +174,7 @@ def configuration():
return render_template( return render_template(
"configuration.j2", "configuration.j2",
is_entreprises_enabled=ScoDocSiteConfig.is_entreprises_enabled(),
form_bonus=form_bonus, form_bonus=form_bonus,
form_scodoc=form_scodoc, form_scodoc=form_scodoc,
scu=scu, scu=scu,

View File

@ -8,16 +8,19 @@
from datetime import datetime from datetime import datetime
import functools import functools
from operator import attrgetter from operator import attrgetter
import yaml
from flask import g from flask import g
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from sqlalchemy.orm import class_mapper from sqlalchemy.orm import class_mapper
import sqlalchemy import sqlalchemy
from app import db from app import db, log
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
REFCOMP_EQUIVALENCE_FILENAME = "ressources/referentiels/equivalences.yaml"
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns # from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
@ -104,6 +107,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
def __repr__(self): def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>" return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
def get_title(self) -> str:
"Titre affichable"
# utilise type_titre (B.U.T.), spécialité, version
return f"{self.type_titre} {self.specialite} {self.get_version()}"
def get_version(self) -> str: def get_version(self) -> str:
"La version, normalement sous forme de date iso yyy-mm-dd" "La version, normalement sous forme de date iso yyy-mm-dd"
if not self.version_orebut: if not self.version_orebut:
@ -124,9 +132,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
"type_departement": self.type_departement, "type_departement": self.type_departement,
"type_titre": self.type_titre, "type_titre": self.type_titre,
"version_orebut": self.version_orebut, "version_orebut": self.version_orebut,
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z" "scodoc_date_loaded": (
if self.scodoc_date_loaded self.scodoc_date_loaded.isoformat() + "Z"
else "", if self.scodoc_date_loaded
else ""
),
"scodoc_orig_filename": self.scodoc_orig_filename, "scodoc_orig_filename": self.scodoc_orig_filename,
"competences": { "competences": {
x.titre: x.to_dict(with_app_critiques=with_app_critiques) x.titre: x.to_dict(with_app_critiques=with_app_critiques)
@ -234,6 +244,92 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return parcours_info return parcours_info
def equivalents(self) -> set["ApcReferentielCompetences"]:
"""Ensemble des référentiels du même département
qui peuvent être considérés comme "équivalents", au sens
une formation de ce référentiel pourrait changer vers un équivalent,
en ignorant les apprentissages critiques.
Pour cela, il faut avoir le même type, etc et les mêmes compétences,
niveaux et parcours (voir map_to_other_referentiel).
"""
candidats = ApcReferentielCompetences.query.filter_by(
dept_id=self.dept_id
).filter(ApcReferentielCompetences.id != self.id)
return {
referentiel
for referentiel in candidats
if not isinstance(self.map_to_other_referentiel(referentiel), str)
}
def map_to_other_referentiel(
self, other: "ApcReferentielCompetences"
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
"""Build mapping between this referentiel and ref2.
If successful, returns 3 dicts mapping self ids to other ids.
Else return a string, error message.
"""
if self.type_structure != other.type_structure:
return "type_structure mismatch"
if self.type_departement != other.type_departement:
return "type_departement mismatch"
# Table d'équivalences entre refs:
equiv = self._load_config_equivalences()
# mêmes parcours ?
eq_parcours = equiv.get("parcours", {})
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
parcours_by_code_2 = {
eq_parcours.get(p.code, p.code): p for p in other.parcours
}
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
return "parcours mismatch"
parcours_map = {
parcours_by_code_1[eq_parcours.get(code, code)]
.id: parcours_by_code_2[eq_parcours.get(code, code)]
.id
for code in parcours_by_code_1
}
# mêmes compétences ?
competence_by_code_1 = {c.titre: c for c in self.competences}
competence_by_code_2 = {c.titre: c for c in other.competences}
if competence_by_code_1.keys() != competence_by_code_2.keys():
return "competences mismatch"
competences_map = {
competence_by_code_1[titre].id: competence_by_code_2[titre].id
for titre in competence_by_code_1
}
# mêmes niveaux (dans chaque compétence) ?
niveaux_map = {}
for titre in competence_by_code_1:
c1 = competence_by_code_1[titre]
c2 = competence_by_code_2[titre]
niveau_by_attr_1 = {(n.annee, n.ordre, n.libelle): n for n in c1.niveaux}
niveau_by_attr_2 = {(n.annee, n.ordre, n.libelle): n for n in c2.niveaux}
if niveau_by_attr_1.keys() != niveau_by_attr_2.keys():
return f"niveaux mismatch in comp. '{titre}'"
niveaux_map.update(
{
niveau_by_attr_1[a].id: niveau_by_attr_2[a].id
for a in niveau_by_attr_1
}
)
return parcours_map, competences_map, niveaux_map
def _load_config_equivalences(self) -> dict:
"""Load config file ressources/referentiels/equivalences.yaml
used to define equivalences between distinct referentiels
"""
try:
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
doc = yaml.safe_load(f.read())
except FileNotFoundError:
log(f"_load_config_equivalences: {REFCOMP_EQUIVALENCE_FILENAME} not found")
return {}
except yaml.parser.ParserError as exc:
raise ScoValueError(
f"erreur dans le fichier {REFCOMP_EQUIVALENCE_FILENAME}"
) from exc
return doc.get(self.specialite, {})
class ApcCompetence(db.Model, XMLModel): class ApcCompetence(db.Model, XMLModel):
"Compétence" "Compétence"
@ -374,9 +470,11 @@ class ApcNiveau(db.Model, XMLModel):
"libelle": self.libelle, "libelle": self.libelle,
"annee": self.annee, "annee": self.annee,
"ordre": self.ordre, "ordre": self.ordre,
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques} "app_critiques": (
if with_app_critiques {x.code: x.to_dict() for x in self.app_critiques}
else {}, if with_app_critiques
else {}
),
} }
def to_dict_bul(self): def to_dict_bul(self):
@ -464,9 +562,9 @@ class ApcNiveau(db.Model, XMLModel):
return [] return []
if competence is None: if competence is None:
parcour_niveaux: list[ parcour_niveaux: list[ApcParcoursNiveauCompetence] = (
ApcParcoursNiveauCompetence annee_parcour.niveaux_competences
] = annee_parcour.niveaux_competences )
niveaux: list[ApcNiveau] = [ niveaux: list[ApcNiveau] = [
pn.competence.niveaux.filter_by(ordre=pn.niveau).first() pn.competence.niveaux.filter_by(ordre=pn.niveau).first()
for pn in parcour_niveaux for pn in parcour_niveaux

View File

@ -297,7 +297,7 @@ class Identite(models.ScoDocModel):
else: else:
return self.nom return self.nom
@cached_property @property
def nomprenom(self, reverse=False) -> str: def nomprenom(self, reverse=False) -> str:
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont" """Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité. Si reverse, "Dupont Pierre", sans civilité.

View File

@ -232,7 +232,9 @@ class ScolarNews(db.Model):
) )
# Transforme les URL en URL absolues # Transforme les URL en URL absolues
base = scu.ScoURL() base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt) txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url' # Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'

View File

@ -1,5 +1,7 @@
"""ScoDoc 9 models : Formations """ScoDoc 9 models : Formations
""" """
from flask import abort, g
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
import app import app
@ -64,6 +66,21 @@ class Formation(db.Model):
"titre complet pour affichage" "titre complet pour affichage"
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}""" return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
@classmethod
def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation":
"""Formation ou 404, cherche uniquement dans le département spécifié
ou le courant (g.scodoc_dept)"""
if not isinstance(formation_id, int):
try:
formation_id = int(formation_id)
except (TypeError, ValueError):
abort(404, "formation_id invalide")
if g.scodoc_dept:
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
if dept_id is not None:
return cls.query.filter_by(id=formation_id, dept_id=dept_id).first_or_404()
return cls.query.filter_by(id=formation_id).first_or_404()
def to_dict(self, with_refcomp_attrs=False, with_departement=True): def to_dict(self, with_refcomp_attrs=False, with_departement=True):
"""As a dict. """As a dict.
Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp. Si with_refcomp_attrs, ajoute attributs permettant de retrouver le ref. de comp.

View File

@ -945,7 +945,7 @@ class FormSemestre(models.ScoDocModel):
ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT
} }
@cached_property @property
def etuds_inscriptions(self) -> dict: def etuds_inscriptions(self) -> dict:
"""Map { etudid : inscription } (incluant DEM et DEF)""" """Map { etudid : inscription } (incluant DEM et DEF)"""
return {ins.etud.id: ins for ins in self.inscriptions} return {ins.etud.id: ins for ins in self.inscriptions}

View File

@ -409,6 +409,14 @@ class UniteEns(models.ScoDocModel):
Renvoie (True, "") si ok, sinon (False, error_message) Renvoie (True, "") si ok, sinon (False, error_message)
""" """
msg = "" msg = ""
# Safety check
if self.formation.referentiel_competence is None:
return False, "pas de référentiel de compétence"
# Si tous les parcours, aucun (tronc commun)
if {p.id for p in parcours} == {
p.id for p in self.formation.referentiel_competence.parcours
}:
parcours = []
# Le niveau est-il dans tous ces parcours ? Sinon, l'enlève # Le niveau est-il dans tous ces parcours ? Sinon, l'enlève
prev_niveau = self.niveau_competence prev_niveau = self.niveau_competence
if ( if (
@ -424,6 +432,7 @@ class UniteEns(models.ScoDocModel):
self.niveau_competence, parcours self.niveau_competence, parcours
) )
if not ok: if not ok:
self.formation.invalidate_cached_sems()
self.niveau_competence = prev_niveau # restore self.niveau_competence = prev_niveau # restore
return False, error_message return False, error_message

View File

@ -48,6 +48,7 @@ from typing import Any
from urllib.parse import urlparse, urlencode, parse_qs, urlunparse from urllib.parse import urlparse, urlencode, parse_qs, urlunparse
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
import reportlab
from reportlab.platypus import Paragraph, Spacer from reportlab.platypus import Paragraph, Spacer
from reportlab.platypus import Table, KeepInFrame from reportlab.platypus import Table, KeepInFrame
from reportlab.lib.colors import Color from reportlab.lib.colors import Color
@ -812,7 +813,10 @@ if __name__ == "__main__":
document, document,
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = doc.getvalue() data = doc.getvalue()
with open("/tmp/gen_table.pdf", "wb") as f: with open("/tmp/gen_table.pdf", "wb") as f:
f.write(data) f.write(data)

View File

@ -30,7 +30,7 @@
import html import html
from flask import g, render_template from flask import g, render_template, url_for
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
@ -163,7 +163,7 @@ def sco_header(
params = { params = {
"page_title": page_title or sco_version.SCONAME, "page_title": page_title or sco_version.SCONAME,
"no_side_bar": no_side_bar, "no_side_bar": no_side_bar,
"ScoURL": scu.ScoURL(), "ScoURL": url_for("scolar.index_html", scodoc_dept=g.scodoc_dept),
"encoding": scu.SCO_ENCODING, "encoding": scu.SCO_ENCODING,
"titrebandeau_mkup": "<td>" + titrebandeau + "</td>", "titrebandeau_mkup": "<td>" + titrebandeau + "</td>",
"authuser": current_user.user_name, "authuser": current_user.user_name,
@ -179,6 +179,7 @@ def sco_header(
H = [ H = [
"""<!DOCTYPE html><html lang="fr"> """<!DOCTYPE html><html lang="fr">
<!-- ScoDoc legacy -->
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<title>%(page_title)s</title> <title>%(page_title)s</title>
@ -219,7 +220,7 @@ def sco_header(
<script> <script>
window.onload=function(){{enableTooltips("gtrcontent")}}; window.onload=function(){{enableTooltips("gtrcontent")}};
const SCO_URL="{scu.ScoURL()}"; const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
const SCO_TIMEZONE="{scu.TIME_ZONE}"; const SCO_TIMEZONE="{scu.TIME_ZONE}";
</script>""" </script>"""
) )

View File

@ -102,25 +102,33 @@ def sidebar_common():
<a href="{home_link}" class="sidebar">Accueil</a> <br> <a href="{home_link}" class="sidebar">Accueil</a> <br>
<div id="authuser"><a id="authuserlink" href="{ <div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page", url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name) scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}">{current_user.user_name}</a> }">{current_user.user_name}</a>
<br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a> <br><a id="deconnectlink" href="{url_for("auth.logout")}">déconnexion</a>
</div> </div>
{sidebar_dept()} {sidebar_dept()}
<h2 class="insidebar">Scolarité</h2> <h2 class="insidebar">Scolarité</h2>
<a href="{scu.ScoURL()}" class="sidebar">Semestres</a> <br> <a href="{
<a href="{scu.NotesURL()}" class="sidebar">Formations</a> <br> url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Semestres</a> <br>
<a href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Formations</a> <br>
""" """
] ]
if current_user.has_permission(Permission.AbsChange): if current_user.has_permission(Permission.AbsChange):
H.append( H.append(
f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduité</a> <br> """ f""" <a href="{
url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Assiduité</a> <br> """
) )
if current_user.has_permission( if current_user.has_permission(
Permission.UsersAdmin Permission.UsersAdmin
) or current_user.has_permission(Permission.UsersView): ) or current_user.has_permission(Permission.UsersView):
H.append( H.append(
f"""<a href="{scu.UsersURL()}" class="sidebar">Utilisateurs</a> <br>""" f"""<a href="{
url_for("users.index_html", scodoc_dept=g.scodoc_dept)
}" class="sidebar">Utilisateurs</a> <br>"""
) )
if current_user.has_permission(Permission.EditPreferences): if current_user.has_permission(Permission.EditPreferences):
@ -141,7 +149,9 @@ def sidebar(etudid: int = None):
params = {} params = {}
H = [ H = [
f"""<div class="sidebar"> f"""
<!-- sidebar py -->
<div class="sidebar">
{ sidebar_common() } { sidebar_common() }
<div class="box-chercheetud">Chercher étudiant:<br> <div class="box-chercheetud">Chercher étudiant:<br>
<form method="get" id="form-chercheetud" <form method="get" id="form-chercheetud"

View File

@ -49,11 +49,13 @@
""" """
import datetime import datetime
import glob import glob
import gzip
import mimetypes import mimetypes
import os import os
import re import re
import shutil import shutil
import time import time
import zlib
import chardet import chardet
@ -62,7 +64,7 @@ from flask import g
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from config import Config from config import Config
from app import log from app import log
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoException, ScoValueError
class BaseArchiver: class BaseArchiver:
@ -241,11 +243,13 @@ class BaseArchiver:
filename: str, filename: str,
data: str | bytes, data: str | bytes,
dept_id: int = None, dept_id: int = None,
compress=False,
): ):
"""Store data in archive, under given filename. """Store data in archive, under given filename.
Filename may be modified (sanitized): return used filename Filename may be modified (sanitized): return used filename
The file is created or replaced. The file is created or replaced.
data may be str or bytes data may be str or bytes
If compress, data is gziped and filename suffix ".gz" added.
""" """
if isinstance(data, str): if isinstance(data, str):
data = data.encode(scu.SCO_ENCODING) data = data.encode(scu.SCO_ENCODING)
@ -255,8 +259,14 @@ class BaseArchiver:
try: try:
scu.GSL.acquire() scu.GSL.acquire()
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
with open(fname, "wb") as f: if compress:
f.write(data) 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: except FileNotFoundError as exc:
raise ScoValueError( raise ScoValueError(
f"Erreur stockage archive (dossier inexistant, chemin {fname})" f"Erreur stockage archive (dossier inexistant, chemin {fname})"
@ -274,8 +284,17 @@ class BaseArchiver:
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
log(f"reading archive file {fname}") log(f"reading archive file {fname}")
try: try:
with open(fname, "rb") as f: if fname.endswith(".gz"):
data = f.read() 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: except FileNotFoundError as exc:
raise ScoValueError( raise ScoValueError(
f"Erreur lecture archive (inexistant, chemin {fname})" 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) archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
data = self.get(archive_id, filename) data = self.get(archive_id, filename)
if filename.endswith(".gz"):
filename = filename[:-3]
mime = mimetypes.guess_type(filename)[0] mime = mimetypes.guess_type(filename)[0]
if mime is None: if mime is None:
mime = "application/octet-stream" mime = "application/octet-stream"

View File

@ -68,7 +68,7 @@ PV_ARCHIVER = SemsArchiver()
def do_formsemestre_archive( def do_formsemestre_archive(
formsemestre_id, formsemestre: FormSemestre,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
description="", description="",
date_jury="", date_jury="",
@ -92,9 +92,8 @@ def do_formsemestre_archive(
raise ScoValueError( raise ScoValueError(
"do_formsemestre_archive: version de bulletin demandée invalide" "do_formsemestre_archive: version de bulletin demandée invalide"
) )
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) 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( archive_id = PV_ARCHIVER.create_obj_archive(
sem_archive_id, description, formsemestre.dept_id sem_archive_id, description, formsemestre.dept_id
) )
@ -102,9 +101,9 @@ def do_formsemestre_archive(
if not group_ids: if not group_ids:
# tous les inscrits du semestre # 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( groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id group_ids, formsemestre_id=formsemestre.id
) )
groups_filename = "-" + groups_infos.groups_filename groups_filename = "-" + groups_infos.groups_filename
etudids = [m["etudid"] for m in groups_infos.members] etudids = [m["etudid"] for m in groups_infos.members]
@ -142,19 +141,23 @@ def do_formsemestre_archive(
) )
# Bulletins en JSON # 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) data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
if data: if data:
PV_ARCHIVER.store( 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 # Décisions de jury, en XLS
if formsemestre.formation.is_apc(): 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() data = response.get_data()
else: # formations classiques else: # formations classiques
data = sco_pv_forms.formsemestre_pvjury( data = sco_pv_forms.formsemestre_pvjury(
formsemestre_id, fmt="xls", publish=False formsemestre.id, fmt="xls", publish=False
) )
if data: if data:
PV_ARCHIVER.store( PV_ARCHIVER.store(
@ -165,7 +168,7 @@ def do_formsemestre_archive(
) )
# Classeur bulletins (PDF) # Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=bul_version formsemestre.id, version=bul_version
) )
if data: if data:
PV_ARCHIVER.store( PV_ARCHIVER.store(
@ -173,10 +176,11 @@ def do_formsemestre_archive(
"Bulletins.pdf", "Bulletins.pdf",
data, data,
dept_id=formsemestre.dept_id, dept_id=formsemestre.dept_id,
compress=True,
) )
# Lettres individuelles (PDF): # Lettres individuelles (PDF):
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id, formsemestre.id,
etudids=etudids, etudids=etudids,
date_jury=date_jury, date_jury=date_jury,
date_commission=date_commission, 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. """Make and store new archive for this formsemestre.
(all students or only selected groups) (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(): if not formsemestre.can_edit_pv():
raise ScoPermissionDenied( raise ScoPermissionDenied(
dest_url=url_for( dest_url=url_for(
@ -320,7 +324,7 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
else: else:
tf[2]["anonymous"] = False tf[2]["anonymous"] = False
do_formsemestre_archive( do_formsemestre_archive(
formsemestre_id, formsemestre,
group_ids=group_ids, group_ids=group_ids,
description=tf[2]["description"], description=tf[2]["description"],
date_jury=tf[2]["date_jury"], date_jury=tf[2]["date_jury"],
@ -352,7 +356,7 @@ def formsemestre_list_archives(formsemestre_id):
"""Page listing archives""" """Page listing archives"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre_id
L = [] archives_descr = []
for archive_id in PV_ARCHIVER.list_obj_archives( for archive_id in PV_ARCHIVER.list_obj_archives(
sem_archive_id, dept_id=formsemestre.dept_id 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 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 ")] 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>") H.append("<p>aucune archive enregistrée</p>")
else: else:
H.append("<ul>") H.append("<ul>")
for a in L: for a in archives_descr:
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"]) archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
H.append( H.append(
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>' 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,
a["date"].strftime("%d/%m/%Y %H:%M"), formsemestre_id=formsemestre_id, archive_name=archive_name
a["description"], )}">supprimer</a>)
formsemestre_id, <ul>"""
archive_name,
)
) )
for filename in a["content"]: for filename in a["content"]:
H.append( H.append(
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>' f"""<li><a href="{
% (formsemestre_id, archive_name, filename, filename) 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"]: if not a["content"]:
H.append("<li><em>aucun fichier !</em></li>") 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): def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
"""Send file to client.""" """Send file to client."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre.id sem_archive_id = formsemestre.id
return PV_ARCHIVER.get_archived_file( return PV_ARCHIVER.get_archived_file(
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id

View File

@ -446,7 +446,8 @@ def _ue_mod_bulletin(
): ):
"""Infos sur les modules (et évaluations) dans une UE """Infos sur les modules (et évaluations) dans une UE
(ajoute les informations aux modimpls) (ajoute les informations aux modimpls)
Result: liste de modules de l'UE avec les infos dans chacun (seulement ceux où l'étudiant est inscrit). Result: liste de modules de l'UE avec les infos dans chacun (seulement
ceux l'étudiant est inscrit).
""" """
bul_show_mod_rangs = sco_preferences.get_preference( bul_show_mod_rangs = sco_preferences.get_preference(
"bul_show_mod_rangs", formsemestre_id "bul_show_mod_rangs", formsemestre_id

View File

@ -61,7 +61,7 @@ from flask_login import current_user
from app.models import FormSemestre, Identite, ScoDocSiteConfig from app.models import FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError from app.scodoc.sco_exceptions import NoteProcessError, ScoPDFFormatError
from app import log from app import log
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
@ -228,7 +228,15 @@ class BulletinGenerator:
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
) )
) )
document.build(story) try:
document.build(story)
except (
ValueError,
KeyError,
reportlab.platypus.doctemplate.LayoutError,
) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return data return data

View File

@ -55,7 +55,6 @@ from flask import g
import app import app
from app import db, log from app import db, log
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_exceptions import ScoException
@ -174,17 +173,15 @@ class EvaluationCache(ScoDocCache):
@classmethod @classmethod
def invalidate_all_sems(cls): def invalidate_all_sems(cls):
"delete all evaluations in current dept from cache" "delete all evaluations in current dept from cache"
from app.models.evaluations import Evaluation
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
evaluation_ids = [ evaluation_ids = [
x[0] e.id
for x in ndb.SimpleQuery( for e in Evaluation.query.join(ModuleImpl)
"""SELECT e.id .join(FormSemestre)
FROM notes_evaluation e, notes_moduleimpl mi, notes_formsemestre s .filter_by(dept_id=g.scodoc_dept_id)
WHERE s.dept_id=%(dept_id)s
AND s.id = mi.formsemestre_id
AND mi.id = e.moduleimpl_id;
""",
{"dept_id": g.scodoc_dept_id},
)
] ]
cls.delete_many(evaluation_ids) cls.delete_many(evaluation_ids)

View File

@ -230,41 +230,41 @@ def next_iso_day(date):
def YearTable( def YearTable(
year, year,
events=[], events_by_day: dict[str, list[dict]],
firstmonth=9, firstmonth=9,
lastmonth=7, lastmonth=7,
halfday=0,
dayattributes="", dayattributes="",
pad_width=8,
): ):
# Code simplifié en 2024: utilisé seulement pour calendrier évaluations
"""Generate a calendar table """Generate a calendar table
events = list of tuples (date, text, color, href [,halfday]) events = list of tuples (date, text, color, href [,halfday])
where date is a string in ISO format (yyyy-mm-dd) where date is a string in ISO format (yyyy-mm-dd)
halfday is boolean (true: morning, false: afternoon) halfday is boolean (true: morning, false: afternoon)
text = text to put in calendar (must be short, 1-5 cars) (optional) text = text to put in calendar (must be short, 1-5 cars) (optional)
if halfday, generate 2 cells per day (morning, afternoon)
""" """
T = [ T = [
'<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">' """<table id="maincalendar" class="maincalendar"
border="3" cellpadding="1" cellspacing="1" frame="box">"""
] ]
T.append("<tr>") T.append("<tr>")
month = firstmonth month = firstmonth
while 1: while True:
T.append('<td valign="top">') T.append('<td valign="top">')
T.append(MonthTableHead(month)) T.append(_month_table_head(month))
T.append( T.append(
MonthTableBody( _month_table_body(
month, month,
year, year,
events, events_by_day,
halfday,
dayattributes, dayattributes,
is_work_saturday(), is_work_saturday(),
pad_width=pad_width,
) )
) )
T.append(MonthTableTail()) T.append(
T.append("</td>") """
</table>
</td>"""
)
if month == lastmonth: if month == lastmonth:
break break
month = month + 1 month = month + 1
@ -322,29 +322,32 @@ WEEKDAYCOLOR = GRAY1
WEEKENDCOLOR = GREEN3 WEEKENDCOLOR = GREEN3
def MonthTableHead(month): def _month_table_head(month):
color = WHITE color = WHITE
return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box"> return f"""<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box">
<tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % ( <tr bgcolor="{color}">
color, <td class="calcol" colspan="2" align="center">{MONTHNAMES_ABREV[month - 1]}</td>
MONTHNAMES_ABREV[month - 1], </tr>\n"""
)
def MonthTableTail(): def _month_table_body(
return "</table>\n" month,
year,
events_by_day: dict[str, list[dict]],
def MonthTableBody( trattributes="",
month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8 work_saturday=False,
): ) -> str:
"""
events : [event]
event = [ yyyy-mm-dd, legend, href, color, descr ] XXX
"""
firstday, nbdays = calendar.monthrange(year, month) firstday, nbdays = calendar.monthrange(year, month)
localtime = time.localtime() localtime = time.localtime()
current_weeknum = time.strftime("%U", localtime) current_weeknum = time.strftime("%U", localtime)
current_year = localtime[0] current_year = localtime[0]
T = [] rows = []
# cherche date du lundi de la 1ere semaine de ce mois # cherche date du lundi de la 1ere semaine de ce mois
monday = ddmmyyyy("1/%d/%d" % (month, year)) monday = ddmmyyyy(f"1/{month}/{year}")
while monday.weekday != 0: while monday.weekday != 0:
monday = monday.prev() monday = monday.prev()
@ -353,158 +356,51 @@ def MonthTableBody(
else: else:
weekend = ("S", "D") weekend = ("S", "D")
if not halfday: for d in range(1, nbdays + 1):
for d in range(1, nbdays + 1): weeknum = time.strftime(
weeknum = time.strftime( "%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT)
"%U", time.strptime("%d/%d/%d" % (d, month, year), scu.DATE_FMT) )
) day = DAYNAMES_ABREV[(firstday + d - 1) % 7]
day = DAYNAMES_ABREV[(firstday + d - 1) % 7] if day in weekend:
if day in weekend: bgcolor = WEEKENDCOLOR
bgcolor = WEEKENDCOLOR weekclass = "wkend"
weekclass = "wkend" attrs = ""
attrs = "" else:
else: bgcolor = WEEKDAYCOLOR
bgcolor = WEEKDAYCOLOR weekclass = "wk" + str(monday).replace("/", "_")
weekclass = "wk" + str(monday).replace("/", "_") attrs = trattributes
attrs = trattributes # events this day ?
color = None events = events_by_day.get(f"{year}-{month:02}-{d:02}", [])
legend = "" color = None
href = "" ev_txts = []
descr = "" for ev in events:
# event this day ? color = ev.get("color")
# each event is a tuple (date, text, color, href) href = ev.get("href", "")
# where date is a string in ISO format (yyyy-mm-dd) description = ev.get("description", "")
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">')
if href: if href:
href = 'href="%s"' % href href = f'href="{href}"'
if descr: if description:
descr = 'title="%s"' % html.escape(descr, quote=True) description = f"""title="{html.escape(description, quote=True)}" """
if href or descr: if href or description:
cc.append("<a %s %s>" % (href, descr)) ev_txts.append(f"""<a {href} {description}>{ev.get("title", "")}</a>""")
if legend or d == 1:
if pad_width is not None:
n = pad_width - len(legend) # pad to 8 cars
if n > 0:
legend = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else: else:
legend = "&nbsp;" # empty cell ev_txts.append(ev.get("title", "&nbsp;"))
cc.append(legend) #
if href or descr: cc = []
cc.append("</a>") if color is not None:
cc.append("</td>") cc.append(f'<td bgcolor="{color}" class="calcell">')
cell = "".join(cc) else:
if day == "D": cc.append('<td class="calcell">')
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"
if day == "D": cc.append(f"{', '.join(ev_txts)}</td>")
monday = monday.next_day(7) cells = "".join(cc)
T.append( if day == "D":
'<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>' monday = monday.next_day(7)
% (bgcolor, weekclass, attrs, d, day) if weeknum == current_weeknum and current_year == year and weekclass != "wkend":
) weekclass += " currentweek"
cc = [] rows.append(
for morning in (True, False): f"""<tr bgcolor="{bgcolor}" class="{weekclass}" {attrs}>
color = None <td class="calday">{d}{day}</td>{cells}</tr>"""
legend = "" )
href = ""
descr = "" return "\n".join(rows)
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 = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else:
legend = "&nbsp;&nbsp;&nbsp;" # 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)

View File

@ -148,7 +148,7 @@ def _convert_formsemestres_to_dicts(
), ),
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"groupicon": groupicon if nb_inscrits > 0 else emptygroupicon, "groupicon": groupicon if nb_inscrits > 0 else emptygroupicon,
"lockimg": lockicon, "lockimg": "" if formsemestre.etat else lockicon,
"modalite": formsemestre.modalite, "modalite": formsemestre.modalite,
"mois_debut": formsemestre.mois_debut(), "mois_debut": formsemestre.mois_debut(),
"mois_fin": formsemestre.mois_fin(), "mois_fin": formsemestre.mois_fin(),
@ -192,7 +192,7 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
"elt_sem_apo", "elt_sem_apo",
] ]
if showcodes: if showcodes:
columns_ids = ("formsemestre_id",) + columns_ids columns_ids.insert(0, "formsemestre_id") # prepend
html_class = "stripe cell-border compact hover order-column table_leftalign semlist" html_class = "stripe cell-border compact hover order-column table_leftalign semlist"
if current_user.has_permission(Permission.EditApogee): if current_user.has_permission(Permission.EditApogee):

View File

@ -58,21 +58,20 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
html_sco_header.sco_header(page_title="Suppression d'une formation"), html_sco_header.sco_header(page_title="Suppression d'une formation"),
f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""", f"""<h2>Suppression de la formation {formation.titre} ({formation.acronyme})</h2>""",
] ]
formsemestres = formation.formsemestres.all()
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id}) if formsemestres:
if sems:
H.append( H.append(
"""<p class="warning">Impossible de supprimer cette formation, """<p class="warning">Impossible de supprimer cette formation,
car les sessions suivantes l'utilisent:</p> car les sessions suivantes l'utilisent:</p>
<ul>""" <ul>"""
) )
for sem in sems: for formsemestre in formsemestres:
H.append( H.append(f"""<li>{formsemestre.html_link_status()}</li>""")
'<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
% sem
)
H.append( H.append(
'</ul><p><a class="stdlink" href="%s">Revenir</a></p>' % scu.NotesURL() f"""</ul>
<p><a class="stdlink" href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}">Revenir</a></p>"""
) )
else: else:
if not dialog_confirmed: if not dialog_confirmed:
@ -85,14 +84,16 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
</p> </p>
""", """,
OK="Supprimer cette formation", OK="Supprimer cette formation",
cancel_url=scu.NotesURL(), cancel_url=url_for("notes.index_html", scodoc_dept=g.scodoc_dept),
parameters={"formation_id": formation_id}, parameters={"formation_id": formation_id},
) )
else: else:
do_formation_delete(formation_id) do_formation_delete(formation_id)
H.append( H.append(
f"""<p>OK, formation supprimée.</p> f"""<p>OK, formation supprimée.</p>
<p><a class="stdlink" href="{scu.NotesURL()}">continuer</a></p>""" <p><a class="stdlink" href="{
url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
}">continuer</a></p>"""
) )
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
@ -252,7 +253,7 @@ def formation_edit(formation_id=None, create=False):
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.NotesURL()) return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else: else:
# check unicity : constraint UNIQUE(acronyme,titre,version) # check unicity : constraint UNIQUE(acronyme,titre,version)
if create: if create:

View File

@ -448,7 +448,7 @@ def module_edit(
( (
"titre", "titre",
{ {
"size": 30, "size": 64,
"explanation": """nom du module. Exemple: "explanation": """nom du module. Exemple:
<em>Introduction à la démarche ergonomique</em>""", <em>Introduction à la démarche ergonomique</em>""",
}, },
@ -456,8 +456,8 @@ def module_edit(
( (
"abbrev", "abbrev",
{ {
"size": 20, "size": 32,
"explanation": """nom abrégé (pour bulletins). "explanation": """(optionnel) nom abrégé pour bulletins.
Exemple: <em>Intro. à l'ergonomie</em>""", Exemple: <em>Intro. à l'ergonomie</em>""",
}, },
), ),

View File

@ -837,8 +837,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<a href="{url_for('notes.refcomp_show', <a href="{url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}" scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
class="stdlink"> class="stdlink">
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.get_title()}
{formation.referentiel_competence.specialite_long}
</a>&nbsp;""" </a>&nbsp;"""
msg_refcomp = "changer" msg_refcomp = "changer"
H.append(f"""<ul><li>{descr_refcomp}""") H.append(f"""<ul><li>{descr_refcomp}""")

View File

@ -25,8 +25,8 @@
# #
############################################################################## ##############################################################################
"""Evaluations """Evaluations"""
"""
import collections import collections
import datetime import datetime
import operator import operator
@ -50,6 +50,7 @@ from app.scodoc import sco_cal
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_gen_cal
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
@ -360,6 +361,106 @@ def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
return etat return etat
class JourEval(sco_gen_cal.Jour):
"""
Représentation d'un jour dans un calendrier d'évaluations
"""
COLOR_INCOMPLETE = "#FF6060"
COLOR_COMPLETE = "#A0FFA0"
COLOR_FUTUR = "#70E0FF"
def __init__(
self,
date: datetime.date,
evaluations: list[Evaluation],
parent: "CalendrierEval",
):
super().__init__(date)
self.evaluations: list[Evaluation] = evaluations
self.evaluations.sort(key=lambda e: e.date_debut)
self.parent: "CalendrierEval" = parent
def get_html(self) -> str:
htmls = []
for e in self.evaluations:
url: str = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl_id,
)
title: str = (
e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
)
htmls.append(
f"""<a
href="{url}"
style="{self._get_eval_style(e)}"
title="{self._get_eval_title(e)}"
class="stdlink"
>{title}</a>"""
)
return ", ".join(htmls)
def _get_eval_style(self, e: Evaluation) -> str:
color: str = ""
# Etat (notes completes) de l'évaluation:
modimpl_result = self.parent.nt.modimpls_results[e.moduleimpl.id]
if modimpl_result.evaluations_etat[e.id].is_complete:
color = JourEval.COLOR_COMPLETE
else:
color = JourEval.COLOR_INCOMPLETE
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
color = JourEval.COLOR_FUTUR
return f"background-color: {color};"
def _get_eval_title(self, e: Evaluation) -> str:
heure_debut_txt, heure_fin_txt = "", ""
if e.date_debut != e.date_fin:
heure_debut_txt = (
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else ""
)
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else ""
title = f"{e.description or e.moduleimpl.module.titre_str()}"
if heure_debut_txt:
title += f" de {heure_debut_txt} à {heure_fin_txt}"
return title
class CalendrierEval(sco_gen_cal.Calendrier):
"""
Représentation des évaluations d'un semestre dans un calendrier
"""
def __init__(self, year: int, evals: list[Evaluation], nt: NotesTableCompat):
# On prend du 01/09 au 31/08
date_debut: datetime.datetime = datetime.datetime(year, 9, 1, 0, 0)
date_fin: datetime.datetime = datetime.datetime(year + 1, 8, 31, 23, 59)
super().__init__(date_debut, date_fin)
# évalutions du semestre
self.evals: dict[datetime.date, list[Evaluation]] = {}
for e in evals:
if e.date_debut is not None:
day = e.date_debut.date()
if day not in self.evals:
self.evals[day] = []
self.evals[day].append(e)
self.nt: NotesTableCompat = nt
def instanciate_jour(self, date: datetime.date) -> JourEval:
return JourEval(date, self.evals.get(date, []), parent=self)
# View
def formsemestre_evaluations_cal(formsemestre_id): def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre""" """Page avec calendrier de toutes les evaluations de ce semestre"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
@ -368,58 +469,9 @@ def formsemestre_evaluations_cal(formsemestre_id):
evaluations = formsemestre.get_evaluations() evaluations = formsemestre.get_evaluations()
nb_evals = len(evaluations) nb_evals = len(evaluations)
color_incomplete = "#FF6060"
color_complete = "#A0FFA0"
color_futur = "#70E0FF"
year = formsemestre.annee_scolaire() year = formsemestre.annee_scolaire()
events = {} # (day, halfday) : event cal = CalendrierEval(year, evaluations, nt)
for e in evaluations: cal_html = cal.get_html()
if e.date_debut is None:
continue # éval. sans date
txt = e.moduleimpl.module.code or e.moduleimpl.module.abbrev or "éval."
if e.date_debut == e.date_fin:
heure_debut_txt, heure_fin_txt = "?", "?"
else:
heure_debut_txt = (
e.date_debut.strftime(scu.TIME_FMT) if e.date_debut else "?"
)
heure_fin_txt = e.date_fin.strftime(scu.TIME_FMT) if e.date_fin else "?"
description = f"""{
e.moduleimpl.module.titre
}, de {heure_debut_txt} à {heure_fin_txt}"""
# Etat (notes completes) de l'évaluation:
modimpl_result = nt.modimpls_results[e.moduleimpl.id]
if modimpl_result.evaluations_etat[e.id].is_complete:
color = color_complete
else:
color = color_incomplete
if e.date_debut > datetime.datetime.now(scu.TIME_ZONE):
color = color_futur
href = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl_id,
)
day = e.date_debut.date().isoformat() # yyyy-mm-dd
event = events.get(day)
if not event:
events[day] = [day, txt, color, href, description, e.moduleimpl]
else:
if event[-1].id != e.moduleimpl.id:
# plusieurs evals de modules differents a la meme date
event[1] += ", " + txt
event[4] += ", " + description
if color == color_incomplete:
event[2] = color_incomplete
if color == color_futur:
event[2] = color_futur
cal_html = sco_cal.YearTable(
year, events=list(events.values()), halfday=False, pad_width=None
)
return f""" return f"""
{ {
@ -435,15 +487,15 @@ def formsemestre_evaluations_cal(formsemestre_id):
</p> </p>
<ul> <ul>
<li>en <span style= <li>en <span style=
"background-color: {color_incomplete}">rouge</span> "background-color: {JourEval.COLOR_INCOMPLETE}">rouge</span>
les évaluations passées auxquelles il manque des notes les évaluations passées auxquelles il manque des notes
</li> </li>
<li>en <span style= <li>en <span style=
"background-color: {color_complete}">vert</span> "background-color: {JourEval.COLOR_COMPLETE}">vert</span>
les évaluations déjà notées les évaluations déjà notées
</li> </li>
<li>en <span style= <li>en <span style=
"background-color: {color_futur}">bleu</span> "background-color: {JourEval.COLOR_FUTUR}">bleu</span>
les évaluations futures les évaluations futures
</li> </li>
</ul> </ul>
@ -541,7 +593,9 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=e.moduleimpl.id, moduleimpl_id=e.moduleimpl.id,
), ),
"module_titre": e.moduleimpl.module.abbrev or e.moduleimpl.module.titre, "module_titre": e.moduleimpl.module.abbrev
or e.moduleimpl.module.titre
or "",
"responsable_id": e.moduleimpl.responsable_id, "responsable_id": e.moduleimpl.responsable_id,
"responsable_nomplogin": sco_users.user_info( "responsable_nomplogin": sco_users.user_info(
e.moduleimpl.responsable_id e.moduleimpl.responsable_id

View File

@ -103,7 +103,7 @@ class ScoPDFFormatError(ScoValueError):
super().__init__( super().__init__(
f"""Erreur dans un format pdf: f"""Erreur dans un format pdf:
<p>{msg}</p> <p>{msg}</p>
<p>Vérifiez les paramètres (polices de caractères, balisage) <p>Vérifiez les paramètres (polices de caractères, balisage, réglages bulletins...)
dans les paramètres ou préférences. dans les paramètres ou préférences.
</p> </p>
""", """,

View File

@ -489,9 +489,10 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
return formation.id, modules_old2new, ues_old2new return formation.id, modules_old2new, ues_old2new
def formation_list_table() -> GenTable: def formation_list_table(detail: bool) -> GenTable:
"""List formation, grouped by titre and sorted by versions """List formation, grouped by titre and sorted by versions
and listing associated semestres and listing associated semestres.
If detail, add column with more details.
returns a table returns a table
""" """
formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id) formations: list[Formation] = Formation.query.filter_by(dept_id=g.scodoc_dept_id)
@ -534,6 +535,15 @@ def formation_list_table() -> GenTable:
if formation.referentiel_competence if formation.referentiel_competence
else "" else ""
), ),
"_referentiel_target": (
url_for(
"notes.refcomp_show",
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id,
)
if formation.referentiel_competence
else ""
),
} }
# Ajoute les semestres associés à chaque formation: # Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by( row["formsemestres"] = formation.formsemestres.order_by(
@ -561,10 +571,15 @@ def formation_list_table() -> GenTable:
else [] else []
) )
) )
# Répartition des UEs dans les semestres
# utilise pour voir si la formation couvre tous les semestres
row["semestres_ues"] = ", ".join(
"S" + str(x if (x is not None and x > 0) else "-")
for x in sorted({(ue.semestre_idx or 0) for ue in formation.ues})
)
# Date surtout utilisées pour le tri:
if row["formsemestres"]: if row["formsemestres"]:
row["date_fin_dernier_sem"] = ( row["date_fin_dernier_sem"] = row["formsemestres"][-1].date_fin.isoformat()
row["formsemestres"][-1].date_fin.isoformat(),
)
row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year row["annee_dernier_sem"] = row["formsemestres"][-1].date_fin.year
else: else:
row["date_fin_dernier_sem"] = "" row["date_fin_dernier_sem"] = ""
@ -617,6 +632,8 @@ def formation_list_table() -> GenTable:
"commentaire", "commentaire",
"sems_list_txt", "sems_list_txt",
) )
if detail:
columns_ids += ("annee_dernier_sem", "semestres_ues")
titles = { titles = {
"buttons": "", "buttons": "",
"commentaire": "Commentaire", "commentaire": "Commentaire",
@ -627,6 +644,9 @@ def formation_list_table() -> GenTable:
"formation_code": "Code", "formation_code": "Code",
"sems_list_txt": "Semestres", "sems_list_txt": "Semestres",
"referentiel": "Réf.", "referentiel": "Réf.",
"date_fin_dernier_sem": "Fin dernier sem.",
"annee_dernier_sem": "Année dernier sem.",
"semestres_ues": "Semestres avec UEs",
} }
return GenTable( return GenTable(
columns_ids=columns_ids, columns_ids=columns_ids,
@ -639,7 +659,7 @@ def formation_list_table() -> GenTable:
html_class="formation_list_table table_leftalign", html_class="formation_list_table table_leftalign",
html_with_td_classes=True, html_with_td_classes=True,
html_sortable=True, html_sortable=True,
base_url=f"{request.base_url}", base_url=f"{request.base_url}" + ("?detail=on" if detail else ""),
page_title=title, page_title=title,
pdf_title=title, pdf_title=title,
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),

View File

@ -1431,18 +1431,25 @@ Ceci n'est possible que si :
def formsemestre_delete2(formsemestre_id, dialog_confirmed=False): def formsemestre_delete2(formsemestre_id, dialog_confirmed=False):
"""Delete a formsemestre (confirmation)""" """Delete a formsemestre (confirmation)"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# Confirmation dialog # Confirmation dialog
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Vous voulez vraiment supprimer ce semestre ???</h2><p>(opération irréversible)</p>""", """<h2>Vous voulez vraiment supprimer ce semestre ???</h2>
<p>(opération irréversible)</p>
""",
dest_url="", dest_url="",
cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id, cancel_url=url_for(
parameters={"formsemestre_id": formsemestre_id}, "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
parameters={"formsemestre_id": formsemestre.id},
) )
# Bon, s'il le faut... # Bon, s'il le faut...
do_formsemestre_delete(formsemestre_id) do_formsemestre_delete(formsemestre.id)
flash("Semestre supprimé !") flash("Semestre supprimé !")
return flask.redirect(scu.ScoURL()) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
def formsemestre_has_decisions_or_compensations( def formsemestre_has_decisions_or_compensations(

View File

@ -143,8 +143,10 @@ def _build_menu_stats(formsemestre: FormSemestre):
] ]
def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
"""HTML to render menubar""" """HTML to render menubar"""
if formsemestre is None:
return ""
formsemestre_id = formsemestre.id formsemestre_id = formsemestre.id
if formsemestre.etat: if formsemestre.etat:
change_lock_msg = "Verrouiller" change_lock_msg = "Verrouiller"
@ -632,7 +634,7 @@ def formsemestre_description_table(
"UE": modimpl.module.ue.acronyme, "UE": modimpl.module.ue.acronyme,
"_UE_td_attrs": ue_info.get("_UE_td_attrs", ""), "_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
"Code": modimpl.module.code or "", "Code": modimpl.module.code or "",
"Module": modimpl.module.abbrev or modimpl.module.titre, "Module": modimpl.module.abbrev or modimpl.module.titre or "",
"_Module_class": "scotext", "_Module_class": "scotext",
"Inscrits": mod_nb_inscrits, "Inscrits": mod_nb_inscrits,
"Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"], "Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"],

153
app/scodoc/sco_gen_cal.py Normal file
View File

@ -0,0 +1,153 @@
"""
Génération d'un calendrier
(Classe abstraite à implémenter dans les classes filles)
"""
import datetime
from flask import render_template
import app.scodoc.sco_utils as scu
from app import g
class Jour:
"""
Représente un jour dans le calendrier
Permet d'obtenir les informations sur le jour
et générer une représentation html
"""
def __init__(self, date: datetime.date):
self.date = date
self.class_list: list[str] = []
if self.is_non_work():
self.class_list.append("non-travail")
if self.is_current_week():
self.class_list.append("sem-courante")
def get_nom(self, short=True):
"""
Renvoie le nom du jour
"M19" ou "Mer 19"
par défaut en version courte
"""
str_jour: str = scu.DAY_NAMES[self.date.weekday()].capitalize()
return (
f"{str_jour[0] if short or self.is_non_work() else str_jour[:3]+' '}"
+ f"{self.date.day}"
)
def is_non_work(self):
"""
Renvoie True si le jour est un jour non travaillé
(en fonction de la préférence du département)
"""
return self.date.weekday() in scu.NonWorkDays.get_all_non_work_days(
dept_id=g.scodoc_dept_id
)
def is_current_week(self):
"""
Renvoie True si le jour est dans la semaine courante
"""
return self.date.isocalendar()[0:2] == datetime.date.today().isocalendar()[0:2]
def get_date(self) -> str:
"""
Renvoie la date du jour au format "dd/mm/yyyy"
"""
return self.date.strftime(scu.DATE_FMT)
def get_html(self):
"""
Renvoie le code html du jour
à surcharger dans les classes filles
l'html final ressemblera à :
<div class="jour {{jour.get_class()}}">
<span class="nom">{{jour.get_nom()}}</span>
<div class="contenu">
{{jour.get_html() | safe}}
</div>
</div>
"""
raise NotImplementedError("Méthode à implémenter dans les classes filles")
def get_class(self):
"""
Renvoie la classe css du jour
utilise self.class_list
-> fait un join de la liste
"""
return " ".join(self.class_list)
class Calendrier:
"""
Représente un calendrier
Permet d'obtenir les informations sur les jours
et générer une représentation html
"""
def __init__(self, date_debut: datetime.date, date_fin: datetime.date):
self.date_debut = date_debut
self.date_fin = date_fin
self.jours: dict[str, list[Jour]] = {}
def _get_dates_between(self) -> list[datetime.date]:
"""
get_dates_between Renvoie la liste des dates entre date_debut et date_fin
Returns:
list[datetime.date]: liste des dates entre date_debut et date_fin
"""
resultat = []
date_actuelle: datetime.date = self.date_debut
while date_actuelle <= self.date_fin:
if isinstance(date_actuelle, datetime.datetime):
resultat.append(date_actuelle.date())
elif isinstance(date_actuelle, datetime.date):
resultat.append(date_actuelle)
date_actuelle += datetime.timedelta(days=1)
return resultat
def organize_by_month(self):
"""
Organise les jours par mois
Instancie un objet Jour pour chaque jour
met à jour self.jours
"""
organized = {}
for date in self._get_dates_between():
# Récupérer le mois en français
month = scu.MONTH_NAMES_ABBREV[date.month - 1]
# Ajouter le jour à la liste correspondante au mois
if month not in organized:
organized[month] = []
jour: Jour = self.instanciate_jour(date)
organized[month].append(jour)
self.jours = organized
def instanciate_jour(self, date: datetime.date) -> Jour:
"""
Instancie un objet Jour pour chaque jour
A surcharger dans les classes filles si besoin
"""
raise NotImplementedError("Méthode à implémenter dans les classes filles")
def get_html(self):
"""
get_html Renvoie le code html du calendrier
"""
self.organize_by_month()
return render_template("calendrier.j2", calendrier=self.jours)

View File

@ -180,7 +180,7 @@ def fiche_etud(etudid=None):
) )
else: else:
info["etat_civil"] = "" info["etat_civil"] = ""
info["ScoURL"] = scu.ScoURL() info["ScoURL"] = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
info["authuser"] = current_user info["authuser"] = current_user
if restrict_etud_data: if restrict_etud_data:
info["info_naissance"] = "" info["info_naissance"] = ""

View File

@ -458,7 +458,12 @@ def pdf_basic_page(
if title: if title:
head = Paragraph(SU(title), StyleSheet["Heading3"]) head = Paragraph(SU(title), StyleSheet["Heading3"])
objects = [head] + objects objects = [head] + objects
document.build(objects)
try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return data return data

View File

@ -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",
{ {
@ -2260,16 +2261,17 @@ class BasePreferences:
before_table="<details><summary>{title}</summary>", before_table="<details><summary>{title}</summary>",
after_table="</details>", after_table="</details>",
) )
dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: if tf[0] == -1:
return flask.redirect(scu.ScoURL()) # cancel return flask.redirect(dest_url) # cancel
else: #
for pref in self.prefs_definition: for pref in self.prefs_definition:
self.prefs[None][pref[0]] = tf[2][pref[0]] self.prefs[None][pref[0]] = tf[2][pref[0]]
self.save() self.save()
flash("Préférences modifiées") flash("Préférences modifiées")
return flask.redirect(scu.ScoURL()) return flask.redirect(dest_url)
def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None): def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None):
"""Build list of elements for TrivialFormulator. """Build list of elements for TrivialFormulator.
@ -2433,10 +2435,12 @@ function set_global_pref(el, pref_name) {
before_table="<details><summary>{title}</summary>", before_table="<details><summary>{title}</summary>",
after_table="</details>", after_table="</details>",
) )
dest_url = ( dest_url = url_for(
scu.NotesURL() "notes.formsemestre_status",
+ "/formsemestre_status?formsemestre_id=%s" % self.formsemestre_id scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre_id,
) )
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
@ -2482,7 +2486,9 @@ function set_global_pref(el, pref_name) {
request.base_url + "?formsemestre_id=" + str(self.formsemestre_id) request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)
) )
elif destination == "global": elif destination == "global":
return flask.redirect(scu.ScoURL() + "/edit_preferences") return flask.redirect(
url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)
)
# #

View File

@ -50,7 +50,7 @@ from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_pv_dict from app.scodoc import sco_pv_dict
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
from app.scodoc.sco_cursus_dut import SituationEtudCursus from app.scodoc.sco_cursus_dut import SituationEtudCursus
from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres from app.scodoc.sco_pv_templates import CourrierIndividuelTemplate, jury_titres
import sco_version import sco_version
@ -132,7 +132,11 @@ def pdf_lettres_individuelles(
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return data return data
@ -241,13 +245,14 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non
titre_jury_court = "s" titre_jury_court = "s"
else: else:
titre_jury_court = "" titre_jury_court = ""
params[ params["autorisations_txt"] = (
"autorisations_txt" """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>"""
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % ( % (
etud.e, etud.e,
titre_jury_court, titre_jury_court,
titre_jury_court, titre_jury_court,
decision["autorisations_descr"], decision["autorisations_descr"],
)
) )
else: else:
params["autorisations_txt"] = "" params["autorisations_txt"] = ""

View File

@ -126,7 +126,11 @@ def pvjury_pdf(
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return data return data

View File

@ -1535,6 +1535,9 @@ def graph_cursus(
# semestre de depart en vert # semestre de depart en vert
n = g.get_node("SEM" + str(formsemestre_id))[0] n = g.get_node("SEM" + str(formsemestre_id))[0]
n.set_color("green") n.set_color("green")
n.set_style("filled")
n.set_fillcolor("lightgreen")
n.set_penwidth(2.0)
# demissions en rouge, octagonal # demissions en rouge, octagonal
for nid in dem_nodes.values(): for nid in dem_nodes.values():
n = g.get_node(nid)[0] n = g.get_node(nid)[0]

View File

@ -51,7 +51,24 @@ SCO_ROLES_DEFAULTS = {
p.UsersView, p.UsersView,
p.ViewEtudData, p.ViewEtudData,
), ),
# Rôles pour l'application relations entreprises # LecteurAPI peut utiliser l'API en lecture
"LecteurAPI": (p.ScoView,),
"Observateur": (p.Observateur,),
# RespPE est le responsable poursuites d'études
# il peut ajouter des tags sur les formations:
# (doit avoir un rôle Ens en plus !)
"RespPe": (p.EditFormationTags,),
# Super Admin est un root: création/suppression de départements
# _tous_ les droits
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
"SuperAdmin": p.ALL_PERMISSIONS,
}
# Rôles pour l'application relations entreprises
# séparés pour pouvoir les réinitialiser lors de l'activation du module Entreprises
# Note: Admin (chef de dept n'a par défaut aucun rôle lié à ce module)
SCO_ROLES_ENTREPRISES_DEFAULT = {
# ObservateurEntreprise est un observateur de l'application entreprise # ObservateurEntreprise est un observateur de l'application entreprise
"ObservateurEntreprise": (p.RelationsEntrepView,), "ObservateurEntreprise": (p.RelationsEntrepView,),
# UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification) # UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification)
@ -70,19 +87,10 @@ SCO_ROLES_DEFAULTS = {
p.RelationsEntrepValidate, p.RelationsEntrepValidate,
p.RelationsEntrepViewCorrs, p.RelationsEntrepViewCorrs,
), ),
# LecteurAPI peut utiliser l'API en lecture
"LecteurAPI": (p.ScoView,),
"Observateur": (p.Observateur,),
# RespPE est le responsable poursuites d'études
# il peut ajouter des tags sur les formations:
# (doit avoir un rôle Ens en plus !)
"RespPe": (p.EditFormationTags,),
# Super Admin est un root: création/suppression de départements
# _tous_ les droits
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
"SuperAdmin": p.ALL_PERMISSIONS,
} }
SCO_ROLES_DEFAULTS.update(SCO_ROLES_ENTREPRISES_DEFAULT)
# Les rôles accessibles via la page d'admin utilisateurs # Les rôles accessibles via la page d'admin utilisateurs
# - associés à un département: # - associés à un département:
ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe") ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe")

View File

@ -47,12 +47,11 @@ from app import db, log
from app.models import Identite from app.models import Identite
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoPDFFormatError, ScoValueError
from app.scodoc.sco_pdf import SU from app.scodoc.sco_pdf import SU
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_import_etuds from app.scodoc import sco_import_etuds
from app.scodoc import sco_etud
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
@ -388,7 +387,10 @@ def _trombino_pdf(groups_infos):
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]), preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
report.seek(0) report.seek(0)
return send_file( return send_file(
report, report,
@ -465,7 +467,10 @@ def _listeappel_photos_pdf(groups_infos):
preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]), preferences=sco_preferences.SemPreferences(sem["formsemestre_id"]),
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return scu.sendPDFFile(data, filename) return scu.sendPDFFile(data, filename)

View File

@ -31,7 +31,7 @@
""" """
import io import io
import reportlab
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.lib.colors import black from reportlab.lib.colors import black
from reportlab.lib.pagesizes import A4, A3 from reportlab.lib.pagesizes import A4, A3
@ -277,10 +277,12 @@ def pdf_trombino_tours(
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
) )
try: try:
document.build(objects) document.build(objects)
except (ValueError, KeyError) as exc: except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return scu.sendPDFFile(data, filename) return scu.sendPDFFile(data, filename)
@ -470,7 +472,10 @@ def pdf_feuille_releve_absences(
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
) )
document.build(objects) try:
document.build(objects)
except (ValueError, KeyError, reportlab.platypus.doctemplate.LayoutError) as exc:
raise ScoPDFFormatError(str(exc)) from exc
data = report.getvalue() data = report.getvalue()
return scu.sendPDFFile(data, filename) return scu.sendPDFFile(data, filename)

View File

@ -552,8 +552,8 @@ DAY_NAMES = ("lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "diman
TIME_FMT = "%H:%M" # affichage des heures TIME_FMT = "%H:%M" # affichage des heures
DATE_FMT = "%d/%m/%Y" # affichage des dates DATE_FMT = "%d/%m/%Y" # affichage des dates
DATEATIME_FMT = DATE_FMT + " à " + DATE_FMT DATEATIME_FMT = DATE_FMT + " à " + TIME_FMT
DATETIME_FMT = DATE_FMT + " " + DATE_FMT DATETIME_FMT = DATE_FMT + " " + TIME_FMT
def fmt_note(val, note_max=None, keep_numeric=False): def fmt_note(val, note_max=None, keep_numeric=False):
@ -785,51 +785,6 @@ BULLETINS_VERSIONS_BUT = BULLETINS_VERSIONS | {
"butcourt": "Version courte spéciale BUT" "butcourt": "Version courte spéciale BUT"
} }
# ----- Support for ScoDoc7 compatibility
def ScoURL():
"""base URL for this sco instance.
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite
= page accueil département
"""
return url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
def NotesURL():
"""URL of Notes
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Notes
= url de base des méthodes de notes
(page accueil programmes).
"""
return url_for("notes.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
def AbsencesURL():
"""URL of Absences"""
return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
]
def AssiduitesURL():
"""URL of Assiduités"""
return url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)[
: -len("/BilanDept")
]
def UsersURL():
"""URL of Users
e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users
= url de base des requêtes ZScoUsers
et page accueil users
"""
return url_for("users.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")]
# ---- Simple python utilities # ---- Simple python utilities

View File

@ -485,6 +485,10 @@
cursor: pointer; cursor: pointer;
} }
.mass-selection em {
margin-left: 16px;
}
.fieldsplit { .fieldsplit {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
@ -726,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,
@ -758,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;
} }

View File

@ -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%;
} }

View File

@ -2411,10 +2411,10 @@ li.notes_formation_list {
padding-top: 10px; padding-top: 10px;
} }
table.formation_list_table { table.dataTable.formation_list_table.gt_table {
width: 100%;
border-collapse: collapse; border-collapse: collapse;
background-color: rgb(0%, 90%, 90%); margin-right: 12px;
margin-left: 12px;
} }
table#formation_list_table tr.gt_hl { table#formation_list_table tr.gt_hl {
@ -2455,8 +2455,8 @@ table.formation_list_table td.buttons span.but_placeholder {
text-align: center; text-align: center;
} }
.formation_list_table td.titre { .formation_list_table td.sems_list_txt {
width: 45%; width: 15%;
} }
.formation_list_table td.commentaire { .formation_list_table td.commentaire {

View File

@ -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();

View File

@ -1,6 +1,6 @@
function _partition_set_attr(partition_id, attr_name, attr_value) { function _partition_set_attr(partition_id, attr_name, attr_value) {
$.post( $.post(
SCO_URL + "/partition_set_attr", SCO_URL + "partition_set_attr",
{ {
partition_id: partition_id, partition_id: partition_id,
attr: attr_name, attr: attr_name,

View File

@ -33,7 +33,7 @@ function update_ue_list() {
let ue_code = $("#tf_ue_code")[0].value; let ue_code = $("#tf_ue_code")[0].value;
let query = let query =
SCO_URL + SCO_URL +
"/Notes/ue_sharing_code?ue_code=" + "Notes/ue_sharing_code?ue_code=" +
ue_code + ue_code +
"&hide_ue_id=" + "&hide_ue_id=" +
ue_id + ue_id +

View File

@ -16,7 +16,7 @@ function display_itemsuivis(active) {
.off("click") .off("click")
.click(function (e) { .click(function (e) {
e.preventDefault(); e.preventDefault();
$.post(SCO_URL + "/itemsuivi_create", { $.post(SCO_URL + "itemsuivi_create", {
etudid: etudid, etudid: etudid,
fmt: "json", fmt: "json",
}).done(item_insert_new); }).done(item_insert_new);
@ -26,7 +26,7 @@ function display_itemsuivis(active) {
} }
// add existing items // add existing items
$.get( $.get(
SCO_URL + "/itemsuivi_list_etud", SCO_URL + "itemsuivi_list_etud",
{ etudid: etudid, fmt: "json" }, { etudid: etudid, fmt: "json" },
function (L) { function (L) {
for (var i in L) { for (var i in L) {
@ -95,7 +95,7 @@ function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
dp.blur(function (e) { dp.blur(function (e) {
var date = this.value; var date = this.value;
// console.log('selected text: ' + date); // console.log('selected text: ' + date);
$.post(SCO_URL + "/itemsuivi_set_date", { $.post(SCO_URL + "itemsuivi_set_date", {
item_date: date, item_date: date,
itemsuivi_id: itemsuivi_id, itemsuivi_id: itemsuivi_id,
}); });
@ -103,7 +103,7 @@ function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
dp.datepicker({ dp.datepicker({
onSelect: function (date, instance) { onSelect: function (date, instance) {
// console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id); // console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id);
$.post(SCO_URL + "/itemsuivi_set_date", { $.post(SCO_URL + "itemsuivi_set_date", {
item_date: date, item_date: date,
itemsuivi_id: itemsuivi_id, itemsuivi_id: itemsuivi_id,
}); });
@ -161,7 +161,7 @@ function Date2DMY(date) {
} }
function itemsuivi_suppress(itemsuivi_id) { function itemsuivi_suppress(itemsuivi_id) {
$.post(SCO_URL + "/itemsuivi_suppress", { itemsuivi_id: itemsuivi_id }); $.post(SCO_URL + "itemsuivi_suppress", { itemsuivi_id: itemsuivi_id });
// Clear items and rebuild: // Clear items and rebuild:
$("ul.listdebouches li.itemsuivi").remove(); $("ul.listdebouches li.itemsuivi").remove();
display_itemsuivis(0); display_itemsuivis(0);

View File

@ -37,7 +37,7 @@ $().ready(function () {
ajax: { ajax: {
url: url:
SCO_URL + SCO_URL +
"/etud_info_html?etudid=" + "etud_info_html?etudid=" +
get_etudid_from_elem(elems[i]) + get_etudid_from_elem(elems[i]) +
qs, qs,
type: "GET", type: "GET",

View File

@ -19,7 +19,7 @@ function loadGroupes() {
$("#gmsg")[0].style.display = "block"; $("#gmsg")[0].style.display = "block";
var partition_id = document.formGroup.partition_id.value; var partition_id = document.formGroup.partition_id.value;
$.get(SCO_URL + "/XMLgetGroupsInPartition", { $.get(SCO_URL + "XMLgetGroupsInPartition", {
partition_id: partition_id, partition_id: partition_id,
}).done(function (data) { }).done(function (data) {
var nodes = data.getElementsByTagName("group"); var nodes = data.getElementsByTagName("group");
@ -384,7 +384,7 @@ function handleError(msg) {
} }
function submitGroups() { function submitGroups() {
var url = SCO_URL + "/setGroups"; var url = SCO_URL + "setGroups";
// build post request body: groupname \n etudid; ... // build post request body: groupname \n etudid; ...
var groupsLists = ""; var groupsLists = "";
var groupsToCreate = ""; var groupsToCreate = "";
@ -443,7 +443,7 @@ function GotoAnother() {
} else } else
document.location = document.location =
SCO_URL + SCO_URL +
"/affect_groups?partition_id=" + "affect_groups?partition_id=" +
document.formGroup.other_partition_id.value; document.formGroup.other_partition_id.value;
} }

View File

@ -5,7 +5,7 @@ $().ready(function () {
for (var i = 0; i < spans.length; i++) { for (var i = 0; i < spans.length; i++) {
var sp = spans[i]; var sp = spans[i];
var etudid = sp.id; var etudid = sp.id;
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid); $(sp).load(SCO_URL + "etud_photo_html?etudid=" + etudid);
} }
}); });
@ -194,7 +194,7 @@ $().ready(function () {
ajax: { ajax: {
url: url:
SCO_URL + SCO_URL +
"/etud_info_html?with_photo=0&etudid=" + "etud_info_html?with_photo=0&etudid=" +
get_etudid_from_elem(elems[i]), get_etudid_from_elem(elems[i]),
}, },
text: "Loading...", text: "Loading...",

View File

@ -34,7 +34,7 @@ function get_notes_and_draw(formsemestre_id, etudid) {
*/ */
var query = var query =
SCO_URL + SCO_URL +
"/Notes/formsemestre_bulletinetud?formsemestre_id=" + "Notes/formsemestre_bulletinetud?formsemestre_id=" +
formsemestre_id + formsemestre_id +
"&etudid=" + "&etudid=" +
etudid + etudid +

View File

@ -42,7 +42,7 @@ async function save_note(elem, v, etudid) {
$("#sco_msg").html("en cours...").show(); $("#sco_msg").html("en cours...").show();
try { try {
const response = await fetch( const response = await fetch(
SCO_URL + "/../api/evaluation/" + evaluation_id + "/notes/set", SCO_URL + "../api/evaluation/" + evaluation_id + "/notes/set",
{ {
method: "POST", method: "POST",
headers: { headers: {

View File

@ -6,7 +6,7 @@ $(function () {
delay: 300, // wait 300ms before suggestions delay: 300, // wait 300ms before suggestions
minLength: 2, // min nb of chars before suggest minLength: 2, // min nb of chars before suggest
position: { collision: "flip" }, // automatic menu position up/down position: { collision: "flip" }, // automatic menu position up/down
source: SCO_URL + "/search_etud_by_name", source: SCO_URL + "search_etud_by_name",
select: function (event, ui) { select: function (event, ui) {
$(".in-expnom").val(ui.item.value); $(".in-expnom").val(ui.item.value);
$("#form-chercheetud").submit(); $("#form-chercheetud").submit();

View File

@ -5,6 +5,6 @@ $().ready(function () {
for (var i = 0; i < spans.size(); i++) { for (var i = 0; i < spans.size(); i++) {
var sp = spans[i]; var sp = spans[i];
var etudid = sp.id; var etudid = sp.id;
$(sp).load(SCO_URL + "/etud_photo_html?etudid=" + etudid); $(sp).load(SCO_URL + "etud_photo_html?etudid=" + etudid);
} }
}); });

View File

@ -22,7 +22,7 @@ document.addEventListener("DOMContentLoaded", () => {
async function delete_validation(etudid, validation_type, validation_id) { async function delete_validation(etudid, validation_type, validation_id) {
const response = await fetch( const response = await fetch(
`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`, `${SCO_URL}../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`,
{ {
method: "POST", method: "POST",
} }
@ -38,7 +38,7 @@ async function delete_validation(etudid, validation_type, validation_id) {
function update_ue_list() { function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value; var ue_id = $("#tf_ue_id")[0].value;
if (ue_id) { if (ue_id) {
var query = SCO_URL + "/Notes/ue_sharing_code?ue_id=" + ue_id; var query = SCO_URL + "Notes/ue_sharing_code?ue_id=" + ue_id;
$.get(query, "", function (data) { $.get(query, "", function (data) {
$("#ue_list_code").html(data); $("#ue_list_code").html(data);
}); });

View File

@ -265,6 +265,8 @@ class Table(Element):
title: str = None, title: str = None,
classes: list[str] = None, classes: list[str] = None,
raw_title: str = None, raw_title: str = None,
no_excel: bool = False,
only_excel: bool = False,
) -> tuple["Cell", "Cell"]: ) -> tuple["Cell", "Cell"]:
"""Record this title, """Record this title,
and create cells for footer and header if they don't already exist. and create cells for footer and header if they don't already exist.
@ -282,6 +284,8 @@ class Table(Element):
classes=classes, classes=classes,
group=self.column_group.get(col_id), group=self.column_group.get(col_id),
raw_content=raw_title or title, raw_content=raw_title or title,
no_excel=no_excel,
only_excel=only_excel,
) )
if self.foot_title_row: if self.foot_title_row:
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell( self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
@ -370,6 +374,7 @@ class Row(Element):
target_attrs: dict = None, target_attrs: dict = None,
target: str = None, target: str = None,
column_classes: set[str] = None, column_classes: set[str] = None,
only_excel: bool = False,
no_excel: bool = False, no_excel: bool = False,
) -> "Cell": ) -> "Cell":
"""Create cell and add it to the row. """Create cell and add it to the row.
@ -397,6 +402,7 @@ class Row(Element):
column_group=group, column_group=group,
title=title, title=title,
raw_title=raw_title, raw_title=raw_title,
only_excel=only_excel,
no_excel=no_excel, no_excel=no_excel,
) )
@ -406,6 +412,7 @@ class Row(Element):
cell: "Cell", cell: "Cell",
column_group: str | None = None, column_group: str | None = None,
title: str | None = None, title: str | None = None,
only_excel: bool = False,
no_excel: bool = False, no_excel: bool = False,
raw_title: str | None = None, raw_title: str | None = None,
) -> "Cell": ) -> "Cell":
@ -414,10 +421,10 @@ class Row(Element):
""" """
cell.data["group"] = column_group or "" cell.data["group"] = column_group or ""
self.cells[col_id] = cell self.cells[col_id] = cell
if col_id not in self.table.column_ids: if not only_excel and col_id not in self.table.column_ids:
self.table.column_ids.append(col_id) self.table.column_ids.append(col_id)
if not no_excel: if not no_excel and col_id not in self.table.raw_column_ids:
self.table.raw_column_ids.append(col_id) self.table.raw_column_ids.append(col_id)
self.table.insert_group(column_group) self.table.insert_group(column_group)
if column_group is not None: if column_group is not None:
@ -425,7 +432,12 @@ class Row(Element):
if title is not None: if title is not None:
self.table.add_title( self.table.add_title(
col_id, title, classes=cell.classes, raw_title=raw_title col_id,
title,
classes=cell.classes,
raw_title=raw_title,
no_excel=no_excel,
only_excel=only_excel,
) )
return cell return cell

View File

@ -4,16 +4,17 @@
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""Liste simple d'étudiants """Liste simple d'étudiants"""
"""
import datetime
from flask import g, url_for from flask import g, url_for
from app import log from app import log
from app.models import FormSemestre, Identite, Justificatif from app.models import FormSemestre, Identite, Justificatif
from app.tables import table_builder as tb from app.tables import table_builder as tb
import app.scodoc.sco_assiduites as scass
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
import app.scodoc.sco_assiduites as scass
from app.scodoc.sco_exceptions import ScoValueError
class TableAssi(tb.Table): class TableAssi(tb.Table):
@ -39,7 +40,13 @@ class TableAssi(tb.Table):
): ):
self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows self.rows: list["RowAssi"] = [] # juste pour que VSCode nous aide sur .rows
classes = ["gt_table"] classes = ["gt_table"]
self.dates = [str(dates[0]) + "T00:00", str(dates[1]) + "T23:59"] try:
self.dates = [
datetime.datetime.fromisoformat(str(dates[0]) + "T00:00"),
datetime.datetime.fromisoformat(str(dates[1]) + "T00:00"),
]
except ValueError as exc:
raise ScoValueError("invalid dates") from exc
self.formsemestre = formsemestre self.formsemestre = formsemestre
self.formsemestre_modimpls = formsemestre_modimpls self.formsemestre_modimpls = formsemestre_modimpls
if convert_values: if convert_values:
@ -97,6 +104,20 @@ class RowAssi(tb.Row):
bilan_etud = url_for( bilan_etud = url_for(
"assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id "assiduites.bilan_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
) )
self.add_cell(
"etudid",
"etudid",
etud.etudid,
"etudinfo",
only_excel=True,
)
self.add_cell(
"code_nip",
"code_nip",
etud.code_nip,
"etudinfo",
only_excel=True,
)
self.add_cell( self.add_cell(
"nom_disp", "nom_disp",
"Nom", "Nom",
@ -119,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(

View File

@ -0,0 +1,51 @@
{% extends "base.j2" %}
{% import 'wtf.j2' as wtf %}
{% block app_content %}
<h1>{{title}}</h1>
<div class="help">
<p>
</p>
<p>
</p>
</div>
<div class="row">
<div class="col-md-8">
<form class="form form-horizontal spacediv" method="post" enctype="multipart/form-data" role="form">
{{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }}
{% if is_enabled %}
<p>Le module <em>relations entreprises</em> est actuellement activé.</p>
<p>Il peut être activé ou désactivé à tout moment sans aucune perte de
données (la désactivation le fait simplement disparaitre des pages
utilisateurs).
<p>
{% else %}
<p>Le module <em>relations entreprises</em> est actuellement désactivé.
</p>
<p>Il peut être activé ou désactivé à tout moment sans aucune perte de
données (la désactivation le fait simplement disparaitre des pages
utilisateurs).
<p>
<p>
Lors de son activation, vous pouvez (re)positionner les rôles qu'il utilise
à leurs valeurs par défaut en cochant la case ci-dessous.
</p>
{{ wtf.form_field(form.set_default_roles_permission) }}
{% endif %}
<div class="form-group spacediv">
{{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }}
</div>
</div>
</div>
<style>
.spacediv {
margin-top: 16px;
}
</style>
{% endblock %}

View File

@ -23,49 +23,8 @@ Calendrier de l'assiduité
for="mode_demi">mode demi journée</label> for="mode_demi">mode demi journée</label>
</div> </div>
<div class="calendrier"> <div class="cal">
{% for mois,jours in calendrier.items() %} {{calendrier|safe}}
<div class="month">
<h3>{{mois}}</h3>
<div class="days {{'demi' if mode_demi else ''}}">
{% for jour in jours %}
{% if jour.is_non_work() %}
<div class="day {{jour.get_class()}}">
<span>{{jour.get_nom()}}</span>
{% else %}
<div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}">
{% endif %}
{% if mode_demi %}
{% if not jour.is_non_work() %}
<span>{{jour.get_nom()}}</span>
<span class="{{jour.get_demi_class(True, show_pres,show_reta)}}"></span>
<span class="{{jour.get_demi_class(False, show_pres,show_reta)}}"></span>
{% endif %}
{% else %}
{% if not jour.is_non_work() %}
<span>{{jour.get_nom(False)}}</span>
{% endif %}
{% endif %}
{% if not jour.is_non_work() and jour.has_assiduites()%}
<div class="dayline">
<div class="dayline-title">
<span>Assiduité du</span>
<br>
<span>{{jour.get_date()}}</span>
{{jour.generate_minitimeline() | safe}}
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="annee"> <div class="annee">
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer <span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
année: </span> année: </span>
@ -77,36 +36,7 @@ Calendrier de l'assiduité
<div class="help"> <div class="help">
<h3>Calendrier</h3> <h3>Calendrier</h3>
<p>Code couleur</p> {% include "assiduites/widgets/legende_couleur.j2" %}
<ul class="couleurs">
<li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
période
</li>
<li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Quart Bleu" class="est_just demo"></span> &rightarrow; la période est couverte par un
justificatif valide</li>
<li><span title="Justif. non valide" class="invalide demo"></span> &rightarrow; la période est
couverte par un justificatif non valide
</li>
<li><span title="Justif. en attente" class="attente demo"></span> &rightarrow; 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
@ -149,7 +79,7 @@ Calendrier de l'assiduité
list-style-type: none; list-style-type: none;
} }
.pageContent { .pageContent {
margin-top: 1vh; margin-top: 1vh;
@ -158,57 +88,34 @@ Calendrier de l'assiduité
.calendrier { .calendrier {
display: flex; display: flex;
justify-content: start; justify-content: center;
overflow-x: scroll; overflow-x: scroll;
border: 1px solid #444; border: 1px solid #444;
border-radius: 12px; border-radius: 12px;
margin-bottom: 12px; margin-bottom: 12px;
} }
.month h3 { .assi_case {
text-align: center;
}
.day,
.demi .day.color.nonwork {
text-align: left;
margin: 2px;
cursor: default;
font-size: 13px;
position: relative;
font-weight: normal;
min-width: 6em;
display: flex; display: flex;
justify-content: start; width: 100%;
height: 100%;
} }
.demo.invalide { .assi_case > span {
background-color: var(--color-justi-invalide) !important; flex: 1;
} }
.demo.attente { .assi_case>span:last-of-type {
background: repeating-linear-gradient(to bottom, border-left: #d5d5d5 solid 1px;
var(--color-justi-attente-stripe) 0px, }
var(--color-justi-attente-stripe) 4px, .assi_case>span:first-of-type {
var(--color-justi-attente) 4px, border-right: #d5d5d5 solid 1px;
var(--color-justi-attente) 7px) !important;
} }
.demo.est_just { .dayline{
background-color: var(--color-justi) !important; display: none;
} }
.demi .day.nonwork>span {
flex: none;
border: none;
}
.demi .day {
border-radius: 0;
}
@media print { @media print {
.couleurs.print { .couleurs.print {
@ -335,10 +242,8 @@ Calendrier de l'assiduité
document.querySelectorAll('[assi_id]').forEach((el, i) => { document.querySelectorAll('[assi_id]').forEach((el, i) => {
el.addEventListener('click', () => { el.addEventListener('click', () => {
const assi_id = el.getAttribute('assi_id'); const assi_id = el.getAttribute('assi_id');
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 %}

View File

@ -67,7 +67,7 @@
text-align: center; text-align: center;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.cell, .header { .cell, .header {
border: 1px solid #ddd; border: 1px solid #ddd;
padding: 10px; padding: 10px;
@ -111,7 +111,7 @@
display: flex; display: flex;
gap: 4px; gap: 4px;
} }
.pointer{ .pointer{
cursor: pointer; cursor: pointer;
} }
@ -220,7 +220,7 @@ async function nouvellePeriode(period = null) {
let periodeDiv = document.createElement("div"); let periodeDiv = document.createElement("div");
periodeDiv.classList.add("cell", "header"); periodeDiv.classList.add("cell", "header");
periodeDiv.id = `periode-${periodId}`; periodeDiv.id = `periode-${periodId}`;
const periodP = document.createElement("p"); const periodP = document.createElement("p");
periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`; periodP.textContent = `Plage du ${date} de ${debut} à ${fin}`;
@ -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;
@ -402,12 +407,12 @@ function sauvegarderAssiduites() {
await nouvellePeriode(periode); await nouvellePeriode(periode);
} }
// Si il y n'a pas d'erreur, on affiche un message de succès // Si il n'y a pas d'erreur, on affiche un message de succès
if (data.errors.length == 0) { if (data.errors.length == 0) {
const span = document.createElement("span"); const span = document.createElement("span");
span.textContent = "Les assiduités ont bien été sauvegardées."; span.textContent = "Le relevé d'assiduité a été enregistré.";
openAlertModal( openAlertModal(
"Sauvegarde des assiduités", "Enregistrement de l'assiduité",
span, span,
null, null,
"var(--color-present)" "var(--color-present)"
@ -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>
@ -597,21 +621,23 @@ main();
</label> </label>
<label for="etatDef"> <label for="etatDef">
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>
</label> </label>
</div> </div>
<!-- Tableau à double entrée <!-- Tableau à double entrée
Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif)) Colonne : Etudiants (Header = Nom, Prénom, Photo (si actif))
Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId) Ligne : Période (Header = Jour, Heure de début, Heure de fin, ModuleImplId)
Contenu : Contenu :
- bouton assiduité (présent, retard, absent) - bouton assiduité (présent, retard, absent)
- Bouton conflit si conflit de période - Bouton conflit si conflit de période
---> --->

View File

@ -25,12 +25,13 @@
setupTimeLine(()=>{creerTousLesEtudiants(etuds)}) setupTimeLine(()=>{creerTousLesEtudiants(etuds)})
{% endif %} {% endif %}
const nonWorkDays = [{{ nonworkdays| safe }}]; const nonWorkDays = [{{ nonworkdays| safe }}];
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 }}
@ -61,7 +62,7 @@
$('#date').on('change', async function(d) { $('#date').on('change', async function(d) {
// On vérifie si la date est un jour travaillé // On vérifie si la date est un jour travaillé
dateCouranteEstTravaillee(); dateCouranteEstTravaillee();
await recupAssiduites(etuds, $("#date").datepicker("getDate")); await recupAssiduites(etuds, $("#date").datepicker("getDate"));
@ -87,7 +88,7 @@
await recupAssiduites(etuds, $("#date").datepicker("getDate")); await recupAssiduites(etuds, $("#date").datepicker("getDate"));
} }
creerTousLesEtudiants(etuds); creerTousLesEtudiants(etuds);
// affichage ou non des PDP // affichage ou non des PDP
afficherPDP(localStorage.getItem("scodoc-etud-pdp") == "true" ) afficherPDP(localStorage.getItem("scodoc-etud-pdp") == "true" )
} }
@ -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"
@ -168,20 +171,26 @@
<input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun" <input type="checkbox" value="remove" name="mass_btn_assiduites" id="mass_rbtn_aucun"
class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer"> class="rbtn aucun" onclick="mettreToutLeMonde('vide', this)" title="Supprimer">
</fieldset> </fieldset>
<em>Les saisies ci-dessous sont enregistrées au fur et à mesure.</em>
</div> </div>
{% endif %} {% endif %}
<div class="etud_holder"> <div class="etud_holder">
<p class="placeholder"> <p class="placeholder">
</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" %}
{% include "assiduites/widgets/conflict.j2" %} {% include "assiduites/widgets/conflict.j2" %}
</section> </section>

View File

@ -1,12 +1,28 @@
<li><span title="Vert" class="present demo"></span> &rightarrow; 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> &rightarrow; retard de l'étudiant lors de la période <li><span title="Vert" class="present demo"></span> &rightarrow; présence de l'étudiant lors de la
</li> période
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la période </li>
</li> <li><span title="Bleu clair" class="nonwork demo"></span> &rightarrow; la période n'est pas travaillée
</li>
<li><span title="Rouge" class="absent demo"></span> &rightarrow; absence de l'étudiant lors de la
période
</li>
<li><span title="Rose" class="demo color absent est_just"></span> &rightarrow; absence justifiée
</li>
<li><span title="Orange" class="retard demo"></span> &rightarrow; retard de l'étudiant lors de la
période
</li>
<li><span title="Jaune clair" class="demo color retard est_just"></span> &rightarrow; retard justifié
</li>
<li><span title="Hachure Bleue" class="justified demo"></span> &rightarrow; l'assiduité est justifiée par un <li><span title="Quart Bleu" class="est_just demo color"></span> &rightarrow; la période est couverte par un
justificatif valide</li> justificatif valide</li>
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> &rightarrow; l'assiduité est <li><span title="Justif. non valide" class="invalide demo color "></span> &rightarrow; 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> &rightarrow; 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>

View File

@ -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");

View File

@ -1,6 +1,4 @@
{# Base de toutes les pages ScoDoc #} {%- block doc -%}<!DOCTYPE html>{# Base de toutes les pages ScoDoc #}
{% block doc -%}
<!DOCTYPE html>
<html{% block html_attribs %}{% endblock html_attribs %}> <html{% block html_attribs %}{% endblock html_attribs %}>
{%- block html %} {%- block html %}
<head> <head>

View File

@ -102,6 +102,6 @@
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script> <script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
<script> <script>
const SCO_URL = "{% if g.scodoc_dept %}{{ const SCO_URL = "{% if g.scodoc_dept %}{{
url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}{% endif %}"; url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)}}{% endif %}";
</script> </script>
{% endblock %} {% endblock %}

View File

@ -11,9 +11,9 @@
)}}">{{etud.nomprenom}}</a></h2> )}}">{{etud.nomprenom}}</a></h2>
{% endif %} {% endif %}
<form name="f" method="GET" action="{{request.base_url}}"> <form name="f" method="GET" action="{{request.base_url}}">
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}"></input> <input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}">
<input type="hidden" name="etudid" value="{{etud.id}}"></input> <input type="hidden" name="etudid" value="{{etud.id}}">
<input type="hidden" name="fmt" value="{{fmt}}"></input> <input type="hidden" name="fmt" value="{{fmt}}">
<div class="bull_titre_semestre"> <div class="bull_titre_semestre">
Bulletin Bulletin
<span class="bull_liensemestre"> <span class="bull_liensemestre">
@ -36,7 +36,7 @@
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
etudid=etud.id, etudid=etud.id,
) )
}}&version='+this.value;"" class="noprint"> }}&version='+this.value;" class="noprint">
{% if formsemestre.formation.is_apc() %} {% if formsemestre.formation.is_apc() %}
{% set menu_items = scu.BULLETINS_VERSIONS_BUT.items() %} {% set menu_items = scu.BULLETINS_VERSIONS_BUT.items() %}
{% else %} {% else %}

View File

@ -0,0 +1,58 @@
{# -*- mode: jinja-html -*- #}
{% extends "sco_page.j2" %}
{% import 'wtf.j2' as wtf %}
{% block app_content %}
<h1>Changer le référentiel de compétences de la formation
{{formation.get_titre_version()}}
</h1>
<div class="help">
<p> Normalement, il n'est pas possible de changer une
formation de référentiel de compétences. En effet, de nombreux éléments sont
déduis du référentiel: les parcours, les compétences, les apprentissages
critiques, et donc les validations de jury des étudiants qui ont suivi la
formation.
</p>
<p> Cependant, dans certains cas, le ministère a publié une nouvelle version
d'un référentiel de spécialité, mais les changements ne sont que très légers:
détails des noms de parcours ou des compétences. Dans ces cas là, la structure
étant la même, il est autorisé de changer le référentiel.
</p>
<p>Seuls les référentiels déjà chargés et compatibles (ayant la même structure)
sont proposés dans le menu ci-dessous.
</p>
<p class="fontred">
Attention: tout changement de référentiel entraine la perte des associations
entre les modules et les apprentissages critiques.
</p>
<p class="fontred">
Attention: <b>fonction expérimentale</b>. Vérifiez vos sauvegardes.
</p>
</div>
<div>La formation est actuellement associée au référentiel
<a class="stdlink" href="{{
url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id)
}}">{{formation.referentiel_competence.get_title()}}</a>
</div>
<form method="POST" style="margin-top: 24px;">
{{ form.hidden_tag() }}
<div>
{{ form.object_select.label }}<br>
{{ form.object_select }}
</div>
<div style="margin-top: 24px;">{{ form.submit() }} {{ form.cancel() }}</div>
</form>
{% endblock %}

View File

@ -27,9 +27,18 @@
<li>Formations se référant à ce référentiel: <li>Formations se référant à ce référentiel:
<ul> <ul>
{% for formation in ref.formations %} {% for formation in ref.formations %}
<li><a class="stdlink" href="{{ <li>
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id ) <a class="stdlink" href="{{
}}">{{ formation.get_titre_version() }}</a></li> url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}">{{ formation.get_titre_version() }}</a>
{% if referentiels_equivalents %}
<a style="margin-left: 8px;" class="stdlink" href="
{{ url_for('notes.formation_change_refcomp',
scodoc_dept=g.scodoc_dept, formation_id=formation.id )
}}
">(changer)</a>
{% endif %}
</li>
{% else %} {% else %}
<li><em>aucune</em></li> <li><em>aucune</em></li>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,88 @@
<div class="calendrier">
{% for mois,jours in calendrier.items() %}
<div class="mois">
<h3>{{mois}}</h3>
<div class="jours">
{% for jour in jours %}
<div class="jour {{jour.get_class()}}">
<span class="nom">{{jour.get_nom()}}</span>
<div class="contenu">
{{jour.get_html() | safe}}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<style>
.calendrier {
display: flex;
justify-content: center;
overflow-x: scroll;
border: 1px solid #444;
border-radius: 12px;
margin-bottom: 12px;
}
.mois {
flex: 1;
}
.mois h3 {
text-align: center;
}
.jour {
text-align: left;
margin: 2px;
cursor: default;
font-size: 13px;
position: relative;
font-weight: normal;
min-width: 6em;
display: flex;
justify-content: start;
}
.jour>.contenu {
background-color: white;
width: 100%;
}
.jour.jour.non-travail>.nom,
.jour.jour.non-travail>.contenu {
border: 0;
background-color: #badfff;
}
.jour>.nom {
width: 3em;
min-width: 3em;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.jour>.contenu,
.jour>.nom {
border: 1px solid #d5d5d5;
position: relative;
}
.jour>.contenu a {
padding: 0px 2px;
}
.jour>.nom {
text-align: center;
}
.sem-courante{
--couleur : #ee752c;
border-left: solid 3px var(--couleur);
border-right: solid 3px var(--couleur);
}
</style>

View File

@ -39,6 +39,18 @@ Heure: <b><tt>{{ time.strftime("%d/%m/%Y %H:%M") }}</tt></b>
<div class="scobox"> <div class="scobox">
<div class="scobox-title">ScoDoc : paramètres généraux</div> <div class="scobox-title">ScoDoc : paramètres généraux</div>
<div style="margin-top: 16px;">
Le module <em>Relations Entreprises</em>
{% if is_entreprises_enabled %}
est <b>activé</b>
{% else %}
n'est pas activé
{% endif %}
: <a class="stdlink" href="{{url_for('scodoc.activate_entreprises')
}}">{% if is_entreprises_enabled %}le désactiver{%else%}l'activer{%endif%}</a>
</div>
<form id="configuration_form_scodoc" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate> <form id="configuration_form_scodoc" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
{{ form_scodoc.hidden_tag() }} {{ form_scodoc.hidden_tag() }}
<div class="row"> <div class="row">

View File

@ -6,7 +6,7 @@
<h2>Accès non autorisé</h2> <h2>Accès non autorisé</h2>
{{ exc | safe }} {{ exc }}
<p class="footer"> <p class="footer">
{% if g.scodoc_dept %} {% if g.scodoc_dept %}

View File

@ -152,7 +152,7 @@ document.addEventListener('DOMContentLoaded', function() {
calendar = new Calendar(container, options); calendar = new Calendar(container, options);
fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt?{{groups_query_args|safe}}&show_modules_titles={{show_modules_titles}}`) fetch(`${SCO_URL}../api/formsemestre/{{formsemestre.id}}/edt?{{groups_query_args|safe}}&show_modules_titles={{show_modules_titles}}`)
.then(r=>{return r.json()}) .then(r=>{return r.json()})
.then(events=>{ .then(events=>{
if (typeof events == 'string') { if (typeof events == 'string') {

View File

@ -3,39 +3,46 @@
<!-- formsemestre_header --> <!-- formsemestre_header -->
<div class="formsemestre_page_title noprint"> <div class="formsemestre_page_title noprint">
<div class="infos"> <div class="infos">
<span class="semtitle"><a class="stdlink" title="{{sco.sem.session_id()}}" href="{{ <span class="semtitle"><a class="stdlink" title="{{sco.formsemestre.session_id()}}" href="{{
url_for('notes.formsemestre_status', url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.sem.id) scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id)
}}">{{sco.sem.titre}}</a> }}">{{sco.formsemestre.titre}}</a>
<a title="{{sco.sem.etapes_apo_str()}}"> <a title="{{sco.formsemestre.etapes_apo_str()}}">
{% if sco.sem.semestre_id != -1 %}, {{sco.sem.formation.get_cursus().SESSION_NAME}} {% if sco.formsemestre.semestre_id != -1 %}, {{sco.formsemestre.formation.get_cursus().SESSION_NAME}}
{{sco.sem.semestre_id}} {{sco.formsemestre.semestre_id}}
{% endif %}</a> {% endif %}</a>
{% if sco.sem.modalite %} en {{sco.sem.modalite}}{% endif %}</span> {% if sco.formsemestre.modalite %} en {{sco.formsemestre.modalite}}{% endif %}
</span>
<span class="dates"> <span class="dates">
<a title="du {{sco.sem.date_debut.strftime('%d/%m/%Y')}} <a title="du {{sco.formsemestre.date_debut.strftime('%d/%m/%Y')}}
au {{sco.sem.date_fin.strftime('%d/%m/%Y')}} ">{{scu.MONTH_NAMES_ABBREV[ sco.sem.date_debut.month - 1]}} au {{sco.formsemestre.date_fin.strftime('%d/%m/%Y')}} ">{{scu.MONTH_NAMES_ABBREV[ sco.formsemestre.date_debut.month - 1]}}
{{sco.sem.date_debut.year}} - {{scu.MONTH_NAMES_ABBREV[sco.sem.date_fin.month - 1]}} {{sco.formsemestre.date_debut.year}} - {{scu.MONTH_NAMES_ABBREV[sco.formsemestre.date_fin.month - 1]}}
{{sco.sem.date_fin.year}}</a></span> {{sco.formsemestre.date_fin.year}}</a>
</span>
<span class="resp"><a <span class="resp"><a
title="{{sco.sem.responsables_str(abbrev_prenom=False)}}">{{sco.sem.responsables_str()}}</a></span> title="{{sco.formsemestre.responsables_str(abbrev_prenom=False)}}">{{sco.formsemestre.responsables_str()}}</a></span>
<span class="nbinscrits"><a class="discretelink" href="{{url_for('scolar.groups_view', scodoc_dept=g.scodoc_dept, <span class="nbinscrits"><a class="discretelink" href="{{url_for('scolar.groups_view', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}">{{sco.sem.inscriptions|length}} inscrits</a></span><span class="lock">{% if formsemestre_id=sco.formsemestre.id)}}">{{sco.formsemestre.inscriptions|length}} inscrits</a></span>
not sco.sem.etat %}<a href="{{url_for('notes.formsemestre_flip_lock', scodoc_dept=g.scodoc_dept, <span class="lock">
formsemestre_id=sco.sem.id)}}">{{scu.icontag("lock_img", border="0", title="Semestre {% if not sco.formsemestre.etat %}<a href="{{url_for('notes.formsemestre_flip_lock', scodoc_dept=g.scodoc_dept,
verrouillé")|safe}}</a>{% endif %}</span><span class="eye"> formsemestre_id=sco.formsemestre.id)}}">{{scu.icontag("lock_img", border="0", title="Semestre
verrouillé")|safe}}</a>
{% endif %}
</span>
<span class="eye">
{% if not scu.is_passerelle_disabled() %} {% if not scu.is_passerelle_disabled() %}
<a href="{{url_for('notes.formsemestre_change_publication_bul', scodoc_dept=g.scodoc_dept, <a href="{{url_for('notes.formsemestre_change_publication_bul', scodoc_dept=g.scodoc_dept,
formsemestre_id=sco.sem.id)}}"> formsemestre_id=sco.formsemestre.id)}}">
{% if sco.sem.bul_hide_xml %} {% if sco.formsemestre.bul_hide_xml %}
{{ scu.ICON_HIDDEN|safe}} {{ scu.ICON_HIDDEN|safe}}
{% else %} {% else %}
{{ scu.ICON_PUBLISHED|safe }} {{ scu.ICON_PUBLISHED|safe }}
{% endif %} {% endif %}
</a>
{% endif %} {% endif %}
</span> </span>
</div> </div>
{{ sco.sem_menu_bar|safe }} {{ sco.formsemestre_status_menu_bar()|safe }}
</div> </div>
<!-- end of formsemestre_header --> <!-- end of formsemestre_header -->

View File

@ -17,8 +17,8 @@ et permet de les effacer une par une.
<p class="help"> <p class="help">
<b>Attention</b>, il vous appartient de vérifier la cohérence du résultat ! <b>Attention</b>, il vous appartient de vérifier la cohérence du résultat !
En principe, <b>l'usage de cette page devrait rester exceptionnel</b>. En principe, <b>l'usage de cette page devrait rester exceptionnel</b>.
Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les
pages de saisie de jury habituelles). pages de saisie de jury habituelles).
</p> </p>
{% if sem_vals.first() %} {% if sem_vals.first() %}
<div class="jury_decisions_list jury_decisions_sems"> <div class="jury_decisions_list jury_decisions_sems">
@ -27,7 +27,7 @@ pages de saisie de jury habituelles).
{% for v in sem_vals %} {% for v in sem_vals %}
<li>{{v.html()|safe}} <li>{{v.html()|safe}}
<form> <form>
<button <button
data-v_id="{{v.id}}" data-type="validation_formsemestre" data-etudid="{{etud.id}}" data-v_id="{{v.id}}" data-type="validation_formsemestre" data-etudid="{{etud.id}}"
>effacer</button></form> >effacer</button></form>
</li> </li>
@ -101,8 +101,8 @@ pages de saisie de jury habituelles).
{% endif %} {% endif %}
{% if not( {% if not(
sem_vals.first() or ue_vals.first() or rcue_vals.first() sem_vals.first() or ue_vals.first() or rcue_vals.first()
or annee_but_vals.first() or autorisations.first()) or annee_but_vals.first() or autorisations.first())
%} %}
<div> <div>
<p class="fontred">aucune décision enregistrée</p> <p class="fontred">aucune décision enregistrée</p>
@ -123,7 +123,7 @@ pages de saisie de jury habituelles).
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const buttons = document.querySelectorAll('.jury_decisions_list button'); const buttons = document.querySelectorAll('.jury_decisions_list button');
buttons.forEach(button => { buttons.forEach(button => {
button.addEventListener('click', (event) => { button.addEventListener('click', (event) => {
// Handle button click event here // Handle button click event here
@ -132,10 +132,10 @@ document.addEventListener('DOMContentLoaded', () => {
const v_id = event.target.dataset.v_id; const v_id = event.target.dataset.v_id;
const validation_type = event.target.dataset.type; const validation_type = event.target.dataset.type;
if (confirm("Supprimer cette validation ?")) { if (confirm("Supprimer cette validation ?")) {
fetch(`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`, fetch(`${SCO_URL}../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`,
{ {
method: "POST", method: "POST",
}).then(response => { }).then(response => {
// Handle the response // Handle the response
if (response.ok) { if (response.ok) {
location.reload(); location.reload();

View File

@ -1,5 +1,5 @@
{%- extends 'babase.j2' -%}
{# -*- Base des pages ordinaires, dans départements -*- #} {# -*- Base des pages ordinaires, dans départements -*- #}
{% extends 'babase.j2' %}
{% block styles %} {% block styles %}
{{super()}} {{super()}}
@ -21,24 +21,25 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<!-- sco_page -->
{% block scodoc_sidebar %} {% block scodoc_sidebar %}
{% include "sidebar.j2" %} {% include "sidebar.j2" %}
{% endblock %} {% endblock %}
<div id="gtrcontent" class="gtrcontent"> <div id="gtrcontent" class="gtrcontent">
{% include "flashed_messages.j2" %} {% include "flashed_messages.j2" %}
{% if sco.sem %} {% if sco.formsemestre %}
{% block formsemestre_header %} {% block formsemestre_header %}
{% include "formsemestre_header.j2" %} {% include "formsemestre_header.j2" %}
{% endblock %} {% endblock %}
{% endif %} {% endif %}
<div class="sco-app-content"> <div class="sco-app-content">
{% block app_content %} {% block app_content %}
page vide page vide
{% endblock %} {% endblock %}
</div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@ -50,7 +51,7 @@
<script> <script>
window.onload = function () { enableTooltips("gtrcontent") }; window.onload = function () { enableTooltips("gtrcontent") };
const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}"; const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}";
</script> </script>
{% endblock %} {% endblock %}

View File

@ -5,7 +5,7 @@
<h2>Erreur !</h2> <h2>Erreur !</h2>
{{ exc | safe }} {{ exc }}
<div style="margin-top: 16px;"> <div style="margin-top: 16px;">
{% if g.scodoc_dept %} {% if g.scodoc_dept %}

View File

@ -237,7 +237,7 @@ span.calendarEdit {
<input class=groupe type=checkbox ${partition.show_in_lists ? "checked" : ""} data-attr=show_in_lists> Afficher sur bulletins et tableaux <input class=groupe type=checkbox ${partition.show_in_lists ? "checked" : ""} data-attr=show_in_lists> Afficher sur bulletins et tableaux
</label> </label>
<label> <label>
<a class="stdlink" href="{{scu.ScoURL() <a class="stdlink" href="{{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}}/groups_auto_repartition/${partition.id}">Répartir les étudiants</a> }}/groups_auto_repartition/${partition.id}">Répartir les étudiants</a>
</label> </label>
</div> </div>

View File

@ -58,7 +58,7 @@
{% if sco.etud_cur_sem %} {% if sco.etud_cur_sem %}
<span title="absences du {{ sco.etud_cur_sem['date_debut'].strftime('%d/%m/%Y') }} <span title="absences du {{ sco.etud_cur_sem['date_debut'].strftime('%d/%m/%Y') }}
au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}">({{sco.prefs["assi_metrique"]}}) au {{ sco.etud_cur_sem['date_fin'].strftime('%d/%m/%Y') }}">({{sco.prefs["assi_metrique"]}})
<br />{{'%1g'|format(sco.nbabsjust)}} J., {{'%1g'|format(sco.nbabsnj)}} N.J.</span> <br />{{'%1g'|format(sco.nb_abs_just)}} J., {{'%1g'|format(sco.nb_abs_nj)}} N.J.</span>
{% endif %} {% endif %}
<ul> <ul>
{% if current_user.has_permission(sco.Permission.AbsChange) %} {% if current_user.has_permission(sco.Permission.AbsChange) %}

View File

@ -2,6 +2,7 @@
"""ScoDoc Flask views """ScoDoc Flask views
""" """
import datetime import datetime
from functools import cached_property
from flask import Blueprint from flask import Blueprint
from flask import g, current_app, request from flask import g, current_app, request
@ -57,8 +58,20 @@ class ScoData:
self.Permission = Permission self.Permission = Permission
self.scu = scu self.scu = scu
self.SCOVERSION = sco_version.SCOVERSION self.SCOVERSION = sco_version.SCOVERSION
# -- Informations étudiant courant, si sélectionné: self._init_etud = etud
if etud is None: self._init_formsemestre = formsemestre
# les comptes d'absences sont initialisés lors de l'accès à etud_cur_sem
self.nb_abs_nj = 0
self.nb_abs_just = 0
self.nb_abs = 0
# .etud, .formsemestre, etc. sont des cached_property
# car ScoData() est créé par @context_processor
# AVANT le décorateur scodoc qui initialise g.scodoc_xxx
@cached_property
def etud(self) -> Identite | None:
"Informations étudiant courant, si sélectionné"
if self._init_etud is None:
etudid = g.get("etudid", None) etudid = g.get("etudid", None)
if etudid is None: if etudid is None:
if request.method == "GET": if request.method == "GET":
@ -66,50 +79,65 @@ class ScoData:
elif request.method == "POST": elif request.method == "POST":
etudid = request.form.get("etudid", None) etudid = request.form.get("etudid", None)
if etudid is not None: if etudid is not None:
etud = Identite.get_etud(etudid) return Identite.get_etud(etudid)
self.etud = etud return self._init_etud
if etud is not None:
# Infos sur l'étudiant courant @cached_property
ins = self.etud.inscription_courante() def etud_cur_sem(self) -> FormSemestre | None:
if ins: "le semestre courant de l'étudiant courant"
self.etud_cur_sem = ins.formsemestre etud = self.etud
( if etud is None:
self.nbabsnj, return None
self.nbabsjust, ins = self.etud.inscription_courante()
self.nbabs, cur_sem = ins.formsemestre
) = sco_assiduites.get_assiduites_count_in_interval( if ins:
etud.id, (
self.etud_cur_sem.date_debut.isoformat(), self.nb_abs_nj,
self.etud_cur_sem.date_fin.isoformat(), self.nb_abs_just,
scu.translate_assiduites_metric( self.nb_abs,
sco_preferences.get_preference("assi_metrique") ) = sco_assiduites.get_assiduites_count_in_interval(
), etud.id,
) cur_sem.date_debut.isoformat(),
else: cur_sem.date_fin.isoformat(),
self.etud_cur_sem = None scu.translate_assiduites_metric(
else: sco_preferences.get_preference("assi_metrique")
self.etud = None ),
# --- Informations sur semestre courant, si sélectionné
if formsemestre is None:
formsemestre_id = retreive_formsemestre_from_request()
if formsemestre_id is not None:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if formsemestre is None:
self.sem = None
self.sem_menu_bar = None
else:
self.sem = formsemestre
self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar(
self.sem
) )
self.formsemestre = formsemestre return cur_sem
# --- Préférences return None
# prefs fallback to global pref if sem is None:
if formsemestre: @cached_property
formsemestre_id = formsemestre.id def formsemestre(self) -> FormSemestre | None:
else: "Le formsemestre courant, si sélectionné"
formsemestre_id = None if self._init_formsemestre is None:
self.prefs = sco_preferences.SemPreferences(formsemestre_id) formsemestre_id = retreive_formsemestre_from_request()
return (
FormSemestre.get_formsemestre(formsemestre_id)
if formsemestre_id is not None
else None
)
return self._init_formsemestre
@cached_property
def sem_menu_bar(self) -> str | None:
"Le html de la bare de menu formsemestre s'il y en a un."
return (
sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre)
if self.formsemestre
else None
)
@cached_property
def prefs(self):
"Préférences"
# prefs fallback to global pref if no current formsemestre:
return sco_preferences.SemPreferences(
self.formsemestre.id if self.formsemestre else None
)
def formsemestre_status_menu_bar(self) -> str:
"Le HTML de la barre de menu semestre"
return sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre)
from app.views import ( from app.views import (

View File

@ -181,7 +181,7 @@ def add_billets_absence_form(etudid):
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.ScoURL()) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
else: else:
e = tf[2]["begin"].split("/") e = tf[2]["begin"].split("/")
begin = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00" begin = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00"
@ -407,7 +407,7 @@ def process_billet_absence_form(billet_id: int):
return "\n".join(H) + "<br>" + tf[1] + F + html_sco_header.sco_footer() return "\n".join(H) + "<br>" + tf[1] + F + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.ScoURL()) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
else: else:
n = _ProcessBilletAbsence(billet, tf[2]["estjust"], tf[2]["description"]) n = _ProcessBilletAbsence(billet, tf[2]["estjust"], tf[2]["description"])
if tf[2]["estjust"]: if tf[2]["estjust"]:

View File

@ -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,
@ -1440,7 +1452,6 @@ def visu_assi_group():
formsemestre_modimpls=formsemestre_modimpls, formsemestre_modimpls=formsemestre_modimpls,
convert_values=(fmt == "html"), convert_values=(fmt == "html"),
) )
# Export en XLS # Export en XLS
if fmt.startswith("xls"): if fmt.startswith("xls"):
return scu.send_file( return scu.send_file(
@ -1915,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)
@ -1958,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",
@ -1978,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,
) )
@ -2474,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 = ""
@ -2556,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(
@ -2566,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:
@ -2604,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(
@ -2644,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(
@ -2654,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))
@ -2763,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:

View File

@ -685,21 +685,29 @@ def module_clone():
# #
@bp.route("/") @bp.route("/")
@bp.route("/index_html") @bp.route("/index_html", alias=True)
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def index_html(): def index_html():
"Page accueil formations" "Page accueil formations"
fmt = request.args.get("fmt", "html") fmt = request.args.get("fmt", "html")
detail = scu.to_bool(request.args.get("detail", False))
editable = current_user.has_permission(Permission.EditFormation) editable = current_user.has_permission(Permission.EditFormation)
table = sco_formations.formation_list_table() table = sco_formations.formation_list_table(detail=detail)
if fmt != "html": if fmt != "html":
return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}") return table.make_page(fmt=fmt, filename=f"Formations-{g.scodoc_dept}")
H = [ H = [
html_sco_header.sco_header(page_title="Formations (programmes)"), html_sco_header.sco_header(page_title="Formations (programmes)"),
"""<h2>Formations (programmes pédagogiques)</h2> f"""<h2>Formations (programmes pédagogiques)</h2>
<form>
<input type="checkbox" id="detailCheckbox" name="detail"
onchange="this.form.submit();"
{'checked' if detail else ''}>
<label for="detailCheckbox">Informations détaillées</label>
</form>
""", """,
table.html(), table.html(),
] ]
@ -799,7 +807,7 @@ def formation_import_xml_form():
{ html_sco_header.sco_footer() } { html_sco_header.sco_footer() }
""" """
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.NotesURL()) return flask.redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else: else:
formation_id, _, _ = sco_formations.formation_import_xml( formation_id, _, _ = sco_formations.formation_import_xml(
tf[2]["xmlfile"].read() tf[2]["xmlfile"].read()

View File

@ -3,6 +3,7 @@ PN / Référentiel de compétences
Emmanuel Viennet, 2021 Emmanuel Viennet, 2021
""" """
from pathlib import Path from pathlib import Path
import re import re
@ -19,11 +20,20 @@ from app import db, log
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import Formation from app.models import Formation
from app.models.but_refcomp import ApcReferentielCompetences from app.models.but_refcomp import ApcReferentielCompetences
from app.but import change_refcomp
from app.but.import_refcomp import orebut_import_refcomp from app.but.import_refcomp import orebut_import_refcomp
from app.but.forms.refcomp_forms import FormationRefCompForm, RefCompLoadForm from app.but.forms.refcomp_forms import (
FormationChangeRefCompForm,
FormationRefCompForm,
RefCompLoadForm,
)
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError from app.scodoc.sco_exceptions import (
ScoFormatError,
ScoNoReferentielCompetences,
ScoValueError,
)
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import notes_bp as bp from app.views import notes_bp as bp
from app.views import ScoData from app.views import ScoData
@ -47,9 +57,12 @@ def refcomp(refcomp_id):
def refcomp_show(refcomp_id): def refcomp_show(refcomp_id):
"""Affichage du référentiel de compétences.""" """Affichage du référentiel de compétences."""
referentiel_competence = ApcReferentielCompetences.query.get_or_404(refcomp_id) referentiel_competence = ApcReferentielCompetences.query.get_or_404(refcomp_id)
# Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = referentiel_competence.equivalents()
return render_template( return render_template(
"but/refcomp_show.j2", "but/refcomp_show.j2",
ref=referentiel_competence, ref=referentiel_competence,
referentiels_equivalents=referentiels_equivalents,
title="Référentiel de compétences", title="Référentiel de compétences",
data_source=url_for( data_source=url_for(
"notes.refcomp", "notes.refcomp",
@ -279,3 +292,55 @@ def refcomp_load(formation_id=None):
formation=formation, formation=formation,
title="Chargement réf. compétences", title="Chargement réf. compétences",
) )
@bp.route("/formation/<int:formation_id>/change_refcomp", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.EditFormation)
def formation_change_refcomp(formation_id: int):
"""Tente de changer le ref. de comp. de la formation"""
formation = Formation.get_formation(formation_id)
ref_comp: ApcReferentielCompetences = formation.referentiel_competence
if ref_comp is None:
raise ScoNoReferentielCompetences(formation=formation)
# Autres référentiels "équivalents" pour proposer de changer les formations:
referentiels_equivalents = ref_comp.equivalents()
form = FormationChangeRefCompForm()
form.object_select.choices = [
(ref.id, ref.get_title()) for ref in referentiels_equivalents
]
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(
url_for(
"notes.refcomp_show",
scodoc_dept=g.scodoc_dept,
refcomp_id=formation.referentiel_competence.id,
)
)
if form.validate_on_submit():
try:
new_ref_id = int(form.object_select.data)
except TypeError as exc:
raise ScoValueError("nouveau refcomp id invalide") from exc
new_ref = None
for ref in referentiels_equivalents:
if ref.id == new_ref_id:
new_ref = ref
break
if new_ref is None:
raise ScoValueError("nouveau refcomp invalide")
change_refcomp.formation_change_referentiel(formation, new_ref)
flash("Formation changée de référentiel")
return redirect(
url_for(
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
)
)
return render_template(
"but/change_refcomp.j2",
form=form,
formation=formation,
referentiels_equivalents=referentiels_equivalents,
title="Changer de référentiel de compétences",
)

View File

@ -53,6 +53,7 @@ from werkzeug.exceptions import BadRequest, NotFound
from app import db, log from app import db, log
from app import entreprises
from app.auth.models import User, Role from app.auth.models import User, Role
from app.auth.cas import set_cas_configuration from app.auth.cas import set_cas_configuration
from app.decorators import ( from app.decorators import (
@ -62,6 +63,7 @@ from app.decorators import (
) )
from app.forms.generic import SimpleConfirmationForm from app.forms.generic import SimpleConfirmationForm
from app.forms.main import config_logos, config_main from app.forms.main import config_logos, config_main
from app.forms.main.activate_entreprises import ActivateEntreprisesForm
from app.forms.main.config_assiduites import ConfigAssiduitesForm from app.forms.main.config_assiduites import ConfigAssiduitesForm
from app.forms.main.config_apo import CodesDecisionsForm from app.forms.main.config_apo import CodesDecisionsForm
from app.forms.main.config_cas import ConfigCASForm from app.forms.main.config_cas import ConfigCASForm
@ -484,6 +486,38 @@ def config_personalized_links():
) )
@bp.route("/ScoDoc/activate_entreprises", methods=["GET", "POST"])
@admin_required
def activate_entreprises():
"""Form activation module entreprises"""
is_enabled = ScoDocSiteConfig.is_entreprises_enabled()
form = ActivateEntreprisesForm(
data={
"set_default_roles_permission": True,
}
)
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.configuration"))
if form.validate_on_submit():
if entreprises.activate_module(
enable=not is_enabled,
set_default_roles_permission=form.data["set_default_roles_permission"],
):
flash("Module entreprise " + ("activé" if not is_enabled else "désactivé"))
return redirect(url_for("scodoc.configuration"))
if is_enabled:
form.submit.label.text = "Désactiver le module relations entreprises"
else:
form.submit.label.text = "Activer le module relations entreprises"
return render_template(
"activate_entreprises.j2",
form=form,
is_enabled=is_enabled,
title="Activation module Relations Entreprises",
)
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"]) @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required @login_required
def table_etud_in_accessible_depts(): def table_etud_in_accessible_depts():

View File

@ -340,8 +340,8 @@ def showEtudLog(etudid, fmt="html"):
# ---------- PAGE ACCUEIL (listes) -------------- # ---------- PAGE ACCUEIL (listes) --------------
@bp.route("/", alias=True) @bp.route("/")
@bp.route("/index_html") @bp.route("/index_html", alias=True)
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
@ -823,7 +823,8 @@ def form_change_coordonnees(etudid):
("telephonemobile", {"size": 13, "title": "Mobile"}), ("telephonemobile", {"size": 13, "title": "Mobile"}),
), ),
initvalues=adr, initvalues=adr,
submitlabel="Valider le formulaire", submitlabel="Enregistrer",
cancelbutton="Annuler",
) )
dest_url = url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid) dest_url = url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
if tf[0] == 0: if tf[0] == 0:
@ -1366,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:
@ -1787,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"]
@ -1953,7 +1960,7 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False):
for formsemestre_id in formsemestre_ids_to_inval: for formsemestre_id in formsemestre_ids_to_inval:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
flash("Étudiant supprimé !") flash("Étudiant supprimé !")
return flask.redirect(scu.ScoURL()) return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
@bp.route("/check_group_apogee") @bp.route("/check_group_apogee")
@ -2147,7 +2154,7 @@ def form_students_import_excel(formsemestre_id=None):
) )
else: else:
sem = None sem = None
dest_url = scu.ScoURL() dest_url = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
if sem and not sem["etat"]: if sem and not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille") raise ScoValueError("Modification impossible: semestre verrouille")
H = [ H = [
@ -2182,13 +2189,15 @@ def form_students_import_excel(formsemestre_id=None):
) )
else: else:
H.append( H.append(
""" f"""
<p>Pour inscrire directement les étudiants dans un semestre de <p>Pour inscrire directement les étudiants dans un semestre de
formation, il suffit d'indiquer le code de ce semestre formation, il suffit d'indiquer le code de ce semestre
(qui doit avoir été créé au préalable). <a class="stdlink" href="%s?showcodes=1">Cliquez ici pour afficher les codes</a> (qui doit avoir été créé au préalable).
<a class="stdlink" href="{
url_for("scolar.index_html", showcodes=1, scodoc_dept=g.scodoc_dept)
}">Cliquez ici pour afficher les codes</a>
</p> </p>
""" """
% (scu.ScoURL())
) )
H.append("""<ol><li>""") H.append("""<ol><li>""")
@ -2413,9 +2422,11 @@ def form_students_import_infos_admissions(formsemestre_id=None):
return "\n".join(H) + tf[1] + help_text + F return "\n".join(H) + tf[1] + help_text + F
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
scu.ScoURL() url_for(
+ "/formsemestre_status?formsemestre_id=" "notes.formsemestre_status",
+ str(formsemestre_id) scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )
else: else:
return sco_import_etuds.students_import_admission( return sco_import_etuds.students_import_admission(

View File

@ -132,7 +132,7 @@ class Mode(IntEnum):
@bp.route("/") @bp.route("/")
@bp.route("/index_html") @bp.route("/index_html", alias=True)
@scodoc @scodoc
@permission_required(Permission.UsersView) @permission_required(Permission.UsersView)
@scodoc7func @scodoc7func
@ -504,7 +504,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
if g.scodoc_dept in selectable_dept_acronyms if g.scodoc_dept in selectable_dept_acronyms
else (auth_dept or "") else (auth_dept or "")
) )
if len(selectable_dept_acronyms) > 1: if len(selectable_dept_acronyms) > 0:
selectable_dept_acronyms = sorted(list(selectable_dept_acronyms)) selectable_dept_acronyms = sorted(list(selectable_dept_acronyms))
descr.append( descr.append(
( (
@ -529,7 +529,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
{ {
"input_type": "separator", "input_type": "separator",
"title": f"""L'utilisateur appartient au département { "title": f"""L'utilisateur appartient au département {
the_user.dept or "(tous)"}""", the_user.dept or "(tous/aucun)"}""",
}, },
) )
) )
@ -539,7 +539,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
"d", "d",
{ {
"input_type": "separator", "input_type": "separator",
"title": f"L'utilisateur sera créé dans le département {auth_dept}", "title": f"L'utilisateur sera créé dans le département {auth_dept or 'aucun'}",
}, },
) )
) )
@ -605,7 +605,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + F return "\n".join(H) + "\n" + tf[1] + F
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(scu.UsersURL()) return flask.redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept))
else: else:
vals = tf[2] vals = tf[2]
roles = set(vals["roles"]).intersection(editable_roles_strings) roles = set(vals["roles"]).intersection(editable_roles_strings)
@ -1080,28 +1080,28 @@ def change_password(user_name, password, password2):
# #
# ici page simplifiee car on peut ne plus avoir # ici page simplifiee car on peut ne plus avoir
# le droit d'acceder aux feuilles de style # le droit d'acceder aux feuilles de style
H.append( return f"""<?xml version="1.0" encoding="{scu.SCO_ENCODING}"?>
"""<h2>Changement effectué !</h2> <!DOCTYPE html>
<p>Ne notez pas ce mot de passe, mais mémorisez le !</p>
<p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à
un tiers, même si c'est un collègue de confiance !</p>
<p><b>Si vous n'êtes pas administrateur, le système va vous redemander
votre login et nouveau mot de passe au prochain accès.</b>
</p>"""
)
return (
f"""<?xml version="1.0" encoding="{scu.SCO_ENCODING}"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html> <html>
<head> <head>
<title>Mot de passe changé</title> <title>Mot de passe changé</title>
<meta http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}" /> <meta http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}" />
<body><h1>Mot de passe changé !</h1> <body>
<h1>Mot de passe changé !</h1>
<h2>Changement effectué</h2>
<p>Ne notez pas ce mot de passe, mais mémorisez le !</p>
<p>Rappel: il est <b>interdit</b> de communiquer son mot de passe à
un tiers, même si c'est un collègue de confiance !</p>
<p><b>Si vous n'êtes pas administrateur, le système va vous redemander
votre login et nouveau mot de passe au prochain accès.</b>
</p>
<a href="{
url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)
}" class="stdlink">Continuer</a>
</body>
</html>
""" """
+ "\n".join(H)
+ f'<a href="{scu.ScoURL()}" class="stdlink">Continuer</a></body></html>'
)
return html_sco_header.sco_header() + "\n".join(H) + F return html_sco_header.sco_header() + "\n".join(H) + F

View File

@ -0,0 +1,18 @@
# Certains référentiels de compétences peuvent être considérés
# comme équivalents
# Si ils ont la même structure
# Voir ApcReferentielCompetences.map_to_other_referentiel
# Mappings: nouveau : ancien
QLIO: # la clé est 'specialite'
parcours: # codes de parcours
OSC: MSC
QMI: MQSE
# et un transitoire UPHF:
MPBS: MP
PCLG: MSC
QPSMI: MQSE
ATN: MTD
# competences: # titres de compétences ('nom_court' dans le XML)
SD: STID

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.957" SCOVERSION = "9.6.966"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -45,7 +45,7 @@ from app.models.evaluations import Evaluation
from app.scodoc import sco_dump_db from app.scodoc import sco_dump_db
from app.scodoc.sco_logos import make_logo_local from app.scodoc.sco_logos import make_logo_local
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import notes, scolar from app.views import notes, scolar, ScoData
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import tools import tools
from tools.fakedatabase import create_test_api_database from tools.fakedatabase import create_test_api_database
@ -58,9 +58,9 @@ cli.register(app)
@app.context_processor @app.context_processor
def inject_sco_utils(): def inject_sco_utils():
"Make scu available in all Jinja templates" "Make scu and sco available in all Jinja templates"
# if modified, put the same in conftest.py#27 # if modified, put the same in conftest.py#27
return dict(scu=scu) return {"scu": scu, "sco": ScoData()}
@app.shell_context_processor @app.shell_context_processor

View File

@ -255,8 +255,20 @@ def test_etudiants_by_name(api_headers):
etuds = r.json() etuds = r.json()
assert etuds == [] assert etuds == []
# #
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
args = {
"prenom": "Prénom",
"nom": "Réçier",
"dept": DEPT_ACRONYM,
"civilite": "X",
}
_ = POST_JSON(
"/etudiant/create",
args,
headers=admin_header,
)
r = requests.get( r = requests.get(
API_URL + "/etudiants/name/REG", API_URL + "/etudiants/name/REC",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT, timeout=scu.SCO_TEST_API_TIMEOUT,
@ -264,7 +276,7 @@ def test_etudiants_by_name(api_headers):
assert r.status_code == 200 assert r.status_code == 200
etuds = r.json() etuds = r.json()
assert len(etuds) == 1 assert len(etuds) == 1
assert etuds[0]["nom"] == "GNIER" assert etuds[0]["nom"] == "ÇIER"
def test_etudiant_annotations(api_headers): def test_etudiant_annotations(api_headers):

View File

@ -708,6 +708,7 @@ def test_formsemestre_resultat(api_headers):
""" """
# Test brutal: compare les texts des json (après suppression des espaces et tabs) # Test brutal: compare les texts des json (après suppression des espaces et tabs)
# ce test cassera à la moindre modification :-) # ce test cassera à la moindre modification :-)
# Pour regénérer le fichier de référence, récupérer venv/res.json
formsemestre_id = 1 formsemestre_id = 1
r = requests.get( r = requests.get(
f"{API_URL}/formsemestre/{formsemestre_id}/resultats", f"{API_URL}/formsemestre/{formsemestre_id}/resultats",

View File

@ -12,6 +12,7 @@ from app.auth.models import User, Role
from app.auth.models import get_super_admin from app.auth.models import get_super_admin
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.views import ScoData
RESOURCES_DIR = "/opt/scodoc/tests/ressources" RESOURCES_DIR = "/opt/scodoc/tests/ressources"
@ -27,7 +28,7 @@ def test_client():
@apptest.context_processor @apptest.context_processor
def inject_sco_utils(): def inject_sco_utils():
"Make scu available in all Jinja templates" "Make scu available in all Jinja templates"
return dict(scu=scu) return {"scu": scu, "sco": ScoData()}
with apptest.test_request_context(): with apptest.test_request_context():
# initialize scodoc "g": # initialize scodoc "g":

Some files were not shown because too many files have changed in this diff Show More