Compare commits
42 Commits
Author | SHA1 | Date |
---|---|---|
Emmanuel Viennet | 043985bff6 | |
Emmanuel Viennet | d20ada1797 | |
Iziram | 778fecabb6 | |
Iziram | fa6f83722e | |
Emmanuel Viennet | baa0412071 | |
Emmanuel Viennet | d51a47b71a | |
Lyanis Souidi | f21ef41de6 | |
Lyanis Souidi | 2d673e7a5d | |
Emmanuel Viennet | 3e43495831 | |
Iziram | a4db8c4ff8 | |
Iziram | 1ac35d04c2 | |
Iziram | 687ac3cf13 | |
Emmanuel Viennet | 18b1f00586 | |
Iziram | 6b985620e9 | |
Iziram | 4d234ba353 | |
Iziram | 5d45fcf656 | |
Iziram | 0a5919b788 | |
Iziram | 09f4525e66 | |
Emmanuel Viennet | 0bc57807de | |
Emmanuel Viennet | 87aaf12d27 | |
Emmanuel Viennet | c8ab9b9b6c | |
Emmanuel Viennet | ad7b48e110 | |
Emmanuel Viennet | f2ce16f161 | |
Emmanuel Viennet | 1ddf9b6ab8 | |
Emmanuel Viennet | 0a2e39cae1 | |
Emmanuel Viennet | a194b4b6e0 | |
Emmanuel Viennet | cbe85dfb7d | |
Emmanuel Viennet | beba69bfe4 | |
Emmanuel Viennet | 41fec29452 | |
Emmanuel Viennet | 9bd05ea241 | |
Emmanuel Viennet | 58b831513d | |
Emmanuel Viennet | b861aba6a3 | |
Emmanuel Viennet | c2443c361f | |
Emmanuel Viennet | ab4731bd43 | |
Emmanuel Viennet | c17bc8b61b | |
Emmanuel Viennet | e44a5ee55d | |
Emmanuel Viennet | a747ed22e2 | |
Emmanuel Viennet | 5d0a932634 | |
Emmanuel Viennet | 2b150cf521 | |
Emmanuel Viennet | 5a5ddcacd7 | |
Emmanuel Viennet | 3f6e65b9da | |
Emmanuel Viennet | 5eba6170a5 |
152
README.md
152
README.md
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
|
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
|
||||||
|
|
||||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
|
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian12>
|
||||||
|
|
||||||
Documentation utilisateur: <https://scodoc.org>
|
Documentation utilisateur: <https://scodoc.org>
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
||||||
|
|
||||||
### Lignes de commandes
|
### Lignes de commandes
|
||||||
|
|
||||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
Voir [le guide de configuration](https://scodoc.org/GuideConfig).
|
||||||
|
|
||||||
## Organisation des fichiers
|
## Organisation des fichiers
|
||||||
|
|
||||||
|
@ -41,45 +41,41 @@ Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configu
|
||||||
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||||
|
|
||||||
Principaux contenus:
|
Principaux contenus:
|
||||||
|
```
|
||||||
/opt/scodoc-data
|
/opt/scodoc-data
|
||||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||||
/opt/scodoc-data/config # Fichiers de configuration
|
/opt/scodoc-data/config # Fichiers de configuration
|
||||||
.../config/logos # Logos de l'établissement
|
.../config/logos # Logos de l'établissement
|
||||||
.../config/depts # un fichier par département
|
.../config/depts # un fichier par département
|
||||||
/opt/scodoc-data/photos # Photos des étudiants
|
/opt/scodoc-data/photos # Photos des étudiants
|
||||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||||
|
```
|
||||||
## Pour les développeurs
|
## Pour les développeurs
|
||||||
|
|
||||||
### Installation du code
|
### Installation du code
|
||||||
|
|
||||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian12)).
|
||||||
|
|
||||||
Puis remplacer `/opt/scodoc` par un clone du git.
|
Puis remplacer `/opt/scodoc` par un clone du git.
|
||||||
|
```bash
|
||||||
|
sudo su
|
||||||
|
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||||
|
apt-get install git # si besoin
|
||||||
|
git clone https://scodoc.org/git/ScoDoc/ScoDoc.git /opt/scodoc
|
||||||
|
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||||
|
|
||||||
sudo su
|
# Donner ce répertoire à l'utilisateur scodoc:
|
||||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
chown -R scodoc:scodoc /opt/scodoc
|
||||||
apt-get install git # si besoin
|
```
|
||||||
cd /opt
|
|
||||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
|
||||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
|
||||||
|
|
||||||
# Renommer le répertoire:
|
|
||||||
mv ScoDoc scodoc
|
|
||||||
|
|
||||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
|
||||||
chown -R scodoc.scodoc /opt/scodoc
|
|
||||||
|
|
||||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||||
|
```bash
|
||||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||||
mv /opt/off-scodoc/venv /opt/scodoc
|
mv /opt/off-scodoc/venv /opt/scodoc
|
||||||
|
```
|
||||||
Et la config:
|
Et la config:
|
||||||
|
```bash
|
||||||
ln -s /opt/scodoc-data/.env /opt/scodoc
|
ln -s /opt/scodoc-data/.env /opt/scodoc
|
||||||
|
```
|
||||||
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
|
Cette dernière commande utilise le `.env` crée lors de l'install, ce qui
|
||||||
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
||||||
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||||
|
@ -88,11 +84,11 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||||
|
|
||||||
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||||
Avant le premier lancement, créer cette base ainsi:
|
Avant le premier lancement, créer cette base ainsi:
|
||||||
|
```bash
|
||||||
./tools/create_database.sh SCODOC_TEST
|
./tools/create_database.sh SCODOC_TEST
|
||||||
export FLASK_ENV=test
|
export FLASK_ENV=test
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
```
|
||||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||||
migrations (changements de schéma) ont eu lieu dans le code.
|
migrations (changements de schéma) ont eu lieu dans le code.
|
||||||
|
@ -100,17 +96,17 @@ migrations (changements de schéma) ont eu lieu dans le code.
|
||||||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||||
scripts de tests:
|
scripts de tests:
|
||||||
Lancer au préalable:
|
Lancer au préalable:
|
||||||
|
```bash
|
||||||
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
flask delete-dept -fy TEST00 && flask create-dept TEST00
|
||||||
|
```
|
||||||
Puis dérouler les tests unitaires:
|
Puis dérouler les tests unitaires:
|
||||||
|
```bash
|
||||||
pytest tests/unit
|
pytest tests/unit
|
||||||
|
```
|
||||||
Ou avec couverture (`pip install pytest-cov`)
|
Ou avec couverture (`pip install pytest-cov`)
|
||||||
|
```bash
|
||||||
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
pytest --cov=app --cov-report=term-missing --cov-branch tests/unit/*
|
||||||
|
```
|
||||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
#### Utilisation des tests unitaires pour initialiser la base de dev
|
||||||
|
|
||||||
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||||
|
@ -119,43 +115,43 @@ développement dans un état connu, par exemple pour éviter de recréer à la m
|
||||||
|
|
||||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||||
par les tests:
|
par les tests:
|
||||||
|
```bash
|
||||||
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
|
||||||
|
```
|
||||||
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
(si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
|
||||||
normalement, par exemple:
|
normalement, par exemple:
|
||||||
|
```bash
|
||||||
pytest tests/unit/test_sco_basic.py
|
pytest tests/unit/test_sco_basic.py
|
||||||
|
```
|
||||||
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
Il est en général nécessaire d'affecter ensuite un mot de passe à (au moins) un
|
||||||
utilisateur:
|
utilisateur:
|
||||||
|
```bash
|
||||||
flask user-password admin
|
flask user-password admin
|
||||||
|
```
|
||||||
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
||||||
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
||||||
|
|
||||||
#### Modification du schéma de la base
|
#### Modification du schéma de la base
|
||||||
|
|
||||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||||
|
```bash
|
||||||
flask db migrate -m "message explicatif....."
|
flask db migrate -m "message explicatif....."
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
```
|
||||||
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
||||||
|
|
||||||
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
||||||
ou variables d'environnement pour interroger la bonne base !).
|
ou variables d'environnement pour interroger la bonne base !).
|
||||||
|
```bash
|
||||||
|
dropdb SCODOC_DEV
|
||||||
|
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||||
|
flask db upgrade # créé les tables à partir des migrations
|
||||||
|
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||||
|
|
||||||
dropdb SCODOC_DEV
|
# puis imports:
|
||||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
flask import-scodoc7-users
|
||||||
flask db upgrade # créé les tables à partir des migrations
|
flask import-scodoc7-dept STID SCOSTID
|
||||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
```
|
||||||
|
|
||||||
# puis imports:
|
|
||||||
flask import-scodoc7-users
|
|
||||||
flask import-scodoc7-dept STID SCOSTID
|
|
||||||
|
|
||||||
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
||||||
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||||
positionner à la bonne étape.
|
positionner à la bonne étape.
|
||||||
|
@ -163,23 +159,23 @@ positionner à la bonne étape.
|
||||||
### Profiling
|
### Profiling
|
||||||
|
|
||||||
Sur une machine de DEV, lancer
|
Sur une machine de DEV, lancer
|
||||||
|
```bash
|
||||||
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
|
||||||
|
```
|
||||||
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
le fichier `.prof` sera alors écrit dans `/opt/scodoc-data` (on peut aussi utiliser `/tmp`).
|
||||||
|
|
||||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||||
|
```bash
|
||||||
pip install snakeviz
|
pip install snakeviz
|
||||||
|
```
|
||||||
puis
|
puis
|
||||||
|
```bash
|
||||||
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
|
||||||
|
```
|
||||||
## Paquet Debian 12
|
## Paquet Debian 12
|
||||||
|
|
||||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||||
important est `postinst`qui se charge de configurer le système (install ou
|
important est `postinst` qui se charge de configurer le système (install ou
|
||||||
upgrade de scodoc9).
|
upgrade de scodoc9).
|
||||||
|
|
||||||
La préparation d'une release se fait à l'aide du script
|
La préparation d'une release se fait à l'aide du script
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]))
|
||||||
|
|
|
@ -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>{
|
||||||
|
|
|
@ -17,53 +17,11 @@ from app.models import (
|
||||||
ApcValidationRCUE,
|
ApcValidationRCUE,
|
||||||
Formation,
|
Formation,
|
||||||
FormSemestreInscription,
|
FormSemestreInscription,
|
||||||
Module,
|
|
||||||
UniteEns,
|
UniteEns,
|
||||||
)
|
)
|
||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
def map_referentiels(
|
|
||||||
ref1: ApcReferentielCompetences, ref2: ApcReferentielCompetences
|
|
||||||
) -> str | tuple[dict[int, int], dict[int, int], dict[int, int]]:
|
|
||||||
"""Build mapping between two referentiels"""
|
|
||||||
if ref1.type_structure != ref2.type_structure:
|
|
||||||
return "type_structure mismatch"
|
|
||||||
if ref1.type_departement != ref2.type_departement:
|
|
||||||
return "type_departement mismatch"
|
|
||||||
# mêmes parcours ?
|
|
||||||
parcours_by_code_1 = {p.code: p for p in ref1.parcours}
|
|
||||||
parcours_by_code_2 = {p.code: p for p in ref2.parcours}
|
|
||||||
if parcours_by_code_1.keys() != parcours_by_code_2.keys():
|
|
||||||
return "parcours mismatch"
|
|
||||||
parcours_map = {
|
|
||||||
parcours_by_code_1[code].id: parcours_by_code_2[code].id
|
|
||||||
for code in parcours_by_code_1
|
|
||||||
}
|
|
||||||
# mêmes compétences ?
|
|
||||||
competence_by_code_1 = {c.titre: c for c in ref1.competences}
|
|
||||||
competence_by_code_2 = {c.titre: c for c in ref2.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 formation_change_referentiel(
|
def formation_change_referentiel(
|
||||||
formation: Formation, new_ref: ApcReferentielCompetences
|
formation: Formation, new_ref: ApcReferentielCompetences
|
||||||
):
|
):
|
||||||
|
@ -73,7 +31,7 @@ def formation_change_referentiel(
|
||||||
if not isinstance(new_ref, ApcReferentielCompetences):
|
if not isinstance(new_ref, ApcReferentielCompetences):
|
||||||
raise ScoValueError("nouveau référentiel invalide")
|
raise ScoValueError("nouveau référentiel invalide")
|
||||||
|
|
||||||
r = map_referentiels(formation.referentiel_competence, new_ref)
|
r = formation.referentiel_competence.map_to_other_referentiel(new_ref)
|
||||||
if isinstance(r, str):
|
if isinstance(r, str):
|
||||||
raise ScoValueError(f"référentiels incompatibles: {r}")
|
raise ScoValueError(f"référentiels incompatibles: {r}")
|
||||||
parcours_map, competences_map, niveaux_map = r
|
parcours_map, competences_map, niveaux_map = r
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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})
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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é.
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>"""
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 où 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 = (
|
|
||||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
legend = " " # empty cell
|
ev_txts.append(ev.get("title", " "))
|
||||||
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 = (
|
|
||||||
" " * (n // 2) + legend + " " * ((n + 1) // 2)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
legend = " " # empty cell
|
|
||||||
cc.append(legend)
|
|
||||||
if href or descr:
|
|
||||||
cc.append("</a>")
|
|
||||||
cc.append("</td>\n")
|
|
||||||
T.append("".join(cc) + "</tr>")
|
|
||||||
return "\n".join(T)
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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>""",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -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> """
|
</a> """
|
||||||
msg_refcomp = "changer"
|
msg_refcomp = "changer"
|
||||||
H.append(f"""<ul><li>{descr_refcomp}""")
|
H.append(f"""<ul><li>{descr_refcomp}""")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
""",
|
""",
|
||||||
|
|
|
@ -575,7 +575,7 @@ def formation_list_table(detail: bool) -> GenTable:
|
||||||
# utilise pour voir si la formation couvre tous les semestres
|
# utilise pour voir si la formation couvre tous les semestres
|
||||||
row["semestres_ues"] = ", ".join(
|
row["semestres_ues"] = ", ".join(
|
||||||
"S" + str(x if (x is not None and x > 0) else "-")
|
"S" + str(x if (x is not None and x > 0) else "-")
|
||||||
for x in sorted({ue.semestre_idx for ue in formation.ues})
|
for x in sorted({(ue.semestre_idx or 0) for ue in formation.ues})
|
||||||
)
|
)
|
||||||
# Date surtout utilisées pour le tri:
|
# Date surtout utilisées pour le tri:
|
||||||
if row["formsemestres"]:
|
if row["formsemestres"]:
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"],
|
||||||
|
|
|
@ -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)
|
|
@ -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"] = ""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
|
@ -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"] = ""
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
.day .dayline {
|
.jour .dayline {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
height: 75px;
|
|
||||||
background-color: #dedede;
|
background-color: #dedede;
|
||||||
border-radius: 15px;
|
border-radius: 8px;
|
||||||
padding: 5px;
|
padding: 5px 5px 15px 5px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border: 2px solid #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day:hover .dayline {
|
.jour:hover .dayline {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dayline .mini-timeline {
|
.dayline .mini-timeline {
|
||||||
margin-top: 10%;
|
margin-top: 10%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -296,7 +296,13 @@ function creerLigneEtudiant(etud, index) {
|
||||||
// Création des boutons d'assiduités
|
// Création des boutons d'assiduités
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
} else if (currentAssiduite.type != "conflit") {
|
} else if (currentAssiduite.type != "conflit") {
|
||||||
["present", "retard", "absent"].forEach((abs) => {
|
const etats = ["retard", "absent"];
|
||||||
|
|
||||||
|
if (!window.nonPresent) {
|
||||||
|
etats.splice(0, 0, "present");
|
||||||
|
}
|
||||||
|
|
||||||
|
etats.forEach((abs) => {
|
||||||
const btn = document.createElement("input");
|
const btn = document.createElement("input");
|
||||||
btn.type = "checkbox";
|
btn.type = "checkbox";
|
||||||
btn.value = abs;
|
btn.value = abs;
|
||||||
|
@ -425,7 +431,7 @@ async function getModuleImpl(assiduite) {
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
moduleimpls[id] = `${data.module.code} ${data.module.abbrev}`;
|
moduleimpls[id] = `${data.module.code} ${data.module.abbrev || ""}`;
|
||||||
return moduleimpls[id];
|
return moduleimpls[id];
|
||||||
})
|
})
|
||||||
.catch((_) => {
|
.catch((_) => {
|
||||||
|
@ -531,12 +537,7 @@ async function MiseAJourLigneEtud(etud) {
|
||||||
|
|
||||||
async function actionAssiduite(etud, etat, type, assiduite = null) {
|
async function actionAssiduite(etud, etat, type, assiduite = null) {
|
||||||
const modimpl_id = $("#moduleimpl_select").val();
|
const modimpl_id = $("#moduleimpl_select").val();
|
||||||
if (
|
if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression";
|
||||||
assiduite &&
|
|
||||||
assiduite.etat.toLowerCase() === etat &&
|
|
||||||
assiduite.moduleimpl_id == modimpl_id
|
|
||||||
)
|
|
||||||
type = "suppression";
|
|
||||||
|
|
||||||
const { deb, fin } = getPeriodAsDate();
|
const { deb, fin } = getPeriodAsDate();
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 +
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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...",
|
||||||
|
|
|
@ -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 +
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 %}
|
|
@ -23,49 +23,8 @@ Calendrier de l'assiduité
|
||||||
for="mode_demi">mode demi journée</label>
|
for="mode_demi">mode demi journée</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendrier">
|
<div class="cal">
|
||||||
{% for mois,jours in calendrier.items() %}
|
{{calendrier|safe}}
|
||||||
<div class="month">
|
|
||||||
<h3>{{mois}}</h3>
|
|
||||||
<div class="days {{'demi' if mode_demi else ''}}">
|
|
||||||
{% for jour in jours %}
|
|
||||||
{% if jour.is_non_work() %}
|
|
||||||
<div class="day {{jour.get_class()}}">
|
|
||||||
<span>{{jour.get_nom()}}</span>
|
|
||||||
{% else %}
|
|
||||||
<div class="day {{jour.get_class(show_pres, show_reta) if not mode_demi else ''}}">
|
|
||||||
{% endif %}
|
|
||||||
{% if mode_demi %}
|
|
||||||
{% if not jour.is_non_work() %}
|
|
||||||
<span>{{jour.get_nom()}}</span>
|
|
||||||
<span class="{{jour.get_demi_class(True, show_pres,show_reta)}}"></span>
|
|
||||||
<span class="{{jour.get_demi_class(False, show_pres,show_reta)}}"></span>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{% if not jour.is_non_work() %}
|
|
||||||
<span>{{jour.get_nom(False)}}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if not jour.is_non_work() and jour.has_assiduites()%}
|
|
||||||
|
|
||||||
<div class="dayline">
|
|
||||||
<div class="dayline-title">
|
|
||||||
<span>Assiduité du</span>
|
|
||||||
<br>
|
|
||||||
<span>{{jour.get_date()}}</span>
|
|
||||||
{{jour.generate_minitimeline() | safe}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
<div class="annee">
|
<div class="annee">
|
||||||
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
<span id="label-annee">Année scolaire</span><span id="label-changer" style="margin-left: 5px;">Changer
|
||||||
année: </span>
|
année: </span>
|
||||||
|
@ -77,36 +36,7 @@ Calendrier de l'assiduité
|
||||||
|
|
||||||
<div class="help">
|
<div class="help">
|
||||||
<h3>Calendrier</h3>
|
<h3>Calendrier</h3>
|
||||||
<p>Code couleur</p>
|
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||||
<ul class="couleurs">
|
|
||||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
|
||||||
période
|
|
||||||
</li>
|
|
||||||
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
|
||||||
</li>
|
|
||||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
|
||||||
période
|
|
||||||
</li>
|
|
||||||
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
|
||||||
</li>
|
|
||||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
|
||||||
période
|
|
||||||
</li>
|
|
||||||
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><span title="Quart Bleu" class="est_just demo"></span> → la période est couverte par un
|
|
||||||
justificatif valide</li>
|
|
||||||
<li><span title="Justif. non valide" class="invalide demo"></span> → la période est
|
|
||||||
couverte par un justificatif non valide
|
|
||||||
</li>
|
|
||||||
<li><span title="Justif. en attente" class="attente demo"></span> → la période
|
|
||||||
a un justificatif en attente de validation
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
|
|
||||||
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="couleurs print">
|
<ul class="couleurs print">
|
||||||
<li><span title="Vert" class="present demo"></span> présence
|
<li><span title="Vert" class="present demo"></span> présence
|
||||||
|
@ -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 %}
|
|
@ -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
|
||||||
--->
|
--->
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,28 @@
|
||||||
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la période
|
<p>Code couleur</p>
|
||||||
</li>
|
<ul class="couleurs">
|
||||||
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la période
|
<li><span title="Vert" class="present demo"></span> → présence de l'étudiant lors de la
|
||||||
</li>
|
période
|
||||||
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la période
|
</li>
|
||||||
</li>
|
<li><span title="Bleu clair" class="nonwork demo"></span> → la période n'est pas travaillée
|
||||||
|
</li>
|
||||||
|
<li><span title="Rouge" class="absent demo"></span> → absence de l'étudiant lors de la
|
||||||
|
période
|
||||||
|
</li>
|
||||||
|
<li><span title="Rose" class="demo color absent est_just"></span> → absence justifiée
|
||||||
|
</li>
|
||||||
|
<li><span title="Orange" class="retard demo"></span> → retard de l'étudiant lors de la
|
||||||
|
période
|
||||||
|
</li>
|
||||||
|
<li><span title="Jaune clair" class="demo color retard est_just"></span> → retard justifié
|
||||||
|
</li>
|
||||||
|
|
||||||
<li><span title="Hachure Bleue" class="justified demo"></span> → l'assiduité est justifiée par un
|
<li><span title="Quart Bleu" class="est_just demo color"></span> → la période est couverte par un
|
||||||
justificatif valide</li>
|
justificatif valide</li>
|
||||||
<li><span title="Hachure Rouge" class="invalid_justified demo"></span> → l'assiduité est
|
<li><span title="Justif. non valide" class="invalide demo color "></span> → la période est
|
||||||
justifiée par un justificatif non valide / en attente de validation
|
couverte par un justificatif non valide
|
||||||
</li>
|
</li>
|
||||||
|
<li><span title="Justif. en attente" class="attente demo color"></span> → la période
|
||||||
|
a un justificatif en attente de validation
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations supplémentaires</p>
|
||||||
|
|
|
@ -74,7 +74,13 @@
|
||||||
setupAssiduiteBubble(block, assiduité);
|
setupAssiduiteBubble(block, assiduité);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: ajout couleur justificatif
|
// ajout couleur justificatif
|
||||||
|
const justificatifs = assiduité.justificatifs || [];
|
||||||
|
const justified = justificatifs.some(
|
||||||
|
(justificatif) => justificatif.etat === "VALIDE"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(justified) block.classList.add("est_just");
|
||||||
|
|
||||||
block.classList.add(assiduité.etat.toLowerCase());
|
block.classList.add(assiduité.etat.toLowerCase());
|
||||||
if(assiduité.etat != "CRENEAU") block.classList.add("color");
|
if(assiduité.etat != "CRENEAU") block.classList.add("color");
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
|
@ -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 -->
|
|
@ -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();
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) %}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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"]:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -685,7 +685,7 @@ 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():
|
||||||
|
@ -807,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()
|
||||||
|
|
|
@ -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",
|
||||||
|
)
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.6.958"
|
SCOVERSION = "9.6.966"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"] == "RÉGNIER"
|
assert etuds[0]["nom"] == "RÉÇIER"
|
||||||
|
|
||||||
|
|
||||||
def test_etudiant_annotations(api_headers):
|
def test_etudiant_annotations(api_headers):
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -4,13 +4,14 @@
|
||||||
"code_nip": "11",
|
"code_nip": "11",
|
||||||
"rang": "1",
|
"rang": "1",
|
||||||
"civilite_str": "Mme",
|
"civilite_str": "Mme",
|
||||||
"nom_disp": "FLEURY",
|
"nom_disp": "BONHOMME",
|
||||||
"prenom": "MADELEINE",
|
"prenom": "MADELEINE",
|
||||||
"code_cursus": "S1",
|
"nom_short": "BONHOMME Ma.",
|
||||||
"ues_validables": "3/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "bonhomme;madeleine",
|
||||||
"moy_gen": "14.36",
|
"moy_gen": "14.36",
|
||||||
"nbabs": 5,
|
|
||||||
"nbabsjust": 1,
|
|
||||||
"moy_ue_1": "14.94",
|
"moy_ue_1": "14.94",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "11.97",
|
"moy_res_3_1": "11.97",
|
||||||
|
@ -48,27 +49,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "17.83",
|
"moy_sae_14_3": "17.83",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "3/3",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"sort_key": "fleury;madeleine",
|
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 8,
|
"etudid": 8,
|
||||||
"code_nip": "NIP8",
|
"code_nip": "NIP8",
|
||||||
"rang": "2",
|
"rang": "2",
|
||||||
"civilite_str": "M.",
|
"civilite_str": "M.",
|
||||||
"nom_disp": "SAUNIER",
|
"nom_disp": "JAMES",
|
||||||
"prenom": "JACQUES",
|
"prenom": "JACQUES",
|
||||||
"code_cursus": "S1",
|
"nom_short": "JAMES Ja.",
|
||||||
"ues_validables": "3/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "james;jacques",
|
||||||
"moy_gen": "12.67",
|
"moy_gen": "12.67",
|
||||||
"nbabs": 3,
|
|
||||||
"nbabsjust": 1,
|
|
||||||
"moy_ue_1": "13.51",
|
"moy_ue_1": "13.51",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "03.27",
|
"moy_res_3_1": "03.27",
|
||||||
|
@ -106,27 +108,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "10.74",
|
"moy_sae_14_3": "10.74",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "3/3",
|
||||||
|
"nbabs": 2,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"sort_key": "saunier;jacques",
|
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 6,
|
"etudid": 6,
|
||||||
"code_nip": "NIP6",
|
"code_nip": "NIP6",
|
||||||
"rang": "3",
|
"rang": "3",
|
||||||
"civilite_str": "",
|
"civilite_str": "",
|
||||||
"nom_disp": "LENFANT",
|
"nom_disp": "THIBAUD",
|
||||||
"prenom": "MAXIME",
|
"prenom": "MAXIME",
|
||||||
"code_cursus": "S1",
|
"nom_short": "THIBAUD Ma.",
|
||||||
"ues_validables": "2/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "thibaud;maxime",
|
||||||
"moy_gen": "12.02",
|
"moy_gen": "12.02",
|
||||||
"nbabs": 0,
|
|
||||||
"nbabsjust": 0,
|
|
||||||
"moy_ue_1": "14.34",
|
"moy_ue_1": "14.34",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "17.68",
|
"moy_res_3_1": "17.68",
|
||||||
|
@ -164,27 +167,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "05.70",
|
"moy_sae_14_3": "05.70",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "2/3",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "lenfant;maxime",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 7,
|
"etudid": 7,
|
||||||
"code_nip": "7",
|
"code_nip": "7",
|
||||||
"rang": "4",
|
"rang": "4",
|
||||||
"civilite_str": "",
|
"civilite_str": "",
|
||||||
"nom_disp": "CUNY",
|
"nom_disp": "ROYER",
|
||||||
"prenom": "CAMILLE",
|
"prenom": "CAMILLE",
|
||||||
"code_cursus": "S1",
|
"nom_short": "ROYER Ca.",
|
||||||
"ues_validables": "2/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "royer;camille",
|
||||||
"moy_gen": "11.88",
|
"moy_gen": "11.88",
|
||||||
"nbabs": 4,
|
|
||||||
"nbabsjust": 4,
|
|
||||||
"moy_ue_1": "07.09",
|
"moy_ue_1": "07.09",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "04.07",
|
"moy_res_3_1": "04.07",
|
||||||
|
@ -222,27 +226,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "10.52",
|
"moy_sae_14_3": "10.52",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "2/3",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"sort_key": "cuny;camille",
|
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 12,
|
"etudid": 12,
|
||||||
"code_nip": "NIP12",
|
"code_nip": "NIP12",
|
||||||
"rang": "5",
|
"rang": "5",
|
||||||
"civilite_str": "M.",
|
"civilite_str": "M.",
|
||||||
"nom_disp": "MOUTON",
|
"nom_disp": "GODIN",
|
||||||
"prenom": "CLAUDE",
|
"prenom": "CLAUDE",
|
||||||
"code_cursus": "S1",
|
"nom_short": "GODIN Cl.",
|
||||||
"ues_validables": "1/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "godin;claude",
|
||||||
"moy_gen": "10.52",
|
"moy_gen": "10.52",
|
||||||
"nbabs": 1,
|
|
||||||
"nbabsjust": 0,
|
|
||||||
"moy_ue_1": "08.93",
|
"moy_ue_1": "08.93",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "07.77",
|
"moy_res_3_1": "07.77",
|
||||||
|
@ -280,27 +285,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "11.09",
|
"moy_sae_14_3": "11.09",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "1/3",
|
||||||
|
"nbabs": 3,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "mouton;claude",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 3,
|
"etudid": 3,
|
||||||
"code_nip": "3",
|
"code_nip": "3",
|
||||||
"rang": "6",
|
"rang": "6",
|
||||||
"civilite_str": "M.",
|
"civilite_str": "M.",
|
||||||
"nom_disp": "R\u00c9GNIER",
|
"nom_disp": "CONSTANT",
|
||||||
"prenom": "PATRICK",
|
"prenom": "PATRICK",
|
||||||
"code_cursus": "S1",
|
"nom_short": "CONSTANT Pa.",
|
||||||
"ues_validables": "2/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "constant;patrick",
|
||||||
"moy_gen": "10.04",
|
"moy_gen": "10.04",
|
||||||
"nbabs": 0,
|
|
||||||
"nbabsjust": 0,
|
|
||||||
"moy_ue_1": "13.06",
|
"moy_ue_1": "13.06",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "05.84",
|
"moy_res_3_1": "05.84",
|
||||||
|
@ -338,27 +344,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "01.55",
|
"moy_sae_14_3": "01.55",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "2/3",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"sort_key": "regnier;patrick",
|
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 13,
|
"etudid": 13,
|
||||||
"code_nip": "13",
|
"code_nip": "13",
|
||||||
"rang": "7",
|
"rang": "7",
|
||||||
"civilite_str": "",
|
"civilite_str": "",
|
||||||
"nom_disp": "ESTEVE",
|
"nom_disp": "TOUSSAINT",
|
||||||
"prenom": "ALIX",
|
"prenom": "ALIX",
|
||||||
"code_cursus": "S1",
|
"nom_short": "TOUSSAINT Al.",
|
||||||
"ues_validables": "1/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "toussaint;alix",
|
||||||
"moy_gen": "08.59",
|
"moy_gen": "08.59",
|
||||||
"nbabs": 0,
|
|
||||||
"nbabsjust": 0,
|
|
||||||
"moy_ue_1": "07.24",
|
"moy_ue_1": "07.24",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "11.90",
|
"moy_res_3_1": "11.90",
|
||||||
|
@ -396,27 +403,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "05.17",
|
"moy_sae_14_3": "05.17",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "1/3",
|
||||||
|
"nbabs": 3,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "esteve;alix",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 16,
|
"etudid": 16,
|
||||||
"code_nip": "NIP16",
|
"code_nip": "NIP16",
|
||||||
"rang": "8",
|
"rang": "8",
|
||||||
"civilite_str": "",
|
"civilite_str": "",
|
||||||
"nom_disp": "GILLES",
|
"nom_disp": "DENIS",
|
||||||
"prenom": "MAXIME",
|
"prenom": "MAXIME",
|
||||||
"code_cursus": "S1",
|
"nom_short": "DENIS Ma.",
|
||||||
"ues_validables": "0/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "denis;maxime",
|
||||||
"moy_gen": "07.21",
|
"moy_gen": "07.21",
|
||||||
"nbabs": 1,
|
|
||||||
"nbabsjust": 1,
|
|
||||||
"moy_ue_1": "06.86",
|
"moy_ue_1": "06.86",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "~",
|
"moy_res_3_1": "~",
|
||||||
|
@ -454,27 +462,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "03.32",
|
"moy_sae_14_3": "03.32",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "0/3",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "gilles;maxime",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 2,
|
"etudid": 2,
|
||||||
"code_nip": "NIP2",
|
"code_nip": "NIP2",
|
||||||
"rang": "9",
|
"rang": "9",
|
||||||
"civilite_str": "Mme",
|
"civilite_str": "Mme",
|
||||||
"nom_disp": "NAUDIN",
|
"nom_disp": "WALTER",
|
||||||
"prenom": "SIMONE",
|
"prenom": "SIMONE",
|
||||||
"code_cursus": "S1",
|
"nom_short": "WALTER Si.",
|
||||||
"ues_validables": "0/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "walter;simone",
|
||||||
"moy_gen": "07.02",
|
"moy_gen": "07.02",
|
||||||
"nbabs": 5,
|
|
||||||
"nbabsjust": 3,
|
|
||||||
"moy_ue_1": "06.82",
|
"moy_ue_1": "06.82",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "16.91",
|
"moy_res_3_1": "16.91",
|
||||||
|
@ -512,27 +521,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "02.10",
|
"moy_sae_14_3": "02.10",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "0/3",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "naudin;simone",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 1,
|
"etudid": 1,
|
||||||
"code_nip": "1",
|
"code_nip": "1",
|
||||||
"rang": "10",
|
"rang": "10",
|
||||||
"civilite_str": "",
|
"civilite_str": "",
|
||||||
"nom_disp": "COSTA",
|
"nom_disp": "GROSS",
|
||||||
"prenom": "SACHA",
|
"prenom": "SACHA",
|
||||||
"code_cursus": "S1",
|
"nom_short": "GROSS Sa.",
|
||||||
"ues_validables": "0/3",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "gross;sacha",
|
||||||
"moy_gen": "05.31",
|
"moy_gen": "05.31",
|
||||||
"nbabs": 2,
|
|
||||||
"nbabsjust": 1,
|
|
||||||
"moy_ue_1": "03.73",
|
"moy_ue_1": "03.73",
|
||||||
"moy_res_1_1": "~",
|
"moy_res_1_1": "~",
|
||||||
"moy_res_3_1": "~",
|
"moy_res_3_1": "~",
|
||||||
|
@ -570,27 +580,28 @@
|
||||||
"moy_res_21_3": "~",
|
"moy_res_21_3": "~",
|
||||||
"moy_sae_14_3": "07.17",
|
"moy_sae_14_3": "07.17",
|
||||||
"moy_sae_15_3": "~",
|
"moy_sae_15_3": "~",
|
||||||
|
"ues_validables": "0/3",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "costa;sacha",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 4,
|
"etudid": 4,
|
||||||
"code_nip": "NIP4",
|
"code_nip": "NIP4",
|
||||||
"rang": "11 ex",
|
"rang": "11 ex",
|
||||||
"civilite_str": "M.",
|
"civilite_str": "M.",
|
||||||
"nom_disp": "GAUTIER",
|
"nom_disp": "BARTHELEMY",
|
||||||
"prenom": "G\u00c9RARD",
|
"prenom": "G\u00c9RARD",
|
||||||
"code_cursus": "S1",
|
"nom_short": "BARTHELEMY G\u00e9.",
|
||||||
"ues_validables": "",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "barthelemy;gerard",
|
||||||
"moy_gen": "",
|
"moy_gen": "",
|
||||||
"nbabs": 3,
|
|
||||||
"nbabsjust": 1,
|
|
||||||
"moy_ue_1": "",
|
"moy_ue_1": "",
|
||||||
"moy_res_1_1": "",
|
"moy_res_1_1": "",
|
||||||
"moy_res_3_1": "",
|
"moy_res_3_1": "",
|
||||||
|
@ -628,27 +639,28 @@
|
||||||
"moy_res_21_3": "",
|
"moy_res_21_3": "",
|
||||||
"moy_sae_14_3": "",
|
"moy_sae_14_3": "",
|
||||||
"moy_sae_15_3": "",
|
"moy_sae_15_3": "",
|
||||||
|
"ues_validables": "",
|
||||||
|
"nbabs": 2,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "gautier;gerard",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 5,
|
"etudid": 5,
|
||||||
"code_nip": "5",
|
"code_nip": "5",
|
||||||
"rang": "11 ex",
|
"rang": "11 ex",
|
||||||
"civilite_str": "Mme",
|
"civilite_str": "Mme",
|
||||||
"nom_disp": "VILLENEUVE",
|
"nom_disp": "MILLOT",
|
||||||
"prenom": "FRAN\u00c7OISE",
|
"prenom": "FRAN\u00c7OISE",
|
||||||
"code_cursus": "S1",
|
"nom_short": "MILLOT Fr.",
|
||||||
"ues_validables": "",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "millot;francoise",
|
||||||
"moy_gen": "",
|
"moy_gen": "",
|
||||||
"nbabs": 0,
|
|
||||||
"nbabsjust": 0,
|
|
||||||
"moy_ue_1": "",
|
"moy_ue_1": "",
|
||||||
"moy_res_1_1": "",
|
"moy_res_1_1": "",
|
||||||
"moy_res_3_1": "",
|
"moy_res_3_1": "",
|
||||||
|
@ -686,27 +698,28 @@
|
||||||
"moy_res_21_3": "",
|
"moy_res_21_3": "",
|
||||||
"moy_sae_14_3": "",
|
"moy_sae_14_3": "",
|
||||||
"moy_sae_15_3": "",
|
"moy_sae_15_3": "",
|
||||||
|
"ues_validables": "",
|
||||||
|
"nbabs": 2,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "villeneuve;francoise",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 9,
|
"etudid": 9,
|
||||||
"code_nip": "9",
|
"code_nip": "9",
|
||||||
"rang": "11 ex",
|
"rang": "11 ex",
|
||||||
"civilite_str": "M.",
|
"civilite_str": "M.",
|
||||||
"nom_disp": "SCHMITT",
|
"nom_disp": "BENOIT",
|
||||||
"prenom": "EMMANUEL",
|
"prenom": "EMMANUEL",
|
||||||
"code_cursus": "S1",
|
"nom_short": "BENOIT Em.",
|
||||||
"ues_validables": "",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "benoit;emmanuel",
|
||||||
"moy_gen": "",
|
"moy_gen": "",
|
||||||
"nbabs": 0,
|
|
||||||
"nbabsjust": 0,
|
|
||||||
"moy_ue_1": "",
|
"moy_ue_1": "",
|
||||||
"moy_res_1_1": "",
|
"moy_res_1_1": "",
|
||||||
"moy_res_3_1": "",
|
"moy_res_3_1": "",
|
||||||
|
@ -744,27 +757,28 @@
|
||||||
"moy_res_21_3": "",
|
"moy_res_21_3": "",
|
||||||
"moy_sae_14_3": "",
|
"moy_sae_14_3": "",
|
||||||
"moy_sae_15_3": "",
|
"moy_sae_15_3": "",
|
||||||
|
"ues_validables": "",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "schmitt;emmanuel",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 10,
|
"etudid": 10,
|
||||||
"code_nip": "NIP10",
|
"code_nip": "NIP10",
|
||||||
"rang": "11 ex",
|
"rang": "11 ex",
|
||||||
"civilite_str": "Mme",
|
"civilite_str": "Mme",
|
||||||
"nom_disp": "BOUTET",
|
"nom_disp": "LECOCQ",
|
||||||
"prenom": "MARGUERITE",
|
"prenom": "MARGUERITE",
|
||||||
"code_cursus": "S1",
|
"nom_short": "LECOCQ Ma.",
|
||||||
"ues_validables": "",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "lecocq;marguerite",
|
||||||
"moy_gen": "",
|
"moy_gen": "",
|
||||||
"nbabs": 0,
|
|
||||||
"nbabsjust": 0,
|
|
||||||
"moy_ue_1": "",
|
"moy_ue_1": "",
|
||||||
"moy_res_1_1": "",
|
"moy_res_1_1": "",
|
||||||
"moy_res_3_1": "",
|
"moy_res_3_1": "",
|
||||||
|
@ -802,27 +816,28 @@
|
||||||
"moy_res_21_3": "",
|
"moy_res_21_3": "",
|
||||||
"moy_sae_14_3": "",
|
"moy_sae_14_3": "",
|
||||||
"moy_sae_15_3": "",
|
"moy_sae_15_3": "",
|
||||||
|
"ues_validables": "",
|
||||||
|
"nbabs": 1,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "boutet;marguerite",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 14,
|
"etudid": 14,
|
||||||
"code_nip": "NIP14",
|
"code_nip": "NIP14",
|
||||||
"rang": "11 ex",
|
"rang": "11 ex",
|
||||||
"civilite_str": "M.",
|
"civilite_str": "M.",
|
||||||
"nom_disp": "ROLLIN",
|
"nom_disp": "ROUSSET",
|
||||||
"prenom": "DERC'HEN",
|
"prenom": "DERC'HEN",
|
||||||
"code_cursus": "S1",
|
"nom_short": "ROUSSET De.",
|
||||||
"ues_validables": "",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "rousset;derchen",
|
||||||
"moy_gen": "",
|
"moy_gen": "",
|
||||||
"nbabs": 0,
|
|
||||||
"nbabsjust": 0,
|
|
||||||
"moy_ue_1": "",
|
"moy_ue_1": "",
|
||||||
"moy_res_1_1": "",
|
"moy_res_1_1": "",
|
||||||
"moy_res_3_1": "",
|
"moy_res_3_1": "",
|
||||||
|
@ -860,27 +875,28 @@
|
||||||
"moy_res_21_3": "",
|
"moy_res_21_3": "",
|
||||||
"moy_sae_14_3": "",
|
"moy_sae_14_3": "",
|
||||||
"moy_sae_15_3": "",
|
"moy_sae_15_3": "",
|
||||||
|
"ues_validables": "",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"sort_key": "rollin;derchen",
|
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"etudid": 15,
|
"etudid": 15,
|
||||||
"code_nip": "15",
|
"code_nip": "15",
|
||||||
"rang": "11 ex",
|
"rang": "11 ex",
|
||||||
"civilite_str": "",
|
"civilite_str": "",
|
||||||
"nom_disp": "DIOT",
|
"nom_disp": "MORAND",
|
||||||
"prenom": "CAMILLE",
|
"prenom": "CAMILLE",
|
||||||
"code_cursus": "S1",
|
"nom_short": "MORAND Ca.",
|
||||||
"ues_validables": "",
|
"partitions": {
|
||||||
|
"1": 1
|
||||||
|
},
|
||||||
|
"sort_key": "morand;camille",
|
||||||
"moy_gen": "",
|
"moy_gen": "",
|
||||||
"nbabs": 4,
|
|
||||||
"nbabsjust": 2,
|
|
||||||
"moy_ue_1": "",
|
"moy_ue_1": "",
|
||||||
"moy_res_1_1": "",
|
"moy_res_1_1": "",
|
||||||
"moy_res_3_1": "",
|
"moy_res_3_1": "",
|
||||||
|
@ -918,13 +934,13 @@
|
||||||
"moy_res_21_3": "",
|
"moy_res_21_3": "",
|
||||||
"moy_sae_14_3": "",
|
"moy_sae_14_3": "",
|
||||||
"moy_sae_15_3": "",
|
"moy_sae_15_3": "",
|
||||||
|
"ues_validables": "",
|
||||||
|
"nbabs": 0,
|
||||||
|
"nbabsjust": 0,
|
||||||
|
"code_cursus": "S1",
|
||||||
"bac": "",
|
"bac": "",
|
||||||
"sort_key": "diot;camille",
|
|
||||||
"specialite": "",
|
"specialite": "",
|
||||||
"type_admission": "",
|
"type_admission": "",
|
||||||
"classement": "",
|
"classement": ""
|
||||||
"partitions": {
|
|
||||||
"1": 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue