forked from ScoDoc/ScoDoc
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6dcc4611cc | ||
|
fc8208f061 | ||
|
06433e9063 | ||
|
89dbac2514 | ||
|
63f21c2dd6 | ||
|
7322cc5b56 | ||
|
12dc837f64 | ||
|
f106b26203 | ||
|
53bc91a1b5 |
152
README.md
152
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
(c) Emmanuel Viennet 1999 - 2024 (voir LICENCE.txt).
|
||||
|
||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian12>
|
||||
Installation: voir instructions à jour sur <https://scodoc.org/GuideInstallDebian11>
|
||||
|
||||
Documentation utilisateur: <https://scodoc.org>
|
||||
|
||||
@ -23,7 +23,7 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
|
||||
|
||||
### Lignes de commandes
|
||||
|
||||
Voir [le guide de configuration](https://scodoc.org/GuideConfig).
|
||||
Voir [https://scodoc.org/GuideConfig](le guide de configuration).
|
||||
|
||||
## Organisation des fichiers
|
||||
|
||||
@ -41,41 +41,45 @@ Ils ne doivent pas être modifiés à la main, sauf certains fichiers de configu
|
||||
Le répertoire `/opt/scodoc-data` doit être régulièrement sauvegardé.
|
||||
|
||||
Principaux contenus:
|
||||
```
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
```
|
||||
|
||||
/opt/scodoc-data
|
||||
/opt/scodoc-data/log # Fichiers de log ScoDoc
|
||||
/opt/scodoc-data/config # Fichiers de configuration
|
||||
.../config/logos # Logos de l'établissement
|
||||
.../config/depts # un fichier par département
|
||||
/opt/scodoc-data/photos # Photos des étudiants
|
||||
/opt/scodoc-data/archives # Archives: PV de jury, maquettes Apogée, fichiers étudiants
|
||||
|
||||
## Pour les développeurs
|
||||
|
||||
### Installation du code
|
||||
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian12)).
|
||||
Installer ScoDoc 9 normalement ([voir la doc](https://scodoc.org/GuideInstallDebian11)).
|
||||
|
||||
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éé !)
|
||||
|
||||
# Donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc:scodoc /opt/scodoc
|
||||
```
|
||||
sudo su
|
||||
mv /opt/scodoc /opt/off-scodoc # ou ce que vous voulez
|
||||
apt-get install git # si besoin
|
||||
cd /opt
|
||||
git clone https://scodoc.org/git/viennet/ScoDoc.git
|
||||
# (ou bien utiliser votre clone gitea si vous l'avez déjà créé !)
|
||||
|
||||
# Renommer le répertoire:
|
||||
mv ScoDoc scodoc
|
||||
|
||||
# Et donner ce répertoire à l'utilisateur scodoc:
|
||||
chown -R scodoc.scodoc /opt/scodoc
|
||||
|
||||
Il faut ensuite installer l'environnement et le fichier de configuration:
|
||||
```bash
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
```
|
||||
|
||||
# Le plus simple est de piquer le virtualenv configuré par l'installeur:
|
||||
mv /opt/off-scodoc/venv /opt/scodoc
|
||||
|
||||
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
|
||||
n'est pas toujours le plus judicieux: vous pouvez modifier son contenu, par
|
||||
exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||
@ -84,11 +88,11 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
|
||||
|
||||
Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`.
|
||||
Avant le premier lancement, créer cette base ainsi:
|
||||
```bash
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
./tools/create_database.sh SCODOC_TEST
|
||||
export FLASK_ENV=test
|
||||
flask db upgrade
|
||||
|
||||
Cette commande n'est nécessaire que la première fois (le contenu de la base
|
||||
est effacé au début de chaque test, mais son schéma reste) et aussi si des
|
||||
migrations (changements de schéma) ont eu lieu dans le code.
|
||||
@ -96,17 +100,17 @@ migrations (changements de schéma) ont eu lieu dans le code.
|
||||
Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
|
||||
scripts de tests:
|
||||
Lancer au préalable:
|
||||
```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:
|
||||
```bash
|
||||
pytest tests/unit
|
||||
```
|
||||
|
||||
pytest tests/unit
|
||||
|
||||
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
|
||||
|
||||
On peut aussi utiliser les tests unitaires pour mettre la base de données de
|
||||
@ -115,43 +119,43 @@ développement dans un état connu, par exemple pour éviter de recréer à la m
|
||||
|
||||
Il suffit de positionner une variable d'environnement indiquant la BD utilisée
|
||||
par les tests:
|
||||
```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
|
||||
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
|
||||
utilisateur:
|
||||
```bash
|
||||
flask user-password admin
|
||||
```
|
||||
|
||||
flask user-password admin
|
||||
|
||||
**Attention:** les tests unitaires **effacent** complètement le contenu de la
|
||||
base de données (tous les départements, et les utilisateurs) avant de commencer !
|
||||
|
||||
#### Modification du schéma de la base
|
||||
|
||||
On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||
```bash
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
flask db migrate -m "message explicatif....."
|
||||
flask db upgrade
|
||||
|
||||
Ne pas oublier de d'ajouter le script de migration à git (`git add migrations/...`).
|
||||
|
||||
**Mémo**: séquence re-création d'une base (vérifiez votre `.env`
|
||||
ou variables d'environnement pour interroger la bonne base !).
|
||||
```bash
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
flask db upgrade # créé les tables à partir des migrations
|
||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
```
|
||||
dropdb SCODOC_DEV
|
||||
tools/create_database.sh SCODOC_DEV # créé base SQL
|
||||
flask db upgrade # créé les tables à partir des migrations
|
||||
flask sco-db-init # ajoute au besoin les constantes (fait en migration 0)
|
||||
|
||||
# puis imports:
|
||||
flask import-scodoc7-users
|
||||
flask import-scodoc7-dept STID SCOSTID
|
||||
|
||||
Si la base utilisée pour les dev n'est plus en phase avec les scripts de
|
||||
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
|
||||
positionner à la bonne étape.
|
||||
@ -159,23 +163,23 @@ positionner à la bonne étape.
|
||||
### Profiling
|
||||
|
||||
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`).
|
||||
|
||||
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
|
||||
```bash
|
||||
pip install snakeviz
|
||||
```
|
||||
|
||||
pip install snakeviz
|
||||
|
||||
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
|
||||
|
||||
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
|
||||
important est `postinst` qui se charge de configurer le système (install ou
|
||||
important est `postinst`qui se charge de configurer le système (install ou
|
||||
upgrade de scodoc9).
|
||||
|
||||
La préparation d'une release se fait à l'aide du script
|
||||
|
@ -637,12 +637,14 @@ def critical_error(msg):
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
log(f"\n*** CRITICAL ERROR: {msg}")
|
||||
subject = f"CRITICAL ERROR: {msg}".strip()[:68]
|
||||
send_scodoc_alarm(subject, msg)
|
||||
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
|
||||
clear_scodoc_cache()
|
||||
raise ScoValueError(
|
||||
f"""
|
||||
Une erreur est survenue, veuillez ré-essayer.
|
||||
Une erreur est survenue.
|
||||
|
||||
Si le problème persiste, merci de contacter le support ScoDoc via
|
||||
{scu.SCO_DISCORD_ASSISTANCE}
|
||||
|
||||
{msg}
|
||||
"""
|
||||
|
@ -1,14 +1,10 @@
|
||||
"""api.__init__
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from flask_json import as_json
|
||||
from flask import Blueprint
|
||||
from flask import current_app, g, request
|
||||
from flask import request, g
|
||||
from flask_login import current_user
|
||||
from app import db
|
||||
from app.decorators import permission_required
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -20,28 +16,6 @@ api_web_bp = Blueprint("apiweb", __name__)
|
||||
API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
|
||||
|
||||
|
||||
def api_permission_required(permission):
|
||||
"""Ce décorateur fait la même chose que @permission_required
|
||||
mais enregistre dans l'attribut .scodoc_permission
|
||||
de la fonction la valeur de la permission.
|
||||
Cette valeur n'est utilisée que pour la génération automatique de la documentation.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
f.scodoc_permission = permission
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
if not current_user.has_permission(permission, scodoc_dept):
|
||||
return current_app.login_manager.unauthorized()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@api_bp.errorhandler(ScoException)
|
||||
@api_web_bp.errorhandler(ScoException)
|
||||
@api_bp.errorhandler(404)
|
||||
|
@ -12,18 +12,15 @@ from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from app import db, log, set_sco_dept
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp, get_model_api_object, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import (
|
||||
Assiduite,
|
||||
Evaluation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
@ -48,8 +45,6 @@ def assiduite(assiduite_id: int = None):
|
||||
"""Retourne un objet assiduité à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"assiduite_id": 1,
|
||||
"etudid": 2,
|
||||
@ -58,17 +53,11 @@ def assiduite(assiduite_id: int = None):
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "retard",
|
||||
"desc": "une description",
|
||||
"user_id": 1 or null,
|
||||
"user_name" : login scodoc or null,
|
||||
"user_nom_complet": "Marie Dupont",
|
||||
"user_id: 1 or null,
|
||||
"user_name" : login scodoc or null
|
||||
"user_nom_complet": "Marie Dupont"
|
||||
"est_just": False or True,
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1;
|
||||
|
||||
"""
|
||||
|
||||
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||
@ -86,23 +75,15 @@ def assiduite(assiduite_id: int = None):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
||||
"""Retourne la liste des justificatifs qui justifient cette assiduité.
|
||||
"""Retourne la liste des justificatifs qui justifie cette assiduitée
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
...
|
||||
]
|
||||
```
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1/justificatifs;
|
||||
/assiduite/1/justificatifs/long;
|
||||
|
||||
"""
|
||||
|
||||
return get_assiduites_justif(assiduite_id, long)
|
||||
@ -136,42 +117,52 @@ def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_count(
|
||||
def count_assiduites(
|
||||
etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False
|
||||
):
|
||||
"""
|
||||
Retourne le nombre d'assiduités d'un étudiant.
|
||||
Retourne le nombre d'assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>/count
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/count/query?
|
||||
|
||||
Les différents filtres :
|
||||
Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
|
||||
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
|
||||
ex: .../query?type=heure
|
||||
Comportement par défaut : compte le nombre d'assiduité enregistrée
|
||||
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemestre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
metric:<array[string]:metric>
|
||||
split:<bool:split>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
metric: la/les métriques de comptage (journee, demi, heure, compte)
|
||||
split: divise le comptage par état
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1/count;
|
||||
/assiduites/1/count/query?etat=retard;
|
||||
/assiduites/1/count/query?split;
|
||||
/assiduites/1/count/query?etat=present,retard&metric=compte,heure;
|
||||
|
||||
"""
|
||||
|
||||
@ -228,35 +219,40 @@ def assiduites_count(
|
||||
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
with_justifs:<bool:with_justifs>
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/query?
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
with_justif:ajoute les justificatifs liés à l'assiduité
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1;
|
||||
/assiduites/1/query?etat=retard;
|
||||
/assiduites/1/query?moduleimpl_id=1;
|
||||
/assiduites/1/query?with_justifs=;
|
||||
|
||||
"""
|
||||
|
||||
@ -268,7 +264,6 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
# Récupération des assiduités de l'étudiant
|
||||
assiduites_query: Query = etud.assiduites
|
||||
|
||||
@ -291,108 +286,6 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
||||
return data_set
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>/evaluations")
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/evaluations")
|
||||
# etudid
|
||||
@bp.route("/assiduites/etudid/<int:etudid>/evaluations")
|
||||
@api_web_bp.route("/assiduites/etudid/<int:etudid>/evaluations")
|
||||
# ine
|
||||
@bp.route("/assiduites/ine/<ine>/evaluations")
|
||||
@api_web_bp.route("/assiduites/ine/<ine>/evaluations")
|
||||
# nip
|
||||
@bp.route("/assiduites/nip/<nip>/evaluations")
|
||||
@api_web_bp.route("/assiduites/nip/<nip>/evaluations")
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Retourne la liste de toutes les évaluations de l'étudiant
|
||||
Pour chaque évaluation, retourne la liste des objets assiduités
|
||||
sur la plage de l'évaluation
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"evaluation_id": 1234,
|
||||
"assiduites": [
|
||||
{
|
||||
"assiduite_id":1234,
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1/evaluations;
|
||||
|
||||
```
|
||||
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
# Récupération des évaluations et des assidiutés
|
||||
etud_evaluations_assiduites: list[dict] = scass.get_etud_evaluations_assiduites(
|
||||
etud
|
||||
)
|
||||
|
||||
return etud_evaluations_assiduites
|
||||
|
||||
|
||||
@api_web_bp.route("/evaluation/<int:evaluation_id>/assiduites")
|
||||
@bp.route("/evaluation/<int:evaluation_id>/assiduites")
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def evaluation_assiduites(evaluation_id):
|
||||
"""
|
||||
Retourne les objets assiduités de chaque étudiant sur la plage de l'évaluation
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"<etudid>" : [
|
||||
{
|
||||
"assiduite_id":1234,
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Évaluations
|
||||
"""
|
||||
# Récupération de l'évaluation
|
||||
try:
|
||||
evaluation: Evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||
except HTTPException:
|
||||
return json_error(404, "L'évaluation n'existe pas")
|
||||
|
||||
evaluation_assiduites_par_etudid: dict[int, list[Assiduite]] = {}
|
||||
for assi in scass.get_evaluation_assiduites(evaluation):
|
||||
etudid: str = str(assi.etudid)
|
||||
etud_assiduites = evaluation_assiduites_par_etudid.get(etudid, [])
|
||||
etud_assiduites.append(assi.to_dict(format_api=True))
|
||||
evaluation_assiduites_par_etudid[etudid] = etud_assiduites
|
||||
|
||||
return evaluation_assiduites_par_etudid
|
||||
|
||||
|
||||
@bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@ -404,34 +297,38 @@ def assiduites_group(with_query: bool = False):
|
||||
Retourne toutes les assiduités d'un groupe d'étudiants
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
etudids:<array[int]:etudids>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
with_justif:<bool:with_justif>
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
etudids:liste des ids des étudiants concernés par la recherche
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
with_justifs:ajoute les justificatifs liés à l'assiduité
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/group/query?etudids=1,2,3;
|
||||
|
||||
"""
|
||||
|
||||
@ -491,34 +388,7 @@ def assiduites_group(with_query: bool = False):
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
"""Retourne toutes les assiduités du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/formsemestre/1;
|
||||
/assiduites/formsemestre/1/query?etat=retard;
|
||||
/assiduites/formsemestre/1/query?moduleimpl_id=1;
|
||||
|
||||
"""
|
||||
"""Retourne toutes les assiduités du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre à partir du formsemestre_id
|
||||
formsemestre: FormSemestre = None
|
||||
@ -565,42 +435,10 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre_count(
|
||||
def count_assiduites_formsemestre(
|
||||
formsemestre_id: int = None, with_query: bool = False
|
||||
):
|
||||
"""Comptage des assiduités du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
metric:<array[string]:metric>
|
||||
split:<bool:split>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
metric: la/les métriques de comptage (journee, demi, heure, compte)
|
||||
split: divise le comptage par état
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/formsemestre/1/count;
|
||||
/assiduites/formsemestre/1/count/query?etat=retard;
|
||||
/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure;
|
||||
|
||||
"""
|
||||
"""Comptage des assiduités du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre à partir du formsemestre_id
|
||||
formsemestre: FormSemestre = None
|
||||
@ -651,10 +489,7 @@ def assiduites_formsemestre_count(
|
||||
def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Enregistrement d'assiduités pour un étudiant (etudid)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -670,12 +505,6 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1/create;[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]
|
||||
/assiduite/1/create;[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]
|
||||
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
@ -729,10 +558,7 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
def assiduites_create():
|
||||
"""
|
||||
Création d'une assiduité ou plusieurs assiduites
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -745,17 +571,12 @@ def assiduites_create():
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"etudid":int,
|
||||
|
||||
"moduleimpl_id": int,
|
||||
"desc":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/create;[{""etudid"":1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]
|
||||
/assiduites/create;[{""etudid"":-1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]
|
||||
|
||||
"""
|
||||
|
||||
@ -926,18 +747,13 @@ def assiduite_delete():
|
||||
"""
|
||||
Suppression d'une assiduité à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<assiduite_id:int>,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/delete;[2,2,3]
|
||||
|
||||
"""
|
||||
# Récupération des ids envoyés dans la liste
|
||||
@ -1012,24 +828,13 @@ def _delete_one(assiduite_id: int) -> tuple[int, str]:
|
||||
def assiduite_edit(assiduite_id: int):
|
||||
"""
|
||||
Edition d'une assiduité à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
{
|
||||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
"est_just"?: bool
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1/edit;{""etat"":""absent""}
|
||||
/assiduite/1/edit;{""moduleimpl_id"":2}
|
||||
/assiduite/1/edit;{""etat"": ""retard"",""moduleimpl_id"":3}
|
||||
|
||||
"""
|
||||
|
||||
# Récupération de l'assiduité à modifier
|
||||
@ -1071,10 +876,7 @@ def assiduite_edit(assiduite_id: int):
|
||||
def assiduites_edit():
|
||||
"""
|
||||
Edition de plusieurs assiduités
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"assiduite_id" : int,
|
||||
@ -1084,13 +886,6 @@ def assiduites_edit():
|
||||
"est_just"?: bool
|
||||
}
|
||||
]
|
||||
```
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/edit;[{""etat"":""absent"",""assiduite_id"":1}]
|
||||
/assiduites/edit;[{""moduleimpl_id"":2,""assiduite_id"":1}]
|
||||
/assiduites/edit;[{""etat"": ""retard"",""moduleimpl_id"":3,""assiduite_id"":1}]
|
||||
|
||||
"""
|
||||
edit_list: list[object] = request.get_json(force=True)
|
||||
|
||||
|
@ -6,11 +6,6 @@
|
||||
|
||||
"""
|
||||
API : billets d'absences
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Billets d'absence
|
||||
|
||||
"""
|
||||
|
||||
from flask import g, request
|
||||
@ -34,7 +29,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def billets_absence_etudiant(etudid: int):
|
||||
"""Liste des billets d'absence pour cet étudiant."""
|
||||
"""Liste des billets d'absence pour cet étudiant"""
|
||||
billets = sco_abs_billets.query_billets_etud(etudid)
|
||||
return [billet.to_dict() for billet in billets]
|
||||
|
||||
@ -46,20 +41,7 @@ def billets_absence_etudiant(etudid: int):
|
||||
@permission_required(Permission.AbsAddBillet)
|
||||
@as_json
|
||||
def billets_absence_create():
|
||||
"""Ajout d'un billet d'absence. Renvoie le billet créé en json.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"etudid" : int,
|
||||
"abs_begin" : date_iso,
|
||||
"abs_end" : date_iso,
|
||||
"description" : string,
|
||||
"justified" : bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
"""Ajout d'un billet d'absence"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
etudid = data.get("etudid")
|
||||
abs_begin = data.get("abs_begin")
|
||||
|
@ -9,11 +9,6 @@
|
||||
|
||||
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
|
||||
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Département
|
||||
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
@ -21,15 +16,26 @@ from flask import request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
from app import db, log
|
||||
import app
|
||||
from app import db
|
||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement, FormSemestre
|
||||
from app.models import departements
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
def get_departement(dept_ident: str) -> Departement:
|
||||
"Le departement, par id ou acronyme. Erreur 404 si pas trouvé."
|
||||
try:
|
||||
dept_id = int(dept_ident)
|
||||
except ValueError:
|
||||
dept_id = None
|
||||
if dept_id is None:
|
||||
return Departement.query.filter_by(acronym=dept_ident).first_or_404()
|
||||
return Departement.query.get_or_404(dept_id)
|
||||
|
||||
|
||||
@bp.route("/departements")
|
||||
@ -38,7 +44,7 @@ from app.scodoc.sco_utils import json_error
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departements_list():
|
||||
"""Liste tous les départements."""
|
||||
"""Liste les départements"""
|
||||
return [dept.to_dict(with_dept_name=True) for dept in Departement.query]
|
||||
|
||||
|
||||
@ -48,7 +54,7 @@ def departements_list():
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departements_ids():
|
||||
"""Liste des ids de tous les départements."""
|
||||
"""Liste des ids de départements"""
|
||||
return [dept.id for dept in Departement.query]
|
||||
|
||||
|
||||
@ -57,12 +63,11 @@ def departements_ids():
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_by_acronym(acronym: str):
|
||||
def departement(acronym: str):
|
||||
"""
|
||||
Info sur un département. Accès par acronyme.
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronym": "TAPI",
|
||||
@ -71,7 +76,6 @@ def departement_by_acronym(acronym: str):
|
||||
"visible": true,
|
||||
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
||||
}
|
||||
```
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return dept.to_dict(with_dept_name=True)
|
||||
@ -98,15 +102,11 @@ def departement_by_id(dept_id: int):
|
||||
def departement_create():
|
||||
"""
|
||||
Création d'un département.
|
||||
Le content type doit être `application/json`.
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"acronym": str,
|
||||
"visible": bool,
|
||||
"visible":bool,
|
||||
}
|
||||
```
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
acronym = str(data.get("acronym", ""))
|
||||
@ -117,9 +117,6 @@ def departement_create():
|
||||
dept = departements.create_dept(acronym, visible=visible)
|
||||
except ScoValueError as exc:
|
||||
return json_error(500, exc.args[0] if exc.args else "")
|
||||
|
||||
log(f"departement_create {dept.acronym}")
|
||||
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@ -130,12 +127,10 @@ def departement_create():
|
||||
@as_json
|
||||
def departement_edit(acronym):
|
||||
"""
|
||||
Édition d'un département: seul le champ `visible` peut être modifié.
|
||||
|
||||
DATA
|
||||
----
|
||||
Edition d'un département: seul visible peut être modifié
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"visible": bool,
|
||||
"visible":bool,
|
||||
}
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
@ -147,7 +142,6 @@ def departement_edit(acronym):
|
||||
dept.visible = visible
|
||||
db.session.add(dept)
|
||||
db.session.commit()
|
||||
log(f"departement_edit {dept.acronym}")
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@ -157,13 +151,11 @@ def departement_edit(acronym):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def departement_delete(acronym):
|
||||
"""
|
||||
Suppression d'un département identifié par son acronyme.
|
||||
Suppression d'un département.
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
acronym = dept.acronym
|
||||
db.session.delete(dept)
|
||||
db.session.commit()
|
||||
log(f"departement_delete {acronym}")
|
||||
return {"OK": True}
|
||||
|
||||
|
||||
@ -172,16 +164,13 @@ def departement_delete(acronym):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_etudiants(acronym: str):
|
||||
def dept_etudiants(acronym: str):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département.
|
||||
Retourne la liste des étudiants d'un département
|
||||
|
||||
PARAMS
|
||||
------
|
||||
acronym : l'acronyme d'un département
|
||||
acronym: l'acronyme d'un département
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"civilite": "M",
|
||||
@ -196,7 +185,6 @@ def departement_etudiants(acronym: str):
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||
@ -207,7 +195,7 @@ def departement_etudiants(acronym: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_etudiants_by_id(dept_id: int):
|
||||
def dept_etudiants_by_id(dept_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département d'id donné.
|
||||
"""
|
||||
@ -220,8 +208,8 @@ def departement_etudiants_by_id(dept_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_formsemestres_ids(acronym: str):
|
||||
"""Liste des ids de tous les formsemestres du département."""
|
||||
def dept_formsemestres_ids(acronym: str):
|
||||
"""liste des ids formsemestre du département"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
@ -231,34 +219,57 @@ def departement_formsemestres_ids(acronym: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_formsemestres_ids_by_id(dept_id: int):
|
||||
"""Liste des ids de tous les formsemestres du département."""
|
||||
def dept_formsemestres_ids_by_id(dept_id: int):
|
||||
"""liste des ids formsemestre du département"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/formsemestres_courants")
|
||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_formsemestres_courants(acronym: str = "", dept_id: int | None = None):
|
||||
def dept_formsemestres_courants(acronym: str):
|
||||
"""
|
||||
Liste les formsemestres du département indiqué (par son acronyme ou son id)
|
||||
contenant la date courante, ou à défaut celle indiquée en argument
|
||||
(au format ISO).
|
||||
|
||||
QUERY
|
||||
-----
|
||||
date_courante:<string:date_courante>
|
||||
Liste des semestres actifs d'un département d'acronyme donné
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"titre": "master machine info",
|
||||
"gestion_semestrielle": false,
|
||||
"scodoc7_id": null,
|
||||
"date_debut": "01/09/2021",
|
||||
"bul_bgcolor": null,
|
||||
"date_fin": "15/12/2022",
|
||||
"resp_can_edit": false,
|
||||
"dept_id": 1,
|
||||
"etat": true,
|
||||
"resp_can_change_ens": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"ens_can_edit_eval": false,
|
||||
"formation_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"elt_sem_apo": null,
|
||||
"semestre_id": 1,
|
||||
"bul_hide_xml": false,
|
||||
"elt_annee_apo": null,
|
||||
"block_moyennes": false,
|
||||
"formsemestre_id": 1,
|
||||
"titre_num": "master machine info semestre 1",
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_fin_iso": "2022-12-15",
|
||||
"responsables": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
dept = (
|
||||
Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
if acronym
|
||||
else Departement.query.get_or_404(dept_id)
|
||||
)
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
date_courante = request.args.get("date_courante")
|
||||
date_courante = datetime.fromisoformat(date_courante) if date_courante else None
|
||||
return [
|
||||
@ -267,3 +278,29 @@ def departement_formsemestres_courants(acronym: str = "", dept_id: int | None =
|
||||
dept, date_courante
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def dept_formsemestres_courants_by_id(dept_id: int):
|
||||
"""
|
||||
Liste des semestres actifs d'un département d'id donné
|
||||
"""
|
||||
# Le département, spécifié par un id ou un acronyme
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
date_courante = request.args.get("date_courante")
|
||||
if date_courante:
|
||||
test_date = datetime.fromisoformat(date_courante)
|
||||
else:
|
||||
test_date = db.func.current_date()
|
||||
# Les semestres en cours de ce département
|
||||
formsemestres = FormSemestre.query.filter(
|
||||
FormSemestre.dept_id == dept.id,
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
|
||||
return [d.to_dict_api() for d in formsemestres]
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
API : accès aux étudiants
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Étudiants
|
||||
"""
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
@ -25,9 +21,8 @@ import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.but import bulletin_but_court
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
Admission,
|
||||
Departement,
|
||||
@ -42,8 +37,9 @@ from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||
|
||||
import app.scodoc.sco_photos as sco_photos
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# Un exemple:
|
||||
@ -93,20 +89,14 @@ def _get_etud_by_code(
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_courants(long: bool = False):
|
||||
def etudiants_courants(long=False):
|
||||
"""
|
||||
La liste des étudiants des semestres "courants".
|
||||
Considère tous les départements dans lesquels l'utilisateur a la
|
||||
permission `ScoView` (donc tous si le dépt. du rôle est `None`),
|
||||
et les formsemestres contenant la date courante,
|
||||
ou à défaut celle indiquée en argument (au format ISO).
|
||||
|
||||
QUERY
|
||||
-----
|
||||
date_courante:<string:date_courante>
|
||||
La liste des étudiants des semestres "courants" (tous départements)
|
||||
(date du jour comprise dans la période couverte par le sem.)
|
||||
dans lesquels l'utilisateur a la permission ScoView
|
||||
(donc tous si le dept du rôle est None).
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1234,
|
||||
@ -119,7 +109,6 @@ def etudiants_courants(long: bool = False):
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
En format "long": voir documentation.
|
||||
|
||||
@ -165,13 +154,10 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
|
||||
`etudid` est unique dans la base (tous départements).
|
||||
Les codes INE et NIP sont uniques au sein d'un département.
|
||||
Si plusieurs objets ont le même code, on ramène le plus récemment inscrit.
|
||||
"""
|
||||
@ -195,18 +181,11 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Retourne la photo de l'étudiant ou un placeholder si non existant.
|
||||
Le paramètre `size` peut prendre la valeur `small` (taille réduite, hauteur
|
||||
environ 90 pixels) ou `orig` (défaut, image de la taille originale).
|
||||
Retourne la photo de l'étudiant
|
||||
correspondant ou un placeholder si non existant.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
size:<string:size>
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
@ -236,7 +215,7 @@ def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = Non
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudChangeAdr)
|
||||
@as_json
|
||||
def etudiant_set_photo_image(etudid: int = None):
|
||||
def set_photo_image(etudid: int = None):
|
||||
"""Enregistre la photo de l'étudiant."""
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.EtudChangeAdr)
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
@ -279,12 +258,9 @@ def etudiant_set_photo_image(etudid: int = None):
|
||||
@as_json
|
||||
def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Info sur le ou les étudiants correspondants.
|
||||
|
||||
Comme `/etudiant` mais renvoie toujours une liste.
|
||||
|
||||
Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie
|
||||
toujours une liste.
|
||||
Si non trouvé, liste vide, pas d'erreur.
|
||||
|
||||
Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a
|
||||
été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
|
||||
"""
|
||||
@ -317,9 +293,8 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
"""Liste des étudiants dont le nom débute par `start`.
|
||||
|
||||
Si `start` fait moins de `min_len=3` caractères, liste vide.
|
||||
"""Liste des étudiants dont le nom débute par start.
|
||||
Si start fait moins de min_len=3 caractères, liste vide.
|
||||
La casse et les accents sont ignorés.
|
||||
"""
|
||||
if len(start) < min_len:
|
||||
@ -354,13 +329,13 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
@as_json
|
||||
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
|
||||
"""
|
||||
Liste des formsemestres qu'un étudiant a suivi, triés par ordre chronologique.
|
||||
Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique.
|
||||
Accès par etudid, nip ou ine.
|
||||
|
||||
Attention, si accès via NIP ou INE, les formsemestres peuvent être de départements
|
||||
Attention, si accès via NIP ou INE, les semestres peuvent être de départements
|
||||
différents (si l'étudiant a changé de département). L'id du département est `dept_id`.
|
||||
|
||||
Si accès par département, ne retourne que les formsemestres suivis dans le département.
|
||||
Si accès par département, ne retourne que les formsemestre suivis dans le département.
|
||||
"""
|
||||
if etudid is not None:
|
||||
q_etud = Identite.query.filter_by(id=etudid)
|
||||
@ -428,14 +403,13 @@ def bulletin(
|
||||
"""
|
||||
Retourne le bulletin d'un étudiant dans un formsemestre.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
code_type : "etudid", "nip" ou "ine"
|
||||
code : valeur du code INE, NIP ou etudid, selon code_type.
|
||||
version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
|
||||
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
||||
|
||||
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
"""
|
||||
if version == "pdf":
|
||||
version = "long"
|
||||
@ -489,13 +463,10 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
"""
|
||||
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"partition_id": 1,
|
||||
@ -520,7 +491,6 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
"group_name": "A"
|
||||
}
|
||||
]
|
||||
```
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -548,12 +518,9 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def etudiant_create(force=False):
|
||||
"""Création d'un nouvel étudiant.
|
||||
|
||||
"""Création d'un nouvel étudiant
|
||||
Si force, crée même si homonymie détectée.
|
||||
|
||||
L'étudiant créé n'est pas inscrit à un semestre.
|
||||
|
||||
Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -621,13 +588,7 @@ def etudiant_edit(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
):
|
||||
"""Édition des données étudiant (identité, admission, adresses).
|
||||
|
||||
PARAMS
|
||||
------
|
||||
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
|
||||
`code`: la valeur du code
|
||||
"""
|
||||
"""Edition des données étudiant (identité, admission, adresses)"""
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
@ -666,23 +627,7 @@ def etudiant_annotation(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
):
|
||||
"""Ajout d'une annotation sur un étudiant.
|
||||
|
||||
Renvoie l'annotation créée.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
|
||||
`code`: la valeur du code
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"comment" : string
|
||||
}
|
||||
```
|
||||
"""
|
||||
"""Ajout d'une annotation sur un étudiant"""
|
||||
if not current_user.has_permission(Permission.ViewEtudData):
|
||||
return json_error(403, "non autorisé (manque ViewEtudData)")
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
@ -719,13 +664,7 @@ def etudiant_annotation_delete(
|
||||
code_type: str = "etudid", code: str = None, annotation_id: int = None
|
||||
):
|
||||
"""
|
||||
Suppression d'une annotation. On spécifie l'étudiant et l'id de l'annotation.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
|
||||
`code`: la valeur du code
|
||||
`annotation_id` : id de l'annotation
|
||||
Suppression d'une annotation
|
||||
"""
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux évaluations
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Évaluations
|
||||
"""
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
@ -18,8 +14,7 @@ from flask_login import current_user, login_required
|
||||
import app
|
||||
from app import log, db
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
@ -36,28 +31,24 @@ import app.scodoc.sco_utils as scu
|
||||
def get_evaluation(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
'coefficient': 1.0,
|
||||
'date_debut': '2016-01-04T08:30:00',
|
||||
'date_fin': '2016-01-04T12:30:00',
|
||||
'description': 'TP Température',
|
||||
'evaluation_type': 0,
|
||||
'id': 15797,
|
||||
'moduleimpl_id': 1234,
|
||||
'note_max': 20.0,
|
||||
'numero': 3,
|
||||
'poids': {
|
||||
'UE1.1': 1.0,
|
||||
'UE1.2': 1.0,
|
||||
'UE1.3': 1.0
|
||||
},
|
||||
'publish_incomplete': False,
|
||||
'visibulletin': True
|
||||
}
|
||||
```
|
||||
'coefficient': 1.0,
|
||||
'date_debut': '2016-01-04T08:30:00',
|
||||
'date_fin': '2016-01-04T12:30:00',
|
||||
'description': 'TP NI9219 Température',
|
||||
'evaluation_type': 0,
|
||||
'id': 15797,
|
||||
'moduleimpl_id': 1234,
|
||||
'note_max': 20.0,
|
||||
'numero': 3,
|
||||
'poids': {
|
||||
'UE1.1': 1.0,
|
||||
'UE1.2': 1.0,
|
||||
'UE1.3': 1.0
|
||||
},
|
||||
'publish_incomplete': False,
|
||||
'visibulletin': True
|
||||
}
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -78,13 +69,11 @@ def get_evaluation(evaluation_id: int):
|
||||
@as_json
|
||||
def moduleimpl_evaluations(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne la liste des évaluations d'un moduleimpl.
|
||||
Retourne la liste des évaluations d'un moduleimpl
|
||||
|
||||
PARAMS
|
||||
------
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
|
||||
Exemple de résultat : voir `/evaluation`.
|
||||
Exemple de résultat : voir /evaluation
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
|
||||
@ -98,36 +87,30 @@ def moduleimpl_evaluations(moduleimpl_id: int):
|
||||
@as_json
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
Retourne la liste des notes de l'évaluation.
|
||||
Retourne la liste des notes de l'évaluation
|
||||
|
||||
PARAMS
|
||||
------
|
||||
evaluation_id : l'id de l'évaluation
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"11": {
|
||||
{
|
||||
"11": {
|
||||
"etudid": 11,
|
||||
"evaluation_id": 1,
|
||||
"value": 15.0,
|
||||
"note_max" : 20.0,
|
||||
"comment": "",
|
||||
"date": "2024-07-19T19:08:44+02:00",
|
||||
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
"12": {
|
||||
},
|
||||
"12": {
|
||||
"etudid": 12,
|
||||
"evaluation_id": 1,
|
||||
"value": "ABS",
|
||||
"note_max" : 20.0,
|
||||
"value": 12.0,
|
||||
"comment": "",
|
||||
"date": "2024-07-19T19:08:44+02:00",
|
||||
"date": "Wed, 20 Apr 2022 06:49:06 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -161,18 +144,13 @@ def evaluation_notes(evaluation_id: int):
|
||||
@as_json
|
||||
def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
|
||||
"""Écriture de notes dans une évaluation.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json",
|
||||
and contains:
|
||||
{
|
||||
'notes' : [ [etudid, value], ... ],
|
||||
'comment' : optional string
|
||||
}
|
||||
```
|
||||
|
||||
Résultat:
|
||||
|
||||
Result:
|
||||
- nb_changed: nombre de notes changées
|
||||
- nb_suppress: nombre de notes effacées
|
||||
- etudids_with_decision: liste des etudiants dont la note a changé
|
||||
@ -207,9 +185,8 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
|
||||
@as_json
|
||||
def evaluation_create(moduleimpl_id: int):
|
||||
"""Création d'une évaluation.
|
||||
|
||||
DATA
|
||||
----
|
||||
The request content type should be "application/json",
|
||||
and contains:
|
||||
{
|
||||
"description" : str,
|
||||
"evaluation_type" : int, // {0,1,2} default 0 (normale)
|
||||
@ -222,8 +199,7 @@ def evaluation_create(moduleimpl_id: int):
|
||||
"coefficient" : float, // si non spécifié, 1.0
|
||||
"poids" : { ue_id : poids } // optionnel
|
||||
}
|
||||
|
||||
Résultat: l'évaluation créée.
|
||||
Result: l'évaluation créée.
|
||||
"""
|
||||
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||
if not moduleimpl.can_edit_evaluation(current_user):
|
||||
@ -273,7 +249,7 @@ def evaluation_create(moduleimpl_id: int):
|
||||
@as_json
|
||||
def evaluation_delete(evaluation_id: int):
|
||||
"""Suppression d'une évaluation.
|
||||
Efface aussi toutes ses notes.
|
||||
Efface aussi toutes ses notes
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux formations
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Formations
|
||||
"""
|
||||
|
||||
from flask import flash, g, request
|
||||
@ -19,15 +15,12 @@ from flask_login import login_required
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
Formation,
|
||||
Module,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_formations
|
||||
@ -42,8 +35,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@as_json
|
||||
def formations():
|
||||
"""
|
||||
Retourne la liste de toutes les formations (tous départements,
|
||||
sauf si route départementale).
|
||||
Retourne la liste de toutes les formations (tous départements)
|
||||
"""
|
||||
query = Formation.query
|
||||
if g.scodoc_dept:
|
||||
@ -63,7 +55,7 @@ def formations_ids():
|
||||
Retourne la liste de toutes les id de formations
|
||||
(tous départements, ou du département indiqué dans la route)
|
||||
|
||||
Exemple de résultat : `[ 17, 99, 32 ]`.
|
||||
Exemple de résultat : [ 17, 99, 32 ]
|
||||
"""
|
||||
query = Formation.query
|
||||
if g.scodoc_dept:
|
||||
@ -79,26 +71,24 @@ def formations_ids():
|
||||
@as_json
|
||||
def formation_by_id(formation_id: int):
|
||||
"""
|
||||
La formation d'id donné.
|
||||
La formation d'id donné
|
||||
|
||||
formation_id : l'id d'une formation
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1
|
||||
}
|
||||
```
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1
|
||||
}
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -130,102 +120,97 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
"""
|
||||
Retourne la formation, avec UE, matières, modules
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formation_id : l'id d'une formation
|
||||
export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation.
|
||||
export_ids : True ou False, si l'on veut ou non exporter les ids
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1,
|
||||
"ue": [
|
||||
{
|
||||
"acronyme": "RT1.1",
|
||||
"numero": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"type": 0,
|
||||
"ue_code": "UCOD11",
|
||||
"ects": 12.0,
|
||||
"is_external": false,
|
||||
"code_apogee": "",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"reference": 1,
|
||||
"matiere": [
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1,
|
||||
"ue": [
|
||||
{
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"module": [
|
||||
"acronyme": "RT1.1",
|
||||
"numero": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"type": 0,
|
||||
"ue_code": "UCOD11",
|
||||
"ects": 12.0,
|
||||
"is_external": false,
|
||||
"code_apogee": "",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"reference": 1,
|
||||
"matiere": [
|
||||
{
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
||||
"code": "R101",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 2,
|
||||
"coefficients": [
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"module": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "12.0"
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
||||
"code": "R101",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 2,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "12.0"
|
||||
},
|
||||
{
|
||||
"ue_reference": "2",
|
||||
"coef": "4.0"
|
||||
},
|
||||
{
|
||||
"ue_reference": "3",
|
||||
"coef": "4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ue_reference": "2",
|
||||
"coef": "4.0"
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
|
||||
"abbrev": "Hygi\u00e8ne informatique",
|
||||
"code": "SAE11",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 3,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "16.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ue_reference": "3",
|
||||
"coef": "4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
|
||||
"abbrev": "Hygi\u00e8ne informatique",
|
||||
"code": "SAE11",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 3,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "16.0"
|
||||
}
|
||||
]
|
||||
...
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -248,8 +233,11 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
@as_json
|
||||
def referentiel_competences(formation_id: int):
|
||||
"""
|
||||
Retourne le référentiel de compétences de la formation
|
||||
ou null si pas de référentiel associé.
|
||||
Retourne le référentiel de compétences
|
||||
|
||||
formation_id : l'id d'une formation
|
||||
|
||||
return null si pas de référentiel associé.
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -260,22 +248,16 @@ def referentiel_competences(formation_id: int):
|
||||
return formation.referentiel_competence.to_dict()
|
||||
|
||||
|
||||
@bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
|
||||
@api_web_bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
|
||||
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_set_parcours(ue_id: int):
|
||||
def set_ue_parcours(ue_id: int):
|
||||
"""Associe UE et parcours BUT.
|
||||
|
||||
La liste des ids de parcours est passée en argument JSON.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ parcour_id1, parcour_id2, ... ]
|
||||
```
|
||||
JSON arg: [parcour_id1, parcour_id2, ...]
|
||||
"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
@ -288,7 +270,7 @@ def ue_set_parcours(ue_id: int):
|
||||
parcours = [
|
||||
ApcParcours.query.get_or_404(int(parcour_id)) for parcour_id in parcours_ids
|
||||
]
|
||||
log(f"ue_set_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
|
||||
log(f"set_ue_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
|
||||
ok, error_message = ue.set_parcours(parcours)
|
||||
if not ok:
|
||||
return json_error(404, error_message)
|
||||
@ -296,19 +278,19 @@ def ue_set_parcours(ue_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
|
||||
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
|
||||
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
||||
"""Associe l'UE au niveau de compétence."""
|
||||
def assoc_ue_niveau(ue_id: int, niveau_id: int):
|
||||
"""Associe l'UE au niveau de compétence"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -325,278 +307,32 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/desassoc_niveau",
|
||||
"/desassoc_ue_niveau/<int:ue_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/desassoc_niveau",
|
||||
"/desassoc_ue_niveau/<int:ue_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_desassoc_niveau(ue_id: int):
|
||||
def desassoc_ue_niveau(ue_id: int):
|
||||
"""Désassocie cette UE de son niveau de compétence
|
||||
(si elle n'est pas associée, ne fait rien).
|
||||
(si elle n'est pas associée, ne fait rien)
|
||||
"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
ok, error_message = ue.set_niveau_competence(None)
|
||||
if not ok:
|
||||
if g.scodoc_dept: # "usage web"
|
||||
flash(error_message, "error")
|
||||
return json_error(404, error_message)
|
||||
if g.scodoc_dept: # "usage web"
|
||||
ue.niveau_competence = None
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
ue.formation.invalidate_cached_sems()
|
||||
log(f"desassoc_ue_niveau: {ue}")
|
||||
if g.scodoc_dept:
|
||||
# "usage web"
|
||||
flash(f"UE {ue.acronyme} dé-associée")
|
||||
return {"status": 0}
|
||||
|
||||
|
||||
@bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
|
||||
@api_web_bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def get_ue(ue_id: int):
|
||||
"""Renvoie l'UE."""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
return ue.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@bp.route("/formation/module/<int:module_id>", methods=["GET"])
|
||||
@api_web_bp.route("/formation/module/<int:module_id>", methods=["GET"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formation_module_get(module_id: int):
|
||||
"""Renvoie le module."""
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
return module.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@bp.route("/formation/ue/set_code_apogee", methods=["POST"])
|
||||
@api_web_bp.route("/formation/ue/set_code_apogee", methods=["POST"])
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""):
|
||||
"""Change le code Apogée de l'UE.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si `ue_id` n'est pas spécifié, utilise l'argument oid du POST.
|
||||
Si `code_apogee` n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST.
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if ue_id is None:
|
||||
ue_id = request.form.get("oid")
|
||||
if ue_id is None:
|
||||
return json_error(404, "argument oid manquant")
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(f"API ue_set_code_apogee: ue_id={ue.id} code_apogee={code_apogee}")
|
||||
|
||||
ue.code_apogee = code_apogee
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""):
|
||||
"""Change le code Apogée du RCUE de l'UE.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si code_apogee n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST (utilisé par `jinplace.js`)
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(f"API ue_set_code_apogee_rcue: ue_id={ue.id} code_apogee={code_apogee}")
|
||||
|
||||
ue.code_apogee_rcue = code_apogee
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route("/formation/module/set_code_apogee", methods=["POST"])
|
||||
@api_web_bp.route("/formation/module/set_code_apogee", methods=["POST"])
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def formation_module_set_code_apogee(
|
||||
module_id: int | None = None, code_apogee: str = ""
|
||||
):
|
||||
"""Change le code Apogée du module.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si `module_id` n'est pas spécifié, utilise l'argument `oid` du POST.
|
||||
Si `code_apogee` n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST (utilisé par jinplace.js)
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if module_id is None:
|
||||
module_id = request.form.get("oid")
|
||||
if module_id is None:
|
||||
return json_error(404, "argument oid manquant")
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(
|
||||
f"API formation_module_set_code_apogee: module_id={module.id} code_apogee={code_apogee}"
|
||||
)
|
||||
|
||||
module.code_apogee = code_apogee
|
||||
db.session.add(module)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def formation_module_edit(module_id: int):
|
||||
"""Édition d'un module. Renvoie le module en json."""
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
module.from_dict(args)
|
||||
db.session.commit()
|
||||
db.session.refresh(module)
|
||||
log(f"API module_edit: module_id={module.id} args={args}")
|
||||
r = module.to_dict(convert_objects=True, with_parcours_ids=True)
|
||||
return r
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_edit(ue_id: int):
|
||||
"""Édition d'une UE. Renvoie l'UE en json."""
|
||||
ue = UniteEns.get_ue(ue_id)
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
ue.from_dict(args)
|
||||
db.session.commit()
|
||||
db.session.refresh(ue)
|
||||
log(f"API ue_edit: ue_id={ue.id} args={args}")
|
||||
r = ue.to_dict(convert_objects=True)
|
||||
return r
|
||||
|
@ -6,12 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux formsemestres
|
||||
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
FormSemestre
|
||||
|
||||
"""
|
||||
from operator import attrgetter, itemgetter
|
||||
|
||||
@ -20,10 +14,9 @@ from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
import sqlalchemy as sa
|
||||
import app
|
||||
from app import db, log
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.comp import res_sem
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
@ -61,37 +54,35 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
formsemestre_id : l'id du formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"block_moyennes": false,
|
||||
"bul_bgcolor": "white",
|
||||
"bul_hide_xml": false,
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_debut": "01/09/2021",
|
||||
"date_fin_iso": "2022-08-31",
|
||||
"date_fin": "31/08/2022",
|
||||
"dept_id": 1,
|
||||
"elt_annee_apo": null,
|
||||
"elt_passage_apo" : null,
|
||||
"elt_sem_apo": null,
|
||||
"ens_can_edit_eval": false,
|
||||
"etat": true,
|
||||
"formation_id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"gestion_semestrielle": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"resp_can_change_ens": true,
|
||||
"resp_can_edit": false,
|
||||
"responsables": [1, 99], // uids
|
||||
"scodoc7_id": null,
|
||||
"semestre_id": 1,
|
||||
"titre_formation" : "BUT GEA",
|
||||
"titre_num": "BUT GEA semestre 1",
|
||||
"titre": "BUT GEA",
|
||||
}
|
||||
```
|
||||
{
|
||||
"block_moyennes": false,
|
||||
"bul_bgcolor": "white",
|
||||
"bul_hide_xml": false,
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_debut": "01/09/2021",
|
||||
"date_fin_iso": "2022-08-31",
|
||||
"date_fin": "31/08/2022",
|
||||
"dept_id": 1,
|
||||
"elt_annee_apo": null,
|
||||
"elt_sem_apo": null,
|
||||
"ens_can_edit_eval": false,
|
||||
"etat": true,
|
||||
"formation_id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"gestion_semestrielle": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"resp_can_change_ens": true,
|
||||
"resp_can_edit": false,
|
||||
"responsables": [1, 99], // uids
|
||||
"scodoc7_id": null,
|
||||
"semestre_id": 1,
|
||||
"titre_formation" : "BUT GEA",
|
||||
"titre_num": "BUT GEA semestre 1",
|
||||
"titre": "BUT GEA",
|
||||
}
|
||||
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -108,28 +99,15 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
@as_json
|
||||
def formsemestres_query():
|
||||
"""
|
||||
Retourne les formsemestres filtrés par étape Apogée ou année scolaire
|
||||
ou département (acronyme ou id) ou état ou code étudiant.
|
||||
Retourne les formsemestres filtrés par
|
||||
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etape_apo : un code étape apogée
|
||||
annee_scolaire : année de début de l'année scolaire
|
||||
dept_acronym : acronyme du département (eg "RT")
|
||||
dept_id : id du département
|
||||
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
||||
etat: 0 si verrouillé, 1 sinon
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etape_apo:<string:etape_apo>
|
||||
annee_scolaire:<string:annee_scolaire>
|
||||
dept_acronym:<string:dept_acronym>
|
||||
dept_id:<int:dept_id>
|
||||
etat:<int:etat>
|
||||
nip:<string:nip>
|
||||
ine:<string:ine>
|
||||
|
||||
"""
|
||||
etape_apo = request.args.get("etape_apo")
|
||||
annee_scolaire = request.args.get("annee_scolaire")
|
||||
@ -199,36 +177,7 @@ def formsemestres_query():
|
||||
@permission_required(Permission.EditFormSemestre)
|
||||
@as_json
|
||||
def formsemestre_edit(formsemestre_id: int):
|
||||
"""Modifie les champs d'un formsemestre.
|
||||
|
||||
On peut spécifier un ou plusieurs champs.
|
||||
|
||||
DATA
|
||||
---
|
||||
```json
|
||||
{
|
||||
"semestre_id" : string,
|
||||
"titre" : string,
|
||||
"date_debut" : date iso,
|
||||
"date_fin" : date iso,
|
||||
"edt_id" : string,
|
||||
"etat" : string,
|
||||
"modalite" : string,
|
||||
"gestion_compensation" : bool,
|
||||
"bul_hide_xml" : bool,
|
||||
"block_moyennes" : bool,
|
||||
"block_moyenne_generale" : bool,
|
||||
"mode_calcul_moyennes" : string,
|
||||
"gestion_semestrielle" : string,
|
||||
"bul_bgcolor" : string,
|
||||
"resp_can_edit" : bool,
|
||||
"resp_can_change_ens" : bool,
|
||||
"ens_can_edit_eval" : bool,
|
||||
"elt_sem_apo" : string,
|
||||
"elt_annee_apo : string,
|
||||
}
|
||||
```
|
||||
"""
|
||||
"""Modifie les champs d'un formsemestre."""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
editable_keys = {
|
||||
@ -260,154 +209,6 @@ def formsemestre_edit(formsemestre_id: int):
|
||||
return formsemestre.to_dict_api()
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/set_etapes", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/apo/set_etapes", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditApogee)
|
||||
def formsemestre_set_apo_etapes():
|
||||
"""Change les codes étapes du semestre indiqué.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur un semestre verrouillé
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
oid : int, le formsemestre_id
|
||||
value : string, eg "V1RT, V1RT2", codes séparés par des virgules
|
||||
}
|
||||
"""
|
||||
formsemestre_id = int(request.form.get("oid"))
|
||||
etapes_apo_str = request.form.get("value")
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
current_etapes = {e.etape_apo for e in formsemestre.etapes}
|
||||
new_etapes = {s.strip() for s in etapes_apo_str.split(",")}
|
||||
|
||||
if new_etapes != current_etapes:
|
||||
formsemestre.etapes = []
|
||||
for etape_apo in new_etapes:
|
||||
etape = FormSemestreEtape(
|
||||
formsemestre_id=formsemestre_id, etape_apo=etape_apo
|
||||
)
|
||||
formsemestre.etapes.append(etape)
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
log(
|
||||
f"""API formsemestre_set_apo_etapes: formsemestre_id={
|
||||
formsemestre.id} code_apogee={etapes_apo_str}"""
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/set_elt_sem", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/apo/set_elt_sem", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditApogee)
|
||||
def formsemestre_set_elt_sem_apo():
|
||||
"""Change les codes étapes du semestre indiqué.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur un semestre verrouillé.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
oid : int, le formsemestre_id
|
||||
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
||||
}
|
||||
```
|
||||
"""
|
||||
oid = int(request.form.get("oid"))
|
||||
value = (request.form.get("value") or "").strip()
|
||||
formsemestre = FormSemestre.get_formsemestre(oid)
|
||||
if value != formsemestre.elt_sem_apo:
|
||||
formsemestre.elt_sem_apo = value
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
log(
|
||||
f"""API formsemestre_set_elt_sem_apo: formsemestre_id={
|
||||
formsemestre.id} code_apogee={value}"""
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/set_elt_annee", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/apo/set_elt_annee", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditApogee)
|
||||
def formsemestre_set_elt_annee_apo():
|
||||
"""Change les codes étapes du semestre indiqué (par le champ oid).
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur un semestre verrouillé.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
oid : int, le formsemestre_id
|
||||
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
||||
}
|
||||
```
|
||||
"""
|
||||
oid = int(request.form.get("oid"))
|
||||
value = (request.form.get("value") or "").strip()
|
||||
formsemestre = FormSemestre.get_formsemestre(oid)
|
||||
if value != formsemestre.elt_annee_apo:
|
||||
formsemestre.elt_annee_apo = value
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
log(
|
||||
f"""API formsemestre_set_elt_annee_apo: formsemestre_id={
|
||||
formsemestre.id} code_apogee={value}"""
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/set_elt_passage", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/apo/set_elt_passage", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditApogee)
|
||||
def formsemestre_set_elt_passage_apo():
|
||||
"""Change les codes apogée de passage du semestre indiqué (par le champ oid).
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur un semestre verrouillé.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
oid : int, le formsemestre_id
|
||||
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
||||
}
|
||||
```
|
||||
"""
|
||||
oid = int(request.form.get("oid"))
|
||||
value = (request.form.get("value") or "").strip()
|
||||
formsemestre = FormSemestre.get_formsemestre(oid)
|
||||
if value != formsemestre.elt_annee_apo:
|
||||
formsemestre.elt_passage_apo = value
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
log(
|
||||
f"""API formsemestre_set_elt_passage_apo: formsemestre_id={
|
||||
formsemestre.id} code_apogee={value}"""
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@ -418,12 +219,9 @@ def formsemestre_set_elt_passage_apo():
|
||||
@as_json
|
||||
def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
"""
|
||||
Retourne les bulletins d'un formsemestre.
|
||||
Retourne les bulletins d'un formsemestre donné
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : int
|
||||
version : string ("long", "short", "selectedevals")
|
||||
formsemestre_id : l'id d'un formesemestre
|
||||
|
||||
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
"""
|
||||
@ -455,67 +253,66 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
"""
|
||||
Retourne la liste des UEs, ressources et SAEs d'un semestre
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"ues": [
|
||||
{
|
||||
"type": 0,
|
||||
"formation_id": 1,
|
||||
"ue_code": "UCOD11",
|
||||
"id": 1,
|
||||
"ects": 12.0,
|
||||
"acronyme": "RT1.1",
|
||||
"is_external": false,
|
||||
"numero": 1,
|
||||
"code_apogee": "",
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"ue_id": 1
|
||||
},
|
||||
...
|
||||
],
|
||||
"ressources": [
|
||||
{
|
||||
"ens": [ 10, 18 ],
|
||||
"formsemestre_id": 1,
|
||||
"id": 15,
|
||||
"module": {
|
||||
"abbrev": "Programmer",
|
||||
"code": "SAE15",
|
||||
"code_apogee": "V7GOP",
|
||||
"coefficient": 1.0,
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"id": 15,
|
||||
"matiere_id": 3,
|
||||
"module_id": 15,
|
||||
"module_type": 3,
|
||||
"numero": 50,
|
||||
"semestre_id": 1,
|
||||
"titre": "Programmer en Python",
|
||||
"ue_id": 3
|
||||
},
|
||||
"module_id": 15,
|
||||
"moduleimpl_id": 15,
|
||||
"responsable_id": 2
|
||||
},
|
||||
...
|
||||
],
|
||||
"saes": [
|
||||
{
|
||||
"ues": [
|
||||
{
|
||||
"type": 0,
|
||||
"formation_id": 1,
|
||||
"ue_code": "UCOD11",
|
||||
"id": 1,
|
||||
"ects": 12.0,
|
||||
"acronyme": "RT1.1",
|
||||
"is_external": false,
|
||||
"numero": 1,
|
||||
"code_apogee": "",
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"ue_id": 1
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
],
|
||||
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
|
||||
}
|
||||
```
|
||||
],
|
||||
"ressources": [
|
||||
{
|
||||
"ens": [ 10, 18 ],
|
||||
"formsemestre_id": 1,
|
||||
"id": 15,
|
||||
"module": {
|
||||
"abbrev": "Programmer",
|
||||
"code": "SAE15",
|
||||
"code_apogee": "V7GOP",
|
||||
"coefficient": 1.0,
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"id": 15,
|
||||
"matiere_id": 3,
|
||||
"module_id": 15,
|
||||
"module_type": 3,
|
||||
"numero": 50,
|
||||
"semestre_id": 1,
|
||||
"titre": "Programmer en Python",
|
||||
"ue_id": 3
|
||||
},
|
||||
"module_id": 15,
|
||||
"moduleimpl_id": 15,
|
||||
"responsable_id": 2
|
||||
},
|
||||
...
|
||||
],
|
||||
"saes": [
|
||||
{
|
||||
...
|
||||
},
|
||||
...
|
||||
],
|
||||
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
|
||||
}
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -579,16 +376,7 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
def formsemestre_etudiants(
|
||||
formsemestre_id: int, with_query: bool = False, long: bool = False
|
||||
):
|
||||
"""Étudiants d'un formsemestre.
|
||||
|
||||
Si l'état est spécifié, ne renvoie que les inscrits (`I`), les
|
||||
démissionnaires (`D`) ou les défaillants (`DEF`)
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etat:<string:etat>
|
||||
|
||||
"""
|
||||
"""Étudiants d'un formsemestre."""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -630,13 +418,13 @@ def formsemestre_etudiants(
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
def etat_evals(formsemestre_id: int):
|
||||
"""
|
||||
Informations sur l'état des évaluations d'un formsemestre.
|
||||
|
||||
Exemple de résultat :
|
||||
formsemestre_id : l'id d'un semestre
|
||||
|
||||
```json
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"id": 1, // moduleimpl_id
|
||||
@ -664,9 +452,11 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
]
|
||||
},
|
||||
]
|
||||
```
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
@ -739,16 +529,8 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_resultat(formsemestre_id: int):
|
||||
"""Tableau récapitulatif des résultats.
|
||||
|
||||
"""Tableau récapitulatif des résultats
|
||||
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
|
||||
|
||||
Si `format=raw`, ne converti pas les valeurs.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
format:<string:format>
|
||||
|
||||
"""
|
||||
format_spec = request.args.get("format", None)
|
||||
if format_spec is not None and format_spec != "raw":
|
||||
@ -788,14 +570,14 @@ def formsemestre_resultat(formsemestre_id: int):
|
||||
return rows
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/groups_get_auto_assignment")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/groups_get_auto_assignment")
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def groups_get_auto_assignment(formsemestre_id: int):
|
||||
"""Rend les données stockées par `groups_save_auto_assignment`."""
|
||||
def get_groups_auto_assignment(formsemestre_id: int):
|
||||
"""rend les données"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -806,27 +588,22 @@ def groups_get_auto_assignment(formsemestre_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/groups_save_auto_assignment", methods=["POST"]
|
||||
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/groups_save_auto_assignment", methods=["POST"]
|
||||
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def groups_save_auto_assignment(formsemestre_id: int):
|
||||
"""Enregistre les données, associées à ce formsemestre.
|
||||
Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs.
|
||||
"""
|
||||
def save_groups_auto_assignment(formsemestre_id: int):
|
||||
"""enregistre les données"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
|
||||
if not formsemestre.can_change_groups():
|
||||
return json_error(403, "non autorisé (can_change_groups)")
|
||||
|
||||
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
|
||||
return json_error(413, "data too large")
|
||||
formsemestre.groups_auto_assignment_data = request.data
|
||||
@ -841,16 +618,11 @@ def groups_save_auto_assignment(formsemestre_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_edt(formsemestre_id: int):
|
||||
"""L'emploi du temps du semestre.
|
||||
|
||||
"""l'emploi du temps du semestre.
|
||||
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
||||
|
||||
Expérimental, ne pas utiliser hors ScoDoc.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
group_ids : string (optionnel) filtre sur les groupes ScoDoc.
|
||||
show_modules_titles: show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
||||
group_ids permet de filtrer sur les groupes ScoDoc.
|
||||
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
|
@ -5,12 +5,7 @@
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions.
|
||||
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Jury
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
|
||||
"""
|
||||
|
||||
import datetime
|
||||
@ -22,8 +17,7 @@ from flask_login import current_user, login_required
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.but import jury_but_results
|
||||
from app.models import (
|
||||
@ -38,7 +32,6 @@ from app.models import (
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
UniteEns,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_cache
|
||||
@ -69,7 +62,7 @@ def decisions_jury(formsemestre_id: int):
|
||||
raise ScoException("non implemente")
|
||||
|
||||
|
||||
def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
def _news_delete_jury_etud(etud: Identite):
|
||||
"génère news sur effacement décision"
|
||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||
url = url_for(
|
||||
@ -78,7 +71,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=etud.id,
|
||||
text=f"""Suppression décision jury {detail} pour <a href="{url}">{etud.nomprenom}</a>""",
|
||||
text=f"""Suppression décision jury pour <a href="{url}">{etud.nomprenom}</a>""",
|
||||
url=url,
|
||||
)
|
||||
|
||||
@ -96,7 +89,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation d'UE."
|
||||
"Efface cette validation"
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
@ -113,7 +106,7 @@ def validation_ue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_formsemestre_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de semestre."
|
||||
"Efface cette validation"
|
||||
# c'est la même chose (formations classiques)
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
@ -165,7 +158,7 @@ def _validation_ue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
"Efface cette autorisation d'inscription."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
@ -194,12 +187,8 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
@as_json
|
||||
def validation_rcue_record(etudid: int):
|
||||
"""Enregistre une validation de RCUE.
|
||||
|
||||
Si une validation existe déjà pour ce RCUE, la remplace.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"code" : str,
|
||||
"ue1_id" : int,
|
||||
@ -209,7 +198,6 @@ def validation_rcue_record(etudid: int):
|
||||
"date" : date_iso, // si non spécifié, now()
|
||||
"parcours_id" :int,
|
||||
}
|
||||
```
|
||||
"""
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
@ -301,12 +289,13 @@ def validation_rcue_record(etudid: int):
|
||||
db.session.add(validation)
|
||||
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="validation_rcue_record",
|
||||
etudid=etudid,
|
||||
msg=f"Enregistrement {validation}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"{operation} {validation}")
|
||||
return validation.to_dict()
|
||||
|
||||
@ -324,18 +313,18 @@ def validation_rcue_record(etudid: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de RCUE."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationRCUE.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"delete validation_ue_delete: etuid={etudid} {validation}")
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail="UE")
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@ -352,45 +341,16 @@ def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_annee_but_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation d'année BUT."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
ordre = validation.ordre
|
||||
log(f"delete validation_annee_but: etuid={etudid} {validation}")
|
||||
log(f"validation_annee_but: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail=f"année BUT{ordre}")
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_dut120/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_dut120/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_dut120_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de DUT120."
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ValidationDUT120.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"delete validation_dut120: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail="diplôme DUT120")
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
@ -3,8 +3,8 @@
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Justificatifs"""
|
||||
|
||||
"""ScoDoc 9 API : Justificatifs
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask_json import as_json
|
||||
@ -19,8 +19,7 @@ from app import db, set_sco_dept
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
|
||||
from app.models.assiduites import (
|
||||
get_formsemestre_from_data,
|
||||
@ -38,11 +37,9 @@ from app.scodoc.sco_groups import get_group_members
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatif(justif_id: int = None):
|
||||
"""Retourne un objet justificatif à partir de son id.
|
||||
"""Retourne un objet justificatif à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"justif_id": 1,
|
||||
"etudid": 2,
|
||||
@ -54,11 +51,6 @@ def justificatif(justif_id: int = None):
|
||||
"entry_date": "2022-10-31T08:00+01:00",
|
||||
"user_id": 1 or null,
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1;
|
||||
|
||||
"""
|
||||
|
||||
@ -99,32 +91,28 @@ def justificatif(justif_id: int = None):
|
||||
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /justificatifs/<int:etudid>
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
order:<bool:order>
|
||||
courant:<bool:courant>
|
||||
group_id:<int:group_id>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur du justificatif
|
||||
date_debut:date de début du justificatif (supérieur ou égal)
|
||||
date_fin:date de fin du justificatif (inférieur ou égal)
|
||||
etat:etat du justificatif → valide, non_valide, attente, modifie
|
||||
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
|
||||
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
|
||||
group_id:<int:group_id>
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatifs/1;
|
||||
/justificatifs/1/query?etat=attente;
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /justificatifs/<int:etudid>/query?
|
||||
|
||||
Les différents filtres :
|
||||
Etat (etat du justificatif -> validé, non validé, modifé, en attente):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=validé,modifié
|
||||
Date debut
|
||||
(date de début du justificatif, sont affichés les justificatifs
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin du justificatif, sont affichés les justificatifs
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
user_id (l'id de l'auteur du justificatif)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
@ -166,32 +154,6 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||
"""
|
||||
Renvoie tous les justificatifs d'un département
|
||||
(en ajoutant un champ "formsemestre" si possible)
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
order:<bool:order>
|
||||
courant:<bool:courant>
|
||||
group_id:<int:group_id>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur du justificatif
|
||||
date_debut:date de début du justificatif (supérieur ou égal)
|
||||
date_fin:date de fin du justificatif (inférieur ou égal)
|
||||
etat:etat du justificatif → valide, non_valide, attente, modifie
|
||||
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
|
||||
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
|
||||
group_id:<int:group_id>
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatifs/dept/1;
|
||||
|
||||
"""
|
||||
|
||||
# Récupération du département et des étudiants du département
|
||||
@ -263,34 +225,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
"""Retourne tous les justificatifs du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
order:<bool:order>
|
||||
courant:<bool:courant>
|
||||
group_id:<int:group_id>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur du justificatif
|
||||
date_debut:date de début du justificatif (supérieur ou égal)
|
||||
date_fin:date de fin du justificatif (inférieur ou égal)
|
||||
etat:etat du justificatif → valide, non_valide, attente, modifie
|
||||
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
|
||||
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
|
||||
group_id:<int:group_id>
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatifs/formsemestre/1;
|
||||
|
||||
"""
|
||||
"""Retourne tous les justificatifs du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre
|
||||
formsemestre: FormSemestre = None
|
||||
@ -338,10 +273,7 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
def justif_create(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Création d'un justificatif pour l'étudiant (etudid)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -356,10 +288,6 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/create;[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""attente""}]
|
||||
|
||||
"""
|
||||
|
||||
@ -413,10 +341,6 @@ def _create_one(
|
||||
errors.append("param 'etat': invalide")
|
||||
|
||||
etat: scu.EtatJustificatif = scu.EtatJustificatif.get(etat)
|
||||
if etat != scu.EtatJustificatif.ATTENTE and not current_user.has_permission(
|
||||
Permission.JustifValidate
|
||||
):
|
||||
errors.append("param 'etat': non autorisé (Permission.JustifValidate)")
|
||||
|
||||
# cas 2 : date_debut
|
||||
date_debut: str = data.get("date_debut", None)
|
||||
@ -490,23 +414,14 @@ def _create_one(
|
||||
def justif_edit(justif_id: int):
|
||||
"""
|
||||
Edition d'un justificatif à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"etat"?: str,
|
||||
"raison"?: str
|
||||
"date_debut"?: str
|
||||
"date_fin"?: str
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/edit;{""etat"":""valide""}
|
||||
/justificatif/1/edit;{""raison"":""MEDIC""}
|
||||
|
||||
"""
|
||||
|
||||
# Récupération du justificatif à modifier
|
||||
@ -525,10 +440,7 @@ def justif_edit(justif_id: int):
|
||||
if etat is None:
|
||||
errors.append("param 'etat': invalide")
|
||||
else:
|
||||
if current_user.has_permission(Permission.JustifValidate):
|
||||
justificatif_unique.etat = etat
|
||||
else:
|
||||
errors.append("param 'etat': non autorisé (Permission.JustifValidate)")
|
||||
justificatif_unique.etat = etat
|
||||
|
||||
# Cas 2 : raison
|
||||
raison: str = data.get("raison", False)
|
||||
@ -581,12 +493,13 @@ def justif_edit(justif_id: int):
|
||||
# Mise à jour du justificatif
|
||||
justificatif_unique.dejustifier_assiduites()
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
Scolog.logdb(
|
||||
method="edit_justificatif",
|
||||
etudid=justificatif_unique.etudiant.id,
|
||||
msg=f"justificatif modif: {justificatif_unique}",
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# Génération du dictionnaire de retour
|
||||
# La couverture correspond
|
||||
@ -613,18 +526,13 @@ def justif_delete():
|
||||
"""
|
||||
Suppression d'un justificatif à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<justif_id:int>,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/delete;[2, 2, 3]
|
||||
|
||||
"""
|
||||
|
||||
@ -679,11 +587,6 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
||||
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||
justificatif_unique.dejustifier_assiduites()
|
||||
Scolog.logdb(
|
||||
method="justificatif/delete",
|
||||
etudid=justificatif_unique.etudiant.id,
|
||||
msg="suppression justificatif",
|
||||
)
|
||||
# On supprime le justificatif
|
||||
db.session.delete(justificatif_unique)
|
||||
|
||||
@ -700,8 +603,6 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
||||
def justif_import(justif_id: int = None):
|
||||
"""
|
||||
Importation d'un fichier (création d'archive)
|
||||
|
||||
> Procédure d'importation de fichier : [importer un justificatif](FichiersJustificatifs.md#importer-un-fichier)
|
||||
"""
|
||||
|
||||
# On vérifie qu'un fichier a bien été envoyé
|
||||
@ -753,8 +654,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
|
||||
"""
|
||||
Retourne un fichier d'une archive d'un justificatif.
|
||||
La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif)
|
||||
|
||||
> Procédure de téléchargement de fichier : [télécharger un justificatif](FichiersJustificatifs.md#télécharger-un-fichier)
|
||||
"""
|
||||
# On récupère le justificatif concerné
|
||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
||||
@ -792,20 +691,14 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
|
||||
def justif_remove(justif_id: int = None):
|
||||
"""
|
||||
Supression d'un fichier ou d'une archive
|
||||
|
||||
> Procédure de suppression de fichier : [supprimer un justificatif](FichiersJustificatifs.md#supprimer-un-fichier)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"remove": <"all"/"list">,
|
||||
"remove": <"all"/"list">
|
||||
|
||||
"filenames"?: [
|
||||
<filename:str>,
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
# On récupère le dictionnaire
|
||||
@ -871,11 +764,6 @@ def justif_remove(justif_id: int = None):
|
||||
def justif_list(justif_id: int = None):
|
||||
"""
|
||||
Liste les fichiers du justificatif
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/list;
|
||||
|
||||
"""
|
||||
|
||||
# Récupération du justificatif concerné
|
||||
@ -918,11 +806,6 @@ def justif_list(justif_id: int = None):
|
||||
def justif_justifies(justif_id: int = None):
|
||||
"""
|
||||
Liste assiduite_id justifiées par le justificatif
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/justifies;
|
||||
|
||||
"""
|
||||
|
||||
# On récupère le justificatif concerné
|
||||
|
@ -34,13 +34,11 @@ from flask import Response, send_file
|
||||
from flask_json import as_json
|
||||
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import Departement
|
||||
from app.scodoc.sco_logos import list_logos, find_logo
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
# Note: l'API logos n'est accessible qu'en mode global (avec jeton, sans dept)
|
||||
|
||||
@ -49,8 +47,8 @@ from app.scodoc.sco_utils import json_error
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_list_globals():
|
||||
"""Liste des noms des logos définis pour le site ScoDoc."""
|
||||
def api_get_glob_logos():
|
||||
"""Liste tous les logos"""
|
||||
logos = list_logos()[None]
|
||||
return list(logos.keys())
|
||||
|
||||
@ -58,12 +56,7 @@ def logo_list_globals():
|
||||
@bp.route("/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_global(logoname):
|
||||
"""Renvoie le logo global de nom donné.
|
||||
|
||||
L'image est au format png ou jpg; le format retourné dépend du format sous lequel
|
||||
l'image a été initialement enregistrée.
|
||||
"""
|
||||
def api_get_glob_logo(logoname):
|
||||
logo = find_logo(logoname=logoname)
|
||||
if logo is None:
|
||||
return json_error(404, message="logo not found")
|
||||
@ -84,10 +77,7 @@ def _core_get_logos(dept_id) -> list:
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_get_local_by_acronym(departement):
|
||||
"""Liste des noms des logos définis pour le département
|
||||
désigné par son acronyme.
|
||||
"""
|
||||
def api_get_local_logos_by_acronym(departement):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
return _core_get_logos(dept_id)
|
||||
|
||||
@ -96,10 +86,7 @@ def logo_get_local_by_acronym(departement):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_get_local_by_id(dept_id):
|
||||
"""Liste des noms des logos définis pour le département
|
||||
désigné par son id.
|
||||
"""
|
||||
def api_get_local_logos_by_id(dept_id):
|
||||
return _core_get_logos(dept_id)
|
||||
|
||||
|
||||
@ -118,13 +105,7 @@ def _core_get_logo(dept_id, logoname) -> Response:
|
||||
@bp.route("/departement/<string:departement>/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_local_dept_by_acronym(departement, logoname):
|
||||
"""Le logo: image (format png ou jpg).
|
||||
|
||||
**Exemple d'utilisation:**
|
||||
|
||||
* `/ScoDoc/api/departement/MMI/logo/header`
|
||||
"""
|
||||
def api_get_local_logo_dept_by_acronym(departement, logoname):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
return _core_get_logo(dept_id, logoname)
|
||||
|
||||
@ -132,11 +113,5 @@ def logo_get_local_dept_by_acronym(departement, logoname):
|
||||
@bp.route("/departement/id/<int:dept_id>/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_local_dept_by_id(dept_id, logoname):
|
||||
"""Le logo: image (format png ou jpg).
|
||||
|
||||
**Exemple d'utilisation:**
|
||||
|
||||
* `/ScoDoc/api/departement/id/3/logo/header`
|
||||
"""
|
||||
def api_get_local_logo_dept_by_id(dept_id, logoname):
|
||||
return _core_get_logo(dept_id, logoname)
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux moduleimpl
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
ModuleImpl
|
||||
"""
|
||||
|
||||
from flask_json import as_json
|
||||
@ -17,8 +13,7 @@ from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc import sco_liste_notes
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -32,43 +27,38 @@ from app.scodoc.sco_permissions import Permission
|
||||
@as_json
|
||||
def moduleimpl(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne le moduleimpl.
|
||||
Retourne un moduleimpl en fonction de son id
|
||||
|
||||
PARAMS
|
||||
------
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"module_id": 1,
|
||||
"responsable_id": 2,
|
||||
"moduleimpl_id": 1,
|
||||
"ens": [],
|
||||
"module": {
|
||||
"heures_tp": 0,
|
||||
"code_apogee": "",
|
||||
"titre": "Initiation aux réseaux informatiques",
|
||||
"coefficient": 1,
|
||||
"module_type": 2,
|
||||
{
|
||||
"id": 1,
|
||||
"ects": null,
|
||||
"abbrev": "Init aux réseaux informatiques",
|
||||
"ue_id": 1,
|
||||
"code": "R101",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0,
|
||||
"matiere_id": 1,
|
||||
"heures_td": 0,
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"module_id": 1
|
||||
"formsemestre_id": 1,
|
||||
"module_id": 1,
|
||||
"responsable_id": 2,
|
||||
"moduleimpl_id": 1,
|
||||
"ens": [],
|
||||
"module": {
|
||||
"heures_tp": 0,
|
||||
"code_apogee": "",
|
||||
"titre": "Initiation aux réseaux informatiques",
|
||||
"coefficient": 1,
|
||||
"module_type": 2,
|
||||
"id": 1,
|
||||
"ects": null,
|
||||
"abbrev": "Init aux réseaux informatiques",
|
||||
"ue_id": 1,
|
||||
"code": "R101",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0,
|
||||
"matiere_id": 1,
|
||||
"heures_td": 0,
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"module_id": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return modimpl.to_dict(convert_objects=True)
|
||||
@ -81,20 +71,16 @@ def moduleimpl(moduleimpl_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def moduleimpl_inscriptions(moduleimpl_id: int):
|
||||
"""Liste des inscriptions à ce moduleimpl.
|
||||
|
||||
"""Liste des inscriptions à ce moduleimpl
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"etudid": 666,
|
||||
"moduleimpl_id": 1234,
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"etudid": 666,
|
||||
"moduleimpl_id": 1234,
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return [i.to_dict() for i in modimpl.inscriptions]
|
||||
@ -106,26 +92,22 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def moduleimpl_notes(moduleimpl_id: int):
|
||||
"""Liste des notes dans ce moduleimpl.
|
||||
|
||||
"""Liste des notes dans ce moduleimpl
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"etudid": 17776, // code de l'étudiant
|
||||
"nom": "DUPONT",
|
||||
"prenom": "Luz",
|
||||
"38411": 16.0, // Note dans l'évaluation d'id 38411
|
||||
"38410": 15.0,
|
||||
"moymod": 15.5, // Moyenne INDICATIVE module
|
||||
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
|
||||
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
|
||||
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
[
|
||||
{
|
||||
"etudid": 17776, // code de l'étudiant
|
||||
"nom": "DUPONT",
|
||||
"prenom": "Luz",
|
||||
"38411": 16.0, // Note dans l'évaluation d'id 38411
|
||||
"38410": 15.0,
|
||||
"moymod": 15.5, // Moyenne INDICATIVE module
|
||||
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
|
||||
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
|
||||
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
app.set_sco_dept(modimpl.formsemestre.departement.acronym)
|
||||
|
@ -6,11 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : partitions
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Groupes et Partitions
|
||||
|
||||
"""
|
||||
from operator import attrgetter
|
||||
|
||||
@ -23,8 +18,7 @@ from sqlalchemy.exc import IntegrityError
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.models import GroupDescr, Partition, Scolog
|
||||
@ -46,8 +40,7 @@ def partition_info(partition_id: int):
|
||||
"""Info sur une partition.
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
```
|
||||
{
|
||||
'bul_show_rank': False,
|
||||
'formsemestre_id': 39,
|
||||
@ -77,11 +70,10 @@ def partition_info(partition_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_partitions(formsemestre_id: int):
|
||||
"""Liste de toutes les partitions d'un formsemestre.
|
||||
"""Liste de toutes les partitions d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
```json
|
||||
{
|
||||
partition_id : {
|
||||
"bul_show_rank": False,
|
||||
@ -95,7 +87,7 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -115,18 +107,13 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_etudiants(group_id: int):
|
||||
def etud_in_group(group_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants dans un groupe
|
||||
(inscrits au groupe et inscrits au semestre).
|
||||
|
||||
PARAMS
|
||||
------
|
||||
group_id : l'id d'un groupe
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
'civilite': 'M',
|
||||
@ -139,7 +126,6 @@ def group_etudiants(group_id: int):
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
@ -164,14 +150,8 @@ def group_etudiants(group_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_etudiants_query(group_id: int):
|
||||
"""Étudiants du groupe, filtrés par état (aucun, `I`, `D`, `DEF`)
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etat : string
|
||||
|
||||
"""
|
||||
def etud_in_group_query(group_id: int):
|
||||
"""Étudiants du groupe, filtrés par état"""
|
||||
etat = request.args.get("etat")
|
||||
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
||||
return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
|
||||
@ -198,8 +178,8 @@ def group_etudiants_query(group_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_set_etudiant(group_id: int, etudid: int):
|
||||
"""Affecte l'étudiant au groupe indiqué."""
|
||||
def set_etud_group(etudid: int, group_id: int):
|
||||
"""Affecte l'étudiant au groupe indiqué"""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
@ -261,8 +241,7 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_remove_etud(partition_id: int, etudid: int):
|
||||
"""Enlève l'étudiant de tous les groupes de cette partition.
|
||||
|
||||
"""Enlève l'étudiant de tous les groupes de cette partition
|
||||
(NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
|
||||
"""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
@ -307,15 +286,12 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_create(partition_id: int): # partition-group-create
|
||||
"""Création d'un groupe dans une partition.
|
||||
"""Création d'un groupe dans une partition
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"group_name" : nom_du_groupe,
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -362,7 +338,7 @@ def group_create(partition_id: int): # partition-group-create
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_delete(group_id: int):
|
||||
"""Suppression d'un groupe."""
|
||||
"""Suppression d'un groupe"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
@ -391,7 +367,7 @@ def group_delete(group_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_edit(group_id: int):
|
||||
"""Édition d'un groupe."""
|
||||
"""Edit a group"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
@ -432,10 +408,9 @@ def group_edit(group_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_set_edt_id(group_id: int, edt_id: str):
|
||||
"""Set edt_id du groupe.
|
||||
|
||||
Contrairement à `/edit`, peut-être changé pour toute partition
|
||||
d'un formsemestre non verrouillé.
|
||||
"""Set edt_id for this group.
|
||||
Contrairement à /edit, peut-être changé pour toute partition
|
||||
ou formsemestre non verrouillé.
|
||||
"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
@ -461,19 +436,16 @@ def group_set_edt_id(group_id: int, edt_id: str):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_create(formsemestre_id: int):
|
||||
"""Création d'une partition dans un semestre.
|
||||
"""Création d'une partition dans un semestre
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"partition_name": str,
|
||||
"numero": int,
|
||||
"bul_show_rank": bool,
|
||||
"show_in_lists": bool,
|
||||
"groups_editable": bool
|
||||
"numero":int,
|
||||
"bul_show_rank":bool,
|
||||
"show_in_lists":bool,
|
||||
"groups_editable":bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -528,14 +500,9 @@ def partition_create(formsemestre_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
"""Modifie l'ordre des partitions du formsemestre.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ partition_id1, partition_id2, ... ]
|
||||
```
|
||||
def formsemestre_order_partitions(formsemestre_id: int):
|
||||
"""Modifie l'ordre des partitions du formsemestre
|
||||
JSON args: [partition_id1, partition_id2, ...]
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -546,7 +513,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
if not formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(partition_ids, list) and not all(
|
||||
if not isinstance(partition_ids, int) and not all(
|
||||
isinstance(x, int) for x in partition_ids
|
||||
):
|
||||
return json_error(
|
||||
@ -560,7 +527,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
db.session.commit()
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||
log(f"formsemestre_set_partitions_order({partition_ids})")
|
||||
log(f"formsemestre_order_partitions({partition_ids})")
|
||||
return [
|
||||
partition.to_dict()
|
||||
for partition in formsemestre.partitions.order_by(Partition.numero)
|
||||
@ -575,13 +542,8 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_order_groups(partition_id: int):
|
||||
"""Modifie l'ordre des groupes de la partition.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ group_id1, group_id2, ... ]
|
||||
```
|
||||
"""Modifie l'ordre des groupes de la partition
|
||||
JSON args: [group_id1, group_id2, ...]
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -592,7 +554,7 @@ def partition_order_groups(partition_id: int):
|
||||
if not partition.formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(group_ids, list) and not all(
|
||||
if not isinstance(group_ids, int) and not all(
|
||||
isinstance(x, int) for x in group_ids
|
||||
):
|
||||
return json_error(
|
||||
@ -617,13 +579,10 @@ def partition_order_groups(partition_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_edit(partition_id: int):
|
||||
"""Modification d'une partition dans un semestre.
|
||||
"""Modification d'une partition dans un semestre
|
||||
|
||||
Tous les champs sont optionnels.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json"
|
||||
All fields are optional:
|
||||
{
|
||||
"partition_name": str,
|
||||
"numero":int,
|
||||
@ -631,7 +590,6 @@ def partition_edit(partition_id: int):
|
||||
"show_in_lists":bool,
|
||||
"groups_editable":bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -695,9 +653,9 @@ def partition_edit(partition_id: int):
|
||||
def partition_delete(partition_id: int):
|
||||
"""Suppression d'une partition (et de tous ses groupes).
|
||||
|
||||
* Note 1: La partition par défaut (tous les étudiants du sem.) ne peut
|
||||
Note 1: La partition par défaut (tous les étudiants du sem.) ne peut
|
||||
pas être supprimée.
|
||||
* Note 2: Si la partition de parcours est supprimée, les étudiants
|
||||
Note 2: Si la partition de parcours est supprimée, les étudiants
|
||||
sont désinscrits des parcours.
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
|
@ -3,18 +3,12 @@ from app import db, log
|
||||
from app.api import api_bp as bp
|
||||
from app.auth.logic import basic_auth, token_auth
|
||||
|
||||
"""
|
||||
CATEGORY
|
||||
--------
|
||||
Authentification API
|
||||
"""
|
||||
|
||||
|
||||
@bp.route("/tokens", methods=["POST"])
|
||||
@basic_auth.login_required
|
||||
@as_json
|
||||
def token_get():
|
||||
"Renvoie un jeton jwt pour l'utilisateur courant."
|
||||
def get_token():
|
||||
"renvoie un jeton jwt pour l'utilisateur courant"
|
||||
token = basic_auth.current_user().get_token()
|
||||
log(f"API: giving token to {basic_auth.current_user()}")
|
||||
db.session.commit()
|
||||
@ -23,8 +17,8 @@ def token_get():
|
||||
|
||||
@bp.route("/tokens", methods=["DELETE"])
|
||||
@token_auth.login_required
|
||||
def token_revoke():
|
||||
"Révoque le jeton de l'utilisateur courant."
|
||||
def revoke_token():
|
||||
"révoque le jeton de l'utilisateur courant"
|
||||
user = token_auth.current_user()
|
||||
user.revoke_token()
|
||||
db.session.commit()
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux utilisateurs
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Utilisateurs
|
||||
"""
|
||||
|
||||
from flask import g, request
|
||||
@ -18,14 +14,15 @@ from flask_login import current_user, login_required
|
||||
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.auth.models import User, Role, UserRole
|
||||
from app.auth.models import is_valid_password
|
||||
from app.decorators import scodoc
|
||||
from app.models import Departement
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement, ScoDocSiteConfig
|
||||
from app.scodoc import sco_edt_cal
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@bp.route("/user/<int:uid>")
|
||||
@ -36,7 +33,7 @@ from app.scodoc.sco_utils import json_error
|
||||
@as_json
|
||||
def user_info(uid: int):
|
||||
"""
|
||||
Info sur un compte utilisateur ScoDoc.
|
||||
Info sur un compte utilisateur scodoc
|
||||
"""
|
||||
user: User = db.session.get(User, uid)
|
||||
if user is None:
|
||||
@ -57,22 +54,11 @@ def user_info(uid: int):
|
||||
@as_json
|
||||
def users_info_query():
|
||||
"""Utilisateurs, filtrés par dept, active ou début nom
|
||||
|
||||
Exemple:
|
||||
```
|
||||
/users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
|
||||
```
|
||||
|
||||
Seuls les utilisateurs "accessibles" (selon les permissions) sont retournés.
|
||||
Si accès via API web, le département de l'URL est ignoré, seules
|
||||
les permissions de l'utilisateur sont prises en compte.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
active: bool
|
||||
departement: string
|
||||
starts_with: string
|
||||
|
||||
"""
|
||||
query = User.query
|
||||
active = request.args.get("active")
|
||||
@ -121,10 +107,7 @@ def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
||||
@as_json
|
||||
def user_create():
|
||||
"""Création d'un utilisateur
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"active":bool (default True),
|
||||
"dept": str or null,
|
||||
@ -133,7 +116,6 @@ def user_create():
|
||||
"user_name": str,
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user_name = args.get("user_name")
|
||||
@ -170,10 +152,8 @@ def user_create():
|
||||
@permission_required(Permission.UsersAdmin)
|
||||
@as_json
|
||||
def user_edit(uid: int):
|
||||
"""Modification d'un utilisateur.
|
||||
|
||||
"""Modification d'un utilisateur
|
||||
Champs modifiables:
|
||||
```json
|
||||
{
|
||||
"dept": str or null,
|
||||
"nom": str,
|
||||
@ -181,7 +161,6 @@ def user_edit(uid: int):
|
||||
"active":bool
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user: User = User.query.get_or_404(uid)
|
||||
@ -220,15 +199,11 @@ def user_edit(uid: int):
|
||||
@permission_required(Permission.UsersAdmin)
|
||||
@as_json
|
||||
def user_password(uid: int):
|
||||
"""Modification du mot de passe d'un utilisateur.
|
||||
|
||||
"""Modification du mot de passe d'un utilisateur
|
||||
Champs modifiables:
|
||||
```json
|
||||
{
|
||||
"password": str
|
||||
}
|
||||
```.
|
||||
|
||||
Si le mot de passe ne convient pas, erreur 400.
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -262,7 +237,7 @@ def user_password(uid: int):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
"""Ajoute un rôle à l'utilisateur dans le département donné."""
|
||||
"""Add a role in the given dept to the user"""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
if dept is not None: # check
|
||||
@ -291,7 +266,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
"""Retire le rôle (dans le département donné) à cet utilisateur."""
|
||||
"""Remove the role (in the given dept) from the user"""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
if dept is not None: # check
|
||||
@ -317,8 +292,8 @@ def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def permissions_list():
|
||||
"""Liste des noms de permissions définies."""
|
||||
def list_permissions():
|
||||
"""Liste des noms de permissions définies"""
|
||||
return list(Permission.permission_by_name.keys())
|
||||
|
||||
|
||||
@ -328,7 +303,7 @@ def permissions_list():
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def role_get(role_name: str):
|
||||
def list_role(role_name: str):
|
||||
"""Un rôle"""
|
||||
return Role.query.filter_by(name=role_name).first_or_404().to_dict()
|
||||
|
||||
@ -339,8 +314,8 @@ def role_get(role_name: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def roles_list():
|
||||
"""Tous les rôles définis."""
|
||||
def list_roles():
|
||||
"""Tous les rôles définis"""
|
||||
return [role.to_dict() for role in Role.query]
|
||||
|
||||
|
||||
@ -357,7 +332,7 @@ def roles_list():
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_permission_add(role_name: str, perm_name: str):
|
||||
"""Ajoute une permission à un rôle."""
|
||||
"""Add permission to role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
permission = Permission.get_by_name(perm_name)
|
||||
if permission is None:
|
||||
@ -382,7 +357,7 @@ def role_permission_add(role_name: str, perm_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_permission_remove(role_name: str, perm_name: str):
|
||||
"""Retire une permission d'un rôle."""
|
||||
"""Remove permission from role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
permission = Permission.get_by_name(perm_name)
|
||||
if permission is None:
|
||||
@ -401,15 +376,10 @@ def role_permission_remove(role_name: str, perm_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_create(role_name: str):
|
||||
"""Création d'un nouveau rôle avec les permissions données.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
"""Create a new role with permissions.
|
||||
{
|
||||
"permissions" : [ 'ScoView', ... ]
|
||||
}
|
||||
```
|
||||
"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first()
|
||||
if role:
|
||||
@ -434,16 +404,11 @@ def role_create(role_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_edit(role_name: str):
|
||||
"""Édition d'un rôle. On peut spécifier un nom et/ou des permissions.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
"""Edit a role. On peut spécifier un nom et/ou des permissions.
|
||||
{
|
||||
"name" : name
|
||||
"permissions" : [ 'ScoView', ... ]
|
||||
}
|
||||
```
|
||||
"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -471,7 +436,7 @@ def role_edit(role_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_delete(role_name: str):
|
||||
"""Suprression d'un rôle."""
|
||||
"""Delete a role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
db.session.delete(role)
|
||||
db.session.commit()
|
||||
|
@ -35,9 +35,9 @@ def after_cas_login():
|
||||
if user.cas_allow_login:
|
||||
current_app.logger.info(f"CAS: login {user.user_name}")
|
||||
if login_user(user):
|
||||
flask.session["scodoc_cas_login_date"] = (
|
||||
datetime.datetime.now().isoformat()
|
||||
)
|
||||
flask.session[
|
||||
"scodoc_cas_login_date"
|
||||
] = datetime.datetime.now().isoformat()
|
||||
user.cas_last_login = datetime.datetime.utcnow()
|
||||
if flask.session.get("CAS_EDT_ID"):
|
||||
# essaie de récupérer l'edt_id s'il est présent
|
||||
@ -45,10 +45,8 @@ def after_cas_login():
|
||||
# via l'expression `cas_edt_id_from_xml_regexp`
|
||||
# voir flask_cas.routing
|
||||
edt_id = flask.session.get("CAS_EDT_ID")
|
||||
current_app.logger.info(
|
||||
f"""after_cas_login: storing edt_id for {
|
||||
user.user_name}: '{edt_id}'"""
|
||||
)
|
||||
current_app.logger.info(f"""after_cas_login: storing edt_id for {
|
||||
user.user_name}: '{edt_id}'""")
|
||||
user.edt_id = edt_id
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
@ -57,17 +55,12 @@ def after_cas_login():
|
||||
current_app.logger.info(
|
||||
f"CAS login denied for {user.user_name} (not allowed to use CAS)"
|
||||
)
|
||||
else: # pas d'utilisateur ScoDoc ou bien compte inactif
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"""CAS login denied for {
|
||||
user.user_name if user else ""
|
||||
} cas_id={cas_id} (unknown or inactive)"""
|
||||
)
|
||||
if ScoDocSiteConfig.is_cas_forced():
|
||||
# Dans ce cas, pas de redirect vers la page de login pour éviter de boucler
|
||||
raise ScoValueError(
|
||||
"compte ScoDoc inexistant ou inactif pour cet utilisateur CAS"
|
||||
)
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found !
|
||||
|
@ -14,15 +14,6 @@ import cracklib # pylint: disable=import-error
|
||||
|
||||
from flask import current_app, g
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
from sqlalchemy.exc import (
|
||||
IntegrityError,
|
||||
DataError,
|
||||
DatabaseError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
StatementError,
|
||||
InterfaceError,
|
||||
)
|
||||
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
@ -57,13 +48,13 @@ def is_valid_password(cleartxt) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_user_name(user_name: str) -> bool:
|
||||
"Check that user_name (aka login) is valid"
|
||||
def invalid_user_name(user_name: str) -> bool:
|
||||
"Check that user_name (aka login) is invalid"
|
||||
return (
|
||||
user_name
|
||||
and (len(user_name) >= 2)
|
||||
and (len(user_name) < USERNAME_STR_LEN)
|
||||
and VALID_LOGIN_EXP.match(user_name)
|
||||
not user_name
|
||||
or (len(user_name) < 2)
|
||||
or (len(user_name) >= USERNAME_STR_LEN)
|
||||
or not VALID_LOGIN_EXP.match(user_name)
|
||||
)
|
||||
|
||||
|
||||
@ -132,7 +123,7 @@ class User(UserMixin, ScoDocModel):
|
||||
# check login:
|
||||
if not "user_name" in kwargs:
|
||||
raise ValueError("missing user_name argument")
|
||||
if not is_valid_user_name(kwargs["user_name"]):
|
||||
if invalid_user_name(kwargs["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
||||
kwargs["nom"] = kwargs.get("nom", "") or ""
|
||||
kwargs["prenom"] = kwargs.get("prenom", "") or ""
|
||||
@ -338,8 +329,7 @@ class User(UserMixin, ScoDocModel):
|
||||
if new_user:
|
||||
if "user_name" in data:
|
||||
# never change name of existing users
|
||||
# (see change_user_name method to do that)
|
||||
if not is_valid_user_name(data["user_name"]):
|
||||
if invalid_user_name(data["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {data['user_name']}")
|
||||
self.user_name = data["user_name"]
|
||||
if "password" in data:
|
||||
@ -532,64 +522,6 @@ class User(UserMixin, ScoDocModel):
|
||||
|
||||
# nomnoacc était le nom en minuscules sans accents (inutile)
|
||||
|
||||
def change_user_name(self, new_user_name: str):
|
||||
"""Modify user name, update all relevant tables.
|
||||
commit session.
|
||||
"""
|
||||
# Safety check
|
||||
new_user_name = new_user_name.strip()
|
||||
if (
|
||||
not is_valid_user_name(new_user_name)
|
||||
or User.query.filter_by(user_name=new_user_name).count() > 0
|
||||
):
|
||||
raise ValueError("invalid user_name")
|
||||
# Le user_name est utilisé dans d'autres tables (sans être une clé)
|
||||
# BulAppreciations.author
|
||||
# EntrepriseHistorique.authenticated_user
|
||||
# EtudAnnotation.author
|
||||
# ScolarNews.authenticated_user
|
||||
# Scolog.authenticated_user
|
||||
from app.models import (
|
||||
BulAppreciations,
|
||||
EtudAnnotation,
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
)
|
||||
from app.entreprises.models import EntrepriseHistorique
|
||||
|
||||
try:
|
||||
# Update all instances of EtudAnnotation
|
||||
db.session.query(BulAppreciations).filter(
|
||||
BulAppreciations.author == self.user_name
|
||||
).update({BulAppreciations.author: new_user_name})
|
||||
db.session.query(EntrepriseHistorique).filter(
|
||||
EntrepriseHistorique.authenticated_user == self.user_name
|
||||
).update({EntrepriseHistorique.authenticated_user: new_user_name})
|
||||
db.session.query(EtudAnnotation).filter(
|
||||
EtudAnnotation.author == self.user_name
|
||||
).update({EtudAnnotation.author: new_user_name})
|
||||
db.session.query(ScolarNews).filter(
|
||||
ScolarNews.authenticated_user == self.user_name
|
||||
).update({ScolarNews.authenticated_user: new_user_name})
|
||||
db.session.query(Scolog).filter(
|
||||
Scolog.authenticated_user == self.user_name
|
||||
).update({Scolog.authenticated_user: new_user_name})
|
||||
# And update ourself:
|
||||
self.user_name = new_user_name
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
except (
|
||||
IntegrityError,
|
||||
DataError,
|
||||
DatabaseError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
StatementError,
|
||||
InterfaceError,
|
||||
) as exc:
|
||||
db.session.rollback()
|
||||
raise exc
|
||||
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
"Notre utilisateur anonyme"
|
||||
|
@ -18,7 +18,7 @@ from app.auth.forms import (
|
||||
ResetPasswordRequestForm,
|
||||
UserCreationForm,
|
||||
)
|
||||
from app.auth.models import Role, User, is_valid_user_name
|
||||
from app.auth.models import Role, User, invalid_user_name
|
||||
from app.auth.email import send_password_reset_email
|
||||
from app.decorators import admin_required
|
||||
from app.forms.generic import SimpleConfirmationForm
|
||||
@ -35,12 +35,10 @@ def _login_form():
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
# note: ceci est la première requête SQL déclenchée par un utilisateur arrivant
|
||||
user = (
|
||||
User.query.filter_by(user_name=form.user_name.data).first()
|
||||
if is_valid_user_name(form.user_name.data)
|
||||
else None
|
||||
)
|
||||
|
||||
if invalid_user_name(form.user_name.data):
|
||||
user = None
|
||||
else:
|
||||
user = User.query.filter_by(user_name=form.user_name.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
current_app.logger.info("login: invalid (%s)", form.user_name.data)
|
||||
flash(_("Nom ou mot de passe invalide"))
|
||||
|
@ -60,7 +60,7 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
||||
f"""<label><input type="checkbox" name="{parcour.id}" value="{parcour.id}"
|
||||
{'checked' if parcour.id in ue_pids else ""}
|
||||
onclick="set_ue_parcour(this);"
|
||||
data-setter="{url_for("apiweb.ue_set_parcours",
|
||||
data-setter="{url_for("apiweb.set_ue_parcours",
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id)}"
|
||||
>{parcour.code}{ects_parcour_txt}</label>"""
|
||||
)
|
||||
|
@ -576,6 +576,7 @@ class BulletinBUT:
|
||||
show_uevalid=self.prefs["bul_show_uevalid"],
|
||||
show_mention=self.prefs["bul_show_mention"],
|
||||
)
|
||||
|
||||
d.update(infos)
|
||||
# --- Rangs
|
||||
d["rang_nt"] = (
|
||||
|
@ -39,7 +39,6 @@ from app.scodoc.codes_cursus import UE_STANDARD
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_pv_lettres_inviduelles import add_dut120_infos
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.views import notes_bp as bp
|
||||
from app.views import ScoData
|
||||
@ -68,6 +67,7 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
|
||||
raise ScoValueError("formation non BUT")
|
||||
|
||||
args = _build_bulletin_but_infos(etud, formsemestre, fmt=fmt)
|
||||
|
||||
if fmt == "pdf":
|
||||
filename = scu.bul_filename(formsemestre, etud, prefix="bul-but")
|
||||
bul_pdf = bulletin_but_court_pdf.make_bulletin_but_court_pdf(args)
|
||||
@ -124,9 +124,7 @@ def _build_bulletin_but_infos(
|
||||
formsemestre, bulletins_sem.res
|
||||
)
|
||||
if warn_html:
|
||||
raise ScoValueError(
|
||||
"<b>Formation mal configurée pour le BUT</b>" + warn_html, safe=True
|
||||
)
|
||||
raise ScoValueError("<b>Formation mal configurée pour le BUT</b>" + warn_html)
|
||||
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
|
||||
refcomp, etud
|
||||
)
|
||||
@ -153,5 +151,4 @@ def _build_bulletin_but_infos(
|
||||
if ue.type == UE_STANDARD and ue.acronyme in ue_acronyms
|
||||
],
|
||||
}
|
||||
add_dut120_infos(formsemestre, etud.id, args)
|
||||
return args
|
||||
|
@ -97,8 +97,6 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
tuple[int, str], ScolarFormSemestreValidation
|
||||
] = None,
|
||||
ues_acronyms: list[str] = None,
|
||||
diplome_dut120: bool = False,
|
||||
diplome_dut120_descr: str = "",
|
||||
):
|
||||
super().__init__(bul, authuser=current_user, filigranne=filigranne)
|
||||
self.bul = bul
|
||||
@ -112,8 +110,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
self.title = title
|
||||
self.ue_validation_by_niveau = ue_validation_by_niveau
|
||||
self.ues_acronyms = ues_acronyms # sans UEs sport
|
||||
self.diplome_dut120 = diplome_dut120
|
||||
self.diplome_dut120_descr = diplome_dut120_descr
|
||||
|
||||
self.nb_ues = len(self.ues_acronyms)
|
||||
# Styles PDF
|
||||
self.style_base = styles.ParagraphStyle("style_base")
|
||||
@ -246,17 +243,13 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
)
|
||||
table_abs_ues.hAlign = "RIGHT"
|
||||
# Ligne (en bas) avec table cursus et boite jury
|
||||
# table_content = [self.table_cursus_but()]
|
||||
# if self.prefs["bul_show_decision"]:
|
||||
# table_content.append([Spacer(1, 8 * mm), self.boite_decisions_jury()])
|
||||
table_content = [self.table_cursus_but()]
|
||||
table_content.append(
|
||||
[Spacer(1, 8 * mm), self.boite_decisions_jury()]
|
||||
if self.prefs["bul_show_decision"]
|
||||
else []
|
||||
)
|
||||
table_cursus_jury = Table(
|
||||
[table_content],
|
||||
[
|
||||
[
|
||||
self.table_cursus_but(),
|
||||
[Spacer(1, 8 * mm), self.boite_decisions_jury()],
|
||||
]
|
||||
],
|
||||
colWidths=(self.width_page_avail - 84 * mm, 84 * mm),
|
||||
style=style_table_2cols,
|
||||
)
|
||||
@ -530,17 +523,14 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
def boite_decisions_jury(self):
|
||||
"""La boite en bas à droite avec jury"""
|
||||
txt = f"""ECTS acquis en BUT : <b>{self.ects_total:g}</b><br/>"""
|
||||
|
||||
if self.bul["semestre"].get("decision_annee", None):
|
||||
txt += f"""
|
||||
Décision année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
||||
Décision saisie le {
|
||||
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime(scu.DATE_FMT)
|
||||
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
||||
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
|
||||
<br/>
|
||||
{self.bul.get("diplomation", "")}
|
||||
"""
|
||||
if self.diplome_dut120_descr:
|
||||
txt += f"""<br/>{self.diplome_dut120_descr}."""
|
||||
|
||||
if self.bul["semestre"].get("autorisation_inscription", None):
|
||||
txt += (
|
||||
"<br/>Autorisé à s'inscrire en <b>"
|
||||
|
@ -73,7 +73,6 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="bul-table",
|
||||
)
|
||||
table_objects = table.gen(fmt=fmt)
|
||||
objects += table_objects
|
||||
@ -428,11 +427,12 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
|
||||
if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE
|
||||
else "*"
|
||||
)
|
||||
note_value = e["note"].get("value", "")
|
||||
t = {
|
||||
"titre": f"{e['description'] or ''}",
|
||||
"moyenne": note_value,
|
||||
"_moyenne_pdf": Paragraph(f"""<para align=right>{note_value}</para>"""),
|
||||
"moyenne": e["note"]["value"],
|
||||
"_moyenne_pdf": Paragraph(
|
||||
f"""<para align=right>{e["note"]["value"]}</para>"""
|
||||
),
|
||||
"coef": coef,
|
||||
"_coef_pdf": Paragraph(
|
||||
f"""<para align=right fontSize={self.small_fontsize}><i>{
|
||||
|
@ -203,7 +203,7 @@ def bulletin_but_xml_compat(
|
||||
e.date_debut.isoformat() if e.date_debut else ""
|
||||
),
|
||||
date_fin=(
|
||||
e.date_fin.isoformat() if e.date_fin else ""
|
||||
e.date_fin.isoformat() if e.date_debut else ""
|
||||
),
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
|
@ -14,11 +14,9 @@ Classe raccordant avec ScoDoc 7:
|
||||
|
||||
"""
|
||||
import collections
|
||||
from collections.abc import Iterable
|
||||
from operator import attrgetter
|
||||
|
||||
from flask import g, url_for
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db, log
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
@ -31,7 +29,7 @@ from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
)
|
||||
from app.models.ues import UEParcours
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.models.but_validations import ApcValidationRCUE
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
@ -44,9 +42,9 @@ from app.scodoc import sco_cursus_dut
|
||||
|
||||
|
||||
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
"""Pour compat ScoDoc 7"""
|
||||
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
|
||||
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
super().__init__(etud, formsemestre_id, res)
|
||||
# Ajustements pour le BUT
|
||||
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT
|
||||
@ -56,22 +54,8 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
return False
|
||||
|
||||
def parcours_validated(self):
|
||||
"True si le parcours (ici diplôme BUT) est validé"
|
||||
return but_parcours_validated(
|
||||
self.etud.id, self.cur_sem.formation.referentiel_competence_id
|
||||
)
|
||||
|
||||
|
||||
def but_parcours_validated(etudid: int, referentiel_competence_id: int) -> bool:
|
||||
"""Détermine si le parcours BUT est validé:
|
||||
ne regarde que si une validation BUT3 est enregistrée
|
||||
"""
|
||||
return any(
|
||||
sco_codes.code_annee_validant(v.code)
|
||||
for v in ApcValidationAnnee.query.filter_by(
|
||||
etudid=etudid, ordre=3, referentiel_competence_id=referentiel_competence_id
|
||||
)
|
||||
)
|
||||
"True si le parcours est validé"
|
||||
return False # XXX TODO
|
||||
|
||||
|
||||
class EtudCursusBUT:
|
||||
@ -209,10 +193,6 @@ class EtudCursusBUT:
|
||||
# slow, utile pour affichage fiche
|
||||
return annee in [n.annee for n in self.competences[competence_id].niveaux]
|
||||
|
||||
def get_ects_acquis(self) -> int:
|
||||
"Nombre d'ECTS validés par etud dans le BUT de ce référentiel"
|
||||
return but_ects_valides(self.etud, self.formation.referentiel_competence.id)
|
||||
|
||||
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
||||
"""Cherche les validations de jury enregistrées pour chaque niveau
|
||||
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
||||
@ -307,136 +287,104 @@ class FormSemestreCursusBUT:
|
||||
)
|
||||
return niveaux_by_annee
|
||||
|
||||
# def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
||||
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
# validation_par_competence_et_annee = {}
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# # On s'assurer qu'elle concerne notre cursus !
|
||||
# ue = validation_rcue.ue2
|
||||
# if ue.id not in self.ue_ids:
|
||||
# if (
|
||||
# ue.formation.referentiel_competences_id
|
||||
# == self.referentiel_competences_id
|
||||
# ):
|
||||
# self.ue_ids = ue.id
|
||||
# else:
|
||||
# continue # skip this validation
|
||||
# niveau = validation_rcue.niveau()
|
||||
# if not niveau.competence.id in validation_par_competence_et_annee:
|
||||
# validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
# previous_validation = validation_par_competence_et_annee.get(
|
||||
# niveau.competence.id
|
||||
# ).get(validation_rcue.annee())
|
||||
# # prend la "meilleure" validation
|
||||
# if (not previous_validation) or (
|
||||
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
# ):
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
# niveau.annee
|
||||
# ] = validation_rcue
|
||||
# return validation_par_competence_et_annee
|
||||
def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
validation_par_competence_et_annee = {}
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# On s'assurer qu'elle concerne notre cursus !
|
||||
ue = validation_rcue.ue2
|
||||
if ue.id not in self.ue_ids:
|
||||
if (
|
||||
ue.formation.referentiel_competences_id
|
||||
== self.referentiel_competences_id
|
||||
):
|
||||
self.ue_ids = ue.id
|
||||
else:
|
||||
continue # skip this validation
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in validation_par_competence_et_annee:
|
||||
validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
return validation_par_competence_et_annee
|
||||
|
||||
# def list_etud_inscriptions(self, etud: Identite):
|
||||
# "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
# self.niveaux_by_annee = {}
|
||||
# "{ annee : liste des niveaux à valider }"
|
||||
# self.niveaux: dict[int, ApcNiveau] = {}
|
||||
# "cache les niveaux"
|
||||
# for annee in (1, 2, 3):
|
||||
# niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
# annee, [self.parcour] if self.parcour else None # XXX WIP
|
||||
# )[1]
|
||||
# # groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
# self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
# niveaux_d[self.parcour.id] if self.parcour else []
|
||||
# )
|
||||
# self.niveaux.update(
|
||||
# {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
# )
|
||||
def list_etud_inscriptions(self, etud: Identite):
|
||||
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
self.niveaux_by_annee = {}
|
||||
"{ annee : liste des niveaux à valider }"
|
||||
self.niveaux: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
annee, [self.parcour] if self.parcour else None # XXX WIP
|
||||
)[1]
|
||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
niveaux_d[self.parcour.id] if self.parcour else []
|
||||
)
|
||||
self.niveaux.update(
|
||||
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
)
|
||||
|
||||
# self.validation_par_competence_et_annee = {}
|
||||
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# niveau = validation_rcue.niveau()
|
||||
# if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
# previous_validation = self.validation_par_competence_et_annee.get(
|
||||
# niveau.competence.id
|
||||
# ).get(validation_rcue.annee())
|
||||
# # prend la "meilleure" validation
|
||||
# if (not previous_validation) or (
|
||||
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
# ):
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
# niveau.annee
|
||||
# ] = validation_rcue
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
|
||||
# self.competences = {
|
||||
# competence.id: competence
|
||||
# for competence in (
|
||||
# self.parcour.query_competences()
|
||||
# if self.parcour
|
||||
# else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
# )
|
||||
# }
|
||||
# "cache { competence_id : competence }"
|
||||
self.competences = {
|
||||
competence.id: competence
|
||||
for competence in (
|
||||
self.parcour.query_competences()
|
||||
if self.parcour
|
||||
else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
)
|
||||
}
|
||||
"cache { competence_id : competence }"
|
||||
|
||||
|
||||
def but_ects_valides(
|
||||
etud: Identite,
|
||||
referentiel_competence_id: int,
|
||||
annees_but: None | Iterable[str] = None,
|
||||
) -> int:
|
||||
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
||||
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
|
||||
Ne prend que les UE associées à des niveaux de compétences,
|
||||
et ne les compte qu'une fois même en cas de redoublement avec re-validation.
|
||||
Si annees_but est spécifié, un iterable "BUT1, "BUT2" par exemple, ne prend que ces années.
|
||||
"""
|
||||
validations = but_validations_ues(etud, referentiel_competence_id, annees_but)
|
||||
ects_dict = {}
|
||||
for v in validations:
|
||||
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
||||
if v.code in CODES_UE_VALIDES:
|
||||
ects_dict[key] = v.ue.ects or 0.0
|
||||
|
||||
return int(sum(ects_dict.values())) if ects_dict else 0
|
||||
|
||||
|
||||
def but_validations_ues(
|
||||
etud: Identite,
|
||||
referentiel_competence_id: int,
|
||||
annees_but: None | Iterable[str] = None,
|
||||
) -> list[ScolarFormSemestreValidation]:
|
||||
"""Query les validations d'UEs pour cet étudiant
|
||||
dans des UEs appartenant à ce référentiel de compétence
|
||||
et en option pour les années BUT indiquées.
|
||||
annees_but : None (tout) ou liste [ "BUT1", ... ]
|
||||
"""
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.join(UniteEns)
|
||||
.join(ApcNiveau)
|
||||
)
|
||||
# restreint à certaines années (utile pour les ECTS du DUT120)
|
||||
if annees_but:
|
||||
validations = validations.filter(ApcNiveau.annee.in_(annees_but))
|
||||
# restreint au référentiel de compétence
|
||||
validations = validations.join(ApcCompetence).filter_by(
|
||||
referentiel_id=referentiel_competence_id
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=referentiel_competence_id)
|
||||
)
|
||||
|
||||
# Tri (nb: fait en python pour gérer les validations externes qui n'ont pas de formsemestre)
|
||||
return sorted(
|
||||
validations,
|
||||
key=lambda v: (
|
||||
(v.formsemestre.semestre_id, v.ue.numero, v.ue.acronyme)
|
||||
if v.formsemestre
|
||||
else (v.ue.semestre_idx or -2, v.ue.numero, v.ue.acronyme)
|
||||
),
|
||||
)
|
||||
ects_dict = {}
|
||||
for v in validations:
|
||||
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
|
||||
if v.code in CODES_UE_VALIDES:
|
||||
ects_dict[key] = v.ue.ects
|
||||
|
||||
return sum(ects_dict.values()) if ects_dict else 0.0
|
||||
|
||||
|
||||
def etud_ues_de_but1_non_validees(
|
||||
|
@ -64,7 +64,6 @@ import re
|
||||
|
||||
import numpy as np
|
||||
from flask import flash, g, url_for
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
@ -82,10 +81,8 @@ from app.models import Evaluation, ModuleImpl, Scolog, ScolarAutorisationInscrip
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
@ -93,7 +90,6 @@ from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import (
|
||||
code_rcue_validant,
|
||||
code_ue_validant,
|
||||
BUT_CODES_ORDER,
|
||||
CODES_RCUE_VALIDES,
|
||||
CODES_UE_VALIDES,
|
||||
@ -425,11 +421,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
+ '</div><div class="warning warning-info">'.join(messages)
|
||||
+ "</div>"
|
||||
)
|
||||
|
||||
# Présente les codes unifiés, avec le code proposé en tête et les autres triés
|
||||
codes_set = set(self.codes)
|
||||
codes_set.remove(self.codes[0])
|
||||
self.codes = [self.codes[0]] + sorted(x or "" for x in codes_set)
|
||||
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
|
||||
|
||||
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
|
||||
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
|
||||
@ -761,13 +753,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
self.validation.date = datetime.now()
|
||||
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
log(f"Recording {self}: {code}")
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: {code}",
|
||||
)
|
||||
db.session.commit()
|
||||
if mark_recorded:
|
||||
self.recorded = True
|
||||
self.invalidate_formsemestre_cache()
|
||||
@ -893,7 +885,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
not only_validantes
|
||||
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
||||
modif |= self.record(code)
|
||||
self.record_autorisation_inscription(code)
|
||||
self.record_autorisation_inscription(code)
|
||||
return modif
|
||||
|
||||
def erase(self, only_one_sem=False):
|
||||
@ -904,8 +896,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
Si only_one_sem, n'efface que les décisions UE et les
|
||||
autorisations de passage du semestre d'origine du deca.
|
||||
|
||||
Efface les validations de DUT120 issues du semestre d'origine du deca.
|
||||
|
||||
Dans tous les cas, efface les validations de l'année en cours.
|
||||
(commite la session.)
|
||||
"""
|
||||
@ -955,17 +945,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
||||
)
|
||||
|
||||
# Efface les validations de DUT120 issues du semestre d'origine du deca.
|
||||
for validation in ValidationDUT120.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=self.formsemestre.id
|
||||
):
|
||||
db.session.delete(validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg="Validation DUT120 effacée",
|
||||
)
|
||||
|
||||
# Efface éventuelles validations de semestre
|
||||
# (en principe inutilisées en BUT)
|
||||
# et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
|
||||
@ -1007,36 +986,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
pour PV jurys
|
||||
"""
|
||||
validations = []
|
||||
# Validations antérieures émises par ce formsemestre
|
||||
for res in (self.res_impair, self.res_pair):
|
||||
if res:
|
||||
validations_anterieures = (
|
||||
ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=res.formsemestre.id
|
||||
)
|
||||
.filter(
|
||||
ScolarFormSemestreValidation.semestre_id
|
||||
!= res.formsemestre.semestre_id
|
||||
)
|
||||
.join(UniteEns)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=res.formsemestre.formation.formation_code)
|
||||
.order_by(
|
||||
sa.desc(UniteEns.semestre_idx),
|
||||
UniteEns.acronyme,
|
||||
sa.desc(ScolarFormSemestreValidation.event_date),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
if validations_anterieures:
|
||||
validations.append(
|
||||
", ".join(
|
||||
v.ue.acronyme
|
||||
for v in validations_anterieures
|
||||
if v and v.ue and code_ue_validant(v.code)
|
||||
)
|
||||
)
|
||||
# Validations des UEs des deux semestres de l'année
|
||||
for res in (self.res_impair, self.res_pair):
|
||||
if res:
|
||||
dec_ues = [
|
||||
@ -1045,10 +994,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if ue.type == UE_STANDARD and ue.id in self.decisions_ues
|
||||
]
|
||||
valids = [dec_ue.descr_validation() for dec_ue in dec_ues]
|
||||
# présentation de la liste des UEs:
|
||||
if valids:
|
||||
validations.append(", ".join(v for v in valids if v))
|
||||
|
||||
validations.append(", ".join(v for v in valids if v))
|
||||
return line_sep.join(validations)
|
||||
|
||||
def descr_pb_coherence(self) -> list[str]:
|
||||
@ -1088,8 +1034,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
return messages
|
||||
|
||||
def valide_diplome(self) -> bool:
|
||||
"Vrai si l'étudiant a validé son diplôme (décision enregistrée)"
|
||||
return self.annee_but == 3 and sco_codes.code_annee_validant(self.code_valide)
|
||||
"Vrai si l'étudiant à validé son diplôme"
|
||||
return False # TODO XXX
|
||||
|
||||
|
||||
def list_ue_parcour_etud(
|
||||
@ -1228,12 +1174,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
code=code,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation {self.rcue}: {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"rcue.record {self}: {code}")
|
||||
|
||||
# Modifie au besoin les codes d'UE
|
||||
@ -1646,12 +1593,13 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
moy_ue=self.moy_ue,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}({self.moy_ue}): {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"DecisionsProposeesUE: recording {self.validation}")
|
||||
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
||||
|
@ -8,12 +8,11 @@
|
||||
"""
|
||||
from flask import g, request, url_for
|
||||
|
||||
from openpyxl.styles import Alignment
|
||||
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
||||
|
||||
from app import log
|
||||
from app.but import jury_but
|
||||
from app.but.cursus_but import but_ects_valides
|
||||
from app.models.but_validations import ValidationDUT120
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -156,14 +155,6 @@ def pvjury_table_but(
|
||||
deca = None
|
||||
|
||||
ects_but_valides = but_ects_valides(etud, referentiel_competence_id)
|
||||
has_diplome = deca.valide_diplome() if deca else False
|
||||
diplome_lst = ["ADM"] if has_diplome else []
|
||||
validation_dut120 = ValidationDUT120.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=formsemestre.id
|
||||
).first()
|
||||
if validation_dut120:
|
||||
diplome_lst.append("Diplôme de DUT validé.")
|
||||
diplome_str = ". ".join(diplome_lst)
|
||||
row = {
|
||||
"nom_pv": (
|
||||
etud.code_ine or etud.code_nip or etud.id
|
||||
@ -181,12 +172,8 @@ def pvjury_table_but(
|
||||
etudid=etud.id,
|
||||
),
|
||||
"cursus": _descr_cursus_but(etud),
|
||||
"ects": (
|
||||
f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}"""
|
||||
if deca
|
||||
else ""
|
||||
),
|
||||
"_ects_xls": deca.ects_annee() if deca else "",
|
||||
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}""",
|
||||
"_ects_xls": deca.ects_annee(),
|
||||
"ects_but": ects_but_valides,
|
||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||
"niveaux": (
|
||||
@ -194,15 +181,10 @@ def pvjury_table_but(
|
||||
),
|
||||
"decision_but": deca.code_valide if deca else "",
|
||||
"devenir": (
|
||||
"Diplôme obtenu"
|
||||
if has_diplome
|
||||
else (
|
||||
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||
if deca
|
||||
else ""
|
||||
)
|
||||
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||
if deca
|
||||
else ""
|
||||
),
|
||||
"diplome": diplome_str,
|
||||
# pour exports excel seulement:
|
||||
"civilite": etud.civilite_etat_civil_str,
|
||||
"nom": etud.nom,
|
||||
@ -211,7 +193,7 @@ def pvjury_table_but(
|
||||
"code_nip": etud.code_nip,
|
||||
"code_ine": etud.code_ine,
|
||||
}
|
||||
if (deca and deca.valide_diplome()) or not only_diplome:
|
||||
if deca.valide_diplome() or not only_diplome:
|
||||
rows.append(row)
|
||||
|
||||
rows.sort(key=lambda x: x["_nom_pv_order"])
|
||||
|
@ -9,14 +9,14 @@
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but, jury_dut120
|
||||
from app.models import Identite, FormSemestre, ScolarNews, ValidationDUT120
|
||||
from app.but import jury_but
|
||||
from app.models import Identite, FormSemestre, ScolarNews
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False, with_dut120=True
|
||||
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False
|
||||
) -> tuple[int, list[jury_but.DecisionsProposeesAnnee]]:
|
||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||
|
||||
@ -27,8 +27,6 @@ def formsemestre_validation_auto_but(
|
||||
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||
|
||||
Enregistre aussi le DUT120.
|
||||
|
||||
Returns:
|
||||
- En mode normal, (nombre d'étudiants pour lesquels on a enregistré au moins un code, []])
|
||||
- En mode dry_run, (0, list[DecisionsProposeesAnnee])
|
||||
@ -42,10 +40,7 @@ def formsemestre_validation_auto_but(
|
||||
etud = Identite.get_etud(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if not dry_run:
|
||||
modified = deca.record_all(only_validantes=only_adm)
|
||||
modified |= validation_dut120_auto(etud, formsemestre)
|
||||
if modified:
|
||||
nb_etud_modif += 1
|
||||
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
||||
else:
|
||||
decas.append(deca)
|
||||
|
||||
@ -61,28 +56,3 @@ def formsemestre_validation_auto_but(
|
||||
),
|
||||
)
|
||||
return nb_etud_modif, decas
|
||||
|
||||
|
||||
def validation_dut120_auto(etud: Identite, formsemestre: FormSemestre) -> bool:
|
||||
"""Si l'étudiant n'a pas déjà validé son DUT120 dans cette spécialité
|
||||
et qu'il satisfait les confitions, l'enregistre.
|
||||
Returns True si nouvelle décision enregistrée.
|
||||
"""
|
||||
refcomp = formsemestre.formation.referentiel_competence
|
||||
if not refcomp:
|
||||
raise ScoValueError("formation non associée à un référentiel de compétences")
|
||||
validation = ValidationDUT120.query.filter_by(
|
||||
etudid=etud.id, referentiel_competence_id=refcomp.id
|
||||
).first()
|
||||
if validation:
|
||||
return False # déjà enregistré
|
||||
if jury_dut120.etud_valide_dut120(etud, refcomp.id):
|
||||
new_validation = ValidationDUT120(
|
||||
etudid=etud.id,
|
||||
referentiel_competence_id=refcomp.id,
|
||||
formsemestre_id=formsemestre.id, # Replace with appropriate value
|
||||
)
|
||||
db.session.add(new_validation)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False # ne peut pas valider
|
||||
|
@ -1,112 +0,0 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury DUT120: gestion et vues
|
||||
|
||||
Ce diplôme est attribué sur demande aux étudiants de BUT ayant acquis les 120 ECTS
|
||||
de BUT 1 et BUT 2.
|
||||
|
||||
"""
|
||||
import time
|
||||
from flask import flash, g, redirect, render_template, request, url_for
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField
|
||||
|
||||
from app import db, log
|
||||
from app.but import cursus_but
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import FormSemestre, Identite, Scolog, ValidationDUT120
|
||||
from app.scodoc.sco_exceptions import ScoPermissionDenied, ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import notes_bp as bp
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def etud_valide_dut120(etud: Identite, referentiel_competence_id: int) -> bool:
|
||||
"""Vrai si l'étudiant satisfait les conditions pour valider le DUT120"""
|
||||
ects_but1_but2 = cursus_but.but_ects_valides(
|
||||
etud, referentiel_competence_id, annees_but=("BUT1", "BUT2")
|
||||
)
|
||||
return ects_but1_but2 >= 120
|
||||
|
||||
|
||||
class ValidationDUT120Form(FlaskForm):
|
||||
"Formulaire validation DUT120"
|
||||
submit = SubmitField("Enregistrer le diplôme DUT 120")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/validate_dut120/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def validate_dut120_etud(etudid: int, formsemestre_id: int):
|
||||
"""Formulaire validation individuelle du DUT120"""
|
||||
# Check arguments
|
||||
etud = Identite.get_etud(etudid)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
refcomp = formsemestre.formation.referentiel_competence
|
||||
if not refcomp:
|
||||
raise ScoValueError("formation non associée à un référentiel de compétences")
|
||||
# Permission
|
||||
if not formsemestre.can_edit_jury():
|
||||
raise ScoPermissionDenied(
|
||||
dest_url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
)
|
||||
|
||||
ects_but1_but2 = cursus_but.but_ects_valides(
|
||||
etud, refcomp.id, annees_but=("BUT1", "BUT2")
|
||||
)
|
||||
|
||||
form = ValidationDUT120Form()
|
||||
# Check if ValidationDUT120 instance already exists
|
||||
existing_validation = ValidationDUT120.query.filter_by(
|
||||
etudid=etud.id, referentiel_competence_id=refcomp.id
|
||||
).first()
|
||||
if existing_validation:
|
||||
flash("DUT120 déjà validé", "info")
|
||||
etud_can_validate_dut = False
|
||||
# Check if the student meets the criteria
|
||||
elif ects_but1_but2 < 120:
|
||||
flash("L'étudiant ne remplit pas les conditions", "warning")
|
||||
etud_can_validate_dut = False # here existing_validation is None
|
||||
else:
|
||||
etud_can_validate_dut = True
|
||||
|
||||
if etud_can_validate_dut and request.method == "POST" and form.validate_on_submit():
|
||||
new_validation = ValidationDUT120(
|
||||
etudid=etud.id,
|
||||
referentiel_competence_id=refcomp.id,
|
||||
formsemestre_id=formsemestre.id, # Replace with appropriate value
|
||||
)
|
||||
db.session.add(new_validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=etud.id,
|
||||
msg=f"Validation DUT120 enregistrée depuis S{formsemestre.semestre_id}",
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"ValidationDUT120 enregistrée pour {etud} depuis {formsemestre}")
|
||||
flash("Validation DUT120 enregistrée", "success")
|
||||
return redirect(etud.url_fiche())
|
||||
|
||||
return render_template(
|
||||
"but/validate_dut120.j2",
|
||||
ects_but1_but2=ects_but1_but2,
|
||||
etud=etud,
|
||||
etud_can_validate_dut=etud_can_validate_dut,
|
||||
form=form,
|
||||
formsemestre=formsemestre,
|
||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||
time=time,
|
||||
title="Délivrance du DUT",
|
||||
validation=existing_validation,
|
||||
)
|
@ -10,25 +10,23 @@ Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def jury_delete_manual(etud: Identite):
|
||||
"""Vue présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant (si permission) de les supprimer une à une.
|
||||
"""Vue (réservée au chef de dept.)
|
||||
présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant de les supprimer une à une.
|
||||
"""
|
||||
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, ue_id=None
|
||||
@ -62,12 +60,8 @@ def jury_delete_manual(etud: Identite):
|
||||
sem_vals=sem_vals,
|
||||
ue_vals=ue_vals,
|
||||
autorisations=autorisations,
|
||||
dut120_vals=ValidationDUT120.query.filter_by(etudid=etud.id).order_by(
|
||||
ValidationDUT120.date
|
||||
),
|
||||
rcue_vals=rcue_vals,
|
||||
annee_but_vals=annee_but_vals,
|
||||
sco=ScoData(),
|
||||
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
||||
read_only=not current_user.has_permission(Permission.EtudInscrit),
|
||||
)
|
||||
|
@ -4,10 +4,13 @@
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions RCUE antérieures
|
||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
||||
|
||||
Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import log
|
||||
from app.but import cursus_but
|
||||
|
@ -27,7 +27,6 @@
|
||||
|
||||
"""caches pour tables APC
|
||||
"""
|
||||
from flask import g
|
||||
|
||||
from app.scodoc import sco_cache
|
||||
|
||||
@ -48,27 +47,3 @@ class EvaluationsPoidsCache(sco_cache.ScoDocCache):
|
||||
"""
|
||||
|
||||
prefix = "EPC"
|
||||
|
||||
@classmethod
|
||||
def invalidate_all(cls):
|
||||
"delete all cached evaluations poids (in current dept)"
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
moduleimpl_ids = [
|
||||
mi.id
|
||||
for mi in ModuleImpl.query.join(FormSemestre).filter_by(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
]
|
||||
cls.delete_many(moduleimpl_ids)
|
||||
|
||||
@classmethod
|
||||
def invalidate_sem(cls, formsemestre_id):
|
||||
"delete cached evaluations poids for this formsemestre from cache"
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
moduleimpl_ids = [
|
||||
mi.id for mi in ModuleImpl.query.filter_by(formsemestre_id=formsemestre_id)
|
||||
]
|
||||
cls.delete_many(moduleimpl_ids)
|
||||
|
@ -45,6 +45,7 @@ from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.sco_exceptions import ScoBugCatcher
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
@ -112,8 +113,6 @@ class ModuleImplResults:
|
||||
"""
|
||||
self.evals_etudids_sans_note = {}
|
||||
"""dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
|
||||
self.evals_type = {}
|
||||
"""Type de chaque eval { evaluation.id : evaluation.evaluation_type }"""
|
||||
self.load_notes(etudids, etudids_actifs)
|
||||
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
|
||||
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
|
||||
@ -165,10 +164,7 @@ class ModuleImplResults:
|
||||
self.evaluations_completes = []
|
||||
self.evaluations_completes_dict = {}
|
||||
self.etudids_attente = set() # empty
|
||||
self.evals_type = {}
|
||||
evaluation: Evaluation
|
||||
for evaluation in moduleimpl.evaluations:
|
||||
self.evals_type[evaluation.id] = evaluation.evaluation_type
|
||||
eval_df = self._load_evaluation_notes(evaluation)
|
||||
# is_complete ssi
|
||||
# tous les inscrits (non dem) au module ont une note
|
||||
@ -274,24 +270,6 @@ class ModuleImplResults:
|
||||
* self.evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
|
||||
def get_evaluations_special_coefs(
|
||||
self, modimpl: ModuleImpl, evaluation_type=Evaluation.EVALUATION_SESSION2
|
||||
) -> np.array:
|
||||
"""Coefficients des évaluations de session 2 ou rattrapage.
|
||||
Les évals de session 2 et rattrapage sont réputées "complètes": elles sont toujours
|
||||
prises en compte mais seules les notes numériques et ABS sont utilisées.
|
||||
Résultat: 2d-array of floats, shape (nb_evals, 1)
|
||||
"""
|
||||
return (
|
||||
np.array(
|
||||
[
|
||||
(e.coefficient if e.evaluation_type == evaluation_type else 0.0)
|
||||
for e in modimpl.evaluations
|
||||
],
|
||||
dtype=float,
|
||||
)
|
||||
).reshape(-1, 1)
|
||||
|
||||
# was _list_notes_evals_titles
|
||||
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"Liste des évaluations complètes"
|
||||
@ -318,41 +296,47 @@ class ModuleImplResults:
|
||||
for (etudid, x) in self.evals_notes[evaluation_id].items()
|
||||
}
|
||||
|
||||
def get_evaluations_rattrapage(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations de rattrapage de ce module.
|
||||
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
||||
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas.
|
||||
Rattrapage: la moyenne du module est la meilleure note entre moyenne
|
||||
des autres évals et la moyenne des notes de rattrapage.
|
||||
des autres évals et la note eval rattrapage.
|
||||
"""
|
||||
return [
|
||||
eval_list = [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_RATTRAPAGE
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluations_session2(self, moduleimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
La moyenne des notes de Session 2 remplace la note de moyenne des autres évals.
|
||||
def get_evaluation_session2(self, moduleimpl: ModuleImpl) -> Evaluation | None:
|
||||
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas.
|
||||
Session 2: remplace la note de moyenne des autres évals.
|
||||
"""
|
||||
return [
|
||||
eval_list = [
|
||||
e
|
||||
for e in moduleimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_SESSION2
|
||||
]
|
||||
if eval_list:
|
||||
return eval_list[0]
|
||||
return None
|
||||
|
||||
def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations bonus non bloquées de ce module, ou liste vide s'il n'en a pas."""
|
||||
"""Les évaluations bonus de ce module, ou liste vide s'il n'en a pas."""
|
||||
return [
|
||||
e
|
||||
for e in modimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS and not e.is_blocked()
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS
|
||||
]
|
||||
|
||||
def get_evaluations_bonus_idx(self, modimpl: ModuleImpl) -> list[int]:
|
||||
"""Les indices des évaluations bonus non bloquées"""
|
||||
"""Les indices des évaluations bonus"""
|
||||
return [
|
||||
i
|
||||
for (i, e) in enumerate(modimpl.evaluations)
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS and not e.is_blocked()
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS
|
||||
]
|
||||
|
||||
|
||||
@ -360,13 +344,12 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
"Calcul des moyennes de modules à la mode BUT"
|
||||
|
||||
def compute_module_moy(
|
||||
self, evals_poids_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
self,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
) -> pd.DataFrame:
|
||||
"""Calcule les moyennes des étudiants dans ce module
|
||||
|
||||
Argument:
|
||||
evals_poids: DataFrame, colonnes: UEs, lignes: EVALs
|
||||
modimpl_coefs_df: DataFrame, colonnes: modimpl_id, lignes: ue_id
|
||||
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
|
||||
|
||||
Résultat: DataFrame, colonnes UE, lignes etud
|
||||
= la note de l'étudiant dans chaque UE pour ce module.
|
||||
@ -387,7 +370,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
|
||||
if nb_ues == 0:
|
||||
return pd.DataFrame(index=self.evals_notes.index, columns=[])
|
||||
# coefs des évals complètes normales (pas rattr., session 2 ni bonus):
|
||||
evals_coefs = self.get_evaluations_coefs(modimpl)
|
||||
evals_poids = evals_poids_df.values * evals_coefs
|
||||
# -> evals_poids shape : (nb_evals, nb_ues)
|
||||
@ -416,47 +398,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
) / np.sum(evals_poids_etuds, axis=1)
|
||||
# etuds_moy_module shape: nb_etuds x nb_ues
|
||||
|
||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||
if evals_session2:
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
# Calcul moyenne notes session2 et remplace (si la note session 2 existe)
|
||||
etuds_moy_module_s2 = self._compute_moy_special(
|
||||
modimpl,
|
||||
evals_notes_stacked,
|
||||
evals_poids_df,
|
||||
Evaluation.EVALUATION_SESSION2,
|
||||
)
|
||||
|
||||
# Vrai si toutes les UEs avec coef non nul ont bien une note de session 2 calculée:
|
||||
mod_coefs = modimpl_coefs_df[modimpl.id]
|
||||
etuds_use_session2 = np.all(
|
||||
np.isfinite(etuds_moy_module_s2[:, mod_coefs != 0]), axis=1
|
||||
)
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2[:, np.newaxis],
|
||||
etuds_moy_module_s2,
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
elif evals_rat:
|
||||
etuds_moy_module_rat = self._compute_moy_special(
|
||||
modimpl,
|
||||
evals_notes_stacked,
|
||||
evals_poids_df,
|
||||
Evaluation.EVALUATION_RATTRAPAGE,
|
||||
)
|
||||
etuds_ue_use_rattrapage = (
|
||||
etuds_moy_module_rat > etuds_moy_module
|
||||
) # etud x UE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_ue_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
np.any(etuds_ue_use_rattrapage, axis=1), index=self.evals_notes.index
|
||||
)
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
@ -464,6 +405,47 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
evals_poids_df,
|
||||
evals_notes_stacked,
|
||||
)
|
||||
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2[:, np.newaxis],
|
||||
np.tile(
|
||||
(notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis],
|
||||
nb_ues,
|
||||
),
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
else:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||
if eval_rat:
|
||||
notes_rat = self.evals_notes[eval_rat.id].values
|
||||
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||
notes_rat = np.where(
|
||||
notes_rat > scu.NOTES_ABSENCE,
|
||||
notes_rat / (eval_rat.note_max / 20.0),
|
||||
np.nan,
|
||||
)
|
||||
# "Étend" le rattrapage sur les UE: la note de rattrapage est la même
|
||||
# pour toutes les UE mais ne remplace que là où elle est supérieure
|
||||
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1)
|
||||
# prend le max
|
||||
etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
|
||||
)
|
||||
# Serie indiquant que l'étudiant utilise une note de rattrapage sur l'une des UE:
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
|
||||
)
|
||||
self.etuds_moy_module = pd.DataFrame(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
@ -471,34 +453,6 @@ class ModuleImplResultsAPC(ModuleImplResults):
|
||||
)
|
||||
return self.etuds_moy_module
|
||||
|
||||
def _compute_moy_special(
|
||||
self,
|
||||
modimpl: ModuleImpl,
|
||||
evals_notes_stacked: np.array,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
evaluation_type: int,
|
||||
) -> np.array:
|
||||
"""Calcul moyenne APC sur évals rattrapage ou session2"""
|
||||
nb_etuds = self.evals_notes.shape[0]
|
||||
nb_ues = evals_poids_df.shape[1]
|
||||
evals_coefs_s2 = self.get_evaluations_special_coefs(
|
||||
modimpl, evaluation_type=evaluation_type
|
||||
)
|
||||
evals_poids_s2 = evals_poids_df.values * evals_coefs_s2
|
||||
poids_stacked_s2 = np.stack(
|
||||
[evals_poids_s2] * nb_etuds
|
||||
) # nb_etuds, nb_evals, nb_ues
|
||||
evals_poids_etuds_s2 = np.where(
|
||||
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
|
||||
poids_stacked_s2,
|
||||
0,
|
||||
)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module_s2 = np.sum(
|
||||
evals_poids_etuds_s2 * evals_notes_stacked, axis=1
|
||||
) / np.sum(evals_poids_etuds_s2, axis=1)
|
||||
return etuds_moy_module_s2
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: pd.DataFrame,
|
||||
@ -571,7 +525,6 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
|
||||
return evals_poids, ues
|
||||
|
||||
|
||||
# appelé par ModuleImpl.check_apc_conformity()
|
||||
def moduleimpl_is_conforme(
|
||||
moduleimpl, evals_poids: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
|
||||
) -> bool:
|
||||
@ -593,12 +546,12 @@ def moduleimpl_is_conforme(
|
||||
if len(modimpl_coefs_df) != nb_ues:
|
||||
# il arrive (#bug) que le cache ne soit pas à jour...
|
||||
sco_cache.invalidate_formsemestre()
|
||||
return app.critical_error("moduleimpl_is_conforme: err 1")
|
||||
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
|
||||
|
||||
if moduleimpl.id not in modimpl_coefs_df:
|
||||
# soupçon de bug cache coef ?
|
||||
sco_cache.invalidate_formsemestre()
|
||||
return app.critical_error("moduleimpl_is_conforme: err 2")
|
||||
raise ScoBugCatcher("Erreur 454 - merci de ré-essayer")
|
||||
|
||||
module_evals_poids = evals_poids.transpose().sum(axis=1) != 0
|
||||
return all((modimpl_coefs_df[moduleimpl.id] != 0).eq(module_evals_poids))
|
||||
@ -640,43 +593,46 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||
evals_coefs_etuds * evals_notes_20, axis=1
|
||||
) / np.sum(evals_coefs_etuds, axis=1)
|
||||
|
||||
evals_session2 = self.get_evaluations_session2(modimpl)
|
||||
evals_rat = self.get_evaluations_rattrapage(modimpl)
|
||||
if evals_session2:
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
# Calcule la moyenne des évaluations de session2
|
||||
etuds_moy_module_s2 = self._compute_moy_special(
|
||||
modimpl, evals_notes_20, Evaluation.EVALUATION_SESSION2
|
||||
)
|
||||
etuds_use_session2 = np.isfinite(etuds_moy_module_s2)
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2,
|
||||
etuds_moy_module_s2,
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
elif evals_rat:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
# Calcule la moyenne des évaluations de rattrapage
|
||||
etuds_moy_module_rat = self._compute_moy_special(
|
||||
modimpl, evals_notes_20, Evaluation.EVALUATION_RATTRAPAGE
|
||||
)
|
||||
etuds_use_rattrapage = etuds_moy_module_rat > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, etuds_moy_module_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage, index=self.evals_notes.index
|
||||
)
|
||||
|
||||
# Application des évaluations bonus:
|
||||
etuds_moy_module = self.apply_bonus(
|
||||
etuds_moy_module,
|
||||
modimpl,
|
||||
evals_notes_20,
|
||||
)
|
||||
|
||||
# Session2 : quand elle existe, remplace la note de module
|
||||
eval_session2 = self.get_evaluation_session2(modimpl)
|
||||
if eval_session2:
|
||||
notes_session2 = self.evals_notes[eval_session2.id].values
|
||||
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
|
||||
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_session2,
|
||||
notes_session2 / (eval_session2.note_max / 20.0),
|
||||
etuds_moy_module,
|
||||
)
|
||||
self.etuds_use_session2 = pd.Series(
|
||||
etuds_use_session2, index=self.evals_notes.index
|
||||
)
|
||||
else:
|
||||
# Rattrapage: remplace la note de module ssi elle est supérieure
|
||||
eval_rat = self.get_evaluation_rattrapage(modimpl)
|
||||
if eval_rat:
|
||||
notes_rat = self.evals_notes[eval_rat.id].values
|
||||
# remplace les notes invalides (ATT, EXC...) par des NaN
|
||||
notes_rat = np.where(
|
||||
notes_rat > scu.NOTES_ABSENCE,
|
||||
notes_rat / (eval_rat.note_max / 20.0),
|
||||
np.nan,
|
||||
)
|
||||
# prend le max
|
||||
etuds_use_rattrapage = notes_rat > etuds_moy_module
|
||||
etuds_moy_module = np.where(
|
||||
etuds_use_rattrapage, notes_rat, etuds_moy_module
|
||||
)
|
||||
self.etuds_use_rattrapage = pd.Series(
|
||||
etuds_use_rattrapage, index=self.evals_notes.index
|
||||
)
|
||||
self.etuds_moy_module = pd.Series(
|
||||
etuds_moy_module,
|
||||
index=self.evals_notes.index,
|
||||
@ -684,28 +640,6 @@ class ModuleImplResultsClassic(ModuleImplResults):
|
||||
|
||||
return self.etuds_moy_module
|
||||
|
||||
def _compute_moy_special(
|
||||
self, modimpl: ModuleImpl, evals_notes_20: np.array, evaluation_type: int
|
||||
) -> np.array:
|
||||
"""Calcul moyenne sur évals rattrapage ou session2"""
|
||||
# n'utilise que les notes valides et ABS (0).
|
||||
# Même calcul que pour les évals normales, mais avec seulement les
|
||||
# coefs des évals de session 2 ou rattrapage:
|
||||
nb_etuds = self.evals_notes.shape[0]
|
||||
evals_coefs = self.get_evaluations_special_coefs(
|
||||
modimpl, evaluation_type=evaluation_type
|
||||
).reshape(-1)
|
||||
coefs_stacked = np.stack([evals_coefs] * nb_etuds)
|
||||
# zéro partout sauf si une note ou ABS:
|
||||
evals_coefs_etuds = np.where(
|
||||
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0
|
||||
)
|
||||
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
|
||||
etuds_moy_module = np.sum(
|
||||
evals_coefs_etuds * evals_notes_20, axis=1
|
||||
) / np.sum(evals_coefs_etuds, axis=1)
|
||||
return etuds_moy_module # array 1d (nb_etuds)
|
||||
|
||||
def apply_bonus(
|
||||
self,
|
||||
etuds_moy_module: np.ndarray,
|
||||
|
@ -183,9 +183,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
||||
return modimpls_notes.swapaxes(0, 1)
|
||||
|
||||
|
||||
def notes_sem_load_cube(
|
||||
formsemestre: FormSemestre, modimpl_coefs_df: pd.DataFrame
|
||||
) -> tuple:
|
||||
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
|
||||
"""Construit le "cube" (tenseur) des notes du semestre.
|
||||
Charge toutes les notes (sql), calcule les moyennes des modules
|
||||
et assemble le cube.
|
||||
@ -209,8 +207,8 @@ def notes_sem_load_cube(
|
||||
etudids, etudids_actifs = formsemestre.etudids_actifs()
|
||||
for modimpl in formsemestre.modimpls_sorted:
|
||||
mod_results = moy_mod.ModuleImplResultsAPC(modimpl, etudids, etudids_actifs)
|
||||
evals_poids = modimpl.get_evaluations_poids()
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids, modimpl_coefs_df)
|
||||
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
|
||||
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
|
||||
modimpls_results[modimpl.id] = mod_results
|
||||
modimpls_evals_poids[modimpl.id] = evals_poids
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
|
@ -59,17 +59,16 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
||||
|
||||
def compute(self):
|
||||
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||
)
|
||||
(
|
||||
self.sem_cube,
|
||||
self.modimpls_evals_poids,
|
||||
self.modimpls_results,
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre, self.modimpl_coefs_df)
|
||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
||||
|
||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||
)
|
||||
# l'idx de la colonne du mod modimpl.id est
|
||||
# modimpl_coefs_df.columns.get_loc(modimpl.id)
|
||||
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
|
||||
|
@ -242,8 +242,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
)
|
||||
}">saisir le coefficient de cette UE avant de continuer</a></p>
|
||||
</div>
|
||||
""",
|
||||
safe=True,
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
|
@ -150,7 +150,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
def etud_ects_tot_sem(self, etudid: int) -> float:
|
||||
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
||||
etud_ues = self.etud_ues(etudid)
|
||||
return sum([ue.ects or 0.0 for ue in etud_ues]) if etud_ues else 0.0
|
||||
return sum([ue.ects or 0 for ue in etud_ues]) if etud_ues else 0.0
|
||||
|
||||
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||
@ -518,8 +518,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
Corrigez ou faite corriger le programme
|
||||
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
||||
""",
|
||||
safe=True,
|
||||
"""
|
||||
)
|
||||
else:
|
||||
# Coefs de l'UE capitalisée en formation classique:
|
||||
|
@ -322,7 +322,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
validations = self.get_formsemestre_validations()
|
||||
return validations.decisions_jury_ues.get(etudid, None)
|
||||
|
||||
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> float:
|
||||
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0:
|
||||
"""Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
|
||||
NB: avant jury, rien d'enregistré, donc zéro ECTS.
|
||||
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decisions_ue()
|
||||
@ -331,7 +331,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
decisions_ues = self.get_etud_decisions_ue(etudid)
|
||||
if not decisions_ues:
|
||||
return 0.0
|
||||
return float(sum(d.get("ects", 0) for d in decisions_ues.values()))
|
||||
return sum([d.get("ects", 0.0) for d in decisions_ues.values()])
|
||||
|
||||
def get_etud_decision_sem(self, etudid: int) -> dict:
|
||||
"""Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||
|
@ -84,9 +84,6 @@ def scodoc(func):
|
||||
|
||||
|
||||
def permission_required(permission):
|
||||
"""Vérifie les permissions"""
|
||||
|
||||
# Attention: l'API utilise api_permission_required
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
|
14
app/email.py
14
app/email.py
@ -9,9 +9,9 @@ import datetime
|
||||
from threading import Thread
|
||||
|
||||
from flask import current_app, g
|
||||
from flask_mail import BadHeaderError, Message
|
||||
from flask_mail import Message
|
||||
|
||||
from app import log, mail
|
||||
from app import mail
|
||||
from app.models.departements import Departement
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc import sco_preferences
|
||||
@ -20,15 +20,7 @@ from app.scodoc import sco_preferences
|
||||
def send_async_email(app, msg):
|
||||
"Send an email, async"
|
||||
with app.app_context():
|
||||
try:
|
||||
mail.send(msg)
|
||||
except BadHeaderError:
|
||||
log(
|
||||
f"""send_async_email: BadHeaderError
|
||||
msg={msg}
|
||||
"""
|
||||
)
|
||||
raise
|
||||
mail.send(msg)
|
||||
|
||||
|
||||
def send_email(
|
||||
|
@ -151,7 +151,7 @@ class EntrepriseHistorique(db.Model):
|
||||
__tablename__ = "are_historique"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
authenticated_user = db.Column(db.Text) # user_name login sans contrainte
|
||||
authenticated_user = db.Column(db.Text)
|
||||
entreprise_id = db.Column(db.Integer)
|
||||
object = db.Column(db.Text)
|
||||
object_id = db.Column(db.Integer)
|
||||
|
@ -338,11 +338,9 @@ def add_entreprise():
|
||||
if form.validate_on_submit():
|
||||
entreprise = Entreprise(
|
||||
nom=form.nom_entreprise.data.strip(),
|
||||
siret=(
|
||||
form.siret.data.strip()
|
||||
if form.siret.data.strip()
|
||||
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}"
|
||||
), # siret provisoire
|
||||
siret=form.siret.data.strip()
|
||||
if form.siret.data.strip()
|
||||
else f"{SIRET_PROVISOIRE_START}{datetime.now().strftime('%d%m%y%H%M%S')}", # siret provisoire
|
||||
siret_provisoire=False if form.siret.data.strip() else True,
|
||||
association=form.association.data,
|
||||
adresse=form.adresse.data.strip(),
|
||||
@ -354,7 +352,7 @@ def add_entreprise():
|
||||
db.session.add(entreprise)
|
||||
db.session.commit()
|
||||
db.session.refresh(entreprise)
|
||||
except Exception:
|
||||
except:
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
@ -806,9 +804,9 @@ def add_offre(entreprise_id):
|
||||
missions=form.missions.data.strip(),
|
||||
duree=form.duree.data.strip(),
|
||||
expiration_date=form.expiration_date.data,
|
||||
correspondant_id=(
|
||||
form.correspondant.data if form.correspondant.data != "" else None
|
||||
),
|
||||
correspondant_id=form.correspondant.data
|
||||
if form.correspondant.data != ""
|
||||
else None,
|
||||
)
|
||||
db.session.add(offre)
|
||||
db.session.commit()
|
||||
@ -1330,11 +1328,9 @@ def add_contact(entreprise_id):
|
||||
).first_or_404(description=f"entreprise {entreprise_id} inconnue")
|
||||
form = ContactCreationForm(
|
||||
date=f"{datetime.now().strftime('%Y-%m-%dT%H:%M')}",
|
||||
utilisateur=(
|
||||
f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||
if current_user.nom and current_user.prenom
|
||||
else ""
|
||||
),
|
||||
utilisateur=f"{current_user.nom} {current_user.prenom} ({current_user.user_name})"
|
||||
if current_user.nom and current_user.prenom
|
||||
else "",
|
||||
)
|
||||
if request.method == "POST" and form.cancel.data:
|
||||
return redirect(url_for("entreprises.contacts", entreprise_id=entreprise_id))
|
||||
@ -1500,9 +1496,9 @@ def add_stage_apprentissage(entreprise_id):
|
||||
date_debut=form.date_debut.data,
|
||||
date_fin=form.date_fin.data,
|
||||
formation_text=formation.formsemestre.titre if formation else None,
|
||||
formation_scodoc=(
|
||||
formation.formsemestre.formsemestre_id if formation else None
|
||||
),
|
||||
formation_scodoc=formation.formsemestre.formsemestre_id
|
||||
if formation
|
||||
else None,
|
||||
notes=form.notes.data.strip(),
|
||||
)
|
||||
db.session.add(stage_apprentissage)
|
||||
@ -1806,7 +1802,7 @@ def import_donnees():
|
||||
db.session.add(entreprise)
|
||||
db.session.commit()
|
||||
db.session.refresh(entreprise)
|
||||
except Exception:
|
||||
except:
|
||||
db.session.rollback()
|
||||
flash("Une erreur est survenue veuillez réessayer.")
|
||||
return render_template(
|
||||
|
@ -62,11 +62,6 @@ class AjoutAssiOrJustForm(FlaskForm):
|
||||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
||||
def disable_all(self):
|
||||
"Disable all fields"
|
||||
for field in self:
|
||||
field.render_kw = {"disabled": True}
|
||||
|
||||
date_debut = StringField(
|
||||
"Date de début",
|
||||
validators=[validators.Length(max=10)],
|
||||
@ -170,7 +165,46 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
)
|
||||
etat = SelectField(
|
||||
"État du justificatif",
|
||||
choices=[], # sera rempli dynamiquement
|
||||
choices=[
|
||||
("", "Choisir..."), # Placeholder
|
||||
(scu.EtatJustificatif.ATTENTE.value, "En attente de validation"),
|
||||
(scu.EtatJustificatif.NON_VALIDE.value, "Non valide"),
|
||||
(scu.EtatJustificatif.MODIFIE.value, "Modifié"),
|
||||
(scu.EtatJustificatif.VALIDE.value, "Valide"),
|
||||
],
|
||||
validators=[DataRequired(message="This field is required.")],
|
||||
)
|
||||
fichiers = MultipleFileField(label="Ajouter des fichiers")
|
||||
|
||||
|
||||
class ChoixDateForm(FlaskForm):
|
||||
"""
|
||||
Formulaire de choix de date
|
||||
(utilisé par la page de choix de date
|
||||
si la date courante n'est pas dans le semestre)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.ok = True
|
||||
self.error_messages: list[str] = [] # used to report our errors
|
||||
|
||||
def set_error(self, err_msg, field=None):
|
||||
"Set error message both in form and field"
|
||||
self.ok = False
|
||||
self.error_messages.append(err_msg)
|
||||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
||||
date = StringField(
|
||||
"Date",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "date",
|
||||
},
|
||||
)
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
@ -1,122 +0,0 @@
|
||||
""" """
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import (
|
||||
StringField,
|
||||
SelectField,
|
||||
RadioField,
|
||||
TextAreaField,
|
||||
validators,
|
||||
SubmitField,
|
||||
)
|
||||
from app.scodoc.sco_utils import EtatAssiduite
|
||||
|
||||
|
||||
class EditAssiForm(FlaskForm):
|
||||
"""
|
||||
Formulaire de modification d'une assiduité
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.ok = True
|
||||
self.error_messages: list[str] = [] # used to report our errors
|
||||
|
||||
def set_error(self, err_msg, field=None):
|
||||
"Set error message both in form and field"
|
||||
self.ok = False
|
||||
self.error_messages.append(err_msg)
|
||||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
||||
def disable_all(self):
|
||||
"Disable all fields"
|
||||
for field in self:
|
||||
field.render_kw = {"disabled": True}
|
||||
|
||||
assi_etat = RadioField(
|
||||
"État:",
|
||||
choices=[
|
||||
(EtatAssiduite.ABSENT.value, EtatAssiduite.ABSENT.version_lisible()),
|
||||
(EtatAssiduite.RETARD.value, EtatAssiduite.RETARD.version_lisible()),
|
||||
(EtatAssiduite.PRESENT.value, EtatAssiduite.PRESENT.version_lisible()),
|
||||
],
|
||||
default="absent",
|
||||
validators=[
|
||||
validators.DataRequired("spécifiez le type d'évènement à signaler"),
|
||||
],
|
||||
)
|
||||
modimpl = SelectField(
|
||||
"Module",
|
||||
choices={}, # will be populated dynamically
|
||||
)
|
||||
description = TextAreaField(
|
||||
"Description",
|
||||
render_kw={
|
||||
"id": "description",
|
||||
"cols": 75,
|
||||
"rows": 4,
|
||||
"maxlength": 500,
|
||||
},
|
||||
)
|
||||
date_debut = StringField(
|
||||
"Date de début",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "assi_date_debut",
|
||||
},
|
||||
)
|
||||
heure_debut = StringField(
|
||||
"Heure début",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_heure_debut",
|
||||
},
|
||||
)
|
||||
heure_fin = StringField(
|
||||
"Heure fin",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_heure_fin",
|
||||
},
|
||||
)
|
||||
date_fin = StringField(
|
||||
"Date de fin",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "assi_date_fin",
|
||||
},
|
||||
)
|
||||
entry_date = StringField(
|
||||
"Date de dépôt ou saisie",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "entry_date",
|
||||
},
|
||||
)
|
||||
entry_time = StringField(
|
||||
"Heure dépôt",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_heure_fin",
|
||||
},
|
||||
)
|
||||
|
||||
submit = SubmitField("Enregistrer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
@ -140,8 +140,8 @@ class ConfigAssiduitesForm(FlaskForm):
|
||||
)
|
||||
edt_ics_title_regexp = StringField(
|
||||
label="Extraction du titre",
|
||||
description=r"""expression régulière python dont le premier groupe
|
||||
sera le titre de l'évènement affiché dans le calendrier ScoDoc.
|
||||
description=r"""expression régulière python dont le premier groupe doit
|
||||
sera le titre de l'évènement affcihé dans le calendrier ScoDoc.
|
||||
Exemple: <tt>Matière : \w+ - ([\w\.\s']+)</tt>
|
||||
""",
|
||||
validators=[Optional(), check_ics_regexp],
|
||||
|
@ -1,66 +0,0 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaire création de ticket de bug
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField, TextAreaField, BooleanField
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class CreateBugReport(FlaskForm):
|
||||
"""Formulaire permettant la création d'un ticket de bug"""
|
||||
|
||||
title = StringField(
|
||||
label="Titre du ticket",
|
||||
validators=[
|
||||
validators.DataRequired("titre du ticket requis"),
|
||||
],
|
||||
)
|
||||
message = TextAreaField(
|
||||
label="Message",
|
||||
id="ticket_message",
|
||||
validators=[
|
||||
validators.DataRequired("message du ticket requis"),
|
||||
],
|
||||
)
|
||||
etab = StringField(label="Etablissement")
|
||||
include_dump = BooleanField(
|
||||
"""Inclure une copie anonymisée de la base de données ?
|
||||
Ces données faciliteront le traitement du problème et resteront strictement confidentielles.
|
||||
""",
|
||||
default=False,
|
||||
)
|
||||
submit = SubmitField("Envoyer")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateBugReport, self).__init__(*args, **kwargs)
|
||||
self.etab.data = sco_preferences.get_preference("InstituteName") or ""
|
@ -1,56 +0,0 @@
|
||||
"""
|
||||
Formulaire FlaskWTF pour les groupes
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, validators
|
||||
|
||||
|
||||
class FeuilleAppelPreForm(FlaskForm):
|
||||
"""
|
||||
Formulaire utiliser dans le téléchargement des feuilles d'émargement
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.ok = True
|
||||
self.error_messages: list[str] = []
|
||||
|
||||
def set_error(self, err_msg, field=None):
|
||||
"Set error message both in form and field"
|
||||
self.ok = False
|
||||
self.error_messages.append(err_msg)
|
||||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
||||
discipline = StringField(
|
||||
"Discipline",
|
||||
)
|
||||
|
||||
ens = StringField(
|
||||
"Enseignant",
|
||||
)
|
||||
|
||||
date = StringField(
|
||||
"Date de la séance",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "date",
|
||||
},
|
||||
)
|
||||
|
||||
heure = StringField(
|
||||
"Heure de début de la séance",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "heure",
|
||||
},
|
||||
)
|
||||
|
||||
submit = SubmitField("Télécharger la liste d'émargement")
|
@ -3,9 +3,7 @@
|
||||
"""Modèles base de données ScoDoc
|
||||
"""
|
||||
|
||||
from flask import abort, g
|
||||
import sqlalchemy
|
||||
import app
|
||||
from app import db
|
||||
|
||||
CODE_STR_LEN = 16 # chaine pour les codes
|
||||
@ -118,42 +116,6 @@ class ScoDocModel(db.Model):
|
||||
args = {field.name: field.data for field in form}
|
||||
return self.from_dict(args)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, oid: int, accept_none=False):
|
||||
"""Instance du modèle ou ou 404 (ou None si accept_none),
|
||||
cherche uniquement dans le département courant.
|
||||
Ne fonctionne que si le modèle a un attribut dept_id.
|
||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
||||
pas à une validation.
|
||||
"""
|
||||
if not isinstance(oid, int):
|
||||
try:
|
||||
oid = int(oid)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "oid invalide")
|
||||
|
||||
if g.scodoc_dept:
|
||||
if hasattr(cls, "_sco_dept_relations"):
|
||||
# Quand dept_id n'est pas dans le modèle courant,
|
||||
# cet attribut indique la liste des tables à joindre pour
|
||||
# obtenir le departement.
|
||||
query = cls.query.filter_by(id=oid)
|
||||
for relation_name in cls._sco_dept_relations:
|
||||
query = query.join(getattr(app.models, relation_name))
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
else:
|
||||
# département accessible dans le modèle courant
|
||||
query = cls.query.filter_by(id=oid, dept_id=g.scodoc_dept_id)
|
||||
else:
|
||||
# Pas de département courant (API non départementale)
|
||||
query = cls.query.filter_by(id=oid)
|
||||
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
|
||||
|
||||
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
||||
from app.models.departements import Departement
|
||||
@ -211,11 +173,7 @@ from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcSituationPro,
|
||||
)
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
|
||||
|
@ -7,10 +7,7 @@ from app import db
|
||||
|
||||
|
||||
class Absence(db.Model):
|
||||
"""LEGACY
|
||||
Ce modèle n'est PLUS UTILISE depuis ScoDoc 9.6 et remplacé par assiduité.
|
||||
une absence (sur une demi-journée)
|
||||
"""
|
||||
"""une absence (sur une demi-journée)"""
|
||||
|
||||
__tablename__ = "absences"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -21,7 +21,6 @@ from app.scodoc import sco_abs_notification
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
@ -189,12 +188,6 @@ class Assiduite(ScoDocModel):
|
||||
):
|
||||
raise ScoValueError("La date de fin n'est pas un jour travaillé")
|
||||
|
||||
# Vérification de l'activation du module
|
||||
if (err_msg := has_assiduites_disable_pref(formsemestre_date_debut)) or (
|
||||
err_msg := has_assiduites_disable_pref(formsemestre_date_fin)
|
||||
):
|
||||
raise ScoValueError(err_msg)
|
||||
|
||||
# Vérification de non duplication des périodes
|
||||
assiduites: Query = etud.assiduites
|
||||
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||
@ -348,7 +341,7 @@ class Assiduite(ScoDocModel):
|
||||
"""
|
||||
Retourne le module associé à l'assiduité
|
||||
Si traduire est vrai, retourne le titre du module précédé du code
|
||||
Sinon retourne l'objet Module ou None
|
||||
Sinon rentourne l'objet Module ou None
|
||||
"""
|
||||
|
||||
if self.moduleimpl_id is not None:
|
||||
@ -358,24 +351,14 @@ class Assiduite(ScoDocModel):
|
||||
return f"{mod.code} {mod.titre}"
|
||||
return mod
|
||||
|
||||
if self.external_data is not None and "module" in self.external_data:
|
||||
elif self.external_data is not None and "module" in self.external_data:
|
||||
return (
|
||||
"Autre module (pas dans la liste)"
|
||||
"Tout module"
|
||||
if self.external_data["module"] == "Autre"
|
||||
else self.external_data["module"]
|
||||
)
|
||||
|
||||
return "Module non spécifié" if traduire else None
|
||||
|
||||
def get_moduleimpl_id(self) -> int | str | None:
|
||||
"""
|
||||
Retourne le ModuleImpl associé à l'assiduité
|
||||
"""
|
||||
if self.moduleimpl_id is not None:
|
||||
return self.moduleimpl_id
|
||||
if self.external_data is not None and "module" in self.external_data:
|
||||
return self.external_data["module"]
|
||||
return None
|
||||
return "Non spécifié" if traduire else None
|
||||
|
||||
def get_saisie(self) -> str:
|
||||
"""
|
||||
@ -412,14 +395,6 @@ class Assiduite(ScoDocModel):
|
||||
if force:
|
||||
raise ScoValueError("Module non renseigné")
|
||||
|
||||
@classmethod
|
||||
def get_assiduite(cls, assiduite_id: int) -> "Assiduite":
|
||||
"""Assiduité ou 404, cherche uniquement dans le département courant"""
|
||||
query = Assiduite.query.filter_by(id=assiduite_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
return query.first_or_404()
|
||||
|
||||
|
||||
class Justificatif(ScoDocModel):
|
||||
"""
|
||||
@ -565,13 +540,13 @@ class Justificatif(ScoDocModel):
|
||||
Raises ScoValueError si paramètres incorrects.
|
||||
"""
|
||||
nouv_justificatif = cls.create_from_dict(locals())
|
||||
db.session.commit()
|
||||
log(f"create_justificatif: etudid={etudiant.id} {nouv_justificatif}")
|
||||
Scolog.logdb(
|
||||
method="create_justificatif",
|
||||
etudid=etudiant.id,
|
||||
msg=f"justificatif: {nouv_justificatif}",
|
||||
)
|
||||
db.session.commit()
|
||||
return nouv_justificatif
|
||||
|
||||
def supprime(self):
|
||||
@ -710,14 +685,10 @@ def is_period_conflicting(
|
||||
date_fin: datetime,
|
||||
collection: Query,
|
||||
collection_cls: Assiduite | Justificatif,
|
||||
obj_id: int = -1,
|
||||
) -> bool:
|
||||
"""
|
||||
Vérifie si une date n'entre pas en collision
|
||||
avec les justificatifs ou assiduites déjà présentes
|
||||
|
||||
On peut donner un objet_id pour exclure un objet de la vérification
|
||||
(utile pour les modifications)
|
||||
"""
|
||||
|
||||
# On s'assure que les dates soient avec TimeZone
|
||||
@ -725,9 +696,7 @@ def is_period_conflicting(
|
||||
date_fin = localize_datetime(date_fin)
|
||||
|
||||
count: int = collection.filter(
|
||||
collection_cls.date_debut < date_fin,
|
||||
collection_cls.date_fin > date_debut,
|
||||
collection_cls.id != obj_id,
|
||||
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
||||
).count()
|
||||
|
||||
return count > 0
|
||||
@ -824,29 +793,3 @@ def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def has_assiduites_disable_pref(formsemestre: FormSemestre) -> str | bool:
|
||||
"""
|
||||
Vérifie si le semestre possède la préférence "assiduites_disable"
|
||||
et renvoie le message d'erreur associé.
|
||||
|
||||
La préférence est un text field. Il est considéré comme vide si :
|
||||
- la chaine de caractère est vide
|
||||
- si elle n'est composée que de caractères d'espacement (espace, tabulation, retour à la ligne)
|
||||
|
||||
|
||||
Si la chaine est vide, la fonction renvoie False
|
||||
"""
|
||||
|
||||
# Si pas de formsemestre, on ne peut pas vérifier la préférence
|
||||
# On considère que la préférence n'est pas activée
|
||||
if formsemestre is None:
|
||||
return False
|
||||
|
||||
pref: str = (
|
||||
sco_preferences.get_preference("assiduites_disable", formsemestre.id) or ""
|
||||
)
|
||||
pref = pref.strip()
|
||||
|
||||
return pref if pref else False
|
||||
|
@ -274,11 +274,6 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
return "type_departement mismatch"
|
||||
# Table d'équivalences entre refs:
|
||||
equiv = self._load_config_equivalences()
|
||||
# Même specialité (ou alias) ?
|
||||
if self.specialite != other.specialite and other.specialite not in equiv.get(
|
||||
"alias", []
|
||||
):
|
||||
return "specialite mismatch"
|
||||
# mêmes parcours ?
|
||||
eq_parcours = equiv.get("parcours", {})
|
||||
parcours_by_code_1 = {eq_parcours.get(p.code, p.code): p for p in self.parcours}
|
||||
@ -322,9 +317,6 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
||||
def _load_config_equivalences(self) -> dict:
|
||||
"""Load config file ressources/referentiels/equivalences.yaml
|
||||
used to define equivalences between distinct referentiels
|
||||
return a dict, with optional keys:
|
||||
alias: list of equivalent names for speciality (eg SD == STID)
|
||||
parcours: dict with equivalent parcours acronyms
|
||||
"""
|
||||
try:
|
||||
with open(REFCOMP_EQUIVALENCE_FILENAME, encoding="utf-8") as f:
|
||||
@ -604,7 +596,6 @@ app_critiques_modules = db.Table(
|
||||
db.ForeignKey("apc_app_critique.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
db.UniqueConstraint("module_id", "app_crit_id", name="uix_module_id_app_crit_id"),
|
||||
)
|
||||
|
||||
|
||||
|
@ -2,12 +2,9 @@
|
||||
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN, ScoDocModel
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
@ -16,7 +13,7 @@ from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class ApcValidationRCUE(ScoDocModel):
|
||||
class ApcValidationRCUE(db.Model):
|
||||
"""Validation des niveaux de compétences
|
||||
|
||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||
@ -61,8 +58,6 @@ class ApcValidationRCUE(ScoDocModel):
|
||||
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
|
||||
parcour = db.relationship("ApcParcours")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud} {
|
||||
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||
@ -118,14 +113,8 @@ class ApcValidationRCUE(ScoDocModel):
|
||||
"formsemestre_id": self.formsemestre_id,
|
||||
}
|
||||
|
||||
def get_codes_apogee(self) -> set[str]:
|
||||
"""Les codes Apogée associés à cette validation RCUE.
|
||||
Prend les codes des deux UEs
|
||||
"""
|
||||
return self.ue1.get_codes_apogee_rcue() | self.ue2.get_codes_apogee_rcue()
|
||||
|
||||
|
||||
class ApcValidationAnnee(ScoDocModel):
|
||||
class ApcValidationAnnee(db.Model):
|
||||
"""Validation des années du BUT"""
|
||||
|
||||
__tablename__ = "apc_validation_annee"
|
||||
@ -156,8 +145,6 @@ class ApcValidationAnnee(ScoDocModel):
|
||||
etud = db.relationship("Identite", backref="apc_validations_annees")
|
||||
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
||||
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
||||
@ -215,9 +202,17 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
.order_by(UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
||||
titres_rcues = _build_decisions_rcue_list(decisions["decision_rcue"])
|
||||
titres_rcues = []
|
||||
for dec_rcue in decisions["decision_rcue"]:
|
||||
niveau = dec_rcue["niveau"]
|
||||
if niveau is None:
|
||||
titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
|
||||
else:
|
||||
titres_rcues.append(
|
||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {
|
||||
dec_rcue["code"]}"""
|
||||
)
|
||||
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
||||
decisions["descr_decisions_rcue_list"] = titres_rcues
|
||||
decisions["descr_decisions_niveaux"] = (
|
||||
"Niveaux de compétences: " + decisions["descr_decisions_rcue"]
|
||||
)
|
||||
@ -241,112 +236,3 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
else:
|
||||
decisions["decision_annee"] = None
|
||||
return decisions
|
||||
|
||||
|
||||
def _build_decisions_rcue_list(decisions_rcue: dict) -> list[str]:
|
||||
"""Formatte liste des décisions niveaux de compétences / RCUE pour
|
||||
lettres individuelles.
|
||||
Le résulat est trié par compétence et donne pour chaque niveau avec validation:
|
||||
[ 'Administrer: niveau 1 ADM, niveau 2 ADJ', ... ]
|
||||
"""
|
||||
# Construit { id_competence : validation }
|
||||
# où validation est {'code': 'CMP', 'niveau': {'annee': 'BUT3', 'competence': {}, ... }
|
||||
validation_by_competence = defaultdict(list)
|
||||
for validation in decisions_rcue:
|
||||
if validation:
|
||||
# Attention, certaines validations de RCUE peuvent ne plus être associées
|
||||
# à un niveau de compétence si l'UE a été déassociée (ce qui ne devrait pas être fait)
|
||||
competence_id = (
|
||||
(validation.get("niveau") or {}).get("competence") or {}
|
||||
).get("id_orebut")
|
||||
validation_by_competence[competence_id].append(validation)
|
||||
# Tri des listes de validation par numéro de compétence
|
||||
validations_niveaux = sorted(
|
||||
validation_by_competence.values(),
|
||||
key=lambda v: (
|
||||
((v[0].get("niveau") or {}).get("competence") or {}).get("numero", 0)
|
||||
if v
|
||||
else -1
|
||||
),
|
||||
)
|
||||
titres_rcues = []
|
||||
empty = {} # pour syntaxe f-string
|
||||
for validations in validations_niveaux:
|
||||
if validations:
|
||||
v = validations[0]
|
||||
titre_competence = ((v.get("niveau") or {}).get("competence", {})).get(
|
||||
"titre", "sans titre ! A vérifier !"
|
||||
)
|
||||
titres_rcues.append(
|
||||
f"""{titre_competence} : """
|
||||
+ ", ".join(
|
||||
[
|
||||
f"niveau {((v.get('niveau') or empty).get('ordre') or '?')} {v.get('code', '?')}"
|
||||
for v in validations
|
||||
]
|
||||
)
|
||||
)
|
||||
return titres_rcues
|
||||
|
||||
|
||||
class ValidationDUT120(ScoDocModel):
|
||||
"""Validations du DUT 120
|
||||
Ce diplôme est attribué sur demande aux étudiants de BUT ayant acquis les 120 ECTS
|
||||
de BUT 1 et BUT 2.
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
"""le semestre origine, dans la plupart des cas le S4 (le diplôme DUT120
|
||||
apparaîtra sur les PV de ce formsemestre)"""
|
||||
referentiel_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
) # pas de cascade, on ne doit pas supprimer un référentiel utilisé
|
||||
"""Identifie la spécialité de DUT décernée"""
|
||||
date = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
"""Date de délivrance"""
|
||||
|
||||
etud = db.relationship("Identite", backref="validations_dut120")
|
||||
formsemestre = db.relationship("FormSemestre", backref="validations_dut120")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<ValidationDUT120 {self.etud}>"""
|
||||
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
date_str = (
|
||||
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
|
||||
if self.date
|
||||
else "(sans date)"
|
||||
)
|
||||
link = (
|
||||
self.formsemestre.html_link_status(
|
||||
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
||||
title=self.formsemestre.titre_annee(),
|
||||
)
|
||||
if self.formsemestre
|
||||
else "externe/antérieure"
|
||||
)
|
||||
specialite = (
|
||||
self.formsemestre.formation.referentiel_competence.get_title()
|
||||
if self.formsemestre.formation.referentiel_competence
|
||||
else "(désassociée!)"
|
||||
)
|
||||
return f"""Diplôme de <b>DUT en 120 ECTS du {specialite}</b> émis par
|
||||
{link}
|
||||
{date_str}
|
||||
"""
|
||||
|
@ -261,7 +261,7 @@ class ScoDocSiteConfig(db.Model):
|
||||
|
||||
@classmethod
|
||||
def is_bul_pdf_disabled(cls) -> bool:
|
||||
"""True si on interdit les exports PDF des bulletins"""
|
||||
"""True si on interdit les exports PDF des bulltins"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
|
||||
return cfg is not None and cfg.value
|
||||
|
||||
|
@ -52,17 +52,6 @@ class Departement(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
||||
|
||||
@classmethod
|
||||
def get_departement(cls, dept_ident: str | int) -> "Departement":
|
||||
"Le département, par id ou acronyme. Erreur 404 si pas trouvé."
|
||||
try:
|
||||
dept_id = int(dept_ident)
|
||||
except ValueError:
|
||||
dept_id = None
|
||||
if dept_id is None:
|
||||
return cls.query.filter_by(acronym=dept_ident).first_or_404()
|
||||
return cls.query.get_or_404(dept_id)
|
||||
|
||||
def to_dict(self, with_dept_name=True, with_dept_preferences=False):
|
||||
data = {
|
||||
"id": self.id,
|
||||
|
@ -179,9 +179,7 @@ class Identite(models.ScoDocModel):
|
||||
|
||||
def html_link_fiche(self) -> str:
|
||||
"lien vers la fiche"
|
||||
return (
|
||||
f"""<a class="etudlink" href="{self.url_fiche()}">{self.nom_prenom()}</a>"""
|
||||
)
|
||||
return f"""<a class="etudlink" href="{self.url_fiche()}">{self.nomprenom}</a>"""
|
||||
|
||||
def url_fiche(self) -> str:
|
||||
"url de la fiche étudiant"
|
||||
@ -199,28 +197,13 @@ class Identite(models.ScoDocModel):
|
||||
return cls.query.filter_by(**args).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def get_etud(cls, etudid: int, accept_none=False) -> "Identite":
|
||||
"""Etudiant ou 404 (ou None si accept_none),
|
||||
cherche uniquement dans le département courant.
|
||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
||||
pas à un étudiant.
|
||||
"""
|
||||
if not isinstance(etudid, int):
|
||||
try:
|
||||
etudid = int(etudid)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "etudid invalide")
|
||||
|
||||
query = (
|
||||
cls.query.filter_by(id=etudid, dept_id=g.scodoc_dept_id)
|
||||
if g.scodoc_dept
|
||||
else cls.query.filter_by(id=etudid)
|
||||
)
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
def get_etud(cls, etudid: int) -> "Identite":
|
||||
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||
if g.scodoc_dept:
|
||||
return cls.query.filter_by(
|
||||
id=etudid, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
return cls.query.filter_by(id=etudid).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_etud(cls, **args) -> "Identite":
|
||||
@ -316,10 +299,9 @@ class Identite(models.ScoDocModel):
|
||||
|
||||
@property
|
||||
def nomprenom(self, reverse=False) -> str:
|
||||
"""DEPRECATED: préférer nom_prenom()
|
||||
Civilité/prénom/nom pour affichages: "M. Pierre Dupont"
|
||||
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||
Si reverse, "Dupont Pierre", sans civilité.
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
Prend l'identité courant et non celle de l'état civile si elles diffèrent.
|
||||
"""
|
||||
nom = self.nom_usuel or self.nom
|
||||
prenom = self.prenom_str
|
||||
@ -327,12 +309,6 @@ class Identite(models.ScoDocModel):
|
||||
return f"{nom} {prenom}".strip()
|
||||
return f"{self.civilite_str} {prenom} {nom}".strip()
|
||||
|
||||
def nom_prenom(self) -> str:
|
||||
"""Civilite NOM Prénom
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
"""
|
||||
return f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()} {self.prenom_str}"
|
||||
|
||||
@property
|
||||
def prenom_str(self):
|
||||
"""Prénom à afficher. Par exemple: "Jean-Christophe" """
|
||||
@ -371,15 +347,14 @@ class Identite(models.ScoDocModel):
|
||||
"Le mail associé à la première adresse de l'étudiant, ou None"
|
||||
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
|
||||
|
||||
def get_formsemestres(self, recent_first=True) -> list:
|
||||
def get_formsemestres(self) -> list:
|
||||
"""Liste des formsemestres dans lesquels l'étudiant est (a été) inscrit,
|
||||
triée par date_debut, le plus récent d'abord (comme "sems" de scodoc7)
|
||||
(si recent_first=False, le plus ancien en tête)
|
||||
triée par date_debut
|
||||
"""
|
||||
return sorted(
|
||||
[ins.formsemestre for ins in self.formsemestre_inscriptions],
|
||||
key=attrgetter("date_debut"),
|
||||
reverse=recent_first,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def get_modimpls_by_formsemestre(
|
||||
@ -418,18 +393,6 @@ class Identite(models.ScoDocModel):
|
||||
modimpls_by_formsemestre[formsemestre.id] = modimpls_sem
|
||||
return modimpls_by_formsemestre
|
||||
|
||||
def get_modimpls_from_formsemestre(
|
||||
self, formsemestre: "FormSemestre"
|
||||
) -> list["ModuleImpl"]:
|
||||
"""
|
||||
Liste des ModuleImpl auxquels l'étudiant est inscrit dans le formsemestre.
|
||||
"""
|
||||
modimpls = ModuleImpl.query.join(ModuleImplInscription).filter(
|
||||
ModuleImplInscription.etudid == self.id,
|
||||
ModuleImpl.formsemestre_id == formsemestre.id,
|
||||
)
|
||||
return modimpls.all()
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict. No other side effect.
|
||||
@ -588,7 +551,7 @@ class Identite(models.ScoDocModel):
|
||||
.all()
|
||||
)
|
||||
|
||||
def inscription_courante(self) -> "FormSemestreInscription | None":
|
||||
def inscription_courante(self):
|
||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||
"""
|
||||
@ -830,7 +793,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
|
||||
dest_endpoint = "notes.index_html"
|
||||
parameters = {}
|
||||
|
||||
err_page = f"""<h3>Code étudiant ({code_name}) dupliqué !</h3>
|
||||
err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
|
||||
<p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
|
||||
ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
|
||||
</p>
|
||||
@ -845,7 +808,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
|
||||
|
||||
log(f"*** error: code {code_name} duplique: {args[code_name]}")
|
||||
|
||||
raise ScoGenError(err_page, safe=True)
|
||||
raise ScoGenError(err_page)
|
||||
|
||||
|
||||
def make_etud_args(
|
||||
@ -1080,9 +1043,8 @@ class Admission(models.ScoDocModel):
|
||||
return args_dict
|
||||
|
||||
|
||||
class ItemSuivi(models.ScoDocModel):
|
||||
"""Suivi scolarité / débouchés"""
|
||||
|
||||
# Suivi scolarité / débouchés
|
||||
class ItemSuivi(db.Model):
|
||||
__tablename__ = "itemsuivi"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -1094,8 +1056,6 @@ class ItemSuivi(models.ScoDocModel):
|
||||
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
situation = db.Column(db.Text)
|
||||
|
||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||
|
||||
|
||||
class ItemSuiviTag(db.Model):
|
||||
__tablename__ = "itemsuivi_tags"
|
||||
@ -1117,7 +1077,7 @@ itemsuivi_tags_assoc = db.Table(
|
||||
)
|
||||
|
||||
|
||||
class EtudAnnotation(models.ScoDocModel):
|
||||
class EtudAnnotation(db.Model):
|
||||
"""Annotation sur un étudiant"""
|
||||
|
||||
__tablename__ = "etud_annotations"
|
||||
@ -1128,8 +1088,6 @@ class EtudAnnotation(models.ScoDocModel):
|
||||
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||
comment = db.Column(db.Text)
|
||||
|
||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||
|
||||
def to_dict(self):
|
||||
"""Représentation dictionnaire."""
|
||||
e = dict(self.__dict__)
|
||||
|
@ -60,8 +60,6 @@ class Evaluation(models.ScoDocModel):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||
|
||||
_sco_dept_relations = ("ModuleImpl", "FormSemestre") # accès au dept_id
|
||||
|
||||
EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer !
|
||||
EVALUATION_RATTRAPAGE = 1
|
||||
EVALUATION_SESSION2 = 2
|
||||
@ -73,15 +71,6 @@ class Evaluation(models.ScoDocModel):
|
||||
EVALUATION_BONUS,
|
||||
}
|
||||
|
||||
def type_abbrev(self) -> str:
|
||||
"Le nom abrégé du type de cette éval."
|
||||
return {
|
||||
self.EVALUATION_NORMALE: "std",
|
||||
self.EVALUATION_RATTRAPAGE: "rattrapage",
|
||||
self.EVALUATION_SESSION2: "session 2",
|
||||
self.EVALUATION_BONUS: "bonus",
|
||||
}.get(self.evaluation_type, "?")
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<Evaluation {self.id} {
|
||||
self.date_debut.isoformat() if self.date_debut else ''} "{
|
||||
@ -269,12 +258,10 @@ class Evaluation(models.ScoDocModel):
|
||||
|
||||
@classmethod
|
||||
def get_evaluation(
|
||||
cls, evaluation_id: int | str, dept_id: int = None, accept_none=False
|
||||
cls, evaluation_id: int | str, dept_id: int = None
|
||||
) -> "Evaluation":
|
||||
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant.
|
||||
Si accept_none, return None si l'id est invalide ou n'existe pas.
|
||||
"""
|
||||
from app.models import FormSemestre
|
||||
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
||||
from app.models import FormSemestre, ModuleImpl
|
||||
|
||||
if not isinstance(evaluation_id, int):
|
||||
try:
|
||||
@ -286,8 +273,6 @@ class Evaluation(models.ScoDocModel):
|
||||
query = cls.query.filter_by(id=evaluation_id)
|
||||
if dept_id is not None:
|
||||
query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id)
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
|
||||
@classmethod
|
||||
@ -369,8 +354,6 @@ class Evaluation(models.ScoDocModel):
|
||||
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||
if self.date_debut.date() == self.date_fin.date(): # même jour
|
||||
if self.date_debut.time() == self.date_fin.time():
|
||||
if self.date_fin.time() == datetime.time(0, 0):
|
||||
return f"le {self.date_debut.strftime('%d/%m/%Y')}" # sans heure
|
||||
return (
|
||||
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||
)
|
||||
@ -434,13 +417,12 @@ class Evaluation(models.ScoDocModel):
|
||||
return modified
|
||||
|
||||
def set_ue_poids(self, ue, poids: float) -> None:
|
||||
"""Set poids évaluation vers cette UE. Commit."""
|
||||
"""Set poids évaluation vers cette UE"""
|
||||
self.update_ue_poids_dict({ue.id: poids})
|
||||
|
||||
def set_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
"""set poids vers les UE (remplace existants)
|
||||
ue_poids_dict = { ue_id : poids }
|
||||
Commit session.
|
||||
"""
|
||||
from app.models.ues import UniteEns
|
||||
|
||||
@ -450,12 +432,9 @@ class Evaluation(models.ScoDocModel):
|
||||
if ue is None:
|
||||
raise ScoValueError("poids vers une UE inexistante")
|
||||
ue_poids = EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)
|
||||
db.session.add(ue_poids)
|
||||
L.append(ue_poids)
|
||||
|
||||
db.session.add(ue_poids)
|
||||
self.ue_poids = L # backref # pylint:disable=attribute-defined-outside-init
|
||||
|
||||
db.session.commit()
|
||||
self.moduleimpl.invalidate_evaluations_poids() # inval cache
|
||||
|
||||
def update_ue_poids_dict(self, ue_poids_dict: dict) -> None:
|
||||
|
@ -12,12 +12,12 @@ from app import db
|
||||
from app import email
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models import ScoDocModel, SHORT_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class Scolog(ScoDocModel):
|
||||
class Scolog(db.Model):
|
||||
"""Log des actions (journal modif etudiants)"""
|
||||
|
||||
__tablename__ = "scolog"
|
||||
@ -27,15 +27,14 @@ class Scolog(ScoDocModel):
|
||||
method = db.Column(db.Text)
|
||||
msg = db.Column(db.Text)
|
||||
etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression
|
||||
authenticated_user = db.Column(db.Text) # user_name login, sans contrainte
|
||||
authenticated_user = db.Column(db.Text) # login, sans contrainte
|
||||
# zope_remote_addr suppressed
|
||||
|
||||
@classmethod
|
||||
def logdb(
|
||||
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
|
||||
):
|
||||
"""Add entry in student's log (replacement for old scolog.logdb).
|
||||
Par défaut ne commite pas."""
|
||||
"""Add entry in student's log (replacement for old scolog.logdb)"""
|
||||
entry = Scolog(
|
||||
method=method,
|
||||
msg=msg,
|
||||
@ -46,21 +45,6 @@ class Scolog(ScoDocModel):
|
||||
if commit:
|
||||
db.session.commit()
|
||||
|
||||
def to_dict(self, convert_date=False) -> dict:
|
||||
"convert to dict"
|
||||
return {
|
||||
"etudid": self.etudid,
|
||||
"date": (
|
||||
(self.date.strftime(scu.DATETIME_FMT) if convert_date else self.date)
|
||||
if self.date
|
||||
else ""
|
||||
),
|
||||
"_date_order": self.date.isoformat() if self.date else "",
|
||||
"authenticated_user": self.authenticated_user or "",
|
||||
"msg": self.msg or "",
|
||||
"method": self.method or "",
|
||||
}
|
||||
|
||||
|
||||
class ScolarNews(db.Model):
|
||||
"""Nouvelles pour page d'accueil"""
|
||||
@ -92,9 +76,7 @@ class ScolarNews(db.Model):
|
||||
date = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), index=True
|
||||
)
|
||||
authenticated_user = db.Column(
|
||||
db.Text, index=True
|
||||
) # user_name login, sans contrainte
|
||||
authenticated_user = db.Column(db.Text, index=True) # login, sans contrainte
|
||||
# type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
|
||||
type = db.Column(db.String(SHORT_STR_LEN), index=True)
|
||||
object = db.Column(
|
||||
|
@ -7,7 +7,7 @@ from flask_sqlalchemy.query import Query
|
||||
import app
|
||||
from app import db
|
||||
from app.comp import df_cache
|
||||
from app.models import ScoDocModel, SHORT_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
@ -23,7 +23,7 @@ from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
|
||||
|
||||
class Formation(ScoDocModel):
|
||||
class Formation(db.Model):
|
||||
"""Programme pédagogique d'une formation"""
|
||||
|
||||
__tablename__ = "notes_formations"
|
||||
@ -297,7 +297,7 @@ class Formation(ScoDocModel):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Matiere(ScoDocModel):
|
||||
class Matiere(db.Model):
|
||||
"""Matières: regroupe les modules d'une UE
|
||||
La matière a peu d'utilité en dehors de la présentation des modules
|
||||
d'une UE.
|
||||
@ -313,7 +313,6 @@ class Matiere(ScoDocModel):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||
_sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
|
||||
|
@ -36,7 +36,6 @@ from app.models.config import ScoDocSiteConfig
|
||||
from app.models.departements import Departement
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.formations import Formation
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.models.moduleimpls import (
|
||||
@ -123,11 +122,9 @@ class FormSemestre(models.ScoDocModel):
|
||||
)
|
||||
"autorise les enseignants à créer des évals dans leurs modimpls"
|
||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
||||
"code element semestre Apogée, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
"code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
elt_annee_apo = db.Column(db.Text())
|
||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||
elt_passage_apo = db.Column(db.Text())
|
||||
"code element passage Apogée"
|
||||
|
||||
# Data pour groups_auto_assignment
|
||||
# (ce champ est utilisé uniquement via l'API par le front js)
|
||||
@ -210,70 +207,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
).first_or_404()
|
||||
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_formsemestre(cls, args: dict, silent=False) -> "FormSemestre":
|
||||
"""Création d'un formsemestre, avec toutes les valeurs par défaut
|
||||
et notification (sauf si silent).
|
||||
Crée la partition par défaut.
|
||||
"""
|
||||
# was sco_formsemestre.do_formsemestre_create
|
||||
if "dept_id" not in args:
|
||||
args["dept_id"] = g.scodoc_dept_id
|
||||
formsemestre: "FormSemestre" = cls.create_from_dict(args)
|
||||
db.session.flush()
|
||||
for etape in args["etapes"]:
|
||||
formsemestre.add_etape(etape)
|
||||
db.session.commit()
|
||||
for u in args["responsables"]:
|
||||
formsemestre.responsables.append(u)
|
||||
# create default partition
|
||||
partition = Partition(
|
||||
formsemestre=formsemestre, partition_name=None, numero=1000000
|
||||
)
|
||||
db.session.add(partition)
|
||||
partition.create_group(default=True)
|
||||
db.session.commit()
|
||||
|
||||
if not silent:
|
||||
url = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=formsemestre.departement.acronym,
|
||||
formsemestre_id=formsemestre.id,
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
text=f"""Création du semestre <a href="{url}">{formsemestre.titre}</a>""",
|
||||
url=url,
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
return formsemestre
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict.
|
||||
args: dict with args in application.
|
||||
returns: dict to store in model's db.
|
||||
"""
|
||||
if "date_debut" in args:
|
||||
args["date_debut"] = scu.convert_fr_date(args["date_debut"])
|
||||
if "date_fin" in args:
|
||||
args["date_fin"] = scu.convert_fr_date(args["date_fin"])
|
||||
if "etat" in args:
|
||||
args["etat"] = bool(args["etat"])
|
||||
if "bul_bgcolor" in args:
|
||||
args["bul_bgcolor"] = args.get("bul_bgcolor") or "white"
|
||||
if "titre" in args:
|
||||
args["titre"] = args.get("titre") or "sans titre"
|
||||
return args
|
||||
|
||||
@classmethod
|
||||
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
||||
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
||||
Add 'etapes' to excluded."""
|
||||
# on ne peut pas affecter directement etapes
|
||||
return super().filter_model_attributes(data, (excluded or set()) | {"etapes"})
|
||||
|
||||
def sort_key(self) -> tuple:
|
||||
"""clé pour tris par ordre de date_debut, le plus ancien en tête
|
||||
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
||||
@ -677,41 +610,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def est_in_semestre_scolaire(
|
||||
cls,
|
||||
date_debut: datetime.date,
|
||||
year=False,
|
||||
periode=None,
|
||||
mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
|
||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||
) -> bool:
|
||||
"""Vrai si la date_debut est dans la période indiquée (1,2,0)
|
||||
du semestre `periode` de l'année scolaire indiquée
|
||||
(ou, à défaut, de celle en cours).
|
||||
|
||||
La période utilise les même conventions que semset["sem_id"];
|
||||
* 1 : première période
|
||||
* 2 : deuxième période
|
||||
* 0 ou période non précisée: annualisé (donc inclut toutes les périodes)
|
||||
)
|
||||
"""
|
||||
if not year:
|
||||
year = scu.annee_scolaire()
|
||||
# n'utilise pas le jour pivot
|
||||
jour_pivot_annee = jour_pivot_periode = 1
|
||||
# calcule l'année universitaire et la période
|
||||
sem_annee, sem_periode = cls.comp_periode(
|
||||
date_debut,
|
||||
mois_pivot_annee,
|
||||
mois_pivot_periode,
|
||||
jour_pivot_annee,
|
||||
jour_pivot_periode,
|
||||
)
|
||||
if periode is None or periode == 0:
|
||||
return sem_annee == year
|
||||
return sem_annee == year and sem_periode == periode
|
||||
|
||||
def est_terminal(self) -> bool:
|
||||
"Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
|
||||
return (self.semestre_id < 0) or (
|
||||
@ -796,7 +694,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
FormSemestre.titre,
|
||||
)
|
||||
|
||||
def etapes_apo_vdi(self) -> list["ApoEtapeVDI"]:
|
||||
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
||||
"Liste des vdis"
|
||||
# was read_formsemestre_etapes
|
||||
return [e.as_apovdi() for e in self.etapes if e.etape_apo]
|
||||
@ -809,9 +707,9 @@ class FormSemestre(models.ScoDocModel):
|
||||
return ""
|
||||
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
||||
|
||||
def add_etape(self, etape_apo: str | ApoEtapeVDI):
|
||||
def add_etape(self, etape_apo: str):
|
||||
"Ajoute une étape"
|
||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=str(etape_apo))
|
||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
|
||||
db.session.add(etape)
|
||||
|
||||
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
||||
@ -845,11 +743,11 @@ class FormSemestre(models.ScoDocModel):
|
||||
else:
|
||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||
|
||||
def est_responsable(self, user: User) -> bool:
|
||||
def est_responsable(self, user: User):
|
||||
"True si l'user est l'un des responsables du semestre"
|
||||
return user.id in [u.id for u in self.responsables]
|
||||
|
||||
def est_chef_or_diretud(self, user: User | None = None) -> bool:
|
||||
def est_chef_or_diretud(self, user: User = None):
|
||||
"Vrai si utilisateur (par def. current) est admin, chef dept ou responsable du semestre"
|
||||
user = user or current_user
|
||||
return user.has_permission(Permission.EditFormSemestre) or self.est_responsable(
|
||||
@ -867,7 +765,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
return True # typiquement admin, chef dept
|
||||
return self.est_responsable(user)
|
||||
|
||||
def can_edit_jury(self, user: User | None = None):
|
||||
def can_edit_jury(self, user: User = None):
|
||||
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||
dans ce semestre: vérifie permission et verrouillage.
|
||||
"""
|
||||
@ -995,12 +893,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
def get_codes_apogee(self, category=None) -> set[str]:
|
||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")
|
||||
category:
|
||||
None: tous,
|
||||
"etapes": étapes associées,
|
||||
"sem: code semestre"
|
||||
"annee": code annuel
|
||||
"passage": code passage
|
||||
category: None: tous, "etapes": étapes associées, "sem: code semestre", "annee": code annuel
|
||||
"""
|
||||
codes = set()
|
||||
if category is None or category == "etapes":
|
||||
@ -1009,8 +902,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
codes |= {x.strip() for x in self.elt_sem_apo.split(",") if x}
|
||||
if (category is None or category == "annee") and self.elt_annee_apo:
|
||||
codes |= {x.strip() for x in self.elt_annee_apo.split(",") if x}
|
||||
if (category is None or category == "passage") and self.elt_passage_apo:
|
||||
codes |= {x.strip() for x in self.elt_passage_apo.split(",") if x}
|
||||
return codes
|
||||
|
||||
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
||||
@ -1047,7 +938,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
def etudids_actifs(self) -> tuple[list[int], set[int]]:
|
||||
"""Liste les etudids inscrits (incluant DEM et DEF),
|
||||
qui sera l'index des dataframes de notes
|
||||
qui ser al'index des dataframes de notes
|
||||
et donne l'ensemble des inscrits non DEM ni DEF.
|
||||
"""
|
||||
return [inscr.etudid for inscr in self.inscriptions], {
|
||||
@ -1334,18 +1225,10 @@ class FormSemestreEtape(db.Model):
|
||||
"Etape False if code empty"
|
||||
return self.etape_apo is not None and (len(self.etape_apo) > 0)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, ApoEtapeVDI):
|
||||
return self.as_apovdi() == other
|
||||
return str(self) == str(other)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Etape {self.id} apo={self.etape_apo!r}>"
|
||||
|
||||
def __str__(self):
|
||||
return self.etape_apo or ""
|
||||
|
||||
def as_apovdi(self) -> "ApoEtapeVDI":
|
||||
def as_apovdi(self) -> ApoEtapeVDI:
|
||||
return ApoEtapeVDI(self.etape_apo)
|
||||
|
||||
|
||||
@ -1498,9 +1381,8 @@ class FormSemestreInscription(db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
|
||||
self.formsemestre_id} (S{self.formsemestre.semestre_id}) etat={self.etat} {
|
||||
('parcours="'+str(self.parcour.code)+'"') if self.parcour else ''
|
||||
} {('etape="'+self.etape+'"') if self.etape else ''}>"""
|
||||
self.formsemestre_id} etat={self.etat} {
|
||||
('parcours='+str(self.parcour)) if self.parcour else ''}>"""
|
||||
|
||||
|
||||
class NotesSemSet(db.Model):
|
||||
|
@ -54,7 +54,6 @@ class Partition(ScoDocModel):
|
||||
cascade="all, delete-orphan",
|
||||
order_by="GroupDescr.numero, GroupDescr.group_name",
|
||||
)
|
||||
_sco_dept_relations = ("FormSemestre",)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Partition, self).__init__(**kwargs)
|
||||
@ -94,10 +93,6 @@ class Partition(ScoDocModel):
|
||||
):
|
||||
group.remove_etud(etud)
|
||||
|
||||
def is_default(self) -> bool:
|
||||
"vrai si partition par défault (tous les étudiants)"
|
||||
return not self.partition_name
|
||||
|
||||
def is_parcours(self) -> bool:
|
||||
"Vrai s'il s'agit de la partition de parcours"
|
||||
return self.partition_name == scu.PARTITION_PARCOURS
|
||||
@ -226,11 +221,6 @@ class GroupDescr(ScoDocModel):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
"Numero = ordre de presentation"
|
||||
|
||||
_sco_dept_relations = (
|
||||
"Partition",
|
||||
"FormSemestre",
|
||||
)
|
||||
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
secondary="group_membership",
|
||||
@ -342,12 +332,13 @@ class GroupDescr(ScoDocModel):
|
||||
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
|
||||
if etud in self.etuds:
|
||||
self.etuds.remove(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="group_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait du groupe {self.group_name} de {self.partition.partition_name}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
# Update parcours
|
||||
if self.partition.partition_name == scu.PARTITION_PARCOURS:
|
||||
self.partition.formsemestre.update_inscriptions_parcours_from_groups(
|
||||
|
@ -6,7 +6,6 @@ from flask import abort, g
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.comp import df_cache
|
||||
@ -60,8 +59,6 @@ class ModuleImpl(ScoDocModel):
|
||||
)
|
||||
"enseignants du module (sans le responsable)"
|
||||
|
||||
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||
|
||||
@ -81,9 +78,7 @@ class ModuleImpl(ScoDocModel):
|
||||
] or self.module.get_edt_ids()
|
||||
|
||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||
"""Les poids des évaluations vers les UEs (accès via cache redis).
|
||||
Toutes les évaluations sont considérées (normales, bonus, rattr., etc.)
|
||||
"""
|
||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||
if evaluations_poids is None:
|
||||
from app.comp import moy_mod
|
||||
@ -113,37 +108,20 @@ class ModuleImpl(ScoDocModel):
|
||||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
||||
def check_apc_conformity(
|
||||
self, res: "ResultatsSemestreBUT", evaluation_type=Evaluation.EVALUATION_NORMALE
|
||||
) -> bool:
|
||||
"""true si les poids des évaluations du type indiqué (normales par défaut)
|
||||
du module permettent de satisfaire les coefficients du PN.
|
||||
def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool:
|
||||
"""true si les poids des évaluations du module permettent de satisfaire
|
||||
les coefficients du PN.
|
||||
"""
|
||||
# appelé par formsemestre_status, liste notes, et moduleimpl_status
|
||||
if not self.module.formation.get_cursus().APC_SAE or (
|
||||
self.module.module_type
|
||||
not in {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
|
||||
self.module.module_type != scu.ModuleType.RESSOURCE
|
||||
and self.module.module_type != scu.ModuleType.SAE
|
||||
):
|
||||
return True # Non BUT, toujours conforme
|
||||
from app.comp import moy_mod
|
||||
|
||||
mod_results = res.modimpls_results.get(self.id)
|
||||
if mod_results is None:
|
||||
app.critical_error("check_apc_conformity: err 1")
|
||||
|
||||
selected_evaluations_ids = [
|
||||
eval_id
|
||||
for eval_id, eval_type in mod_results.evals_type.items()
|
||||
if eval_type == evaluation_type
|
||||
]
|
||||
if not selected_evaluations_ids:
|
||||
return True # conforme si pas d'évaluations
|
||||
selected_evaluations_poids = self.get_evaluations_poids().loc[
|
||||
selected_evaluations_ids
|
||||
]
|
||||
return moy_mod.moduleimpl_is_conforme(
|
||||
self,
|
||||
selected_evaluations_poids,
|
||||
self.get_evaluations_poids(),
|
||||
res.modimpl_coefs_df,
|
||||
)
|
||||
|
||||
@ -255,60 +233,21 @@ class ModuleImpl(ScoDocModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def can_change_inscriptions(self, user: User | None = None, raise_exc=True) -> bool:
|
||||
"""check si user peut inscrire/désinsincrire des étudiants à ce module.
|
||||
Autorise ScoEtudInscrit ou responsables semestre.
|
||||
"""
|
||||
user = current_user if user is None else user
|
||||
if not self.formsemestre.etat:
|
||||
if raise_exc:
|
||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||
return False
|
||||
# -- check access
|
||||
# resp. module ou ou perm. EtudInscrit ou resp. semestre
|
||||
if (
|
||||
user.id != self.responsable_id
|
||||
and not user.has_permission(Permission.EtudInscrit)
|
||||
and user.id not in (u.id for u in self.formsemestre.responsables)
|
||||
):
|
||||
if raise_exc:
|
||||
raise AccessDenied(f"Modification impossible pour {user}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def est_inscrit(self, etud: Identite):
|
||||
def est_inscrit(self, etud: Identite) -> bool:
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
||||
(lent, pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
||||
Retourne ModuleImplInscription si inscrit au module, False sinon.
|
||||
Retourne Vrai si inscrit au module, faux sinon.
|
||||
"""
|
||||
# vérifie inscrit au moduleimpl ET au formsemestre
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
inscription = (
|
||||
ModuleImplInscription.query.filter_by(etudid=etud.id, moduleimpl_id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=etud.id)
|
||||
.first()
|
||||
is_module: int = (
|
||||
ModuleImplInscription.query.filter_by(
|
||||
etudid=etud.id, moduleimpl_id=self.id
|
||||
).count()
|
||||
> 0
|
||||
)
|
||||
|
||||
return inscription or False
|
||||
|
||||
def query_inscriptions(self) -> Query:
|
||||
"""Query ModuleImplInscription: inscrits au moduleimpl et au formsemestre
|
||||
(pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
return (
|
||||
ModuleImplInscription.query.filter_by(moduleimpl_id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=ModuleImplInscription.etudid)
|
||||
)
|
||||
return is_module
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
|
@ -75,8 +75,6 @@ class Module(models.ScoDocModel):
|
||||
backref=db.backref("modules", lazy=True),
|
||||
)
|
||||
|
||||
_sco_dept_relations = "Formation" # accès au dept_id
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.ue_coefs = []
|
||||
super(Module, self).__init__(**kwargs)
|
||||
@ -108,76 +106,31 @@ class Module(models.ScoDocModel):
|
||||
return args_dict
|
||||
|
||||
@classmethod
|
||||
def filter_model_attributes(cls, args: dict, excluded: set[str] = None) -> dict:
|
||||
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
||||
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
||||
Add 'id' to excluded."""
|
||||
# on ne peut pas affecter directement parcours
|
||||
return super().filter_model_attributes(args, (excluded or set()) | {"parcours"})
|
||||
|
||||
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
|
||||
"""Update object's fields given in dict. Add to session but don't commit.
|
||||
True if modification.
|
||||
- can't change ue nor formation
|
||||
- can change matiere_id, iff new matiere in same ue
|
||||
- can change parcours: parcours list of ApcParcour id or instances.
|
||||
"""
|
||||
# Vérifie les changements de matiere
|
||||
new_matiere_id = args.get("matiere_id", self.matiere_id)
|
||||
if new_matiere_id != self.matiere_id:
|
||||
# exists ?
|
||||
from app.models import Matiere
|
||||
|
||||
matiere = db.session.get(Matiere, new_matiere_id)
|
||||
if matiere is None or matiere.ue_id != self.ue_id:
|
||||
raise ScoValueError("invalid matiere")
|
||||
|
||||
modified = super().from_dict(
|
||||
args, excluded=(excluded or set()) | {"formation_id", "ue_id"}
|
||||
)
|
||||
|
||||
existing_parcours = {p.id for p in self.parcours}
|
||||
new_parcours = args.get("parcours", []) or []
|
||||
if existing_parcours != set(new_parcours):
|
||||
self._set_parcours_from_list(new_parcours)
|
||||
return True
|
||||
return modified
|
||||
return super().filter_model_attributes(data, (excluded or set()) | {"parcours"})
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, data: dict) -> "Module":
|
||||
"""Create from given dict, add parcours.
|
||||
Flush session."""
|
||||
module = super().create_from_dict(data)
|
||||
db.session.flush()
|
||||
module._set_parcours_from_list(data.get("parcours", []) or [])
|
||||
return module
|
||||
|
||||
def _set_parcours_from_list(self, parcours: list[ApcParcours | int]):
|
||||
"""Ajoute ces parcours à la liste des parcours du module.
|
||||
Chaque élément est soit un objet parcours soit un id.
|
||||
S'assure que chaque parcours est dans le référentiel de compétence
|
||||
associé à la formation du module.
|
||||
"""
|
||||
for p in parcours:
|
||||
"""Create from given dict, add parcours"""
|
||||
mod = super().create_from_dict(data)
|
||||
for p in data.get("parcours", []) or []:
|
||||
if isinstance(p, ApcParcours):
|
||||
parcour: ApcParcours = p
|
||||
if p.referentiel_id != self.formation.referentiel_competence.id:
|
||||
raise ScoValueError("Parcours hors référentiel du module")
|
||||
else:
|
||||
try:
|
||||
pid = int(p)
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("id de parcours invalide") from exc
|
||||
query = (
|
||||
ApcParcours.query.filter_by(id=pid)
|
||||
.join(ApcReferentielCompetences)
|
||||
.filter_by(id=self.formation.referentiel_competence.id)
|
||||
)
|
||||
pid = int(p)
|
||||
query = ApcParcours.query.filter_by(id=pid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
query = query.join(ApcReferentielCompetences).filter_by(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
parcour: ApcParcours = query.first()
|
||||
if parcour is None:
|
||||
raise ScoValueError("Parcours invalide")
|
||||
self.parcours.append(parcour)
|
||||
mod.parcours.append(parcour)
|
||||
return mod
|
||||
|
||||
def clone(self):
|
||||
"""Create a new copy of this module."""
|
||||
@ -210,29 +163,16 @@ class Module(models.ScoDocModel):
|
||||
mod.app_critiques.append(app_critique)
|
||||
return mod
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
convert_objects=False,
|
||||
with_matiere=False,
|
||||
with_ue=False,
|
||||
with_parcours_ids=False,
|
||||
) -> dict:
|
||||
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
|
||||
"""If convert_objects, convert all attributes to native types
|
||||
(suitable jor json encoding).
|
||||
If convert_objects and with_parcours_ids, give parcours as a list of id (API)
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("formation", None)
|
||||
if convert_objects:
|
||||
if with_parcours_ids:
|
||||
d["parcours"] = [p.id for p in self.parcours]
|
||||
else:
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["ue_coefs"] = [
|
||||
c.to_dict(convert_objects=False)
|
||||
for c in self.ue_coefs
|
||||
# note: don't convert_objects: we do wan't the details of the UEs here
|
||||
c.to_dict(convert_objects=convert_objects) for c in self.ue_coefs
|
||||
]
|
||||
d["app_critiques"] = {x.code: x.to_dict() for x in self.app_critiques}
|
||||
if not with_matiere:
|
||||
@ -400,21 +340,6 @@ class Module(models.ScoDocModel):
|
||||
# Liste seulement les coefs définis:
|
||||
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
||||
|
||||
def get_ue_coefs_descr(self) -> str:
|
||||
"""Description des coefficients vers les UEs (APC)"""
|
||||
coefs_descr = ", ".join(
|
||||
[
|
||||
f"{ue.acronyme}: {co}"
|
||||
for ue, co in self.ue_coefs_list()
|
||||
if isinstance(co, float) and co > 0
|
||||
]
|
||||
)
|
||||
if coefs_descr:
|
||||
descr = "Coefs: " + coefs_descr
|
||||
else:
|
||||
descr = "(pas de coefficients) "
|
||||
return descr
|
||||
|
||||
def get_codes_apogee(self) -> set[str]:
|
||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
||||
if self.code_apogee:
|
||||
|
@ -5,12 +5,11 @@
|
||||
|
||||
import sqlalchemy as sa
|
||||
from app import db
|
||||
from app import models
|
||||
from app.scodoc import safehtml
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class BulAppreciations(models.ScoDocModel):
|
||||
class BulAppreciations(db.Model):
|
||||
"""Appréciations sur bulletins"""
|
||||
|
||||
__tablename__ = "notes_appreciations"
|
||||
@ -28,8 +27,6 @@ class BulAppreciations(models.ScoDocModel):
|
||||
author = db.Column(db.Text) # le pseudo (user_name), sans contrainte
|
||||
comment = db.Column(db.Text) # texte libre
|
||||
|
||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||
|
||||
@classmethod
|
||||
def get_appreciations_list(
|
||||
cls, formsemestre_id: int, etudid: int
|
||||
|
@ -3,10 +3,10 @@
|
||||
"""Model : preferences
|
||||
"""
|
||||
|
||||
from app import db, models
|
||||
from app import db
|
||||
|
||||
|
||||
class ScoPreference(models.ScoDocModel):
|
||||
class ScoPreference(db.Model):
|
||||
"""ScoDoc preferences (par département)"""
|
||||
|
||||
__tablename__ = "sco_prefs"
|
||||
@ -19,8 +19,5 @@ class ScoPreference(models.ScoDocModel):
|
||||
value = db.Column(db.Text())
|
||||
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
|
||||
|
||||
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.departement.acronym
|
||||
} {self.name}={self.value}>"""
|
||||
return f"<{self.__class__.__name__} {self.id} {self.departement.acronym} {self.name}={self.value}>"
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
from flask import abort, g
|
||||
from flask import g
|
||||
import pandas as pd
|
||||
|
||||
from app import db, log
|
||||
@ -46,8 +46,6 @@ class UniteEns(models.ScoDocModel):
|
||||
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
||||
coefficient = db.Column(db.Float)
|
||||
|
||||
# id de l'élément Apogée du RCUE (utilisé pour les UEs de sem. pair du BUT)
|
||||
code_apogee_rcue = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# coef. pour le calcul de moyennes de RCUE. Par défaut, 1.
|
||||
coef_rcue = db.Column(db.Float, nullable=False, default=1.0, server_default="1.0")
|
||||
|
||||
@ -123,16 +121,6 @@ class UniteEns(models.ScoDocModel):
|
||||
|
||||
return args
|
||||
|
||||
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
|
||||
"""Update object's fields given in dict. Add to session but don't commit.
|
||||
True if modification.
|
||||
- can't change formation nor niveau_competence
|
||||
"""
|
||||
return super().from_dict(
|
||||
args,
|
||||
excluded=(excluded or set()) | {"formation_id", "niveau_competence_id"},
|
||||
)
|
||||
|
||||
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
||||
"""as a dict, with the same conversions as in ScoDoc7.
|
||||
If convert_objects, convert all attributes to native types
|
||||
@ -265,30 +253,6 @@ class UniteEns(models.ScoDocModel):
|
||||
log(f"ue.set_ects( ue_id={self.id}, acronyme={self.acronyme}, ects={ects} )")
|
||||
db.session.add(self)
|
||||
|
||||
@classmethod
|
||||
def get_ue(cls, ue_id: int, accept_none=False) -> "UniteEns":
|
||||
"""UE ou 404 (ou None si accept_none),
|
||||
cherche uniquement dans le département courant.
|
||||
Si accept_none, return None si l'id est invalide ou inexistant.
|
||||
"""
|
||||
if not isinstance(ue_id, int):
|
||||
try:
|
||||
ue_id = int(ue_id)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "ue_id invalide")
|
||||
|
||||
query = cls.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
from app.models import Formation
|
||||
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
|
||||
def get_ressources(self):
|
||||
"Liste des modules ressources rattachés à cette UE"
|
||||
return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all()
|
||||
@ -310,12 +274,6 @@ class UniteEns(models.ScoDocModel):
|
||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return set()
|
||||
|
||||
def get_codes_apogee_rcue(self) -> set[str]:
|
||||
"""Les codes Apogée RCUE (codés en base comme "VRT1,VRT2")"""
|
||||
if self.code_apogee_rcue:
|
||||
return {x.strip() for x in self.code_apogee_rcue.split(",") if x}
|
||||
return set()
|
||||
|
||||
def _parcours_niveaux_ids(self, parcours=list[ApcParcours]) -> set[int]:
|
||||
"""set des ids de niveaux communs à tous les parcours listés"""
|
||||
return set.intersection(
|
||||
@ -396,21 +354,7 @@ class UniteEns(models.ScoDocModel):
|
||||
|
||||
return True, ""
|
||||
|
||||
def is_used_in_validation_rcue(self) -> bool:
|
||||
"""Vrai si cette UE est utilisée dans une validation enregistrée d'RCUE."""
|
||||
from app.models.but_validations import ApcValidationRCUE
|
||||
|
||||
return (
|
||||
ApcValidationRCUE.query.filter(
|
||||
db.or_(
|
||||
ApcValidationRCUE.ue1_id == self.id,
|
||||
ApcValidationRCUE.ue2_id == self.id,
|
||||
)
|
||||
).count()
|
||||
> 0
|
||||
)
|
||||
|
||||
def set_niveau_competence(self, niveau: ApcNiveau | None) -> tuple[bool, str]:
|
||||
def set_niveau_competence(self, niveau: ApcNiveau) -> tuple[bool, str]:
|
||||
"""Associe cette UE au niveau de compétence indiqué.
|
||||
Le niveau doit être dans l'un des parcours de l'UE (si elle n'est pas
|
||||
de tronc commun).
|
||||
@ -418,12 +362,7 @@ class UniteEns(models.ScoDocModel):
|
||||
Sinon, raises ScoFormationConflict.
|
||||
|
||||
Si niveau est None, désassocie.
|
||||
|
||||
Si l'UE est utilisée dans un validation de RCUE, on ne peut plus la changer de niveau.
|
||||
|
||||
Returns
|
||||
- True if (de)association done, False on error.
|
||||
- Error message (string)
|
||||
Returns True if (de)association done, False on error.
|
||||
"""
|
||||
# Sanity checks
|
||||
if not self.formation.referentiel_competence:
|
||||
@ -431,12 +370,6 @@ class UniteEns(models.ScoDocModel):
|
||||
False,
|
||||
"La formation n'est pas associée à un référentiel de compétences",
|
||||
)
|
||||
# UE utilisée dans des validations RCUE ?
|
||||
if self.is_used_in_validation_rcue():
|
||||
return (
|
||||
False,
|
||||
"UE utilisée dans un RCUE validé: son niveau ne peut plus être modifié",
|
||||
)
|
||||
if niveau is not None:
|
||||
if self.niveau_competence_id is not None:
|
||||
return (
|
||||
|
@ -2,15 +2,12 @@
|
||||
|
||||
"""Notes, décisions de jury
|
||||
"""
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.events import Scolog
|
||||
from app.models.formations import Formation
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import CODES_UE_VALIDES
|
||||
@ -116,7 +113,6 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
if self.ue.parcours else ""}
|
||||
{("émise par " + link)}
|
||||
: <b>{self.code}</b>{moyenne}
|
||||
<b>{(self.ue.ects or 0):g} ECTS</b>
|
||||
le {self.event_date.strftime(scu.DATEATIME_FMT)}
|
||||
"""
|
||||
else:
|
||||
@ -135,27 +131,6 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
else 0.0
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validations_ues(
|
||||
cls, etud: "Identite", formation_code: str | None = None
|
||||
) -> Query:
|
||||
"""Query les validations d'UE pour cet étudiant dans des UEs de formations
|
||||
du code indiqué, ou toutes si le formation_code est None.
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
query = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.join(UniteEns)
|
||||
.join(FormSemestre, ScolarFormSemestreValidation.formsemestre)
|
||||
)
|
||||
if formation_code is not None:
|
||||
query = query.join(Formation).filter_by(formation_code=formation_code)
|
||||
return query.order_by(
|
||||
FormSemestre.semestre_id, UniteEns.numero, UniteEns.acronyme
|
||||
)
|
||||
|
||||
|
||||
class ScolarAutorisationInscription(db.Model):
|
||||
"""Autorisation d'inscription dans un semestre"""
|
||||
@ -211,7 +186,7 @@ class ScolarAutorisationInscription(db.Model):
|
||||
origin_formsemestre_id: int,
|
||||
semestre_id: int,
|
||||
):
|
||||
"""Ajoute une autorisation (don't commit)"""
|
||||
"""Ajoute une autorisation"""
|
||||
autorisation = cls(
|
||||
etudid=etudid,
|
||||
formation_code=formation_code,
|
||||
|
@ -200,7 +200,7 @@ CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
|
||||
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} # PASD pour enregistrement auto
|
||||
CODES_ANNEE_BUT_VALIDES = {ADJ, ADM, ADSUP}
|
||||
CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
|
@ -55,8 +55,6 @@ from reportlab.lib.colors import Color
|
||||
from reportlab.lib import styles
|
||||
from reportlab.lib.units import cm
|
||||
|
||||
from flask import render_template
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc import sco_excel
|
||||
@ -178,7 +176,6 @@ class GenTable:
|
||||
self.xml_link = xml_link
|
||||
# HTML parameters:
|
||||
if not table_id: # random id
|
||||
log("Warning: GenTable() called without table_id")
|
||||
self.table_id = "gt_" + str(random.randint(0, 1000000))
|
||||
else:
|
||||
self.table_id = table_id
|
||||
@ -317,10 +314,7 @@ class GenTable:
|
||||
|
||||
def get_titles_list(self):
|
||||
"list of titles"
|
||||
titles = [self.titles.get(cid, "") for cid in self.columns_ids]
|
||||
if "row_title" in self.titles and "row_title" not in self.columns_ids:
|
||||
titles.insert(0, self.titles["row_title"])
|
||||
return titles
|
||||
return [self.titles.get(cid, "") for cid in self.columns_ids]
|
||||
|
||||
def gen(self, fmt="html", columns_ids=None):
|
||||
"""Build representation of the table in the specified format.
|
||||
@ -683,15 +677,16 @@ class GenTable:
|
||||
fmt="html",
|
||||
page_title="",
|
||||
filename=None,
|
||||
javascripts=(),
|
||||
cssstyles=[],
|
||||
javascripts=[],
|
||||
with_html_headers=True,
|
||||
publish=True,
|
||||
init_qtip=False,
|
||||
):
|
||||
"""
|
||||
Build page at given format
|
||||
This is a simple page with only a title and the table.
|
||||
If not publish, do not set response header for non HTML formats.
|
||||
If with_html_headers, render a full page using ScoDoc template.
|
||||
If not publish, does not set response header
|
||||
"""
|
||||
if not filename:
|
||||
filename = self.filename
|
||||
@ -699,16 +694,21 @@ class GenTable:
|
||||
html_title = self.html_title or title
|
||||
if fmt == "html":
|
||||
H = []
|
||||
if with_html_headers:
|
||||
H.append(
|
||||
self.html_header
|
||||
or html_sco_header.sco_header(
|
||||
cssstyles=cssstyles,
|
||||
page_title=page_title,
|
||||
javascripts=javascripts,
|
||||
init_qtip=init_qtip,
|
||||
)
|
||||
)
|
||||
if html_title:
|
||||
H.append(html_title)
|
||||
H.append(self.html())
|
||||
if with_html_headers:
|
||||
return render_template(
|
||||
"sco_page.j2",
|
||||
content="\n".join(H),
|
||||
title=page_title,
|
||||
javascripts=javascripts,
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
elif fmt == "pdf":
|
||||
pdf_objs = self.pdf()
|
||||
|
@ -25,7 +25,8 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""HTML Header/Footer for ScoDoc pages"""
|
||||
"""HTML Header/Footer for ScoDoc pages
|
||||
"""
|
||||
|
||||
import html
|
||||
|
||||
@ -97,18 +98,10 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
|
||||
|
||||
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
|
||||
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
|
||||
<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css" />
|
||||
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){{
|
||||
if (document.getElementById('gtrcontent')) {{
|
||||
enableTooltips("gtrcontent");
|
||||
}}
|
||||
if (document.getElementById('sidebar')) {{
|
||||
enableTooltips("sidebar");
|
||||
}}
|
||||
}};
|
||||
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||
</script>
|
||||
|
||||
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||
@ -116,7 +109,6 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
|
||||
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
|
||||
|
||||
<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/DataTables/datatables.min.js"></script>
|
||||
|
||||
<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||
@ -226,14 +218,8 @@ def sco_header(
|
||||
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){{
|
||||
if (document.getElementById('gtrcontent')) {{
|
||||
enableTooltips("gtrcontent");
|
||||
}}
|
||||
if (document.getElementById('sidebar')) {{
|
||||
enableTooltips("sidebar");
|
||||
}}
|
||||
}};
|
||||
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||
|
||||
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
|
||||
const SCO_TIMEZONE="{scu.TIME_ZONE}";
|
||||
</script>"""
|
||||
|
@ -28,7 +28,6 @@
|
||||
"""
|
||||
Génération de la "sidebar" (marge gauche des pages HTML)
|
||||
"""
|
||||
|
||||
from flask import render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
@ -152,7 +151,7 @@ def sidebar(etudid: int = None):
|
||||
H = [
|
||||
f"""
|
||||
<!-- sidebar py -->
|
||||
<div class="sidebar" id="sidebar">
|
||||
<div class="sidebar">
|
||||
{ sidebar_common() }
|
||||
<div class="box-chercheetud">Chercher étudiant:<br>
|
||||
<form method="get" id="form-chercheetud"
|
||||
@ -194,7 +193,7 @@ def sidebar(etudid: int = None):
|
||||
formsemestre.date_debut.strftime(scu.DATE_FMT)
|
||||
} au {
|
||||
formsemestre.date_fin.strftime(scu.DATE_FMT)
|
||||
}" data-tooltip>({
|
||||
}">({
|
||||
sco_preferences.get_preference("assi_metrique", None)})
|
||||
<br>{nbabsjust:1g} J., {nbabsnj:1g} N.J.</span>"""
|
||||
)
|
||||
@ -228,9 +227,12 @@ def sidebar(etudid: int = None):
|
||||
<li><a href="{ url_for('assiduites.calendrier_assi_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Calendrier</a></li>
|
||||
<li><a href="{ url_for('assiduites.liste_assiduites_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Liste</a></li>
|
||||
<li><a href="{ url_for('assiduites.bilan_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" title="Les pages bilan et liste ont été fusionnées">Liste/Bilan</a></li>
|
||||
}">Bilan</a></li>
|
||||
</ul>
|
||||
"""
|
||||
)
|
||||
|
@ -157,6 +157,5 @@ def table_billets(
|
||||
rows=rows,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
table_id="table_billets",
|
||||
)
|
||||
return tab
|
||||
|
@ -31,7 +31,6 @@
|
||||
|
||||
Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
@ -43,15 +43,52 @@ Pour chaque étudiant commun:
|
||||
comparer les résultats
|
||||
|
||||
"""
|
||||
from flask import g, render_template, url_for
|
||||
from flask import g, url_for
|
||||
|
||||
from app import log
|
||||
from app.scodoc import sco_apogee_csv, sco_apogee_reader
|
||||
from app.scodoc.sco_apogee_csv import ApoData
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
_HELP_TXT = """
|
||||
<div class="help">
|
||||
<p>Outil de comparaison de fichiers (maquettes CSV) Apogée.
|
||||
</p>
|
||||
<p>Cet outil compare deux fichiers fournis. Aucune donnée stockée dans ScoDoc n'est utilisée.
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def apo_compare_csv_form():
|
||||
"""Form: submit 2 CSV files to compare them."""
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
|
||||
"""<h2>Comparaison de fichiers Apogée</h2>
|
||||
<form id="apo_csv_add" action="apo_compare_csv" method="post" enctype="multipart/form-data">
|
||||
""",
|
||||
_HELP_TXT,
|
||||
"""
|
||||
<div class="apo_compare_csv_form_but">
|
||||
Fichier Apogée A:
|
||||
<input type="file" size="30" name="file_a"/>
|
||||
</div>
|
||||
<div class="apo_compare_csv_form_but">
|
||||
Fichier Apogée B:
|
||||
<input type="file" size="30" name="file_b"/>
|
||||
</div>
|
||||
<input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
|
||||
<div class="apo_compare_csv_form_submit">
|
||||
<input type="submit" value="Comparer ces fichiers"/>
|
||||
</div>
|
||||
</form>""",
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def apo_compare_csv(file_a, file_b, autodetect=True):
|
||||
"""Page comparing 2 Apogee CSV files"""
|
||||
@ -77,12 +114,17 @@ def apo_compare_csv(file_a, file_b, autodetect=True):
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
) from exc
|
||||
|
||||
return render_template(
|
||||
"apogee/apo_compare_csv.j2",
|
||||
title="Comparaison de fichiers Apogée",
|
||||
content=_apo_compare_csv(apo_data_a, apo_data_b),
|
||||
)
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
|
||||
"<h2>Comparaison de fichiers Apogée</h2>",
|
||||
_HELP_TXT,
|
||||
'<div class="apo_compare_csv">',
|
||||
_apo_compare_csv(apo_data_a, apo_data_b),
|
||||
"</div>",
|
||||
"""<p><a href="apo_compare_csv_form" class="stdlink">Autre comparaison</a></p>""",
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _load_apo_data(csvfile, autodetect=True):
|
||||
@ -246,7 +288,6 @@ def apo_table_compare_etud_results(A, B):
|
||||
html_class="table_leftalign",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="apo_table_compare_etud_results",
|
||||
)
|
||||
return T
|
||||
|
||||
|
@ -43,13 +43,14 @@ import re
|
||||
import time
|
||||
from zipfile import ZipFile
|
||||
|
||||
from flask import g, send_file
|
||||
from flask import send_file
|
||||
import numpy as np
|
||||
|
||||
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
@ -78,6 +79,7 @@ from app.scodoc.codes_cursus import (
|
||||
)
|
||||
from app.scodoc import sco_cursus
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_etud
|
||||
|
||||
|
||||
def _apo_fmt_note(note, fmt="%3.2f"):
|
||||
@ -97,7 +99,7 @@ class EtuCol:
|
||||
"""Valeurs colonnes d'un element pour un etudiant"""
|
||||
|
||||
def __init__(self, nip, apo_elt, init_vals):
|
||||
pass
|
||||
pass # XXX
|
||||
|
||||
|
||||
ETUD_OK = "ok"
|
||||
@ -130,7 +132,7 @@ class ApoEtud(dict):
|
||||
"Vrai si BUT"
|
||||
self.col_elts = {}
|
||||
"{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}"
|
||||
self.etud: Identite | None = None
|
||||
self.etud: Identite = None
|
||||
"etudiant ScoDoc associé"
|
||||
self.etat = None # ETUD_OK, ...
|
||||
self.is_nar = False
|
||||
@ -148,9 +150,9 @@ class ApoEtud(dict):
|
||||
_apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
|
||||
)
|
||||
# Initialisés par associate_sco:
|
||||
self.autre_formsemestre: FormSemestre = None
|
||||
self.autre_sem: dict = None
|
||||
self.autre_res: NotesTableCompat = None
|
||||
self.cur_formsemestre: FormSemestre = None
|
||||
self.cur_sem: dict = None
|
||||
self.cur_res: NotesTableCompat = None
|
||||
self.new_cols = {}
|
||||
"{ col_id : value to record in csv }"
|
||||
@ -169,18 +171,24 @@ class ApoEtud(dict):
|
||||
Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT.
|
||||
"""
|
||||
|
||||
self.etud = Identite.query.filter_by(
|
||||
code_nip=self["nip"], dept_id=g.scodoc_dept_id
|
||||
).first()
|
||||
if not self.etud:
|
||||
# futur: #WIP
|
||||
# etud: Identite = Identite.query.filter_by(code_nip=self["nip"], dept_id=g.scodoc_dept_id).first()
|
||||
# self.etud = etud
|
||||
etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True)
|
||||
if not etuds:
|
||||
# pas dans ScoDoc
|
||||
self.etud = None
|
||||
self.log.append("non inscrit dans ScoDoc")
|
||||
self.etat = ETUD_ORPHELIN
|
||||
else:
|
||||
# futur: #WIP
|
||||
# formsemestre_ids = {
|
||||
# ins.formsemestre_id for ins in etud.formsemestre_inscriptions
|
||||
# }
|
||||
# in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||
self.etud = etuds[0]
|
||||
# cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
|
||||
formsemestre_ids = {
|
||||
ins.formsemestre_id for ins in self.etud.formsemestre_inscriptions
|
||||
}
|
||||
formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
|
||||
in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||
if not in_formsemestre_ids:
|
||||
self.log.append(
|
||||
@ -220,9 +228,7 @@ class ApoEtud(dict):
|
||||
self.new_cols[col_id] = self.cols[col_id]
|
||||
except KeyError as exc:
|
||||
raise ScoFormatError(
|
||||
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{
|
||||
col_id}</tt> non déclarée ?""",
|
||||
safe=True,
|
||||
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{col_id}</tt> non déclarée ?"""
|
||||
) from exc
|
||||
else:
|
||||
try:
|
||||
@ -248,7 +254,7 @@ class ApoEtud(dict):
|
||||
# codes = set([apo_data.apo_csv.cols[col_id].code for col_id in apo_data.apo_csv.col_ids])
|
||||
# return codes - set(sco_elts)
|
||||
|
||||
def search_elt_in_sem(self, code: str, sem: dict) -> dict:
|
||||
def search_elt_in_sem(self, code, sem) -> dict:
|
||||
"""
|
||||
VET code jury etape (en BUT, le code annuel)
|
||||
ELP élément pédagogique: UE, module
|
||||
@ -261,17 +267,13 @@ class ApoEtud(dict):
|
||||
Args:
|
||||
code (str): code apo de l'element cherché
|
||||
sem (dict): semestre dans lequel on cherche l'élément
|
||||
|
||||
Utilise notamment:
|
||||
cur_formsemestre : semestre "courant" pour résultats annuels (VET)
|
||||
autre_formsemestre : autre formsemestre utilisé pour les résultats annuels (VET)
|
||||
cur_sem (dict): semestre "courant" pour résultats annuels (VET)
|
||||
autre_sem (dict): autre semestre utilisé pour calculer les résultats annuels (VET)
|
||||
|
||||
Returns:
|
||||
dict: with N, B, J, R keys, ou None si elt non trouvé
|
||||
"""
|
||||
if not self.etud:
|
||||
return None
|
||||
etudid = self.etud.id
|
||||
etudid = self.etud["etudid"]
|
||||
if not self.cur_res:
|
||||
log("search_elt_in_sem: no cur_res !")
|
||||
return None
|
||||
@ -314,10 +316,10 @@ class ApoEtud(dict):
|
||||
code in {x.strip() for x in sem["elt_annee_apo"].split(",")}
|
||||
):
|
||||
export_res_etape = self.export_res_etape
|
||||
if (not export_res_etape) and self.cur_formsemestre:
|
||||
if (not export_res_etape) and self.cur_sem:
|
||||
# exporte toujours le résultat de l'étape si l'étudiant est diplômé
|
||||
Se = sco_cursus.get_situation_etud_cursus(
|
||||
self.etud, self.cur_formsemestre.id
|
||||
self.etud, self.cur_sem["formsemestre_id"]
|
||||
)
|
||||
export_res_etape = Se.all_other_validated()
|
||||
|
||||
@ -327,15 +329,35 @@ class ApoEtud(dict):
|
||||
self.log.append("export étape désactivé")
|
||||
return VOID_APO_RES
|
||||
|
||||
# Element passage
|
||||
res_passage = self.search_elt_passage(code, res)
|
||||
if res_passage:
|
||||
return res_passage
|
||||
|
||||
# Elements UE
|
||||
res_ue = self.search_elt_ue(code, res)
|
||||
if res_ue:
|
||||
return res_ue
|
||||
decisions_ue = res.get_etud_decisions_ue(etudid)
|
||||
for ue in res.get_ues_stat_dict():
|
||||
if ue["code_apogee"] and code in {
|
||||
x.strip() for x in ue["code_apogee"].split(",")
|
||||
}:
|
||||
if self.export_res_ues:
|
||||
if (
|
||||
decisions_ue and ue["ue_id"] in decisions_ue
|
||||
) or self.export_res_sdj:
|
||||
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
|
||||
code_decision_ue
|
||||
)
|
||||
else:
|
||||
code_decision_ue_apo = ""
|
||||
return dict(
|
||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
B=20,
|
||||
J="",
|
||||
R=code_decision_ue_apo,
|
||||
M="",
|
||||
)
|
||||
else:
|
||||
return VOID_APO_RES
|
||||
else:
|
||||
return VOID_APO_RES
|
||||
|
||||
# Elements Modules
|
||||
modimpls = res.get_modimpls_dict()
|
||||
@ -355,79 +377,9 @@ class ApoEtud(dict):
|
||||
|
||||
if module_code_found:
|
||||
return VOID_APO_RES
|
||||
|
||||
# RCUE du BUT (validations enregistrées seulement, pas avant jury)
|
||||
if res.is_apc:
|
||||
for val_rcue in ApcValidationRCUE.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=sem["formsemestre_id"]
|
||||
):
|
||||
if code in val_rcue.get_codes_apogee():
|
||||
return dict(
|
||||
N="", # n'exporte pas de moyenne RCUE
|
||||
B=20,
|
||||
J="",
|
||||
R=ScoDocSiteConfig.get_code_apo(val_rcue.code),
|
||||
M="",
|
||||
)
|
||||
#
|
||||
return None # element Apogee non trouvé dans ce semestre
|
||||
|
||||
def search_elt_ue(self, code: str, res: NotesTableCompat) -> dict:
|
||||
"""Cherche un résultat d'UE pour ce code Apogée.
|
||||
dict vide si pas de résultat trouvé pour ce code.
|
||||
"""
|
||||
decisions_ue = res.get_etud_decisions_ue(self.etud.id)
|
||||
for ue in res.get_ues_stat_dict():
|
||||
if ue["code_apogee"] and code in {
|
||||
x.strip() for x in ue["code_apogee"].split(",")
|
||||
}:
|
||||
if self.export_res_ues:
|
||||
if (
|
||||
decisions_ue and ue["ue_id"] in decisions_ue
|
||||
) or self.export_res_sdj:
|
||||
# Si dispensé de cette UE, n'exporte rien
|
||||
if (self.etud.id, ue["ue_id"]) in res.dispense_ues:
|
||||
return VOID_APO_RES
|
||||
ue_status = res.get_etud_ue_status(self.etud.id, ue["ue_id"])
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
|
||||
code_decision_ue
|
||||
)
|
||||
else:
|
||||
code_decision_ue_apo = ""
|
||||
return dict(
|
||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
B=20,
|
||||
J="",
|
||||
R=code_decision_ue_apo,
|
||||
M="",
|
||||
)
|
||||
else:
|
||||
return VOID_APO_RES
|
||||
else:
|
||||
return VOID_APO_RES
|
||||
return {} # no UE result found for this code
|
||||
|
||||
def search_elt_passage(self, code: str, res: NotesTableCompat) -> dict:
|
||||
"""Cherche un résultat de type "passage" pour ce code Apogée.
|
||||
dict vide si pas de résultat trouvé pour ce code.
|
||||
L'élement est rempli si:
|
||||
- code est dans les codes passage du formsemestre (sem)
|
||||
- autorisation d'inscription enregistre de sem vers sem d'indice suivant
|
||||
"""
|
||||
if res.formsemestre.semestre_id < 1:
|
||||
return {}
|
||||
next_semestre_id = res.formsemestre.semestre_id + 1
|
||||
if code in res.formsemestre.get_codes_apogee(category="passage"):
|
||||
if next_semestre_id in res.get_autorisations_inscription().get(
|
||||
self.etud.id, set()
|
||||
):
|
||||
return dict(
|
||||
N="", B=20, J="", R=ScoDocSiteConfig.get_code_apo("ADM"), M=""
|
||||
)
|
||||
return {}
|
||||
|
||||
def comp_elt_semestre(self, nt: NotesTableCompat, decision: dict, etudid: int):
|
||||
"""Calcul résultat apo semestre.
|
||||
Toujours vide pour en BUT/APC.
|
||||
@ -466,10 +418,11 @@ class ApoEtud(dict):
|
||||
#
|
||||
# XXX cette règle est discutable, à valider
|
||||
|
||||
if not self.cur_formsemestre:
|
||||
# log('comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id']))
|
||||
if not self.cur_sem:
|
||||
# l'étudiant n'a pas de semestre courant ?!
|
||||
self.log.append("pas de semestre courant")
|
||||
log(f"comp_elt_annuel: etudid {etudid} has no cur_formsemestre")
|
||||
log(f"comp_elt_annuel: etudid {etudid} has no cur_sem")
|
||||
return VOID_APO_RES
|
||||
|
||||
if self.is_apc:
|
||||
@ -485,7 +438,7 @@ class ApoEtud(dict):
|
||||
# ne touche pas aux RATs
|
||||
return VOID_APO_RES
|
||||
|
||||
if not self.autre_formsemestre:
|
||||
if not self.autre_sem:
|
||||
# formations monosemestre, ou code VET semestriel,
|
||||
# ou jury intermediaire et etudiant non redoublant...
|
||||
return self.comp_elt_semestre(self.cur_res, cur_decision, etudid)
|
||||
@ -565,7 +518,7 @@ class ApoEtud(dict):
|
||||
self.validation_annee_but: ApcValidationAnnee = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=self.etud.id,
|
||||
etudid=self.etud["etudid"],
|
||||
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
)
|
||||
@ -574,7 +527,7 @@ class ApoEtud(dict):
|
||||
)
|
||||
|
||||
def etud_set_semestres_de_etape(self, apo_data: "ApoData"):
|
||||
"""Set .cur_formsemestre and .autre_formsemestre et charge les résultats.
|
||||
"""Set .cur_sem and .autre_sem et charge les résultats.
|
||||
Lorsqu'on a une formation semestrialisée mais avec un code étape annuel,
|
||||
il faut considérer les deux semestres ((S1,S2) ou (S3,S4)) pour calculer
|
||||
le code annuel (VET ou VRT1A (voir elt_annee_apo)).
|
||||
@ -582,48 +535,52 @@ class ApoEtud(dict):
|
||||
Pour les jurys intermediaires (janvier, S1 ou S3): (S2 ou S4) de la même
|
||||
étape lors d'une année précédente ?
|
||||
|
||||
Set cur_formsemestre: le formsemestre "courant"
|
||||
et autre_formsemestre, ou None s'il n'y en a pas.
|
||||
Set cur_sem: le semestre "courant" et autre_sem, ou None s'il n'y en a pas.
|
||||
"""
|
||||
# Cherche le formsemestre "courant":
|
||||
cur_formsemestres = [
|
||||
formsemestre
|
||||
for formsemestre in self.etud.get_formsemestres()
|
||||
# Cherche le semestre "courant":
|
||||
cur_sems = [
|
||||
sem
|
||||
for sem in self.etud["sems"]
|
||||
if (
|
||||
(formsemestre.semestre_id == apo_data.cur_semestre_id)
|
||||
and (apo_data.etape in formsemestre.etapes)
|
||||
(sem["semestre_id"] == apo_data.cur_semestre_id)
|
||||
and (apo_data.etape in sem["etapes"])
|
||||
and (
|
||||
FormSemestre.est_in_semestre_scolaire(
|
||||
formsemestre.date_debut,
|
||||
sco_formsemestre.sem_in_semestre_scolaire(
|
||||
sem,
|
||||
apo_data.annee_scolaire,
|
||||
0, # annee complete
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
cur_formsemestre = None
|
||||
if cur_formsemestres:
|
||||
# prend le plus récent avec décision
|
||||
for formsemestre in cur_formsemestres:
|
||||
if not cur_sems:
|
||||
cur_sem = None
|
||||
else:
|
||||
# prend le plus recent avec decision
|
||||
cur_sem = None
|
||||
for sem in cur_sems:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if apo_data.export_res_sdj or res.etud_has_decision(self.etud.id):
|
||||
cur_formsemestre = formsemestre
|
||||
has_decision = res.etud_has_decision(self.etud["etudid"])
|
||||
if has_decision:
|
||||
cur_sem = sem
|
||||
self.cur_res = res
|
||||
break
|
||||
if cur_formsemestres is None:
|
||||
cur_formsemestre = cur_formsemestres[
|
||||
0
|
||||
] # aucun avec décision, prend le plus recent
|
||||
if res.formsemestre.id == cur_formsemestre.id:
|
||||
if cur_sem is None:
|
||||
cur_sem = cur_sems[0] # aucun avec décision, prend le plus recent
|
||||
if res.formsemestre.id == cur_sem["formsemestre_id"]:
|
||||
self.cur_res = res
|
||||
else:
|
||||
self.cur_res = res_sem.load_formsemestre_results(cur_formsemestre)
|
||||
formsemestre = FormSemestre.query.get_or_404(
|
||||
cur_sem["formsemestre_id"]
|
||||
)
|
||||
self.cur_res = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
self.cur_formsemestre = cur_formsemestre
|
||||
self.cur_sem = cur_sem
|
||||
|
||||
if apo_data.cur_semestre_id <= 0:
|
||||
# autre_formsemestre non pertinent pour sessions sans semestres:
|
||||
self.autre_formsemestre = None
|
||||
# "autre_sem" non pertinent pour sessions sans semestres:
|
||||
self.autre_sem = None
|
||||
self.autre_res = None
|
||||
return
|
||||
|
||||
@ -644,49 +601,52 @@ class ApoEtud(dict):
|
||||
courant_mois_debut = 1 # ou 2 (fev-jul)
|
||||
else:
|
||||
raise ValueError("invalid periode value !") # bug ?
|
||||
courant_date_debut = datetime.date(
|
||||
day=1, month=courant_mois_debut, year=courant_annee_debut
|
||||
courant_date_debut = "%d-%02d-01" % (
|
||||
courant_annee_debut,
|
||||
courant_mois_debut,
|
||||
)
|
||||
else:
|
||||
courant_date_debut = datetime.date(day=31, month=12, year=9999)
|
||||
courant_date_debut = "9999-99-99"
|
||||
|
||||
# etud['sems'] est la liste des semestres de l'étudiant, triés par date,
|
||||
# le plus récemment effectué en tête.
|
||||
# Cherche les semestres (antérieurs) de l'indice autre de la même étape apogée
|
||||
# s'il y en a plusieurs, choisit le plus récent ayant une décision
|
||||
|
||||
autres_sems = []
|
||||
for formsemestre in self.etud.get_formsemestres():
|
||||
for sem in self.etud["sems"]:
|
||||
if (
|
||||
formsemestre.semestre_id == autre_semestre_id
|
||||
and apo_data.etape_apogee in formsemestre.etapes
|
||||
sem["semestre_id"] == autre_semestre_id
|
||||
and apo_data.etape_apogee in sem["etapes"]
|
||||
):
|
||||
if (
|
||||
formsemestre.date_debut < courant_date_debut
|
||||
sem["date_debut_iso"] < courant_date_debut
|
||||
): # on demande juste qu'il ait démarré avant
|
||||
autres_sems.append(formsemestre)
|
||||
autres_sems.append(sem)
|
||||
if not autres_sems:
|
||||
autre_formsemestre = None
|
||||
autre_sem = None
|
||||
elif len(autres_sems) == 1:
|
||||
autre_formsemestre = autres_sems[0]
|
||||
autre_sem = autres_sems[0]
|
||||
else:
|
||||
autre_formsemestre = None
|
||||
for formsemestre in autres_sems:
|
||||
autre_sem = None
|
||||
for sem in autres_sems:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if res.is_apc:
|
||||
has_decision = res.etud_has_decision(self.etud.id)
|
||||
has_decision = res.etud_has_decision(self.etud["etudid"])
|
||||
else:
|
||||
has_decision = res.get_etud_decision_sem(self.etud.id)
|
||||
if has_decision or apo_data.export_res_sdj:
|
||||
autre_formsemestre = formsemestre
|
||||
has_decision = res.get_etud_decision_sem(self.etud["etudid"])
|
||||
if has_decision:
|
||||
autre_sem = sem
|
||||
break
|
||||
if autre_formsemestre is None:
|
||||
autre_formsemestre = autres_sems[
|
||||
0
|
||||
] # aucun avec decision, prend le plus recent
|
||||
if autre_sem is None:
|
||||
autre_sem = autres_sems[0] # aucun avec decision, prend le plus recent
|
||||
|
||||
self.autre_formsemestre = autre_formsemestre
|
||||
self.autre_sem = autre_sem
|
||||
# Charge les résultats:
|
||||
if autre_formsemestre:
|
||||
self.autre_res = res_sem.load_formsemestre_results(self.autre_formsemestre)
|
||||
if autre_sem:
|
||||
formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"])
|
||||
self.autre_res = res_sem.load_formsemestre_results(formsemestre)
|
||||
else:
|
||||
self.autre_res = None
|
||||
|
||||
@ -728,8 +688,7 @@ class ApoData:
|
||||
filename = self.orig_filename or e.filename
|
||||
raise ScoFormatError(
|
||||
f"""<h3>Erreur lecture du fichier Apogée <tt>{filename}</tt></h3>
|
||||
<p>{e.args[0]}</p>""",
|
||||
safe=True,
|
||||
<p>{e.args[0]}</p>"""
|
||||
) from e
|
||||
self.etape_apogee = self.get_etape_apogee() # 'V1RT'
|
||||
self.vdi_apogee = self.get_vdi_apogee() # '111'
|
||||
@ -821,9 +780,7 @@ class ApoData:
|
||||
self.sems_periode = None
|
||||
|
||||
def get_etape_apogee(self) -> str:
|
||||
"""Le code etape: 'V1RT', donné par le code de l'élément VET.
|
||||
Le VET doit être parmi les colonnes de la section XX-APO_COLONNES-XX
|
||||
"""
|
||||
"""Le code etape: 'V1RT', donné par le code de l'élément VET"""
|
||||
for elt in self.apo_csv.apo_elts.values():
|
||||
if elt.type_objet == "VET":
|
||||
return elt.code
|
||||
@ -888,8 +845,7 @@ class ApoData:
|
||||
log(f"Colonnes presentes: {present}")
|
||||
raise ScoFormatError(
|
||||
f"""Fichier Apogee invalide<br>Colonnes declarees: <tt>{declared}</tt>
|
||||
<br>Colonnes presentes: <tt>{present}</tt>""",
|
||||
safe=True,
|
||||
<br>Colonnes presentes: <tt>{present}</tt>"""
|
||||
) from exc
|
||||
# l'ensemble de tous les codes des elements apo des semestres:
|
||||
sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set())
|
||||
@ -917,16 +873,6 @@ class ApoData:
|
||||
codes_ues = set().union(
|
||||
*[ue.get_codes_apogee() for ue in formsemestre.get_ues(with_sport=True)]
|
||||
)
|
||||
codes_rcues = (
|
||||
set().union(
|
||||
*[
|
||||
ue.get_codes_apogee_rcue()
|
||||
for ue in formsemestre.get_ues(with_sport=True)
|
||||
]
|
||||
)
|
||||
if self.is_apc
|
||||
else set()
|
||||
)
|
||||
s = set()
|
||||
codes_by_sem[sem["formsemestre_id"]] = s
|
||||
for col_id in self.apo_csv.col_ids[4:]:
|
||||
@ -939,18 +885,13 @@ class ApoData:
|
||||
if code in codes_ues:
|
||||
s.add(code)
|
||||
continue
|
||||
# associé à un RCUE BUT
|
||||
if code in codes_rcues:
|
||||
s.add(code)
|
||||
continue
|
||||
# associé à un module:
|
||||
if code in codes_modules:
|
||||
s.add(code)
|
||||
|
||||
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
|
||||
return codes_by_sem
|
||||
|
||||
def build_cr_table(self) -> GenTable:
|
||||
def build_cr_table(self):
|
||||
"""Table compte rendu des décisions"""
|
||||
rows = [] # tableau compte rendu des decisions
|
||||
for apo_etud in self.etuds:
|
||||
@ -972,14 +913,13 @@ class ApoData:
|
||||
columns_ids = ["NIP", "nom", "prenom"]
|
||||
columns_ids.extend(("etape", "etape_note", "est_NAR", "commentaire"))
|
||||
|
||||
table = GenTable(
|
||||
T = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
titles=dict(zip(columns_ids, columns_ids)),
|
||||
rows=rows,
|
||||
table_id="build_cr_table",
|
||||
xls_sheet_name="Decisions ScoDoc",
|
||||
)
|
||||
return table
|
||||
return T
|
||||
|
||||
def build_adsup_table(self):
|
||||
"""Construit une table listant les ADSUP émis depuis les formsemestres
|
||||
@ -1029,7 +969,6 @@ class ApoData:
|
||||
"rcue": "RCUE",
|
||||
},
|
||||
rows=rows,
|
||||
table_id="adsup_table",
|
||||
xls_sheet_name="ADSUPs",
|
||||
)
|
||||
|
||||
@ -1115,7 +1054,6 @@ def nar_etuds_table(apo_data, nar_etuds):
|
||||
columns_ids=columns_ids,
|
||||
titles=dict(zip(columns_ids, columns_ids)),
|
||||
rows=rows,
|
||||
table_id="nar_etuds_table",
|
||||
xls_sheet_name="NAR ScoDoc",
|
||||
)
|
||||
return table.excel()
|
||||
|
@ -299,14 +299,11 @@ class ApoCSVReadWrite:
|
||||
for i, field in enumerate(fields):
|
||||
cols[self.col_ids[i]] = field
|
||||
except IndexError as exc:
|
||||
raise
|
||||
raise ScoFormatError(
|
||||
f"Fichier Apogee incorrect (colonnes excédentaires ? (<tt>{i}/{field}</tt>))",
|
||||
filename=self.get_filename(),
|
||||
safe=True,
|
||||
) from exc
|
||||
# Ajoute colonnes vides manquantes, pratique si on a édité le fichier Apo à la main...
|
||||
for i in range(len(fields), len(self.col_ids)):
|
||||
cols[self.col_ids[i]] = ""
|
||||
etud_tuples.append(
|
||||
ApoEtudTuple(
|
||||
nip=fields[0], # id etudiant
|
||||
@ -340,8 +337,6 @@ class ApoCSVReadWrite:
|
||||
fields = line.split(APO_SEP)
|
||||
if len(fields) == 2:
|
||||
k, v = fields
|
||||
elif len(fields) == 1:
|
||||
k, v = fields[0], ""
|
||||
else:
|
||||
log(f"Error read CSV: \nline={line}\nfields={fields}")
|
||||
log(dir(f))
|
||||
|
@ -139,7 +139,7 @@ class BaseArchiver:
|
||||
dirs = glob.glob(base + "*")
|
||||
return [os.path.split(x)[1] for x in dirs]
|
||||
|
||||
def list_obj_archives(self, oid: int, dept_id: int = None) -> list[str]:
|
||||
def list_obj_archives(self, oid: int, dept_id: int = None):
|
||||
"""Returns
|
||||
:return: list of archive identifiers for this object (paths to non empty dirs)
|
||||
"""
|
||||
|
@ -3,15 +3,13 @@ Ecrit par Matthias Hartmann.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from functools import wraps
|
||||
from pytz import UTC
|
||||
|
||||
from flask import g, request
|
||||
from flask import g
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import log, db, set_sco_dept
|
||||
from app.models import (
|
||||
Evaluation,
|
||||
Identite,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
@ -19,13 +17,12 @@ from app.models import (
|
||||
ModuleImplInscription,
|
||||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.models.assiduites import Assiduite, Justificatif, has_assiduites_disable_pref
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class CountCalculator:
|
||||
@ -734,125 +731,6 @@ def create_absence_billet(
|
||||
return calculator.to_dict()["demi"]
|
||||
|
||||
|
||||
def get_evaluation_assiduites(evaluation: Evaluation) -> Query:
|
||||
"""
|
||||
Renvoie une query d'assiduité en fonction des étudiants inscrits à l'évaluation
|
||||
et de la date de l'évaluation.
|
||||
|
||||
Attention : Si l'évaluation n'a pas de date, renvoie une liste vide
|
||||
"""
|
||||
|
||||
# Evaluation sans date
|
||||
if evaluation.date_debut is None:
|
||||
return []
|
||||
|
||||
# Récupération des étudiants inscrits à l'évaluation
|
||||
etuds: Query = Identite.query.join(
|
||||
ModuleImplInscription, Identite.id == ModuleImplInscription.etudid
|
||||
).filter(ModuleImplInscription.moduleimpl_id == evaluation.moduleimpl_id)
|
||||
|
||||
etudids: list[int] = [etud.id for etud in etuds]
|
||||
|
||||
# Récupération des assiduités des étudiants inscrits à l'évaluation
|
||||
date_debut: datetime = evaluation.date_debut
|
||||
date_fin: datetime
|
||||
|
||||
if evaluation.date_fin is not None:
|
||||
date_fin = evaluation.date_fin
|
||||
else:
|
||||
# On met à la fin de la journée de date_debut
|
||||
date_fin = datetime.combine(date_debut.date(), time.max)
|
||||
|
||||
# Filtrage par rapport à la plage de l'évaluation
|
||||
assiduites: Query = Assiduite.query.filter(
|
||||
Assiduite.date_debut >= date_debut,
|
||||
Assiduite.date_fin <= date_fin,
|
||||
Assiduite.etudid.in_(etudids),
|
||||
)
|
||||
|
||||
return assiduites
|
||||
|
||||
|
||||
def get_etud_evaluations_assiduites(etud: Identite) -> list[dict]:
|
||||
"""
|
||||
Retourne la liste des évaluations d'un étudiant. Pour chaque évaluation,
|
||||
retourne la liste des assiduités concernant la plage de l'évaluation.
|
||||
"""
|
||||
|
||||
etud_evaluations_assiduites: list[dict] = []
|
||||
|
||||
# On récupère les moduleimpls puis les évaluations liés aux moduleimpls
|
||||
modsimpl_ids: list[int] = [
|
||||
modimp_inscr.moduleimpl_id
|
||||
for modimp_inscr in ModuleImplInscription.query.filter_by(etudid=etud.id)
|
||||
]
|
||||
evaluations: Query = Evaluation.query.filter(
|
||||
Evaluation.moduleimpl_id.in_(modsimpl_ids)
|
||||
)
|
||||
# Pour chaque évaluation, on récupère l'assiduité de l'étudiant sur la plage
|
||||
# de l'évaluation
|
||||
|
||||
for evaluation in evaluations:
|
||||
eval_assis: dict = {"evaluation_id": evaluation.id, "assiduites": []}
|
||||
# Pas d'assiduités si pas de date
|
||||
if evaluation.date_debut is not None:
|
||||
date_debut: datetime = evaluation.date_debut
|
||||
date_fin: datetime
|
||||
|
||||
if evaluation.date_fin is not None:
|
||||
date_fin = evaluation.date_fin
|
||||
else:
|
||||
# On met à la fin de la journée de date_debut
|
||||
date_fin = datetime.combine(date_debut.date(), time.max)
|
||||
|
||||
# Filtrage par rapport à la plage de l'évaluation
|
||||
assiduites: Query = etud.assiduites.filter(
|
||||
Assiduite.date_debut >= date_debut,
|
||||
Assiduite.date_fin <= date_fin,
|
||||
)
|
||||
# On récupère les assiduités et on met à jour le dictionnaire
|
||||
eval_assis["assiduites"] = [
|
||||
assi.to_dict(format_api=True) for assi in assiduites
|
||||
]
|
||||
|
||||
# On ajoute le dictionnaire à la liste des évaluations
|
||||
etud_evaluations_assiduites.append(eval_assis)
|
||||
|
||||
return etud_evaluations_assiduites
|
||||
|
||||
|
||||
# --- Décorateur ---
|
||||
|
||||
|
||||
def check_disabled(func):
|
||||
"""
|
||||
Vérifie sur le module a été désactivé dans les préférences du semestre.
|
||||
Récupère le formsemestre depuis l'url (formsemestre_id)
|
||||
Si le formsemestre est trouvé :
|
||||
- Vérifie si le module a été désactivé dans les préférences du semestre
|
||||
- Si le module a été désactivé, une ScoValueError est levée
|
||||
Sinon :
|
||||
Il ne se passe rien
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# Récupération du formsemestre depuis l'url
|
||||
formsemestre_id = request.args.get("formsemestre_id")
|
||||
# Si on a un formsemestre_id
|
||||
if formsemestre_id:
|
||||
# Récupération du formsemestre
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# Vériication si le module a été désactivé (avec la préférence)
|
||||
pref: str | bool = has_assiduites_disable_pref(formsemestre)
|
||||
# Le module est désactivé si on récupère un message d'erreur (str)
|
||||
if pref:
|
||||
raise ScoValueError(pref, dest_url=request.referrer)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
# Gestion du cache
|
||||
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int, int]:
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
@ -873,7 +751,7 @@ def formsemestre_get_assiduites_count(
|
||||
) -> tuple[int, int, int]:
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
Utilise un cache (si moduleimpl_id n'est pas spécifié).
|
||||
Utilise un cache.
|
||||
"""
|
||||
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
|
||||
return get_assiduites_count_in_interval(
|
||||
@ -901,7 +779,7 @@ def get_assiduites_count_in_interval(
|
||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
On peut spécifier les dates comme datetime ou iso.
|
||||
Utilise un cache (si moduleimpl_id n'est pas spécifié).
|
||||
Utilise un cache.
|
||||
"""
|
||||
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
|
||||
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
|
||||
|
@ -1,102 +0,0 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Rapport de bug ScoDoc
|
||||
|
||||
Permet de créer un rapport de bug (ticket) sur la plateforme git scodoc.org.
|
||||
|
||||
Le principe est le suivant:
|
||||
1- Si l'utilisateur le demande, on dump la base de données et on l'envoie
|
||||
|
||||
2- ScoDoc envoie une requête POST à scodoc.org pour qu'un ticket git soit créé avec les
|
||||
informations fournies par l'utilisateur + quelques métadonnées.
|
||||
|
||||
"""
|
||||
from flask import g
|
||||
from flask_login import current_user
|
||||
import requests
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import sco_version
|
||||
|
||||
from app import log
|
||||
from app.scodoc.sco_dump_db import sco_dump_and_send_db
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def sco_bug_report(
|
||||
title: str = "", message: str = "", etab: str = "", include_dump: bool = False
|
||||
) -> requests.Response:
|
||||
"""Envoi d'un bug report (ticket)"""
|
||||
dump_id = None
|
||||
|
||||
if include_dump:
|
||||
dump = sco_dump_and_send_db()
|
||||
|
||||
try:
|
||||
dump_id = dump.json()["dump_id"]
|
||||
except (requests.exceptions.JSONDecodeError, KeyError):
|
||||
dump_id = "inconnu (erreur)"
|
||||
|
||||
log(f"sco_bug_report: {scu.SCO_BUG_REPORT_URL} by {current_user.user_name}")
|
||||
try:
|
||||
r = requests.post(
|
||||
scu.SCO_BUG_REPORT_URL,
|
||||
json={
|
||||
"ticket": {
|
||||
"title": title,
|
||||
"message": message,
|
||||
"etab": etab,
|
||||
"dept": getattr(g, "scodoc_dept", "-"),
|
||||
},
|
||||
"user": {
|
||||
"name": current_user.get_nomcomplet(),
|
||||
"email": current_user.email,
|
||||
},
|
||||
"dump": {
|
||||
"included": include_dump,
|
||||
"id": dump_id,
|
||||
},
|
||||
"scodoc": {
|
||||
"version": sco_version.SCOVERSION,
|
||||
},
|
||||
},
|
||||
timeout=scu.SCO_ORG_TIMEOUT,
|
||||
)
|
||||
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc:
|
||||
log("ConnectionError: Impossible de joindre le serveur d'assistance")
|
||||
raise ScoValueError(
|
||||
"""
|
||||
Impossible de joindre le serveur d'assistance (scodoc.org).
|
||||
Veuillez contacter le service informatique de votre établissement pour
|
||||
corriger la configuration de ScoDoc. Dans la plupart des cas, il
|
||||
s'agit d'un proxy mal configuré.
|
||||
"""
|
||||
) from exc
|
||||
|
||||
return r
|
@ -69,7 +69,6 @@ from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_pv_dict
|
||||
from app.scodoc import sco_pv_lettres_inviduelles
|
||||
from app.scodoc import sco_users
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType, fmt_note
|
||||
@ -710,11 +709,7 @@ def etud_descr_situation_semestre(
|
||||
decisions_ue : noms (acronymes) des UE validées, séparées par des virgules.
|
||||
descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
|
||||
descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
|
||||
diplomation : "Diplôme obtenu." ou ""
|
||||
parcours_titre, parcours_code, refcomp_specialite, refcomp_specialite_long
|
||||
|
||||
diplome_dut120_descr: phrase explicative si DUT enregistré (en BUT)
|
||||
diplome_dut120: booléen, vrai si DUT enregistré (en BUT)
|
||||
"""
|
||||
# Fonction utilisée par tous les bulletins (APC ou classiques)
|
||||
infos = collections.defaultdict(str)
|
||||
@ -765,19 +760,17 @@ def etud_descr_situation_semestre(
|
||||
infos["descr_decisions_niveaux"] = infos["descr_decisions_rcue"] = ""
|
||||
infos["descr_decision_annee"] = ""
|
||||
|
||||
infos["descr_demission"] = f"Démission le {date_dem}." if date_dem else ""
|
||||
infos["date_demission"] = date_dem if date_dem else ""
|
||||
|
||||
if date_dem:
|
||||
infos["descr_demission"] = f"Démission le {date_dem}."
|
||||
infos["date_demission"] = date_dem
|
||||
infos["decision_jury"] = infos["descr_decision_jury"] = "Démission"
|
||||
infos["situation"] = ". ".join(
|
||||
[x for x in [infos["descr_inscription"], infos["descr_demission"]] if x]
|
||||
)
|
||||
return infos, None # ne donne pas les dec. de jury pour les demissionnaires
|
||||
|
||||
infos["descr_defaillance"] = f"Défaillant{ne}" if date_def else ""
|
||||
infos["date_defaillance"] = date_def or ""
|
||||
if date_def:
|
||||
infos["descr_defaillance"] = f"Défaillant{ne}"
|
||||
infos["date_defaillance"] = date_def
|
||||
infos["descr_decision_jury"] = f"Défaillant{ne}"
|
||||
|
||||
dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid])
|
||||
@ -832,18 +825,12 @@ def etud_descr_situation_semestre(
|
||||
)
|
||||
else:
|
||||
descr_dec += " Diplôme obtenu."
|
||||
|
||||
infos["diplomation"] = "Diplôme obtenu." if pv["validation_parcours"] else ""
|
||||
# Ajoute diplome_dut120_descr et diplome_dut120
|
||||
sco_pv_lettres_inviduelles.add_dut120_infos(formsemestre, etudid, infos)
|
||||
|
||||
_format_situation_fields(
|
||||
infos,
|
||||
[
|
||||
"descr_inscription",
|
||||
"descr_defaillance",
|
||||
"descr_decisions_ue",
|
||||
"diplome_dut120_descr",
|
||||
"descr_decision_annee",
|
||||
],
|
||||
[descr_dec, descr_mention, descr_autorisations],
|
||||
@ -898,9 +885,7 @@ def _dates_insc_dem_def(etudid, formsemestre_id) -> tuple:
|
||||
def _format_situation_fields(
|
||||
infos, field_names: list[str], extra_values: list[str]
|
||||
) -> None:
|
||||
"""Réuni les champs pour former le paragraphe "situation", et ajoute la pontuation
|
||||
aux champs.
|
||||
"""
|
||||
"""Réuni les champs pour former le paragraphe "situation", et ajoute la pontuation aux champs."""
|
||||
infos["situation"] = ". ".join(
|
||||
x
|
||||
for x in [infos.get(field_name, "") for field_name in field_names]
|
||||
@ -1102,9 +1087,7 @@ def do_formsemestre_bulletinetud(
|
||||
flash(f"{etud.nomprenom} n'a pas d'adresse e-mail !")
|
||||
return False, bul_dict["filigranne"]
|
||||
else:
|
||||
mail_bulletin(
|
||||
formsemestre, etud, bul_dict, pdfdata, filename, recipient_addr
|
||||
)
|
||||
mail_bulletin(formsemestre.id, bul_dict, pdfdata, filename, recipient_addr)
|
||||
flash(f"mail envoyé à {recipient_addr}")
|
||||
|
||||
return True, bul_dict["filigranne"]
|
||||
@ -1112,28 +1095,22 @@ def do_formsemestre_bulletinetud(
|
||||
raise ValueError(f"do_formsemestre_bulletinetud: invalid format ({fmt})")
|
||||
|
||||
|
||||
def mail_bulletin(
|
||||
formsemestre: FormSemestre,
|
||||
etud: Identite,
|
||||
infos: dict,
|
||||
pdfdata,
|
||||
filename,
|
||||
recipient_addr,
|
||||
):
|
||||
def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
||||
"""Send bulletin by email to etud
|
||||
If bul_mail_list_abs pref is true, put list of absences in mail body (text).
|
||||
"""
|
||||
webmaster = sco_preferences.get_preference("bul_mail_contact_addr", formsemestre.id)
|
||||
etud = infos["etud"]
|
||||
webmaster = sco_preferences.get_preference("bul_mail_contact_addr", formsemestre_id)
|
||||
dept = scu.unescape_html(
|
||||
sco_preferences.get_preference("DeptName", formsemestre.id)
|
||||
sco_preferences.get_preference("DeptName", formsemestre_id)
|
||||
)
|
||||
copy_addr = sco_preferences.get_preference("email_copy_bulletins", formsemestre.id)
|
||||
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre.id)
|
||||
copy_addr = sco_preferences.get_preference("email_copy_bulletins", formsemestre_id)
|
||||
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id)
|
||||
|
||||
if intro_mail:
|
||||
try:
|
||||
hea = intro_mail % {
|
||||
"nomprenom": etud.nom_prenom(),
|
||||
"nomprenom": etud["nomprenom"],
|
||||
"dept": dept,
|
||||
"webmaster": webmaster,
|
||||
}
|
||||
@ -1147,12 +1124,12 @@ def mail_bulletin(
|
||||
if sco_preferences.get_preference("bul_mail_list_abs"):
|
||||
from app.views.assiduites import generate_bul_list
|
||||
|
||||
etud_identite: Identite = Identite.get_etud(etud["etudid"])
|
||||
form_semestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
hea += "\n\n"
|
||||
hea += generate_bul_list(etud, formsemestre)
|
||||
hea += generate_bul_list(etud_identite, form_semestre)
|
||||
|
||||
subject = f"""Relevé de notes du semestre {
|
||||
formsemestre.semestre_id if formsemestre.semestre_id >= 0 else ''
|
||||
} de {etud.nom_prenom()}"""
|
||||
subject = f"""Relevé de notes de {etud["nomprenom"]}"""
|
||||
recipients = [recipient_addr]
|
||||
sender = email.get_from_addr()
|
||||
if copy_addr:
|
||||
|
@ -226,7 +226,6 @@ class BulletinGenerator:
|
||||
server_name=self.server_name,
|
||||
filigranne=self.filigranne,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
with_page_numbers=self.multi_pages,
|
||||
)
|
||||
)
|
||||
try:
|
||||
|
@ -445,10 +445,7 @@ def dict_decision_jury(
|
||||
...
|
||||
],
|
||||
'situation': 'Inscrit le 25/06/2021. Décision jury: Validé. UE acquises: '
|
||||
'UE31, UE32. Diplôme obtenu.',
|
||||
'diplomation' : 'Diplôme obtenu.' # (ou vide)
|
||||
|
||||
}
|
||||
'UE31, UE32. Diplôme obtenu.'}
|
||||
"""
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
@ -461,10 +458,7 @@ def dict_decision_jury(
|
||||
formsemestre,
|
||||
show_uevalid=prefs["bul_show_uevalid"],
|
||||
)
|
||||
d["diplomation"] = infos["diplomation"]
|
||||
d["situation"] = infos["situation"]
|
||||
d["diplome_dut120"] = infos["diplome_dut120"]
|
||||
d["diplome_dut120_descr"] = infos["diplome_dut120_descr"]
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
etat = decision["etat"]
|
||||
|
@ -106,7 +106,6 @@ def assemble_bulletins_pdf(
|
||||
pagesbookmarks=pagesbookmarks,
|
||||
filigranne=filigranne,
|
||||
preferences=sco_preferences.SemPreferences(formsemestre_id),
|
||||
with_page_numbers=False, # on ne veut pas de no de pages sur les bulletins imprimés en masse
|
||||
)
|
||||
)
|
||||
document.multiBuild(story)
|
||||
@ -123,8 +122,7 @@ def replacement_function(match) -> str:
|
||||
return r'<img %s src="%s"%s/>' % (match.group(2), logo.filepath, match.group(4))
|
||||
raise ScoValueError(
|
||||
'balise "%s": logo "%s" introuvable'
|
||||
% (pydoc.html.escape(balise), pydoc.html.escape(name)),
|
||||
safe=True,
|
||||
% (pydoc.html.escape(balise), pydoc.html.escape(name))
|
||||
)
|
||||
|
||||
|
||||
|
@ -114,7 +114,6 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
||||
html_class="notes_bulletin",
|
||||
html_class_ignore_default=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="std_bul_table",
|
||||
)
|
||||
|
||||
return T.gen(fmt=fmt)
|
||||
|
@ -274,7 +274,6 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
"""expire cache pour un semestre (ou tous ceux du département si formsemestre_id non spécifié).
|
||||
Si pdfonly, n'expire que les bulletins pdf cachés.
|
||||
"""
|
||||
from app.comp import df_cache
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc import sco_cursus
|
||||
|
||||
@ -316,14 +315,12 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
||||
and fid in g.formsemestre_results_cache
|
||||
):
|
||||
del g.formsemestre_results_cache[fid]
|
||||
df_cache.EvaluationsPoidsCache.invalidate_sem(formsemestre_id)
|
||||
|
||||
else:
|
||||
# optimization when we invalidate all evaluations:
|
||||
EvaluationCache.invalidate_all_sems()
|
||||
df_cache.EvaluationsPoidsCache.invalidate_all()
|
||||
if hasattr(g, "formsemestre_results_cache"):
|
||||
del g.formsemestre_results_cache
|
||||
|
||||
SemInscriptionsCache.delete_many(formsemestre_ids)
|
||||
ResultatsSemestreCache.delete_many(formsemestre_ids)
|
||||
ValidationsSemestreCache.delete_many(formsemestre_ids)
|
||||
|
102
app/scodoc/sco_compute_moy.py
Normal file
102
app/scodoc/sco_compute_moy.py
Normal file
@ -0,0 +1,102 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Calcul des moyennes de module (restes de fonctions ScoDoc 7)
|
||||
"""
|
||||
from app.models import ModuleImpl
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
|
||||
def moduleimpl_has_expression(modimpl: ModuleImpl):
|
||||
"""True if we should use a user-defined expression
|
||||
En ScoDoc 9, utilisé pour afficher un avertissement, l'expression elle même
|
||||
n'est plus supportée.
|
||||
"""
|
||||
return (
|
||||
modimpl.computation_expr
|
||||
and modimpl.computation_expr.strip()
|
||||
and modimpl.computation_expr.strip()[0] != "#"
|
||||
)
|
||||
|
||||
|
||||
def formsemestre_expressions_use_abscounts(formsemestre_id):
|
||||
"""True si les notes de ce semestre dépendent des compteurs d'absences.
|
||||
Cela n'est normalement pas le cas, sauf si des formules utilisateur
|
||||
utilisent ces compteurs.
|
||||
"""
|
||||
# check presence of 'nbabs' in expressions
|
||||
ab = "nb_abs" # chaine recherchée
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# 1- moyennes d'UE:
|
||||
elist = formsemestre_ue_computation_expr_list(
|
||||
cnx, {"formsemestre_id": formsemestre_id}
|
||||
)
|
||||
for e in elist:
|
||||
expr = e["computation_expr"].strip()
|
||||
if expr and expr[0] != "#" and ab in expr:
|
||||
return True
|
||||
# 2- moyennes de modules
|
||||
# #sco9 il n'y a plus d'expressions
|
||||
# for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id):
|
||||
# if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]:
|
||||
# return True
|
||||
return False
|
||||
|
||||
|
||||
_formsemestre_ue_computation_exprEditor = ndb.EditableTable(
|
||||
"notes_formsemestre_ue_computation_expr",
|
||||
"notes_formsemestre_ue_computation_expr_id",
|
||||
(
|
||||
"notes_formsemestre_ue_computation_expr_id",
|
||||
"formsemestre_id",
|
||||
"ue_id",
|
||||
"computation_expr",
|
||||
),
|
||||
html_quote=False, # does nt automatically quote
|
||||
)
|
||||
formsemestre_ue_computation_expr_create = _formsemestre_ue_computation_exprEditor.create
|
||||
formsemestre_ue_computation_expr_delete = _formsemestre_ue_computation_exprEditor.delete
|
||||
formsemestre_ue_computation_expr_list = _formsemestre_ue_computation_exprEditor.list
|
||||
formsemestre_ue_computation_expr_edit = _formsemestre_ue_computation_exprEditor.edit
|
||||
|
||||
|
||||
def get_ue_expression(formsemestre_id, ue_id, html_quote=False):
|
||||
"""Returns UE expression (formula), or None if no expression has been defined"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
el = formsemestre_ue_computation_expr_list(
|
||||
cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
|
||||
)
|
||||
if not el:
|
||||
return None
|
||||
else:
|
||||
expr = el[0]["computation_expr"].strip()
|
||||
if expr and expr[0] != "#":
|
||||
if html_quote:
|
||||
expr = ndb.quote_html(expr)
|
||||
return expr
|
||||
else:
|
||||
return None
|
@ -141,7 +141,6 @@ def formsemestre_table_estim_cost(
|
||||
""",
|
||||
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
|
||||
filename=f"EstimCout-S{formsemestre.semestre_id}",
|
||||
table_id="formsemestre_table_estim_cost",
|
||||
)
|
||||
return tab
|
||||
|
||||
|
@ -34,13 +34,13 @@ from app.scodoc import sco_cursus_dut
|
||||
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp import res_sem
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
|
||||
# SituationEtudParcours -> get_situation_etud_cursus
|
||||
def get_situation_etud_cursus(
|
||||
etud: Identite, formsemestre_id: int
|
||||
etud: dict, formsemestre_id: int
|
||||
) -> sco_cursus_dut.SituationEtudCursus:
|
||||
"""renvoie une instance de SituationEtudCursus (ou sous-classe spécialisée)"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
@ -31,18 +31,13 @@
|
||||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
Scolog,
|
||||
UniteEns,
|
||||
)
|
||||
from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import sco_cache, sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.codes_cursus import (
|
||||
CMP,
|
||||
@ -77,7 +72,7 @@ class DecisionSem(object):
|
||||
def __init__(
|
||||
self,
|
||||
code_etat=None,
|
||||
code_etat_ues: dict = None, # { ue_id : code }
|
||||
code_etat_ues={}, # { ue_id : code }
|
||||
new_code_prev="",
|
||||
explication="", # aide pour le jury
|
||||
formsemestre_id_utilise_pour_compenser=None, # None si code != ADC
|
||||
@ -86,7 +81,7 @@ class DecisionSem(object):
|
||||
rule_id=None, # id regle correspondante
|
||||
):
|
||||
self.code_etat = code_etat
|
||||
self.code_etat_ues = code_etat_ues or {}
|
||||
self.code_etat_ues = code_etat_ues
|
||||
self.new_code_prev = new_code_prev
|
||||
self.explication = explication
|
||||
self.formsemestre_id_utilise_pour_compenser = (
|
||||
@ -114,27 +109,20 @@ class DecisionSem(object):
|
||||
|
||||
class SituationEtudCursus:
|
||||
"Semestre dans un cursus"
|
||||
pass
|
||||
|
||||
|
||||
class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"Semestre dans un parcours"
|
||||
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, nt: NotesTableCompat):
|
||||
def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
|
||||
"""
|
||||
etud: dict filled by fill_etuds_info()
|
||||
"""
|
||||
assert formsemestre_id == nt.formsemestre.id
|
||||
self.etud = etud
|
||||
self.etudid = etud.id
|
||||
self.etudid = etud["etudid"]
|
||||
self.formsemestre_id = formsemestre_id
|
||||
self.formsemestres: list[FormSemestre] = []
|
||||
"les semestres parcourus, le plus ancien en tête"
|
||||
self.sem = sco_formsemestre.get_formsemestre(
|
||||
formsemestre_id
|
||||
) # TODO utiliser formsemestres
|
||||
self.cur_sem: FormSemestre = nt.formsemestre
|
||||
self.can_compensate: set[int] = set()
|
||||
"les formsemestre_id qui peuvent compenser le courant"
|
||||
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
self.nt: NotesTableCompat = nt
|
||||
self.formation = self.nt.formsemestre.formation
|
||||
self.parcours = self.nt.parcours
|
||||
@ -142,20 +130,18 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
# pour le DUT, le dernier est toujours S4.
|
||||
# Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
|
||||
# (licences et autres formations en 1 seule session))
|
||||
self.semestre_non_terminal = self.cur_sem.semestre_id != self.parcours.NB_SEM
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
self.semestre_non_terminal = False
|
||||
# Liste des semestres du parcours de cet étudiant:
|
||||
self._comp_semestres()
|
||||
# Determine le semestre "precedent"
|
||||
self._search_prev()
|
||||
self.prev_formsemestre_id = self._search_prev()
|
||||
# Verifie barres
|
||||
self._comp_barres()
|
||||
# Verifie compensation
|
||||
if self.prev_formsemestre and self.cur_sem.gestion_compensation:
|
||||
self.can_compensate_with_prev = (
|
||||
self.prev_formsemestre.id in self.can_compensate
|
||||
)
|
||||
if self.prev and self.sem["gestion_compensation"]:
|
||||
self.can_compensate_with_prev = self.prev["can_compensate"]
|
||||
else:
|
||||
self.can_compensate_with_prev = False
|
||||
|
||||
@ -184,20 +170,20 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
if rule.conclusion[0] in self.parcours.UNUSED_CODES:
|
||||
continue
|
||||
# Saute regles REDOSEM si pas de semestres decales:
|
||||
if (not self.cur_sem.gestion_semestrielle) and rule.conclusion[
|
||||
if (not self.sem["gestion_semestrielle"]) and rule.conclusion[
|
||||
3
|
||||
] == "REDOSEM":
|
||||
continue
|
||||
if rule.match(state):
|
||||
if rule.conclusion[0] == ADC:
|
||||
# dans les regles on ne peut compenser qu'avec le PRECEDENT:
|
||||
fiduc = self.prev_formsemestre.id
|
||||
fiduc = self.prev_formsemestre_id
|
||||
assert fiduc
|
||||
else:
|
||||
fiduc = None
|
||||
# Detection d'incoherences (regles BUG)
|
||||
if rule.conclusion[5] == BUG:
|
||||
log(f"get_possible_choices: inconsistency: state={state}")
|
||||
log("get_possible_choices: inconsistency: state=%s" % str(state))
|
||||
#
|
||||
# valid_semestre = code_semestre_validant(rule.conclusion[0])
|
||||
choices.append(
|
||||
@ -217,15 +203,15 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"Phrase d'explication pour le code devenir"
|
||||
if not devenir:
|
||||
return ""
|
||||
s_idx = self.cur_sem.semestre_id # numero semestre courant
|
||||
if s_idx < 0: # formation sans semestres (eg licence)
|
||||
s = self.sem["semestre_id"] # numero semestre courant
|
||||
if s < 0: # formation sans semestres (eg licence)
|
||||
next_s = 1
|
||||
else:
|
||||
next_s = self._get_next_semestre_id()
|
||||
# log('s=%s next=%s' % (s, next_s))
|
||||
sess_abrv = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
SA = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if self.semestre_non_terminal and not self.all_other_validated():
|
||||
passage = f"Passe en {sess_abrv}{next_s}"
|
||||
passage = "Passe en %s%s" % (SA, next_s)
|
||||
else:
|
||||
passage = "Formation terminée"
|
||||
if devenir == NEXT:
|
||||
@ -233,23 +219,29 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
elif devenir == REO:
|
||||
return "Réorienté"
|
||||
elif devenir == REDOANNEE:
|
||||
return f"Redouble année (recommence {sess_abrv}{s_idx - 1})"
|
||||
return "Redouble année (recommence %s%s)" % (SA, (s - 1))
|
||||
elif devenir == REDOSEM:
|
||||
return f"Redouble semestre (recommence en {sess_abrv}{s_idx})"
|
||||
return "Redouble semestre (recommence en %s%s)" % (SA, s)
|
||||
elif devenir == RA_OR_NEXT:
|
||||
return passage + ", ou redouble année (en {sess_abrv}{s_idx - 1})"
|
||||
return passage + ", ou redouble année (en %s%s)" % (SA, (s - 1))
|
||||
elif devenir == RA_OR_RS:
|
||||
return f"""Redouble semestre {sess_abrv}{s_idx}, ou redouble année (en {
|
||||
sess_abrv}{s_idx - 1})"""
|
||||
return "Redouble semestre %s%s, ou redouble année (en %s%s)" % (
|
||||
SA,
|
||||
s,
|
||||
SA,
|
||||
s - 1,
|
||||
)
|
||||
elif devenir == RS_OR_NEXT:
|
||||
return f"{passage}, ou semestre {sess_abrv}{s_idx}"
|
||||
return passage + ", ou semestre %s%s" % (SA, s)
|
||||
elif devenir == NEXT_OR_NEXT2:
|
||||
# coherent avec get_next_semestre_ids
|
||||
return f"{passage}, ou en semestre {sess_abrv}{s_idx + 2}"
|
||||
return passage + ", ou en semestre %s%s" % (
|
||||
SA,
|
||||
s + 2,
|
||||
) # coherent avec get_next_semestre_ids
|
||||
elif devenir == NEXT2:
|
||||
return f"Passe en {sess_abrv}{s_idx + 2}"
|
||||
return "Passe en %s%s" % (SA, s + 2)
|
||||
else:
|
||||
log(f"explique_devenir: code devenir inconnu: {devenir}")
|
||||
log("explique_devenir: code devenir inconnu: %s" % devenir)
|
||||
return "Code devenir inconnu !"
|
||||
|
||||
def all_other_validated(self):
|
||||
@ -266,7 +258,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
|
||||
def _sems_validated(self, exclude_current=False):
|
||||
"True si semestres du parcours validés"
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
# mono-semestre: juste celui ci
|
||||
decision = self.nt.get_etud_decision_sem(self.etudid)
|
||||
return decision and code_semestre_validant(decision["code"])
|
||||
@ -274,8 +266,8 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
to_validate = set(
|
||||
range(1, self.parcours.NB_SEM + 1)
|
||||
) # ensemble des indices à valider
|
||||
if exclude_current and self.cur_sem.semestre_id in to_validate:
|
||||
to_validate.remove(self.cur_sem.semestre_id)
|
||||
if exclude_current and self.sem["semestre_id"] in to_validate:
|
||||
to_validate.remove(self.sem["semestre_id"])
|
||||
return self._sem_list_validated(to_validate)
|
||||
|
||||
def can_jump_to_next2(self):
|
||||
@ -283,20 +275,20 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
Il faut donc que tous les semestres 1...n-1 soient validés et que n+1 soit en attente.
|
||||
(et que le sem courant n soit validé, ce qui n'est pas testé ici)
|
||||
"""
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
if not self.cur_sem.gestion_semestrielle:
|
||||
n = self.sem["semestre_id"]
|
||||
if not self.sem["gestion_semestrielle"]:
|
||||
return False # pas de semestre décalés
|
||||
if s_idx == NO_SEMESTRE_ID or s_idx > self.parcours.NB_SEM - 2:
|
||||
if n == NO_SEMESTRE_ID or n > self.parcours.NB_SEM - 2:
|
||||
return False # n+2 en dehors du parcours
|
||||
if self._sem_list_validated(set(range(1, s_idx))):
|
||||
# antérieurs validés, teste suivant
|
||||
n1 = s_idx + 1
|
||||
for formsemestre in self.formsemestres:
|
||||
if self._sem_list_validated(set(range(1, n))):
|
||||
# antérieurs validé, teste suivant
|
||||
n1 = n + 1
|
||||
for sem in self.get_semestres():
|
||||
if (
|
||||
formsemestre.semestre_id == n1
|
||||
and formsemestre.formation.formation_code
|
||||
== self.formation.formation_code
|
||||
sem["semestre_id"] == n1
|
||||
and sem["formation_code"] == self.formation.formation_code
|
||||
):
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||
formsemestre
|
||||
)
|
||||
@ -323,17 +315,19 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
return not sem_idx_set
|
||||
|
||||
def _comp_semestres(self):
|
||||
# plus ancien en tête:
|
||||
self.formsemestres = self.etud.get_formsemestres(recent_first=False)
|
||||
|
||||
# etud['sems'] est trie par date decroissante (voir fill_etuds_info)
|
||||
if not "sems" in self.etud:
|
||||
self.etud["sems"] = sco_etud.etud_inscriptions_infos(
|
||||
self.etud["etudid"], self.etud["ne"]
|
||||
)["sems"]
|
||||
sems = self.etud["sems"][:] # copy
|
||||
sems.reverse()
|
||||
# Nb max d'UE et acronymes
|
||||
ue_acros = {} # acronyme ue : 1
|
||||
nb_max_ue = 0
|
||||
sems = []
|
||||
for formsemestre in self.formsemestres: # plus ancien en tête
|
||||
for sem in sems:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
sem = formsemestre.to_dict()
|
||||
sems.append(sem)
|
||||
ues = nt.get_ues_stat_dict(filter_sport=True)
|
||||
for ue in ues:
|
||||
ue_acros[ue["acronyme"]] = 1
|
||||
@ -344,48 +338,37 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
sem["formation_code"] = formsemestre.formation.formation_code
|
||||
# si sem peut servir à compenser le semestre courant, positionne
|
||||
# can_compensate
|
||||
if self.check_compensation_dut(sem, nt):
|
||||
self.can_compensate.add(formsemestre.id)
|
||||
sem["can_compensate"] = self.check_compensation_dut(sem, nt)
|
||||
|
||||
self.ue_acros = list(ue_acros.keys())
|
||||
self.ue_acros.sort()
|
||||
self.nb_max_ue = nb_max_ue
|
||||
self.sems = sems
|
||||
|
||||
def get_semestres(self) -> list[dict]:
|
||||
def get_semestres(self):
|
||||
"""Liste des semestres dans lesquels a été inscrit
|
||||
l'étudiant (quelle que soit la formation), le plus ancien en tête"""
|
||||
return self.sems
|
||||
|
||||
def get_cursus_descr(self, filter_futur=False, filter_formation_code=False) -> str:
|
||||
def get_cursus_descr(self, filter_futur=False):
|
||||
"""Description brève du parcours: "S1, S2, ..."
|
||||
Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
|
||||
Si filter_formation_code, restreint aux semestres de même code formation que le courant.
|
||||
"""
|
||||
cur_begin_date = self.cur_sem.date_debut
|
||||
cur_formation_code = self.cur_sem.formation.formation_code
|
||||
cur_begin_date = self.sem["dateord"]
|
||||
p = []
|
||||
for formsemestre in self.formsemestres:
|
||||
inscription = formsemestre.etuds_inscriptions.get(self.etud.id)
|
||||
if inscription is None:
|
||||
return "non inscrit" # !!!
|
||||
if inscription.etat == scu.DEMISSION:
|
||||
for s in self.sems:
|
||||
if s["ins"]["etat"] == scu.DEMISSION:
|
||||
dem = " (dem.)"
|
||||
else:
|
||||
dem = ""
|
||||
if filter_futur and formsemestre.date_debut > cur_begin_date:
|
||||
if filter_futur and s["dateord"] > cur_begin_date:
|
||||
continue # skip semestres demarrant apres le courant
|
||||
if (
|
||||
filter_formation_code
|
||||
and formsemestre.formation.formation_code != cur_formation_code
|
||||
):
|
||||
continue # restreint aux semestres de la formation courante (pour les PV)
|
||||
session_abbrv = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if formsemestre.semestre_id < 0:
|
||||
session_abbrv = "A" # force, cas des DUT annuels par exemple
|
||||
p.append("%s%d%s" % (session_abbrv, -formsemestre.semestre_id, dem))
|
||||
SA = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if s["semestre_id"] < 0:
|
||||
SA = "A" # force, cas des DUT annuels par exemple
|
||||
p.append("%s%d%s" % (SA, -s["semestre_id"], dem))
|
||||
else:
|
||||
p.append("%s%d%s" % (session_abbrv, formsemestre.semestre_id, dem))
|
||||
p.append("%s%d%s" % (SA, s["semestre_id"], dem))
|
||||
return ", ".join(p)
|
||||
|
||||
def get_parcours_decisions(self):
|
||||
@ -394,7 +377,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
Returns: { semestre_id : code }
|
||||
"""
|
||||
r = {}
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
indices = [NO_SEMESTRE_ID]
|
||||
else:
|
||||
indices = list(range(1, self.parcours.NB_SEM + 1))
|
||||
@ -437,83 +420,83 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"true si ce semestre pourrait etre compensé par un autre (e.g. barres UE > 8)"
|
||||
return self.barres_ue_ok
|
||||
|
||||
def _search_prev(self) -> FormSemestre | None:
|
||||
def _search_prev(self):
|
||||
"""Recherche semestre 'precedent'.
|
||||
positionne .prev_decision
|
||||
return prev_formsemestre_id
|
||||
"""
|
||||
self.prev_formsemestre = None
|
||||
self.prev = None
|
||||
self.prev_decision = None
|
||||
if len(self.formsemestres) < 2:
|
||||
if len(self.sems) < 2:
|
||||
return None
|
||||
# Cherche sem courant dans la liste triee par date_debut
|
||||
cur = None
|
||||
icur = -1
|
||||
for cur in self.formsemestres:
|
||||
for cur in self.sems:
|
||||
icur += 1
|
||||
if cur.id == self.formsemestre_id:
|
||||
if cur["formsemestre_id"] == self.formsemestre_id:
|
||||
break
|
||||
if not cur or cur.id != self.formsemestre_id:
|
||||
if not cur or cur["formsemestre_id"] != self.formsemestre_id:
|
||||
log(
|
||||
f"""*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={
|
||||
self.formsemestre_id}, etudid={self.etudid})"""
|
||||
f"*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={self.formsemestre_id}, etudid={self.etudid})"
|
||||
)
|
||||
return None # pas de semestre courant !!!
|
||||
# Cherche semestre antérieur de même formation (code) et semestre_id precedent
|
||||
#
|
||||
# i = icur - 1 # part du courant, remonte vers le passé
|
||||
i = len(self.formsemestres) - 1 # par du dernier, remonte vers le passé
|
||||
prev_formsemestre = None
|
||||
i = len(self.sems) - 1 # par du dernier, remonte vers le passé
|
||||
prev = None
|
||||
while i >= 0:
|
||||
if (
|
||||
self.formsemestres[i].formation.formation_code
|
||||
== self.formation.formation_code
|
||||
and self.formsemestres[i].semestre_id == cur.semestre_id - 1
|
||||
self.sems[i]["formation_code"] == self.formation.formation_code
|
||||
and self.sems[i]["semestre_id"] == cur["semestre_id"] - 1
|
||||
):
|
||||
prev_formsemestre = self.formsemestres[i]
|
||||
prev = self.sems[i]
|
||||
break
|
||||
i -= 1
|
||||
if not prev_formsemestre:
|
||||
if not prev:
|
||||
return None # pas de precedent trouvé
|
||||
self.prev_formsemestre = prev_formsemestre
|
||||
self.prev = prev
|
||||
# Verifications basiques:
|
||||
# ?
|
||||
# Code etat du semestre precedent:
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_formsemestre)
|
||||
formsemestre = FormSemestre.query.get_or_404(prev["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
self.prev_decision = nt.get_etud_decision_sem(self.etudid)
|
||||
self.prev_moy_gen = nt.get_etud_moy_gen(self.etudid)
|
||||
self.prev_barres_ue_ok = nt.etud_check_conditions_ues(self.etudid)[0]
|
||||
return self.prev["formsemestre_id"]
|
||||
|
||||
def get_next_semestre_ids(self, devenir: str) -> list[int]:
|
||||
def get_next_semestre_ids(self, devenir):
|
||||
"""Liste des numeros de semestres autorises avec ce devenir
|
||||
Ne vérifie pas que le devenir est possible (doit être fait avant),
|
||||
juste que le rang du semestre est dans le parcours [1..NB_SEM]
|
||||
"""
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
s = self.sem["semestre_id"]
|
||||
if devenir == NEXT:
|
||||
ids = [self._get_next_semestre_id()]
|
||||
elif devenir == REDOANNEE:
|
||||
ids = [s_idx - 1]
|
||||
ids = [s - 1]
|
||||
elif devenir == REDOSEM:
|
||||
ids = [s_idx]
|
||||
ids = [s]
|
||||
elif devenir == RA_OR_NEXT:
|
||||
ids = [s_idx - 1, self._get_next_semestre_id()]
|
||||
ids = [s - 1, self._get_next_semestre_id()]
|
||||
elif devenir == RA_OR_RS:
|
||||
ids = [s_idx - 1, s_idx]
|
||||
ids = [s - 1, s]
|
||||
elif devenir == RS_OR_NEXT:
|
||||
ids = [s_idx, self._get_next_semestre_id()]
|
||||
ids = [s, self._get_next_semestre_id()]
|
||||
elif devenir == NEXT_OR_NEXT2:
|
||||
ids = [
|
||||
self._get_next_semestre_id(),
|
||||
s_idx + 2,
|
||||
s + 2,
|
||||
] # cohérent avec explique_devenir()
|
||||
elif devenir == NEXT2:
|
||||
ids = [s_idx + 2]
|
||||
ids = [s + 2]
|
||||
else:
|
||||
ids = [] # reoriente ou autre: pas de next !
|
||||
# clip [1..NB_SEM]
|
||||
r = []
|
||||
for idx in ids:
|
||||
if 0 < idx <= self.parcours.NB_SEM:
|
||||
if idx > 0 and idx <= self.parcours.NB_SEM:
|
||||
r.append(idx)
|
||||
return r
|
||||
|
||||
@ -521,27 +504,27 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"""Indice du semestre suivant non validé.
|
||||
S'il n'y en a pas, ramène NB_SEM+1
|
||||
"""
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
if s_idx >= self.parcours.NB_SEM:
|
||||
s = self.sem["semestre_id"]
|
||||
if s >= self.parcours.NB_SEM:
|
||||
return self.parcours.NB_SEM + 1
|
||||
validated = True
|
||||
while validated and (s_idx < self.parcours.NB_SEM):
|
||||
s_idx = s_idx + 1
|
||||
while validated and (s < self.parcours.NB_SEM):
|
||||
s = s + 1
|
||||
# semestre s validé ?
|
||||
validated = False
|
||||
for formsemestre in self.formsemestres:
|
||||
for sem in self.sems:
|
||||
if (
|
||||
formsemestre.formation.formation_code
|
||||
== self.formation.formation_code
|
||||
and formsemestre.semestre_id == s_idx
|
||||
sem["formation_code"] == self.formation.formation_code
|
||||
and sem["semestre_id"] == s
|
||||
):
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||
formsemestre
|
||||
)
|
||||
decision = nt.get_etud_decision_sem(self.etudid)
|
||||
if decision and code_semestre_validant(decision["code"]):
|
||||
validated = True
|
||||
return s_idx
|
||||
return s
|
||||
|
||||
def valide_decision(self, decision):
|
||||
"""Enregistre la decision (instance de DecisionSem)
|
||||
@ -556,11 +539,8 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
fsid = decision.formsemestre_id_utilise_pour_compenser
|
||||
if fsid:
|
||||
ok = False
|
||||
for formsemestre in self.formsemestres:
|
||||
if (
|
||||
formsemestre.id == fsid
|
||||
and formsemestre.id in self.can_compensate
|
||||
):
|
||||
for sem in self.sems:
|
||||
if sem["formsemestre_id"] == fsid and sem["can_compensate"]:
|
||||
ok = True
|
||||
break
|
||||
if not ok:
|
||||
@ -585,11 +565,13 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
decision.assiduite,
|
||||
decision.formsemestre_id_utilise_pour_compenser,
|
||||
)
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="validate_sem",
|
||||
etudid=self.etudid,
|
||||
commit=False,
|
||||
msg=f"formsemestre_id={self.formsemestre_id} code={decision.code_etat}",
|
||||
msg="formsemestre_id=%s code=%s"
|
||||
% (self.formsemestre_id, decision.code_etat),
|
||||
)
|
||||
# -- decisions UEs
|
||||
formsemestre_validate_ues(
|
||||
@ -599,7 +581,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
decision.assiduite,
|
||||
)
|
||||
# -- modification du code du semestre precedent
|
||||
if self.prev_formsemestre and decision.new_code_prev:
|
||||
if self.prev and decision.new_code_prev:
|
||||
if decision.new_code_prev == ADC:
|
||||
# ne compense le prec. qu'avec le sem. courant
|
||||
fsid = self.formsemestre_id
|
||||
@ -607,29 +589,30 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
fsid = None
|
||||
to_invalidate += formsemestre_update_validation_sem(
|
||||
cnx,
|
||||
self.prev_formsemestre.id,
|
||||
self.prev["formsemestre_id"],
|
||||
self.etudid,
|
||||
decision.new_code_prev,
|
||||
assidu=True,
|
||||
formsemestre_id_utilise_pour_compenser=fsid,
|
||||
)
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="validate_sem",
|
||||
etudid=self.etudid,
|
||||
commit=False,
|
||||
msg=f"formsemestre_id={self.prev_formsemestre.id} code={decision.new_code_prev}",
|
||||
msg="formsemestre_id=%s code=%s"
|
||||
% (self.prev["formsemestre_id"], decision.new_code_prev),
|
||||
)
|
||||
# modifs des codes d'UE (pourraient passer de ADM a CMP, meme sans modif des notes)
|
||||
formsemestre_validate_ues(
|
||||
self.prev_formsemestre.id,
|
||||
self.prev["formsemestre_id"],
|
||||
self.etudid,
|
||||
decision.new_code_prev,
|
||||
decision.assiduite, # attention: en toute rigueur il faudrait utiliser
|
||||
# une indication de l'assiduite au sem. precedent, que nous n'avons pas...
|
||||
decision.assiduite, # attention: en toute rigueur il faudrait utiliser une indication de l'assiduite au sem. precedent, que nous n'avons pas...
|
||||
)
|
||||
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=self.prev_formsemestre.id
|
||||
formsemestre_id=self.prev["formsemestre_id"]
|
||||
) # > modif decisions jury (sem, UE)
|
||||
|
||||
try:
|
||||
@ -711,7 +694,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
class SituationEtudCursusECTS(SituationEtudCursusClassic):
|
||||
"""Gestion parcours basés sur ECTS"""
|
||||
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, nt):
|
||||
def __init__(self, etud, formsemestre_id, nt):
|
||||
SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt)
|
||||
|
||||
def could_be_compensated(self):
|
||||
@ -755,6 +738,14 @@ class SituationEtudCursusECTS(SituationEtudCursusClassic):
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def int_or_null(s):
|
||||
if s == "":
|
||||
return None
|
||||
else:
|
||||
return int(s)
|
||||
|
||||
|
||||
_scolar_formsemestre_validation_editor = ndb.EditableTable(
|
||||
"scolar_formsemestre_validation",
|
||||
"formsemestre_validation_id",
|
||||
@ -925,13 +916,13 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite)
|
||||
cnx, nt, formsemestre_id, etudid, ue_id, code_ue
|
||||
)
|
||||
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="validate_ue",
|
||||
etudid=etudid,
|
||||
msg=f"ue_id={ue_id} code={code_ue}",
|
||||
msg="ue_id=%s code=%s" % (ue_id, code_ue),
|
||||
commit=False,
|
||||
)
|
||||
db.session.commit()
|
||||
cnx.commit()
|
||||
|
||||
|
||||
|
@ -34,10 +34,11 @@ from flask import url_for, g, request
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Scolog
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import safehtml
|
||||
from app.scodoc import html_sco_header
|
||||
@ -76,6 +77,7 @@ def report_debouche_date(start_year=None, fmt="html"):
|
||||
tab.base_url = f"{request.base_url}?start_year={start_year}"
|
||||
return tab.make_page(
|
||||
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
fmt=fmt,
|
||||
with_html_headers=True,
|
||||
@ -220,7 +222,6 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign table_listegroupe",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="table_debouche_etudids",
|
||||
)
|
||||
return tab
|
||||
|
||||
@ -289,8 +290,8 @@ def itemsuivi_suppress(itemsuivi_id):
|
||||
item = itemsuivi_get(cnx, itemsuivi_id, ignore_errors=True)
|
||||
if item:
|
||||
_itemsuivi_delete(cnx, itemsuivi_id)
|
||||
Scolog.logdb(method="itemsuivi_suppress", etudid=item["etudid"], commit=True)
|
||||
log(f"suppressed itemsuivi {itemsuivi_id}")
|
||||
logdb(cnx, method="itemsuivi_suppress", etudid=item["etudid"])
|
||||
log("suppressed itemsuivi %s" % (itemsuivi_id,))
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@ -302,7 +303,7 @@ def itemsuivi_create(etudid, item_date=None, situation="", fmt=None):
|
||||
itemsuivi_id = _itemsuivi_create(
|
||||
cnx, args={"etudid": etudid, "item_date": item_date, "situation": situation}
|
||||
)
|
||||
Scolog.logdb(method="itemsuivi_create", etudid=etudid, commit=True)
|
||||
logdb(cnx, method="itemsuivi_create", etudid=etudid)
|
||||
log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
|
||||
item = itemsuivi_get(cnx, itemsuivi_id)
|
||||
if fmt == "json":
|
||||
|
@ -137,7 +137,6 @@ def _convert_formsemestres_to_dicts(
|
||||
"bul_hide_xml": formsemestre.bul_hide_xml,
|
||||
"dateord": formsemestre.date_debut,
|
||||
"elt_annee_apo": formsemestre.elt_annee_apo,
|
||||
"elt_passage_apo": formsemestre.elt_passage_apo,
|
||||
"elt_sem_apo": formsemestre.elt_sem_apo,
|
||||
"etapes_apo_str": formsemestre.etapes_apo_str(),
|
||||
"formation": f"{formation.acronyme} v{formation.version}",
|
||||
@ -190,7 +189,6 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
||||
"formation",
|
||||
"etapes_apo_str",
|
||||
"elt_annee_apo",
|
||||
"elt_passage_apo",
|
||||
"elt_sem_apo",
|
||||
]
|
||||
if showcodes:
|
||||
@ -200,27 +198,6 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
||||
if current_user.has_permission(Permission.EditApogee):
|
||||
html_class += " apo_editable"
|
||||
tab = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
html_class_ignore_default=True,
|
||||
html_class=html_class,
|
||||
html_sortable=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_save_url="{
|
||||
url_for('apiweb.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-elt_annee_apo_save_url="{
|
||||
url_for('apiweb.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-elt_sem_apo_save_url="{
|
||||
url_for('apiweb.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-elt_passage_apo_save_url="{
|
||||
url_for('apiweb.formsemestre_set_elt_passage_apo', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
rows=sems,
|
||||
titles={
|
||||
"formsemestre_id": "id",
|
||||
"semestre_id_n": "S#",
|
||||
@ -232,10 +209,21 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
||||
"etapes_apo_str": "Étape Apo.",
|
||||
"elt_annee_apo": "Elt. année Apo.",
|
||||
"elt_sem_apo": "Elt. sem. Apo.",
|
||||
"elt_passage_apo": "Elt. pass. Apo.",
|
||||
"formation": "Formation",
|
||||
},
|
||||
columns_ids=columns_ids,
|
||||
rows=sems,
|
||||
table_id="semlist",
|
||||
html_class_ignore_default=True,
|
||||
html_class=html_class,
|
||||
html_sortable=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
|
||||
return tab
|
||||
@ -294,9 +282,6 @@ def _style_sems(sems: list[dict], fmt="html") -> list[dict]:
|
||||
sem["_elt_sem_apo_td_attrs"] = (
|
||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
|
||||
)
|
||||
sem["_elt_passage_apo_td_attrs"] = (
|
||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_passage_apo']}" """
|
||||
)
|
||||
return sems
|
||||
|
||||
|
||||
|
@ -67,7 +67,7 @@ SCO_DUMP_LOCK = "/tmp/scodump.lock"
|
||||
|
||||
def sco_dump_and_send_db(
|
||||
message: str = "", request_url: str = "", traceback_str_base64: str = ""
|
||||
) -> requests.Response:
|
||||
):
|
||||
"""Dump base de données et l'envoie anonymisée pour debug"""
|
||||
traceback_str = base64.urlsafe_b64decode(traceback_str_base64).decode(
|
||||
scu.SCO_ENCODING
|
||||
@ -97,6 +97,7 @@ def sco_dump_and_send_db(
|
||||
|
||||
# Send
|
||||
r = _send_db(ano_db_name, message, request_url, traceback_str=traceback_str)
|
||||
code = r.status_code
|
||||
|
||||
finally:
|
||||
# Drop anonymized database
|
||||
@ -106,7 +107,7 @@ def sco_dump_and_send_db(
|
||||
|
||||
log("sco_dump_and_send_db: done.")
|
||||
|
||||
return r
|
||||
return code
|
||||
|
||||
|
||||
def _duplicate_db(db_name, ano_db_name):
|
||||
@ -215,11 +216,11 @@ def _drop_ano_db(ano_db_name):
|
||||
log("_drop_ano_db: no temp db, nothing to drop")
|
||||
return
|
||||
cmd = ["dropdb", ano_db_name]
|
||||
log(f"sco_dump_and_send_db: {cmd}")
|
||||
log("sco_dump_and_send_db: {}".format(cmd))
|
||||
try:
|
||||
_ = subprocess.check_output(cmd)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
log(f"sco_dump_and_send_db: exception dropdb {exc}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
log("sco_dump_and_send_db: exception dropdb {}".format(e))
|
||||
raise ScoValueError(
|
||||
f"erreur lors de la suppression de la base {ano_db_name}"
|
||||
) from exc
|
||||
"erreur lors de la suppression de la base {}".format(ano_db_name)
|
||||
)
|
||||
|
@ -47,6 +47,7 @@ from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
||||
|
||||
def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||
@ -186,7 +187,7 @@ def formation_edit(formation_id=None, create=False):
|
||||
"acronyme",
|
||||
{
|
||||
"size": 12,
|
||||
"explanation": "identifiant de la formation (par ex. BUT R&T)",
|
||||
"explanation": "identifiant de la formation (par ex. DUT R&T)",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
@ -194,7 +195,7 @@ def formation_edit(formation_id=None, create=False):
|
||||
"titre",
|
||||
{
|
||||
"size": 80,
|
||||
"explanation": "nom de la formation (ex: BUT Réseaux et Télécommunications)",
|
||||
"explanation": "nom complet de la formation (ex: DUT Réseaux et Télécommunications",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
@ -325,7 +326,6 @@ def do_formation_create(args: dict) -> Formation:
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation.id,
|
||||
),
|
||||
safe=True,
|
||||
) from exc
|
||||
|
||||
ScolarNews.add(
|
||||
|
@ -691,13 +691,9 @@ def module_edit(
|
||||
str(parcour.id) for parcour in ref_comp.parcours
|
||||
]
|
||||
+ ["-1"],
|
||||
"explanation": """Parcours dans lesquels est utilisé ce module (inutile
|
||||
hors BUT, pour les modules standards et dans les UEs de bonus).
|
||||
<br>
|
||||
Attention: si le module ne doit pas avoir les mêmes coefficients suivant
|
||||
le parcours, il faut en créer plusieurs versions, car dans ScoDoc chaque
|
||||
module a ses coefficients.
|
||||
""",
|
||||
"explanation": """Parcours dans lesquels est utilisé ce module.<br>
|
||||
Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours,
|
||||
il faut en créer plusieurs versions, car dans ScoDoc chaque module a ses coefficients.""",
|
||||
},
|
||||
)
|
||||
]
|
||||
@ -894,6 +890,23 @@ def module_edit(
|
||||
)
|
||||
|
||||
|
||||
# Edition en ligne du code Apogee
|
||||
def edit_module_set_code_apogee(id=None, value=None):
|
||||
"Set UE code apogee"
|
||||
module_id = id
|
||||
value = str(value).strip("-_ \t")
|
||||
log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
|
||||
|
||||
modules = module_list(args={"module_id": module_id})
|
||||
if not modules:
|
||||
return "module invalide" # should not occur
|
||||
|
||||
do_module_edit({"module_id": module_id, "code_apogee": value})
|
||||
if not value:
|
||||
value = scu.APO_MISSING_CODE_STR
|
||||
return value
|
||||
|
||||
|
||||
def module_table(formation_id):
|
||||
"""Liste des modules de la formation
|
||||
(XXX inutile ou a revoir)
|
||||
|
@ -84,7 +84,6 @@ _ueEditor = ndb.EditableTable(
|
||||
"ects",
|
||||
"is_external",
|
||||
"code_apogee",
|
||||
"code_apogee_rcue",
|
||||
"coefficient",
|
||||
"coef_rcue",
|
||||
"color",
|
||||
@ -426,20 +425,6 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"max_length": APO_CODE_STR_LEN,
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc:
|
||||
form_descr += [
|
||||
(
|
||||
"code_apogee_rcue",
|
||||
{
|
||||
"title": "Code Apogée du RCUE",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code(s) élément pédagogique Apogée du RCUE",
|
||||
"max_length": APO_CODE_STR_LEN,
|
||||
},
|
||||
),
|
||||
]
|
||||
form_descr += [
|
||||
(
|
||||
"is_external",
|
||||
{
|
||||
@ -528,7 +513,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
{ue_parcours_div}
|
||||
{modules_div}
|
||||
|
||||
<div id="bonus_description" class="scobox"></div>
|
||||
<div id="bonus_description"></div>
|
||||
<div id="ue_list_code" class="sco_box sco_green_bg"></div>
|
||||
|
||||
{html_sco_header.sco_footer()}
|
||||
@ -1056,10 +1041,10 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
if current_user.has_permission(Permission.EditFormSemestre):
|
||||
H.append(
|
||||
f"""<ul>
|
||||
<li><b><a class="stdlink" href="{
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id, semestre_id=1)
|
||||
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a></b>
|
||||
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a>
|
||||
</li>
|
||||
</ul>"""
|
||||
)
|
||||
@ -1124,18 +1109,12 @@ def _ue_table_ues(
|
||||
klass = "span_apo_edit"
|
||||
else:
|
||||
klass = ""
|
||||
edit_url = url_for(
|
||||
"apiweb.ue_set_code_apogee",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
ue_id=ue["ue_id"],
|
||||
ue["code_apogee_str"] = (
|
||||
""", Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">"""
|
||||
% (klass, ue["ue_id"], scu.APO_MISSING_CODE_STR)
|
||||
+ (ue["code_apogee"] or "")
|
||||
+ "</span>"
|
||||
)
|
||||
ue[
|
||||
"code_apogee_str"
|
||||
] = f""", Apo: <span
|
||||
class="{klass}" data-url="{edit_url}" id="{ue['ue_id']}"
|
||||
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
|
||||
ue["code_apogee"] or ""
|
||||
}</span>"""
|
||||
|
||||
if cur_ue_semestre_id != ue["semestre_id"]:
|
||||
cur_ue_semestre_id = ue["semestre_id"]
|
||||
@ -1369,17 +1348,16 @@ def _ue_table_modules(
|
||||
heurescoef = (
|
||||
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
|
||||
)
|
||||
edit_url = url_for(
|
||||
"apiweb.formation_module_set_code_apogee",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
module_id=mod["module_id"],
|
||||
if mod_editable:
|
||||
klass = "span_apo_edit"
|
||||
else:
|
||||
klass = ""
|
||||
heurescoef += (
|
||||
', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
|
||||
% (klass, mod["module_id"], scu.APO_MISSING_CODE_STR)
|
||||
+ (mod["code_apogee"] or "")
|
||||
+ "</span>"
|
||||
)
|
||||
heurescoef += f""", Apo: <span
|
||||
class="{'span_apo_edit' if editable else ''}"
|
||||
data-url="{edit_url}" id="{mod["module_id"]}"
|
||||
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
|
||||
mod["code_apogee"] or ""
|
||||
}</span>"""
|
||||
if tag_editable:
|
||||
tag_cls = "module_tag_editor"
|
||||
else:
|
||||
@ -1515,6 +1493,28 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
||||
formation.invalidate_module_coefs()
|
||||
|
||||
|
||||
# essai edition en ligne:
|
||||
def edit_ue_set_code_apogee(id=None, value=None):
|
||||
"set UE code apogee"
|
||||
ue_id = id
|
||||
value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
|
||||
|
||||
ues = ue_list(args={"ue_id": ue_id})
|
||||
if not ues:
|
||||
return "ue invalide"
|
||||
|
||||
do_ue_edit(
|
||||
{"ue_id": ue_id, "code_apogee": value},
|
||||
bypass_lock=True,
|
||||
dont_invalidate_cache=False,
|
||||
)
|
||||
if not value:
|
||||
value = scu.APO_MISSING_CODE_STR
|
||||
return value
|
||||
|
||||
|
||||
def ue_is_locked(ue_id):
|
||||
"""True if UE should not be modified
|
||||
(contains modules used in a locked formsemestre)
|
||||
|
@ -30,7 +30,6 @@
|
||||
Lecture et conversion des ics.
|
||||
|
||||
"""
|
||||
|
||||
from datetime import timezone
|
||||
import glob
|
||||
import os
|
||||
@ -230,7 +229,7 @@ def translate_calendar(
|
||||
heure_deb=event["heure_deb"],
|
||||
heure_fin=event["heure_fin"],
|
||||
moduleimpl_id=modimpl.id,
|
||||
day=event["jour"],
|
||||
jour=event["jour"],
|
||||
)
|
||||
if modimpl and group
|
||||
else None
|
||||
|
@ -247,7 +247,9 @@ def apo_csv_check_etape(semset, set_nips, etape_apo):
|
||||
return nips_ok, apo_nips, nips_no_apo, nips_no_sco, maq_elems, sem_elems
|
||||
|
||||
|
||||
def apo_csv_semset_check(semset, allow_missing_apo=False, allow_missing_csv=False):
|
||||
def apo_csv_semset_check(
|
||||
semset, allow_missing_apo=False, allow_missing_csv=False
|
||||
): # was apo_csv_check
|
||||
"""
|
||||
check students in stored maqs vs students in semset
|
||||
Cas à détecter:
|
||||
@ -344,3 +346,120 @@ def apo_csv_retreive_etuds_by_nip(semset, nips):
|
||||
etuds[nip] = apo_etuds_by_nips.get(nip, {"nip": nip, "etape_apo": "?"})
|
||||
|
||||
return etuds
|
||||
|
||||
|
||||
"""
|
||||
Tests:
|
||||
|
||||
from debug import *
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_etape_apogee import *
|
||||
from app.scodoc.sco_apogee_csv import *
|
||||
from app.scodoc.sco_semset import *
|
||||
|
||||
app.set_sco_dept('RT')
|
||||
csv_data = open('/opt/misc/VDTRT_V1RT.TXT').read()
|
||||
annee_scolaire=2015
|
||||
sem_id=1
|
||||
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=sem_id)
|
||||
print apo_data.etape_apogee
|
||||
|
||||
apo_data.setup()
|
||||
e = apo_data.etuds[0]
|
||||
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
|
||||
e.associate_sco( apo_data)
|
||||
|
||||
print apo_csv_list_stored_archives()
|
||||
|
||||
|
||||
# apo_csv_store(csv_data, annee_scolaire, sem_id)
|
||||
|
||||
|
||||
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos( [sco_groups.get_default_group(formsemestre_id)], formsemestre_id=formsemestre_id)
|
||||
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
#
|
||||
s = SemSet('NSS29902')
|
||||
apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1)
|
||||
|
||||
# cas Tiziri K. (inscrite en S1, démission en fin de S1, pas inscrite en S2)
|
||||
# => pas de décision, ce qui est voulu (?)
|
||||
#
|
||||
|
||||
apo_data.setup()
|
||||
e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0]
|
||||
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
|
||||
e.associate_sco(apo_data)
|
||||
|
||||
self=e
|
||||
col_id='apoL_c0129'
|
||||
|
||||
# --
|
||||
from app.scodoc import sco_portal_apogee
|
||||
_ = go_dept(app, 'GEA').Notes
|
||||
#csv_data = sco_portal_apogee.get_maquette_apogee(etape='V1GE', annee_scolaire=2015)
|
||||
csv_data = open('/tmp/V1GE.txt').read()
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
|
||||
|
||||
|
||||
# ------
|
||||
# les elements inconnus:
|
||||
|
||||
from debug import *
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_etape_apogee import *
|
||||
from app.scodoc.sco_apogee_csv import *
|
||||
from app.scodoc.sco_semset import *
|
||||
|
||||
_ = go_dept(app, 'RT').Notes
|
||||
csv_data = open('/opt/misc/V2RT.csv').read()
|
||||
annee_scolaire=2015
|
||||
sem_id=1
|
||||
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
|
||||
print apo_data.etape_apogee
|
||||
|
||||
apo_data.setup()
|
||||
for e in apo_data.etuds:
|
||||
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
|
||||
e.associate_sco(apo_data)
|
||||
|
||||
# ------
|
||||
# test export jury intermediaire
|
||||
from debug import *
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_etape_apogee import *
|
||||
from app.scodoc.sco_apogee_csv import *
|
||||
from app.scodoc.sco_semset import *
|
||||
|
||||
_ = go_dept(app, 'CJ').Notes
|
||||
csv_data = open('/opt/scodoc/var/scodoc/archives/apo_csv/CJ/2016-1/2017-03-06-21-46-32/V1CJ.csv').read()
|
||||
annee_scolaire=2016
|
||||
sem_id=1
|
||||
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
|
||||
print apo_data.etape_apogee
|
||||
|
||||
apo_data.setup()
|
||||
e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0] #
|
||||
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
|
||||
e.associate_sco(apo_data)
|
||||
|
||||
self=e
|
||||
|
||||
sco_elts = {}
|
||||
col_id='apoL_c0001'
|
||||
code = apo_data.cols[col_id]['Code'] # 'V1RT'
|
||||
|
||||
sem = apo_data.sems_periode[0] # le S1
|
||||
|
||||
"""
|
||||
|
@ -125,19 +125,14 @@ def apo_semset_maq_status(
|
||||
H.append("""<p><em>Aucune maquette chargée</em></p>""")
|
||||
# Upload fichier:
|
||||
H.append(
|
||||
f"""<form id="apo_csv_add" action="view_apo_csv_store"
|
||||
method="post" enctype="multipart/form-data"
|
||||
style="margin-bottom: 8px;"
|
||||
>
|
||||
<div style="margin-top: 12px; margin-bottom: 8px;">
|
||||
{'Charger votre fichier' if tab_archives.is_empty() else 'Ajouter un autre fichier'}
|
||||
maquette Apogée:
|
||||
</div>
|
||||
"""<form id="apo_csv_add" action="view_apo_csv_store" method="post" enctype="multipart/form-data">
|
||||
Charger votre fichier maquette Apogée:
|
||||
<input type="file" size="30" name="csvfile"/>
|
||||
<input type="hidden" name="semset_id" value="{semset_id}"/>
|
||||
<input type="hidden" name="semset_id" value="%s"/>
|
||||
<input type="submit" value="Ajouter ce fichier"/>
|
||||
<input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
|
||||
</form>"""
|
||||
% (semset_id,)
|
||||
)
|
||||
# Récupération sur portail:
|
||||
maquette_url = sco_portal_apogee.get_maquette_url()
|
||||
@ -340,7 +335,7 @@ def apo_semset_maq_status(
|
||||
missing = maq_elems - sem_elems
|
||||
H.append('<div id="apo_elements">')
|
||||
H.append(
|
||||
'<p>Élements Apogée: <span class="apo_elems">%s</span></p>'
|
||||
'<p>Elements Apogée: <span class="apo_elems">%s</span></p>'
|
||||
% ", ".join(
|
||||
[
|
||||
e if not e in missing else '<span class="missing">' + e + "</span>"
|
||||
@ -356,7 +351,7 @@ def apo_semset_maq_status(
|
||||
]
|
||||
H.append(
|
||||
f"""<div class="apo_csv_status_missing_elems">
|
||||
<span class="fontred">Élements Apogée absents dans ScoDoc: </span>
|
||||
<span class="fontred">Elements Apogée absents dans ScoDoc: </span>
|
||||
<span class="apo_elems fontred">{
|
||||
", ".join(sorted(missing))
|
||||
}</span>
|
||||
@ -447,11 +442,11 @@ def table_apo_csv_list(semset):
|
||||
annee_scolaire = semset["annee_scolaire"]
|
||||
sem_id = semset["sem_id"]
|
||||
|
||||
rows = sco_etape_apogee.apo_csv_list_stored_archives(
|
||||
T = sco_etape_apogee.apo_csv_list_stored_archives(
|
||||
annee_scolaire, sem_id, etapes=semset.list_etapes()
|
||||
)
|
||||
|
||||
for t in rows:
|
||||
for t in T:
|
||||
# Ajoute qq infos pour affichage:
|
||||
csv_data = sco_etape_apogee.apo_csv_get(t["etape_apo"], annee_scolaire, sem_id)
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
|
||||
@ -489,13 +484,12 @@ def table_apo_csv_list(semset):
|
||||
"date_str": "Enregistré le",
|
||||
},
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
rows=T,
|
||||
html_class="table_leftalign apo_maq_list",
|
||||
html_sortable=True,
|
||||
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
||||
# caption='Maquettes enregistrées',
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="apo_csv_list",
|
||||
)
|
||||
|
||||
return tab
|
||||
@ -545,8 +539,7 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", fmt="html"):
|
||||
if not isinstance(nip_list, str):
|
||||
nip_list = str(nip_list)
|
||||
nips = nip_list.split(",")
|
||||
etuds_lst = [sco_etud.get_etud_info(code_nip=nip, filled=True) for nip in nips]
|
||||
etuds = [lst[0] for lst in etuds_lst if lst]
|
||||
etuds = [sco_etud.get_etud_info(code_nip=nip, filled=True)[0] for nip in nips]
|
||||
|
||||
for e in etuds:
|
||||
tgt = url_for(
|
||||
@ -589,7 +582,6 @@ def _view_etuds_page(
|
||||
html_class="table_leftalign",
|
||||
filename="students_apo",
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="view_etuds_page",
|
||||
)
|
||||
if fmt != "html":
|
||||
return tab.make_page(fmt=fmt)
|
||||
@ -779,18 +771,12 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
|
||||
e["in_scodoc"] = e["nip"] not in nips_no_sco
|
||||
e["in_scodoc_str"] = {True: "oui", False: "non"}[e["in_scodoc"]]
|
||||
if e["in_scodoc"]:
|
||||
etud = sco_etud.get_etud_info(code_nip=e["nip"], filled=True)
|
||||
if etud:
|
||||
e.update(etud[0])
|
||||
e["_in_scodoc_str_target"] = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
|
||||
)
|
||||
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
|
||||
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
|
||||
else:
|
||||
# race condition?
|
||||
e["in_scodoc"] = False
|
||||
e["_css_row_class"] = "apo_not_scodoc"
|
||||
e.update(sco_etud.get_etud_info(code_nip=e["nip"], filled=True)[0])
|
||||
e["_in_scodoc_str_target"] = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
|
||||
)
|
||||
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
|
||||
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
|
||||
else:
|
||||
e["_css_row_class"] = "apo_not_scodoc"
|
||||
|
||||
@ -812,7 +798,6 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
|
||||
filename="students_" + etape_apo,
|
||||
caption="Étudiants Apogée en " + etape_apo,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
table_id="view_apo_csv",
|
||||
)
|
||||
|
||||
if fmt != "html":
|
||||
|
@ -93,7 +93,7 @@ import json
|
||||
|
||||
from flask import url_for, g
|
||||
|
||||
from app.scodoc import sco_portal_apogee
|
||||
from app.scodoc.sco_portal_apogee import get_inscrits_etape
|
||||
from app import log
|
||||
from app.scodoc.sco_utils import annee_scolaire_debut
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -136,16 +136,11 @@ class DataEtudiant(object):
|
||||
self.etudid = etudid
|
||||
self.data_apogee = None
|
||||
self.data_scodoc = None
|
||||
self.etapes = set()
|
||||
"l'ensemble des étapes où il est inscrit"
|
||||
self.semestres = set()
|
||||
"l'ensemble des formsemestre_id où il est inscrit"
|
||||
self.tags = set()
|
||||
"les anomalies relevées"
|
||||
self.ind_row = "-"
|
||||
"ligne où il compte dans les effectifs"
|
||||
self.etapes = set() # l'ensemble des étapes où il est inscrit
|
||||
self.semestres = set() # l'ensemble des formsemestre_id où il est inscrit
|
||||
self.tags = set() # les anomalies relevées
|
||||
self.ind_row = "-" # là où il compte dans les effectifs (ligne et colonne)
|
||||
self.ind_col = "-"
|
||||
"colonne où il compte dans les effectifs"
|
||||
|
||||
def add_etape(self, etape):
|
||||
self.etapes.add(etape)
|
||||
@ -168,9 +163,9 @@ class DataEtudiant(object):
|
||||
def set_ind_col(self, indicatif):
|
||||
self.ind_col = indicatif
|
||||
|
||||
def get_identity(self) -> str:
|
||||
def get_identity(self):
|
||||
"""
|
||||
Calcule le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
|
||||
Calcul le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
|
||||
:return: L'identité calculée
|
||||
"""
|
||||
if self.data_scodoc is not None:
|
||||
@ -181,12 +176,9 @@ class DataEtudiant(object):
|
||||
|
||||
def _help() -> str:
|
||||
return """
|
||||
<div id="export_help" class="pas_help">
|
||||
<span>Explications sur les tableaux des effectifs
|
||||
et liste des étudiants</span>
|
||||
<div>
|
||||
<p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:
|
||||
</p>
|
||||
<div id="export_help" class="pas_help"> <span>Explications sur les tableaux des effectifs et liste des
|
||||
étudiants</span>
|
||||
<div> <p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:</p>
|
||||
<ul>
|
||||
<li>En colonne le statut de l'étudiant par rapport à Apogée:
|
||||
<ul>
|
||||
@ -414,8 +406,7 @@ class EtapeBilan:
|
||||
for key_etape in self.etapes:
|
||||
annee_apogee, etapestr = key_to_values(key_etape)
|
||||
self.etu_etapes[key_etape] = set()
|
||||
# get_inscrits_etape interroge portail Apo:
|
||||
for etud in sco_portal_apogee.get_inscrits_etape(etapestr, annee_apogee):
|
||||
for etud in get_inscrits_etape(etapestr, annee_apogee):
|
||||
key_etu = self.register_etud_apogee(etud, key_etape)
|
||||
self.etu_etapes[key_etape].add(key_etu)
|
||||
|
||||
@ -453,6 +444,7 @@ class EtapeBilan:
|
||||
data_etu = self.etudiants[key_etu]
|
||||
ind_col = "-"
|
||||
ind_row = "-"
|
||||
|
||||
# calcul de la colonne
|
||||
if len(data_etu.etapes) == 1:
|
||||
ind_col = self.indicatifs[list(data_etu.etapes)[0]]
|
||||
@ -486,34 +478,32 @@ class EtapeBilan:
|
||||
affichage de l'html
|
||||
:return: Le code html à afficher
|
||||
"""
|
||||
if not sco_portal_apogee.has_portal():
|
||||
return """<div id="synthese" class="semset_description">
|
||||
<em>Pas de portail Apogée configuré</em>
|
||||
</div>"""
|
||||
self.load_listes() # chargement des données
|
||||
self.dispatch() # analyse et répartition
|
||||
# calcul de la liste des colonnes et des lignes de la table des effectifs
|
||||
self.all_rows_str = "'" + ",".join(["." + r for r in self.all_rows_ind]) + "'"
|
||||
self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
|
||||
|
||||
return f"""
|
||||
<div id="synthese" class="semset_description">
|
||||
H = [
|
||||
"""<div id="synthese" class="semset_description">
|
||||
<details open="true">
|
||||
<summary><b>Tableau des effectifs</b>
|
||||
</summary>
|
||||
{self._diagtable()}
|
||||
</details>
|
||||
{self.display_tags()}
|
||||
<details open="true">
|
||||
<summary>
|
||||
<b id="effectifs">Liste des étudiants <span id="compte"></span></b>
|
||||
</summary>
|
||||
{entete_liste_etudiant()}
|
||||
{self.table_effectifs()}
|
||||
</details>
|
||||
{_help()}
|
||||
</div>
|
||||
"""
|
||||
<summary><b>Tableau des effectifs</b>
|
||||
</summary>
|
||||
""",
|
||||
self._diagtable(),
|
||||
"""</details>""",
|
||||
self.display_tags(),
|
||||
"""<details open="true">
|
||||
<summary><b id="effectifs">Liste des étudiants <span id="compte"></span></b>
|
||||
</summary>
|
||||
""",
|
||||
entete_liste_etudiant(),
|
||||
self.table_effectifs(),
|
||||
"""</details>""",
|
||||
_help(),
|
||||
]
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
def _inc_count(self, ind_row, ind_col):
|
||||
if (ind_row, ind_col) not in self.repartition:
|
||||
@ -676,9 +666,7 @@ class EtapeBilan:
|
||||
col_ids,
|
||||
self.titres,
|
||||
html_class="repartition",
|
||||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="apo-repartition",
|
||||
).gen(fmt="html")
|
||||
)
|
||||
return "\n".join(H)
|
||||
@ -702,34 +690,26 @@ class EtapeBilan:
|
||||
return "\n".join(H)
|
||||
|
||||
@staticmethod
|
||||
def link_etu(etudid, nom) -> str:
|
||||
"Lien html vers fiche de l'étudiant"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">{nom}</a>"""
|
||||
def link_etu(etudid, nom):
|
||||
return '<a class="stdlink" href="%s">%s</a>' % (
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
nom,
|
||||
)
|
||||
|
||||
def link_semestre(self, semestre, short=False) -> str:
|
||||
"Lien html vers tableau de bord semestre"
|
||||
key = "session_id" if short else "titremois"
|
||||
sem = self.semestres[semestre]
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem['formsemestre_id']
|
||||
)}">{sem[key]}</a>
|
||||
"""
|
||||
def link_semestre(self, semestre, short=False):
|
||||
if short:
|
||||
return (
|
||||
'<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%('
|
||||
"formsemestre_id)s</a> " % self.semestres[semestre]
|
||||
)
|
||||
else:
|
||||
return (
|
||||
'<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s'
|
||||
" %(mois_debut)s - %(mois_fin)s)</a>" % self.semestres[semestre]
|
||||
)
|
||||
|
||||
def table_effectifs(self) -> str:
|
||||
"Table html donnant les étudiants dans chaque semestre"
|
||||
H = [
|
||||
"""
|
||||
<style>
|
||||
table#apo-detail td.semestre {
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
]
|
||||
def table_effectifs(self):
|
||||
H = []
|
||||
|
||||
col_ids = ["tag", "etudiant", "prenom", "nip", "semestre", "apogee", "annee"]
|
||||
titles = {
|
||||
@ -782,10 +762,9 @@ class EtapeBilan:
|
||||
rows,
|
||||
col_ids,
|
||||
titles,
|
||||
table_id="detail",
|
||||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="apo-detail",
|
||||
).gen(fmt="html")
|
||||
)
|
||||
return "\n".join(H)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user